Files
vnidrop/apple/VniDrop/Features/Receive/ReceiveScreen.swift
cdricms ef42875ddb feat(apple): generate type-safe L10n accessors and migrate all key literals
Replaces every stringly-typed localization key in the Apple app with
compile-time-checked accessors generated from localization/strings.json.
A mistyped key is now a build error instead of a silent fallback to the
raw key at runtime. The runtime path is unchanged: plain keys are
String.LocalizationValue constants resolved with String(localized:) and
Apple's String Catalog still does the lookup; keys with arguments become
typed, named functions applying args through String(format:).

Generator: new renderSwiftAccessors emits apple/VniDrop/Generated/L10n.swift,
wired into generate. Renamed generic arg1/arg2 tokens on four keys to
semantic names (receiver, transferName, deviceId) and updated their context
notes; positional output is unchanged so .xcstrings (bar the 4 comments)
and the Android XML regenerate identical.

Migration: every key-carrying value flipped to String.LocalizationValue
end to end, resolved only at the leaf. Zero key literals and zero
LocalizedStringKey remain in app or test code. macOS build passes; iOS
test run pending.
2026-07-23 17:12:43 +02:00

150 lines
4.6 KiB
Swift

import SwiftUI
/// Receive screen, rebuilt on native SwiftUI. A grouped `List` of received
/// transfers with swipe-to-delete, and the acquisition flow as a native sheet.
struct ReceiveScreen: View {
@ObservedObject var model: ReceiveModel
let windowClass: WindowClass
private var transfers: [Transfer] {
model.coreState.transfers.filter { $0.direction == .receive }
}
private var deletable: [Transfer] {
transfers.filter { $0.status.isTerminalReceiveHistory }
}
var body: some View {
NavigationStack {
Group {
if transfers.isEmpty {
emptyState
} else {
history
}
}
.navigationTitle(Text(String(localized: L10n.Receive.title)))
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: model.openAcquisition) {
Label(String(localized: L10n.Button.receiveFiles), systemImage: "plus")
}
}
if !deletable.isEmpty {
ToolbarItem(placement: .primaryAction) {
Button(role: .destructive, action: model.requestClearHistory) {
Label(String(localized: L10n.Receive.clearHistory), systemImage: "trash")
}
}
}
}
}
.adaptiveDrawer(
isPresented: Binding(get: { model.state.isAcquisitionOpen }, set: { _ in }),
windowClass: windowClass,
onDismiss: model.dismissAcquisition
) {
if model.state.ticket.isEmpty {
ReceiveMethodPanel(model: model)
} else {
InvitationReviewPanel(model: model)
}
}
.alert(
Text(String(localized: clearAllPending ? L10n.Receive.clearHistoryTitle : L10n.Receive.deleteHistoryTitle)),
isPresented: Binding(get: { model.state.historyDeleteTarget != nil }, set: { if !$0 { Task { @MainActor in model.dismissHistoryDelete() } } })
) {
Button(String(localized: L10n.Button.cancel), role: .cancel, action: model.dismissHistoryDelete)
Button(String(localized: clearAllPending ? L10n.Receive.clearHistory : L10n.Button.deleteTransfer),
role: .destructive, action: model.confirmHistoryDelete)
} message: {
historyDeleteMessage
}
}
private var history: some View {
List {
Section {
ForEach(transfers) { transfer in
ReceiveTransferRow(
transfer: transfer,
progress: progressForTransfer(events: model.coreState.events, transferId: transfer.transferId)
)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
if transfer.status.isTerminalReceiveHistory {
Button(role: .destructive) {
model.requestDeleteHistoryItem(transfer.transferId)
} label: {
Label(String(localized: L10n.Button.deleteTransfer), systemImage: "trash")
}
}
}
}
} header: {
Text(String(localized: L10n.Receive.historyTitle))
} footer: {
Text(String(localized: L10n.Receive.newSubtitle))
}
}
}
private var emptyState: some View {
ContentUnavailableView {
Label(String(localized: L10n.Receive.emptyTitle), systemImage: "tray.and.arrow.down")
} description: {
Text(String(localized: L10n.Receive.emptyBody))
} actions: {
Button(action: model.openAcquisition) {
Label(String(localized: L10n.Button.receiveFiles), systemImage: "plus")
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
}
}
private var clearAllPending: Bool { model.state.historyDeleteTarget == .all }
@ViewBuilder
private var historyDeleteMessage: some View {
if let target = model.state.historyDeleteTarget {
if target == .all {
Text(String(localized: L10n.Receive.clearHistoryDescription))
} else {
Text(L10n.Receive.deleteHistoryDescription(
transferName: transferName(for: target) ?? String(localized: L10n.Receive.unknownTransfer)))
}
}
}
private func transferName(for target: ReceiveHistoryDeleteTarget) -> String? {
if case .transfer(let id) = target {
return transfers.first { $0.transferId == id }?.transferName
}
return nil
}
}
private struct ReceiveTransferRow: View {
let transfer: Transfer
let progress: TransferProgress?
var body: some View {
HStack(spacing: 12) {
Image(systemName: "doc")
.foregroundStyle(.secondary)
.frame(width: 40, height: 40)
.background(.quaternary, in: RoundedRectangle(cornerRadius: 9))
VStack(alignment: .leading, spacing: 3) {
Text(transfer.transferName ?? String(localized: L10n.Receive.unknownTransfer))
.font(.body).lineLimit(1)
Text("\(formatBytes(transfer.totalSize)) · \(statusLabel(transfer.status))")
.font(.caption).foregroundStyle(.secondary)
if transfer.status == .receiving, let progress {
ProgressRow(labelKey: progress.labelKey, progress: progress.progress, detail: progress.detail)
.padding(.top, 2)
}
}
Spacer(minLength: 0)
}
}
}