Files
yattee/Yattee/Views/Settings/TranslationContributorsView.swift
Arkadiusz Fal eec39445b4 Give the empty tvOS translators screen a focusable Done button
If the bundled translator list fails to load the screen is a bare
ContentUnavailableView, leaving the tvOS remote nothing to focus.
2026-10-17 08:59:14 +02:00

127 lines
3.7 KiB
Swift

//
// TranslationContributorsView.swift
// Yattee
//
// Displays Weblate translation contributors.
//
import NukeUI
import SwiftUI
struct TranslationContributorsView: View {
#if os(tvOS)
@Environment(\.dismiss) private var dismiss
#endif
@State private var contributors: [TranslationContributor] = []
var body: some View {
content
.opaqueWindowBackground()
.navigationTitle(String(localized: "settings.translators.title"))
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.task {
contributors = TranslationContributorsLoader.load()
}
}
// MARK: - Content
@ViewBuilder
private var content: some View {
if contributors.isEmpty {
ContentUnavailableView {
Label(String(localized: "settings.translators.empty"), systemImage: "globe")
} actions: {
#if os(tvOS)
// Nothing else here is focusable; without it the remote is stuck.
Button(String(localized: "common.done")) {
dismiss()
}
#endif
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
contributorsList
}
}
private var contributorsList: some View {
SettingsFormContainer {
SettingsFormSection(footer: "settings.translators.section.footer") {
ForEach(contributors) { contributor in
contributorRow(contributor)
}
}
}
}
private func contributorRow(_ contributor: TranslationContributor) -> some View {
#if os(tvOS)
Button {} label: {
contributorRowContent(contributor)
}
#else
contributorRowContent(contributor)
#endif
}
private func contributorRowContent(_ contributor: TranslationContributor) -> some View {
HStack(spacing: 12) {
// Avatar
LazyImage(url: contributor.gravatarURL) { state in
ZStack {
Circle()
.fill(.quaternary)
.overlay {
if state.image == nil {
Text(String(contributor.displayName.prefix(1).uppercased()))
.font(.subheadline)
.fontWeight(.medium)
.foregroundStyle(.secondary)
}
}
if let image = state.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
}
}
}
.frame(width: 40, height: 40)
.clipShape(Circle())
// Name and languages
VStack(alignment: .leading, spacing: 2) {
Text(contributor.displayName)
.font(.body)
.fontWeight(.semibold)
Text(contributor.languageSummary())
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
}
Spacer()
// Total contributions badge
Text("\(contributor.totalContributions)")
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.secondary)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(.quaternary, in: Capsule())
}
}
}
#Preview {
NavigationStack {
TranslationContributorsView()
}
}