From 3441280599f18238f513400a42f7b03e2db4ad4f Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:24:05 +0200 Subject: [PATCH] feat(apple): send a transfer to a device from the share panel Send to a device now sits alongside the QR code, NFC, and export actions, since an offer is another way to deliver the same invitation. Picking a device pushes the existing transfer rather than re-sharing the files. The picker lists only devices holding a live grant, so nothing offered there can fail on tap, and it distinguishes accepted from waiting for that device to open the app. Also fixes the deprecated SF Symbol and the two Sendable warnings introduced with the contacts screen: the sections now talk to the model directly rather than storing view callbacks that a Binding setter has to convert. --- apple/Tests/ContactsModelTests.swift | 40 +++++++++++ apple/Tests/Fakes.swift | 10 +++ apple/VniDrop/App/RootView.swift | 2 +- apple/VniDrop/Core/CoreGateway.swift | 5 ++ apple/VniDrop/Core/CoreRepository.swift | 12 ++++ .../Features/Contacts/ContactPrompts.swift | 2 +- .../Features/Contacts/ContactsModel.swift | 26 +++++++ .../Features/Contacts/ContactsScreen.swift | 49 ++++++------- .../Features/Contacts/DevicePickerSheet.swift | 71 +++++++++++++++++++ apple/VniDrop/Features/Send/SendScreen.swift | 7 +- .../Features/Send/TransferDetailsView.swift | 7 +- .../Features/Send/TransferShareActions.swift | 14 ++++ .../Features/Settings/SettingsScreen.swift | 2 +- localization/strings.json | 62 ++++++++++++++++ .../composeResources/values-de/strings.xml | 4 ++ .../composeResources/values-es/strings.xml | 4 ++ .../composeResources/values-fr/strings.xml | 4 ++ .../composeResources/values-it/strings.xml | 4 ++ .../composeResources/values-nl/strings.xml | 4 ++ .../composeResources/values-pl/strings.xml | 4 ++ .../composeResources/values-pt/strings.xml | 4 ++ .../composeResources/values-ru/strings.xml | 4 ++ .../composeResources/values/strings.xml | 4 ++ 23 files changed, 310 insertions(+), 35 deletions(-) create mode 100644 apple/VniDrop/Features/Contacts/DevicePickerSheet.swift diff --git a/apple/Tests/ContactsModelTests.swift b/apple/Tests/ContactsModelTests.swift index e0ee2a7..223ba88 100644 --- a/apple/Tests/ContactsModelTests.swift +++ b/apple/Tests/ContactsModelTests.swift @@ -374,6 +374,46 @@ final class ContactsModelTests: XCTestCase { XCTAssertEqual(model.state.heldOffers.map(\.offerId), ["held-1"]) } + /// Offering an existing transfer reuses it rather than creating another. + func testOfferingAnExistingTransferReportsAcceptance() async { + let gateway = FakeCoreGateway() + gateway.offerTransferResult = .success( + ContactSendOutcome( + share: Share( + transferId: 7, ticket: "vnd1:x", transferName: "doc", + contentHash: "h", fileCount: 1, totalSize: 2 + ), + delivered: true + ) + ) + let (model, _) = makeModel(gateway) + + let delivered = await model.offerTransfer(transferId: 7, to: contact("peer")) + + XCTAssertTrue(delivered) + XCTAssertEqual(gateway.offeredTransfers.map(\.transferId), [7]) + XCTAssertEqual(gateway.offeredTransfers.map(\.endpointId), ["peer"]) + } + + /// An offer to a closed device is reported as waiting, not accepted. + func testOfferingToAClosedDeviceReportsItAsWaiting() async { + let gateway = FakeCoreGateway() + gateway.offerTransferResult = .success( + ContactSendOutcome( + share: Share( + transferId: 7, ticket: "vnd1:x", transferName: "doc", + contentHash: "h", fileCount: 1, totalSize: 2 + ), + delivered: false + ) + ) + let (model, _) = makeModel(gateway) + + let delivered = await model.offerTransfer(transferId: 7, to: contact("peer")) + + XCTAssertFalse(delivered) + } + func testUnreachableContactIsSurfacedForRepairing() async { let gateway = FakeCoreGateway() gateway.contactsResult = .success([contact("peer", canSend: false)]) diff --git a/apple/Tests/Fakes.swift b/apple/Tests/Fakes.swift index 925df28..11a9388 100644 --- a/apple/Tests/Fakes.swift +++ b/apple/Tests/Fakes.swift @@ -136,6 +136,16 @@ final class FakeCoreGateway: CoreGateway { sentToContacts.append(endpointId) return sendToContactResult } + private(set) var offeredTransfers: [(transferId: UInt64, endpointId: String)] = [] + var offerTransferResult: Result = .failure(TestError.unimplemented) + + func offerTransferToContact( + transferId: UInt64, + endpointId: String + ) async -> Result { + offeredTransfers.append((transferId, endpointId)) + return offerTransferResult + } func heldOffers() async -> Result<[HeldOfferModel], Error> { heldOffersResult } func pollContactsForOffers() async -> Result { pollCount += 1 diff --git a/apple/VniDrop/App/RootView.swift b/apple/VniDrop/App/RootView.swift index 19bc71f..a1de9ca 100644 --- a/apple/VniDrop/App/RootView.swift +++ b/apple/VniDrop/App/RootView.swift @@ -173,7 +173,7 @@ struct RootView: View { @ViewBuilder private func screen(for destination: AppDestination, windowClass: WindowClass) -> some View { switch destination { - case .send: SendScreen(model: sendModel, windowClass: windowClass) + case .send: SendScreen(model: sendModel, contacts: graph.contactsModel, windowClass: windowClass) case .receive: ReceiveScreen(model: receiveModel, windowClass: windowClass) case .settings: SettingsScreen(model: settingsModel, contacts: graph.contactsModel, windowClass: windowClass) diff --git a/apple/VniDrop/Core/CoreGateway.swift b/apple/VniDrop/Core/CoreGateway.swift index 1af3f03..f7ee8e8 100644 --- a/apple/VniDrop/Core/CoreGateway.swift +++ b/apple/VniDrop/Core/CoreGateway.swift @@ -66,6 +66,11 @@ protocol CoreGateway: AnyObject { transferName: String, senderName: String ) async -> Result + /// Offer an existing share to a remembered device, alongside its QR code. + func offerTransferToContact( + transferId: UInt64, + endpointId: String + ) async -> Result /// Transfers this device is holding for contacts that were not running. func heldOffers() async -> Result<[HeldOfferModel], Error> /// Ask remembered devices whether they hold anything for this one. diff --git a/apple/VniDrop/Core/CoreRepository.swift b/apple/VniDrop/Core/CoreRepository.swift index c96a113..797ceed 100644 --- a/apple/VniDrop/Core/CoreRepository.swift +++ b/apple/VniDrop/Core/CoreRepository.swift @@ -359,6 +359,18 @@ final class CoreRepository: ObservableObject, CoreGateway { } } + func offerTransferToContact( + transferId: UInt64, + endpointId: String + ) async -> Result { + await runCore { + let result = try self.requireCore().offerTransferToContact( + transferId: transferId, endpointId: endpointId + ) + return ContactSendOutcome(share: result.share.toModel(), delivered: result.delivered) + } + } + func heldOffers() async -> Result<[HeldOfferModel], Error> { await runCore { try self.requireCore().listHeldOffers().map { $0.toModel() } } } diff --git a/apple/VniDrop/Features/Contacts/ContactPrompts.swift b/apple/VniDrop/Features/Contacts/ContactPrompts.swift index 9eace1f..09fc280 100644 --- a/apple/VniDrop/Features/Contacts/ContactPrompts.swift +++ b/apple/VniDrop/Features/Contacts/ContactPrompts.swift @@ -110,7 +110,7 @@ private struct PairingSheet: View { var body: some View { VStack(spacing: 16) { - Image(systemSymbol: .laptopcomputerAndIphone) + Image(systemSymbol: .macbookAndIphone) .font(.system(size: 44)) .foregroundStyle(.tint) .padding(.top, 12) diff --git a/apple/VniDrop/Features/Contacts/ContactsModel.swift b/apple/VniDrop/Features/Contacts/ContactsModel.swift index d09da65..019e7a2 100644 --- a/apple/VniDrop/Features/Contacts/ContactsModel.swift +++ b/apple/VniDrop/Features/Contacts/ContactsModel.swift @@ -345,6 +345,32 @@ final class ContactsModel: ObservableObject { } } + /// Push an existing transfer to a remembered device. + /// + /// Returns whether it landed, so the caller can distinguish "accepted" from + /// "waiting for that device to open the app". + @discardableResult + func offerTransfer(transferId: UInt64, to contact: DeviceContact) async -> Bool { + state.busyEndpoints.insert(contact.endpointId) + defer { state.busyEndpoints.remove(contact.endpointId) } + + switch await repository.offerTransferToContact( + transferId: transferId, + endpointId: contact.endpointId + ) { + case .success(let outcome): + let text: UiText = outcome.delivered + ? .dynamic(L10n.Contacts.sentToDevice(device: contact.displayName)) + : .resource(L10n.Contacts.offerHeld) + messages.tryShow(UiMessage(text: text, tone: outcome.delivered ? .success : .info)) + await refresh() + return outcome.delivered + case .failure(let error): + messages.error(error) + return false + } + } + // MARK: - Management func setLabel(endpointId: String, label: String) async { diff --git a/apple/VniDrop/Features/Contacts/ContactsScreen.swift b/apple/VniDrop/Features/Contacts/ContactsScreen.swift index e894c62..77f9a79 100644 --- a/apple/VniDrop/Features/Contacts/ContactsScreen.swift +++ b/apple/VniDrop/Features/Contacts/ContactsScreen.swift @@ -63,22 +63,9 @@ struct ContactsScreen: View { } } - CollectOffersSection( - enabled: model.state.checkForOffersOnOpen, - isChecking: model.state.isCheckingForOffers, - onToggle: model.setCheckForOffersOnOpen, - onCheckNow: { - Task { - let collected = await model.collectWaitingOffers() - if collected == 0 { onNothingWaiting() } - } - } - ) + CollectOffersSection(model: model, onNothingWaiting: onNothingWaiting) - GrantLifetimeSection( - selection: model.state.grantLifetime, - onSelect: model.setGrantLifetime - ) + GrantLifetimeSection(model: model) if !model.state.contacts.isEmpty { Section { @@ -98,7 +85,7 @@ struct ContactsScreen: View { private struct ContactsEmptyState: View { var body: some View { VStack(spacing: 8) { - Image(systemSymbol: .laptopcomputerAndIphone) + Image(systemSymbol: .macbookAndIphone) .font(.system(size: 32)) .foregroundStyle(.tint) Text(String(localized: L10n.Contacts.emptyTitle)) @@ -162,27 +149,33 @@ private struct BlockedRow: View { } private struct CollectOffersSection: View { - let enabled: Bool - let isChecking: Bool - let onToggle: (Bool) -> Void - let onCheckNow: () -> Void + @ObservedObject var model: ContactsModel + let onNothingWaiting: () -> Void var body: some View { Section { Toggle( String(localized: L10n.Contacts.checkOnOpen), - isOn: Binding(get: { enabled }, set: onToggle) + isOn: Binding( + get: { model.state.checkForOffersOnOpen }, + set: { model.setCheckForOffersOnOpen($0) } + ) ) - Button(action: onCheckNow) { + Button { + Task { + let collected = await model.collectWaitingOffers() + if collected == 0 { onNothingWaiting() } + } + } label: { HStack { Text(String(localized: L10n.Contacts.checkNow)) - if isChecking { + if model.state.isCheckingForOffers { Spacer() ProgressView().controlSize(.small) } } } - .disabled(isChecking) + .disabled(model.state.isCheckingForOffers) } footer: { // The privacy cost is the point of the setting, so it is stated // where the switch is, not buried elsewhere. @@ -192,14 +185,16 @@ private struct CollectOffersSection: View { } private struct GrantLifetimeSection: View { - let selection: GrantLifetimeOption - let onSelect: (GrantLifetimeOption) -> Void + @ObservedObject var model: ContactsModel var body: some View { Section { Picker( String(localized: L10n.Contacts.grantLifetimeTitle), - selection: Binding(get: { selection }, set: onSelect) + selection: Binding( + get: { model.state.grantLifetime }, + set: { model.setGrantLifetime($0) } + ) ) { ForEach(GrantLifetimeOption.allCases) { option in Text(Self.label(option)).tag(option) diff --git a/apple/VniDrop/Features/Contacts/DevicePickerSheet.swift b/apple/VniDrop/Features/Contacts/DevicePickerSheet.swift new file mode 100644 index 0000000..e656bdc --- /dev/null +++ b/apple/VniDrop/Features/Contacts/DevicePickerSheet.swift @@ -0,0 +1,71 @@ +import SFSafeSymbols +import SwiftUI + +/// Picks a remembered device to send an existing transfer to. +/// +/// Offered next to the QR code as another way to deliver the same invitation, +/// not as a second share of the same files. +struct DevicePickerSheet: View { + @ObservedObject var model: ContactsModel + let transferId: UInt64 + @Environment(\.dismiss) private var dismiss + + /// Only devices holding a live grant: the rest cannot be reached until they + /// are paired again, so offering them here would fail on tap. + private var reachable: [DeviceContact] { + model.state.contacts.filter(\.canSend) + } + + var body: some View { + NavigationStack { + Group { + if reachable.isEmpty { + ContentUnavailableView { + Label( + String(localized: L10n.Contacts.pickDeviceTitle), + systemSymbol: .macbookAndIphone + ) + } description: { + Text(String(localized: L10n.Contacts.pickDeviceEmpty)) + } + } else { + List(reachable) { contact in + Button { + Task { + await model.offerTransfer(transferId: transferId, to: contact) + dismiss() + } + } label: { + HStack { + VStack(alignment: .leading, spacing: 2) { + Text(contact.displayName) + Text(contact.shortFingerprint) + .font(.caption.monospaced()) + .foregroundStyle(.secondary) + } + Spacer() + if model.state.busyEndpoints.contains(contact.endpointId) { + ProgressView().controlSize(.small) + } + } + } + .disabled(!model.state.busyEndpoints.isEmpty) + } + } + } + .navigationTitle(Text(String(localized: L10n.Contacts.pickDeviceTitle))) + #if os(iOS) + .navigationBarTitleDisplayMode(.inline) + #endif + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(String(localized: L10n.Button.cancel)) { dismiss() } + } + } + } + .task { await model.refresh() } + #if os(macOS) + .frame(minWidth: 380, minHeight: 320) + #endif + } +} diff --git a/apple/VniDrop/Features/Send/SendScreen.swift b/apple/VniDrop/Features/Send/SendScreen.swift index f596ed5..b9bb251 100644 --- a/apple/VniDrop/Features/Send/SendScreen.swift +++ b/apple/VniDrop/Features/Send/SendScreen.swift @@ -5,6 +5,7 @@ import SFSafeSymbols /// with the composer and detail panels as native sheets and delete as an alert. struct SendScreen: View { @ObservedObject var model: SendModel + @ObservedObject var contacts: ContactsModel let windowClass: WindowClass /// Transfer pending an inline (list-level) delete confirmation. @@ -60,7 +61,7 @@ struct SendScreen: View { onDismissed: model.shareSheetDidDismiss ) { if let shareTarget { - TransferSharePanel(model: model, transfer: shareTarget) + TransferSharePanel(model: model, contacts: contacts, transfer: shareTarget) } } } @@ -93,7 +94,7 @@ struct SendScreen: View { /// alert attached here so they present from the detail's own context (presenting /// modals from the parent stack while a detail is pushed is unreliable on macOS). private func detailView(for transfer: Transfer) -> some View { - TransferDetailsView(model: model, transfer: transfer, events: model.coreState.events) + TransferDetailsView(model: model, contacts: contacts, transfer: transfer, events: model.coreState.events) .adaptiveDrawer( isPresented: Binding(get: { model.state.detailPanel != nil }, set: { _ in }), windowClass: windowClass, @@ -101,7 +102,7 @@ struct SendScreen: View { onDismissed: model.shareSheetDidDismiss ) { if let panel = model.state.detailPanel { - DetailPanelContent(model: model, transfer: transfer, panel: panel) + DetailPanelContent(model: model, contacts: contacts, transfer: transfer, panel: panel) } } .alert( diff --git a/apple/VniDrop/Features/Send/TransferDetailsView.swift b/apple/VniDrop/Features/Send/TransferDetailsView.swift index a65dd10..a655858 100644 --- a/apple/VniDrop/Features/Send/TransferDetailsView.swift +++ b/apple/VniDrop/Features/Send/TransferDetailsView.swift @@ -6,6 +6,7 @@ import CoreImage.CIFilterBuiltins struct TransferDetailsView: View { @ObservedObject var model: SendModel + @ObservedObject var contacts: ContactsModel let transfer: Transfer let events: [CoreEventModel] @State private var showStopConfirmation = false @@ -129,6 +130,7 @@ private struct DetailDestination: View { struct DetailPanelContent: View { @ObservedObject var model: SendModel + @ObservedObject var contacts: ContactsModel let transfer: Transfer let panel: TransferDetailPanel @@ -146,7 +148,7 @@ struct DetailPanelContent: View { onAccept: model.acceptReceiver ) case .share: - TransferSharePanel(model: model, transfer: transfer) + TransferSharePanel(model: model, contacts: contacts, transfer: transfer) } } } @@ -296,6 +298,7 @@ private struct ReceiverRow: View { struct TransferSharePanel: View { @Environment(\.vniColors) private var colors @ObservedObject var model: SendModel + @ObservedObject var contacts: ContactsModel let transfer: Transfer var body: some View { @@ -309,7 +312,7 @@ struct TransferSharePanel: View { .font(VniType.bodySmall).foregroundStyle(colors.foregroundLighter) .frame(maxWidth: .infinity) } - ShareActionsView(model: model, transfer: transfer, ticket: ticket) + ShareActionsView(model: model, contacts: contacts, transfer: transfer, ticket: ticket) case .preparing: Text(String(localized: L10n.Transfer.eventPreparing)).foregroundStyle(colors.foregroundLighter) case .unavailable: diff --git a/apple/VniDrop/Features/Send/TransferShareActions.swift b/apple/VniDrop/Features/Send/TransferShareActions.swift index 325f342..7733942 100644 --- a/apple/VniDrop/Features/Send/TransferShareActions.swift +++ b/apple/VniDrop/Features/Send/TransferShareActions.swift @@ -20,14 +20,25 @@ protocol TransferShareActions: AnyObject { struct ShareActionsView: View { @Environment(\.vniColors) private var colors @ObservedObject var model: SendModel + @ObservedObject var contacts: ContactsModel let transfer: Transfer let ticket: String @State private var actions: TransferShareActions = makePlatformShareActions() @State private var writingNfc = false + @State private var choosingDevice = false var body: some View { VStack(spacing: 12) { + // Sending straight to a remembered device is another way to deliver + // this same invitation, so it belongs with the other delivery + // methods rather than in a separate flow. + if contacts.state.contacts.contains(where: \.canSend) { + SecondaryButton( + title: String(localized: L10n.Contacts.sendToDevice), + action: { choosingDevice = true } + ) + } if actions.nfcAvailability != .hidden { SecondaryButton( title: writingNfc ? String(localized: L10n.Transfer.nfcWaiting) : String(localized: L10n.Button.writeNfc), @@ -57,5 +68,8 @@ struct ShareActionsView: View { }, enabled: actions.canUseNativeShare) } .onDisappear { actions.cancelNfcWrite() } + .sheet(isPresented: $choosingDevice) { + DevicePickerSheet(model: contacts, transferId: transfer.transferId) + } } } diff --git a/apple/VniDrop/Features/Settings/SettingsScreen.swift b/apple/VniDrop/Features/Settings/SettingsScreen.swift index efec59b..c2e4176 100644 --- a/apple/VniDrop/Features/Settings/SettingsScreen.swift +++ b/apple/VniDrop/Features/Settings/SettingsScreen.swift @@ -57,7 +57,7 @@ struct SettingsScreen: View { } NavigationLink(value: SettingsSection.contacts) { SettingsRow( - icon: .laptopcomputerAndIphone, + icon: .macbookAndIphone, title: String(localized: L10n.Contacts.title), value: contacts.state.contacts.isEmpty ? nil diff --git a/localization/strings.json b/localization/strings.json index 5b1f178..b908b23 100644 --- a/localization/strings.json +++ b/localization/strings.json @@ -5222,6 +5222,68 @@ "nl": "Annuleer de overdracht om die in te trekken.", "ru": "Отмените передачу, чтобы отозвать её." } + }, + "contacts_send_to_device": { + "context": "Transfer share panel: action that sends this transfer straight to a remembered device.", + "translations": { + "en": "Send to a device", + "fr": "Envoyer à un appareil", + "es": "Enviar a un dispositivo", + "it": "Invia a un dispositivo", + "de": "An ein Gerät senden", + "pt": "Enviar para um dispositivo", + "pl": "Wyślij do urządzenia", + "nl": "Naar een apparaat sturen", + "ru": "Отправить на устройство" + } + }, + "contacts_pick_device_title": { + "context": "Device picker sheet: title when choosing which remembered device to send a transfer to.", + "translations": { + "en": "Choose a device", + "fr": "Choisir un appareil", + "es": "Elegir un dispositivo", + "it": "Scegli un dispositivo", + "de": "Gerät auswählen", + "pt": "Escolher um dispositivo", + "pl": "Wybierz urządzenie", + "nl": "Kies een apparaat", + "ru": "Выберите устройство" + } + }, + "contacts_pick_device_empty": { + "context": "Device picker sheet: shown when no remembered device can currently be sent to.", + "translations": { + "en": "No device can be reached right now. Remembered devices appear here after a transfer.", + "fr": "Aucun appareil n’est joignable pour le moment. Les appareils enregistrés apparaissent ici après un transfert.", + "es": "Ningún dispositivo está disponible ahora. Los dispositivos guardados aparecen aquí tras una transferencia.", + "it": "Nessun dispositivo è raggiungibile ora. I dispositivi memorizzati compaiono qui dopo un trasferimento.", + "de": "Derzeit ist kein Gerät erreichbar. Gespeicherte Geräte erscheinen hier nach einer Übertragung.", + "pt": "Nenhum dispositivo está acessível agora. Os dispositivos guardados aparecem aqui após uma transferência.", + "pl": "Żadne urządzenie nie jest teraz dostępne. Zapamiętane urządzenia pojawią się tu po przesłaniu.", + "nl": "Er is nu geen apparaat bereikbaar. Onthouden apparaten verschijnen hier na een overdracht.", + "ru": "Сейчас ни одно устройство недоступно. Сохранённые устройства появятся здесь после передачи." + } + }, + "contacts_sent_to_device": { + "context": "Confirmation after a transfer was accepted by the device it was sent to. {device} = device name.", + "args": [ + { + "name": "device", + "type": "string" + } + ], + "translations": { + "en": "{device} accepted the transfer", + "fr": "{device} a accepté le transfert", + "es": "{device} aceptó la transferencia", + "it": "{device} ha accettato il trasferimento", + "de": "{device} hat die Übertragung angenommen", + "pt": "{device} aceitou a transferência", + "pl": "{device} zaakceptowało przesyłkę", + "nl": "{device} heeft de overdracht geaccepteerd", + "ru": "{device} принял передачу" + } } } } diff --git a/shared/src/commonMain/composeResources/values-de/strings.xml b/shared/src/commonMain/composeResources/values-de/strings.xml index 675746c..95749e9 100644 --- a/shared/src/commonMain/composeResources/values-de/strings.xml +++ b/shared/src/commonMain/composeResources/values-de/strings.xml @@ -344,6 +344,10 @@ Dieses Gerät ist nicht geöffnet. Die Übertragung wird beim nächsten Öffnen von VniDrop zugestellt. Wartet auf Zustellung Brechen Sie die Übertragung ab, um sie zurückzuziehen. + An ein Gerät senden + Gerät auswählen + Derzeit ist kein Gerät erreichbar. Gespeicherte Geräte erscheinen hier nach einer Übertragung. + %1$s hat die Übertragung angenommen %1$d Datei %1$d Dateien diff --git a/shared/src/commonMain/composeResources/values-es/strings.xml b/shared/src/commonMain/composeResources/values-es/strings.xml index a08d012..fbf3875 100644 --- a/shared/src/commonMain/composeResources/values-es/strings.xml +++ b/shared/src/commonMain/composeResources/values-es/strings.xml @@ -344,6 +344,10 @@ Ese dispositivo no está abierto. La transferencia se entregará la próxima vez que abra VniDrop. Pendientes de entrega Cancela la transferencia para retirarla. + Enviar a un dispositivo + Elegir un dispositivo + Ningún dispositivo está disponible ahora. Los dispositivos guardados aparecen aquí tras una transferencia. + %1$s aceptó la transferencia %1$d archivo %1$d archivos diff --git a/shared/src/commonMain/composeResources/values-fr/strings.xml b/shared/src/commonMain/composeResources/values-fr/strings.xml index 4194cd3..6d9f507 100644 --- a/shared/src/commonMain/composeResources/values-fr/strings.xml +++ b/shared/src/commonMain/composeResources/values-fr/strings.xml @@ -344,6 +344,10 @@ Cet appareil n’est pas ouvert. Le transfert sera remis à sa prochaine ouverture de VniDrop. En attente de remise Annulez le transfert pour le retirer. + Envoyer à un appareil + Choisir un appareil + Aucun appareil n’est joignable pour le moment. Les appareils enregistrés apparaissent ici après un transfert. + %1$s a accepté le transfert %1$d fichier %1$d fichiers diff --git a/shared/src/commonMain/composeResources/values-it/strings.xml b/shared/src/commonMain/composeResources/values-it/strings.xml index 729b7b3..dabd605 100644 --- a/shared/src/commonMain/composeResources/values-it/strings.xml +++ b/shared/src/commonMain/composeResources/values-it/strings.xml @@ -344,6 +344,10 @@ Quel dispositivo non è aperto. Il trasferimento verrà consegnato alla prossima apertura di VniDrop. In attesa di consegna Annulla il trasferimento per ritirarlo. + Invia a un dispositivo + Scegli un dispositivo + Nessun dispositivo è raggiungibile ora. I dispositivi memorizzati compaiono qui dopo un trasferimento. + %1$s ha accettato il trasferimento %1$d file %1$d file diff --git a/shared/src/commonMain/composeResources/values-nl/strings.xml b/shared/src/commonMain/composeResources/values-nl/strings.xml index 7f39a09..32c6159 100644 --- a/shared/src/commonMain/composeResources/values-nl/strings.xml +++ b/shared/src/commonMain/composeResources/values-nl/strings.xml @@ -344,6 +344,10 @@ Dat apparaat is niet geopend. De overdracht wordt bezorgd zodra het VniDrop weer opent. Wacht op bezorging Annuleer de overdracht om die in te trekken. + Naar een apparaat sturen + Kies een apparaat + Er is nu geen apparaat bereikbaar. Onthouden apparaten verschijnen hier na een overdracht. + %1$s heeft de overdracht geaccepteerd %1$d bestand %1$d bestanden diff --git a/shared/src/commonMain/composeResources/values-pl/strings.xml b/shared/src/commonMain/composeResources/values-pl/strings.xml index dd56ddd..42e49eb 100644 --- a/shared/src/commonMain/composeResources/values-pl/strings.xml +++ b/shared/src/commonMain/composeResources/values-pl/strings.xml @@ -344,6 +344,10 @@ To urządzenie nie jest otwarte. Przesyłka zostanie dostarczona przy następnym uruchomieniu VniDrop. Oczekuje na dostarczenie Anuluj przesyłkę, aby ją wycofać. + Wyślij do urządzenia + Wybierz urządzenie + Żadne urządzenie nie jest teraz dostępne. Zapamiętane urządzenia pojawią się tu po przesłaniu. + %1$s zaakceptowało przesyłkę %1$d plik %1$d pliki diff --git a/shared/src/commonMain/composeResources/values-pt/strings.xml b/shared/src/commonMain/composeResources/values-pt/strings.xml index c201221..b7228e8 100644 --- a/shared/src/commonMain/composeResources/values-pt/strings.xml +++ b/shared/src/commonMain/composeResources/values-pt/strings.xml @@ -344,6 +344,10 @@ Esse dispositivo não está aberto. A transferência será entregue da próxima vez que abrir o VniDrop. A aguardar entrega Cancele a transferência para a retirar. + Enviar para um dispositivo + Escolher um dispositivo + Nenhum dispositivo está acessível agora. Os dispositivos guardados aparecem aqui após uma transferência. + %1$s aceitou a transferência %1$d ficheiro %1$d ficheiros diff --git a/shared/src/commonMain/composeResources/values-ru/strings.xml b/shared/src/commonMain/composeResources/values-ru/strings.xml index 3f65363..42445f8 100644 --- a/shared/src/commonMain/composeResources/values-ru/strings.xml +++ b/shared/src/commonMain/composeResources/values-ru/strings.xml @@ -344,6 +344,10 @@ Это устройство не открыто. Передача будет доставлена при следующем запуске VniDrop. Ожидает доставки Отмените передачу, чтобы отозвать её. + Отправить на устройство + Выберите устройство + Сейчас ни одно устройство недоступно. Сохранённые устройства появятся здесь после передачи. + %1$s принял передачу %1$d файл %1$d файла diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index ba8ff93..2be10da 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -344,6 +344,10 @@ That device is not open. The transfer will be delivered the next time it opens VniDrop. Waiting to be delivered Cancel the transfer to withdraw it. + Send to a device + Choose a device + No device can be reached right now. Remembered devices appear here after a transfer. + %1$s accepted the transfer %1$d files %1$d file