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