diff --git a/Fixtures/View+Fixtures.swift b/Fixtures/View+Fixtures.swift index d9d0b26f..4a967a96 100644 --- a/Fixtures/View+Fixtures.swift +++ b/Fixtures/View+Fixtures.swift @@ -35,7 +35,6 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier { let api = InvidiousAPI() api.validInstance = true - api.signedIn = true return api } diff --git a/Model/Accounts/Account.swift b/Model/Accounts/Account.swift index 47ff8fd9..3a83805a 100644 --- a/Model/Accounts/Account.swift +++ b/Model/Accounts/Account.swift @@ -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) { diff --git a/Model/Accounts/AccountValidator.swift b/Model/Accounts/AccountValidator.swift index 309595c4..4a428537 100644 --- a/Model/Accounts/AccountValidator.swift +++ b/Model/Accounts/AccountValidator.swift @@ -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" } diff --git a/Model/Accounts/AccountsModel.swift b/Model/Accounts/AccountsModel.swift index 2b91ded0..39a4c5d4 100644 --- a/Model/Accounts/AccountsModel.swift +++ b/Model/Accounts/AccountsModel.swift @@ -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 + } + } } diff --git a/Model/Applications/InvidiousAPI.swift b/Model/Applications/InvidiousAPI.swift index f04e85ad..34a6a130 100644 --- a/Model/Applications/InvidiousAPI.swift +++ b/Model/Applications/InvidiousAPI.swift @@ -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?, 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=(?[^;]*);"# + 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? { diff --git a/Model/Applications/PipedAPI.swift b/Model/Applications/PipedAPI.swift index 3759d45f..1302ce94 100644 --- a/Model/Applications/PipedAPI.swift +++ b/Model/Applications/PipedAPI.swift @@ -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() diff --git a/Model/Applications/VideosApp.swift b/Model/Applications/VideosApp.swift index 32567e99..47e5ce53 100644 --- a/Model/Applications/VideosApp.swift +++ b/Model/Applications/VideosApp.swift @@ -11,10 +11,6 @@ enum VideosApp: String, CaseIterable { true } - var accountsUsePassword: Bool { - self == .piped - } - var supportsPopular: Bool { self == .invidious } diff --git a/Model/KeychainModel.swift b/Model/KeychainModel.swift new file mode 100644 index 00000000..a68a4f6c --- /dev/null +++ b/Model/KeychainModel.swift @@ -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")) + } +} diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 54463e4a..6699600e 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -33,6 +33,7 @@ extension Defaults.Keys { #endif static let accountPickerDisplaysUsername = Key("accountPickerDisplaysUsername", default: accountPickerDisplaysUsernameDefault) #endif + static let accountPickerDisplaysAnonymousAccounts = Key("accountPickerDisplaysAnonymousAccounts", default: true) #if os(iOS) static let lockPortraitWhenBrowsing = Key("lockPortraitWhenBrowsing", default: UIDevice.current.userInterfaceIdiom == .phone) #endif diff --git a/Shared/Navigation/AccountsMenuView.swift b/Shared/Navigation/AccountsMenuView.swift index 4f534f81..8fd90c3a 100644 --- a/Shared/Navigation/AccountsMenuView.swift +++ b/Shared/Navigation/AccountsMenuView.swift @@ -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 { diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 2945cb1d..9388fa46 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -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) diff --git a/Shared/Settings/AccountForm.swift b/Shared/Settings/AccountForm.swift index 47df0120..c6a78e4a 100644 --- a/Shared/Settings/AccountForm.swift +++ b/Shared/Settings/AccountForm.swift @@ -63,10 +63,6 @@ struct AccountForm: View { #if os(macOS) .padding(.horizontal) #endif - - #if os(iOS) - helpButton - #endif } #else formFields @@ -76,33 +72,10 @@ 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 { - SecureField("Password", text: $password) - } + TextField("Username", text: $username) + SecureField("Password", text: $password) } } @@ -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 } diff --git a/Shared/Settings/BrowsingSettings.swift b/Shared/Settings/BrowsingSettings.swift index ea6548ff..3f804b9e 100644 --- a/Shared/Settings/BrowsingSettings.swift +++ b/Shared/Settings/BrowsingSettings.swift @@ -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 + interfaceSettings 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) } } diff --git a/Shared/Settings/SettingsView.swift b/Shared/Settings/SettingsView.swift index 5ef99a53..cecb3ccc 100644 --- a/Shared/Settings/SettingsView.swift +++ b/Shared/Settings/SettingsView.swift @@ -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: diff --git a/Shared/Views/OpenSettingsButton.swift b/Shared/Views/OpenSettingsButton.swift index 68e35c81..4607de33 100644 --- a/Shared/Views/OpenSettingsButton.swift +++ b/Shared/Views/OpenSettingsButton.swift @@ -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 diff --git a/Shared/YatteeApp.swift b/Shared/YatteeApp.swift index ccfa2cc3..ed85c78a 100644 --- a/Shared/YatteeApp.swift +++ b/Shared/YatteeApp.swift @@ -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) + } + } + } } diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index 994bcfdd..616e9d8a 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -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 = ""; }; 37599F33272B44000087F250 /* FavoritesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesModel.swift; sourceTree = ""; }; 37599F37272B4D740087F250 /* FavoriteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteButton.swift; sourceTree = ""; }; + 375B8AB228B580D300397B31 /* KeychainModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainModel.swift; sourceTree = ""; }; 375DFB5726F9DA010013F468 /* InstancesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesModel.swift; sourceTree = ""; }; 375E45F427B1976B00BA7902 /* MPVOGLView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPVOGLView.swift; sourceTree = ""; }; 375E45F727B1AC4700BA7902 /* PlayerControlsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsModel.swift; sourceTree = ""; }; @@ -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" */; diff --git a/Yattee.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Yattee.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e987479b..2e728edf 100644 --- a/Yattee.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Yattee.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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", diff --git a/tvOS/AccountSelectionView.swift b/tvOS/AccountSelectionView.swift index 3e50c6a6..e3138049 100644 --- a/tvOS/AccountSelectionView.swift +++ b/tvOS/AccountSelectionView.swift @@ -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? {