diff --git a/NearbyShare/Connection/InboundNearbyConnection.swift b/NearbyShare/Connection/InboundNearbyConnection.swift index 0a8ba3f..412da68 100644 --- a/NearbyShare/Connection/InboundNearbyConnection.swift +++ b/NearbyShare/Connection/InboundNearbyConnection.swift @@ -539,6 +539,12 @@ class InboundNearbyConnection: NearbyConnection { let acceptAutomatically = isAuthenticated || Settings.sharedInstance.automaticallyAcceptFiles DispatchQueue.main.async { + if !NearbyConnectionManager.shared.claimIncomingTransfer(id: self.id) { + log("[InboundNearbyConnection \(self.id)] Another transfer is active. Rejecting incoming transfer.") + self.rejectTransfer() + return + } + if acceptAutomatically { self.delegate?.obtainedUserConsentAutomatically(transfer: metadata, device: self.remoteDeviceInfo!, connection: self) self.submitUserConsent(accepted: true, trustDevice: false) diff --git a/NearbyShare/Connection/NearbyConnectionManager.swift b/NearbyShare/Connection/NearbyConnectionManager.swift index 940cd24..5385420 100644 --- a/NearbyShare/Connection/NearbyConnectionManager.swift +++ b/NearbyShare/Connection/NearbyConnectionManager.swift @@ -29,6 +29,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb private var tcpListener: NWListener private var mdnsServices: [NetService] = [] private var incomingConnections: [String: InboundNearbyConnection] = [:] + private var activeIncomingTransferID: String? private var foundServices: [String: FoundServiceInfo] = [:] private var outboundAppDelegates: [OutboundAppDelegate] = [] private var inboundAppDelegates: [InboundAppDelegate] = [] @@ -340,6 +341,9 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb func connectionWasTerminated(connection: InboundNearbyConnection, savedFiles: [URL], error: Error?) { + if activeIncomingTransferID == connection.id { + activeIncomingTransferID = nil + } incomingConnections.removeValue(forKey: connection.id) if !connection.wasUserRejected { @@ -481,6 +485,14 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb info.connection.cancel() } } + + func claimIncomingTransfer(id: String) -> Bool { + if let activeIncomingTransferID, activeIncomingTransferID != id { + return false + } + activeIncomingTransferID = id + return true + } private func endpointID(for service: NWBrowser.Result) -> String? { diff --git a/NearbyShare/Model/DataModel.swift b/NearbyShare/Model/DataModel.swift index e5413bd..4052f95 100644 --- a/NearbyShare/Model/DataModel.swift +++ b/NearbyShare/Model/DataModel.swift @@ -6,6 +6,7 @@ // import Foundation +import UniformTypeIdentifiers import Network import CryptoKit import SwiftECC @@ -144,28 +145,57 @@ public struct TransferMetadata { if let textTitle = textDescription { return textTitle } else if files.count == 1 { - return files[0].name + return fileTypeSummary(for: files[0]) } else { return String.localizedStringWithFormat("NFiles".localized(), files.count) } } + + private func fileTypeSummary(for file: FileMetadata) -> String { + let mimeType = file.mimeType.lowercased() + let ext = URL(fileURLWithPath: file.name).pathExtension.lowercased() + let type = UTType(mimeType: mimeType) ?? UTType(filenameExtension: ext) + + if let type, type.conforms(to: .pdf) { + return UTType.pdf.localizedDescription ?? "PDF" + } + if let type, type.conforms(to: .image) { + return UTType.image.localizedDescription ?? "Image" + } + if let type, type.conforms(to: .video) || type.conforms(to: .movie) { + return UTType.video.localizedDescription ?? "Video" + } + if let type, type.conforms(to: .audio) { + return UTType.audio.localizedDescription ?? "Audio" + } + if let type, type.conforms(to: .text) { + return UTType.text.localizedDescription ?? "Text" + } + if let type, type.conforms(to: .archive) { + return UTType.archive.localizedDescription ?? "Archive" + } + if let type, let description = type.localizedDescription { + return description + } + return "File" + } - public func getDescription(deviceName: String, alreadyAccepted: Bool) -> String { + public func getDescription(deviceName: String) -> String { let fileStr = getSummary() switch type { case .file: - return String(format: (alreadyAccepted ? "DeviceCurrentlySendingFiles" : "DeviceSendingFiles").localized(), arguments: [deviceName, fileStr]) + return "DeviceSendingFiles".localized(with: fileStr, deviceName) case .text: - return String(format: (alreadyAccepted ? "DeviceCurrentlySendingText" : "DeviceSendingText").localized(), arguments: [deviceName, fileStr]) + return "DeviceSendingText".localized(with: deviceName) case .url: - return String(format: (alreadyAccepted ? "DeviceCurrentlySendingUrl" : "DeviceSendingUrl").localized(), arguments: [deviceName, fileStr]) + return "DeviceSendingUrl".localized(with: deviceName) case .wifiPassword: - return String(format: (alreadyAccepted ? "DeviceCurrentlySendingWifiPassword" : "DeviceSendingWifiPassword").localized(), arguments: [deviceName, fileStr]) + return "DeviceSendingWifiPassword".localized(with: deviceName) } } diff --git a/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9e4b29a..e987b65 100644 --- a/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -52,7 +52,7 @@ "location" : "https://github.com/leonboe1/LUI.git", "state" : { "branch" : "main", - "revision" : "87f9d26e499b49838dc9135e063d10e1701480e9" + "revision" : "b35bcccf183c075d8d58d5b5763702b29a779b32" } }, { @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "959a56cda79509e66abd367a6498951b656aa460", - "version" : "1.34.0" + "revision" : "c5ab62237f21cad094812719a1bbe29443407c5f", + "version" : "1.34.1" } }, { @@ -124,7 +124,7 @@ "location" : "https://github.com/siteline/swiftui-introspect", "state" : { "branch" : "main", - "revision" : "69d97e5463727a67a5aef8db2986667d3bd83a83" + "revision" : "379cbeb697ef528b24c56c59f77a8cfc394ae793" } }, { diff --git a/QuickDrop.xcodeproj/xcshareddata/xcschemes/QuickDrop.xcscheme b/QuickDrop.xcodeproj/xcshareddata/xcschemes/QuickDrop.xcscheme index a660787..d2ce80f 100644 --- a/QuickDrop.xcodeproj/xcshareddata/xcschemes/QuickDrop.xcscheme +++ b/QuickDrop.xcodeproj/xcshareddata/xcschemes/QuickDrop.xcscheme @@ -34,7 +34,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "de" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/QuickDrop/AppDelegate.swift b/QuickDrop/AppDelegate.swift index 3f3f387..a0bc8d9 100644 --- a/QuickDrop/AppDelegate.swift +++ b/QuickDrop/AppDelegate.swift @@ -329,7 +329,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele // Ensure the window is always on top NSApp.activate(ignoringOtherApps: true) window.makeKeyAndOrderFront(nil) - window.level = .normal + window.level = .floating } diff --git a/QuickDrop/Localizable.xcstrings b/QuickDrop/Localizable.xcstrings index 65a8627..7f37a17 100644 --- a/QuickDrop/Localizable.xcstrings +++ b/QuickDrop/Localizable.xcstrings @@ -2097,6 +2097,472 @@ } } }, + "Archive" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "أرشيف" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arxiu" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archiv" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arkiv" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archiv" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αρχείο zip" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archive" + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archive" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archive" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archive" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archivo comprimido" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archivo comprimido" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arkisto" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archive" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archive" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "ארכיון" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "आर्काइव" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arhiva" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archívum" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arsip" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archivio" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "アーカイブ" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "아카이브" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arkaib" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arkiv" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archief" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archiwum" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arquivo" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arquivo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arhivă" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Архив" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arkiv" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "แฟ้มเก็บถาวร" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arşiv" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Архів" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lưu trữ" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "归档" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "壓縮檔" + } + } + } + }, + "Audio" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "صوت" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Àudio" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lyd" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ήχος" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio" + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ääni" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "שמע" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ऑडियो" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hang" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オーディオ" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "오디오" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lyd" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Áudio" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Áudio" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Audio" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Аудио" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ljud" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "เสียง" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ses" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Аудіо" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Âm thanh" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "音频" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "音訊" + } + } + } + }, "AutoAcceptFromThisDevice" : { "extractionState" : "manual", "localizations" : { @@ -6055,6 +6521,239 @@ } } }, + "CopiedToClipboard" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تم النسخ إلى الحافظة" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiat al portapapers" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zkopírováno do schránky" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopieret til udklipsholderen" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "In die Zwischenablage kopiert" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αντιγράφηκε στο πρόχειρο" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copied to Clipboard" + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copied to Clipboard" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copied to Clipboard" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copied to Clipboard" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiado al portapapeles" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiado al portapapeles" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopioitu leikepöydälle" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copié dans le presse-papiers" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copié dans le presse-papiers" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "הועתק ללוח" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्लिपबोर्ड में कॉपी किया गया" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopirano u međuspremnik" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vágólapra másolva" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disalin ke Papan Klip" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiato negli appunti" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クリップボードにコピーしました" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "클립보드에 복사됨" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disalin ke Papan Klip" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopiert til utklippstavlen" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gekopieerd naar klembord" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skopiowano do schowka" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiado para a área de transferência" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiado para a área de transferência" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copiat în clipboard" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скопировано в буфер" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopierat till Urklipp" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "คัดลอกไปยังคลิปบอร์ดแล้ว" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Panoya Kopyalandı" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скопійовано до буфера" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã sao chép vào clipboard" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已复制到剪贴板" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已複製到剪貼簿" + } + } + } + }, "CouldNotOpenDocumentsFolder" : { "extractionState" : "manual", "localizations" : { @@ -6987,938 +7686,6 @@ } } }, - "DeviceCurrentlySendingFiles" : { - "extractionState" : "manual", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ يرسل لك %2$@" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ t'està enviant %2$@" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ vám odesílá %2$@" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ sender dig %2$@" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ sendet %2$@" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ο/η %1$@ σας στέλνει το %2$@" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ is sending you %2$@" - } - }, - "en-AU" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ is sharing %2$@ with you" - } - }, - "en-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ is sending you %2$@" - } - }, - "en-GB" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ is sending you %2$@" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ te está enviando %2$@" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ te está enviando %2$@" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ lähettää sinulle %2$@" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ vous envoie %2$@" - } - }, - "fr-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ vous envoie %2$@" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ שולח לך %2$@" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ आपको %2$@ भेज रहा है" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ vam šalje %2$@" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ éppen küldi neked: %2$@" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ mengirimkan %2$@ kepada Anda" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ ti sta inviando %2$@" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ が %2$@ を送信中" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@이(가) %2$@을(를) 보내는 중입니다" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ sedang menghantar anda %2$@" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ sender deg %2$@" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ stuurt je %2$@" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ przesyła do Ciebie %2$@" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ está enviando %2$@ para você" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ está a enviar-te %2$@" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ îți trimite %2$@" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ отправляет вам %2$@" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ skickar dig %2$@" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ กำลังส่ง %2$@ ให้คุณ" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@, size %2$@ gönderiyor" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ надсилає вам %2$@" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ đang gửi bạn %2$@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ 正在向你发送 %2$@" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ 正在向您傳送 %2$@" - } - } - } - }, - "DeviceCurrentlySendingText" : { - "extractionState" : "manual", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "قام %1$@ بإدراج \"%2$@\" في الحافظة الخاصة بك." - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ ha inserit \"%2$@\" al teu porta-retalls." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ vložil \"%2$@\" do vaší schránky." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ indsatte \"%2$@\" i udklipsholderen." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ hat \"%2$@\" in deine Zwischenablage eingefügt." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ο/η %1$@ πρόσθεσε \"%2$@\" στο πρόχειρό σας." - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ inserted \"%2$@\" to your clipboard." - } - }, - "en-AU" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ placed \"%2$@\" on your clipboard." - } - }, - "en-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ inserted \"%2$@\" into your clipboard." - } - }, - "en-GB" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ added \"%2$@\" to your clipboard." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ ha insertado \"%2$@\" en tu portapapeles." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ insertó \"%2$@\" en tu portapapeles." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ lisäsi \"%2$@\" leikepöydällesi." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ a inséré \"%2$@\" dans votre presse-papiers." - } - }, - "fr-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ a inséré \"%2$@\" dans votre presse-papiers." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ הכניס \"%2$@\" ללוח שלך." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ ने \"%2$@\" को आपके क्लिपबोर्ड में डाला।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ je umetnuo/la \"%2$@\" u vaš međuspremnik." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ beillesztette a(z) \"%2$@\" szöveget a vágólapodra." - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ memasukkan \"%2$@\" ke clipboard Anda." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ ha inserito \"%2$@\" negli appunti." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ が \"%2$@\" をクリップボードに挿入しました。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@이(가) \"%2$@\"을(를) 클립보드에 삽입했습니다." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ memasukkan \"%2$@\" ke dalam papan klip anda." - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ la inn \"%2$@\" i utklippstavlen din." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ heeft \"%2$@\" in je klembord geplaatst." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ wstawiono \"%2$@\" do schowka." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ inseriu \"%2$@\" na sua área de transferência." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ inseriu \"%2$@\" na sua área de transferência." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ a introdus \"%2$@\" în clipboard-ul tău." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ вставил \"%2$@\" в ваш буфер обмена." - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ infogade \"%2$@\" i ditt urklipp." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ ได้แทรก \"%2$@\" ไปยังคลิปบอร์ดของคุณแล้ว" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ \"%2$@\" panonuza ekledi." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ вставив \"%2$@\" до вашого буфера обміну." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ đã chèn \"%2$@\" vào bảng ghi tạm của bạn." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ 已将 \"%2$@\" 插入到你的剪贴板。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ 已將 \"%2$@\" 插入您的剪貼簿。" - } - } - } - }, - "DeviceCurrentlySendingUrl" : { - "extractionState" : "manual", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "قام %1$@ بفتح \"%2$@\"." - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ ha obert \"%2$@\"." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ otevřel \"%2$@\"." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ åbnede \"%2$@\"." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ hat \"%2$@\" geöffnet." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ο/η %1$@ άνοιξε \"%2$@\"." - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ opened \"%2$@\"." - } - }, - "en-AU" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ opened \"%2$@\"." - } - }, - "en-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ opened \"%2$@.\"" - } - }, - "en-GB" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ opened \"%2$@\"." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ ha abierto \"%2$@\"." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ abrió \"%2$@\"." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ avasi \"%2$@\"." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ a ouvert \"%2$@\"." - } - }, - "fr-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ a ouvert \"%2$@\"." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ פתח \"%2$@\"." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ ने \"%2$@\" खोल दिया।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ je otvorio/la \"%2$@\"." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ megnyitotta a(z) \"%2$@\" hivatkozást." - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ membuka \"%2$@\"." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ ha aperto \"%2$@\"." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ が \"%2$@\" を開きました。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@이(가) \"%2$@\"을(를) 열었습니다." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ membuka \"%2$@\"." - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ åpnet \"%2$@\"." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ heeft \"%2$@\" geopend." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ otworzył \"%2$@\"." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ abriu \"%2$@\"." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ abriu \"%2$@\"." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ a deschis \"%2$@\"." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ открыл \"%2$@\"." - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ öppnade \"%2$@\"." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ ได้เปิด \"%2$@\" แล้ว" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ \"%2$@\" açtı." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ відкрив \"%2$@\"." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ đã mở \"%2$@\"." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ 已打开 \"%2$@\"。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ 開啟了 \"%2$@\"。" - } - } - } - }, - "DeviceCurrentlySendingWifiPassword" : { - "extractionState" : "manual", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ أضاف كلمة مرور \"%2$@\" إلى الحافظة الخاصة بك." - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ ha inserit la contrasenya de \"%2$@\" al teu porta-retalls." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ vložil heslo k „%2$@“ do vaší schránky." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ har indsat kodeordet for \"%2$@\" på dit udklipsholder." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ hat das Passwort von „%2$@“ in deine Zwischenablage eingefügt." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ο/Η %1$@ εισήγαγε τον κωδικό πρόσβασης του \"%2$@\" στο πρόχειρό σας." - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ inserted the password for \"%2$@\" into your clipboard." - } - }, - "en-AU" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ inserted the password for \"%2$@\" into your clipboard." - } - }, - "en-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ inserted the password of \"%2$@\" into your clipboard." - } - }, - "en-GB" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ inserted the password of \"%2$@\" into your clipboard." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ insertó la contraseña de \"%2$@\" en tu portapapeles." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ insertó la contraseña de \"%2$@\" en tu portapapeles." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ lisäsi \"%2$@\"-salasanan leikepöydällesi." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ a inséré le mot de passe de « %2$@ » dans votre presse-papiers." - } - }, - "fr-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ a inséré le mot de passe de « %2$@ » dans votre presse-papiers." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ הכניס את הסיסמה של \"%2$@\" ללוח שלך." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ ने \"%2$@\" का पासवर्ड आपके क्लिपबोर्ड में डाला।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ je umetnuo lozinku za \"%2$@\" u vaš međuspremnik." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ a(z) „%2$@” jelszavát bemásolta a vágólapodra." - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ memasukkan kata sandi \"%2$@\" ke papan klip Anda." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ ha inserito la password di \"%2$@\" negli appunti." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@が「%2$@」のパスワードをクリップボードに追加しました。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@이(가) \"%2$@\"의 비밀번호를 클립보드에 삽입했습니다." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ telah memasukkan kata laluan \"%2$@\" ke papan klip anda." - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ har satt inn passordet til \"%2$@\" på utklippstavlen din." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ heeft het wachtwoord van \"%2$@\" aan je klembord toegevoegd." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ wstawił hasło do „%2$@” do Twojego schowka." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ inseriu a senha de \"%2$@\" na sua área de transferência." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ inseriu a palavra-passe de \"%2$@\" na sua área de transferência." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ a introdus parola pentru \"%2$@\" în clipboard-ul tău." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ скопировал пароль от «%2$@» в ваш буфер обмена." - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ har lagt till lösenordet för \"%2$@\" till ditt urklipp." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ ได้คัดลอกรหัสผ่านของ \"%2$@\" ไปยังคลิปบอร์ดของคุณแล้ว" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@, \"%2$@\" parolasını panonuza kopyaladı." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ вставив пароль від \"%2$@\" у ваш буфер обміну." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ đã chèn mật khẩu của \"%2$@\" vào clipboard của bạn." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ 已将“%2$@”的密码插入到你的剪贴板。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ 已將「%2$@」的密碼插入到您的剪貼簿。" - } - } - } - }, "DeviceName" : { "extractionState" : "manual", "localizations" : { @@ -8386,234 +8153,235 @@ } }, "DeviceSendingFiles" : { + "comment" : "E.g. „Photos from Google Pixel 6 Pro“", "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ يرسل لك %2$@" + "value" : "%1$@ من %2$@" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ t'està enviant %2$@" + "value" : "%1$@ de %2$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ vám odesílá %2$@" + "value" : "%1$@ z %2$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ sender dig %2$@" + "value" : "%1$@ fra %2$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ möchte dir %2$@ senden" + "value" : "%1$@ von %2$@" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Ο/η %1$@ σας στέλνει το %2$@" + "value" : "%1$@ από %2$@" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ is sending you %2$@" + "value" : "%1$@ from %2$@" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ is sharing %2$@ with you" + "value" : "%1$@ from %2$@" } }, "en-CA" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ is sending you %2$@" + "value" : "%1$@ from %2$@" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ is sending you %2$@" + "value" : "%1$@ from %2$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ te quiere enviar %2$@" + "value" : "%1$@ de %2$@" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ te está enviando %2$@" + "value" : "%1$@ de %2$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ lähettää sinulle %2$@" + "value" : "%1$@ laitteesta %2$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ vous envoie %2$@" + "value" : "%1$@ de %2$@" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ vous envoie %2$@" + "value" : "%1$@ de %2$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ שולח לך %2$@" + "value" : "%1$@ מ-%2$@" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ आपको %2$@ भेज रहा है" + "value" : "%2$@ से %1$@" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ vam šalje %2$@" + "value" : "%1$@ s %2$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ a következőt küldi: %2$@" + "value" : "%1$@ – %2$@ készülékről" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ mengirimkan %2$@ kepada Anda" + "value" : "%1$@ dari %2$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ ti sta mandando %2$@" + "value" : "%1$@ da %2$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@が%2$@を送信しています" + "value" : "%2$@の%1$@" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@에서 %2$@을(를) 공유하려고 합니다." + "value" : "%2$@의 %1$@" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ sedang menghantar anda %2$@" + "value" : "%1$@ dari %2$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ sender deg %2$@" + "value" : "%1$@ fra %2$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ stuurt je %2$@" + "value" : "%1$@ van %2$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ przesyła do Ciebie %2$@" + "value" : "%1$@ od %2$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ está te enviando %2$@" + "value" : "%1$@ de %2$@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ está a enviar %2$@" + "value" : "%1$@ de %2$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ vă trimite %2$@" + "value" : "%1$@ de la %2$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ отправляет Вам %2$@" + "value" : "%1$@ от %2$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ skickar dig %2$@" + "value" : "%1$@ från %2$@" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ กำลังส่งไฟล์ให้คุณ %2$@" + "value" : "%1$@ จาก %2$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@, size %2$@ gönderiyor" + "value" : "%2$@ kaynağından %1$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ відправляє Вам %2$@" + "value" : "%1$@ від %2$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ đang gửi bạn %2$@" + "value" : "%1$@ từ %2$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ 正向你发送 %2$@" + "value" : "来自 %2$@ 的 %1$@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ 正在向您傳送 %2$@" + "value" : "%2$@ 的 %1$@" } } } @@ -8624,229 +8392,229 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ يريد إدراج \"%2$@\" في الحافظة الخاصة بك." + "value" : "نص من %1$@" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ vol inserir \"%2$@\" al teu porta-retalls." + "value" : "Text de %1$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ chce vložit \"%2$@\" do vaší schránky." + "value" : "Text z %1$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ ønsker at indsætte \"%2$@\" i din udklipsholder." + "value" : "Tekst fra %1$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ möchte \"%2$@\" in Deine Zwischenablage einfügen." + "value" : "Text von %1$@" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Ο/η %1$@ επιθυμεί να προσθέσει \"%2$@\" στο πρόχειρό σας." + "value" : "Κείμενο από %1$@" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ wants to insert \"%2$@\" to your clipboard." + "value" : "Text from %1$@" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ wants to paste \"%2$@\" on your clipboard." + "value" : "Text from %1$@" } }, "en-CA" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ wants to insert \"%2$@\" into your clipboard." + "value" : "Text from %1$@" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ wants to add \"%2$@\" to your clipboard." + "value" : "Text from %1$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ quiere insertar \"%2$@\" en tu portapapeles." + "value" : "Texto de %1$@" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ quiere insertar \"%2$@\" en tu portapapeles." + "value" : "Texto de %1$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ haluaa lisätä \"%2$@\" leikepöydällesi." + "value" : "Teksti laitteesta %1$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ souhaite insérer \"%2$@\" dans votre presse-papiers." + "value" : "Texte de %1$@" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ souhaite insérer \"%2$@\" dans votre presse-papiers." + "value" : "Texte de %1$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ רוצה להכניס \"%2$@\" ללוח שלך." + "value" : "טקסט מ-%1$@" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ \"%2$@\" को आपके क्लिपबोर्ड में डालना चाहता है।" + "value" : "%1$@ से टेक्स्ट" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ želi umetnuti \"%2$@\" u vaš međuspremnik." + "value" : "Tekst s %1$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ be szeretné illeszteni a(z) \"%2$@\" elemet a vágólapodra." + "value" : "Szöveg innen: %1$@" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ ingin memasukkan \"%2$@\" ke clipboard Anda." + "value" : "Teks dari %1$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ vuole inserire \"%2$@\" negli appunti." + "value" : "Testo da %1$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@があなたのクリップボードに「%2$@」を挿入しようとしています。" + "value" : "%1$@のテキスト" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@님이 클립보드에 \"%2$@\"을(를) 삽입하려고 합니다." + "value" : "%1$@의 텍스트" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ ingin memasukkan \"%2$@\" ke dalam papan klip anda." + "value" : "Teks dari %1$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ vil legge inn \"%2$@\" i utklippstavlen din." + "value" : "Tekst fra %1$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ wil \"%2$@\" naar uw klembord kopiëren." + "value" : "Tekst van %1$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ chce wstawić \"%2$@\" do Twojego schowka." + "value" : "Tekst od %1$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ deseja inserir \"%2$@\" na sua área de transferência." + "value" : "Texto de %1$@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ quer inserir \"%2$@\" na sua área de transferência." + "value" : "Texto de %1$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ dorește să insereze \"%2$@\" în clipboard-ul tău." + "value" : "Text de la %1$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ хочет вставить \"%2$@\" в ваш буфер обмена." + "value" : "Текст от %1$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ vill infoga \"%2$@\" till ditt urklipp." + "value" : "Text från %1$@" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ ต้องการเพิ่ม \"%2$@\" ไปยังคลิปบอร์ดของคุณ" + "value" : "ข้อความจาก %1$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ \"%2$@\" panonuza eklemek istiyor." + "value" : "%1$@ kaynağından metin" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ хоче вставити \"%2$@\" у ваш буфер обміну." + "value" : "Текст від %1$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ muốn chèn \"%2$@\" vào bảng ghi tạm của bạn." + "value" : "Văn bản từ %1$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ 想将“%2$@”插入到你的剪贴板。" + "value" : "来自 %1$@ 的文本" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ 想將 \"%2$@\" 插入您的剪貼簿。" + "value" : "%1$@ 的文字" } } } @@ -8857,229 +8625,229 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ يريد فتح \"%2$@\" على هذا الجهاز." + "value" : "رابط من %1$@" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ vol obrir \"%2$@\" en aquest dispositiu." + "value" : "URL de %1$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ chce otevřít \"%2$@\" na tomto zařízení." + "value" : "URL z %1$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ ønsker at åbne \"%2$@\" på denne enhed." + "value" : "URL fra %1$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ möchte \"%2$@\" auf diesem Gerät öffnen." + "value" : "URL von %1$@" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Ο/η %1$@ θέλει να ανοίξει \"%2$@\" στη συσκευή σας." + "value" : "URL από %1$@" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ wants to open \"%2$@\" on this device." + "value" : "URL from %1$@" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ wants to open \"%2$@\" here." + "value" : "URL from %1$@" } }, "en-CA" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ wants to open \"%2$@\" on your device." + "value" : "URL from %1$@" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ wants to open \"%2$@\" on this device." + "value" : "URL from %1$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ quiere abrir \"%2$@\" en este dispositivo." + "value" : "URL de %1$@" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ quiere abrir \"%2$@\" en este dispositivo." + "value" : "URL de %1$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ haluaa avata \"%2$@\" tällä laitteella." + "value" : "URL laitteesta %1$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ souhaite ouvrir \"%2$@\" sur cet appareil." + "value" : "URL de %1$@" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ souhaite ouvrir \"%2$@\" sur cet appareil." + "value" : "URL de %1$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ רוצה לפתוח \"%2$@\" במכשיר זה." + "value" : "קישור מ-%1$@" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ आपके इस डिवाइस पर \"%2$@\" खोलना चाहता है।" + "value" : "%1$@ से यूआरएल" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ želi otvoriti \"%2$@\" na ovom uređaju." + "value" : "URL s %1$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ meg akarja nyitni a(z) \"%2$@\" elemet ezen az eszközön." + "value" : "URL innen: %1$@" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ ingin membuka \"%2$@\" di perangkat ini." + "value" : "URL dari %1$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ vuole aprire \"%2$@\" su questo dispositivo." + "value" : "URL da %1$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@がこのデバイスで「%2$@」を開こうとしています。" + "value" : "%1$@のURL" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@님이 이 장치에서 \"%2$@\"을(를) 열려고 합니다." + "value" : "%1$@의 URL" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ ingin membuka \"%2$@\" pada peranti ini." + "value" : "URL dari %1$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ vil åpne \"%2$@\" på denne enheten." + "value" : "URL fra %1$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ wil \"%2$@\" openen op dit apparaat." + "value" : "URL van %1$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ chce otworzyć \"%2$@\" na tym urządzeniu." + "value" : "URL od %1$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ deseja abrir \"%2$@\" neste dispositivo." + "value" : "URL de %1$@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ quer abrir \"%2$@\" neste dispositivo." + "value" : "URL de %1$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ dorește să deschidă \"%2$@\" pe acest dispozitiv." + "value" : "URL de la %1$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ хочет открыть \"%2$@\" на этом устройстве." + "value" : "Ссылка от %1$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ vill öppna \"%2$@\" på den här enheten." + "value" : "URL från %1$@" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ ต้องการเปิด \"%2$@\" บนอุปกรณ์นี้" + "value" : "URL จาก %1$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ \"%2$@\" dosyasını bu cihazda açmak istiyor." + "value" : "%1$@ kaynağından URL" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ хоче відкрити \"%2$@\" на цьому пристрої." + "value" : "URL від %1$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ muốn mở \"%2$@\" trên thiết bị này." + "value" : "URL từ %1$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ 想在此设备上打开“%2$@”。" + "value" : "来自 %1$@ 的链接" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ 想在此裝置上打開 \"%2$@\"。" + "value" : "%1$@ 的網址" } } } @@ -9090,229 +8858,229 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ يريد إضافة كلمة مرور شبكة Wi-Fi \"%2$@\" إلى الحافظة الخاصة بك." + "value" : "كلمة مرور Wi-Fi من %1$@" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ vol inserir la contrasenya Wi-Fi de \"%2$@\" al teu porta-retalls." + "value" : "Contrasenya Wi-Fi de %1$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ chce vložit Wi-Fi heslo k „%2$@“ do vaší schránky." + "value" : "Wi-Fi heslo z %1$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ vil indsætte Wi-Fi-kodeordet for \"%2$@\" på dit udklipsholder." + "value" : "Wi-Fi-adgangskode fra %1$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ möchte das WLAN-Passwort von „%2$@“ in deine Zwischenablage einfügen." + "value" : "WLAN-Passwort von %1$@" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Ο/Η %1$@ θέλει να εισαγάγει τον κωδικό Wi-Fi του \"%2$@\" στο πρόχειρό σας." + "value" : "Κωδικός Wi-Fi από %1$@" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ wants to insert the Wi-Fi password for \"%2$@\" into your clipboard." + "value" : "Wi-Fi password from %1$@" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ wants to insert the Wi-Fi password for \"%2$@\" into your clipboard." + "value" : "Wi-Fi password from %1$@" } }, "en-CA" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ wants to insert the Wi-Fi password of \"%2$@\" into your clipboard." + "value" : "Wi-Fi password from %1$@" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ wants to insert the Wi-Fi password of \"%2$@\" into your clipboard." + "value" : "Wi-Fi password from %1$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ quiere insertar la contraseña Wi-Fi de \"%2$@\" en tu portapapeles." + "value" : "Contraseña Wi-Fi de %1$@" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ quiere insertar la contraseña Wi-Fi de \"%2$@\" en tu portapapeles." + "value" : "Contraseña de Wi-Fi de %1$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ haluaa lisätä \"%2$@\" Wi-Fi-salasanan leikepöydällesi." + "value" : "Wi-Fi-salasana laitteesta %1$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ souhaite insérer le mot de passe Wi-Fi de « %2$@ » dans votre presse-papiers." + "value" : "Mot de passe Wi-Fi de %1$@" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ veut insérer le mot de passe Wi-Fi de « %2$@ » dans votre presse-papiers." + "value" : "Mot de passe Wi-Fi de %1$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ מבקש להכניס את סיסמת ה-Wi-Fi של \"%2$@\" ללוח שלך." + "value" : "סיסמת Wi-Fi מ-%1$@" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ आपके क्लिपबोर्ड में \"%2$@\" का वाई-फाई पासवर्ड डालना चाहता है।" + "value" : "%1$@ से वाई-फाई पासवर्ड" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ želi umetnuti Wi-Fi lozinku za \"%2$@\" u vaš međuspremnik." + "value" : "Wi-Fi lozinka s %1$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ a(z) „%2$@” Wi-Fi jelszavát szeretné a vágólapodra másolni." + "value" : "Wi-Fi jelszó innen: %1$@" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ ingin memasukkan kata sandi Wi-Fi \"%2$@\" ke papan klip Anda." + "value" : "Kata sandi Wi-Fi dari %1$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ vuole inserire la password Wi-Fi di \"%2$@\" negli appunti." + "value" : "Password Wi-Fi da %1$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@が「%2$@」のWi-Fiパスワードをクリップボードに追加しようとしています。" + "value" : "%1$@のWi-Fiパスワード" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@이(가) \"%2$@\"의 Wi-Fi 비밀번호를 클립보드에 삽입하려고 합니다." + "value" : "%1$@의 Wi-Fi 비밀번호" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ ingin memasukkan kata laluan Wi-Fi \"%2$@\" ke papan klip anda." + "value" : "Kata laluan Wi-Fi dari %1$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ ønsker å sette inn Wi-Fi-passordet til \"%2$@\" på utklippstavlen din." + "value" : "Wi-Fi-passord fra %1$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ wil het Wi-Fi-wachtwoord van \"%2$@\" aan je klembord toevoegen." + "value" : "Wi-Fi-wachtwoord van %1$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ chce wstawić hasło Wi-Fi do „%2$@” do Twojego schowka." + "value" : "Hasło Wi-Fi od %1$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ quer inserir a senha Wi-Fi de \"%2$@\" na sua área de transferência." + "value" : "Senha do Wi-Fi de %1$@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ quer inserir a palavra-passe Wi-Fi de \"%2$@\" na sua área de transferência." + "value" : "Senha Wi-Fi de %1$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ dorește să introducă parola Wi-Fi a \"%2$@\" în clipboard-ul tău." + "value" : "Parolă Wi-Fi de la %1$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ хочет скопировать пароль Wi-Fi «%2$@» в ваш буфер обмена." + "value" : "Пароль Wi-Fi от %1$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ vill lägga till Wi‑Fi-lösenordet för \"%2$@\" till ditt urklipp." + "value" : "Wi-Fi-lösenord från %1$@" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ ต้องการคัดลอกรหัสผ่าน Wi-Fi ของ \"%2$@\" ไปยังคลิปบอร์ดของคุณ" + "value" : "รหัสผ่าน Wi-Fi จาก %1$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@, \"%2$@\" Wi-Fi şifresini panonuza kopyalamak istiyor." + "value" : "%1$@ kaynağından Wi-Fi şifresi" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ хоче вставити пароль Wi-Fi від \"%2$@\" у ваш буфер обміну." + "value" : "Пароль Wi-Fi від %1$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ muốn chèn mật khẩu Wi-Fi của \"%2$@\" vào clipboard của bạn." + "value" : "Mật khẩu Wi-Fi từ %1$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ 想要将“%2$@”的 Wi-Fi 密码插入到你的剪贴板。" + "value" : "来自 %1$@ 的 Wi-Fi 密码" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ 想將「%2$@」的 Wi-Fi 密碼插入到您的剪貼簿。" + "value" : "%1$@ 的 Wi-Fi 密碼" } } } @@ -14908,6 +14676,239 @@ } } }, + "File" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "ملف" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fitxer" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Soubor" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fil" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datei" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αρχείο" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "File" + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "File" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "File" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "File" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archivo" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archivo" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiedosto" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fichier" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fichier" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "קובץ" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "फाइल" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datoteka" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fájl" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Berkas" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "File" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファイル" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "파일" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fail" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fil" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bestand" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plik" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arquivo" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ficheiro" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fișier" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Файл" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fil" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ไฟล์" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dosya" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Файл" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tệp" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "文件" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檔案" + } + } + } + }, "FileTransferCompleted" : { "extractionState" : "manual", "localizations" : { @@ -14920,13 +14921,13 @@ "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Transferència de fitxers completada" + "value" : "Transferència de fitxer completada" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Přenos souborů dokončen" + "value" : "Přenos souboru dokončen" } }, "da" : { @@ -14944,7 +14945,7 @@ "el" : { "stringUnit" : { "state" : "translated", - "value" : "Η μεταφορά αρχείων ολοκληρώθηκε" + "value" : "Η μεταφορά αρχείου ολοκληρώθηκε" } }, "en" : { @@ -14974,13 +14975,13 @@ "es" : { "stringUnit" : { "state" : "translated", - "value" : "Transferencia de archivos completada" + "value" : "Transferencia de archivo completada" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Transferencia de archivos completada" + "value" : "Transferencia de archivo completada" } }, "fi" : { @@ -15004,7 +15005,7 @@ "he" : { "stringUnit" : { "state" : "translated", - "value" : "העברת הקובץ הושלמה" + "value" : "העברת קובץ הושלמה" } }, "hi" : { @@ -15016,7 +15017,7 @@ "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Prijenos datoteka završen" + "value" : "Prijenos datoteke dovršen" } }, "hu" : { @@ -15070,7 +15071,7 @@ "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Transfer plików zakończony" + "value" : "Transfer pliku zakończony" } }, "pt-BR" : { @@ -15082,13 +15083,13 @@ "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Transferência de ficheiros concluída" + "value" : "Transferência de ficheiro concluída" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Transferul de fișiere a fost finalizat" + "value" : "Transferul de fișiere finalizat" } }, "ru" : { @@ -15106,25 +15107,25 @@ "th" : { "stringUnit" : { "state" : "translated", - "value" : "การถ่ายโอนไฟล์เสร็จสิ้นแล้ว" + "value" : "ถ่ายโอนไฟล์เสร็จสมบูรณ์" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Dosya Transferi Tamamlandı" + "value" : "Dosya Aktarımı Tamamlandı" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Передача файлів завершена" + "value" : "Передача файлу завершена" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Chuyển tệp thành công" + "value" : "Chuyển tệp xong" } }, "zh-Hans" : { @@ -16306,6 +16307,240 @@ } } }, + "FromDevice" : { + "comment" : "e.g. „from Google Pixel 6“", + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "من %1$@" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "des de %1$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "od %1$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "fra %1$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "von %1$@" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "από %1$@" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "from %1$@" + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "from %1$@" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "from %1$@" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "from %1$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "de %1$@" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "de %1$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "laitteesta %1$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "de %1$@" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "de %1$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "מ-‎%1$@" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ से" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "od %1$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@-ról/-ről" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "dari %1$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "da %1$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ から" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@에서" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "dari %1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "fra %1$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "van %1$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "z %1$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "de %1$@" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "de %1$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "de la %1$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "от %1$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "från %1$@" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "จาก %1$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ cihazından" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "від %1$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "từ %1$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "来自 %1$@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "來自 %1$@" + } + } + } + }, "General" : { "extractionState" : "manual", "localizations" : { @@ -16772,6 +17007,239 @@ } } }, + "Image" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "صورة" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Imatge" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obrázek" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Billede" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bild" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Εικόνα" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Image" + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "Image" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Image" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Image" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Imagen" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Imagen" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kuva" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Image" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Image" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "תמונה" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "छवि" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Slika" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kép" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gambar" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Immagine" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "画像" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이미지" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Imej" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bilde" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afbeelding" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obraz" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Imagem" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Imagem" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Imagine" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Изображение" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bild" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "รูปภาพ" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Görsel" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Зображення" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hình ảnh" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "图片" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "圖片" + } + } + } + }, "ImportToPhotos" : { "extractionState" : "manual", "localizations" : { @@ -22120,6 +22588,239 @@ } } }, + "PDF" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "पीडीएफ" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF" + } + } + } + }, "PinCode" : { "extractionState" : "manual", "localizations" : { @@ -27930,6 +28631,239 @@ } } }, + "Saved" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تم الحفظ" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Desat" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uloženo" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gemt" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gespeichert" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αποθηκεύτηκε" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saved" + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saved" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saved" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saved" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guardado" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guardado" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tallennettu" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enregistré" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enregistré" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "נשמר" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सहेजा गया" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spremljeno" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mentve" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tersimpan" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salvato" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存しました" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "저장됨" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disimpan" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lagret" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opgeslagen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zapisano" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salvo" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Guardado" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salvat" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сохранено" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sparad" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "บันทึกแล้ว" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kaydedildi" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Збережено" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã lưu" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已保存" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已儲存" + } + } + } + }, "SearchingForDevices" : { "extractionState" : "manual", "localizations" : { @@ -30259,6 +31193,239 @@ } } }, + "Text" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "نص" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Text" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Text" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tekst" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Text" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Κείμενο" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Text" + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "Text" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Text" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Text" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Texto" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Texto" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Teksti" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Texte" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Texte" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "טקסט" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "टेक्स्ट" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tekst" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Szöveg" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Teks" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Testo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "テキスト" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "텍스트" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Teks" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tekst" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tekst" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tekst" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Texto" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Texto" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Text" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Текст" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Text" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ข้อความ" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Metin" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Текст" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Văn bản" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "文本" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "文字" + } + } + } + }, "TimeoutButton" : { "extractionState" : "manual", "localizations" : { @@ -34453,6 +35620,239 @@ } } }, + "URLOpened" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تم فتح الرابط" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL oberta" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL otevřena" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL åbnet" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL geöffnet" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Άνοιξε το URL" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL Opened" + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL Opened" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL Opened" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL Opened" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL abierta" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL abierta" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL avattu" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL ouverte" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL ouverte" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "ה-URL נפתח" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "यूआरएल खोला गया" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL otvoren" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL megnyitva" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL Dibuka" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL aperto" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "URLを開きました" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL이 열렸습니다" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL Dibuka" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL åpnet" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL geopend" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL otwarty" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL aberta" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL aberto" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL deschis" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL открыт" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL öppnad" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "เปิด URL แล้ว" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL Açıldı" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL відкрито" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã mở URL" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL 已打开" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已開啟網址" + } + } + } + }, "Unknown" : { "extractionState" : "manual", "localizations" : { @@ -35618,6 +37018,239 @@ } } }, + "Video" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "فيديو" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vídeo" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Βίντεο" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vidéo" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vidéo" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "וידאו" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "वीडियो" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Videó" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ビデオ" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "비디오" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wideo" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vídeo" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vídeo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Видео" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "วิดีโอ" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Відео" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Video" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "视频" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "影片" + } + } + } + }, "VisibleAs" : { "extractionState" : "manual", "localizations" : { diff --git a/QuickDrop/View/Toast/QuickDropToastView.swift b/QuickDrop/View/Toast/QuickDropToastView.swift index dd5e797..2f5a56e 100644 --- a/QuickDrop/View/Toast/QuickDropToastView.swift +++ b/QuickDrop/View/Toast/QuickDropToastView.swift @@ -9,13 +9,16 @@ import AppKit import LUI import SwiftUI -let toastViewSize = CGSize(width: 400, height: 120) +let toastViewSize = CGSize(width: 420, height: 75) +let actionColumnWidth: CGFloat = 120 +let actionColumnWidthEnd: CGFloat = 165 struct QuickDropToastView: View { @ObservedObject var settings = Settings.sharedInstance @ObservedObject var receiveModel: ReceiveModel @State var autoHider = DispatchWorkItem(block: {}) + @State private var isHovering = false /// Cancel while receiving. public var onCancel: () -> Void @@ -28,93 +31,188 @@ struct QuickDropToastView: View { self.onCancel = onCancel } + private func startAutoHide(_ actions: ReceiveModel.ToastViewAction) { + autoHider.cancel() + autoHider = DispatchWorkItem(block: { + actions.closeToastAction() + }) + + DispatchQueue.main.asyncAfter(deadline: .now() + actions.autoHideDelay, execute: autoHider) + } + + private func startAutoHideIfPossible() { + guard !isHovering, let actions = receiveModel.toastActions else { return } + startAutoHide(actions) + } + public var body: some View { - let isDone = receiveModel.toastActions != nil + let consent = receiveModel.consentState + let actions = receiveModel.toastActions + let hasActionButtons = actions?.openFilesAction != nil || actions?.importPhotosAction != nil + let showsRightColumn = consent != nil || hasActionButtons - VStack(spacing: 12) { - HStack(spacing: 10) { - - let iconSize = 39.0 - - AppIconView(hasPlusIcon: false, size: iconSize) - .frame(width: iconSize, height: iconSize) + let subHeaderSize = 21.0 - VStack(alignment: .leading, spacing: 2) { - Text(String("QuickDrop")) - .font(.system(size: 16, weight: .semibold)) + let closeButtonVisible = isHovering && actions != nil + let deviceName = receiveModel.activeDeviceName ?? "UnknownDevice".localized() + + ZStack(alignment: .topTrailing) { + HStack(alignment: .center, spacing: 12) { + VStack(alignment: .leading, spacing: 10) { + HStack(spacing: 10) { - Text(isDone ? "FileTransferCompleted" : "Receiving") - .font(.system(size: 13)) - .foregroundColor(.secondary) - .lineLimit(1) - .truncationMode(.tail) + let iconSize = 39.0 + + AppIconView(hasPlusIcon: false, size: iconSize) + .frame(width: iconSize, height: iconSize) + + VStack(alignment: .leading, spacing: 2) { + if let consent = consent { + Text(verbatim: "QuickDrop | \(consent.pinCodeMessage)") + .font(.system(size: 14, weight: .semibold)) + + Text(consent.message) + .font(.system(size: 13)) + .foregroundColor(.secondary) + .lineLimit(1) + .truncationMode(.tail) + .frame(height: subHeaderSize) + } else { + HStack { + Text(String("QuickDrop")) + .font(.system(size: 14, weight: .semibold)) + + + if consent == nil, actions == nil { + Text("FromDevice".localized(with: deviceName)) + .font(.system(size: 13)) + .foregroundColor(.secondary) + .lineLimit(1) + .truncationMode(.tail) + .padding(.trailing, 16) + } + } + + if consent == nil, actions == nil { + HStack { + ZStack { + CapsuleProgress(value: 0.5) + .opacity(0) + + if let progress = receiveModel.progress { + CapsuleProgress(value: progress) + } + } + .animation(.easeInOut, value: receiveModel.progress == nil) + + Button(action: onCancel) { + Image(systemName: "xmark") + .font(.system(size: 10, weight: .semibold)) + .foregroundColor(.secondary) + .frame(width: 18, height: 18) + } + .buttonStyle(PlainButtonStyle()) + } + .padding(.trailing, 16) + .frame(height: subHeaderSize) + } + else { + + Text(LocalizedStringKey(actions?.completionMessageKey ?? "FileTransferCompleted")) + .font(.system(size: 13)) + .foregroundColor(.secondary) + .lineLimit(1) + .truncationMode(.tail) + .frame(height: subHeaderSize) + } + } + } + } + + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) - Spacer(minLength: 12) + if showsRightColumn { + ActionColumn(width: consent == nil ? actionColumnWidthEnd : actionColumnWidth) { + if let consent = consent { + ActionButtonRow { + QuickDropToastViewButton(title: "Decline") { + consent.declineAction() + } + } + + ActionDividerLine() + + ActionButtonRow { + if consent.allowsTrust { + QuickDropToastViewMenuButton(title: "Accept") { + Button("Accept".localized()) { + consent.acceptAction(false) + } + Button("AutoAcceptFromThisDevice".localized()) { + consent.acceptAction(true) + } + } + } else { + QuickDropToastViewButton(title: "Accept") { + consent.acceptAction(false) + } + } + } + } else if let actions = actions { + if let openFilesAction = actions.openFilesAction { + ActionButtonRow { + QuickDropToastViewButton(title: "OpenInFinder") { + openFilesAction() + actions.closeToastAction() + } + } + } + + if let onImportToPhotos = actions.importPhotosAction { + if actions.openFilesAction != nil { + ActionDividerLine() + } + + ActionButtonRow { + QuickDropToastViewButton(title: "ImportToPhotos") { + onImportToPhotos() + actions.closeToastAction() + } + } + } + } + } + } } - ZStack { - - QuickDropToastViewButton(title: "OpenInFinder") { } - .opacity(0) - - if let actions = receiveModel.toastActions { - HStack(spacing: 8) { - - QuickDropToastViewButton(title: "OpenInFinder") { - actions.openFilesAction() - actions.closeToastAction() - } + } + .padding(.leading, 16) + .frame(height: toastViewSize.height) + .contentShape(Rectangle()) + .onHover { hovering in + withAnimation(.easeInOut(duration: 0.15)) { + isHovering = hovering + } - if let onImportToPhotos = actions.importPhotosAction { - QuickDropToastViewButton(title: "ImportToPhotos") { - onImportToPhotos() - actions.closeToastAction() - } - } - - Spacer() - - QuickDropToastViewButton(title: "Done", action: actions.closeToastAction) - .onAppear { - self.autoHider = DispatchWorkItem(block: { - actions.closeToastAction() - }) - - DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: self.autoHider) - } - .onDisappear { - self.autoHider.cancel() - } - } - .buttonStyle(.bordered) - .controlSize(.small) - } - - HStack { - ZStack { - CapsuleProgress(value: 0.5) - .opacity(0) - - if let progress = receiveModel.progress { - CapsuleProgress(value: progress) - } - } - .animation(.easeInOut, value: receiveModel.progress == nil) - - Button(action: onCancel) { - Image(systemName: "xmark") - .font(.system(size: 10, weight: .semibold)) - .foregroundColor(.secondary) - .frame(width: 18, height: 18) - } - .buttonStyle(PlainButtonStyle()) - } - .opacity(isDone ? 0 : 1) + if hovering { + autoHider.cancel() + } else { + startAutoHideIfPossible() } } - .padding(16) + .onChange(of: receiveModel.toastActions != nil) { hasActions in + if hasActions { + startAutoHideIfPossible() + } else { + autoHider.cancel() + } + } + .onAppear { + startAutoHideIfPossible() + } .background( VisualEffectView(material: .hudWindow) .clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous)) @@ -123,6 +221,30 @@ struct QuickDropToastView: View { RoundedRectangle(cornerRadius: 16, style: .continuous) .stroke(Color.black.opacity(0.03), lineWidth: 1) ) + .overlay(alignment: .topTrailing) { + if closeButtonVisible, let actions = actions { + Button(action: actions.closeToastAction) { + Image(systemName: "xmark") + .font(.system(size: 9, weight: .semibold)) + .foregroundColor(.secondary) + .frame(width: 18, height: 18) + .background( + Circle() + .fill(Color(NSColor.windowBackgroundColor).opacity(0.9)) + .shadow(color: Color.black.opacity(0.12), radius: 2, x: 0, y: 1) + ) + } + .buttonStyle(PlainButtonStyle()) + .onHover { hovering in + if hovering { + withAnimation(.easeInOut(duration: 0.15)) { + isHovering = true + } + } + } + .transition(.opacity.combined(with: .scale(scale: 0.95))) + } + } .shadow(color: Color.black.opacity(0.03), radius: 20) .frame(maxWidth: .infinity) } @@ -135,22 +257,79 @@ fileprivate struct QuickDropToastViewButton: View { var body: some View { Button(action: action) { - Text(title.localized()) - .padding(.horizontal, 12) - .padding(.vertical, 4) - .background( - Color.gray.opacity(0.15) - .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) - ) - .overlay( - RoundedRectangle(cornerRadius: 8, style: .continuous) - .stroke(Color.black.opacity(0.04), lineWidth: 1) - ) + ToastButtonLabel(title: title) } .buttonStyle(.plain) // prevents blue macOS button look } } +fileprivate struct QuickDropToastViewMenuButton: View { + let title: String + @ViewBuilder let content: () -> Content + + var body: some View { + Menu { + content() + } label: { + ToastButtonLabel(title: title) + } + .menuStyle(.borderlessButton) + } +} + +fileprivate struct ToastButtonLabel: View { + let title: String + + var body: some View { + Text(title.localized()) + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(.mainColor.opacity(0.75)) + .lineLimit(1) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(.horizontal, 14) + .padding(.vertical, 8) + .contentShape(Rectangle()) + } +} + +fileprivate struct ActionColumn: View { + let width: CGFloat + @ViewBuilder let content: () -> Content + + var body: some View { + VStack(spacing: 0) { + content() + } + .padding(.vertical, 3) + .frame(width: width) + .frame(maxHeight: .infinity) + .overlay(alignment: .leading) { + Rectangle() + .foregroundColor(.mainColor.opacity(0.05)) + .frame(width: 2) + } + .minimumScaleFactor(0.7) + } +} + +fileprivate struct ActionButtonRow: View { + @ViewBuilder let content: () -> Content + + var body: some View { + content() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +fileprivate struct ActionDividerLine: View { + var body: some View { + Rectangle() + .foregroundColor(.mainColor.opacity(0.05)) + .frame(height: 2) + .frame(maxWidth: .infinity) + } +} + // NSVisualEffectView wrapper for macOS 11 compatible blur/vibrancy struct VisualEffectView: NSViewRepresentable { @@ -196,7 +375,7 @@ struct CapsuleProgress: View { ) } } - .frame(height: 8) + .frame(height: 6) .animation(.easeInOut(duration: 0.25), value: value) } } @@ -206,7 +385,6 @@ struct CapsuleProgress: View { struct QuickDropToastView_Previews: PreviewProvider { struct Demo: View { @State var model = ReceiveModel(controlPlusScreen: { _ in }) - @State var done = true var body: some View { QuickDropToastView( @@ -219,10 +397,89 @@ struct QuickDropToastView_Previews: PreviewProvider { .background(Color.black.opacity(0.2)) .onAppear { model.progress = 1 - model.toastActions = .init(openFilesAction: {}, importPhotosAction: {}, closeToastAction: {}) + model.toastActions = .init(completionMessageKey: "Saved", autoHideDelay: 10, openFilesAction: {}, importPhotosAction: {}, closeToastAction: {}) } } } - static var previews: some View { Demo() } + struct ConsentDemo: View { + @State var model = ReceiveModel(controlPlusScreen: { _ in }) + + var body: some View { + QuickDropToastView( + receiveModel: model, + onCancel: { } + ) + .frame(width: toastViewSize.width, height: toastViewSize.height) + .clipped() + .padding(100) + .background(QuickDropToastView_Previews.previewWallpaper) + .onAppear { + model.consentState = .init( + transferID: "preview", + pinCodeMessage: "PIN: 1233", + message: "45 images from Pixel 6 Pro", + allowsTrust: true, + acceptAction: { _ in }, + declineAction: { } + ) + } + } + } + + struct ProgressDemo: View { + @State var model = ReceiveModel(controlPlusScreen: { _ in }) + + var body: some View { + QuickDropToastView( + receiveModel: model, + onCancel: { } + ) + .frame(width: toastViewSize.width, height: toastViewSize.height) + .clipped() + .padding(100) + .background(QuickDropToastView_Previews.previewWallpaper) + .onAppear { + model.progress = 0.42 + model.toastActions = nil + } + } + } + + struct CompletedDemo: View { + @State var model = ReceiveModel(controlPlusScreen: { _ in }) + + var body: some View { + QuickDropToastView( + receiveModel: model, + onCancel: { } + ) + .frame(width: toastViewSize.width, height: toastViewSize.height) + .clipped() + .padding(100) + .background(previewWallpaper) + .onAppear { + model.progress = 1 + model.toastActions = .init(completionMessageKey: "Saved", autoHideDelay: 10, openFilesAction: {}, importPhotosAction: {}, closeToastAction: {}) + } + } + } + + private static let previewWallpaper = LinearGradient( + gradient: Gradient(colors: [ + Color(red: 0.92, green: 0.82, blue: 0.94), + Color(red: 0.86, green: 0.76, blue: 0.90), + Color(red: 0.78, green: 0.70, blue: 0.86) + ]), + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + + static var previews: some View { + Group { + ConsentDemo() + ProgressDemo() + CompletedDemo() + } + } } diff --git a/QuickDrop/ViewModel/ReceiveModel.swift b/QuickDrop/ViewModel/ReceiveModel.swift index 1d012c8..ba02048 100644 --- a/QuickDrop/ViewModel/ReceiveModel.swift +++ b/QuickDrop/ViewModel/ReceiveModel.swift @@ -7,26 +7,23 @@ import SwiftUI import StoreKit -import UserNotifications import LUI -#if os(macOS) -import BezelNotification -#endif - class ReceiveModel: ObservableObject, InboundAppDelegate { /// For each connection ID, store the last reported progress value private var processes: [String: Double] = [:] - #if os(macOS) +#if os(macOS) @Published var progress: Double? = nil @Published var toastActions: ToastViewAction? = nil + @Published var consentState: ConsentToastState? = nil + @Published var activeDeviceName: String? = nil private var pendingReview = false private var toastWindow: NSWindow? private var toastHosting: NSHostingView? private let monitor = AllowedWorkMonitor() - #endif +#endif let controlPlusScreen: (Bool) -> Void @@ -67,7 +64,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate { AudioManager.playIncomingFileSound() #endif - let mainMessage = transfer.getDescription(deviceName: device.name ?? "UnknownDevice".localized(), alreadyAccepted: false) + let mainMessage = transfer.getDescription(deviceName: device.name ?? "UnknownDevice".localized()) let pinCodeMessage = transfer.getPinCodeMessage() let transferID = transfer.id @@ -75,39 +72,36 @@ class ReceiveModel: ObservableObject, InboundAppDelegate { let primaryButtonTitle = "Accept".localized() let primaryButtonAction = { (trustDevice: Bool) in NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: true, trustDevice: trustDevice) - - #if os(macOS) - if transfer.type == .text { - DispatchQueue.main.async { - BezelNotification.show(messageText: "InsertedIntoClipboard".localized(), icon: .clipboard) - } - } - #endif } let secondaryButtonTitle = "Decline".localized() let secondaryButtonAction = { NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: false, trustDevice: false) } - + #if os(macOS) - let alert = NSAlert() - alert.alertStyle = .informational - - alert.messageText = title - alert.informativeText = mainMessage - alert.addButton(withTitle: primaryButtonTitle) - alert.addButton(withTitle: secondaryButtonTitle) - - if transfer.allowsToBeAddedAsTrustedDevice { - alert.showsSuppressionButton = true - alert.suppressionButton?.title = "AutoAcceptFromThisDevice".localized() - } - - let result = alert.runModal() - - if result == .alertFirstButtonReturn { - primaryButtonAction(alert.suppressionButton?.state == .on) - } else if result == .alertSecondButtonReturn { - secondaryButtonAction() + DispatchQueue.main.async { + self.toastActions = nil + self.progress = nil + self.activeDeviceName = device.name ?? "UnknownDevice".localized() + self.consentState = ConsentToastState( + transferID: transferID, + pinCodeMessage: pinCodeMessage, + message: mainMessage, + allowsTrust: transfer.allowsToBeAddedAsTrustedDevice, + acceptAction: { [weak self] trustDevice in + DispatchQueue.main.async { + withAnimation { + self?.consentState = nil + } + } + primaryButtonAction(trustDevice) + }, + declineAction: { [weak self] in + self?.consentState = nil + secondaryButtonAction() + self?.hideQuickDropToast() + } + ) + self.showQuickDropToast(for: transferID) } #else // iOS @@ -129,33 +123,18 @@ class ReceiveModel: ObservableObject, InboundAppDelegate { func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo) { - let mainMessage = transfer.getDescription(deviceName: device.name ?? "UnknownDevice".localized(), alreadyAccepted: true) + let mainMessage = transfer.getDescription(deviceName: device.name ?? "UnknownDevice".localized()) #if os(macOS) NSApp.activate(ignoringOtherApps: true) AudioManager.playIncomingFileSound() - - // If file -> no notification, as there is the QuickDrop progress toast - if transfer.type != .file { - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, _ in - if granted { - log("[ReceiveModel] User granted notification permissions") - - let notificationContent = UNMutableNotificationContent() - notificationContent.title = "QuickDrop" - notificationContent.body = mainMessage - notificationContent.sound = nil - let notificationId = UUID().uuidString - - let notificationReq = UNNotificationRequest(identifier: notificationId, content: notificationContent, trigger: nil) - UNUserNotificationCenter.current().add(notificationReq) - - DispatchQueue.main.asyncAfter(deadline: .now() + 5) { - UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [notificationId]) - } - } - } + DispatchQueue.main.async { + self.toastActions = nil + self.progress = nil + self.activeDeviceName = device.name ?? "UnknownDevice".localized() + self.consentState = nil + self.showQuickDropToast(for: transfer.id) } #else // If text is received, nothing is shown, as it is directly inserted into clipboard automatically @@ -196,11 +175,17 @@ class ReceiveModel: ObservableObject, InboundAppDelegate { #if os(macOS) DispatchQueue.main.async { + let wasPendingConsent = self.consentState?.transferID == connectionID if self.processes.isEmpty { self.progress = nil } + if wasPendingConsent { + self.consentState = nil + self.toastActions = nil + self.hideQuickDropToast() + } + finishMacTermination(device: device, savedFiles: savedFiles, error: error, wasPendingConsent: wasPendingConsent) } - finish() #else if self.processes.isEmpty { ProgressAlert.shared.updateProgress(nil, onCancel: {}) { @@ -251,16 +236,27 @@ class ReceiveModel: ObservableObject, InboundAppDelegate { #if os(macOS) + let completionKey: String + let autoHideDelay: TimeInterval + + if !savedFiles.isEmpty { + completionKey = "Saved" + autoHideDelay = 10 + } else if wasPlainTextTransfer { + completionKey = "CopiedToClipboard" + autoHideDelay = 10 + } else { + completionKey = "URLOpened" + autoHideDelay = 4 + } + if currentCount == 0 { pendingReview = true } - let openFilesAction = { - if !savedFiles.isEmpty { - log("[SaveFilesManager] Opening \(savedFiles.count) file(s) in Finder.") - NSWorkspace.shared.activateFileViewerSelecting(savedFiles) - } - + let openFilesAction: (() -> ())? = savedFiles.isEmpty ? nil : { + log("[SaveFilesManager] Opening \(savedFiles.count) file(s) in Finder.") + NSWorkspace.shared.activateFileViewerSelecting(savedFiles) showReviewIfAppropriate(currentTransmissionCount: currentCount) } @@ -292,12 +288,12 @@ class ReceiveModel: ObservableObject, InboundAppDelegate { DispatchQueue.main.async { withAnimation { - self.toastActions = ToastViewAction(openFilesAction: openFilesAction, importPhotosAction: importPhotosAction, closeToastAction: closeToastAction) + self.toastActions = ToastViewAction(completionMessageKey: completionKey, autoHideDelay: autoHideDelay, openFilesAction: openFilesAction, importPhotosAction: importPhotosAction, closeToastAction: closeToastAction) } // Show received files if wanted if Settings.sharedInstance.openFinderAfterReceiving { - self.toastActions?.openFilesAction() + self.toastActions?.openFilesAction?() } } #endif @@ -307,7 +303,89 @@ class ReceiveModel: ObservableObject, InboundAppDelegate { } } + #if os(macOS) + func finishMacTermination(device: RemoteDeviceInfo?, savedFiles: [URL], error: (any Error)?, wasPendingConsent: Bool) { + if wasPendingConsent { + return + } + + if let error = error { + if let name = device?.name { + self.hideQuickDropToast() + + controlPlusScreen(false) + + errorVibration() + ErrorAlertHandler.shared.showErrorAlert(for: name, error: error) + } + return + } + + let currentCount = Settings.sharedInstance.incomingTransmissionCount + let completionKey: String + let autoHideDelay: TimeInterval + + if !savedFiles.isEmpty { + completionKey = "Saved" + autoHideDelay = 10 + } else if wasPlainTextTransfer { + completionKey = "CopiedToClipboard" + autoHideDelay = 10 + } else { + completionKey = "URLOpened" + autoHideDelay = 4 + } + + if currentCount == 0 { + pendingReview = true + } + + let openFilesAction: (() -> ())? = savedFiles.isEmpty ? nil : { + log("[SaveFilesManager] Opening \(savedFiles.count) file(s) in Finder.") + NSWorkspace.shared.activateFileViewerSelecting(savedFiles) + showReviewIfAppropriate(currentTransmissionCount: currentCount) + } + + let hasPhotoOrVideos = PhotoManager.hasPhotosOrVideos(at: savedFiles) + let importPhotosAction: (()->())? = hasPhotoOrVideos ? { + Task { + do { + try await PhotoManager.saveMediaToPhotoLibrary(from: savedFiles) + showReviewIfAppropriate(currentTransmissionCount: currentCount) + } + catch { + DispatchQueue.main.async { + let alert = NSAlert() + alert.alertStyle = .critical + alert.messageText = "CouldNotSaveMediaToPhotoLibrary".localized() + alert.informativeText = error.localizedDescription + alert.addButton(withTitle: "CloseAlert".localized()) + + alert.runModal() + } + } + } + } : nil + + let closeToastAction = { + self.hideQuickDropToast() + showReviewIfAppropriate(currentTransmissionCount: currentCount) + } + + withAnimation { + self.toastActions = ToastViewAction(completionMessageKey: completionKey, autoHideDelay: autoHideDelay, openFilesAction: openFilesAction, importPhotosAction: importPhotosAction, closeToastAction: closeToastAction) + } + + // Show received files if wanted + if Settings.sharedInstance.openFinderAfterReceiving { + self.toastActions?.openFilesAction?() + } + + Settings.sharedInstance.incomingTransmissionCount = currentCount + 1 + log("[ReceiveModel] Successful transmission. Current count: \(currentCount)") + } + func showReviewIfAppropriate(currentTransmissionCount: Int) { if pendingReview { // Dont request review again @@ -324,6 +402,12 @@ class ReceiveModel: ObservableObject, InboundAppDelegate { func showPlusScreen() { DispatchQueue.main.async { +#if os(macOS) + self.consentState = nil + self.toastActions = nil + self.progress = nil + self.hideQuickDropToast() +#endif self.controlPlusScreen(true) } } @@ -397,7 +481,13 @@ class ReceiveModel: ObservableObject, InboundAppDelegate { func hideQuickDropToast() { guard let window = toastWindow, - let screen = NSScreen.main else { return } + let screen = NSScreen.main else { + self.toastWindow = nil + self.toastHosting = nil + self.toastActions = nil + self.consentState = nil + return + } let offscreenY = screen.visibleFrame.origin.y + screen.visibleFrame.height + 20 @@ -411,14 +501,28 @@ class ReceiveModel: ObservableObject, InboundAppDelegate { self.toastWindow = nil self.toastHosting = nil self.toastActions = nil + self.consentState = nil + self.activeDeviceName = nil }) } struct ToastViewAction { - let openFilesAction: () -> () + let completionMessageKey: String + let autoHideDelay: TimeInterval + let openFilesAction: (() -> ())? let importPhotosAction: (() -> ())? let closeToastAction: () -> () } + + + struct ConsentToastState { + let transferID: String + let pinCodeMessage: String + let message: String + let allowsTrust: Bool + let acceptAction: (Bool) -> () + let declineAction: () -> () + } #endif }