Add username/password login and keychain manager

Fix #224
This commit is contained in:
Arkadiusz Fal 2022-08-26 01:36:46 +02:00
parent 08ed810b9e
commit 2f2fd67860
19 changed files with 280 additions and 107 deletions

View File

@ -35,7 +35,6 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier {
let api = InvidiousAPI() let api = InvidiousAPI()
api.validInstance = true api.validInstance = true
api.signedIn = true
return api return api
} }

View File

@ -5,13 +5,12 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
static var bridge = AccountsBridge() static var bridge = AccountsBridge()
let id: String let id: String
let app: VideosApp var app: VideosApp?
let instanceID: String? let instanceID: String?
var name: String? var name: String?
let url: String let url: String
let username: String var username: String
let password: String? var password: String?
var token: String?
let anonymous: Bool let anonymous: Bool
let country: String? let country: String?
let region: String? let region: String?
@ -24,7 +23,6 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
url: String? = nil, url: String? = nil,
username: String? = nil, username: String? = nil,
password: String? = nil, password: String? = nil,
token: String? = nil,
anonymous: Bool = false, anonymous: Bool = false,
country: String? = nil, country: String? = nil,
region: String? = nil region: String? = nil
@ -32,19 +30,26 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
self.anonymous = anonymous self.anonymous = anonymous
self.id = id ?? (anonymous ? "anonymous-\(instanceID ?? url ?? UUID().uuidString)" : UUID().uuidString) self.id = id ?? (anonymous ? "anonymous-\(instanceID ?? url ?? UUID().uuidString)" : UUID().uuidString)
self.app = app ?? .invidious
self.instanceID = instanceID self.instanceID = instanceID
self.name = name self.name = name
self.url = url ?? "" self.url = url ?? ""
self.username = username ?? "" self.username = username ?? ""
self.token = token
self.password = password ?? "" self.password = password ?? ""
self.country = country self.country = country
self.region = region 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! { 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 { var isPublic: Bool {
@ -52,8 +57,12 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
} }
var shortUsername: String { var shortUsername: String {
guard username.count > 10 else { let (username, _) = credentials
return username
guard let username = username,
username.count > 10
else {
return username ?? ""
} }
let index = username.index(username.startIndex, offsetBy: 11) let index = username.index(username.startIndex, offsetBy: 11)
@ -61,7 +70,11 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
} }
var description: String { 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) { func hash(into hasher: inout Hasher) {

View File

@ -43,14 +43,6 @@ final class AccountValidator: Service {
$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"]) $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]) { configure("/login", requestMethods: [.post]) {
$0.headers["Content-Type"] = "application/json" $0.headers["Content-Type"] = "application/json"
} }
@ -167,7 +159,8 @@ final class AccountValidator: Service {
var accountRequest: Request? { var accountRequest: Request? {
switch app.wrappedValue { switch app.wrappedValue {
case .invidious: 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: case .piped:
return login.request(.post, json: ["username": account.username, "password": account.password]) return login.request(.post, json: ["username": account.username, "password": account.password])
default: default:
@ -184,18 +177,10 @@ final class AccountValidator: Service {
error?.wrappedValue = nil error?.wrappedValue = nil
} }
var invidiousCookieHeader: String {
"SID=\(account.username)"
}
var login: Resource { var login: Resource {
resource("/login") resource("/login")
} }
var feed: Resource {
resource("/api/v1/auth/feed")
}
var videoResourceBasePath: String { var videoResourceBasePath: String {
app.wrappedValue == .invidious ? "/api/v1/videos" : "/streams" app.wrappedValue == .invidious ? "/api/v1/videos" : "/streams"
} }

View File

@ -93,22 +93,47 @@ final class AccountsModel: ObservableObject {
Defaults[.accounts].first { $0.id == id } Defaults[.accounts].first { $0.id == id }
} }
static func add(instance: Instance, name: String, username: String, password: String? = nil) -> Account { static func add(instance: Instance, name: String, username: String, password: String) -> Account {
let account = Account( let account = Account(instanceID: instance.id, name: name, url: instance.apiURL)
instanceID: instance.id,
name: name,
url: instance.apiURL,
username: username,
password: password
)
Defaults[.accounts].append(account) Defaults[.accounts].append(account)
setCredentials(account, username: username, password: password)
return account return account
} }
static func remove(_ account: Account) { static func remove(_ account: Account) {
if let accountIndex = Defaults[.accounts].firstIndex(where: { $0.id == account.id }) { 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) 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
}
}
} }

View File

@ -1,3 +1,4 @@
import Alamofire
import AVKit import AVKit
import Defaults import Defaults
import Foundation import Foundation
@ -10,7 +11,12 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
@Published var account: Account! @Published var account: Account!
@Published var validInstance = true @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) { init(account: Account? = nil) {
super.init() super.init()
@ -25,7 +31,6 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
func setAccount(_ account: Account) { func setAccount(_ account: Account) {
self.account = account self.account = account
signedIn = false
validInstance = account.anonymous validInstance = account.anonymous
@ -57,28 +62,23 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
} }
func validateSID() { func validateSID() {
guard !signedIn else { guard signedIn, !(account.token?.isEmpty ?? true) else {
return return
} }
feed? feed?
.load() .load()
.onSuccess { _ in .onFailure { _ in
self.signedIn = true self.updateToken(force: 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."
)
} }
} }
func configure() { func configure() {
invalidateConfiguration()
configure { configure {
if !self.account.username.isEmpty { if let cookie = self.cookieHeader {
$0.headers["Cookie"] = self.cookieHeader $0.headers["Cookie"] = cookie
} }
$0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"]) $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) 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 { private func pathPattern(_ path: String) -> String {
@ -180,8 +245,9 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
"\(Self.basePath)/\(path)" "\(Self.basePath)/\(path)"
} }
private var cookieHeader: String { private var cookieHeader: String? {
"SID=\(account.username)" guard let token = account?.token, !token.isEmpty else { return nil }
return "SID=\(token)"
} }
var popular: Resource? { var popular: Resource? {

View File

@ -109,23 +109,33 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
} }
func updateToken() { func updateToken() {
guard !account.anonymous else { let (username, password) = AccountsModel.getCredentials(account)
guard !account.anonymous,
let username = username,
let password = password
else {
return return
} }
account.token = nil
login.request( login.request(
.post, .post,
json: ["username": account.username, "password": account.password] json: ["username": username, "password": password]
) )
.onSuccess { response in .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 { if let error = response.json.dictionaryValue["error"]?.string {
NavigationModel.shared.presentAlert( NavigationModel.shared.presentAlert(
title: "Could not connect with your account", title: "Account Error",
message: 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() self.configure()

View File

@ -11,10 +11,6 @@ enum VideosApp: String, CaseIterable {
true true
} }
var accountsUsePassword: Bool {
self == .piped
}
var supportsPopular: Bool { var supportsPopular: Bool {
self == .invidious self == .invidious
} }

26
Model/KeychainModel.swift Normal file
View 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"))
}
}

View File

@ -33,6 +33,7 @@ extension Defaults.Keys {
#endif #endif
static let accountPickerDisplaysUsername = Key<Bool>("accountPickerDisplaysUsername", default: accountPickerDisplaysUsernameDefault) static let accountPickerDisplaysUsername = Key<Bool>("accountPickerDisplaysUsername", default: accountPickerDisplaysUsernameDefault)
#endif #endif
static let accountPickerDisplaysAnonymousAccounts = Key<Bool>("accountPickerDisplaysAnonymousAccounts", default: true)
#if os(iOS) #if os(iOS)
static let lockPortraitWhenBrowsing = Key<Bool>("lockPortraitWhenBrowsing", default: UIDevice.current.userInterfaceIdiom == .phone) static let lockPortraitWhenBrowsing = Key<Bool>("lockPortraitWhenBrowsing", default: UIDevice.current.userInterfaceIdiom == .phone)
#endif #endif

View File

@ -7,6 +7,7 @@ struct AccountsMenuView: View {
@Default(.accounts) private var accounts @Default(.accounts) private var accounts
@Default(.instances) private var instances @Default(.instances) private var instances
@Default(.accountPickerDisplaysUsername) private var accountPickerDisplaysUsername @Default(.accountPickerDisplaysUsername) private var accountPickerDisplaysUsername
@Default(.accountPickerDisplaysAnonymousAccounts) private var accountPickerDisplaysAnonymousAccounts
@ViewBuilder var body: some View { @ViewBuilder var body: some View {
if !instances.isEmpty { if !instances.isEmpty {
@ -48,7 +49,8 @@ struct AccountsMenuView: View {
} }
private var allAccounts: [Account] { 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 { private func accountButtonTitle(account: Account) -> String {

View File

@ -46,11 +46,14 @@ struct ContentView: View {
.environmentObject(settings) .environmentObject(settings)
#endif #endif
} }
.onChange(of: accounts.current) { _ in
subscriptions.load(force: true)
playlists.load(force: true)
}
.onChange(of: accounts.signedIn) { _ in .onChange(of: accounts.signedIn) { _ in
subscriptions.load(force: true) subscriptions.load(force: true)
playlists.load(force: true) playlists.load(force: true)
} }
.environmentObject(accounts) .environmentObject(accounts)
.environmentObject(comments) .environmentObject(comments)
.environmentObject(instances) .environmentObject(instances)

View File

@ -63,10 +63,6 @@ struct AccountForm: View {
#if os(macOS) #if os(macOS)
.padding(.horizontal) .padding(.horizontal)
#endif #endif
#if os(iOS)
helpButton
#endif
} }
#else #else
formFields formFields
@ -76,35 +72,12 @@ struct AccountForm: View {
.onChange(of: password) { _ in validate() } .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 { var formFields: some View {
Group { Group {
if !instance.app.accountsUsePassword { TextField("Username", text: $username)
TextField("Name", text: $name)
}
TextField(usernamePrompt, text: $username)
if instance.app.accountsUsePassword {
SecureField("Password", text: $password) SecureField("Password", text: $password)
} }
} }
}
var usernamePrompt: String { var usernamePrompt: String {
switch instance.app { switch instance.app {
@ -127,10 +100,6 @@ struct AccountForm: View {
Spacer() Spacer()
#if os(macOS)
helpButton
#endif
Button("Save", action: submitForm) Button("Save", action: submitForm)
.disabled(!isValid) .disabled(!isValid)
#if !os(tvOS) #if !os(tvOS)
@ -148,9 +117,7 @@ struct AccountForm: View {
isValid = false isValid = false
validationDebounce.invalidate() validationDebounce.invalidate()
let passwordIsValid = instance.app.accountsUsePassword ? !password.isEmpty : true guard !username.isEmpty, !password.isEmpty else {
guard !username.isEmpty, passwordIsValid else {
validator.reset() validator.reset()
return return
} }

View File

@ -6,6 +6,7 @@ struct BrowsingSettings: View {
@Default(.accountPickerDisplaysUsername) private var accountPickerDisplaysUsername @Default(.accountPickerDisplaysUsername) private var accountPickerDisplaysUsername
@Default(.roundedThumbnails) private var roundedThumbnails @Default(.roundedThumbnails) private var roundedThumbnails
#endif #endif
@Default(.accountPickerDisplaysAnonymousAccounts) private var accountPickerDisplaysAnonymousAccounts
#if os(iOS) #if os(iOS)
@Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing @Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing
#endif #endif
@ -37,9 +38,7 @@ struct BrowsingSettings: View {
private var sections: some View { private var sections: some View {
Group { Group {
#if !os(tvOS)
interfaceSettings interfaceSettings
#endif
thumbnailsSettings thumbnailsSettings
visibleSectionsSettings visibleSectionsSettings
} }
@ -61,6 +60,8 @@ struct BrowsingSettings: View {
#if !os(tvOS) #if !os(tvOS)
Toggle("Show account username", isOn: $accountPickerDisplaysUsername) Toggle("Show account username", isOn: $accountPickerDisplaysUsername)
#endif #endif
Toggle("Show anonymous accounts", isOn: $accountPickerDisplaysAnonymousAccounts)
} }
} }

View File

@ -225,11 +225,11 @@ struct SettingsView: View {
private var windowHeight: Double { private var windowHeight: Double {
switch selection { switch selection {
case .browsing: case .browsing:
return 390 return 400
case .player: case .player:
return 420 return 420
case .quality: case .quality:
return 400 return 420
case .history: case .history:
return 480 return 480
case .sponsorBlock: case .sponsorBlock:

View File

@ -19,6 +19,7 @@ struct OpenSettingsButton: View {
} label: { } label: {
Label("Open Settings", systemImage: "gearshape.2") Label("Open Settings", systemImage: "gearshape.2")
} }
.buttonStyle(.plain)
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
button button

View File

@ -170,6 +170,8 @@ struct YatteeApp: App {
SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared) SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
SDWebImageManager.defaultImageCache = PINCache(name: "stream.yattee.app") SDWebImageManager.defaultImageCache = PINCache(name: "stream.yattee.app")
migrateAccounts()
if !Defaults[.lastAccountIsPublic] { if !Defaults[.lastAccountIsPublic] {
accounts.configureAccount() accounts.configureAccount()
} }
@ -246,4 +248,28 @@ struct YatteeApp: App {
} }
#endif #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)
}
}
}
} }

View File

@ -211,6 +211,7 @@
37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; }; 37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; };
37319F0627103F94004ECCD0 /* 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 */; }; 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 */; }; 3736A1FE286BB72300C9E5EE /* libavdevice.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3736A1EF286BB72300C9E5EE /* libavdevice.xcframework */; };
3736A1FF286BB72300C9E5EE /* 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 */; }; 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 */; }; 37599F38272B4D740087F250 /* FavoriteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F37272B4D740087F250 /* FavoriteButton.swift */; };
37599F39272B4D740087F250 /* 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 */; }; 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 */; }; 375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375DFB5726F9DA010013F468 /* InstancesModel.swift */; };
375DFB5926F9DA010013F468 /* 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 */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 375E45F727B1AC4700BA7902 /* PlayerControlsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsModel.swift; sourceTree = "<group>"; };
@ -1321,6 +1328,7 @@
37C2211F27ADA3A200305B41 /* libz.tbd in Frameworks */, 37C2211F27ADA3A200305B41 /* libz.tbd in Frameworks */,
37FB284B2722099E00A57617 /* SDWebImageSwiftUI in Frameworks */, 37FB284B2722099E00A57617 /* SDWebImageSwiftUI in Frameworks */,
3736A210286BB72300C9E5EE /* libavcodec.xcframework in Frameworks */, 3736A210286BB72300C9E5EE /* libavcodec.xcframework in Frameworks */,
375B8AB128B57F4200397B31 /* KeychainAccess in Frameworks */,
3736A21A286BB72300C9E5EE /* libmpv.xcframework in Frameworks */, 3736A21A286BB72300C9E5EE /* libmpv.xcframework in Frameworks */,
3765917C27237D21009F956E /* PINCache in Frameworks */, 3765917C27237D21009F956E /* PINCache in Frameworks */,
37BD07B72698AB2E003EBB87 /* Defaults in Frameworks */, 37BD07B72698AB2E003EBB87 /* Defaults in Frameworks */,
@ -1375,6 +1383,7 @@
370F4FD927CC16CB001B35DC /* libxcb-shm.0.0.0.dylib in Frameworks */, 370F4FD927CC16CB001B35DC /* libxcb-shm.0.0.0.dylib in Frameworks */,
370F4FCA27CC16CB001B35DC /* libXau.6.dylib in Frameworks */, 370F4FCA27CC16CB001B35DC /* libXau.6.dylib in Frameworks */,
370F4FD527CC16CB001B35DC /* libfontconfig.1.dylib in Frameworks */, 370F4FD527CC16CB001B35DC /* libfontconfig.1.dylib in Frameworks */,
375B8AB728B583BD00397B31 /* KeychainAccess in Frameworks */,
37A5DBC6285E06B100CA4DD1 /* SwiftUIPager in Frameworks */, 37A5DBC6285E06B100CA4DD1 /* SwiftUIPager in Frameworks */,
370F4FCE27CC16CB001B35DC /* libswresample.4.3.100.dylib in Frameworks */, 370F4FCE27CC16CB001B35DC /* libswresample.4.3.100.dylib in Frameworks */,
370F4FDA27CC16CB001B35DC /* libmpv.dylib in Frameworks */, 370F4FDA27CC16CB001B35DC /* libmpv.dylib in Frameworks */,
@ -1422,6 +1431,7 @@
3736A20D286BB72300C9E5EE /* libavutil.xcframework in Frameworks */, 3736A20D286BB72300C9E5EE /* libavutil.xcframework in Frameworks */,
3736A213286BB72300C9E5EE /* libswresample.xcframework in Frameworks */, 3736A213286BB72300C9E5EE /* libswresample.xcframework in Frameworks */,
37FB285427220D8400A57617 /* SDWebImagePINPlugin in Frameworks */, 37FB285427220D8400A57617 /* SDWebImagePINPlugin in Frameworks */,
3732BFD028B83763009F3F4D /* KeychainAccess in Frameworks */,
3772003927E8EEB700CB2475 /* AVFoundation.framework in Frameworks */, 3772003927E8EEB700CB2475 /* AVFoundation.framework in Frameworks */,
3736A20F286BB72300C9E5EE /* libass.xcframework in Frameworks */, 3736A20F286BB72300C9E5EE /* libass.xcframework in Frameworks */,
3736A1FF286BB72300C9E5EE /* libavdevice.xcframework in Frameworks */, 3736A1FF286BB72300C9E5EE /* libavdevice.xcframework in Frameworks */,
@ -2072,6 +2082,7 @@
37599F33272B44000087F250 /* FavoritesModel.swift */, 37599F33272B44000087F250 /* FavoritesModel.swift */,
37BC50AB2778BCBA00510953 /* HistoryModel.swift */, 37BC50AB2778BCBA00510953 /* HistoryModel.swift */,
377ABC3F286E4AD5009C986F /* InstancesManifest.swift */, 377ABC3F286E4AD5009C986F /* InstancesManifest.swift */,
375B8AB228B580D300397B31 /* KeychainModel.swift */,
377ABC43286E4B74009C986F /* ManifestedInstance.swift */, 377ABC43286E4B74009C986F /* ManifestedInstance.swift */,
37EF5C212739D37B00B03725 /* MenuModel.swift */, 37EF5C212739D37B00B03725 /* MenuModel.swift */,
371F2F19269B43D300E4A7AB /* NavigationModel.swift */, 371F2F19269B43D300E4A7AB /* NavigationModel.swift */,
@ -2295,6 +2306,7 @@
372AA40F286D067B0000B1DC /* Repeat */, 372AA40F286D067B0000B1DC /* Repeat */,
37EE6DC428A305AD00BFD632 /* Reachability */, 37EE6DC428A305AD00BFD632 /* Reachability */,
3799AC0828B03CED001376F9 /* ActiveLabel */, 3799AC0828B03CED001376F9 /* ActiveLabel */,
375B8AB028B57F4200397B31 /* KeychainAccess */,
); );
productName = "Yattee (iOS)"; productName = "Yattee (iOS)";
productReference = 37D4B0C92671614900C925CA /* Yattee.app */; productReference = 37D4B0C92671614900C925CA /* Yattee.app */;
@ -2331,6 +2343,7 @@
37CF8B8528535E5A00B71E37 /* SDWebImage */, 37CF8B8528535E5A00B71E37 /* SDWebImage */,
37A5DBC5285E06B100CA4DD1 /* SwiftUIPager */, 37A5DBC5285E06B100CA4DD1 /* SwiftUIPager */,
372AA413286D06A10000B1DC /* Repeat */, 372AA413286D06A10000B1DC /* Repeat */,
375B8AB628B583BD00397B31 /* KeychainAccess */,
); );
productName = "Yattee (macOS)"; productName = "Yattee (macOS)";
productReference = 37D4B0CF2671614900C925CA /* Yattee.app */; productReference = 37D4B0CF2671614900C925CA /* Yattee.app */;
@ -2407,6 +2420,7 @@
37CF8B8728535E6300B71E37 /* SDWebImage */, 37CF8B8728535E6300B71E37 /* SDWebImage */,
372AA411286D06950000B1DC /* Repeat */, 372AA411286D06950000B1DC /* Repeat */,
37E80F42287B7AAF00561799 /* SwiftUIPager */, 37E80F42287B7AAF00561799 /* SwiftUIPager */,
3732BFCF28B83763009F3F4D /* KeychainAccess */,
); );
productName = Yattee; productName = Yattee;
productReference = 37D4B158267164AE00C925CA /* Yattee.app */; productReference = 37D4B158267164AE00C925CA /* Yattee.app */;
@ -2506,6 +2520,7 @@
372AA40E286D067B0000B1DC /* XCRemoteSwiftPackageReference "Repeat" */, 372AA40E286D067B0000B1DC /* XCRemoteSwiftPackageReference "Repeat" */,
37EE6DC328A305AD00BFD632 /* XCRemoteSwiftPackageReference "Reachability" */, 37EE6DC328A305AD00BFD632 /* XCRemoteSwiftPackageReference "Reachability" */,
3799AC0728B03CEC001376F9 /* XCRemoteSwiftPackageReference "ActiveLabel.swift" */, 3799AC0728B03CEC001376F9 /* XCRemoteSwiftPackageReference "ActiveLabel.swift" */,
375B8AAF28B57F4200397B31 /* XCRemoteSwiftPackageReference "KeychainAccess" */,
); );
productRefGroup = 37D4B0CA2671614900C925CA /* Products */; productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -2857,6 +2872,7 @@
376A33E42720CB35000C1D6B /* Account.swift in Sources */, 376A33E42720CB35000C1D6B /* Account.swift in Sources */,
3756C2A62861131100E4B059 /* NetworkState.swift in Sources */, 3756C2A62861131100E4B059 /* NetworkState.swift in Sources */,
37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */, 37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */,
375B8AB328B580D300397B31 /* KeychainModel.swift in Sources */,
37BC50AC2778BCBA00510953 /* HistoryModel.swift in Sources */, 37BC50AC2778BCBA00510953 /* HistoryModel.swift in Sources */,
37AAF29026740715007FC770 /* Channel.swift in Sources */, 37AAF29026740715007FC770 /* Channel.swift in Sources */,
37DD9DBA2785D60300539416 /* FramePreferenceKey.swift in Sources */, 37DD9DBA2785D60300539416 /* FramePreferenceKey.swift in Sources */,
@ -3032,6 +3048,7 @@
37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */, 37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
377ABC45286E4B74009C986F /* ManifestedInstance.swift in Sources */, 377ABC45286E4B74009C986F /* ManifestedInstance.swift in Sources */,
3795593727B08538007FF8F4 /* StreamControl.swift in Sources */, 3795593727B08538007FF8F4 /* StreamControl.swift in Sources */,
375B8AB428B580D300397B31 /* KeychainModel.swift in Sources */,
37F7AB5528A951B200FB46B5 /* Power.swift in Sources */, 37F7AB5528A951B200FB46B5 /* Power.swift in Sources */,
372CFD16285F2E2A00B0B54B /* ControlsBar.swift in Sources */, 372CFD16285F2E2A00B0B54B /* ControlsBar.swift in Sources */,
37FFC441272734C3009FFD26 /* Throttle.swift in Sources */, 37FFC441272734C3009FFD26 /* Throttle.swift in Sources */,
@ -3372,6 +3389,7 @@
3752069F285E910600CA655F /* ChapterView.swift in Sources */, 3752069F285E910600CA655F /* ChapterView.swift in Sources */,
37F4AD1D28612B23004D0F66 /* OpeningStream.swift in Sources */, 37F4AD1D28612B23004D0F66 /* OpeningStream.swift in Sources */,
3705B180267B4DFB00704544 /* TrendingCountry.swift in Sources */, 3705B180267B4DFB00704544 /* TrendingCountry.swift in Sources */,
375B8AB528B580D300397B31 /* KeychainModel.swift in Sources */,
373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */, 373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */,
37C3A24B27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */, 37C3A24B27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
37F4AD2128612DFD004D0F66 /* Buffering.swift in Sources */, 37F4AD2128612DFD004D0F66 /* Buffering.swift in Sources */,
@ -4328,6 +4346,14 @@
minimumVersion = 0.6.0; minimumVersion = 0.6.0;
}; };
}; };
375B8AAF28B57F4200397B31 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git";
requirement = {
branch = master;
kind = branch;
};
};
3765917827237D07009F956E /* XCRemoteSwiftPackageReference "PINCache" */ = { 3765917827237D07009F956E /* XCRemoteSwiftPackageReference "PINCache" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/pinterest/PINCache"; repositoryURL = "https://github.com/pinterest/PINCache";
@ -4500,6 +4526,21 @@
package = 372AA40E286D067B0000B1DC /* XCRemoteSwiftPackageReference "Repeat" */; package = 372AA40E286D067B0000B1DC /* XCRemoteSwiftPackageReference "Repeat" */;
productName = 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 */ = { 3765917B27237D21009F956E /* PINCache */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = 3765917827237D07009F956E /* XCRemoteSwiftPackageReference "PINCache" */; package = 3765917827237D07009F956E /* XCRemoteSwiftPackageReference "PINCache" */;

View File

@ -27,6 +27,15 @@
"version" : "6.3.0" "version" : "6.3.0"
} }
}, },
{
"identity" : "keychainaccess",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kishikawakatsumi/KeychainAccess.git",
"state" : {
"branch" : "master",
"revision" : "6299daec1d74be12164fec090faf9ed14d0da9d6"
}
},
{ {
"identity" : "libwebp-xcode", "identity" : "libwebp-xcode",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@ -9,6 +9,7 @@ struct AccountSelectionView: View {
@Default(.accounts) private var accounts @Default(.accounts) private var accounts
@Default(.instances) private var instances @Default(.instances) private var instances
@Default(.accountPickerDisplaysAnonymousAccounts) private var accountPickerDisplaysAnonymousAccounts
var body: some View { var body: some View {
Section(header: Text(showHeader ? "Current Location" : "")) { Section(header: Text(showHeader ? "Current Location" : "")) {
@ -32,7 +33,8 @@ struct AccountSelectionView: View {
} }
var allAccounts: [Account] { 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? { private var nextAccount: Account? {