mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +00:00
parent
08ed810b9e
commit
2f2fd67860
@ -35,7 +35,6 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier {
|
||||
let api = InvidiousAPI()
|
||||
|
||||
api.validInstance = true
|
||||
api.signedIn = true
|
||||
|
||||
return api
|
||||
}
|
||||
|
@ -5,13 +5,12 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
|
||||
static var bridge = AccountsBridge()
|
||||
|
||||
let id: String
|
||||
let app: VideosApp
|
||||
var app: VideosApp?
|
||||
let instanceID: String?
|
||||
var name: String?
|
||||
let url: String
|
||||
let username: String
|
||||
let password: String?
|
||||
var token: String?
|
||||
var username: String
|
||||
var password: String?
|
||||
let anonymous: Bool
|
||||
let country: String?
|
||||
let region: String?
|
||||
@ -24,7 +23,6 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
|
||||
url: String? = nil,
|
||||
username: String? = nil,
|
||||
password: String? = nil,
|
||||
token: String? = nil,
|
||||
anonymous: Bool = false,
|
||||
country: String? = nil,
|
||||
region: String? = nil
|
||||
@ -32,19 +30,26 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
|
||||
self.anonymous = anonymous
|
||||
|
||||
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
|
||||
self.app = app ?? instance.app
|
||||
}
|
||||
|
||||
var token: String? {
|
||||
KeychainModel.shared.getAccountKey(self, "token")
|
||||
}
|
||||
|
||||
var credentials: (String?, String?) {
|
||||
AccountsModel.getCredentials(self)
|
||||
}
|
||||
|
||||
var instance: Instance! {
|
||||
Defaults[.instances].first { $0.id == instanceID } ?? Instance(app: app, name: url, apiURL: url)
|
||||
Defaults[.instances].first { $0.id == instanceID } ?? Instance(app: app ?? .invidious, name: url, apiURL: url)
|
||||
}
|
||||
|
||||
var isPublic: Bool {
|
||||
@ -52,8 +57,12 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
|
||||
}
|
||||
|
||||
var shortUsername: String {
|
||||
guard username.count > 10 else {
|
||||
return username
|
||||
let (username, _) = credentials
|
||||
|
||||
guard let username = username,
|
||||
username.count > 10
|
||||
else {
|
||||
return username ?? ""
|
||||
}
|
||||
|
||||
let index = username.index(username.startIndex, offsetBy: 11)
|
||||
@ -61,7 +70,11 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
|
||||
}
|
||||
|
||||
var description: String {
|
||||
(name != nil && name!.isEmpty) ? shortUsername : name!
|
||||
guard let name = name, !name.isEmpty else {
|
||||
return shortUsername
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
|
@ -43,14 +43,6 @@ final class AccountValidator: Service {
|
||||
$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
|
||||
}
|
||||
|
||||
configure("/api/v1/auth/feed", requestMethods: [.get]) {
|
||||
guard self.account != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
$0.headers["Cookie"] = self.invidiousCookieHeader
|
||||
}
|
||||
|
||||
configure("/login", requestMethods: [.post]) {
|
||||
$0.headers["Content-Type"] = "application/json"
|
||||
}
|
||||
@ -167,7 +159,8 @@ final class AccountValidator: Service {
|
||||
var accountRequest: Request? {
|
||||
switch app.wrappedValue {
|
||||
case .invidious:
|
||||
return feed.load()
|
||||
guard let password = account.password else { return nil }
|
||||
return login.request(.post, urlEncoded: ["email": account.username, "password": password])
|
||||
case .piped:
|
||||
return login.request(.post, json: ["username": account.username, "password": account.password])
|
||||
default:
|
||||
@ -184,18 +177,10 @@ final class AccountValidator: Service {
|
||||
error?.wrappedValue = nil
|
||||
}
|
||||
|
||||
var invidiousCookieHeader: String {
|
||||
"SID=\(account.username)"
|
||||
}
|
||||
|
||||
var login: Resource {
|
||||
resource("/login")
|
||||
}
|
||||
|
||||
var feed: Resource {
|
||||
resource("/api/v1/auth/feed")
|
||||
}
|
||||
|
||||
var videoResourceBasePath: String {
|
||||
app.wrappedValue == .invidious ? "/api/v1/videos" : "/streams"
|
||||
}
|
||||
|
@ -93,22 +93,47 @@ final class AccountsModel: ObservableObject {
|
||||
Defaults[.accounts].first { $0.id == id }
|
||||
}
|
||||
|
||||
static func add(instance: Instance, name: String, username: String, password: String? = nil) -> Account {
|
||||
let account = Account(
|
||||
instanceID: instance.id,
|
||||
name: name,
|
||||
url: instance.apiURL,
|
||||
username: username,
|
||||
password: password
|
||||
)
|
||||
static func add(instance: Instance, name: String, username: String, password: String) -> Account {
|
||||
let account = Account(instanceID: instance.id, name: name, url: instance.apiURL)
|
||||
Defaults[.accounts].append(account)
|
||||
|
||||
setCredentials(account, username: username, password: password)
|
||||
|
||||
return account
|
||||
}
|
||||
|
||||
static func remove(_ account: Account) {
|
||||
if let accountIndex = Defaults[.accounts].firstIndex(where: { $0.id == account.id }) {
|
||||
let account = Defaults[.accounts][accountIndex]
|
||||
KeychainModel.shared.removeAccountKeys(account)
|
||||
Defaults[.accounts].remove(at: accountIndex)
|
||||
}
|
||||
}
|
||||
|
||||
static func setToken(_ account: Account, _ token: String) {
|
||||
KeychainModel.shared.updateAccountKey(account, "token", token)
|
||||
}
|
||||
|
||||
static func setCredentials(_ account: Account, username: String, password: String) {
|
||||
KeychainModel.shared.updateAccountKey(account, "username", username)
|
||||
KeychainModel.shared.updateAccountKey(account, "password", password)
|
||||
}
|
||||
|
||||
static func getCredentials(_ account: Account) -> (String?, String?) {
|
||||
(
|
||||
KeychainModel.shared.getAccountKey(account, "username"),
|
||||
KeychainModel.shared.getAccountKey(account, "password")
|
||||
)
|
||||
}
|
||||
|
||||
static func removeDefaultsCredentials(_ account: Account) {
|
||||
if let accountIndex = Defaults[.accounts].firstIndex(where: { $0.id == account.id }) {
|
||||
var account = Defaults[.accounts][accountIndex]
|
||||
account.name = ""
|
||||
account.username = ""
|
||||
account.password = nil
|
||||
|
||||
Defaults[.accounts][accountIndex] = account
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import Alamofire
|
||||
import AVKit
|
||||
import Defaults
|
||||
import Foundation
|
||||
@ -10,7 +11,12 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
@Published var account: Account!
|
||||
|
||||
@Published var validInstance = true
|
||||
@Published var signedIn = false
|
||||
|
||||
var signedIn: Bool {
|
||||
guard let account = account else { return false }
|
||||
|
||||
return !account.anonymous && !(account.token?.isEmpty ?? true)
|
||||
}
|
||||
|
||||
init(account: Account? = nil) {
|
||||
super.init()
|
||||
@ -25,7 +31,6 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
|
||||
func setAccount(_ account: Account) {
|
||||
self.account = account
|
||||
signedIn = false
|
||||
|
||||
validInstance = account.anonymous
|
||||
|
||||
@ -57,28 +62,23 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
}
|
||||
|
||||
func validateSID() {
|
||||
guard !signedIn else {
|
||||
guard signedIn, !(account.token?.isEmpty ?? true) else {
|
||||
return
|
||||
}
|
||||
|
||||
feed?
|
||||
.load()
|
||||
.onSuccess { _ in
|
||||
self.signedIn = true
|
||||
}
|
||||
.onFailure { requestError in
|
||||
self.signedIn = false
|
||||
NavigationModel.shared.presentAlert(
|
||||
title: "Could not connect with your account",
|
||||
message: "\(requestError.httpStatusCode ?? -1) - \(requestError.userMessage)\nIf this issue persists, try removing and adding your account again in Settings."
|
||||
)
|
||||
.onFailure { _ in
|
||||
self.updateToken(force: true)
|
||||
}
|
||||
}
|
||||
|
||||
func configure() {
|
||||
invalidateConfiguration()
|
||||
|
||||
configure {
|
||||
if !self.account.username.isEmpty {
|
||||
$0.headers["Cookie"] = self.cookieHeader
|
||||
if let cookie = self.cookieHeader {
|
||||
$0.headers["Cookie"] = cookie
|
||||
}
|
||||
$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"])
|
||||
}
|
||||
@ -170,6 +170,71 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
|
||||
return CommentsPage(comments: comments, nextPage: nextPage, disabled: disabled)
|
||||
}
|
||||
|
||||
updateToken()
|
||||
}
|
||||
|
||||
func updateToken(force: Bool = false) {
|
||||
let (username, password) = AccountsModel.getCredentials(account)
|
||||
guard !account.anonymous,
|
||||
(account.token?.isEmpty ?? true) || force
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let username = username,
|
||||
let password = password,
|
||||
!username.isEmpty,
|
||||
!password.isEmpty
|
||||
else {
|
||||
NavigationModel.shared.presentAlert(
|
||||
title: "Account Error",
|
||||
message: "Remove and add your account again in Settings."
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let presentTokenUpdateFailedAlert: (AFDataResponse<Data?>?, String?) -> Void = { response, message in
|
||||
NavigationModel.shared.presentAlert(
|
||||
title: "Account Error",
|
||||
message: message ?? "\(response?.response?.statusCode ?? -1) - \(response?.error?.errorDescription ?? "unknown")\nIf this issue persists, try removing and adding your account again in Settings."
|
||||
)
|
||||
}
|
||||
|
||||
AF
|
||||
.request(login.url, method: .post, parameters: ["email": username, "password": password], encoding: URLEncoding.default)
|
||||
.redirect(using: .doNotFollow)
|
||||
.response { response in
|
||||
guard let headers = response.response?.headers,
|
||||
let cookies = headers["Set-Cookie"]
|
||||
else {
|
||||
presentTokenUpdateFailedAlert(response, nil)
|
||||
return
|
||||
}
|
||||
|
||||
let sidRegex = #"SID=(?<sid>[^;]*);"#
|
||||
guard let sidRegex = try? NSRegularExpression(pattern: sidRegex),
|
||||
let match = sidRegex.matches(in: cookies, range: NSRange(cookies.startIndex..., in: cookies)).first
|
||||
else {
|
||||
presentTokenUpdateFailedAlert(nil, "Could not extract SID from received cookies: \(cookies)")
|
||||
return
|
||||
}
|
||||
|
||||
let matchRange = match.range(withName: "sid")
|
||||
|
||||
if let substringRange = Range(matchRange, in: cookies) {
|
||||
print("updating invidious token")
|
||||
let sid = String(cookies[substringRange])
|
||||
AccountsModel.setToken(self.account, sid)
|
||||
self.configure()
|
||||
} else {
|
||||
presentTokenUpdateFailedAlert(nil, "Could not extract SID from received cookies: \(cookies)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var login: Resource {
|
||||
resource(baseURL: account.url, path: "login")
|
||||
}
|
||||
|
||||
private func pathPattern(_ path: String) -> String {
|
||||
@ -180,8 +245,9 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
"\(Self.basePath)/\(path)"
|
||||
}
|
||||
|
||||
private var cookieHeader: String {
|
||||
"SID=\(account.username)"
|
||||
private var cookieHeader: String? {
|
||||
guard let token = account?.token, !token.isEmpty else { return nil }
|
||||
return "SID=\(token)"
|
||||
}
|
||||
|
||||
var popular: Resource? {
|
||||
|
@ -109,23 +109,33 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
}
|
||||
|
||||
func updateToken() {
|
||||
guard !account.anonymous else {
|
||||
let (username, password) = AccountsModel.getCredentials(account)
|
||||
|
||||
guard !account.anonymous,
|
||||
let username = username,
|
||||
let password = password
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
account.token = nil
|
||||
|
||||
login.request(
|
||||
.post,
|
||||
json: ["username": account.username, "password": account.password]
|
||||
json: ["username": username, "password": password]
|
||||
)
|
||||
.onSuccess { response in
|
||||
self.account.token = response.json.dictionaryValue["token"]?.string ?? ""
|
||||
let token = response.json.dictionaryValue["token"]?.string ?? ""
|
||||
if let error = response.json.dictionaryValue["error"]?.string {
|
||||
NavigationModel.shared.presentAlert(
|
||||
title: "Could not connect with your account",
|
||||
title: "Account Error",
|
||||
message: error
|
||||
)
|
||||
} else if !token.isEmpty {
|
||||
AccountsModel.setToken(self.account, token)
|
||||
} else {
|
||||
NavigationModel.shared.presentAlert(
|
||||
title: "Account Error",
|
||||
message: "Could not update your token."
|
||||
)
|
||||
}
|
||||
|
||||
self.configure()
|
||||
|
@ -11,10 +11,6 @@ enum VideosApp: String, CaseIterable {
|
||||
true
|
||||
}
|
||||
|
||||
var accountsUsePassword: Bool {
|
||||
self == .piped
|
||||
}
|
||||
|
||||
var supportsPopular: Bool {
|
||||
self == .invidious
|
||||
}
|
||||
|
26
Model/KeychainModel.swift
Normal file
26
Model/KeychainModel.swift
Normal file
@ -0,0 +1,26 @@
|
||||
import Foundation
|
||||
import KeychainAccess
|
||||
|
||||
struct KeychainModel {
|
||||
static var shared = KeychainModel()
|
||||
|
||||
var keychain = Keychain(service: "stream.yattee.app")
|
||||
|
||||
func updateAccountKey(_ account: Account, _ key: String, _ value: String) {
|
||||
keychain[accountKey(account, key)] = value
|
||||
}
|
||||
|
||||
func getAccountKey(_ account: Account, _ key: String) -> String? {
|
||||
keychain[accountKey(account, key)]
|
||||
}
|
||||
|
||||
func accountKey(_ account: Account, _ key: String) -> String {
|
||||
"\(account.id)-\(key)"
|
||||
}
|
||||
|
||||
func removeAccountKeys(_ account: Account) {
|
||||
try? keychain.remove(accountKey(account, "token"))
|
||||
try? keychain.remove(accountKey(account, "username"))
|
||||
try? keychain.remove(accountKey(account, "password"))
|
||||
}
|
||||
}
|
@ -33,6 +33,7 @@ extension Defaults.Keys {
|
||||
#endif
|
||||
static let accountPickerDisplaysUsername = Key<Bool>("accountPickerDisplaysUsername", default: accountPickerDisplaysUsernameDefault)
|
||||
#endif
|
||||
static let accountPickerDisplaysAnonymousAccounts = Key<Bool>("accountPickerDisplaysAnonymousAccounts", default: true)
|
||||
#if os(iOS)
|
||||
static let lockPortraitWhenBrowsing = Key<Bool>("lockPortraitWhenBrowsing", default: UIDevice.current.userInterfaceIdiom == .phone)
|
||||
#endif
|
||||
|
@ -7,6 +7,7 @@ struct AccountsMenuView: View {
|
||||
@Default(.accounts) private var accounts
|
||||
@Default(.instances) private var instances
|
||||
@Default(.accountPickerDisplaysUsername) private var accountPickerDisplaysUsername
|
||||
@Default(.accountPickerDisplaysAnonymousAccounts) private var accountPickerDisplaysAnonymousAccounts
|
||||
|
||||
@ViewBuilder var body: some View {
|
||||
if !instances.isEmpty {
|
||||
@ -48,7 +49,8 @@ struct AccountsMenuView: View {
|
||||
}
|
||||
|
||||
private var allAccounts: [Account] {
|
||||
accounts + instances.map(\.anonymousAccount) + [model.publicAccount].compactMap { $0 }
|
||||
let anonymousAccounts = accountPickerDisplaysAnonymousAccounts ? instances.map(\.anonymousAccount) : []
|
||||
return accounts + anonymousAccounts + [model.publicAccount].compactMap { $0 }
|
||||
}
|
||||
|
||||
private func accountButtonTitle(account: Account) -> String {
|
||||
|
@ -46,11 +46,14 @@ struct ContentView: View {
|
||||
.environmentObject(settings)
|
||||
#endif
|
||||
}
|
||||
.onChange(of: accounts.current) { _ in
|
||||
subscriptions.load(force: true)
|
||||
playlists.load(force: true)
|
||||
}
|
||||
.onChange(of: accounts.signedIn) { _ in
|
||||
subscriptions.load(force: true)
|
||||
playlists.load(force: true)
|
||||
}
|
||||
|
||||
.environmentObject(accounts)
|
||||
.environmentObject(comments)
|
||||
.environmentObject(instances)
|
||||
|
@ -63,10 +63,6 @@ struct AccountForm: View {
|
||||
#if os(macOS)
|
||||
.padding(.horizontal)
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
helpButton
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
formFields
|
||||
@ -76,35 +72,12 @@ struct AccountForm: View {
|
||||
.onChange(of: password) { _ in validate() }
|
||||
}
|
||||
|
||||
var helpButton: some View {
|
||||
Group {
|
||||
if instance.app == .invidious {
|
||||
Button {
|
||||
openURL(URL(string: "https://github.com/yattee/yattee/wiki/Adding-Invidious-instance-and-account")!)
|
||||
} label: {
|
||||
Label("How to add Invidious account?", systemImage: "questionmark.circle")
|
||||
#if os(macOS)
|
||||
.help("How to add Invidious account?")
|
||||
.labelStyle(.iconOnly)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var formFields: some View {
|
||||
Group {
|
||||
if !instance.app.accountsUsePassword {
|
||||
TextField("Name", text: $name)
|
||||
}
|
||||
|
||||
TextField(usernamePrompt, text: $username)
|
||||
|
||||
if instance.app.accountsUsePassword {
|
||||
TextField("Username", text: $username)
|
||||
SecureField("Password", text: $password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var usernamePrompt: String {
|
||||
switch instance.app {
|
||||
@ -127,10 +100,6 @@ struct AccountForm: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
#if os(macOS)
|
||||
helpButton
|
||||
#endif
|
||||
|
||||
Button("Save", action: submitForm)
|
||||
.disabled(!isValid)
|
||||
#if !os(tvOS)
|
||||
@ -148,9 +117,7 @@ struct AccountForm: View {
|
||||
isValid = false
|
||||
validationDebounce.invalidate()
|
||||
|
||||
let passwordIsValid = instance.app.accountsUsePassword ? !password.isEmpty : true
|
||||
|
||||
guard !username.isEmpty, passwordIsValid else {
|
||||
guard !username.isEmpty, !password.isEmpty else {
|
||||
validator.reset()
|
||||
return
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ struct BrowsingSettings: View {
|
||||
@Default(.accountPickerDisplaysUsername) private var accountPickerDisplaysUsername
|
||||
@Default(.roundedThumbnails) private var roundedThumbnails
|
||||
#endif
|
||||
@Default(.accountPickerDisplaysAnonymousAccounts) private var accountPickerDisplaysAnonymousAccounts
|
||||
#if os(iOS)
|
||||
@Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing
|
||||
#endif
|
||||
@ -37,9 +38,7 @@ struct BrowsingSettings: View {
|
||||
|
||||
private var sections: some View {
|
||||
Group {
|
||||
#if !os(tvOS)
|
||||
interfaceSettings
|
||||
#endif
|
||||
thumbnailsSettings
|
||||
visibleSectionsSettings
|
||||
}
|
||||
@ -61,6 +60,8 @@ struct BrowsingSettings: View {
|
||||
#if !os(tvOS)
|
||||
Toggle("Show account username", isOn: $accountPickerDisplaysUsername)
|
||||
#endif
|
||||
|
||||
Toggle("Show anonymous accounts", isOn: $accountPickerDisplaysAnonymousAccounts)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -225,11 +225,11 @@ struct SettingsView: View {
|
||||
private var windowHeight: Double {
|
||||
switch selection {
|
||||
case .browsing:
|
||||
return 390
|
||||
return 400
|
||||
case .player:
|
||||
return 420
|
||||
case .quality:
|
||||
return 400
|
||||
return 420
|
||||
case .history:
|
||||
return 480
|
||||
case .sponsorBlock:
|
||||
|
@ -19,6 +19,7 @@ struct OpenSettingsButton: View {
|
||||
} label: {
|
||||
Label("Open Settings", systemImage: "gearshape.2")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
button
|
||||
|
@ -170,6 +170,8 @@ struct YatteeApp: App {
|
||||
SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
|
||||
SDWebImageManager.defaultImageCache = PINCache(name: "stream.yattee.app")
|
||||
|
||||
migrateAccounts()
|
||||
|
||||
if !Defaults[.lastAccountIsPublic] {
|
||||
accounts.configureAccount()
|
||||
}
|
||||
@ -246,4 +248,28 @@ struct YatteeApp: App {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func migrateAccounts() {
|
||||
Defaults[.accounts].forEach { account in
|
||||
if !account.username.isEmpty || !(account.password?.isEmpty ?? true) || !(account.name?.isEmpty ?? true) {
|
||||
print("Account needs migration: \(account.description)")
|
||||
if account.app == .invidious {
|
||||
if let name = account.name, !name.isEmpty {
|
||||
AccountsModel.setCredentials(account, username: name, password: "")
|
||||
}
|
||||
if !account.username.isEmpty {
|
||||
AccountsModel.setToken(account, account.username)
|
||||
}
|
||||
} else if account.app == .piped,
|
||||
!account.username.isEmpty,
|
||||
let password = account.password,
|
||||
!password.isEmpty
|
||||
{
|
||||
AccountsModel.setCredentials(account, username: account.username, password: password)
|
||||
}
|
||||
|
||||
AccountsModel.removeDefaultsCredentials(account)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -211,6 +211,7 @@
|
||||
37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; };
|
||||
37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; };
|
||||
37319F0727103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; };
|
||||
3732BFD028B83763009F3F4D /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 3732BFCF28B83763009F3F4D /* KeychainAccess */; };
|
||||
3736A1FE286BB72300C9E5EE /* libavdevice.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3736A1EF286BB72300C9E5EE /* libavdevice.xcframework */; };
|
||||
3736A1FF286BB72300C9E5EE /* libavdevice.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3736A1EF286BB72300C9E5EE /* libavdevice.xcframework */; };
|
||||
3736A200286BB72300C9E5EE /* libuchardet.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3736A1F0286BB72300C9E5EE /* libuchardet.xcframework */; };
|
||||
@ -340,6 +341,11 @@
|
||||
37599F38272B4D740087F250 /* FavoriteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F37272B4D740087F250 /* FavoriteButton.swift */; };
|
||||
37599F39272B4D740087F250 /* FavoriteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F37272B4D740087F250 /* FavoriteButton.swift */; };
|
||||
37599F3A272B4D740087F250 /* FavoriteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F37272B4D740087F250 /* FavoriteButton.swift */; };
|
||||
375B8AB128B57F4200397B31 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 375B8AB028B57F4200397B31 /* KeychainAccess */; };
|
||||
375B8AB328B580D300397B31 /* KeychainModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375B8AB228B580D300397B31 /* KeychainModel.swift */; };
|
||||
375B8AB428B580D300397B31 /* KeychainModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375B8AB228B580D300397B31 /* KeychainModel.swift */; };
|
||||
375B8AB528B580D300397B31 /* KeychainModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375B8AB228B580D300397B31 /* KeychainModel.swift */; };
|
||||
375B8AB728B583BD00397B31 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 375B8AB628B583BD00397B31 /* KeychainAccess */; };
|
||||
375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375DFB5726F9DA010013F468 /* InstancesModel.swift */; };
|
||||
375DFB5926F9DA010013F468 /* InstancesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375DFB5726F9DA010013F468 /* InstancesModel.swift */; };
|
||||
375DFB5A26F9DA010013F468 /* InstancesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375DFB5726F9DA010013F468 /* InstancesModel.swift */; };
|
||||
@ -1073,6 +1079,7 @@
|
||||
37599F2F272B42810087F250 /* FavoriteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteItem.swift; sourceTree = "<group>"; };
|
||||
37599F33272B44000087F250 /* FavoritesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesModel.swift; sourceTree = "<group>"; };
|
||||
37599F37272B4D740087F250 /* FavoriteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteButton.swift; sourceTree = "<group>"; };
|
||||
375B8AB228B580D300397B31 /* KeychainModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainModel.swift; sourceTree = "<group>"; };
|
||||
375DFB5726F9DA010013F468 /* InstancesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesModel.swift; sourceTree = "<group>"; };
|
||||
375E45F427B1976B00BA7902 /* MPVOGLView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPVOGLView.swift; sourceTree = "<group>"; };
|
||||
375E45F727B1AC4700BA7902 /* PlayerControlsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsModel.swift; sourceTree = "<group>"; };
|
||||
@ -1321,6 +1328,7 @@
|
||||
37C2211F27ADA3A200305B41 /* libz.tbd in Frameworks */,
|
||||
37FB284B2722099E00A57617 /* SDWebImageSwiftUI in Frameworks */,
|
||||
3736A210286BB72300C9E5EE /* libavcodec.xcframework in Frameworks */,
|
||||
375B8AB128B57F4200397B31 /* KeychainAccess in Frameworks */,
|
||||
3736A21A286BB72300C9E5EE /* libmpv.xcframework in Frameworks */,
|
||||
3765917C27237D21009F956E /* PINCache in Frameworks */,
|
||||
37BD07B72698AB2E003EBB87 /* Defaults in Frameworks */,
|
||||
@ -1375,6 +1383,7 @@
|
||||
370F4FD927CC16CB001B35DC /* libxcb-shm.0.0.0.dylib in Frameworks */,
|
||||
370F4FCA27CC16CB001B35DC /* libXau.6.dylib in Frameworks */,
|
||||
370F4FD527CC16CB001B35DC /* libfontconfig.1.dylib in Frameworks */,
|
||||
375B8AB728B583BD00397B31 /* KeychainAccess in Frameworks */,
|
||||
37A5DBC6285E06B100CA4DD1 /* SwiftUIPager in Frameworks */,
|
||||
370F4FCE27CC16CB001B35DC /* libswresample.4.3.100.dylib in Frameworks */,
|
||||
370F4FDA27CC16CB001B35DC /* libmpv.dylib in Frameworks */,
|
||||
@ -1422,6 +1431,7 @@
|
||||
3736A20D286BB72300C9E5EE /* libavutil.xcframework in Frameworks */,
|
||||
3736A213286BB72300C9E5EE /* libswresample.xcframework in Frameworks */,
|
||||
37FB285427220D8400A57617 /* SDWebImagePINPlugin in Frameworks */,
|
||||
3732BFD028B83763009F3F4D /* KeychainAccess in Frameworks */,
|
||||
3772003927E8EEB700CB2475 /* AVFoundation.framework in Frameworks */,
|
||||
3736A20F286BB72300C9E5EE /* libass.xcframework in Frameworks */,
|
||||
3736A1FF286BB72300C9E5EE /* libavdevice.xcframework in Frameworks */,
|
||||
@ -2072,6 +2082,7 @@
|
||||
37599F33272B44000087F250 /* FavoritesModel.swift */,
|
||||
37BC50AB2778BCBA00510953 /* HistoryModel.swift */,
|
||||
377ABC3F286E4AD5009C986F /* InstancesManifest.swift */,
|
||||
375B8AB228B580D300397B31 /* KeychainModel.swift */,
|
||||
377ABC43286E4B74009C986F /* ManifestedInstance.swift */,
|
||||
37EF5C212739D37B00B03725 /* MenuModel.swift */,
|
||||
371F2F19269B43D300E4A7AB /* NavigationModel.swift */,
|
||||
@ -2295,6 +2306,7 @@
|
||||
372AA40F286D067B0000B1DC /* Repeat */,
|
||||
37EE6DC428A305AD00BFD632 /* Reachability */,
|
||||
3799AC0828B03CED001376F9 /* ActiveLabel */,
|
||||
375B8AB028B57F4200397B31 /* KeychainAccess */,
|
||||
);
|
||||
productName = "Yattee (iOS)";
|
||||
productReference = 37D4B0C92671614900C925CA /* Yattee.app */;
|
||||
@ -2331,6 +2343,7 @@
|
||||
37CF8B8528535E5A00B71E37 /* SDWebImage */,
|
||||
37A5DBC5285E06B100CA4DD1 /* SwiftUIPager */,
|
||||
372AA413286D06A10000B1DC /* Repeat */,
|
||||
375B8AB628B583BD00397B31 /* KeychainAccess */,
|
||||
);
|
||||
productName = "Yattee (macOS)";
|
||||
productReference = 37D4B0CF2671614900C925CA /* Yattee.app */;
|
||||
@ -2407,6 +2420,7 @@
|
||||
37CF8B8728535E6300B71E37 /* SDWebImage */,
|
||||
372AA411286D06950000B1DC /* Repeat */,
|
||||
37E80F42287B7AAF00561799 /* SwiftUIPager */,
|
||||
3732BFCF28B83763009F3F4D /* KeychainAccess */,
|
||||
);
|
||||
productName = Yattee;
|
||||
productReference = 37D4B158267164AE00C925CA /* Yattee.app */;
|
||||
@ -2506,6 +2520,7 @@
|
||||
372AA40E286D067B0000B1DC /* XCRemoteSwiftPackageReference "Repeat" */,
|
||||
37EE6DC328A305AD00BFD632 /* XCRemoteSwiftPackageReference "Reachability" */,
|
||||
3799AC0728B03CEC001376F9 /* XCRemoteSwiftPackageReference "ActiveLabel.swift" */,
|
||||
375B8AAF28B57F4200397B31 /* XCRemoteSwiftPackageReference "KeychainAccess" */,
|
||||
);
|
||||
productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
|
||||
projectDirPath = "";
|
||||
@ -2857,6 +2872,7 @@
|
||||
376A33E42720CB35000C1D6B /* Account.swift in Sources */,
|
||||
3756C2A62861131100E4B059 /* NetworkState.swift in Sources */,
|
||||
37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
||||
375B8AB328B580D300397B31 /* KeychainModel.swift in Sources */,
|
||||
37BC50AC2778BCBA00510953 /* HistoryModel.swift in Sources */,
|
||||
37AAF29026740715007FC770 /* Channel.swift in Sources */,
|
||||
37DD9DBA2785D60300539416 /* FramePreferenceKey.swift in Sources */,
|
||||
@ -3032,6 +3048,7 @@
|
||||
37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
|
||||
377ABC45286E4B74009C986F /* ManifestedInstance.swift in Sources */,
|
||||
3795593727B08538007FF8F4 /* StreamControl.swift in Sources */,
|
||||
375B8AB428B580D300397B31 /* KeychainModel.swift in Sources */,
|
||||
37F7AB5528A951B200FB46B5 /* Power.swift in Sources */,
|
||||
372CFD16285F2E2A00B0B54B /* ControlsBar.swift in Sources */,
|
||||
37FFC441272734C3009FFD26 /* Throttle.swift in Sources */,
|
||||
@ -3372,6 +3389,7 @@
|
||||
3752069F285E910600CA655F /* ChapterView.swift in Sources */,
|
||||
37F4AD1D28612B23004D0F66 /* OpeningStream.swift in Sources */,
|
||||
3705B180267B4DFB00704544 /* TrendingCountry.swift in Sources */,
|
||||
375B8AB528B580D300397B31 /* KeychainModel.swift in Sources */,
|
||||
373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
||||
37C3A24B27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
|
||||
37F4AD2128612DFD004D0F66 /* Buffering.swift in Sources */,
|
||||
@ -4328,6 +4346,14 @@
|
||||
minimumVersion = 0.6.0;
|
||||
};
|
||||
};
|
||||
375B8AAF28B57F4200397B31 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git";
|
||||
requirement = {
|
||||
branch = master;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
3765917827237D07009F956E /* XCRemoteSwiftPackageReference "PINCache" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/pinterest/PINCache";
|
||||
@ -4500,6 +4526,21 @@
|
||||
package = 372AA40E286D067B0000B1DC /* XCRemoteSwiftPackageReference "Repeat" */;
|
||||
productName = Repeat;
|
||||
};
|
||||
3732BFCF28B83763009F3F4D /* KeychainAccess */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 375B8AAF28B57F4200397B31 /* XCRemoteSwiftPackageReference "KeychainAccess" */;
|
||||
productName = KeychainAccess;
|
||||
};
|
||||
375B8AB028B57F4200397B31 /* KeychainAccess */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 375B8AAF28B57F4200397B31 /* XCRemoteSwiftPackageReference "KeychainAccess" */;
|
||||
productName = KeychainAccess;
|
||||
};
|
||||
375B8AB628B583BD00397B31 /* KeychainAccess */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 375B8AAF28B57F4200397B31 /* XCRemoteSwiftPackageReference "KeychainAccess" */;
|
||||
productName = KeychainAccess;
|
||||
};
|
||||
3765917B27237D21009F956E /* PINCache */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 3765917827237D07009F956E /* XCRemoteSwiftPackageReference "PINCache" */;
|
||||
|
@ -27,6 +27,15 @@
|
||||
"version" : "6.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "keychainaccess",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kishikawakatsumi/KeychainAccess.git",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "6299daec1d74be12164fec090faf9ed14d0da9d6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "libwebp-xcode",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
@ -9,6 +9,7 @@ struct AccountSelectionView: View {
|
||||
|
||||
@Default(.accounts) private var accounts
|
||||
@Default(.instances) private var instances
|
||||
@Default(.accountPickerDisplaysAnonymousAccounts) private var accountPickerDisplaysAnonymousAccounts
|
||||
|
||||
var body: some View {
|
||||
Section(header: Text(showHeader ? "Current Location" : "")) {
|
||||
@ -32,7 +33,8 @@ struct AccountSelectionView: View {
|
||||
}
|
||||
|
||||
var allAccounts: [Account] {
|
||||
accounts + instances.map(\.anonymousAccount) + [accountsModel.publicAccount].compactMap { $0 }
|
||||
let anonymousAccounts = accountPickerDisplaysAnonymousAccounts ? instances.map(\.anonymousAccount) : []
|
||||
return accounts + anonymousAccounts + [accountsModel.publicAccount].compactMap { $0 }
|
||||
}
|
||||
|
||||
private var nextAccount: Account? {
|
||||
|
Loading…
Reference in New Issue
Block a user