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()
api.validInstance = true
api.signedIn = true
return api
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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" */;

View File

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

View File

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