mirror of
https://github.com/yattee/yattee.git
synced 2026-07-31 04:12:03 +00:00
Show missing-credentials indicator for remote server sources
After reinstalling the app and importing sources from iCloud, remote server instances with Keychain-only credentials showed no indication that login is needed, unlike WebDAV/SMB sources. - Show the orange key icon for Yattee Server instances without stored basic auth credentials (auth is always required for this type) - Persist usesBasicAuth/usesAccountLogin flags on Instance (synced via iCloud) so missing Invidious/Piped/PeerTube credentials are detectable too; maintained on credential save/delete, login/logout, and legacy migration - Centralize the check in AppEnvironment.needsCredentials(for:) used by SourcesListView, MediaSourcesView, and SourceRow - Backfill the flags at launch from credentials currently in the Keychain so existing setups publish them without a re-login
This commit is contained in:
@@ -333,6 +333,10 @@ final class AppEnvironment {
|
||||
// Set up circular dependencies after all properties are initialized
|
||||
bgRefreshManager.setAppEnvironment(self)
|
||||
|
||||
// Backfill credential flags for instances created before the flags
|
||||
// existed, so existing setups publish them to iCloud without a re-login
|
||||
backfillCredentialFlags()
|
||||
|
||||
// Log device capabilities on startup for debugging
|
||||
HardwareCapabilities.shared.logCapabilities()
|
||||
|
||||
@@ -424,6 +428,47 @@ final class AppEnvironment {
|
||||
}
|
||||
}
|
||||
|
||||
/// Backfills `usesBasicAuth`/`usesAccountLogin` on instances that were created
|
||||
/// before these flags existed, based on credentials currently in the Keychain.
|
||||
/// Only ever sets the flags — a missing Keychain entry (e.g. right after a
|
||||
/// reinstall) must not clear a previously recorded flag.
|
||||
private func backfillCredentialFlags() {
|
||||
for instance in instancesManager.instances {
|
||||
if !instance.usesBasicAuth,
|
||||
instance.supportsHTTPBasicAuthProxy,
|
||||
basicAuthCredentialsManager.hasCredentials(for: instance) {
|
||||
instancesManager.setUsesBasicAuth(true, for: instance)
|
||||
}
|
||||
|
||||
if !instance.usesAccountLogin,
|
||||
instance.supportsAuthentication,
|
||||
let manager = credentialsManager(for: instance),
|
||||
manager.isLoggedIn(for: instance) {
|
||||
instancesManager.setUsesAccountLogin(true, for: instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an instance is known to require credentials that are missing from
|
||||
/// the Keychain (e.g. after reinstalling the app and importing sources from iCloud).
|
||||
/// - Yattee Server always requires basic auth; other types require it when
|
||||
/// `usesBasicAuth` was recorded.
|
||||
/// - Invidious/Piped account logins are checked when `usesAccountLogin` was recorded.
|
||||
func needsCredentials(for instance: Instance) -> Bool {
|
||||
if instance.type == .yatteeServer || instance.usesBasicAuth,
|
||||
!basicAuthCredentialsManager.hasCredentials(for: instance) {
|
||||
return true
|
||||
}
|
||||
|
||||
if instance.usesAccountLogin,
|
||||
let manager = credentialsManager(for: instance),
|
||||
!manager.isLoggedIn(for: instance) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - Preview/Testing Support
|
||||
|
||||
@MainActor
|
||||
|
||||
@@ -174,6 +174,26 @@ final class InstancesManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Records whether an instance sits behind HTTP Basic Auth.
|
||||
/// Persisted (and synced to iCloud) so missing Keychain credentials can be
|
||||
/// detected after a reinstall.
|
||||
func setUsesBasicAuth(_ value: Bool, for instance: Instance) {
|
||||
guard let index = instances.firstIndex(where: { $0.id == instance.id }),
|
||||
instances[index].usesBasicAuth != value else { return }
|
||||
instances[index].usesBasicAuth = value
|
||||
saveInstances()
|
||||
}
|
||||
|
||||
/// Records whether the user has logged into an account on an instance.
|
||||
/// Persisted (and synced to iCloud) so missing Keychain credentials can be
|
||||
/// detected after a reinstall.
|
||||
func setUsesAccountLogin(_ value: Bool, for instance: Instance) {
|
||||
guard let index = instances.firstIndex(where: { $0.id == instance.id }),
|
||||
instances[index].usesAccountLogin != value else { return }
|
||||
instances[index].usesAccountLogin = value
|
||||
saveInstances()
|
||||
}
|
||||
|
||||
/// Sets the given instance as the primary (first) instance.
|
||||
func setPrimary(_ instance: Instance) {
|
||||
LoggingService.shared.debug("[InstancesManager] setPrimary called for: \(instance.displayName)", category: .general)
|
||||
|
||||
@@ -76,6 +76,16 @@ struct Instance: Identifiable, Codable, Hashable, Sendable {
|
||||
/// Whether to route video streams through this instance instead of connecting directly to YouTube CDN.
|
||||
var proxiesVideos: Bool
|
||||
|
||||
/// Whether this instance sits behind an HTTP Basic Auth reverse proxy.
|
||||
/// Set when basic auth credentials are stored, so a missing Keychain entry
|
||||
/// (e.g. after reinstalling and importing sources from iCloud) can be detected.
|
||||
/// Yattee Server always requires basic auth regardless of this flag.
|
||||
var usesBasicAuth: Bool
|
||||
|
||||
/// Whether the user has logged into an account on this instance (Invidious/Piped).
|
||||
/// Set on login, so a missing Keychain credential can be detected after reinstall.
|
||||
var usesAccountLogin: Bool
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(
|
||||
@@ -87,7 +97,9 @@ struct Instance: Identifiable, Codable, Hashable, Sendable {
|
||||
dateAdded: Date = Date(),
|
||||
apiKey: String? = nil,
|
||||
allowInvalidCertificates: Bool = false,
|
||||
proxiesVideos: Bool = false
|
||||
proxiesVideos: Bool = false,
|
||||
usesBasicAuth: Bool = false,
|
||||
usesAccountLogin: Bool = false
|
||||
) {
|
||||
self.id = id
|
||||
self.type = type
|
||||
@@ -98,6 +110,8 @@ struct Instance: Identifiable, Codable, Hashable, Sendable {
|
||||
self.apiKey = apiKey
|
||||
self.allowInvalidCertificates = allowInvalidCertificates
|
||||
self.proxiesVideos = proxiesVideos
|
||||
self.usesBasicAuth = usesBasicAuth
|
||||
self.usesAccountLogin = usesAccountLogin
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
@@ -111,6 +125,8 @@ struct Instance: Identifiable, Codable, Hashable, Sendable {
|
||||
apiKey = try container.decodeIfPresent(String.self, forKey: .apiKey)
|
||||
allowInvalidCertificates = try container.decode(Bool.self, forKey: .allowInvalidCertificates)
|
||||
proxiesVideos = try container.decodeIfPresent(Bool.self, forKey: .proxiesVideos) ?? false
|
||||
usesBasicAuth = try container.decodeIfPresent(Bool.self, forKey: .usesBasicAuth) ?? false
|
||||
usesAccountLogin = try container.decodeIfPresent(Bool.self, forKey: .usesAccountLogin) ?? false
|
||||
}
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
@@ -221,13 +221,16 @@ final class LegacyDataMigrationService {
|
||||
password: basicAuthCredentials.password,
|
||||
for: instance
|
||||
)
|
||||
instancesManager.setUsesBasicAuth(true, for: instance)
|
||||
}
|
||||
invidiousCredentialsManager.setCredential(credential, for: instance)
|
||||
instancesManager.setUsesAccountLogin(true, for: instance)
|
||||
|
||||
case .piped:
|
||||
credential = try await pipedAPI.login(username: username, password: password, instance: instance)
|
||||
addInstanceIfNeeded(instance)
|
||||
pipedCredentialsManager.setCredential(credential, for: instance)
|
||||
instancesManager.setUsesAccountLogin(true, for: instance)
|
||||
|
||||
default:
|
||||
throw APIError.notSupported
|
||||
@@ -264,6 +267,7 @@ final class LegacyDataMigrationService {
|
||||
password: basicAuthCredentials.password,
|
||||
for: instance
|
||||
)
|
||||
instancesManager.setUsesBasicAuth(true, for: instance)
|
||||
}
|
||||
|
||||
removeLegacyInstance(item)
|
||||
|
||||
@@ -40,6 +40,12 @@ struct MediaSourcesView: View {
|
||||
appEnvironment?.settingsManager.listStyle ?? .inset
|
||||
}
|
||||
|
||||
/// Whether a remote server instance requires credentials that are missing from
|
||||
/// the Keychain (e.g. after reinstalling and importing sources from iCloud).
|
||||
private func needsCredentials(_ instance: Instance) -> Bool {
|
||||
appEnvironment?.needsCredentials(for: instance) ?? false
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
#if os(tvOS)
|
||||
@@ -517,6 +523,12 @@ struct MediaSourcesView: View {
|
||||
Text("\(instance.type.displayName) - \(instance.url.host ?? instance.url.absoluteString)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
if needsCredentials(instance) {
|
||||
Label(String(localized: "sources.status.authRequired"), systemImage: "key.fill")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -753,7 +753,8 @@ struct AddRemoteServerView: View {
|
||||
type: type,
|
||||
url: url,
|
||||
name: name.isEmpty ? nil : name,
|
||||
allowInvalidCertificates: allowInvalidCertificates
|
||||
allowInvalidCertificates: allowInvalidCertificates,
|
||||
usesBasicAuth: true
|
||||
)
|
||||
|
||||
appEnvironment.basicAuthCredentialsManager.setCredentials(
|
||||
@@ -783,14 +784,16 @@ struct AddRemoteServerView: View {
|
||||
|
||||
// For other instance types: optionally store HTTP Basic Auth credentials
|
||||
// (used when the instance is fronted by a reverse proxy that requires basic auth).
|
||||
let hasBasicAuth = !basicAuthUsername.isEmpty && !basicAuthPassword.isEmpty
|
||||
let instance = Instance(
|
||||
type: type,
|
||||
url: url,
|
||||
name: name.isEmpty ? nil : name,
|
||||
allowInvalidCertificates: allowInvalidCertificates
|
||||
allowInvalidCertificates: allowInvalidCertificates,
|
||||
usesBasicAuth: hasBasicAuth
|
||||
)
|
||||
|
||||
if !basicAuthUsername.isEmpty, !basicAuthPassword.isEmpty {
|
||||
if hasBasicAuth {
|
||||
appEnvironment.basicAuthCredentialsManager.setCredentials(
|
||||
username: basicAuthUsername,
|
||||
password: basicAuthPassword,
|
||||
|
||||
@@ -345,6 +345,7 @@ private struct EditRemoteServerContent: View {
|
||||
.fullScreenCover(isPresented: $showLoginSheet) {
|
||||
InstanceLoginView(instance: instance) { credential in
|
||||
appEnvironment?.credentialsManager(for: instance)?.setCredential(credential, for: instance)
|
||||
appEnvironment?.instancesManager.setUsesAccountLogin(true, for: instance)
|
||||
isLoggedIn = true
|
||||
}
|
||||
}
|
||||
@@ -352,6 +353,7 @@ private struct EditRemoteServerContent: View {
|
||||
.sheet(isPresented: $showLoginSheet) {
|
||||
InstanceLoginView(instance: instance) { credential in
|
||||
appEnvironment?.credentialsManager(for: instance)?.setCredential(credential, for: instance)
|
||||
appEnvironment?.instancesManager.setUsesAccountLogin(true, for: instance)
|
||||
isLoggedIn = true
|
||||
}
|
||||
}
|
||||
@@ -409,6 +411,7 @@ private struct EditRemoteServerContent: View {
|
||||
|
||||
private func logout() {
|
||||
appEnvironment?.credentialsManager(for: instance)?.deleteCredential(for: instance)
|
||||
appEnvironment?.instancesManager.setUsesAccountLogin(false, for: instance)
|
||||
isLoggedIn = false
|
||||
}
|
||||
|
||||
@@ -445,7 +448,9 @@ private struct EditRemoteServerContent: View {
|
||||
}
|
||||
|
||||
private func performSave() {
|
||||
var updated = instance
|
||||
// Start from the manager's latest copy so flags updated while this sheet
|
||||
// was open (e.g. usesAccountLogin set by the login sheet) are preserved.
|
||||
var updated = appEnvironment?.instancesManager.instances.first { $0.id == instance.id } ?? instance
|
||||
updated.name = name.isEmpty ? nil : name
|
||||
updated.isEnabled = isEnabled
|
||||
updated.allowInvalidCertificates = allowInvalidCertificates
|
||||
@@ -458,14 +463,17 @@ private struct EditRemoteServerContent: View {
|
||||
if hadStoredBasicAuth {
|
||||
appEnvironment?.basicAuthCredentialsManager.deleteCredentials(for: instance)
|
||||
}
|
||||
updated.usesBasicAuth = false
|
||||
} else if !basicAuthUsername.isEmpty, !basicAuthPassword.isEmpty {
|
||||
appEnvironment?.basicAuthCredentialsManager.setCredentials(
|
||||
username: basicAuthUsername,
|
||||
password: basicAuthPassword,
|
||||
for: instance
|
||||
)
|
||||
updated.usesBasicAuth = true
|
||||
} else if hadStoredBasicAuth, instance.type != .yatteeServer {
|
||||
appEnvironment?.basicAuthCredentialsManager.deleteCredentials(for: instance)
|
||||
updated.usesBasicAuth = false
|
||||
}
|
||||
|
||||
appEnvironment?.instancesManager.update(updated)
|
||||
|
||||
@@ -32,6 +32,13 @@ struct SourceRow: View {
|
||||
return status == .authFailed || status == .authRequired
|
||||
}
|
||||
|
||||
/// Whether a remote server instance requires credentials that are missing from
|
||||
/// the Keychain (e.g. after reinstalling and importing sources from iCloud).
|
||||
private var needsCredentials: Bool {
|
||||
guard case .remoteServer(let instance) = source else { return false }
|
||||
return appEnvironment?.needsCredentials(for: instance) ?? false
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
#if os(tvOS)
|
||||
Button(action: onEdit) {
|
||||
@@ -92,7 +99,7 @@ struct SourceRow: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var statusView: some View {
|
||||
if needsPassword {
|
||||
if needsPassword || needsCredentials {
|
||||
Label(String(localized: "sources.status.authRequired"), systemImage: "key.fill")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.orange)
|
||||
|
||||
@@ -403,9 +403,19 @@ struct SourcesListView: View {
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
||||
/// Whether a remote server instance requires credentials that are missing from
|
||||
/// the Keychain (e.g. after reinstalling and importing sources from iCloud).
|
||||
private func needsCredentials(_ instance: Instance) -> Bool {
|
||||
appEnvironment?.needsCredentials(for: instance) ?? false
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func instanceStatusView(for instance: Instance) -> some View {
|
||||
if let status = instancesManager?.status(for: instance) {
|
||||
if needsCredentials(instance) {
|
||||
Label(String(localized: "sources.status.authRequired"), systemImage: "key.fill")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.orange)
|
||||
} else if let status = instancesManager?.status(for: instance) {
|
||||
switch status {
|
||||
case .authFailed:
|
||||
Label(String(localized: "sources.status.authFailed"), systemImage: "exclamationmark.triangle.fill")
|
||||
|
||||
Reference in New Issue
Block a user