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>
142 lines
3.6 KiB
Swift
142 lines
3.6 KiB
Swift
import ManaAuthUI
|
|
import ManaCore
|
|
import SwiftUI
|
|
|
|
/// Reader für Feed-Items (Pool-Artikel, noch nicht in der Lese-Liste).
|
|
///
|
|
/// FeedItems kommen schon mit Readability-extrahiertem `content` aus
|
|
/// dem Pool, also können wir sie direkt im Native-Reader anzeigen ohne
|
|
/// Save-Roundtrip. Toolbar-Action „In Lese-Liste" macht den Save dann
|
|
/// explizit.
|
|
struct FeedItemReaderView: View {
|
|
let item: FeedItem
|
|
|
|
@Environment(AuthClient.self) private var auth
|
|
private var api: PagetaAPI { PagetaAPI(auth: auth) }
|
|
|
|
@State private var saveState: SaveState = .idle
|
|
|
|
enum SaveState: Equatable {
|
|
case idle
|
|
case saving
|
|
case saved
|
|
case error(String)
|
|
}
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
header
|
|
ForEach(paragraphs, id: \.self) { paragraph in
|
|
Text(paragraph)
|
|
.font(.body)
|
|
.foregroundStyle(PagetaTheme.foreground)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
}
|
|
.padding(.horizontal, 20)
|
|
.padding(.vertical, 16)
|
|
.frame(maxWidth: 720, alignment: .leading)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
.background(PagetaTheme.background)
|
|
.contentMargins(.bottom, 60, for: .scrollContent)
|
|
.navigationTitle("")
|
|
#if os(iOS)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
#endif
|
|
.toolbar { toolbarContent }
|
|
}
|
|
|
|
private var header: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack(spacing: 6) {
|
|
Text(item.topic.emoji)
|
|
Text(item.topic.label)
|
|
.font(.caption.weight(.medium))
|
|
.foregroundStyle(.secondary)
|
|
Text("•").foregroundStyle(.secondary)
|
|
Text(item.siteName)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
if let mins = item.readingTimeMinutes {
|
|
Text("•").foregroundStyle(.secondary)
|
|
Text(PagetaStrings.metaMinRead(mins))
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Spacer()
|
|
}
|
|
Text(item.title)
|
|
.font(.system(.title2, design: .serif, weight: .semibold))
|
|
.foregroundStyle(PagetaTheme.foreground)
|
|
if let author = item.author, !author.isEmpty {
|
|
Text("von \(author)")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
@ToolbarContentBuilder
|
|
private var toolbarContent: some ToolbarContent {
|
|
ToolbarItem(placement: .primaryAction) {
|
|
Menu {
|
|
Button {
|
|
Task { await save() }
|
|
} label: {
|
|
Label(saveLabel, systemImage: saveIcon)
|
|
}
|
|
.disabled(saveState == .saving || saveState == .saved)
|
|
if let url = URL(string: item.originalUrl) {
|
|
Link(destination: url) {
|
|
Label(PagetaStrings.actionOpenInBrowser, systemImage: "safari")
|
|
}
|
|
ShareLink(item: url) {
|
|
Label(PagetaStrings.actionShareShort, systemImage: "square.and.arrow.up")
|
|
}
|
|
}
|
|
} label: {
|
|
Image(systemName: "ellipsis")
|
|
}
|
|
}
|
|
}
|
|
|
|
private var saveLabel: String {
|
|
switch saveState {
|
|
case .idle: PagetaStrings.feedSaveToStack
|
|
case .saving: PagetaStrings.feedSaving
|
|
case .saved: "Gespeichert"
|
|
case .error: "Erneut versuchen"
|
|
}
|
|
}
|
|
|
|
private var saveIcon: String {
|
|
switch saveState {
|
|
case .idle, .error: "books.vertical"
|
|
case .saving: "ellipsis"
|
|
case .saved: "checkmark"
|
|
}
|
|
}
|
|
|
|
private var paragraphs: [String] {
|
|
item.content
|
|
.components(separatedBy: "\n\n")
|
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
.filter { !$0.isEmpty }
|
|
}
|
|
|
|
private func save() async {
|
|
guard let url = URL(string: item.originalUrl) else {
|
|
saveState = .error(PagetaStrings.feedInvalidUrl)
|
|
return
|
|
}
|
|
saveState = .saving
|
|
do {
|
|
try await api.saveArticle(url: url)
|
|
saveState = .saved
|
|
} catch {
|
|
saveState = .error(error.localizedDescription)
|
|
}
|
|
}
|
|
}
|