pageta-native/Sources/Features/FeedTabView.swift
Till JS 8d6fc1a666 i18n: pageta-native voll über PagetaStrings (Phase 4)
Alle UI-Strings der Feature-/App-Schicht ziehen aus mana/i18n
(Listen/Detail/Marker/Feed/Rückblick/Zusammenfassen/Settings/Account
+ Archiv/Bulk/Note-Sheet/Palette); Interpolation als generierte Funktionen.
Verwaisten-Check grün (0 hartkodierte UI-Strings). Onboarding bleibt
seine eigene SOT (mana/content/onboarding).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 16:06:55 +02:00

195 lines
5 KiB
Swift

import ManaAuthUI
import ManaComponentsUI
import ManaCore
import SwiftUI
struct FeedTabView: View {
@Environment(AuthClient.self) private var auth
@Environment(\.openURL) private var openURL
private var api: PagetaAPI {
PagetaAPI(auth: auth)
}
@State private var items: [FeedItem] = []
@State private var loadState: LoadState = .idle
enum LoadState: Equatable {
case idle
case loading
case loaded
case error(String)
}
var body: some View {
NavigationStack {
Group {
if items.isEmpty {
switch loadState {
case .idle, .loading:
ProgressView()
.accessibilityLabel(PagetaStrings.stateLoading)
.frame(maxWidth: .infinity, maxHeight: .infinity)
case let .error(message):
ContentUnavailableView(
PagetaStrings.feedLoadError,
systemImage: "exclamationmark.triangle",
description: Text(message)
)
case .loaded:
ContentUnavailableView(
PagetaStrings.feedEmptyTitle,
systemImage: "newspaper",
description: Text(PagetaStrings.feedEmptyBody)
)
}
} else {
List(items) { item in
NavigationLink {
FeedItemReaderView(item: item)
} label: {
FeedItemRow(item: item)
}
.listRowInsets(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16))
.swipeActions(edge: .trailing) {
Button {
Task { await saveToReadingList(item) }
} label: {
Label(PagetaStrings.feedSaveToStackShort, systemImage: "books.vertical")
}
.tint(PagetaTheme.primary)
}
.contextMenu {
Button {
Task { await saveToReadingList(item) }
} label: {
Label(PagetaStrings.feedSaveToStack, systemImage: "books.vertical")
}
Divider()
if let url = URL(string: item.originalUrl) {
Button {
openURL(url)
} label: {
Label(PagetaStrings.actionOpenOriginal, systemImage: "safari")
}
ShareLink(item: url) {
Label(PagetaStrings.actionShare, systemImage: "square.and.arrow.up")
}
}
Button {
PlatformClipboard.copy(item.originalUrl)
} label: {
Label(PagetaStrings.actionCopyLink, systemImage: "link")
}
}
}
.listStyle(.plain)
.macReadingWidth()
.refreshable {
await loadFeed()
}
}
}
.navigationTitle("Feed")
.toolbar {
#if os(macOS)
ToolbarItem(placement: .primaryAction) {
Button {
Task { await loadFeed() }
} label: {
Label(PagetaStrings.actionRefresh, systemImage: "arrow.clockwise")
}
.keyboardShortcut("r", modifiers: .command)
.disabled(loadState == .loading)
}
#endif
}
.paperBackground()
.task {
if items.isEmpty {
await loadFeed()
}
}
}
}
private func loadFeed() async {
loadState = .loading
do {
let fetched = try await api.fetchFeed(limit: 50)
items = fetched
loadState = .loaded
} catch {
loadState = .error(error.localizedDescription)
}
}
private func saveToReadingList(_ item: FeedItem) async {
guard let url = URL(string: item.originalUrl) else { return }
// Best-effort — Fehler werden geloggt, UI bleibt unverändert
// (Item bleibt im Feed; Lese-Liste-Reload zieht's dann rüber).
do {
try await api.saveArticle(url: url)
Log.api.info("Saved feed item \(item.id, privacy: .public) to reading list")
} catch {
Log.api.error("Save feed item failed: \(error.localizedDescription, privacy: .public)")
}
}
}
struct FeedItemRow: View {
let item: FeedItem
private var accessibilityDescription: String {
var parts: [String] = []
parts.append(item.title)
parts.append(item.siteName)
if !item.topic.label.isEmpty { parts.append(item.topic.label) }
return parts.joined(separator: ", ")
}
var body: some View {
HStack(alignment: .top, spacing: 12) {
if let imageUrl = item.imageUrl, let url = URL(string: imageUrl) {
AsyncImage(url: url) { phase in
if let image = phase.image {
image.resizable().aspectRatio(contentMode: .fill)
} else {
// Adaptiver Placeholder — paper-cream wäre im
// Dark-Mode ein helles Quadrat.
Color.secondary.opacity(0.15)
}
}
.frame(width: 64, height: 64)
.clipShape(RoundedRectangle(cornerRadius: 6))
.accessibilityHidden(true)
}
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 6) {
Text(item.topic.emoji)
.accessibilityHidden(true)
Text(item.topic.label)
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
Text(item.siteName)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
}
Text(item.title)
.font(.body.weight(.medium))
.foregroundStyle(.primary)
.lineLimit(3)
if let excerpt = item.excerpt, !excerpt.isEmpty {
Text(excerpt)
.font(.callout)
.foregroundStyle(.secondary)
.lineLimit(2)
}
}
}
.padding(.vertical, 4)
.accessibilityElement(children: .ignore)
.accessibilityLabel(accessibilityDescription)
}
}