Files
yattee/Yattee/Views/Settings/SourceRow.swift
Arkadiusz Fal 6994b9353a 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
2026-07-23 22:57:24 +02:00

140 lines
4.7 KiB
Swift

//
// SourceRow.swift
// Yattee
//
// Unified row view for displaying any source type in the sources list.
//
import SwiftUI
struct SourceRow: View {
let source: UnifiedSource
let onEdit: () -> Void
@Environment(\.appEnvironment) private var appEnvironment
/// Computed property that checks password status reactively for WebDAV sources
private var needsPassword: Bool {
guard case .fileSource(let mediaSource) = source,
mediaSource.type == .webdav else { return false }
return appEnvironment?.mediaSourcesManager.needsPassword(for: mediaSource) ?? false
}
/// Computed property that checks instance status for auth issues
private var instanceStatus: InstanceStatus? {
guard case .remoteServer(let instance) = source else { return nil }
return appEnvironment?.instancesManager.status(for: instance)
}
/// Whether this instance has auth issues (failed or required)
private var hasAuthIssue: Bool {
guard let status = instanceStatus else { return false }
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) {
rowContent
}
.buttonStyle(.card)
.accessibilityIdentifier(accessibilityId)
#else
rowContent
.contentShape(Rectangle())
.onTapGesture(perform: onEdit)
.accessibilityElement(children: .combine)
.accessibilityIdentifier(accessibilityId)
#endif
}
private var rowContent: some View {
HStack {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(source.name)
.font(.headline)
if !source.isEnabled {
Text(String(localized: "sources.status.disabled"))
.font(.caption)
.foregroundStyle(.secondary)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(.quaternary)
.clipShape(Capsule())
}
}
Text(source.urlDisplayString)
.font(.caption)
.foregroundStyle(.secondary)
statusView
}
Spacer()
}
}
/// Generates a unique accessibility identifier for the source row.
/// Format: sources.row.<type>.<host>
private var accessibilityId: String {
switch source {
case .remoteServer(let instance):
let host = instance.url.host ?? "unknown"
return "sources.row.\(instance.type.rawValue).\(host)"
case .fileSource(let mediaSource):
let identifier = mediaSource.url.host ?? mediaSource.name.replacingOccurrences(of: " ", with: "_")
return "sources.row.\(mediaSource.type.rawValue).\(identifier)"
}
}
@ViewBuilder
private var statusView: some View {
if needsPassword || needsCredentials {
Label(String(localized: "sources.status.authRequired"), systemImage: "key.fill")
.font(.caption2)
.foregroundStyle(.orange)
} else if hasAuthIssue {
// Show auth issue for remote servers (Yattee Server auth failed)
if instanceStatus == .authFailed {
Label(String(localized: "sources.status.authFailed"), systemImage: "exclamationmark.triangle.fill")
.font(.caption2)
.foregroundStyle(.orange)
} else {
Label(String(localized: "sources.status.authRequired"), systemImage: "key.fill")
.font(.caption2)
.foregroundStyle(.orange)
}
}
}
}
// MARK: - Preview
#Preview {
List {
SourceRow(
source: .remoteServer(Instance(type: .invidious, url: URL(string: "https://invidious.example.com")!)),
onEdit: {}
)
SourceRow(
source: .fileSource(.webdav(name: "My NAS", url: URL(string: "https://nas.local:5006")!)),
onEdit: {}
)
SourceRow(
source: .fileSource(.localFolder(name: "Movies", url: URL(fileURLWithPath: "/Users/user/Movies"))),
onEdit: {}
)
}
.appEnvironment(.preview)
}