47 Commits
Author SHA1 Message Date
Leon Böttger a082d524fa Add multicast networking capability 2025-08-08 08:25:07 +02:00
Leon Böttger 3926f08a36 Add logging on file sizes 2025-08-02 11:22:56 +02:00
Leon Böttger 5bf8469732 Add ability to open app at login 2025-07-29 11:24:31 +02:00
Leon Böttger eac2df3d44 Add error instructions for timeout 2025-07-28 21:12:22 +02:00
Leon Böttger 86bde438e2 Decrease timeout interval 2025-07-28 14:59:08 +02:00
Leon Böttger de7cd2bf98 Add data transfer timer timeout 2025-07-28 14:58:34 +02:00
Leon Böttger 6985d4b1ba Improve error handling 2025-07-28 14:33:18 +02:00
Leon Böttger ddd9a32023 Better error message for failed signature check, throw error on canceled file transfer 2025-07-28 14:10:16 +02:00
Leon Böttger f18649b959 WIP: Add SwiftUI browse view 2025-07-28 12:29:22 +02:00
Leon Böttger 273b7459d8 Fix compilation for iOS 2025-07-28 11:57:50 +02:00
Leon Böttger d2fc94df75 Refactoring 2025-07-28 11:36:04 +02:00
Leon Böttger 0842559f49 Increment build 2025-07-28 11:05:02 +02:00
Leon Böttger 5f40e1d759 Fix could not restart device discovery after stopping 2025-07-27 20:54:28 +02:00
Leon Böttger a311aa01b0 Refactoring 2025-07-27 16:04:58 +02:00
Leon Böttger af5ff5e4fe Improved logging 2025-07-27 16:01:35 +02:00
Leon Böttger 77657ece73 Improve logging 2025-07-27 15:13:29 +02:00
Leon Böttger e3562ab03a Refactoring 2025-07-27 15:08:13 +02:00
Leon Böttger dacb1f868a Add ability to drop files into main window to upload 2025-07-27 14:41:11 +02:00
Leon Böttger 9f2af19adb Refactoring 2025-07-27 12:21:40 +02:00
Leon Böttger f4a21d3f0a Add wakelock when transferring files 2025-07-27 12:14:08 +02:00
Leon Böttger 0f6fef89fe Improve strings 2025-07-27 11:39:47 +02:00
Leon Böttger fb7dda8824 Refactoring 2025-07-26 19:27:59 +02:00
Leon Böttger 13716f41e2 Refactoring 2025-07-26 19:19:34 +02:00
Leon Böttger 98d1596672 Refactoring 2025-07-26 19:01:31 +02:00
Leon Böttger 4262c387a2 Only call connectionWasTerminated if not rejected 2025-07-26 18:53:58 +02:00
Leon Böttger 2c4d661ef3 Also show error message for connections that were not approved yet 2025-07-26 18:48:14 +02:00
Leon Böttger 71ef68d4cb Fix icons for older macOS versions 2025-07-26 13:15:41 +02:00
Leon Böttger b6e3943374 Remove unused code and strings 2025-07-26 13:05:11 +02:00
Leon Böttger a3679c10c0 Migrate to string catalog 2025-07-26 13:01:05 +02:00
Leon Böttger 5d79379b6b Add support for sharing folders 2025-07-26 12:55:40 +02:00
Leon Böttger 381c81e7d6 Remove unused strings 2025-07-26 12:22:44 +02:00
Leon Böttger 4ab9f0d815 Add missing translations 2025-07-26 12:06:30 +02:00
Leon Böttger 277c76e636 Fix timeout interval too short 2025-07-26 11:48:22 +02:00
Leon Böttger 5ecc66d57b Fix too short timeout interval 2025-07-26 11:40:17 +02:00
Leon Böttger b68e4a7345 Show review prompt only once 2025-07-25 15:04:34 +02:00
Leon Böttger 5f9104d4da Refactoring 2025-07-25 14:53:01 +02:00
Leon Böttger c1defe9eee Better error message if transfer was cancelled 2025-07-25 14:39:46 +02:00
Leon Böttger 83a6ed7040 Remove progress if file was aborted 2025-07-25 14:35:37 +02:00
Leon Böttger ae2fcfe886 Fix: share extension was unable to export logs 2025-07-25 14:18:34 +02:00
Leon Böttger 7371208227 Formatting 2025-07-25 13:36:25 +02:00
Leon Böttger 8bac50360f Formatting 2025-07-25 13:26:10 +02:00
Leon Böttger de2686d55e Remove unused code 2025-07-25 13:10:09 +02:00
Leon Böttger 8a9b63954c Add timeout for incoming/outgoing connections, better error handling for outgoing connections 2025-07-25 13:09:01 +02:00
Leon Böttger f3005c3dcf Add logging info to keep-alive frames 2025-07-25 11:44:07 +02:00
Leon Böttger c52dde3526 Reduce logging 2025-07-25 11:33:50 +02:00
Leon Böttger 07b6d9d6fa Refactoring 2025-07-25 11:29:40 +02:00
Leon Böttger 24ce9dd677 More detailed error messages if connection closed by peer or network connection was interrupted 2025-07-25 11:07:45 +02:00
131 changed files with 17197 additions and 5207 deletions

No files matched your search

@@ -0,0 +1,564 @@
//
// InboundNearbyConnection.swift
// QuickDrop
//
// Created by Grishka on 08.04.2023.
//
import CommonCrypto
import CryptoKit
import Foundation
import Network
import System
import BigInt
import SwiftECC
#if os(macOS)
import AppKit
#else
import UIKit
#endif
class InboundNearbyConnection: NearbyConnection {
private var filesToBeReceived: [Int64: InternalFileInfo] = [:]
private var currentState: State = .initial
private var cipherCommitment: Data?
private var textPayloadID: Int64 = 0
private var isPlainTextTransfer = false
public var wasRejected = false
public var delegate: InboundNearbyConnectionDelegate?
enum State {
case initial, receivedConnectionRequest, sentUkeyServerInit, receivedUkeyClientFinish, sentConnectionResponse, sentPairedKeyResult, receivedPairedKeyResult, waitingForUserConsent, receivingFiles, disconnected
}
override init(connection: NWConnection, id: String) {
super.init(connection: connection, id: id)
}
override func disconnect() {
super.disconnect()
currentState = .disconnected
deletePartiallyReceivedFiles()
DispatchQueue.main.async {
self.delegate?.connectionWasTerminated(connection: self, error: self.lastError)
SaveFilesManager.shared.movePendingFilesToTarget()
SaveFilesManager.shared.stopAccessingSecurityScopedResource()
}
}
override func processReceivedFrame(frameData: Data) {
if currentState != .receivingFiles {
log("[InboundNearbyConnection] Received frame in state \(currentState)...")
}
do {
switch currentState {
case .initial:
let frame = try Location_Nearby_Connections_OfflineFrame(serializedBytes: frameData)
try processConnectionRequestFrame(frame)
case .receivedConnectionRequest:
let msg = try Securegcm_Ukey2Message(serializedBytes: frameData)
ukeyClientInitMsgData = frameData
try processUkey2ClientInit(msg)
case .sentUkeyServerInit:
let msg = try Securegcm_Ukey2Message(serializedBytes: frameData)
try processUkey2ClientFinish(msg, raw: frameData)
case .receivedUkeyClientFinish:
let frame = try Location_Nearby_Connections_OfflineFrame(serializedBytes: frameData)
try processConnectionResponseFrame(frame)
default:
var smsg: Securemessage_SecureMessage? = nil
do {
smsg = try Securemessage_SecureMessage(serializedBytes: frameData)
} catch {
log("[InboundNearbyConnection] Error deserializing secure message (probably due to packet filter)")
lastError = NearbyError.packetFilterError
protocolError()
}
if let smsg = smsg {
try decryptAndProcessReceivedSecureMessage(smsg)
}
}
} catch {
lastError = error
log("[InboundNearbyConnection] Error receiving frame: \(error) in state \(currentState).")
protocolError()
}
}
override func processTransferSetupFrame(_ frame: Sharing_Nearby_Frame) throws {
if frame.hasV1 && frame.v1.hasType, case .cancel = frame.v1.type {
self.lastError = NearbyError.canceled(reason: .userCanceled)
self.cancelled = true
log("[InboundNearbyConnection] Transfer canceled")
try sendDisconnectionAndDisconnect()
return
}
switch currentState {
case .sentConnectionResponse:
try processPairedKeyEncryptionFrame(frame)
case .sentPairedKeyResult:
try processPairedKeyResultFrame(frame)
case .receivedPairedKeyResult:
try processIntroductionFrame(frame)
default:
log("[InboundNearbyConnection] Unexpected connection state in processTransferSetupFrame: \(currentState)")
log(frame.debugDescription)
}
}
override func isServer() -> Bool {
return true
}
override func processFileChunk(frame: Location_Nearby_Connections_PayloadTransferFrame) throws {
let id = frame.payloadHeader.id
guard let fileInfo = filesToBeReceived[id] else { throw NearbyError.protocolError("File payload ID \(id) is not known") }
let currentOffset = fileInfo.bytesTransferred
guard frame.payloadChunk.offset == currentOffset else { throw NearbyError.protocolError("Invalid offset into file \(frame.payloadChunk.offset), expected \(currentOffset)") }
guard currentOffset + Int64(frame.payloadChunk.body.count) <= fileInfo.meta.size else { throw NearbyError.protocolError("Transferred file size exceeds previously specified value") }
if frame.payloadChunk.body.count > 0 {
do {
try fileInfo.fileHandle?.write(contentsOf: frame.payloadChunk.body)
filesToBeReceived[id]!.bytesTransferred += Int64(frame.payloadChunk.body.count)
fileInfo.progress?.completedUnitCount = filesToBeReceived[id]!.bytesTransferred
// only for logging
self.bytesTransferred += Int64(frame.payloadChunk.body.count)
} catch {
log("[InboundNearbyConnection] Error occurred during writing file: \(error.localizedDescription)")
throw NearbyError.protocolError(error.localizedDescription)
}
}
else if (frame.payloadChunk.flags & 1) == 1 {
try fileInfo.fileHandle?.close()
filesToBeReceived[id]!.fileHandle = nil
#if os(macOS)
fileInfo.progress?.unpublish()
#endif
SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL)
filesToBeReceived.removeValue(forKey: id)
if filesToBeReceived.isEmpty {
log("[InboundNearbyConnection] All files received, sending disconnection frame and disconnecting.")
try sendDisconnectionAndDisconnect()
}
}
}
override func processBytesPayload(payload: Data, id: Int64) throws -> Bool {
if id == textPayloadID {
if let urlStr = String(data: payload, encoding: .utf8) {
if isPlainTextTransfer {
#if os(macOS)
// macOS clipboard
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(urlStr, forType: .string)
NearbyConnectionManager.shared.mainAppDelegate?.showCopiedToClipboardAlert()
#elseif os(iOS)
// iOS clipboard
UIPasteboard.general.string = urlStr
// Optionally show an alert (requires a way to present it)
// For example, post a notification or use a delegate to show a toast or alert
#endif
} else if let url = URL(string: urlStr) {
#if os(macOS)
NSWorkspace.shared.open(url)
#elseif os(iOS)
UIApplication.shared.open(url, options: [:], completionHandler: nil)
#endif
}
}
log("[InboundNearbyConnection] Received text payload. Disconnecting...")
try sendDisconnectionAndDisconnect()
return true
}
else if let fileInfo = filesToBeReceived[id] {
fileInfo.fileHandle?.write(payload)
filesToBeReceived[id]!.bytesTransferred += Int64(payload.count)
fileInfo.progress?.completedUnitCount = filesToBeReceived[id]!.bytesTransferred
try fileInfo.fileHandle?.close()
filesToBeReceived[id]!.fileHandle = nil
#if os(macOS)
fileInfo.progress?.unpublish()
#endif
filesToBeReceived.removeValue(forKey: id)
SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL)
log("[InboundNearbyConnection] Received file payload. Disconnecting...")
try sendDisconnectionAndDisconnect()
return true
}
return false
}
private func processConnectionRequestFrame(_ frame: Location_Nearby_Connections_OfflineFrame) throws {
guard frame.hasV1 && frame.v1.hasConnectionRequest && frame.v1.connectionRequest.hasEndpointInfo else { throw NearbyError.requiredFieldMissing("connectionRequest.endpointInfo") }
guard case .connectionRequest = frame.v1.type else { throw NearbyError.protocolError("Unexpected frame type \(frame.v1.type)") }
let endpointInfo = frame.v1.connectionRequest.endpointInfo
guard endpointInfo.count > 17 else { throw NearbyError.protocolError("Endpoint info too short") }
let deviceNameLength = Int(endpointInfo[17])
guard endpointInfo.count >= deviceNameLength + 18 else { throw NearbyError.protocolError("Endpoint info too short to contain the device name") }
guard let deviceName = String(data: endpointInfo[18 ..< (18 + deviceNameLength)], encoding: .utf8) else { throw NearbyError.protocolError("Device name is not valid UTF-8") }
let rawDeviceType = Int(endpointInfo[0] & 7) >> 1
remoteDeviceInfo = RemoteDeviceInfo(name: deviceName, type: RemoteDeviceInfo.DeviceType.fromRawValue(value: rawDeviceType))
currentState = .receivedConnectionRequest
}
private func processUkey2ClientInit(_ msg: Securegcm_Ukey2Message) throws {
guard msg.hasMessageType, msg.hasMessageData else { throw NearbyError.requiredFieldMissing("clientInit ukey2message.type|data") }
guard case .clientInit = msg.messageType else {
sendUkey2Alert(type: .badMessageType)
log("[InboundNearbyConnection] Unsupported message type: \(msg.messageType)")
throw NearbyError.ukey2
}
let clientInit: Securegcm_Ukey2ClientInit
do {
clientInit = try Securegcm_Ukey2ClientInit(serializedBytes: msg.messageData)
} catch {
sendUkey2Alert(type: .badMessageData)
log("[InboundNearbyConnection] Failed to parse clientInit: \(error)")
throw NearbyError.ukey2
}
guard clientInit.version == 1 else {
sendUkey2Alert(type: .badVersion)
log("[InboundNearbyConnection] Unsupported clientInit version: \(clientInit.version)")
throw NearbyError.ukey2
}
guard clientInit.random.count == 32 else {
sendUkey2Alert(type: .badRandom)
log("[InboundNearbyConnection] Unsupported clientInit random: \(clientInit.random.count)")
throw NearbyError.ukey2
}
var found = false
for commitment in clientInit.cipherCommitments {
if case .p256Sha512 = commitment.handshakeCipher {
found = true
cipherCommitment = commitment.commitment
break
}
}
guard found else {
sendUkey2Alert(type: .badHandshakeCipher)
log("[InboundNearbyConnection] Unsupported clientInit handshakeCipher: \(clientInit.cipherCommitments)")
throw NearbyError.ukey2
}
guard clientInit.nextProtocol == "AES_256_CBC-HMAC_SHA256" else {
sendUkey2Alert(type: .badNextProtocol)
log("[InboundNearbyConnection] Unsupported clientInit nextProtocol: \(clientInit.nextProtocol)")
throw NearbyError.ukey2
}
let domain = Domain.instance(curve: .EC256r1)
let (pubKey, privKey) = domain.makeKeyPair()
publicKey = pubKey
privateKey = privKey
var serverInit = Securegcm_Ukey2ServerInit()
serverInit.version = 1
serverInit.random = Data.randomData(length: 32)
serverInit.handshakeCipher = .p256Sha512
var pkey = Securemessage_GenericPublicKey()
pkey.type = .ecP256
pkey.ecP256PublicKey = Securemessage_EcP256PublicKey()
pkey.ecP256PublicKey.x = Data(pubKey.w.x.asSignedBytes())
pkey.ecP256PublicKey.y = Data(pubKey.w.y.asSignedBytes())
serverInit.publicKey = try pkey.serializedData()
var serverInitMsg = Securegcm_Ukey2Message()
serverInitMsg.messageType = .serverInit
serverInitMsg.messageData = try serverInit.serializedData()
let serverInitData = try serverInitMsg.serializedData()
ukeyServerInitMsgData = serverInitData
sendFrameAsync(serverInitData)
currentState = .sentUkeyServerInit
}
private func processUkey2ClientFinish(_ msg: Securegcm_Ukey2Message, raw: Data) throws {
guard msg.hasMessageType, msg.hasMessageData else { throw NearbyError.requiredFieldMissing("clientFinish ukey2message.type|data") }
guard case .clientFinish = msg.messageType else {
log("[InboundNearbyConnection] Unexpected message type \(msg.messageType)")
throw NearbyError.ukey2
}
var sha = SHA512()
sha.update(data: raw)
guard cipherCommitment == Data(sha.finalize()) else {
log("[InboundNearbyConnection] Invalid cipherCommitment in clientFinish")
throw NearbyError.ukey2
}
let clientFinish = try Securegcm_Ukey2ClientFinished(serializedBytes: msg.messageData)
guard clientFinish.hasPublicKey else { throw NearbyError.requiredFieldMissing("ukey2clientFinish.publicKey") }
let clientKey = try Securemessage_GenericPublicKey(serializedBytes: clientFinish.publicKey)
try finalizeKeyExchange(peerKey: clientKey)
currentState = .receivedUkeyClientFinish
}
private func processConnectionResponseFrame(_ frame: Location_Nearby_Connections_OfflineFrame) throws {
guard frame.hasV1, frame.v1.hasType else { throw NearbyError.requiredFieldMissing("offlineFrame.v1.type") }
if case .connectionResponse = frame.v1.type {
var resp = Location_Nearby_Connections_OfflineFrame()
resp.version = .v1
resp.v1 = Location_Nearby_Connections_V1Frame()
resp.v1.type = .connectionResponse
resp.v1.connectionResponse = Location_Nearby_Connections_ConnectionResponseFrame()
resp.v1.connectionResponse.response = .accept
resp.v1.connectionResponse.status = 0
resp.v1.connectionResponse.osInfo = Location_Nearby_Connections_OsInfo()
resp.v1.connectionResponse.osInfo.type = .apple
try sendFrameAsync(resp.serializedData())
encryptionDone = true
var pairedEncryption = Sharing_Nearby_Frame()
pairedEncryption.version = .v1
pairedEncryption.v1 = Sharing_Nearby_V1Frame()
pairedEncryption.v1.type = .pairedKeyEncryption
pairedEncryption.v1.pairedKeyEncryption = Sharing_Nearby_PairedKeyEncryptionFrame()
// Presumably used for all the phone number stuff that no one needs anyway
pairedEncryption.v1.pairedKeyEncryption.secretIDHash = Data.randomData(length: 6)
pairedEncryption.v1.pairedKeyEncryption.signedData = Data.randomData(length: 72)
try sendTransferSetupFrame(pairedEncryption)
currentState = .sentConnectionResponse
} else {
log("[InboundNearbyConnection] Unhandled offline frame plaintext: \(frame)")
}
}
private func processPairedKeyEncryptionFrame(_ frame: Sharing_Nearby_Frame) throws {
guard frame.hasV1, frame.v1.hasPairedKeyEncryption else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyEncryption") }
var pairedResult = Sharing_Nearby_Frame()
pairedResult.version = .v1
pairedResult.v1 = Sharing_Nearby_V1Frame()
pairedResult.v1.type = .pairedKeyResult
pairedResult.v1.pairedKeyResult = Sharing_Nearby_PairedKeyResultFrame()
pairedResult.v1.pairedKeyResult.status = .unable
try sendTransferSetupFrame(pairedResult)
currentState = .sentPairedKeyResult
}
private func processPairedKeyResultFrame(_ frame: Sharing_Nearby_Frame) throws {
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyResult") }
currentState = .receivedPairedKeyResult
}
private func makeFileDestinationURL(_ initialDest: URL) -> URL {
var dest = initialDest
if FileManager.default.fileExists(atPath: dest.path) {
var counter = 1
var path: String
let ext = dest.pathExtension
let baseUrl = dest.deletingPathExtension()
repeat {
path = "\(baseUrl.path) (\(counter))"
if !ext.isEmpty {
path += ".\(ext)"
}
counter += 1
} while FileManager.default.fileExists(atPath: path)
dest = URL(fileURLWithPath: path)
}
return dest
}
private func processIntroductionFrame(_ frame: Sharing_Nearby_Frame) throws {
guard frame.hasV1, frame.v1.hasIntroduction else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.introduction") }
currentState = .waitingForUserConsent
if frame.v1.introduction.fileMetadata.count > 0 && frame.v1.introduction.textMetadata.isEmpty {
let saveDirectory = SaveFilesManager.shared.getSaveDirectory()
for file in frame.v1.introduction.fileMetadata {
let dest = makeFileDestinationURL(saveDirectory.appendingPathComponent(file.name))
let info = InternalFileInfo(meta: FileMetadata(name: file.name, size: file.size, mimeType: file.mimeType),
payloadID: file.payloadID,
destinationURL: dest)
filesToBeReceived[file.payloadID] = info
}
let metadata = TransferMetadata(files: filesToBeReceived.map { $0.value.meta }, id: id, pinCode: pinCode)
DispatchQueue.main.async {
self.delegate?.obtainUserConsent(for: metadata, from: self.remoteDeviceInfo!, connection: self)
}
}
else if let textMetadata = frame.v1.introduction.textMetadata.first {
if textMetadata.type == .url || textMetadata.type == .text {
let isClipboardText = textMetadata.type == .text
let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: textMetadata.textTitle, transferType: isClipboardText ? .text : .url)
textPayloadID = textMetadata.payloadID
if isClipboardText{
isPlainTextTransfer = true
}
DispatchQueue.main.async {
self.delegate?.obtainUserConsent(for: metadata, from: self.remoteDeviceInfo!, connection: self)
}
}
else {
rejectDueToUnsupportedFileType(frame)
}
} else {
rejectDueToUnsupportedFileType(frame)
}
}
func rejectDueToUnsupportedFileType(_ frame: Sharing_Nearby_Frame) {
log("[InboundNearbyConnection] Rejecting transfer due to unsupported file type. Frame is \(frame.debugDescription)")
NearbyConnectionManager.shared.mainAppDelegate?.showUnsupportedFileAlert(for: remoteDeviceInfo)
rejectTransfer(with: .unsupportedAttachmentType)
}
func submitUserConsent(accepted: Bool, storeInTemp: Bool = false) {
DispatchQueue.global(qos: .utility).async {
if accepted {
self.acceptTransfer(storeInTemp: storeInTemp)
} else {
self.rejectTransfer()
}
}
}
private func acceptTransfer(storeInTemp: Bool) {
if currentState == .disconnected {
log("[InboundNearbyConnection] Detected timeout, not accepting transfer")
return
}
do {
if storeInTemp {
try FileManager.default.createDirectory(at: SaveFilesManager.shared.tempDirectory, withIntermediateDirectories: true)
}
for (id, file) in filesToBeReceived {
let targetURL = storeInTemp ? SaveFilesManager.shared.tempDirectory.appendingPathComponent(file.destinationURL.lastPathComponent) : file.destinationURL
FileManager.default.createFile(atPath: targetURL.path, contents: nil)
let handle = try FileHandle(forWritingTo: targetURL)
filesToBeReceived[id]!.fileHandle = handle
let progress = Progress()
progress.fileURL = targetURL
progress.totalUnitCount = file.meta.size
progress.kind = .file
progress.isPausable = false
#if os(macOS)
progress.publish()
#endif
filesToBeReceived[id]!.progress = progress
filesToBeReceived[id]!.created = true
log("[InboundNearbyConnection] Accepted file with size \(file.meta.size)")
}
var frame = Sharing_Nearby_Frame()
frame.version = .v1
frame.v1.type = .response
frame.v1.connectionResponse.status = .accept
currentState = .receivingFiles
isTransferring = true
try sendTransferSetupFrame(frame)
} catch {
lastError = error
protocolError()
}
}
private func rejectTransfer(with reason: Sharing_Nearby_ConnectionResponseFrame.Status = .reject) {
self.wasRejected = true
log("[InboundNearbyConnection] Rejecting transfer because of \( reason)")
var frame = Sharing_Nearby_Frame()
frame.version = .v1
frame.v1.type = .response
frame.v1.connectionResponse.status = reason
do {
try sendTransferSetupFrame(frame)
try sendDisconnectionAndDisconnect()
} catch {
log("[InboundNearbyConnection] Error \(error)")
protocolError()
}
}
private func deletePartiallyReceivedFiles() {
for (_, file) in filesToBeReceived {
#if os(macOS)
if let progress = file.progress {
progress.unpublish()
}
#endif
guard file.created else { continue }
do {
try FileManager.default.removeItem(at: file.destinationURL)
}
catch {
// if it fails, we don't care. Could be because file was not created yet
}
}
}
}
@@ -0,0 +1,617 @@
//
// NearbyConnection.swift
// QuickDrop
//
// Created by Grishka on 09.04.2023.
//
import CommonCrypto
import CryptoKit
import Foundation
import Network
import System
import BigInt
import SwiftECC
class NearbyConnection {
private let maxFrameLength = 5 * 1024 * 1024
private static let dispatchQueue = DispatchQueue(label: "com.leonboettger.quickdrop.queue", qos: .utility)
let id: String
let connection: NWConnection
var remoteDeviceInfo: RemoteDeviceInfo?
var encryptionDone: Bool = false
var lastError: Error?
var bytesTransferred: Int64 = 0
var cancelled: Bool = false
var isTransferring: Bool = false {
didSet {
log("[NearbyConnection] Now transferring. Setting up data transfer inactivity timer.")
startDataTransferTimer(previousBytesTransferred: bytesTransferred)
}
}
private var payloadBuffers: [Int64: NSMutableData] = [:]
private var connectionClosed: Bool = false
private var inactivityTimer: DispatchSourceTimer?
private let timeoutInterval: TimeInterval = 30
// UKEY2-related state
var publicKey: ECPublicKey?
var privateKey: ECPrivateKey?
var ukeyClientInitMsgData: Data?
var ukeyServerInitMsgData: Data?
// SecureMessage encryption keys
var decryptKey: [UInt8]?
var encryptKey: [UInt8]?
var recvHmacKey: SymmetricKey?
var sendHmacKey: SymmetricKey?
// SecureMessage sequence numbers
private var serverSeq: Int32 = 0
private var clientSeq: Int32 = 0
private(set) var pinCode: String?
init(connection: NWConnection, id: String) {
self.connection = connection
self.id = id
}
func start() {
connection.stateUpdateHandler = { state in
if !self.connectionClosed {
if case .ready = state {
self.connectionReady()
self.receiveFrameAsync()
} else if case let .failed(err) = state {
// If connection reset by peer, it could still be a valid file transfer that has not been processed yet. => Wait
if err == .posix(.ECONNRESET) {
NearbyConnection.dispatchQueue.asyncAfter(deadline: .now() + 0.5) {
// If already closed, ignore the error, as file transfer has been processed
if !self.connectionClosed {
recordErrorAndDisconnect(err: err)
}
}
}
else {
recordErrorAndDisconnect(err: err)
}
}
}
else {
log("[NearbyConnection] Connection already closed, ignoring state update: \(state)")
}
}
connection.start(queue: NearbyConnection.dispatchQueue)
func recordErrorAndDisconnect(err: NWError) {
self.lastError = err
log("[NearbyConnection] Connection Error: \(err)")
// If the error is a connection reset, it could be due to firewall issues.
if err == .posix(.ENOTCONN) {
ConnectionFailureTracker.shared.recordFailure {
NearbyConnectionManager.shared.mainAppDelegate?.showFirewallAlert()
}
}
if err == .posix(.ENETDOWN) {
self.lastError = NearbyError.protocolError("Error.NetworkDown".localized())
}
self.disconnect()
}
}
func connectionReady() {}
func protocolError() {
log("[NearbyConnection] Protocol error: \(String(describing: lastError))")
disconnect()
}
func processReceivedFrame(frameData _: Data) {
fatalError()
}
func processTransferSetupFrame(_: Sharing_Nearby_Frame) throws {
fatalError()
}
func isServer() -> Bool {
fatalError()
}
func processFileChunk(frame _: Location_Nearby_Connections_PayloadTransferFrame) throws {
protocolError()
}
func processBytesPayload(payload _: Data, id _: Int64) throws -> Bool {
return false
}
private func receiveFrameAsync() {
connection.receive(minimumIncompleteLength: 4, maximumLength: 4) { content, _, isComplete, error in
if self.connectionClosed {
return
}
if isComplete {
log("[NearbyConnection] Connection closed by peer during receiveFrameAsync")
self.lastError = NearbyError.protocolError("Error.ClosedByPeer".localized())
self.disconnect()
return
}
if !(error == nil) {
self.lastError = error
self.protocolError()
return
}
guard let content = content else {
assertionFailure()
return
}
let frameLength = UInt32(content[0]) << 24 | UInt32(content[1]) << 16 | UInt32(content[2]) << 8 | UInt32(content[3])
guard frameLength < self.maxFrameLength else {
self.lastError = NearbyError.protocolError("Unexpected packet length")
self.protocolError()
return
}
self.startAndResetHeartbeatTimer()
self.receiveFrameAsync(length: frameLength)
}
}
private func receiveFrameAsync(length: UInt32) {
connection.receive(minimumIncompleteLength: Int(length), maximumLength: Int(length)) { content, _, isComplete, _ in
if self.connectionClosed {
return
}
if isComplete {
log("[NearbyConnection] Connection closed by peer during receiveFrameAsync")
self.lastError = NearbyError.protocolError("Error.ClosedByPeer".localized())
self.disconnect()
return
}
guard let content = content else {
self.protocolError()
return
}
self.startAndResetHeartbeatTimer()
self.processReceivedFrame(frameData: content)
self.receiveFrameAsync()
}
}
func sendFrameAsync(_ frame: Data, completion: (() -> Void)? = nil) {
if connectionClosed {
return
}
var lengthPrefixedData = Data(capacity: frame.count + 4)
let length: Int = frame.count
lengthPrefixedData.append(contentsOf: [
UInt8(truncatingIfNeeded: length >> 24),
UInt8(truncatingIfNeeded: length >> 16),
UInt8(truncatingIfNeeded: length >> 8),
UInt8(truncatingIfNeeded: length),
])
lengthPrefixedData.append(frame)
connection.send(content: lengthPrefixedData, completion: .contentProcessed { _ in
if let completion = completion {
completion()
}
})
}
func encryptAndSendOfflineFrame(_ frame: Location_Nearby_Connections_OfflineFrame, completion: (() -> Void)? = nil) throws {
var d2dMsg = Securegcm_DeviceToDeviceMessage()
serverSeq += 1
d2dMsg.sequenceNumber = serverSeq
d2dMsg.message = try frame.serializedData()
let serializedMsg = try [UInt8](d2dMsg.serializedData())
let iv = Data.randomData(length: 16)
var encryptedData = Data(count: serializedMsg.count + 16)
var encryptedLength: size_t = 0
encryptedData.withUnsafeMutableBytes {
let status = CCCrypt(
CCOperation(kCCEncrypt),
CCAlgorithm(kCCAlgorithmAES128),
CCOptions(kCCOptionPKCS7Padding),
encryptKey, kCCKeySizeAES256,
[UInt8](iv),
serializedMsg, serializedMsg.count,
$0.baseAddress, $0.count,
&encryptedLength
)
guard status == kCCSuccess else { fatalError("CCCrypt error: \(status)") }
}
var hb = Securemessage_HeaderAndBody()
hb.body = encryptedData.prefix(encryptedLength)
hb.header = Securemessage_Header()
hb.header.encryptionScheme = .aes256Cbc
hb.header.signatureScheme = .hmacSha256
hb.header.iv = iv
var md = Securegcm_GcmMetadata()
md.type = .deviceToDeviceMessage
md.version = 1
hb.header.publicMetadata = try md.serializedData()
var smsg = Securemessage_SecureMessage()
smsg.headerAndBody = try hb.serializedData()
smsg.signature = Data(HMAC<SHA256>.authenticationCode(for: smsg.headerAndBody, using: sendHmacKey!))
try sendFrameAsync(smsg.serializedData(), completion: completion)
}
func sendTransferSetupFrame(_ frame: Sharing_Nearby_Frame) throws {
log("[NearbyConnection] Sending transfer setup frame.")
try sendBytesPayload(data: frame.serializedData(), id: Int64.random(in: Int64.min ... Int64.max))
}
func sendBytesPayload(data: Data, id: Int64) throws {
var transfer = Location_Nearby_Connections_PayloadTransferFrame()
transfer.packetType = .data
transfer.payloadChunk.offset = 0
transfer.payloadChunk.flags = 0
transfer.payloadChunk.body = data
transfer.payloadHeader.id = id
transfer.payloadHeader.type = .bytes
transfer.payloadHeader.totalSize = Int64(transfer.payloadChunk.body.count)
transfer.payloadHeader.isSensitive = false
var wrapper = Location_Nearby_Connections_OfflineFrame()
wrapper.version = .v1
wrapper.v1 = Location_Nearby_Connections_V1Frame()
wrapper.v1.type = .payloadTransfer
wrapper.v1.payloadTransfer = transfer
try encryptAndSendOfflineFrame(wrapper)
transfer.payloadChunk.flags = 1 // .lastChunk
transfer.payloadChunk.offset = Int64(transfer.payloadChunk.body.count)
transfer.payloadChunk.clearBody()
wrapper.v1.payloadTransfer = transfer
try encryptAndSendOfflineFrame(wrapper)
}
func decryptAndProcessReceivedSecureMessage(_ smsg: Securemessage_SecureMessage) throws {
guard smsg.hasSignature, smsg.hasHeaderAndBody else { throw NearbyError.requiredFieldMissing("secureMessage.signature|headerAndBody") }
let hmac = Data(HMAC<SHA256>.authenticationCode(for: smsg.headerAndBody, using: recvHmacKey!))
guard hmac == smsg.signature else { throw NearbyError.protocolError("Error.Signature".localized()) }
let headerAndBody = try Securemessage_HeaderAndBody(serializedBytes: smsg.headerAndBody)
var decryptedData = Data(count: headerAndBody.body.count)
var decryptedLength = 0
decryptedData.withUnsafeMutableBytes {
let status = CCCrypt(
CCOperation(kCCDecrypt),
CCAlgorithm(kCCAlgorithmAES128),
CCOptions(kCCOptionPKCS7Padding),
decryptKey, kCCKeySizeAES256,
[UInt8](headerAndBody.header.iv),
[UInt8](headerAndBody.body), headerAndBody.body.count,
$0.baseAddress, $0.count,
&decryptedLength
)
guard status == kCCSuccess else { fatalError("CCCrypt error: \(status)") }
}
decryptedData = decryptedData.prefix(decryptedLength)
let d2dMsg = try Securegcm_DeviceToDeviceMessage(serializedBytes: decryptedData)
guard d2dMsg.hasMessage, d2dMsg.hasSequenceNumber else { throw NearbyError.requiredFieldMissing("d2dMessage.message|sequenceNumber") }
clientSeq += 1
guard d2dMsg.sequenceNumber == clientSeq else { throw NearbyError.protocolError("Wrong sequence number. Expected \(clientSeq), got \(d2dMsg.sequenceNumber)") }
let offlineFrame = try Location_Nearby_Connections_OfflineFrame(serializedBytes: d2dMsg.message)
if offlineFrame.hasV1, offlineFrame.v1.hasType, case .payloadTransfer = offlineFrame.v1.type {
guard offlineFrame.v1.hasPayloadTransfer else { throw NearbyError.requiredFieldMissing("offlineFrame.v1.payloadTransfer") }
let payloadTransfer = offlineFrame.v1.payloadTransfer
let header = payloadTransfer.payloadHeader
let chunk = payloadTransfer.payloadChunk
guard header.hasType, header.hasID else { throw NearbyError.requiredFieldMissing("payloadHeader.type|id") }
guard payloadTransfer.hasPayloadChunk, chunk.hasOffset, chunk.hasFlags else {
if payloadTransfer.controlMessage.event == .payloadCanceled {
log("[NearbyConnection] Cancel control frame received.")
throw NearbyError.canceled(reason: .userCanceled)
}
log("[NearbyConnection] Payload transfer chunk is missing offset or flags. Frame is \(payloadTransfer.debugDescription)")
throw NearbyError.requiredFieldMissing("payloadChunk.offset|flags")
}
if case .bytes = header.type {
let payloadID = header.id
if header.totalSize > maxFrameLength {
payloadBuffers.removeValue(forKey: payloadID)
throw NearbyError.protocolError("Payload too large (\(header.totalSize) bytes)")
}
if payloadBuffers[payloadID] == nil {
payloadBuffers[payloadID] = NSMutableData(capacity: Int(header.totalSize))
}
let buffer = payloadBuffers[payloadID]!
guard chunk.offset == buffer.count else {
payloadBuffers.removeValue(forKey: payloadID)
throw NearbyError.protocolError("Unexpected chunk offset \(chunk.offset), expected \(buffer.count)")
}
if chunk.hasBody {
buffer.append(chunk.body)
}
if (chunk.flags & 1) == 1 {
payloadBuffers.removeValue(forKey: payloadID)
if try !processBytesPayload(payload: Data(buffer), id: payloadID) {
let innerFrame = try Sharing_Nearby_Frame(serializedBytes: buffer as Data)
try processTransferSetupFrame(innerFrame)
}
}
} else if case .file = header.type {
try processFileChunk(frame: payloadTransfer)
}
}
else if offlineFrame.hasV1, offlineFrame.v1.hasType, case .keepAlive = offlineFrame.v1.type {
log("[NearbyConnection] Sent keep-alive, \(self.bytesTransferred) bytes sent")
sendKeepAlive(ack: true)
} else {
log("[NearbyConnection] Unhandled offline frame encrypted: \(offlineFrame)")
}
}
static func pinCodeFromAuthKey(_ key: SymmetricKey) -> String {
var hash = 0
var multiplier = 1
let keyBytes: [UInt8] = key.withUnsafeBytes {
[UInt8]($0)
}
for _byte in keyBytes {
let byte = Int(Int8(bitPattern: _byte))
hash = (hash + byte * multiplier) % 9973
multiplier = (multiplier * 31) % 9973
}
return String(format: "%04d", abs(hash))
}
static func hkdfExtract(salt: Data, ikm: Data) -> Data {
return HMAC<SHA256>.authenticationCode(for: ikm, using: SymmetricKey(data: salt)).withUnsafeBytes { Data(bytes: $0.baseAddress!, count: $0.count) }
}
static func hkdfExpand(prk: Data, info: Data, length: Int) -> Data {
var okm = Data()
var t = Data()
var i = 0
while okm.count < length {
i = i + 1
let toDigest = t + info + Data([UInt8(truncatingIfNeeded: i)])
t = HMAC<SHA256>.authenticationCode(for: toDigest, using: SymmetricKey(data: prk)).withUnsafeBytes { Data(bytes: $0.baseAddress!, count: $0.count) }
okm = okm + t
}
return okm.subdata(in: 0 ..< length)
}
static func hkdf(inputKeyMaterial: SymmetricKey, salt: Data, info: Data, outputByteCount: Int) -> SymmetricKey {
return HKDF<SHA256>.deriveKey(inputKeyMaterial: inputKeyMaterial, salt: salt, info: info, outputByteCount: outputByteCount)
}
func finalizeKeyExchange(peerKey: Securemessage_GenericPublicKey) throws {
guard peerKey.hasEcP256PublicKey else { throw NearbyError.requiredFieldMissing("peerKey.ecP256PublicKey") }
let domain = Domain.instance(curve: .EC256r1)
var clientX = peerKey.ecP256PublicKey.x
var clientY = peerKey.ecP256PublicKey.y
if clientX.count > 32 {
clientX = clientX.suffix(32)
}
if clientY.count > 32 {
clientY = clientY.suffix(32)
}
let key = try ECPublicKey(domain: domain, w: Point(BInt(magnitude: [UInt8](clientX)), BInt(magnitude: [UInt8](clientY))))
let dhs = try (privateKey?.domain.multiplyPoint(key.w, privateKey!.s).x.asMagnitudeBytes())!
var sha = SHA256()
sha.update(data: dhs)
let derivedSecretKey = Data(sha.finalize())
var ukeyInfo = Data()
ukeyInfo.append(ukeyClientInitMsgData!)
ukeyInfo.append(ukeyServerInitMsgData!)
let authString = NearbyConnection.hkdf(inputKeyMaterial: SymmetricKey(data: derivedSecretKey), salt: "UKEY2 v1 auth".data(using: .utf8)!, info: ukeyInfo, outputByteCount: 32)
let nextSecret = NearbyConnection.hkdf(inputKeyMaterial: SymmetricKey(data: derivedSecretKey), salt: "UKEY2 v1 next".data(using: .utf8)!, info: ukeyInfo, outputByteCount: 32)
pinCode = NearbyConnection.pinCodeFromAuthKey(authString)
let salt = Data([0x82, 0xAA, 0x55, 0xA0, 0xD3, 0x97, 0xF8, 0x83, 0x46, 0xCA, 0x1C,
0xEE, 0x8D, 0x39, 0x09, 0xB9, 0x5F, 0x13, 0xFA, 0x7D, 0xEB, 0x1D,
0x4A, 0xB3, 0x83, 0x76, 0xB8, 0x25, 0x6D, 0xA8, 0x55, 0x10])
let d2dClientKey = NearbyConnection.hkdf(inputKeyMaterial: nextSecret, salt: salt, info: "client".data(using: .utf8)!, outputByteCount: 32)
let d2dServerKey = NearbyConnection.hkdf(inputKeyMaterial: nextSecret, salt: salt, info: "server".data(using: .utf8)!, outputByteCount: 32)
sha = SHA256()
sha.update(data: "SecureMessage".data(using: .utf8)!)
let smsgSalt = Data(sha.finalize())
let clientKey = NearbyConnection.hkdf(inputKeyMaterial: d2dClientKey, salt: smsgSalt, info: "ENC:2".data(using: .utf8)!, outputByteCount: 32).withUnsafeBytes { [UInt8]($0) }
let clientHmacKey = NearbyConnection.hkdf(inputKeyMaterial: d2dClientKey, salt: smsgSalt, info: "SIG:1".data(using: .utf8)!, outputByteCount: 32)
let serverKey = NearbyConnection.hkdf(inputKeyMaterial: d2dServerKey, salt: smsgSalt, info: "ENC:2".data(using: .utf8)!, outputByteCount: 32).withUnsafeBytes { [UInt8]($0) }
let serverHmacKey = NearbyConnection.hkdf(inputKeyMaterial: d2dServerKey, salt: smsgSalt, info: "SIG:1".data(using: .utf8)!, outputByteCount: 32)
if isServer() {
decryptKey = clientKey
recvHmacKey = clientHmacKey
encryptKey = serverKey
sendHmacKey = serverHmacKey
} else {
decryptKey = serverKey
recvHmacKey = serverHmacKey
encryptKey = clientKey
sendHmacKey = clientHmacKey
}
}
func disconnect() {
log("[NearbyConnection] Disconnecting from connection.")
connection.stateUpdateHandler = nil
inactivityTimer?.cancel()
inactivityTimer = nil
connectionClosed = true
connection.send(content: nil, isComplete: true, completion: .contentProcessed { _ in })
}
func sendDisconnectionAndDisconnect() throws {
var offlineFrame = Location_Nearby_Connections_OfflineFrame()
offlineFrame.version = .v1
offlineFrame.v1.type = .disconnection
offlineFrame.v1.disconnection = Location_Nearby_Connections_DisconnectionFrame()
if encryptionDone {
try encryptAndSendOfflineFrame(offlineFrame)
} else {
try sendFrameAsync(offlineFrame.serializedData())
}
log("[NearbyConnection] Sent disconnection frame during sendDisconnectionAndDisconnect")
disconnect()
}
func sendUkey2Alert(type: Securegcm_Ukey2Alert.AlertType) {
var alert = Securegcm_Ukey2Alert()
alert.type = type
var msg = Securegcm_Ukey2Message()
msg.messageType = .alert
msg.messageData = try! alert.serializedData()
sendFrameAsync(try! msg.serializedData())
log("[NearbyConnection] Sent UKEY2 alert: \(type)")
disconnect()
}
func sendKeepAlive(ack: Bool) {
var offlineFrame = Location_Nearby_Connections_OfflineFrame()
offlineFrame.version = .v1
offlineFrame.v1.type = .keepAlive
offlineFrame.v1.keepAlive.ack = ack
do {
if encryptionDone {
try encryptAndSendOfflineFrame(offlineFrame)
} else {
try sendFrameAsync(offlineFrame.serializedData())
}
} catch {
log("[NearbyConnection] Error sending KEEP_ALIVE: \(error)")
}
}
func startAndResetHeartbeatTimer() {
// Cancel previous timer if any
inactivityTimer?.cancel()
inactivityTimer = DispatchSource.makeTimerSource(queue: NearbyConnection.dispatchQueue)
inactivityTimer?.schedule(deadline: .now() + timeoutInterval)
inactivityTimer?.setEventHandler { [weak self] in
guard let self = self else { return }
if !self.connectionClosed {
log("[NearbyConnection] Connection timeout: No message received in \(self.timeoutInterval) seconds")
self.lastError = NearbyError.canceled(reason: .timedOut)
self.disconnect()
}
}
inactivityTimer?.resume()
}
func startDataTransferTimer(previousBytesTransferred: Int64) {
NearbyConnection.dispatchQueue.asyncAfter(deadline: .now() + timeoutInterval) {
if previousBytesTransferred < self.bytesTransferred {
// everything good, transferred more than last check, schedule next check
self.startDataTransferTimer(previousBytesTransferred: self.bytesTransferred)
}
else {
// connection stale, need to abort
if !self.connectionClosed {
log("[NearbyConnection] Connection timeout: No more data received in \(self.timeoutInterval) seconds")
self.lastError = NearbyError.canceled(reason: .timedOut)
self.disconnect()
}
}
}
}
// -- MARK: - Internal Data Model
struct InternalFileInfo {
let meta: FileMetadata
let payloadID: Int64
let destinationURL: URL
var bytesTransferred: Int64 = 0
var fileHandle: FileHandle?
var progress: Progress?
var created: Bool = false
}
}
@@ -0,0 +1,335 @@
//
// NearbyConnectionManager.swift
// QuickDrop
//
// Created by Grishka on 08.04.2023.
//
import Foundation
import Network
import System
#if os(iOS)
import UIKit
#endif
public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearbyConnectionDelegate, OutboundNearbyConnectionDelegate {
private let sleepManager = SleepManager.shared
private var tcpListener: NWListener
private var mdnsServices: [NetService] = []
private var incomingConnections: [String: InboundNearbyConnection] = [:]
private var foundServices: [String: FoundServiceInfo] = [:]
private var shareExtensionDelegates: [ShareExtensionDelegate] = []
private var outgoingTransfers: [String: OutgoingTransferInfo] = [:]
private var startedDeviceDiscovery = false
private var browsers: [NWBrowser] = []
private let serviceTypes = ["_FC9F5ED42C8A._tcp."]
public let endpointID: [UInt8] = generateEndpointID()
public var mainAppDelegate: (any MainAppDelegate)?
public static let shared = NearbyConnectionManager()
override init() {
tcpListener = try! NWListener(using: NWParameters(tls: .none))
super.init()
}
public func becomeVisible() {
startTCPListener()
}
private func startTCPListener() {
tcpListener.stateUpdateHandler = { (state: NWListener.State) in
if case .ready = state {
self.initMDNS()
}
}
tcpListener.newConnectionHandler = { (connection: NWConnection) in
let id = UUID().uuidString
let conn = InboundNearbyConnection(connection: connection, id: id)
self.incomingConnections[id] = conn
conn.delegate = self
conn.start()
}
tcpListener.start(queue: .global(qos: .utility))
}
private static func generateEndpointID() -> [UInt8] {
let userDefaultsKey = UserDefaultsKeys.endpointID.rawValue
// Try to retrieve from UserDefaults
if let savedString = UserDefaults.standard.string(forKey: userDefaultsKey),
let savedData = savedString.data(using: .utf8)
{
return [UInt8](savedData)
}
// Generate a new random ID
var id: [UInt8] = []
let alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".compactMap { UInt8($0.asciiValue!) }
for _ in 0 ..< 4 {
id.append(alphabet[Int.random(in: 0 ..< alphabet.count)])
}
// Save to UserDefaults as String
let idString = String(bytes: id, encoding: .utf8) ?? ""
UserDefaults.standard.set(idString, forKey: userDefaultsKey)
return id
}
private func initMDNS() {
let nameBytes: [UInt8] = [
0x23, // PCP
endpointID[0], endpointID[1], endpointID[2], endpointID[3],
0xFC, 0x9F, 0x5E, // Service ID hash
0, 0,
]
let name = Data(nameBytes).urlSafeBase64EncodedString()
let port = Int32(tcpListener.port!.rawValue)
mdnsServices = serviceTypes.map { serviceType in
let service = NetService(domain: "", type: serviceType, name: name, port: port)
service.delegate = self
service.setTXTRecord(NetService.data(fromTXTRecord: [
"n": getEndpointInfo().serialize().urlSafeBase64EncodedString().data(using: .utf8)!,
]))
service.publish()
return service
}
}
func getEndpointInfo() -> EndpointInfo {
#if os(macOS)
let endpointInfo = EndpointInfo(name: Host.current().localizedName!, deviceType: .computer)
#else
let endpointInfo = EndpointInfo(name: UIDevice.current.name, deviceType: .phone)
#endif
return endpointInfo
}
func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo, connection _: InboundNearbyConnection) {
mainAppDelegate?.obtainUserConsent(for: transfer, from: device)
}
func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?) {
incomingConnections.removeValue(forKey: connection.id)
if !connection.wasRejected {
mainAppDelegate?.incomingTransfer(id: connection.id, didFinishWith: error)
}
}
public func submitUserConsent(transferID: String, accept: Bool, storeInTemp: Bool = false) {
guard let conn = incomingConnections[transferID] else { return }
log("[NearbyConnectionManager] Submitting user consent for transfer, accepted: \(accept), store in temp: \(storeInTemp)")
conn.submitUserConsent(accepted: accept, storeInTemp: storeInTemp)
}
public func startDeviceDiscovery() {
if !startedDeviceDiscovery {
startedDeviceDiscovery = true
foundServices.removeAll()
log("[NearbyConnectionManager] Starting device discovery")
if browsers.isEmpty {
for type in serviceTypes {
log("[NearbyConnectionManager] Starting browser for type \(type)")
let browser = NWBrowser(for: .bonjourWithTXTRecord(type: type, domain: nil), using: .tcp)
browser.browseResultsChangedHandler = { _, changes in
for change in changes {
switch change {
case let .added(res):
self.addFoundDevice(service: res)
case let .removed(res):
self.removeFoundDevice(service: res)
default:
log("[NearbyConnectionManager] Ignoring change \(change)")
}
}
}
browser.start(queue: .main)
browsers.append(browser)
}
}
}
}
public func stopDeviceDiscovery() {
if startedDeviceDiscovery {
log("Stopping device discovery")
for browser in browsers {
browser.cancel()
}
browsers.removeAll()
startedDeviceDiscovery = false
}
}
public func addShareExtensionDelegate(_ delegate: ShareExtensionDelegate) {
shareExtensionDelegates.append(delegate)
for service in foundServices.values {
guard let device = service.device else { continue }
delegate.addDevice(device: device)
}
}
public func removeShareExtensionDelegate(_ delegate: ShareExtensionDelegate) {
shareExtensionDelegates.removeAll(where: { $0 === delegate })
}
public func cancelOutgoingTransfer(id: String) {
guard let transfer = outgoingTransfers[id] else { return }
transfer.connection.cancel()
}
private func endpointID(for service: NWBrowser.Result) -> String? {
guard case let NWEndpoint.service(name: serviceName, type: _, domain: _, interface: _) = service.endpoint else { return nil }
guard let nameData = Data.dataFromUrlSafeBase64(serviceName) else { return nil }
guard nameData.count >= 10 else { return nil }
let pcp = nameData[0]
guard pcp == 0x23 else { return nil }
let endpointID = String(data: nameData.subdata(in: 1 ..< 5), encoding: .ascii)!
let serviceIDHash = nameData.subdata(in: 5 ..< 8)
guard serviceIDHash == Data([0xFC, 0x9F, 0x5E]) else { return nil }
return endpointID
}
private func addFoundDevice(service: NWBrowser.Result) {
log("[NearbyConnectionManager] Found Service \(service)")
for interface in service.interfaces {
if case .loopback = interface.type {
return
}
}
guard let endpointID = endpointID(for: service) else { return }
var foundService = FoundServiceInfo(service: service)
guard case let NWBrowser.Result.Metadata.bonjour(txtRecord) = service.metadata else { return }
guard let endpointInfoEncoded = txtRecord.dictionary["n"] else { return }
guard let endpointInfo = Data.dataFromUrlSafeBase64(endpointInfoEncoded) else { return }
guard endpointInfo.count >= 19 else { return }
let deviceType = RemoteDeviceInfo.DeviceType.fromRawValue(value: (Int(endpointInfo[0]) >> 1) & 7)
let deviceNameLength = Int(endpointInfo[17])
guard endpointInfo.count >= deviceNameLength + 17 else { return }
guard let deviceName = String(data: endpointInfo.subdata(in: 18 ..< (18 + deviceNameLength)), encoding: .utf8) else { return }
let deviceInfo = RemoteDeviceInfo(name: deviceName, type: deviceType, id: endpointID)
foundService.device = deviceInfo
foundServices[endpointID] = foundService
for delegate in shareExtensionDelegates {
delegate.addDevice(device: deviceInfo)
}
}
private func removeFoundDevice(service: NWBrowser.Result) {
guard let endpointID = endpointID(for: service) else { return }
guard let _ = foundServices.removeValue(forKey: endpointID) else { return }
for delegate in shareExtensionDelegates {
delegate.removeDevice(id: endpointID)
}
}
public func startOutgoingTransfer(deviceID: String, delegate: ShareExtensionDelegate, urls: [URL], textToSend: String?) {
guard let info = foundServices[deviceID] else { return }
let tcp = NWProtocolTCP.Options()
tcp.noDelay = true
let nwconn = NWConnection(to: info.service.endpoint, using: NWParameters(tls: .none, tcp: tcp))
let conn = OutboundNearbyConnection(connection: nwconn, id: deviceID, urlsToSend: urls, textToSend: textToSend)
conn.delegate = self
let transfer = OutgoingTransferInfo(service: info.service, device: info.device!, connection: conn, delegate: delegate)
outgoingTransfers[deviceID] = transfer
conn.start()
}
func outboundConnectionWasEstablished(connection: OutboundNearbyConnection) {
guard let transfer = outgoingTransfers[connection.id] else { return }
DispatchQueue.main.async {
transfer.delegate.connectionWasEstablished(pinCode: connection.pinCode!)
}
}
func outboundConnectionTransferAccepted(connection: OutboundNearbyConnection) {
guard let transfer = outgoingTransfers[connection.id] else { return }
DispatchQueue.main.async {
transfer.delegate.transferAccepted()
}
}
func outboundConnection(connection: OutboundNearbyConnection, transferProgress: Double) {
guard let transfer = outgoingTransfers[connection.id] else { return }
DispatchQueue.main.async {
transfer.delegate.transferProgress(progress: transferProgress)
}
}
func outboundConnection(connection: OutboundNearbyConnection, failedWithError: Error) {
guard let transfer = outgoingTransfers[connection.id] else { return }
DispatchQueue.main.async {
transfer.delegate.connectionFailed(with: failedWithError)
}
outgoingTransfers.removeValue(forKey: connection.id)
}
func outboundConnectionTransferFinished(connection: OutboundNearbyConnection) {
guard let transfer = outgoingTransfers[connection.id] else { return }
DispatchQueue.main.async {
transfer.delegate.transferFinished()
}
outgoingTransfers.removeValue(forKey: connection.id)
}
public func getActiveConnectionsCount() -> Int {
return incomingConnections.count + outgoingTransfers.count
}
// -- MARK: - Internal Data Model
private struct FoundServiceInfo {
let service: NWBrowser.Result
var device: RemoteDeviceInfo?
}
private struct OutgoingTransferInfo {
let service: NWBrowser.Result
let device: RemoteDeviceInfo
let connection: OutboundNearbyConnection
let delegate: ShareExtensionDelegate
}
}
@@ -0,0 +1,501 @@
//
// OutboundNearbyConnection.swift
// NearbyShare
//
// Created by Grishka on 23.09.2023.
//
import CommonCrypto
import CryptoKit
import Foundation
import Network
import System
import UniformTypeIdentifiers
import BigInt
import SwiftECC
class OutboundNearbyConnection: NearbyConnection {
private var currentState: State = .initial
private let urlsToSend: [URL]
private let textToSend: String?
private var ukeyClientFinishMsgData: Data?
private var queue: [OutgoingFileTransfer] = []
private var currentTransfer: OutgoingFileTransfer?
private var totalBytesToSend: Int64 = 0
private var totalBytesSent: Int64 = 0
private var textPayloadID: Int64 = 0
public var delegate: OutboundNearbyConnectionDelegate?
enum State {
case initial, sentUkeyClientInit, sentUkeyClientFinish, sentPairedKeyEncryption, sentPairedKeyResult, sentIntroduction, sendingFiles
}
init(connection: NWConnection, id: String, urlsToSend: [URL], textToSend: String?) {
self.urlsToSend = urlsToSend
self.textToSend = textToSend
super.init(connection: connection, id: id)
if hasURL() || textToSend != nil {
textPayloadID = Int64.random(in: Int64.min ... Int64.max)
}
}
deinit {
if let transfer = currentTransfer, let handle = transfer.handle {
try? handle.close()
}
for transfer in queue {
if let handle = transfer.handle {
try? handle.close()
}
}
}
override func disconnect() {
super.disconnect()
if let error = lastError {
DispatchQueue.main.async {
self.delegate?.outboundConnection(connection: self, failedWithError: error)
}
}
}
public func cancel() {
cancelled = true
if encryptionDone {
var cancel = Sharing_Nearby_Frame()
cancel.version = .v1
cancel.v1 = Sharing_Nearby_V1Frame()
cancel.v1.type = .cancel
try? sendTransferSetupFrame(cancel)
}
try? sendDisconnectionAndDisconnect()
}
override func connectionReady() {
super.connectionReady()
do {
try sendConnectionRequest()
try sendUkey2ClientInit()
} catch {
lastError = error
protocolError()
}
}
override func isServer() -> Bool {
return false
}
override func processReceivedFrame(frameData: Data) {
if currentState != .sendingFiles {
log("[OutboundNearbyConnection] Received frame in state \(currentState)...")
}
do {
switch currentState {
case .initial:
protocolError()
case .sentUkeyClientInit:
try processUkey2ServerInit(frame: Securegcm_Ukey2Message(serializedBytes: frameData), raw: frameData)
case .sentUkeyClientFinish:
try processConnectionResponse(frame: Location_Nearby_Connections_OfflineFrame(serializedBytes: frameData))
default:
let smsg = try Securemessage_SecureMessage(serializedBytes: frameData)
try decryptAndProcessReceivedSecureMessage(smsg)
}
} catch {
log("[OutboundNearbyConnection] Error occured while processing frame with data \(frameData.hex): \(error)")
if case NearbyError.ukey2 = error {
// do nothing
}
else if currentState == .sentUkeyClientInit {
sendUkey2Alert(type: .badMessage)
}
lastError = error
protocolError()
}
}
override func processTransferSetupFrame(_ frame: Sharing_Nearby_Frame) throws {
if frame.hasV1 && frame.v1.hasType, case .cancel = frame.v1.type {
self.cancelled = true
self.lastError = NearbyError.canceled(reason: .userCanceled)
log("[OutboundNearbyConnection] Transfer canceled")
try sendDisconnectionAndDisconnect()
delegate?.outboundConnection(connection: self, failedWithError: self.lastError!)
return
}
switch currentState {
case .sentPairedKeyEncryption:
try processPairedKeyEncryption(frame: frame)
case .sentPairedKeyResult:
try processPairedKeyResult(frame: frame)
case .sentIntroduction:
try processConsent(frame: frame)
case .sendingFiles:
break
default:
assertionFailure("[OutboundNearbyConnection] Unexpected state \(currentState)")
}
}
override func protocolError() {
super.protocolError()
delegate?.outboundConnection(connection: self, failedWithError: lastError!)
}
private func sendConnectionRequest() throws {
let endpointInfo = NearbyConnectionManager.shared.getEndpointInfo()
var frame = Location_Nearby_Connections_OfflineFrame()
frame.version = .v1
frame.v1 = Location_Nearby_Connections_V1Frame()
frame.v1.type = .connectionRequest
frame.v1.connectionRequest = Location_Nearby_Connections_ConnectionRequestFrame()
frame.v1.connectionRequest.endpointID = String(bytes: NearbyConnectionManager.shared.endpointID, encoding: .ascii)!
frame.v1.connectionRequest.endpointName = endpointInfo.name
frame.v1.connectionRequest.endpointInfo = endpointInfo.serialize()
frame.v1.connectionRequest.mediums = [.wifiLan]
try sendFrameAsync(frame.serializedData())
}
private func sendUkey2ClientInit() throws {
let domain = Domain.instance(curve: .EC256r1)
let (pubKey, privKey) = domain.makeKeyPair()
publicKey = pubKey
privateKey = privKey
var finishFrame = Securegcm_Ukey2Message()
finishFrame.messageType = .clientFinish
var finish = Securegcm_Ukey2ClientFinished()
var pkey = Securemessage_GenericPublicKey()
pkey.type = .ecP256
pkey.ecP256PublicKey = Securemessage_EcP256PublicKey()
pkey.ecP256PublicKey.x = Data(pubKey.w.x.asSignedBytes())
pkey.ecP256PublicKey.y = Data(pubKey.w.y.asSignedBytes())
finish.publicKey = try pkey.serializedData()
finishFrame.messageData = try finish.serializedData()
ukeyClientFinishMsgData = try finishFrame.serializedData()
var frame = Securegcm_Ukey2Message()
frame.messageType = .clientInit
var clientInit = Securegcm_Ukey2ClientInit()
clientInit.version = 1
clientInit.random = Data.randomData(length: 32)
clientInit.nextProtocol = "AES_256_CBC-HMAC_SHA256"
var sha = SHA512()
sha.update(data: ukeyClientFinishMsgData!)
var commitment = Securegcm_Ukey2ClientInit.CipherCommitment()
commitment.commitment = Data(sha.finalize())
commitment.handshakeCipher = .p256Sha512
clientInit.cipherCommitments.append(commitment)
frame.messageData = try clientInit.serializedData()
ukeyClientInitMsgData = try frame.serializedData()
sendFrameAsync(ukeyClientInitMsgData!)
currentState = .sentUkeyClientInit
}
private func processUkey2ServerInit(frame: Securegcm_Ukey2Message, raw: Data) throws {
ukeyServerInitMsgData = raw
guard frame.messageType == .serverInit else {
sendUkey2Alert(type: .badMessageType)
log("[OutboundNearbyConnection] Invalid message type: \(frame.messageType)")
throw NearbyError.ukey2
}
let serverInit = try Securegcm_Ukey2ServerInit(serializedBytes: frame.messageData)
guard serverInit.version == 1 else {
sendUkey2Alert(type: .badVersion)
log("[OutboundNearbyConnection] Invalid version: \(serverInit.version)")
throw NearbyError.ukey2
}
guard serverInit.random.count == 32 else {
sendUkey2Alert(type: .badRandom)
log("[OutboundNearbyConnection] Invalid random: \(serverInit.random.count)")
throw NearbyError.ukey2
}
guard serverInit.handshakeCipher == .p256Sha512 else {
sendUkey2Alert(type: .badHandshakeCipher)
log("[OutboundNearbyConnection] Invalid handshake cipher: \(serverInit.handshakeCipher)")
throw NearbyError.ukey2
}
let serverKey = try Securemessage_GenericPublicKey(serializedBytes: serverInit.publicKey)
try finalizeKeyExchange(peerKey: serverKey)
sendFrameAsync(ukeyClientFinishMsgData!)
currentState = .sentUkeyClientFinish
var resp = Location_Nearby_Connections_OfflineFrame()
resp.version = .v1
resp.v1 = Location_Nearby_Connections_V1Frame()
resp.v1.type = .connectionResponse
resp.v1.connectionResponse = Location_Nearby_Connections_ConnectionResponseFrame()
resp.v1.connectionResponse.response = .accept
resp.v1.connectionResponse.status = 0
resp.v1.connectionResponse.osInfo = Location_Nearby_Connections_OsInfo()
resp.v1.connectionResponse.osInfo.type = .apple
try sendFrameAsync(resp.serializedData())
encryptionDone = true
delegate?.outboundConnectionWasEstablished(connection: self)
}
private func processConnectionResponse(frame: Location_Nearby_Connections_OfflineFrame) throws {
guard frame.version == .v1 else { throw NearbyError.protocolError("Unexpected offline frame version \(frame.version)") }
guard frame.v1.type == .connectionResponse else { throw NearbyError.protocolError("Unexpected frame type \(frame.v1.type)") }
guard frame.v1.connectionResponse.response == .accept else { throw NearbyError.protocolError("Connection was rejected by recipient") }
var pairedEncryption = Sharing_Nearby_Frame()
pairedEncryption.version = .v1
pairedEncryption.v1 = Sharing_Nearby_V1Frame()
pairedEncryption.v1.type = .pairedKeyEncryption
pairedEncryption.v1.pairedKeyEncryption = Sharing_Nearby_PairedKeyEncryptionFrame()
pairedEncryption.v1.pairedKeyEncryption.secretIDHash = Data.randomData(length: 6)
pairedEncryption.v1.pairedKeyEncryption.signedData = Data.randomData(length: 72)
try sendTransferSetupFrame(pairedEncryption)
currentState = .sentPairedKeyEncryption
}
private func processPairedKeyEncryption(frame: Sharing_Nearby_Frame) throws {
guard frame.hasV1, frame.v1.hasPairedKeyEncryption else { throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.pairedKeyEncryption") }
var pairedResult = Sharing_Nearby_Frame()
pairedResult.version = .v1
pairedResult.v1 = Sharing_Nearby_V1Frame()
pairedResult.v1.type = .pairedKeyResult
pairedResult.v1.pairedKeyResult = Sharing_Nearby_PairedKeyResultFrame()
pairedResult.v1.pairedKeyResult.status = .unable
try sendTransferSetupFrame(pairedResult)
currentState = .sentPairedKeyResult
}
private func processPairedKeyResult(frame: Sharing_Nearby_Frame) throws {
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.pairedKeyResult") }
var introduction = Sharing_Nearby_Frame()
introduction.version = .v1
introduction.v1.type = .introduction
if let textToSend = textToSend {
var meta = Sharing_Nearby_TextMetadata()
meta.type = .text
meta.textTitle = textToSend
meta.size = Int64(textToSend.utf8.count)
meta.payloadID = textPayloadID
introduction.v1.introduction.textMetadata.append(meta)
} else if hasURL() {
var meta = Sharing_Nearby_TextMetadata()
meta.type = .url
meta.textTitle = urlsToSend[0].host ?? "URL"
meta.size = Int64(urlsToSend[0].absoluteString.utf8.count)
meta.payloadID = textPayloadID
introduction.v1.introduction.textMetadata.append(meta)
} else {
for url in urlsToSend {
guard url.isFileURL else { continue }
var meta = Sharing_Nearby_FileMetadata()
meta.name = OutboundNearbyConnection.sanitizeFileName(name: url.lastPathComponent)
let attrs = try FileManager.default.attributesOfItem(atPath: url.path)
meta.size = (attrs[FileAttributeKey.size] as! NSNumber).int64Value
let typeID = try? url.resourceValues(forKeys: [.typeIdentifierKey]).typeIdentifier
meta.mimeType = "application/octet-stream"
if let typeID = typeID {
let type = UTType(typeID)
if let type = type, let mimeType = type.preferredMIMEType {
meta.mimeType = mimeType
}
}
if meta.mimeType.starts(with: "image/") {
meta.type = .image
} else if meta.mimeType.starts(with: "video/") {
meta.type = .video
} else if meta.mimeType.starts(with: "audio/") {
meta.type = .audio
} else if url.pathExtension.lowercased() == "apk" {
meta.type = .app
} else {
meta.type = .unknown
}
meta.payloadID = Int64.random(in: Int64.min ... Int64.max)
try queue.append(OutgoingFileTransfer(url: url, payloadID: meta.payloadID, handle: FileHandle(forReadingFrom: url), totalBytes: meta.size, currentOffset: 0))
introduction.v1.introduction.fileMetadata.append(meta)
totalBytesToSend += meta.size
log("[OutboundNearbyConnection] Sending file with \(meta.size) bytes")
}
}
try sendTransferSetupFrame(introduction)
currentState = .sentIntroduction
}
private func processConsent(frame: Sharing_Nearby_Frame) throws {
guard frame.version == .v1, frame.v1.type == .response else { throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.type==response") }
switch frame.v1.connectionResponse.status {
case .accept:
currentState = .sendingFiles
isTransferring = true
delegate?.outboundConnectionTransferAccepted(connection: self)
if let textToSend = textToSend {
try sendText(text: textToSend)
}
if hasURL() {
try sendURL()
} else {
try sendNextFileChunk()
}
case .reject, .unknown:
delegate?.outboundConnection(connection: self, failedWithError: NearbyError.canceled(reason: .userRejected))
try sendDisconnectionAndDisconnect()
case .notEnoughSpace:
delegate?.outboundConnection(connection: self, failedWithError: NearbyError.canceled(reason: .notEnoughSpace))
try sendDisconnectionAndDisconnect()
case .timedOut:
delegate?.outboundConnection(connection: self, failedWithError: NearbyError.canceled(reason: .timedOut))
try sendDisconnectionAndDisconnect()
case .unsupportedAttachmentType:
delegate?.outboundConnection(connection: self, failedWithError: NearbyError.canceled(reason: .unsupportedType))
try sendDisconnectionAndDisconnect()
}
}
private func hasURL() -> Bool {
urlsToSend.count == 1 && !urlsToSend[0].isFileURL
}
private func sendURL() throws {
try sendText(text: urlsToSend[0].absoluteString)
}
private func sendText(text: String) throws {
try sendBytesPayload(data: Data(text.utf8), id: textPayloadID)
delegate?.outboundConnectionTransferFinished(connection: self)
try sendDisconnectionAndDisconnect()
}
private func sendNextFileChunk() throws {
if cancelled {
return
}
if currentTransfer == nil || currentTransfer?.currentOffset == currentTransfer?.totalBytes {
if currentTransfer != nil && currentTransfer?.handle != nil {
try currentTransfer?.handle?.close()
}
if queue.isEmpty {
log("[OutboundNearbyConnection] Disconnecting because all files have been transferred")
try sendDisconnectionAndDisconnect()
delegate?.outboundConnectionTransferFinished(connection: self)
return
}
currentTransfer = queue.removeFirst()
}
let fileBuffer: Data
guard let _fileBuffer = try currentTransfer!.handle!.read(upToCount: 512 * 1024) else {
throw NearbyError.inputOutput
}
fileBuffer = _fileBuffer
var transfer = Location_Nearby_Connections_PayloadTransferFrame()
transfer.packetType = .data
transfer.payloadChunk.offset = currentTransfer!.currentOffset
transfer.payloadChunk.flags = 0
transfer.payloadChunk.body = fileBuffer
transfer.payloadHeader.id = currentTransfer!.payloadID
transfer.payloadHeader.type = .file
transfer.payloadHeader.totalSize = Int64(currentTransfer!.totalBytes)
transfer.payloadHeader.isSensitive = false
currentTransfer!.currentOffset += Int64(fileBuffer.count)
var wrapper = Location_Nearby_Connections_OfflineFrame()
wrapper.version = .v1
wrapper.v1 = Location_Nearby_Connections_V1Frame()
wrapper.v1.type = .payloadTransfer
wrapper.v1.payloadTransfer = transfer
try encryptAndSendOfflineFrame(wrapper, completion: {
do {
try self.sendNextFileChunk()
} catch {
self.lastError = error
self.protocolError()
}
})
totalBytesSent += Int64(fileBuffer.count)
// only for logging
self.bytesTransferred += Int64(fileBuffer.count)
startAndResetHeartbeatTimer()
delegate?.outboundConnection(connection: self, transferProgress: Double(totalBytesSent) / Double(totalBytesToSend))
if currentTransfer!.currentOffset == currentTransfer!.totalBytes {
// Signal end of file (yes, all this for one bit)
var transfer = Location_Nearby_Connections_PayloadTransferFrame()
transfer.packetType = .data
transfer.payloadChunk.offset = currentTransfer!.currentOffset
transfer.payloadChunk.flags = 1 // <- this one here
transfer.payloadHeader.id = currentTransfer!.payloadID
transfer.payloadHeader.type = .file
transfer.payloadHeader.totalSize = Int64(currentTransfer!.totalBytes)
transfer.payloadHeader.isSensitive = false
var wrapper = Location_Nearby_Connections_OfflineFrame()
wrapper.version = .v1
wrapper.v1 = Location_Nearby_Connections_V1Frame()
wrapper.v1.type = .payloadTransfer
wrapper.v1.payloadTransfer = transfer
try encryptAndSendOfflineFrame(wrapper)
log("[OutboundNearbyConnection] Sent EOF, current transfer: \(String(describing: currentTransfer))")
}
}
private static func sanitizeFileName(name: String) -> String {
return name.replacingOccurrences(of: "[\\/\\\\?%\\*:\\|\"<>=]", with: "_", options: .regularExpression)
}
// -- MARK: - Internal Data Model
private struct OutgoingFileTransfer {
let url: URL
let payloadID: Int64
let handle: FileHandle?
let totalBytes: Int64
var currentOffset: Int64
}
}
-47
View File
@@ -1,47 +0,0 @@
//
// Data+Extensions.swift
// QuickDrop
//
// Created by Grishka on 08.04.2023.
//
import CoreFoundation
import Foundation
extension Data {
func urlSafeBase64EncodedString() -> String {
return String(base64EncodedString().replacingOccurrences(of: "=", with: "").map {
if $0 == "/" {
return "_"
} else if $0 == "+" {
return "-"
} else {
return $0
}
})
}
static func randomData(length: Int) -> Data {
var data = Data(count: length)
data.withUnsafeMutableBytes {
guard SecRandomCopyBytes(kSecRandomDefault, length, $0.baseAddress!) == 0 else { fatalError() }
}
return data
}
static func dataFromUrlSafeBase64(_ str: String) -> Data? {
var regularB64 = String(str.map {
if $0 == "_" {
return "/"
} else if $0 == "-" {
return "+"
} else {
return $0
}
})
while (regularB64.count % 4) != 0 {
regularB64 = regularB64 + "="
}
return Data(base64Encoded: regularB64, options: .ignoreUnknownCharacters)
}
}
-31
View File
@@ -1,31 +0,0 @@
//
// Helpers.swift
// QuickDrop
//
// Created by Leon Böttger on 04.04.25.
//
import Foundation
public func isPlusVersion() -> Bool {
return UserDefaults.standard.bool(forKey: UserDefaultsKeys.plusVersion.rawValue)
}
public func isFileTransferRestricted() -> Bool {
(!isPlusVersion() && transmissionCount() > 1)
}
public func transmissionCount() -> Int {
return UserDefaults.standard.integer(forKey: UserDefaultsKeys.transmissionCount.rawValue)
}
public enum UserDefaultsKeys: String, CaseIterable {
case isEligibleForIap = "isEligibleForIap"
case appLaunchedBefore = "ShowedWelcomeScreen"
case plusVersion = "isPlusVersion"
case transmissionCount = "reviewRequestCountKey"
case automaticallyAcceptFiles = "automaticallyAcceptFiles"
case saveFolderBookmark = "saveFolderBookmark"
case openFinderAfterReceiving = "openFinderAfterReceiving"
case endpointID = "endpointID"
}
+188
View File
@@ -0,0 +1,188 @@
//
// DataModel.swift
// QuickDrop
//
// Created by Leon Böttger on 26.07.25.
//
import Foundation
import Network
protocol InboundNearbyConnectionDelegate {
func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo, connection: InboundNearbyConnection)
func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?)
}
protocol OutboundNearbyConnectionDelegate {
func outboundConnectionWasEstablished(connection: OutboundNearbyConnection)
func outboundConnection(connection: OutboundNearbyConnection, transferProgress: Double)
func outboundConnectionTransferAccepted(connection: OutboundNearbyConnection)
func outboundConnection(connection: OutboundNearbyConnection, failedWithError: Error)
func outboundConnectionTransferFinished(connection: OutboundNearbyConnection)
}
public protocol MainAppDelegate {
func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo)
func incomingTransfer(id: String, didFinishWith error: Error?)
func showFirewallAlert()
func showCopiedToClipboardAlert()
func showUnsupportedFileAlert(for: RemoteDeviceInfo?)
}
public protocol ShareExtensionDelegate: AnyObject {
func addDevice(device: RemoteDeviceInfo)
func removeDevice(id: String)
func connectionWasEstablished(pinCode: String)
func connectionFailed(with error: Error)
func transferAccepted()
func transferProgress(progress: Double)
func transferFinished()
}
public struct RemoteDeviceInfo: Identifiable, Equatable {
public let name: String
public let type: DeviceType
public var id: String?
init(name: String, type: DeviceType, id: String? = nil) {
self.name = name
self.type = type
self.id = id
}
init(info: EndpointInfo) {
name = info.name
type = info.deviceType
}
public enum DeviceType: Int32 {
case unknown = 0
case phone
case tablet
case computer
public static func fromRawValue(value: Int) -> DeviceType {
switch value {
case 0:
return .unknown
case 1:
return .phone
case 2:
return .tablet
case 3:
return .computer
default:
return .unknown
}
}
}
}
public struct TransferMetadata {
public let files: [FileMetadata]
public let id: String
public let pinCode: String?
public let textDescription: String?
public let type: TransferType
init(files: [FileMetadata], id: String, pinCode: String?, textDescription: String? = nil, transferType: TransferType = .file) {
self.files = files
self.id = id
self.pinCode = pinCode
self.textDescription = textDescription
self.type = transferType
}
public enum TransferType {
case file
case text
case url
}
}
public struct FileMetadata {
public let name: String
public let size: Int64
public let mimeType: String
}
struct EndpointInfo {
let name: String
let deviceType: RemoteDeviceInfo.DeviceType
init(name: String, deviceType: RemoteDeviceInfo.DeviceType) {
self.name = name
self.deviceType = deviceType
}
init?(data: Data) {
guard data.count > 17 else { return nil }
let deviceNameLength = Int(data[17])
guard data.count >= deviceNameLength + 18 else { return nil }
guard let deviceName = String(data: data[18 ..< (18 + deviceNameLength)], encoding: .utf8) else { return nil }
let rawDeviceType = Int(data[0] & 7) >> 1
name = deviceName
deviceType = RemoteDeviceInfo.DeviceType.fromRawValue(value: rawDeviceType)
}
func serialize() -> Data {
// 1 byte: Version(3 bits)|Visibility(1 bit)|Device Type(3 bits)|Reserved(1 bits)
// Device types: unknown=0, phone=1, tablet=2, laptop=3
var endpointInfo: [UInt8] = [UInt8(deviceType.rawValue << 1)]
// 16 bytes: unknown random bytes
for _ in 0 ... 15 {
endpointInfo.append(UInt8.random(in: 0 ... 255))
}
// Device name in UTF-8 prefixed with 1-byte length
var nameChars = [UInt8](name.utf8)
if nameChars.count > 255 {
nameChars = [UInt8](nameChars[0 ..< 255])
}
endpointInfo.append(UInt8(nameChars.count))
for ch in nameChars {
endpointInfo.append(UInt8(ch))
}
return Data(endpointInfo)
}
}
public enum NearbyError: Error {
case protocolError(_ message: String)
case requiredFieldMissing(_ message: String)
case ukey2
case packetFilterError
case inputOutput
case canceled(reason: CancellationReason)
public enum CancellationReason {
case userRejected, userCanceled, notEnoughSpace, unsupportedType, timedOut
public func localizedDescription() -> String {
switch self {
case .userRejected:
"TransferDeclined".localized()
case .userCanceled:
"TransferCanceled".localized()
case .notEnoughSpace:
"NotEnoughSpace".localized()
case .unsupportedType:
"UnsupportedType".localized()
case .timedOut:
"TransferTimedOut".localized()
}
}
}
}
File renamed without changes.
-39
View File
@@ -1,39 +0,0 @@
//
// SleepManager.swift
// QuickDrop
//
// Created by Leon Böttger on 03.05.25.
//
import Foundation
import IOKit
import IOKit.pwr_mgt
class SleepManager{
public static let shared = SleepManager()
private var assertionID: IOPMAssertionID = 0
public func disableSleep() {
if(assertionID != 0) {
return
}
IOPMAssertionCreateWithName(
kIOPMAssertionTypePreventUserIdleSystemSleep as CFString,
IOPMAssertionLevel(kIOPMAssertionLevelOn),
"QuickDrop Data Transfer" as CFString,
&assertionID
)
}
public func enableSleep() {
if(assertionID == 0 || NearbyConnectionManager.shared.getActiveConnectionsCount() != 0){
return
}
IOPMAssertionRelease(assertionID)
assertionID = 0
}
private init() {}
}
+100
View File
@@ -0,0 +1,100 @@
//
// Helpers.swift
// QuickDrop
//
// Created by Leon Böttger on 04.04.25.
//
import Foundation
extension Data {
func urlSafeBase64EncodedString() -> String {
return String(base64EncodedString().replacingOccurrences(of: "=", with: "").map {
if $0 == "/" {
return "_"
} else if $0 == "+" {
return "-"
} else {
return $0
}
})
}
static func randomData(length: Int) -> Data {
var data = Data(count: length)
data.withUnsafeMutableBytes {
guard SecRandomCopyBytes(kSecRandomDefault, length, $0.baseAddress!) == 0 else { fatalError() }
}
return data
}
static func dataFromUrlSafeBase64(_ str: String) -> Data? {
var regularB64 = String(str.map {
if $0 == "_" {
return "/"
} else if $0 == "-" {
return "+"
} else {
return $0
}
})
while (regularB64.count % 4) != 0 {
regularB64 = regularB64 + "="
}
return Data(base64Encoded: regularB64, options: .ignoreUnknownCharacters)
}
var hex: String {
return map { String(format: "%02hhx", $0) }.joined()
}
}
extension String {
var dataFromHex: Data {
var data = Data(capacity: count / 2)
var index = startIndex
while index < endIndex {
let nextIndex = self.index(index, offsetBy: 2)
if nextIndex <= endIndex,
let byte = UInt8(self[index ..< nextIndex], radix: 16)
{
data.append(byte)
} else {
break // or handle invalid hex gracefully
}
index = nextIndex
}
return data
}
}
public enum UserDefaultsKeys: String, CaseIterable {
case isEligibleForIap = "isEligibleForIap"
case appLaunchedBefore = "ShowedWelcomeScreen"
case plusVersion = "isPlusVersion"
case transmissionCount = "reviewRequestCountKey"
case automaticallyAcceptFiles = "automaticallyAcceptFiles"
case saveFolderBookmark = "saveFolderBookmark"
case openFinderAfterReceiving = "openFinderAfterReceiving"
case endpointID = "endpointID"
}
public func isPlusVersion() -> Bool {
return UserDefaults.standard.bool(forKey: UserDefaultsKeys.plusVersion.rawValue)
}
public func isFileTransferRestricted() -> Bool {
(!isPlusVersion() && transmissionCount() > 1)
}
public func transmissionCount() -> Int {
return UserDefaults.standard.integer(forKey: UserDefaultsKeys.transmissionCount.rawValue)
}
File renamed without changes.
@@ -21,6 +21,7 @@ final class LogManager {
@Published var loggingEnabled = true
/// Private initializer
private init() {
// Use the App Group container
@@ -36,6 +37,7 @@ final class LogManager {
self.logFileURL = directory?.appendingPathComponent(fileName)
}
/// Writes a specified string to the log file.
func writeToFile(string: String) {
guard let logFileURL = logFileURL else { return }
@@ -70,6 +72,7 @@ final class LogManager {
}
}
/// Gets the contents of the log file.
func getLogString() -> String {
guard let logFileURL = logFileURL else { return "" }
@@ -80,7 +83,8 @@ final class LogManager {
return ""
}
}
/// Deletes all log files in the shared container.
func deleteLogs() {
log("[LUI] Deleting all logs...")
@@ -104,6 +108,7 @@ final class LogManager {
}
}
/// Logs a specified string
public func log(_ text: String) {
let logger = LogManager.sharedInstance
@@ -120,6 +125,7 @@ public func log(_ text: String) {
}
}
/// Returns the shared App Group directory
fileprivate func getAppGroupDirectory() -> URL? {
let groupIdentifier = "group.com.leonboettger.neardrop"
+231
View File
@@ -0,0 +1,231 @@
//
// SaveFilesManager.swift
// QuickDrop
//
// Created by Leon Böttger on 26.07.25.
//
import Foundation
import ImageIO
#if os(macOS)
import AppKit
#endif
public class SaveFilesManager {
private init() {
// remove old temp directory
let tempPath = FileManager.default.temporaryDirectory
let fileManager = FileManager.default
do {
let contents = try fileManager.contentsOfDirectory(at: tempPath, includingPropertiesForKeys: nil)
var didSomething = false
for item in contents {
didSomething = true
try fileManager.removeItem(at: item)
}
if didSomething {
log("Temporary directory cleared.")
}
} catch {
log("Failed to list contents of temp directory: \(error)")
}
}
public static let shared = SaveFilesManager()
let tempDirectory: URL = FileManager.default.temporaryDirectory.appendingPathComponent("Pending")
private var securityScopeUrl: URL?
private var filesFinishedDownloading = [URL]()
private var filesFinishedDownloadingSinceLastRun = [URL]()
public func registerFileFinishedDownloading(_ fileURL: URL) {
filesFinishedDownloading.append(fileURL)
filesFinishedDownloadingSinceLastRun.append(fileURL)
// read out EXIF timestamp and manually set file creation date to it
applyEXIFTimestamps(at: fileURL)
}
public func movePendingFilesToTarget() {
if isPlusVersion() {
do {
let fileManager = FileManager.default
let files = try fileManager.contentsOfDirectory(at: tempDirectory, includingPropertiesForKeys: nil)
let target = getSaveDirectory()
var loggedExecution = false
for file in files {
if !loggedExecution {
log("Moving pending files to target directory")
loggedExecution = true
}
let fileName = file.lastPathComponent
let destinationURL = target.appendingPathComponent(fileName)
if !filesFinishedDownloading.contains(destinationURL) {
log("File \(file) not finished downloading, skipping")
continue
}
log("Moving file: \(file.lastPathComponent) to \(destinationURL.lastPathComponent)")
do {
try fileManager.copyItem(at: file, to: destinationURL)
let progress = Progress()
progress.fileURL = destinationURL
progress.totalUnitCount = 10
progress.kind = .file
progress.isPausable = false
#if os(macOS)
progress.publish()
#endif
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
progress.completedUnitCount = 10
#if os(macOS)
progress.unpublish()
#endif
}
try fileManager.removeItem(at: file)
}
catch {
log("Error moving file: \(error)")
}
}
log("Moved all pending files to target directory")
}
catch {
// Pending directory doesn't exist or is empty
}
}
if !filesFinishedDownloadingSinceLastRun.isEmpty && !isFileTransferRestricted() {
#if os(macOS)
if UserDefaults.standard.bool(forKey: UserDefaultsKeys.openFinderAfterReceiving.rawValue) {
log("Opening \(filesFinishedDownloadingSinceLastRun.count) file(s) in Finder.")
NSWorkspace.shared.activateFileViewerSelecting(filesFinishedDownloadingSinceLastRun)
}
#endif
// Clear the list of finished files
filesFinishedDownloadingSinceLastRun.removeAll()
}
stopAccessingSecurityScopedResource()
}
public func stopAccessingSecurityScopedResource() {
guard let url = securityScopeUrl else {
return
}
log("Stopping access to security scoped resource: \(url)")
url.stopAccessingSecurityScopedResource()
securityScopeUrl = nil
}
public func getSaveDirectory() -> URL {
if let securityScopeUrl = securityScopeUrl {
log("Using existing security scope URL: \(securityScopeUrl)")
return securityScopeUrl
}
if let bookmarkData = UserDefaults.standard.data(forKey: UserDefaultsKeys.saveFolderBookmark.rawValue) {
var isStale = false
// Not supported on iOS
#if os(macOS)
do {
let url = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
if !isStale {
if url.startAccessingSecurityScopedResource() {
log("Successfully accessed security scoped resource: \(url)")
securityScopeUrl = url
return url
}
} else {
log("Bookmark is stale, using default downloads folder.")
}
} catch {
log("Failed to resolve bookmark: \(error), using default downloads folder.")
}
#endif
}
do {
return try FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: true).resolvingSymlinksInPath()
} catch {
fatalError("Failed to get downloads directory: \(error)")
}
}
/// Returns the EXIF original date of an image file, adjusted to the current time zone.
private func exifOriginalDate(from url: URL) -> Date? {
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
let exif = properties[kCGImagePropertyExifDictionary] as? [CFString: Any],
let dateString = exif[kCGImagePropertyExifDateTimeOriginal] as? String
else {
return nil
}
let formatter = DateFormatter()
formatter.dateFormat = "yyyy:MM:dd HH:mm:ss"
formatter.timeZone = TimeZone.current // Use the current device time zone
return formatter.date(from: dateString)
}
/// Applies EXIF timestamps to a given file or directory.
private func applyEXIFTimestamps(at url: URL) {
let fm = FileManager.default
var isDir: ObjCBool = false
guard fm.fileExists(atPath: url.path, isDirectory: &isDir) else { return }
if isDir.boolValue {
// It's a directory — enumerate files
if let enumerator = fm.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey]) {
for case let fileURL as URL in enumerator {
guard (try? fileURL.resourceValues(forKeys: [.isRegularFileKey]))?.isRegularFile == true else { continue }
applyEXIFTimestamp(to: fileURL)
}
}
} else {
// It's a single file
applyEXIFTimestamp(to: url)
}
}
/// Applies EXIF timestamp (if available) to a single file
private func applyEXIFTimestamp(to fileURL: URL) {
guard let exifDate = exifOriginalDate(from: fileURL) else {
return
}
// Set the "creation date"
try? FileManager.default.setAttributes([.creationDate: exifDate], ofItemAtPath: fileURL.path)
}
}
+78
View File
@@ -0,0 +1,78 @@
//
// SleepManager.swift
// QuickDrop
//
// Created by Leon Böttger on 03.05.25.
//
import Foundation
#if os(macOS)
import IOKit
import IOKit.pwr_mgt
#elseif os(iOS)
import UIKit
#endif
class SleepManager {
public static let shared = SleepManager()
#if os(macOS)
private var assertionID: IOPMAssertionID = 0
#else
private var assertionID: Int = 0
#endif
private var timer: Timer?
private init() {
// Start periodic check
timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
self?.updateSleepState()
}
RunLoop.main.add(timer!, forMode: .common)
}
private func disableSleep() {
if assertionID != 0 { return }
log("[SleepManager] Enabling Wakelock")
#if os(macOS)
IOPMAssertionCreateWithName(
kIOPMAssertionTypePreventUserIdleDisplaySleep as CFString,
IOPMAssertionLevel(kIOPMAssertionLevelOn),
"QuickDrop Data Transfer" as CFString,
&assertionID
)
#elseif os(iOS)
UIApplication.shared.isIdleTimerDisabled = true
assertionID = 1
#endif
}
private func enableSleep() {
if assertionID == 0 { return }
log("[SleepManager] Disabling Wakelock")
#if os(macOS)
IOPMAssertionRelease(assertionID)
#elseif os(iOS)
UIApplication.shared.isIdleTimerDisabled = false
#endif
assertionID = 0
}
private func updateSleepState() {
let activeConnections = NearbyConnectionManager.shared.getActiveConnectionsCount()
if activeConnections > 0 {
disableSleep()
} else {
enableSleep()
}
}
}
@@ -0,0 +1,188 @@
//
// DataModel.swift
// QuickDrop
//
// Created by Leon Böttger on 26.07.25.
//
import Foundation
import Network
protocol InboundNearbyConnectionDelegate {
func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo, connection: InboundNearbyConnection)
func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?)
}
protocol OutboundNearbyConnectionDelegate {
func outboundConnectionWasEstablished(connection: OutboundNearbyConnection)
func outboundConnection(connection: OutboundNearbyConnection, transferProgress: Double)
func outboundConnectionTransferAccepted(connection: OutboundNearbyConnection)
func outboundConnection(connection: OutboundNearbyConnection, failedWithError: Error)
func outboundConnectionTransferFinished(connection: OutboundNearbyConnection)
}
public protocol MainAppDelegate {
func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo)
func incomingTransfer(id: String, didFinishWith error: Error?)
func showFirewallAlert()
func showCopiedToClipboardAlert()
func showUnsupportedFileAlert(for: RemoteDeviceInfo?)
}
public protocol ShareExtensionDelegate: AnyObject {
func addDevice(device: RemoteDeviceInfo)
func removeDevice(id: String)
func connectionWasEstablished(pinCode: String)
func connectionFailed(with error: Error)
func transferAccepted()
func transferProgress(progress: Double)
func transferFinished()
}
public struct RemoteDeviceInfo {
public let name: String
public let type: DeviceType
public var id: String?
init(name: String, type: DeviceType, id: String? = nil) {
self.name = name
self.type = type
self.id = id
}
init(info: EndpointInfo) {
name = info.name
type = info.deviceType
}
public enum DeviceType: Int32 {
case unknown = 0
case phone
case tablet
case computer
public static func fromRawValue(value: Int) -> DeviceType {
switch value {
case 0:
return .unknown
case 1:
return .phone
case 2:
return .tablet
case 3:
return .computer
default:
return .unknown
}
}
}
}
public struct TransferMetadata {
public let files: [FileMetadata]
public let id: String
public let pinCode: String?
public let textDescription: String?
public let type: TransferType
init(files: [FileMetadata], id: String, pinCode: String?, textDescription: String? = nil, transferType: TransferType = .file) {
self.files = files
self.id = id
self.pinCode = pinCode
self.textDescription = textDescription
self.type = transferType
}
public enum TransferType {
case file
case text
case url
}
}
public struct FileMetadata {
public let name: String
public let size: Int64
public let mimeType: String
}
struct EndpointInfo {
let name: String
let deviceType: RemoteDeviceInfo.DeviceType
init(name: String, deviceType: RemoteDeviceInfo.DeviceType) {
self.name = name
self.deviceType = deviceType
}
init?(data: Data) {
guard data.count > 17 else { return nil }
let deviceNameLength = Int(data[17])
guard data.count >= deviceNameLength + 18 else { return nil }
guard let deviceName = String(data: data[18 ..< (18 + deviceNameLength)], encoding: .utf8) else { return nil }
let rawDeviceType = Int(data[0] & 7) >> 1
name = deviceName
deviceType = RemoteDeviceInfo.DeviceType.fromRawValue(value: rawDeviceType)
}
func serialize() -> Data {
// 1 byte: Version(3 bits)|Visibility(1 bit)|Device Type(3 bits)|Reserved(1 bits)
// Device types: unknown=0, phone=1, tablet=2, laptop=3
var endpointInfo: [UInt8] = [UInt8(deviceType.rawValue << 1)]
// 16 bytes: unknown random bytes
for _ in 0 ... 15 {
endpointInfo.append(UInt8.random(in: 0 ... 255))
}
// Device name in UTF-8 prefixed with 1-byte length
var nameChars = [UInt8](name.utf8)
if nameChars.count > 255 {
nameChars = [UInt8](nameChars[0 ..< 255])
}
endpointInfo.append(UInt8(nameChars.count))
for ch in nameChars {
endpointInfo.append(UInt8(ch))
}
return Data(endpointInfo)
}
}
public enum NearbyError: Error {
case protocolError(_ message: String)
case requiredFieldMissing(_ message: String)
case ukey2
case packetFilterError
case inputOutput
case canceled(reason: CancellationReason)
public enum CancellationReason {
case userRejected, userCanceled, notEnoughSpace, unsupportedType, timedOut
public func localizedDescription() -> String {
switch self {
case .userRejected:
"TransferDeclined".localized()
case .userCanceled:
"TransferCanceled".localized()
case .notEnoughSpace:
"NotEnoughSpace".localized()
case .unsupportedType:
"UnsupportedType".localized()
case .timedOut:
"TransferTimedOut".localized()
}
}
}
}
@@ -11,41 +11,49 @@ import CryptoKit
import Foundation
import Network
import System
import ImageIO
import BigInt
import SwiftECC
class InboundNearbyConnection: NearbyConnection {
private var filesToBeReceived: [Int64: InternalFileInfo] = [:]
private var currentState: State = .initial
public var delegate: InboundNearbyConnectionDelegate?
private var cipherCommitment: Data?
private var textPayloadID: Int64 = 0
private var isPlainTextTransfer = false
public var wasRejected = false
public var delegate: InboundNearbyConnectionDelegate?
enum State {
case initial, receivedConnectionRequest, sentUkeyServerInit, receivedUkeyClientFinish, sentConnectionResponse, sentPairedKeyResult, receivedPairedKeyResult, waitingForUserConsent, receivingFiles, disconnected
}
override init(connection: NWConnection, id: String) {
super.init(connection: connection, id: id)
}
override func handleConnectionClosure() {
super.handleConnectionClosure()
override func disconnect() {
super.disconnect()
currentState = .disconnected
deletePartiallyReceivedFiles()
DispatchQueue.main.async {
self.delegate?.connectionWasTerminated(connection: self, error: self.lastError)
SaveFilesManager.shared.movePendingFilesToTarget()
SaveFilesManager.shared.stopAccessingSecurityScopedResource()
}
}
override func processReceivedFrame(frameData: Data) {
log("Received frame in state \(currentState)...")
if currentState != .receivingFiles {
log("[InboundNearbyConnection] Received frame in state \(currentState)...")
}
do {
switch currentState {
@@ -69,7 +77,7 @@ class InboundNearbyConnection: NearbyConnection {
do {
smsg = try Securemessage_SecureMessage(serializedBytes: frameData)
} catch {
log("Error deserializing secure message (probably due to packet filter)")
log("[InboundNearbyConnection] Error deserializing secure message (probably due to packet filter)")
lastError = NearbyError.packetFilterError
protocolError()
}
@@ -80,14 +88,15 @@ class InboundNearbyConnection: NearbyConnection {
}
} catch {
lastError = error
log("Error receiving frame: \(error) in state \(currentState).")
log("[InboundNearbyConnection] Error receiving frame: \(error) in state \(currentState).")
protocolError()
}
}
override func processTransferSetupFrame(_ frame: Sharing_Nearby_Frame) throws {
if frame.hasV1 && frame.v1.hasType, case .cancel = frame.v1.type {
log("Transfer canceled")
log("[InboundNearbyConnection] Transfer canceled")
try sendDisconnectionAndDisconnect()
return
}
@@ -99,20 +108,22 @@ class InboundNearbyConnection: NearbyConnection {
case .receivedPairedKeyResult:
try processIntroductionFrame(frame)
default:
log("Unexpected connection state in processTransferSetupFrame: \(currentState)")
log("[InboundNearbyConnection] Unexpected connection state in processTransferSetupFrame: \(currentState)")
log(frame.debugDescription)
}
}
override func isServer() -> Bool {
return true
}
override func processFileChunk(frame: Location_Nearby_Connections_PayloadTransferFrame) throws {
let id = frame.payloadHeader.id
guard let fileInfo = transferredFiles[id] else { throw NearbyError.protocolError("File payload ID \(id) is not known") }
guard let fileInfo = filesToBeReceived[id] else { throw NearbyError.protocolError("File payload ID \(id) is not known") }
let currentOffset = fileInfo.bytesTransferred
@@ -123,29 +134,33 @@ class InboundNearbyConnection: NearbyConnection {
if frame.payloadChunk.body.count > 0 {
do {
try fileInfo.fileHandle?.write(contentsOf: frame.payloadChunk.body)
transferredFiles[id]!.bytesTransferred += Int64(frame.payloadChunk.body.count)
fileInfo.progress?.completedUnitCount = transferredFiles[id]!.bytesTransferred
filesToBeReceived[id]!.bytesTransferred += Int64(frame.payloadChunk.body.count)
fileInfo.progress?.completedUnitCount = filesToBeReceived[id]!.bytesTransferred
// only for logging
self.bytesTransferred += Int64(frame.payloadChunk.body.count)
} catch {
log("Error occurred during writing file: \(error.localizedDescription)")
log("[InboundNearbyConnection] Error occurred during writing file: \(error.localizedDescription)")
throw NearbyError.protocolError(error.localizedDescription)
}
}
else if (frame.payloadChunk.flags & 1) == 1 {
try fileInfo.fileHandle?.close()
transferredFiles[id]!.fileHandle = nil
filesToBeReceived[id]!.fileHandle = nil
fileInfo.progress?.unpublish()
SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL)
transferredFiles.removeValue(forKey: id)
filesToBeReceived.removeValue(forKey: id)
if transferredFiles.isEmpty {
log("All files received, sending disconnection frame and disconnecting.")
if filesToBeReceived.isEmpty {
log("[InboundNearbyConnection] All files received, sending disconnection frame and disconnecting.")
try sendDisconnectionAndDisconnect()
}
}
}
override func processBytesPayload(payload: Data, id: Int64) throws -> Bool {
if id == textPayloadID {
if let urlStr = String(data: payload, encoding: .utf8) {
@@ -163,26 +178,27 @@ class InboundNearbyConnection: NearbyConnection {
}
}
log("Received text payload. Disconnecting...")
log("[InboundNearbyConnection] Received text payload. Disconnecting...")
try sendDisconnectionAndDisconnect()
return true
}
else if let fileInfo = transferredFiles[id] {
else if let fileInfo = filesToBeReceived[id] {
fileInfo.fileHandle?.write(payload)
transferredFiles[id]!.bytesTransferred += Int64(payload.count)
fileInfo.progress?.completedUnitCount = transferredFiles[id]!.bytesTransferred
filesToBeReceived[id]!.bytesTransferred += Int64(payload.count)
fileInfo.progress?.completedUnitCount = filesToBeReceived[id]!.bytesTransferred
try fileInfo.fileHandle?.close()
transferredFiles[id]!.fileHandle = nil
filesToBeReceived[id]!.fileHandle = nil
fileInfo.progress?.unpublish()
transferredFiles.removeValue(forKey: id)
filesToBeReceived.removeValue(forKey: id)
SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL)
log("Received file payload. Disconnecting...")
log("[InboundNearbyConnection] Received file payload. Disconnecting...")
try sendDisconnectionAndDisconnect()
return true
}
return false
}
private func processConnectionRequestFrame(_ frame: Location_Nearby_Connections_OfflineFrame) throws {
@@ -205,11 +221,13 @@ class InboundNearbyConnection: NearbyConnection {
remoteDeviceInfo = RemoteDeviceInfo(name: deviceName, type: RemoteDeviceInfo.DeviceType.fromRawValue(value: rawDeviceType))
currentState = .receivedConnectionRequest
}
private func processUkey2ClientInit(_ msg: Securegcm_Ukey2Message) throws {
guard msg.hasMessageType, msg.hasMessageData else { throw NearbyError.requiredFieldMissing("clientInit ukey2message.type|data") }
guard case .clientInit = msg.messageType else {
sendUkey2Alert(type: .badMessageType)
log("[InboundNearbyConnection] Unsupported message type: \(msg.messageType)")
throw NearbyError.ukey2
}
let clientInit: Securegcm_Ukey2ClientInit
@@ -217,14 +235,17 @@ class InboundNearbyConnection: NearbyConnection {
clientInit = try Securegcm_Ukey2ClientInit(serializedBytes: msg.messageData)
} catch {
sendUkey2Alert(type: .badMessageData)
log("[InboundNearbyConnection] Failed to parse clientInit: \(error)")
throw NearbyError.ukey2
}
guard clientInit.version == 1 else {
sendUkey2Alert(type: .badVersion)
log("[InboundNearbyConnection] Unsupported clientInit version: \(clientInit.version)")
throw NearbyError.ukey2
}
guard clientInit.random.count == 32 else {
sendUkey2Alert(type: .badRandom)
log("[InboundNearbyConnection] Unsupported clientInit random: \(clientInit.random.count)")
throw NearbyError.ukey2
}
var found = false
@@ -237,10 +258,12 @@ class InboundNearbyConnection: NearbyConnection {
}
guard found else {
sendUkey2Alert(type: .badHandshakeCipher)
log("[InboundNearbyConnection] Unsupported clientInit handshakeCipher: \(clientInit.cipherCommitments)")
throw NearbyError.ukey2
}
guard clientInit.nextProtocol == "AES_256_CBC-HMAC_SHA256" else {
sendUkey2Alert(type: .badNextProtocol)
log("[InboundNearbyConnection] Unsupported clientInit nextProtocol: \(clientInit.nextProtocol)")
throw NearbyError.ukey2
}
@@ -270,13 +293,20 @@ class InboundNearbyConnection: NearbyConnection {
currentState = .sentUkeyServerInit
}
private func processUkey2ClientFinish(_ msg: Securegcm_Ukey2Message, raw: Data) throws {
guard msg.hasMessageType, msg.hasMessageData else { throw NearbyError.requiredFieldMissing("clientFinish ukey2message.type|data") }
guard case .clientFinish = msg.messageType else { throw NearbyError.ukey2 }
guard case .clientFinish = msg.messageType else {
log("[InboundNearbyConnection] Unexpected message type \(msg.messageType)")
throw NearbyError.ukey2
}
var sha = SHA512()
sha.update(data: raw)
guard cipherCommitment == Data(sha.finalize()) else { throw NearbyError.ukey2 }
guard cipherCommitment == Data(sha.finalize()) else {
log("[InboundNearbyConnection] Invalid cipherCommitment in clientFinish")
throw NearbyError.ukey2
}
let clientFinish = try Securegcm_Ukey2ClientFinished(serializedBytes: msg.messageData)
guard clientFinish.hasPublicKey else { throw NearbyError.requiredFieldMissing("ukey2clientFinish.publicKey") }
@@ -286,6 +316,7 @@ class InboundNearbyConnection: NearbyConnection {
currentState = .receivedUkeyClientFinish
}
private func processConnectionResponseFrame(_ frame: Location_Nearby_Connections_OfflineFrame) throws {
guard frame.hasV1, frame.v1.hasType else { throw NearbyError.requiredFieldMissing("offlineFrame.v1.type") }
@@ -314,10 +345,11 @@ class InboundNearbyConnection: NearbyConnection {
try sendTransferSetupFrame(pairedEncryption)
currentState = .sentConnectionResponse
} else {
log("Unhandled offline frame plaintext: \(frame)")
log("[InboundNearbyConnection] Unhandled offline frame plaintext: \(frame)")
}
}
private func processPairedKeyEncryptionFrame(_ frame: Sharing_Nearby_Frame) throws {
guard frame.hasV1, frame.v1.hasPairedKeyEncryption else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyEncryption") }
var pairedResult = Sharing_Nearby_Frame()
@@ -330,10 +362,12 @@ class InboundNearbyConnection: NearbyConnection {
currentState = .sentPairedKeyResult
}
private func processPairedKeyResultFrame(_ frame: Sharing_Nearby_Frame) throws {
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyResult") }
currentState = .receivedPairedKeyResult
}
private func makeFileDestinationURL(_ initialDest: URL) -> URL {
var dest = initialDest
@@ -353,6 +387,7 @@ class InboundNearbyConnection: NearbyConnection {
}
return dest
}
private func processIntroductionFrame(_ frame: Sharing_Nearby_Frame) throws {
guard frame.hasV1, frame.v1.hasIntroduction else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.introduction") }
@@ -366,9 +401,9 @@ class InboundNearbyConnection: NearbyConnection {
let info = InternalFileInfo(meta: FileMetadata(name: file.name, size: file.size, mimeType: file.mimeType),
payloadID: file.payloadID,
destinationURL: dest)
transferredFiles[file.payloadID] = info
filesToBeReceived[file.payloadID] = info
}
let metadata = TransferMetadata(files: transferredFiles.map { $0.value.meta }, id: id, pinCode: pinCode)
let metadata = TransferMetadata(files: filesToBeReceived.map { $0.value.meta }, id: id, pinCode: pinCode)
DispatchQueue.main.async {
self.delegate?.obtainUserConsent(for: metadata, from: self.remoteDeviceInfo!, connection: self)
}
@@ -398,13 +433,15 @@ class InboundNearbyConnection: NearbyConnection {
}
}
func rejectDueToUnsupportedFileType(_ frame: Sharing_Nearby_Frame) {
log("Rejecting transfer due to unsupported file type. Frame is \(frame.debugDescription)")
log("[InboundNearbyConnection] Rejecting transfer due to unsupported file type. Frame is \(frame.debugDescription)")
NearbyConnectionManager.shared.mainAppDelegate?.showUnsupportedFileAlert(for: remoteDeviceInfo)
rejectTransfer(with: .unsupportedAttachmentType)
}
func submitUserConsent(accepted: Bool, storeInTemp: Bool = false) {
DispatchQueue.global(qos: .utility).async {
@@ -416,9 +453,10 @@ class InboundNearbyConnection: NearbyConnection {
}
}
private func acceptTransfer(storeInTemp: Bool) {
if currentState == .disconnected {
log("Detected timeout, not accepting transfer")
log("[InboundNearbyConnection] Detected timeout, not accepting transfer")
return
}
@@ -427,12 +465,12 @@ class InboundNearbyConnection: NearbyConnection {
try FileManager.default.createDirectory(at: SaveFilesManager.shared.tempDirectory, withIntermediateDirectories: true)
}
for (id, file) in transferredFiles {
for (id, file) in filesToBeReceived {
let targetURL = storeInTemp ? SaveFilesManager.shared.tempDirectory.appendingPathComponent(file.destinationURL.lastPathComponent) : file.destinationURL
FileManager.default.createFile(atPath: targetURL.path, contents: nil)
let handle = try FileHandle(forWritingTo: targetURL)
transferredFiles[id]!.fileHandle = handle
filesToBeReceived[id]!.fileHandle = handle
let progress = Progress()
progress.fileURL = targetURL
@@ -440,8 +478,8 @@ class InboundNearbyConnection: NearbyConnection {
progress.kind = .file
progress.isPausable = false
progress.publish()
transferredFiles[id]!.progress = progress
transferredFiles[id]!.created = true
filesToBeReceived[id]!.progress = progress
filesToBeReceived[id]!.created = true
}
var frame = Sharing_Nearby_Frame()
@@ -456,9 +494,12 @@ class InboundNearbyConnection: NearbyConnection {
}
}
private func rejectTransfer(with reason: Sharing_Nearby_ConnectionResponseFrame.Status = .reject) {
log("Rejecting transfer because of \( reason)")
self.wasRejected = true
log("[InboundNearbyConnection] Rejecting transfer because of \( reason)")
var frame = Sharing_Nearby_Frame()
frame.version = .v1
@@ -468,13 +509,19 @@ class InboundNearbyConnection: NearbyConnection {
try sendTransferSetupFrame(frame)
try sendDisconnectionAndDisconnect()
} catch {
log("Error \(error)")
log("[InboundNearbyConnection] Error \(error)")
protocolError()
}
}
private func deletePartiallyReceivedFiles() {
for (_, file) in transferredFiles {
for (_, file) in filesToBeReceived {
if let progress = file.progress {
progress.unpublish()
}
guard file.created else { continue }
do {
try FileManager.default.removeItem(at: file.destinationURL)
@@ -485,237 +532,3 @@ class InboundNearbyConnection: NearbyConnection {
}
}
}
protocol InboundNearbyConnectionDelegate {
func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo, connection: InboundNearbyConnection)
func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?)
}
public class SaveFilesManager {
private init() {
// remove old temp directory
let tempPath = FileManager.default.temporaryDirectory
let fileManager = FileManager.default
do {
let contents = try fileManager.contentsOfDirectory(at: tempPath, includingPropertiesForKeys: nil)
var didSomething = false
for item in contents {
didSomething = true
try fileManager.removeItem(at: item)
}
if didSomething {
log("Temporary directory cleared.")
}
} catch {
log("Failed to list contents of temp directory: \(error)")
}
}
public static let shared = SaveFilesManager()
let tempDirectory: URL = FileManager.default.temporaryDirectory.appendingPathComponent("Pending")
private var securityScopeUrl: URL?
private var filesFinishedDownloading = [URL]()
private var filesFinishedDownloadingSinceLastRun = [URL]()
public func registerFileFinishedDownloading(_ fileURL: URL) {
filesFinishedDownloading.append(fileURL)
filesFinishedDownloadingSinceLastRun.append(fileURL)
// read out EXIF timestamp and manually set file creation date to it
applyEXIFTimestamps(at: fileURL)
}
public func movePendingFilesToTarget() {
if isPlusVersion() {
do {
let fileManager = FileManager.default
let files = try fileManager.contentsOfDirectory(at: tempDirectory, includingPropertiesForKeys: nil)
let target = getSaveDirectory()
var loggedExecution = false
for file in files {
if !loggedExecution {
log("Moving pending files to target directory")
loggedExecution = true
}
let fileName = file.lastPathComponent
let destinationURL = target.appendingPathComponent(fileName)
if !filesFinishedDownloading.contains(destinationURL) {
log("File \(file) not finished downloading, skipping")
continue
}
log("Moving file: \(file.lastPathComponent) to \(destinationURL.lastPathComponent)")
do {
try fileManager.copyItem(at: file, to: destinationURL)
let progress = Progress()
progress.fileURL = destinationURL
progress.totalUnitCount = 10
progress.kind = .file
progress.isPausable = false
progress.publish()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
progress.completedUnitCount = 10
progress.unpublish()
}
try fileManager.removeItem(at: file)
}
catch {
log("Error moving file: \(error)")
}
}
log("Moved all pending files to target directory")
}
catch {
// Pending directory doesn't exist or is empty
}
}
if !filesFinishedDownloadingSinceLastRun.isEmpty && !isFileTransferRestricted() {
if UserDefaults.standard.bool(forKey: UserDefaultsKeys.openFinderAfterReceiving.rawValue) {
log("Opening \(filesFinishedDownloadingSinceLastRun.count) file(s) in Finder.")
NSWorkspace.shared.activateFileViewerSelecting(filesFinishedDownloadingSinceLastRun)
}
// Clear the list of finished files
filesFinishedDownloadingSinceLastRun.removeAll()
}
stopAccessingSecurityScopedResource()
}
public func stopAccessingSecurityScopedResource() {
guard let url = securityScopeUrl else {
return
}
log("Stopping access to security scoped resource: \(url)")
url.stopAccessingSecurityScopedResource()
securityScopeUrl = nil
}
public func getSaveDirectory() -> URL {
if let securityScopeUrl = securityScopeUrl {
log("Using existing security scope URL: \(securityScopeUrl)")
return securityScopeUrl
}
if let bookmarkData = UserDefaults.standard.data(forKey: UserDefaultsKeys.saveFolderBookmark.rawValue) {
var isStale = false
do {
let url = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
if !isStale {
if url.startAccessingSecurityScopedResource() {
log("Successfully accessed security scoped resource: \(url)")
securityScopeUrl = url
return url
}
} else {
log("Bookmark is stale, using default downloads folder.")
}
} catch {
log("Failed to resolve bookmark: \(error), using default downloads folder.")
}
}
do {
return try FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: true).resolvingSymlinksInPath()
} catch {
fatalError("Failed to get downloads directory: \(error)")
}
}
/// Returns the EXIF original date of an image file, adjusted to the current time zone.
private func exifOriginalDate(from url: URL) -> Date? {
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
let exif = properties[kCGImagePropertyExifDictionary] as? [CFString: Any],
let dateString = exif[kCGImagePropertyExifDateTimeOriginal] as? String
else {
return nil
}
let formatter = DateFormatter()
formatter.dateFormat = "yyyy:MM:dd HH:mm:ss"
formatter.timeZone = TimeZone.current // Use the current device time zone
return formatter.date(from: dateString)
}
/// Applies EXIF timestamps to a given file or directory.
private func applyEXIFTimestamps(at url: URL) {
let fm = FileManager.default
var isDir: ObjCBool = false
guard fm.fileExists(atPath: url.path, isDirectory: &isDir) else { return }
if isDir.boolValue {
// It's a directory — enumerate files
if let enumerator = fm.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey]) {
for case let fileURL as URL in enumerator {
guard (try? fileURL.resourceValues(forKeys: [.isRegularFileKey]))?.isRegularFile == true else { continue }
applyEXIFTimestamp(to: fileURL)
}
}
} else {
// It's a single file
applyEXIFTimestamp(to: url)
}
}
/// Applies EXIF timestamp (if available) to a single file
private func applyEXIFTimestamp(to fileURL: URL) {
guard let exifDate = exifOriginalDate(from: fileURL) else {
return
}
// Set the "creation date"
try? FileManager.default.setAttributes([.creationDate: exifDate], ofItemAtPath: fileURL.path)
}
}
extension Data {
var hex: String {
return map { String(format: "%02hhx", $0) }.joined()
}
}
extension String {
var dataFromHex: Data {
var data = Data(capacity: count / 2)
var index = startIndex
while index < endIndex {
let nextIndex = self.index(index, offsetBy: 2)
if nextIndex <= endIndex,
let byte = UInt8(self[index ..< nextIndex], radix: 16)
{
data.append(byte)
} else {
break // or handle invalid hex gracefully
}
index = nextIndex
}
return data
}
}
@@ -15,18 +15,24 @@ import BigInt
import SwiftECC
class NearbyConnection {
static let SANE_FRAME_LENGTH = 5 * 1024 * 1024
private static let dispatchQueue = DispatchQueue(label: "com.leonboettger.quickdrop.queue", qos: .utility) // FIFO (non-concurrent) queue to avoid those exciting concurrency bugs
private let maxFrameLength = 5 * 1024 * 1024
private static let dispatchQueue = DispatchQueue(label: "com.leonboettger.quickdrop.queue", qos: .utility)
let id: String
let connection: NWConnection
var remoteDeviceInfo: RemoteDeviceInfo?
private var payloadBuffers: [Int64: NSMutableData] = [:]
var encryptionDone: Bool = false
var transferredFiles: [Int64: InternalFileInfo] = [:]
public let id: String
var lastError: Error?
var bytesTransferred: Int64 = 0
private var payloadBuffers: [Int64: NSMutableData] = [:]
private var connectionClosed: Bool = false
private var inactivityTimer: DispatchSourceTimer?
private let timeoutInterval: TimeInterval = 50
// UKEY2-related state
var publicKey: ECPublicKey?
var privateKey: ECPrivateKey?
@@ -45,13 +51,16 @@ class NearbyConnection {
private(set) var pinCode: String?
init(connection: NWConnection, id: String) {
self.connection = connection
self.id = id
}
func start() {
connection.stateUpdateHandler = { state in
if !self.connectionClosed {
if case .ready = state {
self.connectionReady()
@@ -74,15 +83,15 @@ class NearbyConnection {
}
}
else {
log("Connection already closed, ignoring state update: \(state)")
log("[NearbyConnection] Connection already closed, ignoring state update: \(state)")
}
}
// connection.start(queue: .global(qos: .utility))
connection.start(queue: NearbyConnection.dispatchQueue)
func recordErrorAndDisconnect(err: NWError) {
self.lastError = err
log("Connection Error: \(err)")
log("[NearbyConnection] Connection Error: \(err)")
// If the error is a connection reset, it could be due to firewall issues.
if err == .posix(.ENOTCONN) {
@@ -91,49 +100,58 @@ class NearbyConnection {
}
}
self.handleConnectionClosure()
if err == .posix(.ENETDOWN) {
self.lastError = NearbyError.protocolError("Error.NetworkDown".localized())
}
self.disconnect()
}
}
func connectionReady() {}
func handleConnectionClosure() {
log("Connection closed")
}
func protocolError() {
log("Protocol error: \(String(describing: lastError))")
log("[NearbyConnection] Protocol error: \(String(describing: lastError))")
disconnect()
}
func processReceivedFrame(frameData _: Data) {
fatalError()
}
func processTransferSetupFrame(_: Sharing_Nearby_Frame) throws {
fatalError()
}
func isServer() -> Bool {
fatalError()
}
func processFileChunk(frame _: Location_Nearby_Connections_PayloadTransferFrame) throws {
protocolError()
}
func processBytesPayload(payload _: Data, id _: Int64) throws -> Bool {
return false
}
private func receiveFrameAsync() {
connection.receive(minimumIncompleteLength: 4, maximumLength: 4) { content, _, isComplete, error in
if self.connectionClosed {
return
}
if isComplete {
log("Connection closed by peer during receiveFrameAsync")
self.handleConnectionClosure()
log("[NearbyConnection] Connection closed by peer during receiveFrameAsync")
self.lastError = NearbyError.protocolError("Error.ClosedByPeer".localized())
self.disconnect()
return
}
if !(error == nil) {
@@ -146,34 +164,41 @@ class NearbyConnection {
return
}
let frameLength = UInt32(content[0]) << 24 | UInt32(content[1]) << 16 | UInt32(content[2]) << 8 | UInt32(content[3])
guard frameLength < NearbyConnection.SANE_FRAME_LENGTH else {
guard frameLength < self.maxFrameLength else {
self.lastError = NearbyError.protocolError("Unexpected packet length")
self.protocolError()
return
}
self.startInactivityTimer()
self.receiveFrameAsync(length: frameLength)
}
}
private func receiveFrameAsync(length: UInt32) {
connection.receive(minimumIncompleteLength: Int(length), maximumLength: Int(length)) { content, _, isComplete, _ in
if self.connectionClosed {
return
}
if isComplete {
log("Connection closed by peer during receiveFrameAsync")
self.handleConnectionClosure()
log("[NearbyConnection] Connection closed by peer during receiveFrameAsync")
self.lastError = NearbyError.protocolError("Error.ClosedByPeer".localized())
self.disconnect()
return
}
guard let content = content else {
self.protocolError()
return
}
self.startInactivityTimer()
self.processReceivedFrame(frameData: content)
self.receiveFrameAsync()
}
}
func sendFrameAsync(_ frame: Data, completion: (() -> Void)? = nil) {
if connectionClosed {
return
@@ -194,6 +219,7 @@ class NearbyConnection {
})
}
func encryptAndSendOfflineFrame(_ frame: Location_Nearby_Connections_OfflineFrame, completion: (() -> Void)? = nil) throws {
var d2dMsg = Securegcm_DeviceToDeviceMessage()
serverSeq += 1
@@ -235,11 +261,15 @@ class NearbyConnection {
try sendFrameAsync(smsg.serializedData(), completion: completion)
}
func sendTransferSetupFrame(_ frame: Sharing_Nearby_Frame) throws {
log("[NearbyConnection] Sending transfer setup frame.")
try sendBytesPayload(data: frame.serializedData(), id: Int64.random(in: Int64.min ... Int64.max))
}
func sendBytesPayload(data: Data, id: Int64) throws {
var transfer = Location_Nearby_Connections_PayloadTransferFrame()
transfer.packetType = .data
transfer.payloadChunk.offset = 0
@@ -264,13 +294,12 @@ class NearbyConnection {
try encryptAndSendOfflineFrame(wrapper)
}
func decryptAndProcessReceivedSecureMessage(_ smsg: Securemessage_SecureMessage) throws {
guard smsg.hasSignature, smsg.hasHeaderAndBody else { throw NearbyError.requiredFieldMissing("secureMessage.signature|headerAndBody") }
if NearbyConnectionManager.shared.checkSignature {
let hmac = Data(HMAC<SHA256>.authenticationCode(for: smsg.headerAndBody, using: recvHmacKey!))
guard hmac == smsg.signature else { throw NearbyError.protocolError("hmac!=signature: Expected \(hmac), got \(smsg.signature)") }
}
let hmac = Data(HMAC<SHA256>.authenticationCode(for: smsg.headerAndBody, using: recvHmacKey!))
guard hmac == smsg.signature else { throw NearbyError.protocolError("hmac!=signature: Expected \(hmac), got \(smsg.signature)") }
let headerAndBody = try Securemessage_HeaderAndBody(serializedBytes: smsg.headerAndBody)
var decryptedData = Data(count: headerAndBody.body.count)
@@ -289,37 +318,56 @@ class NearbyConnection {
)
guard status == kCCSuccess else { fatalError("CCCrypt error: \(status)") }
}
decryptedData = decryptedData.prefix(decryptedLength)
let d2dMsg = try Securegcm_DeviceToDeviceMessage(serializedBytes: decryptedData)
guard d2dMsg.hasMessage, d2dMsg.hasSequenceNumber else { throw NearbyError.requiredFieldMissing("d2dMessage.message|sequenceNumber") }
clientSeq += 1
guard d2dMsg.sequenceNumber == clientSeq else { throw NearbyError.protocolError("Wrong sequence number. Expected \(clientSeq), got \(d2dMsg.sequenceNumber)") }
let offlineFrame = try Location_Nearby_Connections_OfflineFrame(serializedBytes: d2dMsg.message)
if offlineFrame.hasV1, offlineFrame.v1.hasType, case .payloadTransfer = offlineFrame.v1.type {
guard offlineFrame.v1.hasPayloadTransfer else { throw NearbyError.requiredFieldMissing("offlineFrame.v1.payloadTransfer") }
let payloadTransfer = offlineFrame.v1.payloadTransfer
let header = payloadTransfer.payloadHeader
let chunk = payloadTransfer.payloadChunk
guard header.hasType, header.hasID else { throw NearbyError.requiredFieldMissing("payloadHeader.type|id") }
guard payloadTransfer.hasPayloadChunk, chunk.hasOffset, chunk.hasFlags else { throw NearbyError.requiredFieldMissing("payloadTransfer.payloadChunk|offset|flags") }
guard payloadTransfer.hasPayloadChunk, chunk.hasOffset, chunk.hasFlags else {
log("[NearbyConnection] Payload transfer chunk is missing offset or flags, likely canceled.")
throw NearbyError.canceled(reason: .userCanceled)
}
if case .bytes = header.type {
let payloadID = header.id
if header.totalSize > InboundNearbyConnection.SANE_FRAME_LENGTH {
if header.totalSize > maxFrameLength {
payloadBuffers.removeValue(forKey: payloadID)
throw NearbyError.protocolError("Payload too large (\(header.totalSize) bytes)")
}
if payloadBuffers[payloadID] == nil {
payloadBuffers[payloadID] = NSMutableData(capacity: Int(header.totalSize))
}
let buffer = payloadBuffers[payloadID]!
guard chunk.offset == buffer.count else {
payloadBuffers.removeValue(forKey: payloadID)
throw NearbyError.protocolError("Unexpected chunk offset \(chunk.offset), expected \(buffer.count)")
}
if chunk.hasBody {
buffer.append(chunk.body)
}
if (chunk.flags & 1) == 1 {
payloadBuffers.removeValue(forKey: payloadID)
if try !processBytesPayload(payload: Data(buffer), id: payloadID) {
@@ -327,17 +375,23 @@ class NearbyConnection {
try processTransferSetupFrame(innerFrame)
}
}
} else if case .file = header.type {
try processFileChunk(frame: payloadTransfer)
}
} else if offlineFrame.hasV1, offlineFrame.v1.hasType, case .keepAlive = offlineFrame.v1.type {
log("Sent keep-alive")
}
else if offlineFrame.hasV1, offlineFrame.v1.hasType, case .keepAlive = offlineFrame.v1.type {
log("[NearbyConnection] Sent keep-alive, \(self.bytesTransferred) bytes sent")
sendKeepAlive(ack: true)
} else {
log("Unhandled offline frame encrypted: \(offlineFrame)")
log("[NearbyConnection] Unhandled offline frame encrypted: \(offlineFrame)")
}
}
static func pinCodeFromAuthKey(_ key: SymmetricKey) -> String {
var hash = 0
var multiplier = 1
@@ -354,10 +408,12 @@ class NearbyConnection {
return String(format: "%04d", abs(hash))
}
static func hkdfExtract(salt: Data, ikm: Data) -> Data {
return HMAC<SHA256>.authenticationCode(for: ikm, using: SymmetricKey(data: salt)).withUnsafeBytes { Data(bytes: $0.baseAddress!, count: $0.count) }
}
static func hkdfExpand(prk: Data, info: Data, length: Int) -> Data {
var okm = Data()
var t = Data()
@@ -371,14 +427,12 @@ class NearbyConnection {
return okm.subdata(in: 0 ..< length)
}
static func hkdf(inputKeyMaterial: SymmetricKey, salt: Data, info: Data, outputByteCount: Int) -> SymmetricKey {
if #available(macOS 11.0, *) {
return HKDF<SHA256>.deriveKey(inputKeyMaterial: inputKeyMaterial, salt: salt, info: info, outputByteCount: outputByteCount)
} else {
return SymmetricKey(data: hkdfExpand(prk: hkdfExtract(salt: salt, ikm: inputKeyMaterial.withUnsafeBytes { Data(bytes: $0.baseAddress!, count: $0.count) }), info: info, length: outputByteCount))
}
return HKDF<SHA256>.deriveKey(inputKeyMaterial: inputKeyMaterial, salt: salt, info: info, outputByteCount: outputByteCount)
}
func finalizeKeyExchange(peerKey: Securemessage_GenericPublicKey) throws {
guard peerKey.hasEcP256PublicKey else { throw NearbyError.requiredFieldMissing("peerKey.ecP256PublicKey") }
@@ -435,18 +489,18 @@ class NearbyConnection {
}
}
func disconnect() {
log("Disconnecting from connection.")
log("[NearbyConnection] Disconnecting from connection.")
connection.stateUpdateHandler = nil
inactivityTimer?.cancel()
inactivityTimer = nil
connectionClosed = true
self.handleConnectionClosure()
connection.send(content: nil, isComplete: true, completion: .contentProcessed { _ in
})
connection.send(content: nil, isComplete: true, completion: .contentProcessed { _ in })
}
func sendDisconnectionAndDisconnect() throws {
var offlineFrame = Location_Nearby_Connections_OfflineFrame()
offlineFrame.version = .v1
@@ -459,10 +513,11 @@ class NearbyConnection {
try sendFrameAsync(offlineFrame.serializedData())
}
log("Sent disconnection frame during sendDisconnectionAndDisconnect")
log("[NearbyConnection] Sent disconnection frame during sendDisconnectionAndDisconnect")
disconnect()
}
func sendUkey2Alert(type: Securegcm_Ukey2Alert.AlertType) {
var alert = Securegcm_Ukey2Alert()
alert.type = type
@@ -471,10 +526,11 @@ class NearbyConnection {
msg.messageData = try! alert.serializedData()
sendFrameAsync(try! msg.serializedData())
log("Sent UKEY2 alert: \(type)")
log("[NearbyConnection] Sent UKEY2 alert: \(type)")
disconnect()
}
func sendKeepAlive(ack: Bool) {
var offlineFrame = Location_Nearby_Connections_OfflineFrame()
offlineFrame.version = .v1
@@ -488,17 +544,49 @@ class NearbyConnection {
try sendFrameAsync(offlineFrame.serializedData())
}
} catch {
log("Error sending KEEP_ALIVE: \(error)")
log("[NearbyConnection] Error sending KEEP_ALIVE: \(error)")
}
}
func startInactivityTimer() {
// Cancel previous timer if any
inactivityTimer?.cancel()
inactivityTimer = DispatchSource.makeTimerSource(queue: NearbyConnection.dispatchQueue)
inactivityTimer?.schedule(deadline: .now() + timeoutInterval)
inactivityTimer?.setEventHandler { [weak self] in
guard let self = self else { return }
if !self.connectionClosed {
log("[NearbyConnection] Connection timeout: No message received in \(self.timeoutInterval) seconds")
self.lastError = NearbyError.canceled(reason: .timedOut)
self.disconnect()
}
}
inactivityTimer?.resume()
}
// -- MARK: - Internal Data Model
struct InternalFileInfo {
let meta: FileMetadata
let payloadID: Int64
let destinationURL: URL
var bytesTransferred: Int64 = 0
var fileHandle: FileHandle?
var progress: Progress?
var created: Bool = false
}
}
struct InternalFileInfo {
let meta: FileMetadata
let payloadID: Int64
let destinationURL: URL
var bytesTransferred: Int64 = 0
var fileHandle: FileHandle?
var progress: Progress?
var created: Bool = false
public extension String {
func localized() -> String {
let localizedString = NSLocalizedString(self, comment: "")
return localizedString
}
}
@@ -9,189 +9,35 @@ import Foundation
import Network
import System
public struct RemoteDeviceInfo {
public let name: String
public let type: DeviceType
public var id: String?
init(name: String, type: DeviceType, id: String? = nil) {
self.name = name
self.type = type
self.id = id
}
init(info: EndpointInfo) {
name = info.name
type = info.deviceType
}
public enum DeviceType: Int32 {
case unknown = 0
case phone
case tablet
case computer
public static func fromRawValue(value: Int) -> DeviceType {
switch value {
case 0:
return .unknown
case 1:
return .phone
case 2:
return .tablet
case 3:
return .computer
default:
return .unknown
}
}
}
}
public enum NearbyError: Error {
case protocolError(_ message: String)
case requiredFieldMissing(_ message: String)
case ukey2
case packetFilterError
case inputOutput
case canceled(reason: CancellationReason)
public enum CancellationReason {
case userRejected, userCanceled, notEnoughSpace, unsupportedType, timedOut
}
}
public struct TransferMetadata {
public let files: [FileMetadata]
public let id: String
public let pinCode: String?
public let textDescription: String?
public let type: TransferType
init(files: [FileMetadata], id: String, pinCode: String?, textDescription: String? = nil, transferType: TransferType = .file) {
self.files = files
self.id = id
self.pinCode = pinCode
self.textDescription = textDescription
self.type = transferType
}
public enum TransferType {
case file
case text
case url
}
}
public struct FileMetadata {
public let name: String
public let size: Int64
public let mimeType: String
}
struct FoundServiceInfo {
let service: NWBrowser.Result
var device: RemoteDeviceInfo?
}
struct OutgoingTransferInfo {
let service: NWBrowser.Result
let device: RemoteDeviceInfo
let connection: OutboundNearbyConnection
let delegate: ShareExtensionDelegate
}
struct EndpointInfo {
let name: String
let deviceType: RemoteDeviceInfo.DeviceType
init(name: String, deviceType: RemoteDeviceInfo.DeviceType) {
self.name = name
self.deviceType = deviceType
}
init?(data: Data) {
guard data.count > 17 else { return nil }
let deviceNameLength = Int(data[17])
guard data.count >= deviceNameLength + 18 else { return nil }
guard let deviceName = String(data: data[18 ..< (18 + deviceNameLength)], encoding: .utf8) else { return nil }
let rawDeviceType = Int(data[0] & 7) >> 1
name = deviceName
deviceType = RemoteDeviceInfo.DeviceType.fromRawValue(value: rawDeviceType)
}
func serialize() -> Data {
// 1 byte: Version(3 bits)|Visibility(1 bit)|Device Type(3 bits)|Reserved(1 bits)
// Device types: unknown=0, phone=1, tablet=2, laptop=3
var endpointInfo: [UInt8] = [UInt8(deviceType.rawValue << 1)]
// 16 bytes: unknown random bytes
for _ in 0 ... 15 {
endpointInfo.append(UInt8.random(in: 0 ... 255))
}
// Device name in UTF-8 prefixed with 1-byte length
var nameChars = [UInt8](name.utf8)
if nameChars.count > 255 {
nameChars = [UInt8](nameChars[0 ..< 255])
}
endpointInfo.append(UInt8(nameChars.count))
for ch in nameChars {
endpointInfo.append(UInt8(ch))
}
return Data(endpointInfo)
}
}
public protocol MainAppDelegate {
func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo)
func incomingTransfer(id: String, didFinishWith error: Error?)
func showFirewallAlert()
func showCopiedToClipboardAlert()
func showUnsupportedFileAlert(for: RemoteDeviceInfo?)
}
public protocol ShareExtensionDelegate: AnyObject {
func addDevice(device: RemoteDeviceInfo)
func removeDevice(id: String)
func connectionWasEstablished(pinCode: String)
func connectionFailed(with error: Error)
func transferAccepted()
func transferProgress(progress: Double)
func transferFinished()
}
public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearbyConnectionDelegate, OutboundNearbyConnectionDelegate {
public var checkSignature = true {
didSet {
if !checkSignature {
log("WARNING: Signature checking is disabled. Resume with caution.")
}
}
}
private let sleepManager = SleepManager.shared
private var tcpListener: NWListener
public let endpointID: [UInt8] = generateEndpointID()
private var mdnsServices: [NetService] = []
private var activeConnections: [String: InboundNearbyConnection] = [:]
private var incomingConnections: [String: InboundNearbyConnection] = [:]
private var foundServices: [String: FoundServiceInfo] = [:]
private var shareExtensionDelegates: [ShareExtensionDelegate] = []
private var outgoingTransfers: [String: OutgoingTransferInfo] = [:]
public var mainAppDelegate: (any MainAppDelegate)?
private var discoveryRefCount = 0
let serviceTypes = ["_FC9F5ED42C8A._tcp."]
private var startedDeviceDiscovery = false
private var browsers: [NWBrowser] = []
private let serviceTypes = ["_FC9F5ED42C8A._tcp."]
public let endpointID: [UInt8] = generateEndpointID()
public var mainAppDelegate: (any MainAppDelegate)?
public static let shared = NearbyConnectionManager()
override init() {
tcpListener = try! NWListener(using: NWParameters(tls: .none))
super.init()
}
public func becomeVisible() {
startTCPListener()
}
private func startTCPListener() {
tcpListener.stateUpdateHandler = { (state: NWListener.State) in
@@ -202,13 +48,14 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
tcpListener.newConnectionHandler = { (connection: NWConnection) in
let id = UUID().uuidString
let conn = InboundNearbyConnection(connection: connection, id: id)
self.activeConnections[id] = conn
self.incomingConnections[id] = conn
conn.delegate = self
conn.start()
}
tcpListener.start(queue: .global(qos: .utility))
}
private static func generateEndpointID() -> [UInt8] {
let userDefaultsKey = UserDefaultsKeys.endpointID.rawValue
@@ -222,7 +69,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
// Generate a new random ID
var id: [UInt8] = []
let alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".compactMap { UInt8($0.asciiValue!) }
for _ in 0 ..< 4 { // use 0..<4 instead of 0...3 for clarity
for _ in 0 ..< 4 {
id.append(alphabet[Int.random(in: 0 ..< alphabet.count)])
}
@@ -233,6 +80,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
return id
}
private func initMDNS() {
let nameBytes: [UInt8] = [
0x23, // PCP
@@ -240,6 +88,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
0xFC, 0x9F, 0x5E, // Service ID hash
0, 0,
]
let name = Data(nameBytes).urlSafeBase64EncodedString()
let endpointInfo = EndpointInfo(name: Host.current().localizedName!, deviceType: .computer)
@@ -256,35 +105,40 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
}
}
func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo, connection _: InboundNearbyConnection) {
guard let delegate = mainAppDelegate else { return }
delegate.obtainUserConsent(for: transfer, from: device)
mainAppDelegate?.obtainUserConsent(for: transfer, from: device)
}
func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?) {
guard let delegate = mainAppDelegate else { return }
delegate.incomingTransfer(id: connection.id, didFinishWith: error)
activeConnections.removeValue(forKey: connection.id)
incomingConnections.removeValue(forKey: connection.id)
if !connection.wasRejected {
mainAppDelegate?.incomingTransfer(id: connection.id, didFinishWith: error)
}
}
public func submitUserConsent(transferID: String, accept: Bool, storeInTemp: Bool = false) {
guard let conn = activeConnections[transferID] else { return }
guard let conn = incomingConnections[transferID] else { return }
log("Submitting user consent for transfer, accepted: \(accept), store in temp: \(storeInTemp)")
log("[NearbyConnectionManager] Submitting user consent for transfer, accepted: \(accept), store in temp: \(storeInTemp)")
conn.submitUserConsent(accepted: accept, storeInTemp: storeInTemp)
}
public func startDeviceDiscovery() {
log("Device discovery requested.")
if discoveryRefCount == 0 {
if !startedDeviceDiscovery {
startedDeviceDiscovery = true
foundServices.removeAll()
log("Starting device discovery")
log("[NearbyConnectionManager] Starting device discovery")
if browsers.isEmpty {
for type in serviceTypes {
log("Starting browser for type \(type)")
log("[NearbyConnectionManager] Starting browser for type \(type)")
let browser = NWBrowser(for: .bonjourWithTXTRecord(type: type, domain: nil), using: .tcp)
browser.browseResultsChangedHandler = { _, changes in
@@ -295,7 +149,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
case let .removed(res):
self.removeFoundDevice(service: res)
default:
log("Ignoring change \(change)")
log("[NearbyConnectionManager] Ignoring change \(change)")
}
}
}
@@ -304,23 +158,24 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
}
}
}
discoveryRefCount += 1
}
public func stopDeviceDiscovery() {
discoveryRefCount -= 1
assert(discoveryRefCount >= 0)
if discoveryRefCount == 0 {
if startedDeviceDiscovery {
log("Stopping device discovery")
for browser in browsers {
browser.cancel()
}
browsers.removeAll()
startedDeviceDiscovery = false
}
}
public func addShareExtensionDelegate(_ delegate: ShareExtensionDelegate) {
shareExtensionDelegates.append(delegate)
for service in foundServices.values {
@@ -329,15 +184,18 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
}
}
public func removeShareExtensionDelegate(_ delegate: ShareExtensionDelegate) {
shareExtensionDelegates.removeAll(where: { $0 === delegate })
}
public func cancelOutgoingTransfer(id: String) {
guard let transfer = outgoingTransfers[id] else { return }
transfer.connection.cancel()
}
private func endpointID(for service: NWBrowser.Result) -> String? {
guard case let NWEndpoint.service(name: serviceName, type: _, domain: _, interface: _) = service.endpoint else { return nil }
guard let nameData = Data.dataFromUrlSafeBase64(serviceName) else { return nil }
@@ -350,16 +208,15 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
return endpointID
}
private func addFoundDevice(service: NWBrowser.Result) {
log("found service \(service)")
log("[NearbyConnectionManager] Found Service \(service)")
for interface in service.interfaces {
if case .loopback = interface.type {
log("ignoring localhost service")
return
}
}
guard let endpointID = endpointID(for: service) else { return }
log("service name is valid, endpoint ID \(endpointID)")
var foundService = FoundServiceInfo(service: service)
guard case let NWBrowser.Result.Metadata.bonjour(txtRecord) = service.metadata else { return }
@@ -379,6 +236,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
}
}
private func removeFoundDevice(service: NWBrowser.Result) {
guard let endpointID = endpointID(for: service) else { return }
guard let _ = foundServices.removeValue(forKey: endpointID) else { return }
@@ -387,6 +245,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
}
}
public func startOutgoingTransfer(deviceID: String, delegate: ShareExtensionDelegate, urls: [URL], textToSend: String?) {
guard let info = foundServices[deviceID] else { return }
let tcp = NWProtocolTCP.Options()
@@ -399,12 +258,14 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
conn.start()
}
func outboundConnectionWasEstablished(connection: OutboundNearbyConnection) {
guard let transfer = outgoingTransfers[connection.id] else { return }
DispatchQueue.main.async {
transfer.delegate.connectionWasEstablished(pinCode: connection.pinCode!)
}
}
func outboundConnectionTransferAccepted(connection: OutboundNearbyConnection) {
guard let transfer = outgoingTransfers[connection.id] else { return }
@@ -413,6 +274,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
}
}
func outboundConnection(connection: OutboundNearbyConnection, transferProgress: Double) {
guard let transfer = outgoingTransfers[connection.id] else { return }
DispatchQueue.main.async {
@@ -420,6 +282,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
}
}
func outboundConnection(connection: OutboundNearbyConnection, failedWithError: Error) {
guard let transfer = outgoingTransfers[connection.id] else { return }
DispatchQueue.main.async {
@@ -427,6 +290,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
}
outgoingTransfers.removeValue(forKey: connection.id)
}
func outboundConnectionTransferFinished(connection: OutboundNearbyConnection) {
guard let transfer = outgoingTransfers[connection.id] else { return }
@@ -436,7 +300,24 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
outgoingTransfers.removeValue(forKey: connection.id)
}
public func getActiveConnectionsCount() -> Int {
return activeConnections.count
return incomingConnections.count + outgoingTransfers.count
}
// -- MARK: - Internal Data Model
private struct FoundServiceInfo {
let service: NWBrowser.Result
var device: RemoteDeviceInfo?
}
private struct OutgoingTransferInfo {
let service: NWBrowser.Result
let device: RemoteDeviceInfo
let connection: OutboundNearbyConnection
let delegate: ShareExtensionDelegate
}
}
@@ -11,7 +11,6 @@ import Foundation
import Network
import System
import UniformTypeIdentifiers
import BigInt
import SwiftECC
@@ -46,6 +45,7 @@ class OutboundNearbyConnection: NearbyConnection {
}
}
deinit {
if let transfer = currentTransfer, let handle = transfer.handle {
try? handle.close()
@@ -56,7 +56,19 @@ class OutboundNearbyConnection: NearbyConnection {
}
}
}
override func disconnect() {
super.disconnect()
if let error = lastError {
DispatchQueue.main.async {
self.delegate?.outboundConnection(connection: self, failedWithError: error)
}
}
}
public func cancel() {
cancelled = true
if encryptionDone {
@@ -68,6 +80,7 @@ class OutboundNearbyConnection: NearbyConnection {
}
try? sendDisconnectionAndDisconnect()
}
override func connectionReady() {
super.connectionReady()
@@ -79,14 +92,20 @@ class OutboundNearbyConnection: NearbyConnection {
protocolError()
}
}
override func isServer() -> Bool {
return false
}
override func processReceivedFrame(frameData: Data) {
if currentState != .sendingFiles {
log("[OutboundNearbyConnection] Received frame in state \(currentState)...")
}
do {
log("received \(frameData), state is \(currentState)")
switch currentState {
case .initial:
protocolError()
@@ -99,6 +118,9 @@ class OutboundNearbyConnection: NearbyConnection {
try decryptAndProcessReceivedSecureMessage(smsg)
}
} catch {
log("[OutboundNearbyConnection] Error occured while processing frame with data \(frameData.hex): \(error)")
if case NearbyError.ukey2 = error {
// do nothing
}
@@ -110,14 +132,17 @@ class OutboundNearbyConnection: NearbyConnection {
}
}
override func processTransferSetupFrame(_ frame: Sharing_Nearby_Frame) throws {
if frame.hasV1 && frame.v1.hasType, case .cancel = frame.v1.type {
log("Transfer canceled")
log("[OutboundNearbyConnection] Transfer canceled")
try sendDisconnectionAndDisconnect()
delegate?.outboundConnection(connection: self, failedWithError: NearbyError.canceled(reason: .userCanceled))
return
}
log(frame.debugDescription)
switch currentState {
case .sentPairedKeyEncryption:
try processPairedKeyEncryption(frame: frame)
@@ -128,15 +153,17 @@ class OutboundNearbyConnection: NearbyConnection {
case .sendingFiles:
break
default:
assertionFailure("Unexpected state \(currentState)")
assertionFailure("[OutboundNearbyConnection] Unexpected state \(currentState)")
}
}
override func protocolError() {
super.protocolError()
delegate?.outboundConnection(connection: self, failedWithError: lastError!)
}
private func sendConnectionRequest() throws {
var frame = Location_Nearby_Connections_OfflineFrame()
frame.version = .v1
@@ -151,6 +178,7 @@ class OutboundNearbyConnection: NearbyConnection {
try sendFrameAsync(frame.serializedData())
}
private func sendUkey2ClientInit() throws {
let domain = Domain.instance(curve: .EC256r1)
let (pubKey, privKey) = domain.makeKeyPair()
@@ -188,24 +216,29 @@ class OutboundNearbyConnection: NearbyConnection {
sendFrameAsync(ukeyClientInitMsgData!)
currentState = .sentUkeyClientInit
}
private func processUkey2ServerInit(frame: Securegcm_Ukey2Message, raw: Data) throws {
ukeyServerInitMsgData = raw
guard frame.messageType == .serverInit else {
sendUkey2Alert(type: .badMessageType)
log("[OutboundNearbyConnection] Invalid message type: \(frame.messageType)")
throw NearbyError.ukey2
}
let serverInit = try Securegcm_Ukey2ServerInit(serializedBytes: frame.messageData)
guard serverInit.version == 1 else {
sendUkey2Alert(type: .badVersion)
log("[OutboundNearbyConnection] Invalid version: \(serverInit.version)")
throw NearbyError.ukey2
}
guard serverInit.random.count == 32 else {
sendUkey2Alert(type: .badRandom)
log("[OutboundNearbyConnection] Invalid random: \(serverInit.random.count)")
throw NearbyError.ukey2
}
guard serverInit.handshakeCipher == .p256Sha512 else {
sendUkey2Alert(type: .badHandshakeCipher)
log("[OutboundNearbyConnection] Invalid handshake cipher: \(serverInit.handshakeCipher)")
throw NearbyError.ukey2
}
@@ -229,8 +262,8 @@ class OutboundNearbyConnection: NearbyConnection {
delegate?.outboundConnectionWasEstablished(connection: self)
}
private func processConnectionResponse(frame: Location_Nearby_Connections_OfflineFrame) throws {
log("connection response: \(frame)")
guard frame.version == .v1 else { throw NearbyError.protocolError("Unexpected offline frame version \(frame.version)") }
guard frame.v1.type == .connectionResponse else { throw NearbyError.protocolError("Unexpected frame type \(frame.v1.type)") }
guard frame.v1.connectionResponse.response == .accept else { throw NearbyError.protocolError("Connection was rejected by recipient") }
@@ -247,6 +280,7 @@ class OutboundNearbyConnection: NearbyConnection {
currentState = .sentPairedKeyEncryption
}
private func processPairedKeyEncryption(frame: Sharing_Nearby_Frame) throws {
guard frame.hasV1, frame.v1.hasPairedKeyEncryption else { throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.pairedKeyEncryption") }
var pairedResult = Sharing_Nearby_Frame()
@@ -259,6 +293,7 @@ class OutboundNearbyConnection: NearbyConnection {
currentState = .sentPairedKeyResult
}
private func processPairedKeyResult(frame: Sharing_Nearby_Frame) throws {
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.pairedKeyResult") }
@@ -291,15 +326,9 @@ class OutboundNearbyConnection: NearbyConnection {
let typeID = try? url.resourceValues(forKeys: [.typeIdentifierKey]).typeIdentifier
meta.mimeType = "application/octet-stream"
if let typeID = typeID {
if #available(macOS 11.0, *) {
let type = UTType(typeID)
if let type = type, let mimeType = type.preferredMIMEType {
meta.mimeType = mimeType
}
} else {
if let mimeType = UTTypeCopyPreferredTagWithClass(typeID as CFString, kUTTagClassMIMEType) {
meta.mimeType = (mimeType.takeRetainedValue() as NSString) as String
}
let type = UTType(typeID)
if let type = type, let mimeType = type.preferredMIMEType {
meta.mimeType = mimeType
}
}
if meta.mimeType.starts(with: "image/") {
@@ -319,11 +348,12 @@ class OutboundNearbyConnection: NearbyConnection {
totalBytesToSend += meta.size
}
}
log("sent introduction: \(introduction)")
log("[OutboundNearbyConnection] Sent introduction: \(introduction)")
try sendTransferSetupFrame(introduction)
currentState = .sentIntroduction
}
private func processConsent(frame: Sharing_Nearby_Frame) throws {
guard frame.version == .v1, frame.v1.type == .response else { throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.type==response") }
@@ -355,19 +385,23 @@ class OutboundNearbyConnection: NearbyConnection {
}
}
private func hasURL() -> Bool {
urlsToSend.count == 1 && !urlsToSend[0].isFileURL
}
private func sendURL() throws {
try sendText(text: urlsToSend[0].absoluteString)
}
private func sendText(text: String) throws {
try sendBytesPayload(data: Data(text.utf8), id: textPayloadID)
delegate?.outboundConnectionTransferFinished(connection: self)
try sendDisconnectionAndDisconnect()
}
private func sendNextFileChunk() throws {
if cancelled {
@@ -378,7 +412,7 @@ class OutboundNearbyConnection: NearbyConnection {
try currentTransfer?.handle?.close()
}
if queue.isEmpty {
log("Disconnecting because all files have been transferred")
log("[OutboundNearbyConnection] Disconnecting because all files have been transferred")
try sendDisconnectionAndDisconnect()
delegate?.outboundConnectionTransferFinished(connection: self)
return
@@ -387,14 +421,10 @@ class OutboundNearbyConnection: NearbyConnection {
}
let fileBuffer: Data
if #available(macOS 10.15.4, *) {
guard let _fileBuffer = try currentTransfer!.handle!.read(upToCount: 512 * 1024) else {
throw NearbyError.inputOutput
}
fileBuffer = _fileBuffer
} else {
fileBuffer = currentTransfer!.handle!.readData(ofLength: 512 * 1024)
guard let _fileBuffer = try currentTransfer!.handle!.read(upToCount: 512 * 1024) else {
throw NearbyError.inputOutput
}
fileBuffer = _fileBuffer
var transfer = Location_Nearby_Connections_PayloadTransferFrame()
transfer.packetType = .data
@@ -421,7 +451,11 @@ class OutboundNearbyConnection: NearbyConnection {
}
})
totalBytesSent += Int64(fileBuffer.count)
log("sent file chunk \(totalBytesSent)")
// only for logging
self.bytesTransferred += Int64(fileBuffer.count)
startInactivityTimer()
delegate?.outboundConnection(connection: self, transferProgress: Double(totalBytesSent) / Double(totalBytesToSend))
if currentTransfer!.currentOffset == currentTransfer!.totalBytes {
@@ -441,27 +475,23 @@ class OutboundNearbyConnection: NearbyConnection {
wrapper.v1.type = .payloadTransfer
wrapper.v1.payloadTransfer = transfer
try encryptAndSendOfflineFrame(wrapper)
log("sent EOF, current transfer: \(String(describing: currentTransfer))")
log("[OutboundNearbyConnection] Sent EOF, current transfer: \(String(describing: currentTransfer))")
}
}
private static func sanitizeFileName(name: String) -> String {
return name.replacingOccurrences(of: "[\\/\\\\?%\\*:\\|\"<>=]", with: "_", options: .regularExpression)
}
}
private struct OutgoingFileTransfer {
let url: URL
let payloadID: Int64
let handle: FileHandle?
let totalBytes: Int64
var currentOffset: Int64
}
protocol OutboundNearbyConnectionDelegate {
func outboundConnectionWasEstablished(connection: OutboundNearbyConnection)
func outboundConnection(connection: OutboundNearbyConnection, transferProgress: Double)
func outboundConnectionTransferAccepted(connection: OutboundNearbyConnection)
func outboundConnection(connection: OutboundNearbyConnection, failedWithError: Error)
func outboundConnectionTransferFinished(connection: OutboundNearbyConnection)
// -- MARK: - Internal Data Model
private struct OutgoingFileTransfer {
let url: URL
let payloadID: Int64
let handle: FileHandle?
let totalBytes: Int64
var currentOffset: Int64
}
}
@@ -0,0 +1,81 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto2";
package securegcm;
import "securemessage.proto";
option optimize_for = LITE_RUNTIME;
option java_package = "com.google.security.cryptauth.lib.securegcm";
option java_outer_classname = "DeviceToDeviceMessagesProto";
option objc_class_prefix = "SGCM";
// Used by protocols between devices
message DeviceToDeviceMessage {
// the payload of the message
optional bytes message = 1;
// the sequence number of the message - must be increasing.
optional int32 sequence_number = 2;
}
// sent as the first message from initiator to responder
// in an unauthenticated Diffie-Hellman Key Exchange
message InitiatorHello {
// The session public key to send to the responder
optional securemessage.GenericPublicKey public_dh_key = 1;
// The protocol version
optional int32 protocol_version = 2 [default = 0];
}
// sent inside the header of the first message from the responder to the
// initiator in an unauthenticated Diffie-Hellman Key Exchange
message ResponderHello {
// The session public key to send to the initiator
optional securemessage.GenericPublicKey public_dh_key = 1;
// The protocol version
optional int32 protocol_version = 2 [default = 0];
}
// Type of curve
enum Curve { ED_25519 = 1; }
// A convenience proto for encoding curve points in affine representation
message EcPoint {
required Curve curve = 1;
// x and y are encoded in big-endian two's complement
// client MUST verify (x,y) is a valid point on the specified curve
required bytes x = 2;
required bytes y = 3;
}
message SpakeHandshakeMessage {
// Each flow in the protocol bumps this counter
optional int32 flow_number = 1;
// Some (but not all) SPAKE flows send a point on an elliptic curve
optional EcPoint ec_point = 2;
// Some (but not all) SPAKE flows send a hash value
optional bytes hash_value = 3;
// The last flow of a SPAKE protocol can send an optional payload,
// since the key exchange is already complete on the sender's side.
optional bytes payload = 4;
}
@@ -0,0 +1,403 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto2";
package location.nearby.connections;
option optimize_for = LITE_RUNTIME;
option java_outer_classname = "OfflineWireFormatsProto";
option java_package = "com.google.location.nearby.connections.proto";
option objc_class_prefix = "GNCP";
message OfflineFrame {
enum Version {
UNKNOWN_VERSION = 0;
V1 = 1;
}
optional Version version = 1;
// Right now there's only 1 version, but if there are more, exactly one of
// the following fields will be set.
optional V1Frame v1 = 2;
}
message V1Frame {
enum FrameType {
UNKNOWN_FRAME_TYPE = 0;
CONNECTION_REQUEST = 1;
CONNECTION_RESPONSE = 2;
PAYLOAD_TRANSFER = 3;
BANDWIDTH_UPGRADE_NEGOTIATION = 4;
KEEP_ALIVE = 5;
DISCONNECTION = 6;
PAIRED_KEY_ENCRYPTION = 7;
}
optional FrameType type = 1;
// Exactly one of the following fields will be set.
optional ConnectionRequestFrame connection_request = 2;
optional ConnectionResponseFrame connection_response = 3;
optional PayloadTransferFrame payload_transfer = 4;
optional BandwidthUpgradeNegotiationFrame bandwidth_upgrade_negotiation = 5;
optional KeepAliveFrame keep_alive = 6;
optional DisconnectionFrame disconnection = 7;
optional PairedKeyEncryptionFrame paired_key_encryption = 8;
}
message ConnectionRequestFrame {
// Should always match cs/symbol:location.nearby.proto.connections.Medium
// LINT.IfChange
enum Medium {
UNKNOWN_MEDIUM = 0;
MDNS = 1 [deprecated = true];
BLUETOOTH = 2;
WIFI_HOTSPOT = 3;
BLE = 4;
WIFI_LAN = 5;
WIFI_AWARE = 6;
NFC = 7;
WIFI_DIRECT = 8;
WEB_RTC = 9;
BLE_L2CAP = 10;
USB = 11;
}
// LINT.ThenChange(//depot/google3/third_party/nearby/proto/connections_enums.proto)
optional string endpoint_id = 1;
optional string endpoint_name = 2;
optional bytes handshake_data = 3;
// A random number generated for each outgoing connection that is presently
// used to act as a tiebreaker when 2 devices connect to each other
// simultaneously; this can also be used for other initialization-scoped
// things in the future.
optional int32 nonce = 4;
// The mediums this device supports upgrading to. This list should be filtered
// by both the strategy and this device's individual limitations.
repeated Medium mediums = 5;
optional bytes endpoint_info = 6;
optional MediumMetadata medium_metadata = 7;
optional int32 keep_alive_interval_millis = 8;
optional int32 keep_alive_timeout_millis = 9;
// The type of {@link Device} object.
optional int32 device_type = 10 [default = 0];
// The bytes of serialized {@link Device} object.
optional bytes device_info = 11;
}
message ConnectionResponseFrame {
// This doesn't need to send back endpoint_id and endpoint_name (like
// the ConnectionRequestFrame does) because those have already been
// transmitted out-of-band, at the time this endpoint was discovered.
// One of:
//
// - ConnectionsStatusCodes.STATUS_OK
// - ConnectionsStatusCodes.STATUS_CONNECTION_REJECTED.
optional int32 status = 1 [deprecated = true];
optional bytes handshake_data = 2;
// Used to replace the status integer parameter with a meaningful enum item.
// Map ConnectionsStatusCodes.STATUS_OK to ACCEPT and
// ConnectionsStatusCodes.STATUS_CONNECTION_REJECTED to REJECT.
// Flag: connection_replace_status_with_response_connectionResponseFrame
enum ResponseStatus {
UNKNOWN_RESPONSE_STATUS = 0;
ACCEPT = 1;
REJECT = 2;
}
optional ResponseStatus response = 3;
optional OsInfo os_info = 4;
// A bitmask value to indicate which medium supports Multiplex transmission
// feature. Each supporting medium could utilize one bit starting from the
// least significant bit in this field. eq. BT utilizes the LSB bit which 0x01
// means bt supports multiplex while 0x00 means not. Refer to ClientProxy.java
// for the bit usages.
optional int32 multiplex_socket_bitmask = 5;
optional int32 nearby_connections_version = 6;
}
message PayloadTransferFrame {
enum PacketType {
UNKNOWN_PACKET_TYPE = 0;
DATA = 1;
CONTROL = 2;
}
message PayloadHeader {
enum PayloadType {
UNKNOWN_PAYLOAD_TYPE = 0;
BYTES = 1;
FILE = 2;
STREAM = 3;
}
optional int64 id = 1;
optional PayloadType type = 2;
optional int64 total_size = 3;
optional bool is_sensitive = 4;
optional string file_name = 5;
optional string parent_folder = 6;
}
// Accompanies DATA packets.
message PayloadChunk {
enum Flags {
LAST_CHUNK = 0x1;
}
optional int32 flags = 1;
optional int64 offset = 2;
optional bytes body = 3;
}
// Accompanies CONTROL packets.
message ControlMessage {
enum EventType {
UNKNOWN_EVENT_TYPE = 0;
PAYLOAD_ERROR = 1;
PAYLOAD_CANCELED = 2;
PAYLOAD_RECEIVED_ACK = 3;
}
optional EventType event = 1;
optional int64 offset = 2;
}
optional PacketType packet_type = 1;
optional PayloadHeader payload_header = 2;
// Exactly one of the following fields will be set, depending on the type.
optional PayloadChunk payload_chunk = 3;
optional ControlMessage control_message = 4;
}
message BandwidthUpgradeNegotiationFrame {
enum EventType {
UNKNOWN_EVENT_TYPE = 0;
UPGRADE_PATH_AVAILABLE = 1;
LAST_WRITE_TO_PRIOR_CHANNEL = 2;
SAFE_TO_CLOSE_PRIOR_CHANNEL = 3;
CLIENT_INTRODUCTION = 4;
UPGRADE_FAILURE = 5;
CLIENT_INTRODUCTION_ACK = 6;
}
// Accompanies UPGRADE_PATH_AVAILABLE and UPGRADE_FAILURE events.
message UpgradePathInfo {
// Should always match cs/symbol:location.nearby.proto.connections.Medium
enum Medium {
UNKNOWN_MEDIUM = 0;
MDNS = 1 [deprecated = true];
BLUETOOTH = 2;
WIFI_HOTSPOT = 3;
BLE = 4;
WIFI_LAN = 5;
WIFI_AWARE = 6;
NFC = 7;
WIFI_DIRECT = 8;
WEB_RTC = 9;
// 10 is reserved.
USB = 11;
}
// Accompanies Medium.WIFI_HOTSPOT.
message WifiHotspotCredentials {
optional string ssid = 1;
optional string password = 2;
optional int32 port = 3;
optional string gateway = 4 [default = "0.0.0.0"];
// This field can be a band or frequency
optional int32 frequency = 5 [default = -1];
}
// Accompanies Medium.WIFI_LAN.
message WifiLanSocket {
optional bytes ip_address = 1;
optional int32 wifi_port = 2;
}
// Accompanies Medium.BLUETOOTH.
message BluetoothCredentials {
optional string service_name = 1;
optional string mac_address = 2;
}
// Accompanies Medium.WIFI_AWARE.
message WifiAwareCredentials {
optional string service_id = 1;
optional bytes service_info = 2;
optional string password = 3;
}
// Accompanies Medium.WIFI_DIRECT.
message WifiDirectCredentials {
optional string ssid = 1;
optional string password = 2;
optional int32 port = 3;
optional int32 frequency = 4;
optional string gateway = 5 [default = "0.0.0.0"];
}
// Accompanies Medium.WEB_RTC
message WebRtcCredentials {
optional string peer_id = 1;
optional LocationHint location_hint = 2;
}
optional Medium medium = 1;
// Exactly one of the following fields will be set.
optional WifiHotspotCredentials wifi_hotspot_credentials = 2;
optional WifiLanSocket wifi_lan_socket = 3;
optional BluetoothCredentials bluetooth_credentials = 4;
optional WifiAwareCredentials wifi_aware_credentials = 5;
optional WifiDirectCredentials wifi_direct_credentials = 6;
optional WebRtcCredentials web_rtc_credentials = 8;
// Disable Encryption for this upgrade medium to improve throughput.
optional bool supports_disabling_encryption = 7;
// An ack will be sent after the CLIENT_INTRODUCTION frame.
optional bool supports_client_introduction_ack = 9;
}
// Accompanies CLIENT_INTRODUCTION events.
message ClientIntroduction {
optional string endpoint_id = 1;
optional bool supports_disabling_encryption = 2;
}
// Accompanies CLIENT_INTRODUCTION_ACK events.
message ClientIntroductionAck {}
optional EventType event_type = 1;
// Exactly one of the following fields will be set.
optional UpgradePathInfo upgrade_path_info = 2;
optional ClientIntroduction client_introduction = 3;
optional ClientIntroductionAck client_introduction_ack = 4;
}
message KeepAliveFrame {
// And ack will be sent after receiving KEEP_ALIVE frame.
optional bool ack = 1;
}
// Informs the remote side to immediately severe the socket connection.
// Used in bandwidth upgrades to get around a race condition, but may be used
// in other situations to trigger a faster disconnection event than waiting for
// socket closed on the remote side.
message DisconnectionFrame {
// Apply safe-to-disconnect protocol if true.
optional bool request_safe_to_disconnect = 1;
// Ack of receiving Disconnection frame will be sent to the sender
// frame.
optional bool ack_safe_to_disconnect = 2;
}
// A paired key encryption packet sent between devices, contains signed data.
message PairedKeyEncryptionFrame {
// The encrypted data (raw authentication token for the established
// connection) in byte array format.
optional bytes signed_data = 1;
}
message MediumMetadata {
// True if local device supports 5GHz.
optional bool supports_5_ghz = 1;
// WiFi LAN BSSID, in the form of a six-byte MAC address: XX:XX:XX:XX:XX:XX
optional string bssid = 2;
// IP address, in network byte order: the highest order byte of the address is
// in byte[0].
optional bytes ip_address = 3;
// True if local device supports 6GHz.
optional bool supports_6_ghz = 4;
// True if local device has mobile radio.
optional bool mobile_radio = 5;
// The frequency of the WiFi LAN AP(in MHz). Or -1 is not associated with an
// AP over WiFi, -2 represents the active network uses an Ethernet transport.
optional int32 ap_frequency = 6 [default = -1];
// Available channels on the local device.
optional AvailableChannels available_channels = 7;
// Usable WiFi Direct client channels on the local device.
optional WifiDirectCliUsableChannels wifi_direct_cli_usable_channels = 8;
// Usable WiFi LAN channels on the local device.
optional WifiLanUsableChannels wifi_lan_usable_channels = 9;
// Usable WiFi Aware channels on the local device.
optional WifiAwareUsableChannels wifi_aware_usable_channels = 10;
// Usable WiFi Hotspot STA channels on the local device.
optional WifiHotspotStaUsableChannels wifi_hotspot_sta_usable_channels = 11;
}
// Available channels on the local device.
message AvailableChannels {
repeated int32 channels = 1 [packed = true];
}
// Usable WiFi Direct client channels on the local device.
message WifiDirectCliUsableChannels {
repeated int32 channels = 1 [packed = true];
}
// Usable WiFi LAN channels on the local device.
message WifiLanUsableChannels {
repeated int32 channels = 1 [packed = true];
}
// Usable WiFi Aware channels on the local device.
message WifiAwareUsableChannels {
repeated int32 channels = 1 [packed = true];
}
// Usable WiFi Hotspot STA channels on the local device.
message WifiHotspotStaUsableChannels {
repeated int32 channels = 1 [packed = true];
}
// LocationHint is used to specify a location as well as format.
message LocationHint {
// Location is the location, provided in the format specified by format.
optional string location = 1;
// the format of location.
optional LocationStandard.Format format = 2;
}
message LocationStandard {
enum Format {
UNKNOWN = 0;
// E164 country codes:
// https://en.wikipedia.org/wiki/List_of_country_calling_codes
// e.g. +1 for USA
E164_CALLING = 1;
// ISO 3166-1 alpha-2 country codes:
// https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
ISO_3166_1_ALPHA_2 = 2;
}
}
// Device capability for OS information.
message OsInfo {
enum OsType {
UNKNOWN_OS_TYPE = 0;
ANDROID = 1;
CHROME_OS = 2;
WINDOWS = 3;
APPLE = 4;
LINUX = 100; // g3 test environment
}
optional OsType type = 1;
}
@@ -0,0 +1,308 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto2";
package securegcm;
option optimize_for = LITE_RUNTIME;
option java_package = "com.google.security.cryptauth.lib.securegcm";
option java_outer_classname = "SecureGcmProto";
option objc_class_prefix = "SGCM";
// Message used only during enrollment
// Field numbers should be kept in sync with DeviceInfo in:
// java/com/google/security/cryptauth/backend/services/common/common.proto
message GcmDeviceInfo {
// This field's name does not match the one in DeviceInfo for legacy reasons.
// Consider using long_device_id and device_type instead when enrolling
// non-android devices.
optional fixed64 android_device_id = 1;
// Used for device_address of DeviceInfo field 2, but for GCM capable devices.
optional bytes gcm_registration_id = 102;
// Used for device_address of DeviceInfo field 2, but for iOS devices.
optional bytes apn_registration_id = 202;
// Does the user have notifications enabled for the given device address.
optional bool notification_enabled = 203 [default = true];
// Used for device_address of DeviceInfo field 2, a Bluetooth Mac address for
// the device (e.g., to be used with EasyUnlock)
optional string bluetooth_mac_address = 302;
// SHA-256 hash of the device master key (from the key exchange).
// Differs from DeviceInfo field 3, which contains the actual master key.
optional bytes device_master_key_hash = 103;
// A SecureMessage.EcP256PublicKey
required bytes user_public_key = 4;
// device's model name
// (e.g., an android.os.Build.MODEL or UIDevice.model)
optional string device_model = 7;
// device's locale
optional string locale = 8;
// The handle for user_public_key (and implicitly, a master key)
optional bytes key_handle = 9;
// The initial counter value for the device, sent by the device
optional int64 counter = 12 [default = 0];
// The Operating System version on the device
// (e.g., an android.os.Build.DISPLAY or UIDevice.systemVersion)
optional string device_os_version = 13;
// The Operating System version number on the device
// (e.g., an android.os.Build.VERSION.SDK_INT)
optional int64 device_os_version_code = 14;
// The Operating System release on the device
// (e.g., an android.os.Build.VERSION.RELEASE)
optional string device_os_release = 15;
// The Operating System codename on the device
// (e.g., an android.os.Build.VERSION.CODENAME or UIDevice.systemName)
optional string device_os_codename = 16;
// The software version running on the device
// (e.g., Authenticator app version string)
optional string device_software_version = 17;
// The software version number running on the device
// (e.g., Authenticator app version code)
optional int64 device_software_version_code = 18;
// Software package information if applicable
// (e.g., com.google.android.apps.authenticator2)
optional string device_software_package = 19;
// Size of the display in thousandths of an inch (e.g., 7000 mils = 7 in)
optional int32 device_display_diagonal_mils = 22;
// For Authzen capable devices, their Authzen protocol version
optional int32 device_authzen_version = 24;
// Not all devices have device identifiers that fit in 64 bits.
optional bytes long_device_id = 29;
// The device manufacturer name
// (e.g., android.os.Build.MANUFACTURER)
optional string device_manufacturer = 31;
// Used to indicate which type of device this is.
optional DeviceType device_type = 32 [default = ANDROID];
// Fields corresponding to screenlock type/features and hardware features
// should be numbered in the 400 range.
// Is this device using a secure screenlock (e.g., pattern or pin unlock)
optional bool using_secure_screenlock = 400 [default = false];
// Is auto-unlocking the screenlock (e.g., when at "home") supported?
optional bool auto_unlock_screenlock_supported = 401 [default = false];
// Is auto-unlocking the screenlock (e.g., when at "home") enabled?
optional bool auto_unlock_screenlock_enabled = 402 [default = false];
// Does the device have a Bluetooth (classic) radio?
optional bool bluetooth_radio_supported = 403 [default = false];
// Is the Bluetooth (classic) radio on?
optional bool bluetooth_radio_enabled = 404 [default = false];
// Does the device hardware support a mobile data connection?
optional bool mobile_data_supported = 405 [default = false];
// Does the device support tethering?
optional bool tethering_supported = 406 [default = false];
// Does the device have a BLE radio?
optional bool ble_radio_supported = 407 [default = false];
// Is the device a "Pixel Experience" Android device?
optional bool pixel_experience = 408 [default = false];
// Is the device running in the ARC++ container on a chromebook?
optional bool arc_plus_plus = 409 [default = false];
// Is the value set in |using_secure_screenlock| reliable? On some Android
// devices, the platform API to get the screenlock state is not trustworthy.
// See b/32212161.
optional bool is_screenlock_state_flaky = 410 [default = false];
// A list of multi-device software features supported by the device.
repeated SoftwareFeature supported_software_features = 411;
// A list of multi-device software features currently enabled (active) on the
// device.
repeated SoftwareFeature enabled_software_features = 412;
// The enrollment session id this is sent with
optional bytes enrollment_session_id = 1000;
// A copy of the user's OAuth token
optional string oauth_token = 1001;
}
// This enum is used by iOS devices as values for device_display_diagonal_mils
// in GcmDeviceInfo. There is no good way to calculate it on those devices.
enum AppleDeviceDiagonalMils {
// This is the mils diagonal on an iPhone 5.
APPLE_PHONE = 4000;
// This is the mils diagonal on an iPad mini.
APPLE_PAD = 7900;
}
// This should be kept in sync with DeviceType in:
// java/com/google/security/cryptauth/backend/services/common/common_enums.proto
enum DeviceType {
UNKNOWN = 0;
ANDROID = 1;
CHROME = 2;
IOS = 3;
BROWSER = 4;
OSX = 5;
}
// MultiDevice features which may be supported and enabled on a device. See
enum SoftwareFeature {
UNKNOWN_FEATURE = 0;
BETTER_TOGETHER_HOST = 1;
BETTER_TOGETHER_CLIENT = 2;
EASY_UNLOCK_HOST = 3;
EASY_UNLOCK_CLIENT = 4;
MAGIC_TETHER_HOST = 5;
MAGIC_TETHER_CLIENT = 6;
SMS_CONNECT_HOST = 7;
SMS_CONNECT_CLIENT = 8;
}
// A list of "reasons" that can be provided for calling server-side APIs.
// This is particularly important for calls that can be triggered by different
// kinds of events. Please try to keep reasons as generic as possible, so that
// codes can be re-used by various callers in a sensible fashion.
enum InvocationReason {
REASON_UNKNOWN = 0;
// First run of the software package invoking this call
REASON_INITIALIZATION = 1;
// Ordinary periodic actions (e.g. monthly master key rotation)
REASON_PERIODIC = 2;
// Slow-cycle periodic action (e.g. yearly keypair rotation???)
REASON_SLOW_PERIODIC = 3;
// Fast-cycle periodic action (e.g. daily sync for Smart Lock users)
REASON_FAST_PERIODIC = 4;
// Expired state (e.g. expired credentials, or cached entries) was detected
REASON_EXPIRATION = 5;
// An unexpected protocol failure occurred (so attempting to repair state)
REASON_FAILURE_RECOVERY = 6;
// A new account has been added to the device
REASON_NEW_ACCOUNT = 7;
// An existing account on the device has been changed
REASON_CHANGED_ACCOUNT = 8;
// The user toggled the state of a feature (e.g. Smart Lock enabled via BT)
REASON_FEATURE_TOGGLED = 9;
// A "push" from the server caused this action (e.g. a sync tickle)
REASON_SERVER_INITIATED = 10;
// A local address change triggered this (e.g. GCM registration id changed)
REASON_ADDRESS_CHANGE = 11;
// A software update has triggered this
REASON_SOFTWARE_UPDATE = 12;
// A manual action by the user triggered this (e.g. commands sent via adb)
REASON_MANUAL = 13;
// A custom key has been invalidated on the device (e.g. screen lock is
// disabled).
REASON_CUSTOM_KEY_INVALIDATION = 14;
// Periodic action triggered by auth_proximity
REASON_PROXIMITY_PERIODIC = 15;
}
enum Type {
ENROLLMENT = 0;
TICKLE = 1;
TX_REQUEST = 2;
TX_REPLY = 3;
TX_SYNC_REQUEST = 4;
TX_SYNC_RESPONSE = 5;
TX_PING = 6;
DEVICE_INFO_UPDATE = 7;
TX_CANCEL_REQUEST = 8;
// DEPRECATED (can be re-used after Aug 2015)
PROXIMITYAUTH_PAIRING = 10;
// The kind of identity assertion generated by a "GCM V1" device (i.e.,
// an Android phone that has registered with us a public and a symmetric
// key)
GCMV1_IDENTITY_ASSERTION = 11;
// Device-to-device communications are protected by an unauthenticated
// Diffie-Hellman exchange. The InitiatorHello message is simply the
// initiator's public DH key, and is not encoded as a SecureMessage, so
// it doesn't have a tag.
// The ResponderHello message (which is sent by the responder
// to the initiator), on the other hand, carries a payload that is protected
// by the derived shared key. It also contains the responder's
// public DH key. ResponderHelloAndPayload messages have the
// DEVICE_TO_DEVICE_RESPONDER_HELLO tag.
DEVICE_TO_DEVICE_RESPONDER_HELLO_PAYLOAD = 12;
// Device-to-device communications are protected by an unauthenticated
// Diffie-Hellman exchange. Once the initiator and responder
// agree on a shared key (through Diffie-Hellman), they will use messages
// tagged with DEVICE_TO_DEVICE_MESSAGE to exchange data.
DEVICE_TO_DEVICE_MESSAGE = 13;
// Notification to let a device know it should contact a nearby device.
DEVICE_PROXIMITY_CALLBACK = 14;
// Device-to-device communications are protected by an unauthenticated
// Diffie-Hellman exchange. During device-to-device authentication, the first
// message from initiator (the challenge) is signed and put into the payload
// of the message sent back to the initiator.
UNLOCK_KEY_SIGNED_CHALLENGE = 15;
// Specialty (corp only) features
LOGIN_NOTIFICATION = 101;
}
message GcmMetadata {
required Type type = 1;
optional int32 version = 2 [default = 0];
}
message Tickle {
// Time after which this tickle should expire
optional fixed64 expiry_time = 1;
}
message LoginNotificationInfo {
// Time at which the server received the login notification request.
optional fixed64 creation_time = 2;
// Must correspond to user_id in LoginNotificationRequest, if set.
optional string email = 3;
// Host where the user's credentials were used to login, if meaningful.
optional string host = 4;
// Location from where the user's credentials were used, if meaningful.
optional string source = 5;
// Type of login, e.g. ssh, gnome-screensaver, or web.
optional string event_type = 6;
}
@@ -0,0 +1,126 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Proto definitions for SecureMessage format
syntax = "proto2";
package securemessage;
option optimize_for = LITE_RUNTIME;
option java_package = "com.google.security.cryptauth.lib.securemessage";
option java_outer_classname = "SecureMessageProto";
option objc_class_prefix = "SMSG";
message SecureMessage {
// Must contain a HeaderAndBody message
required bytes header_and_body = 1;
// Signature of header_and_body
required bytes signature = 2;
}
// Supported "signature" schemes (both symmetric key and public key based)
enum SigScheme {
HMAC_SHA256 = 1;
ECDSA_P256_SHA256 = 2;
// Not recommended -- use ECDSA_P256_SHA256 instead
RSA2048_SHA256 = 3;
}
// Supported encryption schemes
enum EncScheme {
// No encryption
NONE = 1;
AES_256_CBC = 2;
}
message Header {
required SigScheme signature_scheme = 1;
required EncScheme encryption_scheme = 2;
// Identifies the verification key
optional bytes verification_key_id = 3;
// Identifies the decryption key
optional bytes decryption_key_id = 4;
// Encryption may use an IV
optional bytes iv = 5;
// Arbitrary per-protocol public data, to be sent with the plain-text header
optional bytes public_metadata = 6;
// The length of some associated data this is not sent in this SecureMessage,
// but which will be bound to the signature.
optional uint32 associated_data_length = 7 [default = 0];
}
message HeaderAndBody {
// Public data about this message (to be bound in the signature)
required Header header = 1;
// Payload data
required bytes body = 2;
}
// Must be kept wire-format compatible with HeaderAndBody. Provides the
// SecureMessage code with a consistent wire-format representation that
// remains stable irrespective of protobuf implementation choices. This
// low-level representation of a HeaderAndBody should not be used by
// any code outside of the SecureMessage library implementation/tests.
message HeaderAndBodyInternal {
// A raw (wire-format) byte encoding of a Header, suitable for hashing
required bytes header = 1;
// Payload data
required bytes body = 2;
}
// -------
// The remainder of the messages defined here are provided only for
// convenience. They are not needed for SecureMessage proper, but are
// commonly useful wherever SecureMessage might be applied.
// -------
// A list of supported public key types
enum PublicKeyType {
EC_P256 = 1;
RSA2048 = 2;
// 2048-bit MODP group 14, from RFC 3526
DH2048_MODP = 3;
}
// A convenience proto for encoding NIST P-256 elliptic curve public keys
message EcP256PublicKey {
// x and y are encoded in big-endian two's complement (slightly wasteful)
// Client MUST verify (x,y) is a valid point on NIST P256
required bytes x = 1;
required bytes y = 2;
}
// A convenience proto for encoding RSA public keys with small exponents
message SimpleRsaPublicKey {
// Encoded in big-endian two's complement
required bytes n = 1;
optional int32 e = 2 [default = 65537];
}
// A convenience proto for encoding Diffie-Hellman public keys,
// for use only when Elliptic Curve based key exchanges are not possible.
// (Note that the group parameters must be specified separately)
message DhPublicKey {
// Big-endian two's complement encoded group element
required bytes y = 1;
}
message GenericPublicKey {
required PublicKeyType type = 1;
optional EcP256PublicKey ec_p256_public_key = 2;
optional SimpleRsaPublicKey rsa2048_public_key = 3;
// Use only as a last resort
optional DhPublicKey dh2048_public_key = 4;
}
@@ -0,0 +1,105 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto2";
package securegcm;
option optimize_for = LITE_RUNTIME;
option java_package = "com.google.security.cryptauth.lib.securegcm";
option java_outer_classname = "UkeyProto";
message Ukey2Message {
enum Type {
UNKNOWN_DO_NOT_USE = 0;
ALERT = 1;
CLIENT_INIT = 2;
SERVER_INIT = 3;
CLIENT_FINISH = 4;
}
optional Type message_type = 1; // Identifies message type
optional bytes message_data = 2; // Actual message, to be parsed according to
// message_type
}
message Ukey2Alert {
enum AlertType {
// Framing errors
BAD_MESSAGE = 1; // The message could not be deserialized
BAD_MESSAGE_TYPE = 2; // message_type has an undefined value
INCORRECT_MESSAGE = 3; // message_type received does not correspond to
// expected type at this stage of the protocol
BAD_MESSAGE_DATA = 4; // Could not deserialize message_data as per
// value inmessage_type
// ClientInit and ServerInit errors
BAD_VERSION = 100; // version is invalid; server cannot find
// suitable version to speak with client.
BAD_RANDOM = 101; // Random data is missing or of incorrect
// length
BAD_HANDSHAKE_CIPHER = 102; // No suitable handshake ciphers were found
BAD_NEXT_PROTOCOL = 103; // The next protocol is missing, unknown, or
// unsupported
BAD_PUBLIC_KEY = 104; // The public key could not be parsed
// Other errors
INTERNAL_ERROR = 200; // An internal error has occurred. error_message
// may contain additional details for logging
// and debugging.
}
optional AlertType type = 1;
optional string error_message = 2;
}
enum Ukey2HandshakeCipher {
RESERVED = 0;
P256_SHA512 = 100; // NIST P-256 used for ECDH, SHA512 used for
// commitment
CURVE25519_SHA512 = 200; // Curve 25519 used for ECDH, SHA512 used for
// commitment
}
message Ukey2ClientInit {
optional int32 version = 1; // highest supported version for rollback
// protection
optional bytes random = 2; // random bytes for replay/reuse protection
// One commitment (hash of ClientFinished containing public key) per supported
// cipher
message CipherCommitment {
optional Ukey2HandshakeCipher handshake_cipher = 1;
optional bytes commitment = 2;
}
repeated CipherCommitment cipher_commitments = 3;
// Next protocol that the client wants to speak.
optional string next_protocol = 4;
}
message Ukey2ServerInit {
optional int32 version = 1; // highest supported version for rollback
// protection
optional bytes random = 2; // random bytes for replay/reuse protection
// Selected Cipher and corresponding public key
optional Ukey2HandshakeCipher handshake_cipher = 3;
optional bytes public_key = 4;
}
message Ukey2ClientFinished {
optional bytes public_key = 1; // public key matching selected handshake
// cipher
}
@@ -0,0 +1,236 @@
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Brought from: //depot/google3/location/nearby/sharing/proto/wire_format.proto
// At CL 317565061
syntax = "proto2";
package sharing.nearby;
// Required in Chrome.
option optimize_for = LITE_RUNTIME;
// File metadata. Does not include the actual bytes of the file.
// NEXT_ID=6
message FileMetadata {
enum Type {
UNKNOWN = 0;
IMAGE = 1;
VIDEO = 2;
APP = 3;
AUDIO = 4;
}
// The human readable name of this file (eg. 'Cookbook.pdf').
optional string name = 1;
// The type of file (eg. 'IMAGE' from 'dog.jpg'). Specifying a type helps
// provide a richer experience on the receiving side.
optional Type type = 2 [default = UNKNOWN];
// The FILE payload id that will be sent as a follow up containing the actual
// bytes of the file.
optional int64 payload_id = 3;
// The total size of the file.
optional int64 size = 4;
// The mimeType of file (eg. 'image/jpeg' from 'dog.jpg'). Specifying a
// mimeType helps provide a richer experience on receiving side.
optional string mime_type = 5 [default = "application/octet-stream"];
// A uuid for the attachment. Should be unique across all attachments.
optional int64 id = 6;
}
// NEXT_ID=5
message TextMetadata {
enum Type {
UNKNOWN = 0;
TEXT = 1;
// Open with browsers.
URL = 2;
// Open with map apps.
ADDRESS = 3;
// Dial.
PHONE_NUMBER = 4;
}
// The title of the text content.
optional string text_title = 2;
// The type of text (phone number, url, address, or plain text).
optional Type type = 3 [default = UNKNOWN];
// The BYTE payload id that will be sent as a follow up containing the actual
// bytes of the text.
optional int64 payload_id = 4;
// The size of the text content.
optional int64 size = 5;
// A uuid for the attachment. Should be unique across all attachments.
optional int64 id = 6;
}
// NEXT_ID=5
message WifiCredentialsMetadata {
enum SecurityType {
UNKNOWN_SECURITY_TYPE = 0;
OPEN = 1;
WPA_PSK = 2;
WEP = 3;
}
// The Wifi network name. This will be sent in introduction.
optional string ssid = 2;
// The security type of network (OPEN, WPA_PSK, WEP).
optional SecurityType security_type = 3 [default = UNKNOWN_SECURITY_TYPE];
// The BYTE payload id that will be sent as a follow up containing the
// password.
optional int64 payload_id = 4;
// A uuid for the attachment. Should be unique across all attachments.
optional int64 id = 5;
}
// A frame used when sending messages over the wire.
// NEXT_ID=3
message Frame {
enum Version {
UNKNOWN_VERSION = 0;
V1 = 1;
}
optional Version version = 1;
// Right now there's only 1 version, but if there are more, exactly one of
// the following fields will be set.
optional V1Frame v1 = 2;
}
// NEXT_ID=7
message V1Frame {
enum FrameType {
UNKNOWN_FRAME_TYPE = 0;
INTRODUCTION = 1;
RESPONSE = 2;
PAIRED_KEY_ENCRYPTION = 3;
PAIRED_KEY_RESULT = 4;
CERTIFICATE_INFO = 5;
CANCEL = 6;
}
optional FrameType type = 1;
// Exactly one of the following fields will be set.
optional IntroductionFrame introduction = 2;
optional ConnectionResponseFrame connection_response = 3;
optional PairedKeyEncryptionFrame paired_key_encryption = 4;
optional PairedKeyResultFrame paired_key_result = 5;
optional CertificateInfoFrame certificate_info = 6;
}
// An introduction packet sent by the sending side. Contains a list of files
// they'd like to share.
// NEXT_ID=4
message IntroductionFrame {
repeated FileMetadata file_metadata = 1;
repeated TextMetadata text_metadata = 2;
// The required app package to open the content. May be null.
optional string required_package = 3;
repeated WifiCredentialsMetadata wifi_credentials_metadata = 4;
}
// A response packet sent by the receiving side. Accepts or rejects the list of
// files.
// NEXT_ID=2
message ConnectionResponseFrame {
enum Status {
UNKNOWN = 0;
ACCEPT = 1;
REJECT = 2;
NOT_ENOUGH_SPACE = 3;
UNSUPPORTED_ATTACHMENT_TYPE = 4;
TIMED_OUT = 5;
}
// The receiving side's response.
optional Status status = 1;
}
// A paired key encryption packet sent between devices, contains signed data.
// NEXT_ID=3
message PairedKeyEncryptionFrame {
// The encrypted data in byte array format.
optional bytes signed_data = 1;
// The hash of a certificate id.
optional bytes secret_id_hash = 2;
// An optional encrypted data in byte array format.
optional bytes optional_signed_data = 3;
}
// A paired key verification result packet sent between devices.
// NEXT_ID=2
message PairedKeyResultFrame {
enum Status {
UNKNOWN = 0;
SUCCESS = 1;
FAIL = 2;
UNABLE = 3;
}
// The verification result.
optional Status status = 1;
}
// A package containing certificate info to be shared to remote device offline.
// NEXT_ID=2
message CertificateInfoFrame {
// The public certificates to be shared with remote devices.
repeated PublicCertificate public_certificate = 1;
}
// A public certificate from the local device.
// NEXT_ID=8
message PublicCertificate {
// The unique id of the public certificate.
optional bytes secret_id = 1;
// A bytes representation of a Secret Key owned by contact, to decrypt the
// metadata_key stored within the advertisement.
optional bytes authenticity_key = 2;
// A bytes representation a public key of X509Certificate, owned by contact,
// to decrypt encrypted UKEY2 (from Nearby Connections API) as a hand shake in
// contact verification phase.
optional bytes public_key = 3;
// The time in millis from epoch when this certificate becomes effective.
optional int64 start_time = 4;
// The time in millis from epoch when this certificate expires.
optional int64 end_time = 5;
// The encrypted metadata in bytes, contains personal information of the
// device/user who created this certificate. Needs to be decrypted into bytes,
// and converted back to EncryptedMetadata object to access fields.
optional bytes encrypted_metadata_bytes = 6;
// The tag for verifying metadata_encryption_key.
optional bytes metadata_encryption_key_tag = 7;
}
// NEXT_ID=3
message WifiCredentials {
// Wi-Fi password.
optional string password = 1;
// True if the network is a hidden network that is not broadcasting its SSID.
// Default is false.
optional bool hidden_ssid = 2 [default = false];
}
@@ -0,0 +1,41 @@
//
// ConnectionFailureTracker.swift
// QuickDrop
//
// Created by Leon Böttger on 15.05.25.
//
import Foundation
class ConnectionFailureTracker {
static let shared = ConnectionFailureTracker()
private var failureTimestamps: [Date] = []
private var lastAlertShownAt: Date?
private let queue = DispatchQueue(label: "ConnectionFailureTrackerQueue")
private init() {}
func recordFailure(onTrigger: @escaping () -> Void) {
queue.async {
let now = Date()
// Remove old timestamps older than 10 seconds
self.failureTimestamps = self.failureTimestamps.filter { now.timeIntervalSince($0) < 10 }
self.failureTimestamps.append(now)
// Check if alert can be shown
let canShowAlert = self.failureTimestamps.count >= 2 &&
(self.lastAlertShownAt == nil || now.timeIntervalSince(self.lastAlertShownAt!) >= 60)
if canShowAlert {
self.lastAlertShownAt = now
self.failureTimestamps.removeAll()
DispatchQueue.main.async {
onTrigger()
}
}
}
}
}
+100
View File
@@ -0,0 +1,100 @@
//
// Helpers.swift
// QuickDrop
//
// Created by Leon Böttger on 04.04.25.
//
import Foundation
extension Data {
func urlSafeBase64EncodedString() -> String {
return String(base64EncodedString().replacingOccurrences(of: "=", with: "").map {
if $0 == "/" {
return "_"
} else if $0 == "+" {
return "-"
} else {
return $0
}
})
}
static func randomData(length: Int) -> Data {
var data = Data(count: length)
data.withUnsafeMutableBytes {
guard SecRandomCopyBytes(kSecRandomDefault, length, $0.baseAddress!) == 0 else { fatalError() }
}
return data
}
static func dataFromUrlSafeBase64(_ str: String) -> Data? {
var regularB64 = String(str.map {
if $0 == "_" {
return "/"
} else if $0 == "-" {
return "+"
} else {
return $0
}
})
while (regularB64.count % 4) != 0 {
regularB64 = regularB64 + "="
}
return Data(base64Encoded: regularB64, options: .ignoreUnknownCharacters)
}
var hex: String {
return map { String(format: "%02hhx", $0) }.joined()
}
}
extension String {
var dataFromHex: Data {
var data = Data(capacity: count / 2)
var index = startIndex
while index < endIndex {
let nextIndex = self.index(index, offsetBy: 2)
if nextIndex <= endIndex,
let byte = UInt8(self[index ..< nextIndex], radix: 16)
{
data.append(byte)
} else {
break // or handle invalid hex gracefully
}
index = nextIndex
}
return data
}
}
public enum UserDefaultsKeys: String, CaseIterable {
case isEligibleForIap = "isEligibleForIap"
case appLaunchedBefore = "ShowedWelcomeScreen"
case plusVersion = "isPlusVersion"
case transmissionCount = "reviewRequestCountKey"
case automaticallyAcceptFiles = "automaticallyAcceptFiles"
case saveFolderBookmark = "saveFolderBookmark"
case openFinderAfterReceiving = "openFinderAfterReceiving"
case endpointID = "endpointID"
}
public func isPlusVersion() -> Bool {
return UserDefaults.standard.bool(forKey: UserDefaultsKeys.plusVersion.rawValue)
}
public func isFileTransferRestricted() -> Bool {
(!isPlusVersion() && transmissionCount() > 1)
}
public func transmissionCount() -> Int {
return UserDefaults.standard.integer(forKey: UserDefaultsKeys.transmissionCount.rawValue)
}
+133
View File
@@ -0,0 +1,133 @@
//
// Logger.swift
// QuickDrop
//
// Created by Leon Böttger on 26.02.25.
//
import Foundation
import os
fileprivate let loggerQueue = DispatchQueue(label: "loggerQueue", qos: .utility)
/// Manages logging functionality
final class LogManager {
/// The URL of the log file
let logFileURL: URL?
/// Shared instance
public static let sharedInstance = LogManager()
@Published var loggingEnabled = true
/// Private initializer
private init() {
// Use the App Group container
let directory = getAppGroupDirectory()
// Create file name from date
let formatter = DateFormatter()
formatter.dateFormat = "dd-MM-yyyy"
let dateString = formatter.string(from: Date())
let fileName = "\(dateString).log"
// Full path in app group container
self.logFileURL = directory?.appendingPathComponent(fileName)
}
/// Writes a specified string to the log file.
func writeToFile(string: String) {
guard let logFileURL = logFileURL else { return }
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
let timestamp = formatter.string(from: Date())
guard let data = (timestamp + " - " + string + "\n").data(using: .utf8) else { return }
loggerQueue.async {
let fileManager = FileManager.default
if !fileManager.fileExists(atPath: logFileURL.path) {
// Clean up older logs
self.deleteLogs()
// Create file atomically
try? data.write(to: logFileURL, options: .atomicWrite)
return
}
// Atomic append with file locking using FileHandle
do {
let fileHandle = try FileHandle(forWritingTo: logFileURL)
defer { try? fileHandle.close() }
// Locking is handled by the serial dispatch queue
try fileHandle.seekToEnd()
try fileHandle.write(contentsOf: data)
} catch {
print("[Logger] Failed to write to log file: \(error.localizedDescription)")
}
}
}
/// Gets the contents of the log file.
func getLogString() -> String {
guard let logFileURL = logFileURL else { return "" }
do {
return try String(contentsOf: logFileURL, encoding: .utf8)
} catch {
log("[LUI] Error reading file: \(error.localizedDescription)")
return ""
}
}
/// Deletes all log files in the shared container.
func deleteLogs() {
log("[LUI] Deleting all logs...")
guard let directory = getAppGroupDirectory() else { return }
let fileManager = FileManager.default
guard let allFiles = try? fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil) else {
return
}
let logFiles = allFiles.filter { $0.pathExtension == "log" }
for file in logFiles {
do {
log("[LUI] Removing \(file.lastPathComponent)")
try fileManager.removeItem(at: file)
} catch {
print("[Logger] Failed to delete file: \(error.localizedDescription)")
}
}
}
}
/// Logs a specified string
public func log(_ text: String) {
let logger = LogManager.sharedInstance
if logger.loggingEnabled {
loggerQueue.async {
print(text)
#if !os(tvOS)
let isAppExtension = Bundle.main.bundlePath.hasSuffix(".appex")
let prefix = isAppExtension ? "[EXT]" : "[APP]"
logger.writeToFile(string: "\(prefix) \(text)")
#endif
}
}
}
/// Returns the shared App Group directory
fileprivate func getAppGroupDirectory() -> URL? {
let groupIdentifier = "group.com.leonboettger.neardrop"
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupIdentifier)
}
@@ -0,0 +1,220 @@
//
// SaveFilesManager.swift
// QuickDrop
//
// Created by Leon Böttger on 26.07.25.
//
import Foundation
import AppKit
import ImageIO
public class SaveFilesManager {
private init() {
// remove old temp directory
let tempPath = FileManager.default.temporaryDirectory
let fileManager = FileManager.default
do {
let contents = try fileManager.contentsOfDirectory(at: tempPath, includingPropertiesForKeys: nil)
var didSomething = false
for item in contents {
didSomething = true
try fileManager.removeItem(at: item)
}
if didSomething {
log("Temporary directory cleared.")
}
} catch {
log("Failed to list contents of temp directory: \(error)")
}
}
public static let shared = SaveFilesManager()
let tempDirectory: URL = FileManager.default.temporaryDirectory.appendingPathComponent("Pending")
private var securityScopeUrl: URL?
private var filesFinishedDownloading = [URL]()
private var filesFinishedDownloadingSinceLastRun = [URL]()
public func registerFileFinishedDownloading(_ fileURL: URL) {
filesFinishedDownloading.append(fileURL)
filesFinishedDownloadingSinceLastRun.append(fileURL)
// read out EXIF timestamp and manually set file creation date to it
applyEXIFTimestamps(at: fileURL)
}
public func movePendingFilesToTarget() {
if isPlusVersion() {
do {
let fileManager = FileManager.default
let files = try fileManager.contentsOfDirectory(at: tempDirectory, includingPropertiesForKeys: nil)
let target = getSaveDirectory()
var loggedExecution = false
for file in files {
if !loggedExecution {
log("Moving pending files to target directory")
loggedExecution = true
}
let fileName = file.lastPathComponent
let destinationURL = target.appendingPathComponent(fileName)
if !filesFinishedDownloading.contains(destinationURL) {
log("File \(file) not finished downloading, skipping")
continue
}
log("Moving file: \(file.lastPathComponent) to \(destinationURL.lastPathComponent)")
do {
try fileManager.copyItem(at: file, to: destinationURL)
let progress = Progress()
progress.fileURL = destinationURL
progress.totalUnitCount = 10
progress.kind = .file
progress.isPausable = false
progress.publish()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
progress.completedUnitCount = 10
progress.unpublish()
}
try fileManager.removeItem(at: file)
}
catch {
log("Error moving file: \(error)")
}
}
log("Moved all pending files to target directory")
}
catch {
// Pending directory doesn't exist or is empty
}
}
if !filesFinishedDownloadingSinceLastRun.isEmpty && !isFileTransferRestricted() {
if UserDefaults.standard.bool(forKey: UserDefaultsKeys.openFinderAfterReceiving.rawValue) {
log("Opening \(filesFinishedDownloadingSinceLastRun.count) file(s) in Finder.")
NSWorkspace.shared.activateFileViewerSelecting(filesFinishedDownloadingSinceLastRun)
}
// Clear the list of finished files
filesFinishedDownloadingSinceLastRun.removeAll()
}
stopAccessingSecurityScopedResource()
}
public func stopAccessingSecurityScopedResource() {
guard let url = securityScopeUrl else {
return
}
log("Stopping access to security scoped resource: \(url)")
url.stopAccessingSecurityScopedResource()
securityScopeUrl = nil
}
public func getSaveDirectory() -> URL {
if let securityScopeUrl = securityScopeUrl {
log("Using existing security scope URL: \(securityScopeUrl)")
return securityScopeUrl
}
if let bookmarkData = UserDefaults.standard.data(forKey: UserDefaultsKeys.saveFolderBookmark.rawValue) {
var isStale = false
do {
let url = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
if !isStale {
if url.startAccessingSecurityScopedResource() {
log("Successfully accessed security scoped resource: \(url)")
securityScopeUrl = url
return url
}
} else {
log("Bookmark is stale, using default downloads folder.")
}
} catch {
log("Failed to resolve bookmark: \(error), using default downloads folder.")
}
}
do {
return try FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: true).resolvingSymlinksInPath()
} catch {
fatalError("Failed to get downloads directory: \(error)")
}
}
/// Returns the EXIF original date of an image file, adjusted to the current time zone.
private func exifOriginalDate(from url: URL) -> Date? {
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
let exif = properties[kCGImagePropertyExifDictionary] as? [CFString: Any],
let dateString = exif[kCGImagePropertyExifDateTimeOriginal] as? String
else {
return nil
}
let formatter = DateFormatter()
formatter.dateFormat = "yyyy:MM:dd HH:mm:ss"
formatter.timeZone = TimeZone.current // Use the current device time zone
return formatter.date(from: dateString)
}
/// Applies EXIF timestamps to a given file or directory.
private func applyEXIFTimestamps(at url: URL) {
let fm = FileManager.default
var isDir: ObjCBool = false
guard fm.fileExists(atPath: url.path, isDirectory: &isDir) else { return }
if isDir.boolValue {
// It's a directory — enumerate files
if let enumerator = fm.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey]) {
for case let fileURL as URL in enumerator {
guard (try? fileURL.resourceValues(forKeys: [.isRegularFileKey]))?.isRegularFile == true else { continue }
applyEXIFTimestamp(to: fileURL)
}
}
} else {
// It's a single file
applyEXIFTimestamp(to: url)
}
}
/// Applies EXIF timestamp (if available) to a single file
private func applyEXIFTimestamp(to fileURL: URL) {
guard let exifDate = exifOriginalDate(from: fileURL) else {
return
}
// Set the "creation date"
try? FileManager.default.setAttributes([.creationDate: exifDate], ofItemAtPath: fileURL.path)
}
}
@@ -0,0 +1,59 @@
//
// SleepManager.swift
// QuickDrop
//
// Created by Leon Böttger on 03.05.25.
//
import Foundation
import IOKit
import IOKit.pwr_mgt
class SleepManager {
public static let shared = SleepManager()
private var assertionID: IOPMAssertionID = 0
private var timer: Timer?
private init() {
// Start periodic check
timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
self?.updateSleepState()
}
RunLoop.main.add(timer!, forMode: .common)
}
private func disableSleep() {
if assertionID != 0 {
return
}
log("[SleepManager] Enabling Wakelock")
IOPMAssertionCreateWithName(
kIOPMAssertionTypePreventUserIdleDisplaySleep as CFString,
IOPMAssertionLevel(kIOPMAssertionLevelOn),
"QuickDrop Data Transfer" as CFString,
&assertionID
)
}
private func enableSleep() {
if assertionID == 0 {
return
}
log("[SleepManager] Disabling Wakelock")
IOPMAssertionRelease(assertionID)
assertionID = 0
}
private func updateSleepState() {
let activeConnections = NearbyConnectionManager.shared.getActiveConnectionsCount()
if activeConnections > 0 {
disableSleep()
} else {
enableSleep()
}
}
}
File diff suppressed because it is too large. Load diff
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1620"
LastUpgradeVersion = "1640"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1620"
LastUpgradeVersion = "1640"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
+35 -113
View File
@@ -8,7 +8,6 @@
import AudioToolbox
import BezelNotification
import Cocoa
import NearbyShare
import Network
import StoreKit
import SwiftUI
@@ -23,13 +22,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
var welcomeWindow: NSWindow?
var plusWindow: NSWindow?
private var isAlertShown = false
private var sheetView: NSPanel? = nil
private var sheetAttachedWindow: NSWindow? = nil
var firewallAlertWindow: NSWindow?
var apIsolationAlertWindow: NSWindow?
var networkFilterAlertWindow: NSWindow?
private var errorAlertHandler = ErrorAlertHandler.shared
private var iapManager: IAPManager?
var showsFirewallAlert = false
@@ -50,7 +46,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
self.visibleItem = visibleItem
menu.addItem(visibleItem)
menu.addItem(withTitle: String(format: NSLocalizedString("DeviceName", value: "Device name: %@", comment: ""), arguments: [Host.current().localizedName ?? "Mac"]), action: nil, keyEquivalent: "")
menu.addItem(withTitle: String(format: "DeviceName".localized(), arguments: [Host.current().localizedName ?? "Mac"]), action: nil, keyEquivalent: "")
menu.addItem(NSMenuItem.separator())
@@ -59,10 +55,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
menu.addItem(NSMenuItem.separator())
let userManualItem = NSMenuItem(title: NSLocalizedString("UserManual", value: "User Manual", comment: ""), action: #selector(openWelcomeScreen), keyEquivalent: "")
let userManualItem = NSMenuItem(title: "UserManual".localized(), action: #selector(openWelcomeScreen), keyEquivalent: "")
menu.addItem(userManualItem)
menu.addItem(withTitle: NSLocalizedString("Quit", value: "Quit QuickDrop", comment: ""), action: #selector(NSApplication.terminate(_:)), keyEquivalent: "")
menu.addItem(withTitle: "Quit".localized(), action: #selector(NSApplication.terminate(_:)), keyEquivalent: "")
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem?.button?.image = NSImage(named: "MenuBarIcon")
@@ -146,6 +142,22 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
iapManager?.stopObserving()
}
func application(_ application: NSApplication, open urls: [URL]) {
for url in urls {
if url.scheme == "quickdrop", url.host == "sendLog" {
sendLoggingString() // this time it runs in the main app
}
if url.scheme == "quickdrop", url.host == "openLog", let url = LogManager.sharedInstance.logFileURL {
log("Opening log file: \(url)")
// open folder containing the log file
NSWorkspace.shared.activateFileViewerSelecting([url])
}
}
}
func applicationSupportsSecureRestorableState(_: NSApplication) -> Bool {
@@ -275,52 +287,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
// MARK: - Alerts and Notifications
enum AlertType: String {
case ApIsolation
case NetworkFilter
case Firewall
}
func openAlert(type: AlertType) {
log("Opening Alert for \(type)")
DispatchQueue.main.async { [self] in
// Create an NSWindow to host the SwiftUI view
let alertWindow = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: issueViewWidth, height: issueViewHeight),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
switch type {
case .ApIsolation:
apIsolationAlertWindow = alertWindow
alertWindow.contentView = NSHostingView(rootView: ApIsolationIssueView(closeView: { self.apIsolationAlertWindow?.close() }))
case .NetworkFilter:
networkFilterAlertWindow = alertWindow
alertWindow.contentView = NSHostingView(rootView: NetworkFilterIssueView())
case .Firewall:
firewallAlertWindow = alertWindow
alertWindow.contentView = NSHostingView(rootView: FirewallIssueView())
}
alertWindow.title = "QuickDrop"
alertWindow.center()
alertWindow.isReleasedWhenClosed = false
alertWindow.setFrameAutosaveName(type.rawValue)
NSApp.activate(ignoringOtherApps: true)
alertWindow.makeKeyAndOrderFront(nil)
alertWindow.level = .normal
}
}
func showFirewallAlert() {
openAlert(type: .Firewall)
ErrorAlertHandler.shared.openAlert(type: .Firewall)
}
@@ -335,12 +304,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
DispatchQueue.main.async {
AudioManager.playErrorSound()
NSApp.activate(ignoringOtherApps: true)
let alert = NSAlert()
alert.alertStyle = .critical
alert.messageText = String(format: NSLocalizedString("TransferError", value: "Failed to receive files from %@", comment: ""), arguments: [device?.name ?? "??"])
alert.messageText = String(format: "TransferError".localized(), arguments: [device?.name ?? "??"])
alert.informativeText = "UnsupportedFileType".localized()
alert.addButton(withTitle: "InformDeveloper".localized())
@@ -368,7 +339,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
activeIncomingTransfers[transfer.id] = TransferInfo(device: device, transfer: transfer)
AudioManager.playSound()
AudioManager.playIncomingFileSound()
let acceptAutomatically = UserDefaults.standard.bool(forKey: UserDefaultsKeys.automaticallyAcceptFiles.rawValue)
@@ -379,7 +350,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
} else if transfer.files.count == 1 {
fileStr = transfer.files[0].name
} else {
fileStr = String.localizedStringWithFormat(NSLocalizedString("NFiles", value: "%d files", comment: ""), transfer.files.count)
fileStr = String.localizedStringWithFormat("NFiles".localized(), transfer.files.count)
}
@@ -397,7 +368,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
}
let pinCodeMessage = String(format: NSLocalizedString("PinCode", value: "PIN: %@", comment: ""), arguments: [transfer.pinCode ?? "?"])
let pinCodeMessage = String(format: "PinCode".localized(), arguments: [transfer.pinCode ?? "?"])
let transferID = transfer.id
if acceptAutomatically {
@@ -427,8 +398,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
alert.messageText = "QuickDrop - \(pinCodeMessage)"
alert.informativeText = mainMessage
alert.addButton(withTitle: NSLocalizedString("Accept", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Decline", comment: ""))
alert.addButton(withTitle: "Accept".localized())
alert.addButton(withTitle: "Decline".localized())
let result = alert.runModal()
@@ -454,67 +425,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
func incomingTransfer(id: String, didFinishWith error: Error?) {
guard let transfer = activeIncomingTransfers[id] else { return }
if let error = error {
NSApp.activate(ignoringOtherApps: true)
var description = ""
if let ne = (error as? NearbyError) {
switch ne {
case .inputOutput:
description = "I/O Error"
case .protocolError(errorMessage: let errorMessage):
log("Protocol error ")
description = NSLocalizedString("Error.Protocol", value: "Communication error", comment: "") + " (\(errorMessage))"
case .packetFilterError:
openAlert(type: .NetworkFilter)
plusWindow?.close()
return
case .requiredFieldMissing(errorMessage: let errorMessage):
description = NSLocalizedString("Error.Protocol", value: "Communication error", comment: "") + "(\(errorMessage))"
case .ukey2:
description = NSLocalizedString("Error.Crypto", value: "Encryption error", comment: "") + "(\(ne.localizedDescription))"
case .canceled(reason: _):
break // can't happen for incoming transfers
}
} else {
description = error.localizedDescription
}
let alert = NSAlert()
alert.alertStyle = .critical
alert.messageText = String(format: NSLocalizedString("TransferError", value: "Failed to receive files from %@", comment: ""), arguments: [transfer.device.name])
alert.informativeText = description
alert.addButton(withTitle: "InformDeveloper".localized())
alert.addButton(withTitle: "CloseAlert".localized())
if let plusWindow = plusWindow {
log("Closing plus screen because of error")
plusWindow.close()
}
// Prevent multiple alerts at the same time
if self.isAlertShown {
log("Skipping alert for error \(error.localizedDescription) because one is already shown")
return
}
else {
self.isAlertShown = true
log("Showing alert with message: \"\(alert.messageText)\" and description: \"\(alert.informativeText)\"")
log("Already successful transmissions: \(transmissionCount())")
let result = alert.runModal()
self.isAlertShown = false
if result == .alertFirstButtonReturn {
sendLoggingString()
}
}
ErrorAlertHandler.shared.showErrorAlert(for: activeIncomingTransfers[id]?.device.name ?? "Android", error: error)
} else {
let currentCount = transmissionCount()
if currentCount % 20 == 0 {
if currentCount == 0 {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
SKStoreReviewController.requestReview()
}
@@ -535,10 +457,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
scanner.scan { allowed in
if allowed {
log("✅ Device-to-device likely allowed (peer responded on LAN).")
self.apIsolationAlertWindow?.close()
ErrorAlertHandler.shared.closeApIsolationAlert()
} else {
log("❌ Device-to-device check failed. Informing user...")
self.openAlert(type: .ApIsolation)
ErrorAlertHandler.shared.openAlert(type: .ApIsolation)
}
}
}
-26
View File
@@ -1,26 +0,0 @@
//
// AudioManager.swift
// QuickDrop
//
// Created by Leon Böttger on 01.03.25.
//
import AVFoundation
class AudioManager {
static var audioPlayer: AVAudioPlayer?
static func playSound() {
guard let soundURL = Bundle.main.url(forResource: "timeIsNow", withExtension: "mp3") else {
log("Sound file not found")
return
}
do {
audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
audioPlayer?.play()
} catch {
log("Error playing sound: \(error.localizedDescription)")
}
}
}
-202
View File
@@ -1,202 +0,0 @@
/* No comment provided by engineer. */
"Accept" = "Accept";
/* No comment provided by engineer. */
"Decline" = "Decline";
/* No comment provided by engineer. */
"DeviceName" = "Device name: %@";
/* No comment provided by engineer. */
"DeviceSendingFiles" = "%1$@ is sending you %2$@";
"DeviceCurrentlySendingText" = "%1$@ inserted \"%2$@\" to your clipboard.";
"DeviceSendingText" = "%1$@ wants to insert \"%2$@\" to your Mac's clipboard.";
"DeviceCurrentlySendingUrl" = "%1$@ opened \"%2$@\".";
"DeviceSendingUrl" = "%1$@ wants to open \"%2$@\" on your Mac.";
"InsertedIntoClipboard" = "Inserted";
"UnsupportedFileType" = "The file you tried to send is currently not supported. Please send an error report.";
/* No comment provided by engineer. */
"Error.Crypto" = "Encryption error";
/* No comment provided by engineer. */
"Error.Protocol" = "Communication error";
/* No comment provided by engineer. */
"NFiles" = "%d files";
/* No comment provided by engineer. */
"NotificationsDenied.Message" = "QuickDrop needs to be able to display notifications for incoming file transfers. Please allow notifications in System Settings.";
/* No comment provided by engineer. */
"NotificationsDenied.OpenSettings" = "Open settings";
/* No comment provided by engineer. */
"NotificationsDenied.Title" = "Notification Permission Required";
/* No comment provided by engineer. */
"PinCode" = "PIN: %@";
/* No comment provided by engineer. */
"Quit" = "Close QuickDrop";
/* No comment provided by engineer. */
"TransferError" = "Failed to receive files from %@";
/* No comment provided by engineer. */
"VisibleToEveryone" = "Visible to everyone";
"NoNetworkConnection" = "No Network Connection";
"WelcomeToQuickDrop" = "Welcome to QuickDrop";
"Acknowledgements" = "Acknowledgements";
"SupportQuickDrop" = "Support QuickDrop";
"RecommendedApps" = "Recommended Apps";
"GetSupport" = "Support";
"ReadyToReceive" = "Ready to receive";
"PrivacyPolicy" = "Privacy Policy";
"SendClipboard" = "Send Clipboard";
"Clipboard" = "Clipboard";
"DeviceCurrentlySendingFiles" = "%1$@ is sending you %2$@";
"UserManual" = "User Manual & Settings";
"Settings" = "Settings";
"AutomaticallyAcceptFiles" = "Automatically Accept Incoming Files";
"AutomaticallyAcceptFilesFooter" = "Enabling this setting is only recommended in your private network. Anyone on the same network can send files to your device without your consent.";
"SaveFilesTo" = "Save Incoming Files To:";
"SaveFilesLocation" = "Incomping Files Location";
"SaveFilesToButton" = "Select…";
"DownloadsFolder" = "Downloads";
"OpenFinderAfterReceiving" = "Show Files in Finder After Receiving";
"SendFiles" = "Send Files";
"ReceiveFiles" = "Receive Files";
"DeviceNotShown" = "Device Not Shown?";
"Troubleshooting" = "Troubleshooting";
"AndroidApp" = "Mobile App";
"AndroidAppDescription" = "QuickDrop is now also available as a separate app for your smartphone! Even though QuickDrop can also be used without a separate smartphone app, the app makes file transfer even easier and quicker. With it, you can even transfer files when you are not connected to your home's Wi-Fi network, by connecting the Mac to your smartphone's hotspot. The app still provides the same end-to-end encryption when transferring files.
The mobile app is currently in beta. To register for the beta, please send an email to quickdrop-beta@leonboettger.com or use the button below. Thanks for your interest!";
"JoinBeta" = "Join the Beta";
"BetaEmailBottom" = "Hi there,
please add me to the QuickDrop for Android beta.
–––––––––––––––
Please provide the email address of your Google Account below, so you can be invited to the beta. Afterward, you will receive an email with a link to download the app from the Google Play Store.
Your email address will only be used for this purpose and will not be stored or shared.
Your email address: ";
"SendFilesDescription" = "To send a file to your Android device, click on the Share icon or right-click on the file and select \"Share\". Afterward, select \"QuickDrop\" from the list of apps and follow the instructions.
If you don't see QuickDrop in the list, click on \"Edit Extensions\", scroll down, and enable the toggle for QuickDrop.
You also can send the current content of your Mac's clipboard to your Android device by clicking on QuickDrop's menu bar icon and selecting \"Send Clipboard\".";
"UserManualDescription" = "When QuickDrop is running, it will appear in the menu bar at the top of your screen. Click the QuickDrop icon if you would like to stop receiving files.
To send files from your Android device to your Mac, tap on the share icon on your phone, and select \"Quick Share\". Your Mac will be listed as an available device. Files will be saved in your Downloads folder.
Important: Your Android device and your Mac need to be on the same WiFi network to transfer files.";
"TroubleshootingDescription" = "If your Mac does not show up on your Android device even though both devices are connected to the same Wi-Fi network, or the transmission failed, try the following:
1) Open System Settings, and go to \"Privacy & Security\" > \"Local Network\". Make sure that QuickDrop is enabled. If you use a third-party firewall or an antivirus program, please check the documentation of this program to allow incoming connections for QuickDrop.
2) Restart QuickDrop by clicking the menu bar icon, choose the last entry, and open the app again. Restart your Android device.
Note: Some networks do not allow devices to communicate with each other. In this case, you need to download the QuickDrop app for your phone, and connect your Mac to your phone's hotspot to transfer files. The link to the app can be found in the sidebar on the left.";
"plusview_title" = "Thanks for using QuickDrop!";
"plusview_description" = "Unlock QuickDrop+ to continue exchanging files without limits. QuickDrop+ is a one-time purchase – no subscription required. All current and future features are included.";
"plusview_success_title" = "Thanks for your purchase!";
"plusview_success_description" = "You can now continue receiving files without restrictions. Any files you tried to send were already saved on your device.";
"plusview_success_button" = "Got it!";
"plusview_unlock_label" = "Support QuickDrop";
"plusview_restorepurchase" = "Already bought?";
"plusview_restorefailed" = "There was a problem restoring your purchase";
"plusview_restorefailed_proceed" = "Okay";
"plusview_purchasefailed" = "Purchase failed";
"plusview_purchasefaileddescription" = "If this message appeared unexpectedly, please make sure that you are connected to the Internet and In-App-Purchases are enabled for your device and then try again.";
"plusview_nothingtorestore" = "There is nothing to restore!";
"plusview_productRequestFailed" = "Unable to fetch In-App Purchases at the moment.";
/* Share Extension */
"Connecting" = "Connecting...";
"NotEnoughSpace" = "Not enough disk space";
"Sending" = "Sending...";
"TransferCanceled" = "Canceled";
"TransferDeclined" = "Declined";
"TransferFinished" = "Transfer finished";
"TransferTimedOut" = "Timed out";
"UnsupportedType" = "Attachment type not supported";
"QrCodeInstructions" = "Scan the QR Code with your Android device to connect it to your Mac. Make sure your phone is connected to the same network.\n\nIf you do not want to scan the QR code for every file transfer, install the QuickDrop app (see above).";
"ConnectWithQuickDropApp" = "Connect with QuickDrop App";
"ConnectWithoutApp" = "Connect without App";
"QrCodeInstructionsApp" = "To connect your Mac to your Android device, open the QuickDrop app on your Android device. Make sure your phone is connected to the same network.\n\nTo install QuickDrop on your phone, scan the QR code or search for 'QuickDrop' on the Google Play Store.";
"QuickDropAndroidAppAdvertisement" = "QuickDrop is now also available as a separate app for your smartphone! The app provides additional features, such as the ability to transfer files when you are not connected to your home's Wi-Fi network, by connecting the Mac to your smartphone's hotspot.\n\nTo install QuickDrop on your phone, scan the QR code or search for 'QuickDrop' on the Google Play Store.";
"TypeNotSupported" = "Type not supported";
"TypeNotSupportedDescription" = "The selected file type is not supported. If you want to share it anyway, right-click the file, select \"Compress\" and then send the compressed file.";
"TypeNotSupportedButton" = "Okay";
"CloseAlert" = "Close";
"InformDeveloper" = "Send Bug Report";
"TimeoutTitle" = "Something went wrong";
"TimeoutDescription" = "QuickDrop could not connect to your phone. Please open the settings of your Android device and search for 'Quick Share'. Change the setting 'Who can share with you' to 'Everyone', and try again.";
"TimeoutButton" = "Okay";
"ApIsolationHeader" = "Your router might not allow device-to-device communication";
"ApIsolationDescription" = "QuickDrop has detected that your Wi-Fi router might block devices from talking to each other. To transfer files between your phone and your Mac, you’ll need to disable client isolation (also known as AP isolation or Wi-Fi isolation).
Here’s how to disable it:
1. Log into your router: Open a web browser and enter your router’s IP address (usually 192.168.0.1 or 192.168.1.1), or use the button on the bottom of this page. Log in with your admin username and password. (Check your router label or manual if you’re unsure.)
2. Find the setting: Look under sections like Wireless Settings, Advanced Settings, or Security. Look for options named “AP Isolation”, “Client Isolation”, “Wireless Isolation”, or “Intra-BSS Blocking”.
3. Disable it: Make sure this setting is turned off. Save your changes and reboot the router if necessary.
Once disabled, devices on your Wi-Fi (like your phone and Mac) will be able to discover and connect to each other. Please note: If you are on a guest network, this setting might not be available. In that case, use a different network, or download the QuickDrop app for your phone, and connect your Mac to your phone's hotspot to transfer files. Search for 'QuickDrop' on Google Play to download the app.";
"ApIsolationAction" = "Open Router Settings";
"ApIsolationFindRouterFailedHeader" = "Could not find router IP";
"ApIsolationFindRouterFailedDescription" = "QuickDrop was not able to determine the address of your router. Please refer to the user manual of your Wi-Fi router to obtain its address.";
"FirewallHeader" = "A firewall might block incoming file transfers";
"FirewallDescription" = "QuickDrop has detected that a firewall might prevent that your Android device can connect to your Mac. Firewalls are used to block incoming connections per default. For receiving files, it is required that incoming connections are allowed. This is because your phone tries to connect to your Mac – creating an incoming connection.
You most likely use an antivirus program on your Mac. You have two options:
1. Allow incoming connections for QuickDrop in your firewall settings. This procedure varies with every program, so you may have to ask the manufacturer of your firewall/antivirus software how this can be done.
2. Disable the firewall of your antivirus software, and use the Mac's inbuilt firewall instead. Apple's firewall is optimized for QuickDrop.
Here’s how to disable the firewall of your antivirus program: Open System Settings on your Mac, navigate to Network > VPN & Filter, and disable the content filter. Then, navigate to Network > Firewall and click the toggle to enable the Mac's firewall.
Once this is done, your phone will be able to connect to your Mac.";
"NetworkFilterHeader" = "A packet filter might prevent file transfer";
"NetworkFilterDescription" = "QuickDrop has detected that a program installed on your Mac modifies network traffic. As a result of that, the file you tried to send from your Android device could not be decrypted.
Your Mac may have a faulty packet filter enabled, which might be installed by your antivirus program (e.g., ESET). Open System Preferences > Network > VPN & Filter and disable the content filter.
Once this is done, you should be able to transfer files successfully.";
"TransmitUsingCable" = "Transfer With Cable";
"DownloadCableConnectionApp" = "Download iDroid";
"DownloadCableConnectionAppDescription" = "If you want to transfer files and browse the storage of your Android device via USB, you can now do so with the new iDroid app.\n\niDroid supports downloading and uploading files and whole folders using Drag & Drop.";
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFiles</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@files@</string>
<key>files</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d file</string>
<key>other</key>
<string>%d files</string>
</dict>
</dict>
</dict>
</plist>
-110
View File
@@ -1,110 +0,0 @@
//
// CableConnectionView.swift
// QuickDrop
//
// Created by Leon Böttger on 02.06.25.
//
//import SwiftUI
//
//struct CableConnectionView: View {
// let closeView: () -> Void
//
// @Environment(\.colorScheme) var colorScheme
//
// var body: some View {
//
// VStack {
//
// HStack {
//
// HStack(spacing: 5) {
//
// let imageSize: CGFloat = advertisesApp ? 20 : 25
//
// Image(.quickDropIcon)
// .resizable()
// .aspectRatio(contentMode: .fit)
// .frame(width: imageSize, height: imageSize)
//
// if advertisesApp {
// Text("QuickDrop")
// }
// }
// .padding(.leading, 18)
//
// if !advertisesApp {
// Spacer()
//
// HStack {
// Text("ConnectWithQuickDropApp")
// .padding(5)
// .padding(.horizontal, 4)
// .background(Capsule().fill(colorScheme == .light ? .white : .white.opacity(0.15)).opacity(connectWithApp ? 1 : 0))
//
// Text("ConnectWithoutApp")
// .padding(5)
// .padding(.horizontal, 4)
// .background(Capsule().fill(colorScheme == .light ? .white : .white.opacity(0.15)).opacity(connectWithApp ? 0 : 1))
// }
// .padding(.horizontal, 1)
// .padding(4)
// .background(Capsule().fill(Color.gray.opacity(colorScheme == .light ? 0.2 : 0.15)))
// .onTapGesture {
// withAnimation {
// connectWithApp.toggle()
// }
// }
// .animation(.smooth, value:connectWithApp)
// }
//
// Spacer()
//
// Button(action: {
// closeView()
// }, label: {
// Image(systemName: "xmark")
// .font(.system(size: 20))
// .foregroundColor(.gray)
// })
// .buttonStyle(PlainButtonStyle())
// }
// .padding(.trailing, 15)
// .padding(.leading, 10)
// .padding(.top, 15)
// .padding(.bottom, advertisesApp ? -10 : 0)
//
// Spacer()
//
// HStack {
// Spacer()
//
// Image(connectWithApp ? .qrApp : .QR)
// .resizable()
// .aspectRatio(1, contentMode: .fit)
// .frame(height: 160)
//
// Spacer()
//
//
// Text(getDescription().localized())
// .padding(.top, 5)
// .padding(.horizontal)
// .fixedSize(horizontal: false, vertical: true)
// .multilineTextAlignment(.center)
//
// Spacer()
// }
// .padding(.horizontal)
// .padding(.bottom)
//
// Spacer()
//
// }
// .frame(width: qrCodeViewSize.width, height: qrCodeViewSize.height)
// }
//}
//
//#Preview {
// QrCodeView(advertisesApp: false, closeView: {})
//}
+13 -2
View File
@@ -2,7 +2,18 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.leonboettger.neardrop</string>
<key>CFBundleURLSchemes</key>
<array>
<string>quickdrop</string>
</array>
</dict>
</array>
</dict>
</plist>
File diff suppressed because it is too large. Load diff
-1
View File
@@ -7,7 +7,6 @@
import SwiftUI
import StoreKit
import NearbyShare
let plusViewWidth: CGFloat = 440
let plusViewHeight: CGFloat = 200
+41
View File
@@ -0,0 +1,41 @@
//
// AudioManager.swift
// QuickDrop
//
// Created by Leon Böttger on 01.03.25.
//
import AVFoundation
class AudioManager {
static private var incomingFileSoundPlayer: AVAudioPlayer?
static private var errorSoundPlayer: AVAudioPlayer?
static func playIncomingFileSound() {
guard let soundURL = Bundle.main.url(forResource: "timeIsNow", withExtension: "mp3") else {
log("Sound file not found")
return
}
do {
incomingFileSoundPlayer = try AVAudioPlayer(contentsOf: soundURL)
incomingFileSoundPlayer?.play()
} catch {
log("Error playing sound: \(error.localizedDescription)")
}
}
static func playErrorSound() {
guard let soundURL = Bundle.main.url(forResource: "justMaybe", withExtension: "mp3") else {
log("Sound file not found")
return
}
do {
errorSoundPlayer = try AVAudioPlayer(contentsOf: soundURL)
errorSoundPlayer?.play()
} catch {
log("Error playing sound: \(error.localizedDescription)")
}
}
}
+200
View File
@@ -0,0 +1,200 @@
//
// ErrorAlertHandler.swift
// QuickDrop
//
// Created by Leon Böttger on 25.07.25.
//
import AudioToolbox
import BezelNotification
import Cocoa
import Network
import StoreKit
import SwiftUI
import UserNotifications
class ErrorAlertHandler {
private init() {}
static let shared = ErrorAlertHandler()
private var isAlertShown = false
private var firewallAlertWindow: NSWindow?
private var apIsolationAlertWindow: NSWindow?
private var networkFilterAlertWindow: NSWindow?
func showErrorAlert(for deviceName: String, error: Error) {
NSApp.activate(ignoringOtherApps: true)
var description = ""
let fixInstructions = " " + "Error.FixInstructions".localized()
if let ne = (error as? NearbyError) {
switch ne {
case .inputOutput:
description = "I/O Error." + fixInstructions
case .protocolError(errorMessage: let errorMessage):
description = errorMessage + fixInstructions
case .packetFilterError:
openAlert(type: .NetworkFilter)
return
case .requiredFieldMissing(errorMessage: let errorMessage):
description = errorMessage + fixInstructions
case .ukey2:
description = "Error.Crypto".localized() + ": \(ne.localizedDescription)" + fixInstructions
case .canceled(reason: let reason):
if reason == .timedOut {
description = reason.localizedDescription() + fixInstructions
}
else {
description = reason.localizedDescription()
}
}
} else {
description = error.localizedDescription
}
let alert = NSAlert()
alert.alertStyle = .critical
alert.messageText = String(format: "TransferError".localized(), arguments: [deviceName])
alert.informativeText = description
alert.addButton(withTitle: "InformDeveloper".localized())
alert.addButton(withTitle: "CloseAlert".localized())
// Prevent multiple alerts at the same time
if self.isAlertShown {
log("Skipping alert for error \(error.localizedDescription) because one is already shown")
return
}
else {
AudioManager.playErrorSound()
self.isAlertShown = true
log("Showing alert with message: \"\(alert.messageText)\" and description: \"\(alert.informativeText)\"")
log("Unsuccessful transmission. Already successful transmissions: \(transmissionCount())")
let result = alert.runModal()
self.isAlertShown = false
if result == .alertFirstButtonReturn {
log("Sending logging string")
sendLoggingString()
}
}
}
func openAlert(type: AlertType) {
log("Opening Alert for \(type)")
DispatchQueue.main.async { [self] in
// Create an NSWindow to host the SwiftUI view
let alertWindow = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: issueViewWidth, height: issueViewHeight),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
switch type {
case .ApIsolation:
apIsolationAlertWindow = alertWindow
alertWindow.contentView = NSHostingView(rootView: ApIsolationIssueView(closeView: { self.apIsolationAlertWindow?.close() }))
case .NetworkFilter:
networkFilterAlertWindow = alertWindow
alertWindow.contentView = NSHostingView(rootView: NetworkFilterIssueView())
case .Firewall:
firewallAlertWindow = alertWindow
alertWindow.contentView = NSHostingView(rootView: FirewallIssueView())
}
alertWindow.title = "QuickDrop"
alertWindow.center()
alertWindow.isReleasedWhenClosed = false
alertWindow.setFrameAutosaveName(type.rawValue)
NSApp.activate(ignoringOtherApps: true)
alertWindow.makeKeyAndOrderFront(nil)
alertWindow.level = .normal
}
}
func closeApIsolationAlert() {
log("Closing AP Isolation Alert")
DispatchQueue.main.async {
self.apIsolationAlertWindow?.close()
self.apIsolationAlertWindow = nil
}
}
}
func sendLoggingString() {
// If we're in an extension, redirect to the main app
if Bundle.main.bundlePath.hasSuffix(".appex") {
log("sendLoggingString: in extension, redirecting to main app")
if let url = URL(string: "quickdrop://sendLog") {
NSWorkspace.shared.open(url)
}
return
}
// We're in the main app – do the actual sending
if let url = LogManager.sharedInstance.logFileURL {
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
log("Sending logging string with file URL: \(url)")
sendEmailWithAttachment(
fileURL: url,
recipients: ["quickdrop@leonboettger.com"],
subject: "QuickDrop \(appVersion) - \(getDeviceAndSystem())"
)
}
}
fileprivate func getDeviceAndSystem() -> String {
// Get Mac model identifier
var size: Int = 0
sysctlbyname("hw.model", nil, &size, nil, 0)
var modelBuffer = [CChar](repeating: 0, count: size)
sysctlbyname("hw.model", &modelBuffer, &size, nil, 0)
let model = String(cString: modelBuffer)
// Get macOS version
let version = ProcessInfo.processInfo.operatingSystemVersion
let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
return "\(model) - \(versionString)"
}
func sendEmailWithAttachment(fileURL: URL, recipients: [String], subject: String) {
guard let emailService = NSSharingService(named: .composeEmail) else {
log("No email service available")
return
}
emailService.recipients = recipients
emailService.subject = subject
emailService.perform(withItems: [fileURL])
log("Email service opened with recipients: \(recipients) and subject: \(subject)")
}
func copyToClipboard(_ text: String) {
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(text, forType: .string)
}
enum AlertType: String {
case ApIsolation
case NetworkFilter
case Firewall
}
+156
View File
@@ -0,0 +1,156 @@
/*
LaunchAtLogin-Modern
https://github.com/sindresorhus/LaunchAtLogin-Modern
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#if os(macOS) || targetEnvironment(macCatalyst)
import SwiftUI
import ServiceManagement
import os.log
@available(macOS 13.0, *)
public enum LaunchAtLogin {
fileprivate static let observable = Observable()
/**
Toggle “launch at login” for your app or check whether it's enabled.
*/
public static var isEnabled: Bool {
get { SMAppService.mainApp.status == .enabled }
set {
observable.objectWillChange.send()
do {
if newValue {
if SMAppService.mainApp.status == .enabled {
try? SMAppService.mainApp.unregister()
}
try SMAppService.mainApp.register()
} else {
try SMAppService.mainApp.unregister()
}
} catch {
log("Failed to \(newValue ? "enable" : "disable") launch at login: \(error.localizedDescription)")
}
}
}
/**
Whether the app was launched at login.
- Important: This property must only be checked in `NSApplicationDelegate#applicationDidFinishLaunching`.
*/
public static var wasLaunchedAtLogin: Bool {
let event = NSAppleEventManager.shared().currentAppleEvent
return event?.eventID == kAEOpenApplication
&& event?.paramDescriptor(forKeyword: keyAEPropData)?.enumCodeValue == keyAELaunchedAsLogInItem
}
}
@available(macOS 13.0, *)
extension LaunchAtLogin {
final class Observable: ObservableObject {
var isEnabled: Bool {
get { LaunchAtLogin.isEnabled }
set {
LaunchAtLogin.isEnabled = newValue
}
}
}
}
@available(macOS 13.0, *)
extension LaunchAtLogin {
/**
This package comes with a `LaunchAtLogin.Toggle` view which is like the built-in `Toggle` but with a predefined binding and label. Clicking the view toggles “launch at login” for your app.
```
struct ContentView: View {
var body: some View {
LaunchAtLogin.Toggle()
}
}
```
The default label is `"Launch at login"`, but it can be overridden for localization and other needs:
```
struct ContentView: View {
var body: some View {
LaunchAtLogin.Toggle {
Text("Launch at login")
}
}
}
```
*/
public struct Toggle<Label: View>: View {
@ObservedObject private var launchAtLogin = LaunchAtLogin.observable
private let label: Label
/**
Creates a toggle that displays a custom label.
- Parameters:
- label: A view that describes the purpose of the toggle.
*/
public init(@ViewBuilder label: () -> Label) {
self.label = label()
}
public var body: some View {
SwiftUI.Toggle(isOn: $launchAtLogin.isEnabled) { label }
}
}
}
@available(macOS 13.0, *)
extension LaunchAtLogin.Toggle<Text> {
/**
Creates a toggle that generates its label from a localized string key.
This initializer creates a ``Text`` view on your behalf with the provided `titleKey`.
- Parameters:
- titleKey: The key for the toggle's localized title, that describes the purpose of the toggle.
*/
public init(_ titleKey: LocalizedStringKey) {
label = Text(titleKey)
}
/**
Creates a toggle that generates its label from a string.
This initializer creates a `Text` view on your behalf with the provided `title`.
- Parameters:
- title: A string that describes the purpose of the toggle.
*/
public init(_ title: some StringProtocol) {
label = Text(title)
}
/**
Creates a toggle with the default title of `Launch at login`.
*/
public init() {
self.init("Launch at login")
}
}
#endif
@@ -10,6 +10,8 @@ import Foundation
import AppKit
import Network
fileprivate let routerCheckQueue = DispatchQueue(label: "routerCheckQueue")
struct ApIsolationIssueView: View {
@Environment(\.colorScheme) var colorScheme
@@ -27,6 +29,7 @@ struct ApIsolationIssueView: View {
}
}
func checkIfApIsolationStillEnabled() {
scanner.scan { isConnectionAllowed in
if isConnectionAllowed {
@@ -48,8 +51,6 @@ struct ApIsolationIssueView: View {
}
}
fileprivate let routerCheckQueue = DispatchQueue(label: "routerCheckQueue")
func openRouterSettingsPage() async {
let routerIP: String? = await withCheckedContinuation { continuation in
@@ -61,6 +62,7 @@ func openRouterSettingsPage() async {
guard let routerIP else {
DispatchQueue.main.async {
AudioManager.playErrorSound()
let alert = NSAlert()
alert.alertStyle = .critical
@@ -77,6 +79,8 @@ func openRouterSettingsPage() async {
NSWorkspace.shared.open(url)
}
}
/// Attempts to get the default gateway IP address (e.g., 192.168.1.1)
/// and ensures it's reachable. Returns nil if unreachable or not found.
func getDefaultGatewayIP() -> String? {
@@ -98,6 +102,7 @@ func getDefaultGatewayIP() -> String? {
return nil
}
/// Executes a shell command and returns the output
@discardableResult
func shell(_ command: String) -> String {
@@ -114,6 +119,7 @@ func shell(_ command: String) -> String {
return String(data: data, encoding: .utf8) ?? ""
}
/// Checks if a host (IP address or hostname) is reachable on port 80
func isHostReachable(_ host: String) -> Bool {
let semaphore = DispatchSemaphore(value: 0)
@@ -145,7 +151,6 @@ func isHostReachable(_ host: String) -> Bool {
}
#Preview {
ApIsolationIssueView(closeView: {})
.frame(width: issueViewWidth, height: issueViewHeight)
+53
View File
@@ -0,0 +1,53 @@
//
// LicenseWindow.swift
// QuickDrop
//
// Created by Leon Böttger on 05.03.25.
//
import SwiftUI
struct LicensePage: View {
var body: some View {
ZStack {
Color.defaultBackground
.edgesIgnoringSafeArea(.all)
ScrollView {
LazyVStack {
if let path = Bundle.main.path(forResource: "License", ofType: "txt"),
let licenseText = try? String(contentsOfFile: path) {
Text(licenseText)
.padding()
}
else {
Text(Bundle.main.path(forResource: "License", ofType: "txt") ?? "License file not found")
}
}
}
}
}
}
func openLicenseWindow() -> NSWindow {
// Create a new NSWindow and retain it in the `newWindow` property
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 500, height: 400),
styleMask: [.titled, .closable, .resizable],
backing: .buffered,
defer: false
)
window.center()
window.isReleasedWhenClosed = false
window.title = "Acknowledgements".localized()
window.contentView = NSHostingView(rootView: LicensePage())
// Ensure the window is always on top
NSApp.activate(ignoringOtherApps: true) // Brings the whole app to the front
window.makeKeyAndOrderFront(nil)
window.level = .normal
// Retain the window to prevent deallocation
return window
}
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23727" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23504"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23727"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
@@ -7,9 +7,9 @@
import SwiftUI
import AppKit
import NearbyShare
struct SettingsView: View {
@AppStorage(UserDefaultsKeys.automaticallyAcceptFiles.rawValue) private var automaticallyAcceptFiles = false
@AppStorage(UserDefaultsKeys.openFinderAfterReceiving.rawValue) private var openFinderAfterReceiving = false
@AppStorage(UserDefaultsKeys.saveFolderBookmark.rawValue) private var saveFolderPath: Data = Data()
@@ -45,10 +45,26 @@ struct SettingsView: View {
Toggle("OpenFinderAfterReceiving", isOn: $openFinderAfterReceiving)
.padding(.top, 5)
Text("OpenFinderAfterReceivingFooter")
.font(.footnote)
.padding(.top, 5)
if #available(macOS 13.0, *) {
Divider()
.padding(.vertical, 10)
LaunchAtLogin.Toggle {
Text("LaunchAtLogin")
}
.padding(.top, 5)
}
}
}
}
private func getSavedFolderPath() -> String? {
if !saveFolderPath.isEmpty {
var isStale = false
@@ -62,6 +78,7 @@ struct SettingsView: View {
return nil // No saved folder
}
private func selectFolder() {
let panel = NSOpenPanel()
panel.title = "SaveFilesLocation".localized()
@@ -6,19 +6,15 @@
//
import SwiftUI
import NearbyShare
struct TutorialView: View {
let title: String
let text: String
let showsLicense: Bool
var showsBetaJoinButton: Bool = false
let openPlus: () -> Void
@State private var licenseWindow: NSWindow?
@State var taps = 0
@AppStorage(UserDefaultsKeys.plusVersion.rawValue) var isPlusVersion = false
var body: some View {
@@ -31,22 +27,6 @@ struct TutorialView: View {
.padding()
.frame(width: 550)
if showsBetaJoinButton {
Button {
let betaTextBottom = "BetaEmailBottom".localized()
if let url = URL(string: "mailto:quickdrop-beta@leonboettger.com?subject=QuickDrop for Android Beta&body=\(betaTextBottom)") {
NSWorkspace.shared.open(url)
}
} label: {
Text("JoinBeta")
}
.keyboardShortcut(.defaultAction)
.padding(.top)
}
if showsLicense {
HStack(spacing: 30) {
@@ -106,7 +86,7 @@ struct LargeAppIconView<Content: View>: View {
Image("AppIconHighRes")
.resizable()
.frame(width: 150, height: 150)
.padding(.top, 50)
.padding(.top, 40)
Text(title.localized())
.font(.largeTitle)
@@ -120,53 +100,6 @@ struct LargeAppIconView<Content: View>: View {
}
}
func sendLoggingString() {
if let url = LogManager.sharedInstance.logFileURL {
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
sendEmailWithAttachment(fileURL: url, recipients: ["quickdrop@leonboettger.com"], subject: "QuickDrop \(appVersion) - \(getDeviceAndSystem())")
}
}
fileprivate func getDeviceAndSystem() -> String {
// Get Mac model identifier
var size: Int = 0
sysctlbyname("hw.model", nil, &size, nil, 0)
var modelBuffer = [CChar](repeating: 0, count: size)
sysctlbyname("hw.model", &modelBuffer, &size, nil, 0)
let model = String(cString: modelBuffer)
// Get macOS version
let version = ProcessInfo.processInfo.operatingSystemVersion
let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
return "\(model) - \(versionString)"
}
func sendEmailWithAttachment(fileURL: URL, recipients: [String], subject: String) {
guard let emailService = NSSharingService(named: .composeEmail) else {
log("No email service available")
return
}
emailService.recipients = recipients
emailService.subject = subject
emailService.perform(withItems: [fileURL])
}
func copyToClipboard(_ text: String) {
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(text, forType: .string)
}
#Preview {
WelcomeScreen(openPlusScreen: {}, openAppAdvertisementView: {}, openCableTransmissionView: {}, checkForNetworkIssues: {})
@@ -6,7 +6,7 @@
//
import SwiftUI
import NearbyShare
import UniformTypeIdentifiers
struct WelcomeScreen: View {
@@ -40,11 +40,11 @@ struct WelcomeScreen: View {
openPrivacyPolicy()
}
ExternalLinkLabel(label: "AndroidApp", icon: "smartphone") {
ExternalLinkLabel(label: "AndroidApp", icon: getPhoneIcon()) {
openAppAdvertisementView()
}
ExternalLinkLabel(label: "TransmitUsingCable", icon: "cable.connector") {
ExternalLinkLabel(label: "TransmitUsingCable", icon: getCableIcon()) {
openCableTransmissionView()
}
@@ -83,6 +83,10 @@ struct WelcomeScreen: View {
}
}
.frame(width: 1000, height: 600)
.onDrop(of: [.fileURL], isTargeted: nil) { providers in
handleDrop(providers: providers)
return true
}
}
func openPrivacyPolicy() {
@@ -90,6 +94,57 @@ struct WelcomeScreen: View {
NSWorkspace.shared.open(url)
}
}
func getPhoneIcon() -> String {
if #available(macOS 14.0, *) {
return "smartphone"
}
return "iphone.rear.camera"
}
func getCableIcon() -> String {
if #available(macOS 12.0, *) {
return "cable.connector"
}
return "externaldrive.connected.to.line.below.fill"
}
func handleDrop(providers: [NSItemProvider]) {
let dispatchGroup = DispatchGroup()
var urls: [URL] = []
let urlQueue = DispatchQueue(label: "DroppedURLsQueue")
for provider in providers {
if provider.hasItemConformingToTypeIdentifier(UTType.fileURL.identifier) {
dispatchGroup.enter()
provider.loadItem(forTypeIdentifier: UTType.fileURL.identifier, options: nil) { (item, error) in
defer { dispatchGroup.leave() }
if let data = item as? Data,
let url = NSURL(absoluteURLWithDataRepresentation: data, relativeTo: nil) as URL? {
urlQueue.sync {
urls.append(url)
}
} else if let url = item as? URL {
urlQueue.sync {
urls.append(url)
}
}
}
}
}
dispatchGroup.notify(queue: .main) {
if !urls.isEmpty {
sendToSharingService(items: urls)
}
}
}
func sendToSharingService(items: [Any]) {
if let sharingService = NSSharingService(named: NSSharingService.Name("com.leonboettger.neardrop.ShareExtension")) {
sharingService.perform(withItems: items)
}
}
}
-191
View File
@@ -1,191 +0,0 @@
/* No comment provided by engineer. */
"Accept" = "قبول";
/* No comment provided by engineer. */
"Decline" = "رفض";
/* No comment provided by engineer. */
"DeviceName" = "اسم الجهاز: %@";
/* No comment provided by engineer. */
"DeviceSendingFiles" = "%1$@ يرسل لك %2$@";
"DeviceCurrentlySendingText" = "قام %1$@ بإدراج \"%2$@\" في الحافظة الخاصة بك.";
"DeviceSendingText" = "يرغب %1$@ في إدراج \"%2$@\" في حافظة جهاز Mac الخاص بك.";
"DeviceCurrentlySendingUrl" = "قام %1$@ بفتح \"%2$@\".";
"DeviceSendingUrl" = "يرغب %1$@ في فتح \"%2$@\" على جهاز Mac الخاص بك.";
"InsertedIntoClipboard" = "تم الإدراج";
"UnsupportedFileType" = "الملف الذي حاولت إرساله غير مدعوم حالياً. يُرجى إرسال تقرير بالخطأ.";
/* No comment provided by engineer. */
"Error.Crypto" = "خطأ في التشفير";
/* No comment provided by engineer. */
"Error.Protocol" = "خطأ في الاتصال";
/* No comment provided by engineer. */
"NFiles" = "%d ملفات";
/* No comment provided by engineer. */
"NotificationsDenied.Message" = "يحتاج QuickDrop إلى القدرة على عرض الإشعارات لنقل الملفات الواردة. يرجى السماح بالإشعارات في إعدادات النظام.";
/* No comment provided by engineer. */
"NotificationsDenied.OpenSettings" = "افتح الإعدادات";
/* No comment provided by engineer. */
"NotificationsDenied.Title" = "مطلوب إذن الإشعارات";
/* No comment provided by engineer. */
"PinCode" = "الرمز السري: %@";
/* No comment provided by engineer. */
"Quit" = "إنهاء QuickDrop";
/* No comment provided by engineer. */
"TransferError" = "فشل في استقبال الملفات من %@";
/* No comment provided by engineer. */
"VisibleToEveryone" = "مرئي للجميع";
"NoNetworkConnection" = "لا يوجد اتصال بالشبكة";
"RecommendedApps" = "التطبيقات الموصى بها";
"GetSupport" = "الدعم";
"PrivacyPolicy" = "سياسة الخصوصية";
"ReadyToReceive" = "جاهز للاستلام";
"SendClipboard" = "إرسال الحافظة";
"Clipboard" = "الحافظة";
"DeviceCurrentlySendingFiles" = "%1$@ يرسل لك %2$@";
"UserManual" = "دليل المستخدم والإعدادات";
"Settings" = "الإعدادات";
"AutomaticallyAcceptFiles" = "قبول الملفات الواردة تلقائيًا";
"AutomaticallyAcceptFilesFooter" = "يُوصى بتمكين هذا الإعداد فقط في شبكتك الخاصة. يمكن لأي شخص على نفس الشبكة إرسال الملفات إلى جهازك دون موافقتك.";
"SaveFilesTo" = "حفظ الملفات الواردة في:";
"SaveFilesLocation" = "موقع الملفات الواردة";
"SaveFilesToButton" = "تحديد…";
"OpenFinderAfterReceiving" = "عرض الملفات في Finder بعد الاستلام";
"DownloadsFolder" = "التنزيلات";
"SendFiles" = "إرسال الملفات";
"ReceiveFiles" = "استقبال الملفات";
"DeviceNotShown" = "الجهاز غير ظاهر؟";
"Troubleshooting" = "استكشاف الأخطاء وإصلاحها";
"AndroidApp" = "تطبيق Android";
"AndroidAppDescription" = "QuickDrop متاح الآن أيضًا كتطبيق منفصل لجهاز Android الخاص بك! على الرغم من أنه يمكن استخدام QuickDrop بدون تطبيق Android، فإن QuickDrop لنظام Android يجعل نقل الملفات أسهل وأسرع. باستخدام تطبيق Android، يمكنك حتى نقل الملفات عندما لا تكون متصلاً بشبكة Wi-Fi المنزلية، عن طريق توصيل جهاز Mac بنقطة الاتصال المحمولة الخاصة بجهاز Android. لا يزال التطبيق يوفر نفس التشفير من طرف إلى طرف عند نقل الملفات.
QuickDrop لنظام Android في المرحلة التجريبية حاليًا. للتسجيل في النسخة التجريبية، يرجى إرسال بريد إلكتروني إلى quickdrop-beta@leonboettger.com أو استخدام الزر أدناه. شكرًا لاهتمامك!";
"JoinBeta" = "انضم إلى النسخة التجريبية لتطبيق QuickDrop على Android";
"BetaEmailBottom" = "مرحبًا،
يرجى إضافتي إلى النسخة التجريبية لتطبيق QuickDrop على Android.
–––––––––––––––
يرجى تقديم عنوان بريدك الإلكتروني في Google أدناه حتى نتمكن من دعوتك للانضمام إلى النسخة التجريبية. بعد ذلك، ستتلقى رسالة بريد إلكتروني تحتوي على رابط لتنزيل التطبيق من متجر Google Play.
سيتم استخدام عنوان بريدك الإلكتروني لهذا الغرض فقط، ولن يتم تخزينه أو مشاركته.
عنوان بريدك الإلكتروني: ";
"SendFilesDescription" = "لإرسال ملف إلى جهاز Android الخاص بك، انقر فوق أيقونة المشاركة أو انقر بزر الماوس الأيمن على الملف ثم اختر \"مشاركة\". بعد ذلك، اختر \"QuickDrop\" من قائمة التطبيقات واتبع التعليمات.
إذا لم يظهر QuickDrop في القائمة، فانقر فوق \"تعديل الامتدادات\"، وانتقل لأسفل، وقم بتمكين QuickDrop.
يمكنك أيضًا إرسال المحتوى الحالي للحافظة الخاصة بجهاز Mac إلى جهاز Android الخاص بك عن طريق النقر على أيقونة QuickDrop في شريط القوائم واختيار \"إرسال الحافظة\".";
"WelcomeToQuickDrop" = "مرحبًا بك في QuickDrop";
"Acknowledgements" = "الشكر والتقدير";
"SupportQuickDrop" = "ادعم QuickDrop";
"UserManualDescription" = "عند تشغيل QuickDrop، سيظهر في شريط القوائم في أعلى الشاشة. انقر على أيقونة QuickDrop إذا كنت ترغب في إيقاف استقبال الملفات.
لإرسال الملفات من جهاز Android الخاص بك إلى جهاز Mac، اضغط على أيقونة المشاركة على هاتفك ثم اختر \"Quick Share\". سيتم إدراج جهاز Mac كجهاز متاح. سيتم حفظ الملفات في مجلد التنزيلات.
مهم: يجب أن يكون جهاز Android وجهاز Mac متصلين بنفس شبكة WiFi لنقل الملفات.";
"TroubleshootingDescription" = "إذا لم يظهر جهاز Mac على جهاز Android الخاص بك، على الرغم من أن كلا الجهازين متصلان بنفس شبكة Wi-Fi، أو إذا فشلت عملية الإرسال، جرّب ما يلي:
1) افتح إعدادات النظام، وانتقل إلى \"الخصوصية والأمان\" > \"الشبكة المحلية\". تأكد من تفعيل QuickDrop. إذا كنت تستخدم جدار حماية أو برنامج مكافحة فيروسات تابعًا لجهة خارجية، يُرجى الرجوع إلى وثائق البرنامج للسماح بالاتصالات الواردة لـ QuickDrop.
2) أعد تشغيل QuickDrop بالنقر على أيقونة شريط القوائم، واختر الخيار الأخير، ثم افتح التطبيق مرة أخرى. أعد تشغيل جهاز Android الخاص بك.
ملاحظة: بعض الشبكات لا تسمح للأجهزة بالتواصل مع بعضها. في هذه الحالة، تحتاج إلى تنزيل تطبيق QuickDrop على هاتفك وتوصيل جهاز Mac بنقطة الاتصال الخاصة بهاتفك لإرسال الملفات. يمكنك العثور على رابط التطبيق في الشريط الجانبي على اليمين.";
"plusview_title" = "شكرًا لاستخدامك QuickDrop!";
"plusview_description" = "قم بفتح QuickDrop+ لمواصلة تبادل الملفات بلا حدود. QuickDrop+ هو شراء لمرة واحدة – لا حاجة للاشتراك. تشمل جميع الميزات الحالية والمستقبلية.";
"plusview_success_title" = "شكرًا لشرائك!";
"plusview_success_description" = "يمكنك الآن متابعة استلام الملفات بدون قيود. أي ملفات حاولت إرسالها سابقًا تم حفظها بالفعل على جهازك.";
"plusview_success_button" = "حسنًا!";
"plusview_unlock_label" = "ادعم QuickDrop";
"plusview_restorepurchase" = "هل اشتريته بالفعل؟";
"plusview_restorefailed" = "حدثت مشكلة أثناء استعادة عملية الشراء";
"plusview_restorefailed_proceed" = "موافق";
"plusview_purchasefailed" = "فشل الشراء";
"plusview_purchasefaileddescription" = "إذا ظهر هذا الرسالة بشكل غير متوقع، يرجى التأكد من اتصالك بالإنترنت وأن عمليات الشراء داخل التطبيق مفعّلة على جهازك، ثم حاول مرة أخرى.";
"plusview_nothingtorestore" = "لا يوجد شيء لاستعادته!";
"plusview_productRequestFailed" = "تعذر جلب عمليات الشراء داخل التطبيق في الوقت الحالي.";
"Connecting" = "جارٍ الاتصال...";
"NotEnoughSpace" = "مساحة القرص غير كافية";
"Sending" = "جارٍ الإرسال...";
"TransferCanceled" = "تم الإلغاء";
"TransferDeclined" = "تم الرفض";
"TransferFinished" = "اكتمل النقل";
"TransferTimedOut" = "انتهت المهلة";
"UnsupportedType" = "نوع المرفق غير مدعوم";
"QrCodeInstructions" = "امسح رمز الاستجابة السريعة (QR) باستخدام جهاز Android الخاص بك لتوصيله بجهاز Mac. تأكد من أن هاتفك متصل بنفس الشبكة.\n\nإذا كنت لا ترغب في مسح رمز الاستجابة السريعة في كل مرة تقوم فيها بنقل ملف، فقم بتثبيت تطبيق QuickDrop (انظر أعلاه).";
"ConnectWithQuickDropApp" = "الاتصال بتطبيق QuickDrop";
"ConnectWithoutApp" = "الاتصال بدون تطبيق";
"QrCodeInstructionsApp" = "لربط جهاز Mac بجهاز Android الخاص بك، افتح تطبيق QuickDrop على جهاز Android. تأكد من أن هاتفك متصل بالشبكة نفسها.\n\nلتثبيت QuickDrop على هاتفك، امسح رمز QR ضوئيًا أو ابحث عن 'QuickDrop' في متجر Google Play.";
"QuickDropAndroidAppAdvertisement" = "أصبح QuickDrop متاحًا الآن أيضًا كتطبيق منفصل لهاتفك الذكي! يوفر التطبيق ميزات إضافية، مثل إمكانية نقل الملفات حتى عندما لا تكون متصلاً بشبكة Wi-Fi المنزلية، عن طريق توصيل Mac بنقطة الاتصال الخاصة بهاتفك الذكي.\n\nلتثبيت QuickDrop على هاتفك، امسح رمز QR ضوئيًا أو ابحث عن 'QuickDrop' في متجر Google Play.";
"TypeNotSupported" = "نوع غير مدعوم";
"TypeNotSupportedDescription" = "نوع الملف المحدد غير مدعوم. إذا كنت ترغب في مشاركته على أي حال، انقر بزر الفأرة الأيمن على الملف، واختر \"ضغط\" ثم أرسل الملف المضغوط.";
"TypeNotSupportedButton" = "حسنًا";
"CloseAlert" = "إغلاق";
"InformDeveloper" = "إرسال تقرير عن خلل";
"TimeoutTitle" = "حدث خطأ ما";
"TimeoutDescription" = "تعذر على QuickDrop الاتصال بهاتفك. يرجى فتح إعدادات جهاز Android والبحث عن 'Quick Share'. قم بتغيير الإعداد 'من يمكنه المشاركة معك' إلى 'الجميع' ثم حاول مرة أخرى.";
"TimeoutButton" = "موافق";
"ApIsolationHeader" = "قد لا يسمح جهاز التوجيه الخاص بك بالتواصل بين الأجهزة";
"ApIsolationDescription" = "اكتشف QuickDrop أن جهاز التوجيه اللاسلكي الخاص بك قد يمنع الأجهزة من التواصل مع بعضها البعض. لنقل الملفات بين هاتفك وMac، ستحتاج إلى تعطيل عزل العميل (المعروف أيضًا باسم عزل AP أو عزل Wi-Fi).
إليك كيفية تعطيله:
1. قم بتسجيل الدخول إلى جهاز التوجيه الخاص بك: افتح متصفح الويب وأدخل عنوان IP لجهاز التوجيه الخاص بك (عادةً 192.168.0.1 أو 192.168.1.1)، أو استخدم الزر في أسفل هذه الصفحة. قم بتسجيل الدخول باستخدام اسم المستخدم وكلمة المرور الخاصة بالمسؤول. (تحقق من ملصق جهاز التوجيه أو الدليل إذا لم تكن متأكدًا.)
2. ابحث عن الإعداد: تحقق من أقسام مثل إعدادات الشبكة اللاسلكية أو الإعدادات المتقدمة أو الأمان. ابحث عن خيارات باسم \"عزل AP\" أو \"عزل العميل\" أو \"العزل اللاسلكي\" أو \"حظر Intra-BSS\".
3. قم بتعطيله: تأكد من أن هذا الإعداد معطل. احفظ التغييرات وأعد تشغيل جهاز التوجيه إذا لزم الأمر.
بمجرد التعطيل، ستتمكن الأجهزة على شبكة Wi-Fi الخاصة بك (مثل هاتفك وMac) من اكتشاف بعضها البعض والاتصال. يرجى الملاحظة: إذا كنت متصلاً بشبكة ضيوف، فقد لا يكون هذا الإعداد متاحًا. في هذه الحالة، استخدم شبكة مختلفة، أو قم بتنزيل تطبيق QuickDrop على هاتفك، وقم بتوصيل جهاز Mac بنقطة الاتصال الخاصة بهاتفك لنقل الملفات. ابحث عن “QuickDrop” في متجر Google Play لتنزيل التطبيق.";
"ApIsolationAction" = "فتح إعدادات جهاز التوجيه";
"ApIsolationFindRouterFailedHeader" = "تعذر العثور على عنوان IP لجهاز التوجيه";
"ApIsolationFindRouterFailedDescription" = "لم يتمكن QuickDrop من تحديد عنوان جهاز التوجيه الخاص بك. يرجى الرجوع إلى دليل مستخدم جهاز التوجيه اللاسلكي الخاص بك للحصول على العنوان.";
"FirewallHeader" = "قد يمنع جدار الحماية نقل الملفات الواردة";
"FirewallDescription" = "اكتشف QuickDrop أن جدار الحماية قد يمنع جهاز Android الخاص بك من الاتصال بجهاز Mac الخاص بك. يتم استخدام جدران الحماية لحظر الاتصالات الواردة بشكل افتراضي. لتلقي الملفات، يجب السماح بالاتصالات الواردة، حيث يحاول هاتفك الاتصال بجهاز Mac – مما يُنشئ اتصالاً وارداً.
من المحتمل أنك تستخدم برنامج مكافحة فيروسات على جهاز Mac الخاص بك. لديك خياران:
1. السماح بالاتصالات الواردة لتطبيق QuickDrop في إعدادات جدار الحماية الخاص بك. تختلف هذه العملية حسب البرنامج، لذلك قد تحتاج إلى التواصل مع الشركة المصنعة لبرنامج الحماية/جدار الحماية لمعرفة كيفية المتابعة.
2. تعطيل جدار الحماية الخاص ببرنامج مكافحة الفيروسات واستخدام جدار الحماية المدمج في نظام macOS. جدار الحماية الخاص بـ Apple مُحسَّن لتطبيق QuickDrop.
إليك كيفية تعطيل جدار الحماية لبرنامج مكافحة الفيروسات: افتح إعدادات النظام على جهاز Mac، وانتقل إلى الشبكة > VPN والتصفية، ثم قم بإيقاف تشغيل مرشح المحتوى. بعد ذلك، انتقل إلى الشبكة > جدار الحماية، وقم بتفعيل جدار الحماية الخاص بـ Mac.
بعد القيام بذلك، سيتمكن هاتفك من الاتصال بجهاز Mac.";
"NetworkFilterHeader" = "قد يمنع مرشح الحزم نقل الملفات";
"NetworkFilterDescription" = "اكتشف QuickDrop أن برنامجًا مثبتًا على جهاز Mac الخاص بك يقوم بتعديل حركة مرور الشبكة. ونتيجة لذلك، تعذر فك تشفير الملف الذي حاولت إرساله من جهاز Android.
قد يكون هناك مرشح حزم معيب مفعل على جهاز Mac، ربما تم تثبيته بواسطة برنامج مكافحة الفيروسات (مثل ESET). افتح تفضيلات النظام > الشبكة > VPN والتصفية، ثم قم بإيقاف تشغيل مرشح المحتوى.
بمجرد الانتهاء من ذلك، يجب أن تتمكن من نقل الملفات بنجاح.";
"TransmitUsingCable" = "النقل باستخدام الكابل";
"DownloadCableConnectionApp" = "تحميل iDroid";
"DownloadCableConnectionAppDescription" = "إذا كنت تريد نقل الملفات واستعراض تخزين جهاز Android الخاص بك عبر USB، يمكنك الآن القيام بذلك باستخدام تطبيق iDroid الجديد.\n\niDroid يدعم تنزيل وتحميل الملفات والمجلدات الكاملة باستخدام السحب والإفلات.";
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFiles</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@files@</string>
<key>files</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d ملف</string>
<key>other</key>
<string>%d ملفات</string>
</dict>
</dict>
</dict>
</plist>
-195
View File
@@ -1,195 +0,0 @@
/* No comment provided by engineer. */
"Accept" = "Akzeptieren";
/* No comment provided by engineer. */
"Decline" = "Ablehnen";
/* No comment provided by engineer. */
"DeviceName" = "Gerätename: %@";
/* No comment provided by engineer. */
"DeviceSendingFiles" = "%1$@ möchte dir %2$@ senden";
"DeviceCurrentlySendingText" = "%1$@ hat \"%2$@\" in deine Zwischenablage eingefügt.";
"DeviceSendingText" = "%1$@ möchte \"%2$@\" in die Zwischenablage deines Macs einfügen.";
"DeviceCurrentlySendingUrl" = "%1$@ hat \"%2$@\" geöffnet.";
"DeviceSendingUrl" = "%1$@ möchte \"%2$@\" auf deinem Mac öffnen.";
"InsertedIntoClipboard" = "Eingefügt";
"UnsupportedFileType" = "Die Datei, die du senden wolltest, wird derzeit nicht unterstützt. Bitte sende einen Fehlerbericht.";
/* No comment provided by engineer. */
"Error.Crypto" = "Verschlüsselungsfehler";
/* No comment provided by engineer. */
"Error.Protocol" = "Fehler bei der Datenübertragung";
/* No comment provided by engineer. */
"NFiles" = "%d Dateien";
/* No comment provided by engineer. */
"NotificationsDenied.Message" = "QuickDrop benötigt die Erlaubnis, Mitteilungen für eingehende Übertragungen anzuzeigen. Bitte erlaube Mitteilungen in den Systemeinstellungen.";
/* No comment provided by engineer. */
"NotificationsDenied.OpenSettings" = "Einstellungen öffnen";
/* No comment provided by engineer. */
"NotificationsDenied.Title" = "Mitteilungsberechtigungen notwendig";
/* No comment provided by engineer. */
"PinCode" = "PIN: %@";
/* No comment provided by engineer. */
"Quit" = "QuickDrop beenden";
/* No comment provided by engineer. */
"TransferError" = "Die Daten von %@ konnten nicht empfangen werden";
/* No comment provided by engineer. */
"VisibleToEveryone" = "Für alle sichtbar";
"NoNetworkConnection" = "Keine Netzwerkverbindung";
"RecommendedApps" = "Empfohlene Apps";
"GetSupport" = "Support";
"PrivacyPolicy" = "Datenschutzerklärung";
"ReadyToReceive" = "Empfangsbereit";
"SendClipboard" = "Zwischenablage senden";
"Clipboard" = "Zwischenablage";
"DeviceCurrentlySendingFiles" = "%1$@ sendet %2$@";
"UserManual" = "Benutzerhandbuch & Einstellungen";
"Settings" = "Einstellungen";
"AutomaticallyAcceptFiles" = "Eingehende Dateien automatisch akzeptieren";
"AutomaticallyAcceptFilesFooter" = "Diese Einstellung wird nur in privaten Netzwerken empfohlen. Jede Person im selben Netzwerk kann ohne deine Zustimmung Dateien an diesen Mac senden.";
"SaveFilesTo" = "Eingehende Dateien speichern in:";
"SaveFilesLocation" = "Speicherort der eingehenden Dateien";
"SaveFilesToButton" = "Auswählen…";
"OpenFinderAfterReceiving" = "Dateien nach Empfang im Finder anzeigen";
"DownloadsFolder" = "Downloads";
"SendFiles" = "Dateien senden";
"ReceiveFiles" = "Dateien empfangen";
"DeviceNotShown" = "Gerät nicht sichtbar?";
"Troubleshooting" = "Fehlerbehebung";
"AndroidApp" = "Android-App";
"AndroidAppDescription" = "QuickDrop ist jetzt auch als eigenständige App für dein Android-Gerät verfügbar! Auch wenn QuickDrop ohne eine Android-App verwendet werden kann, macht QuickDrop für Android den Dateitransfer noch einfacher und schneller. Mit der Android-App kannst du sogar Dateien übertragen, wenn du nicht mit dem WLAN-Netzwerk zu Hause verbunden bist, indem du den Mac mit dem Hotspot deines Androids verbindest. Die Android-App bietet weiterhin die gleiche Ende-zu-Ende-Verschlüsselung beim Dateitransfer.
QuickDrop für Android befindet sich derzeit in der Beta-Phase. Um dich für die Beta zu registrieren, sende bitte eine E-Mail an quickdrop-beta@leonboettger.com oder verwende den Button unten. Danke für dein Interesse!";
"JoinBeta" = "An der QuickDrop für Android Beta teilnehmen";
"BetaEmailBottom" = "Hallo,
bitte füge mich zur QuickDrop für Android Beta hinzu.
–––––––––––––––
Bitte gib unten die E-Mail-Adresse deines Google-Kontos an, damit du zur Beta eingeladen werden kannst. Anschließend erhältst du eine E-Mail mit einem Link zum Herunterladen der App aus dem Google Play Store.
Deine E-Mail-Adresse wird nur für diesen Zweck verwendet und nicht gespeichert oder weitergegeben.
Deine E-Mail-Adresse: ";
"SendFilesDescription" = "Um eine Datei an dein Android-Gerät zu senden, klicke mit der rechten Maustaste auf die Datei und wähle „Teilen“ oder wähle das Teilen-Symbol aus. Klicke anschließend auf „QuickDrop“ und folge den Anweisungen.
Falls QuickDrop nicht in der Liste erscheint, klicke auf „Erweiterungen bearbeiten“, scrolle nach unten und aktiviere den Schalter für QuickDrop.
Du kannst auch den aktuellen Inhalt der Zwischenablage deines Macs an dein Android-Gerät senden, indem du auf das QuickDrop-Symbol in der Menüleiste klickst und „Zwischenablage senden“ auswählst.";
"WelcomeToQuickDrop" = "Willkommen bei QuickDrop";
"Acknowledgements" = "Danksagungen";
"SupportQuickDrop" = "QuickDrop unterstützen";
"UserManualDescription" = "Wenn QuickDrop ausgeführt wird, wird es in der Menüleiste am oberen Rand des Bildschirms angezeigt. Klicke auf das QuickDrop-Symbol, um den Empfang von Dateien zu beenden.
Um Dateien von deinem Android-Gerät an deinen Mac zu senden, tippe auf deinem Handy auf das Teilen-Symbol und wähle „Quick Share“ aus. Dein Mac wird als verfügbares Gerät aufgelistet. Die Dateien werden im Downloads-Ordner gespeichert.
Wichtig: Dein Android-Gerät und dein Mac müssen sich im selben Netzwerk befinden, um Dateien zu übertragen.";
"TroubleshootingDescription" = "Wenn dein Mac nicht auf deinem Android-Gerät angezeigt wird, obwohl beide Geräte mit demselben WLAN verbunden sind, oder die Übertragung fehlgeschlagen ist, versuche Folgendes:
1) Öffne die Systemeinstellungen und gehe zu \"Datenschutz & Sicherheit\" > \"Lokales Netzwerk\". Stelle sicher, dass QuickDrop aktiviert ist. Wenn du eine Firewall oder ein Antivirenprogramm eines Drittanbieters verwendest, überprüfe die Dokumentation, um eingehende Verbindungen für QuickDrop zuzulassen.
2) Starte QuickDrop neu, indem du auf das Symbol in der Menüleiste klickst, den letzten Eintrag auswählst und die App erneut öffnest. Starte auch dein Android-Gerät neu.
Hinweis: Einige Netzwerke erlauben keine Kommunikation zwischen Geräten. In diesem Fall lade die QuickDrop-App auf dein Telefon herunter und verbinde deinen Mac mit dem Hotspot deines Telefons, um Dateien zu übertragen. Den Link zur App findest du in der Seitenleiste links.";
"plusview_title" = "Danke, dass du QuickDrop benutzt!";
"plusview_description" = "Schalte QuickDrop+ frei, um weiterhin Dateien unbegrenzt auszutauschen. QuickDrop+ ist ein Einmalkauf - ein Abonnement ist nicht erforderlich. Alle aktuellen und zukünftigen Funktionen sind enthalten.";
"plusview_success_title" = "Danke für deinen Kauf!";
"plusview_success_description" = "Du kannst nun weiterhin Dateien ohne Einschränkungen empfangen. Alle Dateien, welche gerade gesendet wurden, wurden bereits gespeichert.";
"plusview_success_button" = "Alles klar!";
"plusview_unlock_label" = "Unterstütze QuickDrop";
"plusview_restorepurchase" = "Bereits gekauft?";
"plusview_restorefailed" = "Es gab ein Problem beim Wiederherstellen deines Kaufs";
"plusview_restorefailed_proceed" = "Okay";
"plusview_purchasefailed" = "Kauf fehlgeschlagen";
"plusview_purchasefaileddescription" = "Falls diese Nachricht unerwartet erscheint, stelle sicher, dass du mit dem Internet verbunden bist und In-App-Käufe auf deinem Gerät aktiviert sind, und versuche es erneut.";
"plusview_nothingtorestore" = "Es gibt nichts wiederherzustellen!";
"plusview_productRequestFailed" = "In-App-Käufe können momentan nicht abgerufen werden.";
"Connecting" = "Verbinden...";
"NotEnoughSpace" = "Nicht genügend Speicherplatz";
"Sending" = "Senden...";
"TransferCanceled" = "Abgebrochen";
"TransferDeclined" = "Abgelehnt";
"TransferFinished" = "Übertragung abgeschlossen";
"TransferTimedOut" = "Zeitüberschreitung";
"UnsupportedType" = "Typ nicht unterstützt";
"QrCodeInstructions" = "Scanne den QR-Code mit deinem Android-Gerät, um es mit deinem Mac zu verbinden. Stelle sicher, dass dein Handy mit demselben Netzwerk verbunden ist.\n\nWenn du nicht bei jeder Dateiübertragung den QR-Code scannen willst, installiere die QuickDrop-App (siehe oben).";
"ConnectWithQuickDropApp" = "Mit QuickDrop-App verbinden";
"ConnectWithoutApp" = "Ohne App verbinden";
"QrCodeInstructionsApp" = "Um deinen Mac mit deinem Android-Gerät zu verbinden, öffne die QuickDrop-App auf deinem Android-Gerät. Stelle sicher, dass dein Telefon mit demselben Netzwerk verbunden ist.\n\nUm QuickDrop auf deinem Telefon zu installieren, scanne den QR-Code oder suche nach 'QuickDrop' im Google Play Store.";
"QuickDropAndroidAppAdvertisement" = "QuickDrop ist jetzt auch als eigenständige App für dein Smartphone verfügbar! Die App bietet zusätzliche Funktionen, wie die Möglichkeit, Dateien zu übertragen, wenn du nicht mit dem WLAN-Netzwerk zu Hause verbunden bist, indem du den Mac mit dem Hotspot deines Smartphones verbindest.\n\nUm QuickDrop auf deinem Telefon zu installieren, scanne den QR-Code oder suche nach 'QuickDrop' im Google Play Store.";
"TypeNotSupported" = "Typ nicht unterstützt";
"TypeNotSupportedDescription" = "Der ausgewählte Dateityp wird nicht unterstützt. Wenn du ihn trotzdem teilen möchtest, klicke mit der rechten Maustaste auf die Datei, wähle „Komprimieren“ aus und sende dann die komprimierte Datei.";
"TypeNotSupportedButton" = "Okay";
"CloseAlert" = "Schließen";
"InformDeveloper" = "Fehlerbericht senden";
"TimeoutTitle" = "Etwas ist schiefgelaufen";
"TimeoutDescription" = "QuickDrop konnte keine Verbindung zu deinem Telefon herstellen. Öffne die Einstellungen deines Android-Geräts und suche nach 'Quick Share'. Ändere die Einstellung 'Wer kann mit dir teilen' auf 'Jeder' und versuche es erneut.";
"TimeoutButton" = "OK";
"ApIsolationHeader" = "Dein Router erlaubt möglicherweise keine Kommunikation zwischen Geräten";
"ApIsolationDescription" = "QuickDrop hat erkannt, dass dein WLAN-Router möglicherweise verhindert, dass Geräte miteinander kommunizieren. Um Dateien zwischen deinem Telefon und deinem Mac zu übertragen, musst du die Client-Isolierung (auch bekannt als AP-Isolation oder WLAN-Isolation) deaktivieren.
So deaktivierst du sie:
1. Melde dich bei deinem Router an: Öffne einen Webbrowser und gib die IP-Adresse deines Routers ein (normalerweise 192.168.0.1 oder 192.168.1.1), oder verwende die Schaltfläche unten auf dieser Seite. Melde dich mit deinem Admin-Benutzernamen und Passwort an. (Sieh auf dem Etikett deines Routers oder im Handbuch nach, wenn du unsicher bist.)
2. Finde die Einstellung: Suche in Abschnitten wie WLAN-Einstellungen, Erweiterte Einstellungen oder Sicherheit. Suche nach Optionen mit den Namen „AP-Isolation“, „Client-Isolation“, „WLAN-Isolation“ oder „Intra-BSS-Blocking“. Bei einer Fritz!Box muss die Einstellung „Die angezeigten WLAN-Geräte dürfen untereinander kommunizieren“ unter „WLAN“ > „Funknetz“ aktiviert werden.
3. Deaktivieren: Stelle sicher, dass diese Einstellung deaktiviert (oder bei der Fritz!Box aktiviert) ist. Speichere die Änderungen und starte den Router bei Bedarf neu.
Anschließend können sich Geräte in deinem WLAN (wie dein Telefon und Mac) gegenseitig erkennen und verbinden. Bitte beachte: Wenn du dich in einem Gastnetzwerk befindest, ist diese Einstellung möglicherweise nicht verfügbar. In diesem Fall verwende bitte ein anderes Netzwerk oder lade die QuickDrop-App auf dein Handy herunter und verbinde deinen Mac mit dem Hotspot deines Handys, um Dateien zu übertragen. Suche im Google Play Store nach „QuickDrop“, um die App herunterzuladen.";
"ApIsolationAction" = "Router-Einstellungen öffnen";
"ApIsolationFindRouterFailedHeader" = "Router-IP konnte nicht gefunden werden";
"ApIsolationFindRouterFailedDescription" = "QuickDrop konnte die Adresse deines Routers nicht ermitteln. Bitte schaue im Benutzerhandbuch deines WLAN-Routers nach, um die Adresse zu erfahren.";
"FirewallHeader" = "Eine Firewall könnte eingehende Dateiübertragungen blockieren";
"FirewallDescription" = "QuickDrop hat festgestellt, dass eine Firewall möglicherweise verhindert, dass sich dein Android-Gerät mit deinem Mac verbinden kann. Firewalls blockieren standardmäßig eingehende Verbindungen. Um Dateien zu empfangen, müssen eingehende Verbindungen erlaubt sein, da dein Telefon versucht, eine Verbindung zu deinem Mac herzustellen – was eine eingehende Verbindung darstellt.
Wahrscheinlich verwendest du ein Antivirenprogramm auf deinem Mac. Du hast zwei Optionen:
1. Erlaube eingehende Verbindungen für QuickDrop in deinen Firewall-Einstellungen. Dieses Verfahren variiert je nach Programm, daher solltest du den Hersteller deiner Firewall-/Antivirensoftware kontaktieren, um zu erfahren, wie dies durchgeführt werden kann.
2. Deaktiviere die Firewall deiner Antivirensoftware und verwende stattdessen die integrierte Firewall des Mac. Die Firewall von Apple ist für QuickDrop optimiert.
So deaktivierst du die Firewall deiner Antivirensoftware: Öffne die Systemeinstellungen auf deinem Mac, navigiere zu Netzwerk > VPN & Filter und deaktiviere den Inhaltsfilter. Gehe dann zu Netzwerk > Firewall und aktiviere die Firewall des Mac.
Sobald dies erledigt ist, kann sich dein Telefon mit deinem Mac verbinden.";
"NetworkFilterHeader" = "Ein Paketfilter könnte die Dateiübertragung verhindern";
"NetworkFilterDescription" = "QuickDrop hat festgestellt, dass ein auf deinem Mac installiertes Programm den Netzwerkverkehr verändert. Dadurch konnte die Datei, die du von deinem Android-Gerät senden wolltest, nicht entschlüsselt werden.
Möglicherweise ist auf deinem Mac ein fehlerhafter Paketfilter aktiviert, der z. B. von einem Antivirenprogramm (z. B. ESET) stammt. Öffne Systemeinstellungen > Netzwerk > VPN & Filter und deaktiviere den Inhaltsfilter.
Danach solltest du Dateien erfolgreich übertragen können.";
"TransmitUsingCable" = "Mit Kabel übertragen";
"DownloadCableConnectionApp" = "iDroid herunterladen";
"DownloadCableConnectionAppDescription" = "Wenn du Dateien übertragen und den Speicher deines Android-Geräts per USB durchsuchen willst, kannst du das jetzt mit der neuen iDroid-App tun.\n\niDroid unterstützt das Herunterladen und Hochladen von Dateien und ganzen Ordnern per Drag & Drop.";
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFiles</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@files@</string>
<key>files</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d Datei</string>
<key>other</key>
<string>%d Dateien</string>
</dict>
</dict>
</dict>
</plist>
-193
View File
@@ -1,193 +0,0 @@
/* No comment provided by engineer. */
"Accept" = "Aceptar";
/* No comment provided by engineer. */
"Decline" = "Rechazar";
/* No comment provided by engineer. */
"DeviceName" = "Nombre del dispositivo: %@";
/* No comment provided by engineer. */
"DeviceSendingFiles" = "%1$@ te quiere enviar %2$@";
"DeviceCurrentlySendingText" = "%1$@ ha insertado \"%2$@\" en tu portapapeles.";
"DeviceSendingText" = "%1$@ quiere insertar \"%2$@\" en el portapapeles de tu Mac.";
"DeviceCurrentlySendingUrl" = "%1$@ ha abierto \"%2$@\".";
"DeviceSendingUrl" = "%1$@ quiere abrir \"%2$@\" en tu Mac.";
"InsertedIntoClipboard" = "Insertado";
"UnsupportedFileType" = "El archivo que intentaste enviar no es compatible actualmente. Por favor, envía un informe de error.";
/* No comment provided by engineer. */
"Error.Crypto" = " Error de encriptación";
/* No comment provided by engineer. */
"Error.Protocol" = "Error de comunicación";
/* No comment provided by engineer. */
//"NFiles" = "%d archivos";
/* No comment provided by engineer. */
"NotificationsDenied.Message" = "QuickDrop necesita poder mostrar notificaciones para las transferencias de archivos entrantes. Permite las notificaciones en la configuración del sistema.";
/* No comment provided by engineer. */
"NotificationsDenied.OpenSettings" = "Abrir la configuración del sistema";
/* No comment provided by engineer. */
"NotificationsDenied.Title" = "Permiso de notificaciones necesario";
/* No comment provided by engineer. */
"PinCode" = "PIN: %@";
/* No comment provided by engineer. */
"Quit" = "Salir de QuickDrop";
/* No comment provided by engineer. */
"TransferError" = "Error al recibir archivos de %@";
/* No comment provided by engineer. */
"VisibleToEveryone" = "Visible para todos";
"NoNetworkConnection" = "Sin conexión de red";
"RecommendedApps" = "Aplicaciones recomendadas";
"GetSupport" = "Soporte";
"PrivacyPolicy" = "Política de privacidad";
"ReadyToReceive" = "Listo para recibir";
"SendClipboard" = "Enviar contenido del portapapeles";
"Clipboard" = "Portapapeles";
"DeviceCurrentlySendingFiles" = "%1$@ te está enviando %2$@";
"UserManual" = "Manual de usuario y configuración";
"Settings" = "Configuración";
"AutomaticallyAcceptFiles" = "Aceptar automáticamente archivos entrantes";
"AutomaticallyAcceptFilesFooter" = "Se recomienda habilitar esta opción solo en su red privada. Cualquier persona en la misma red puede enviar archivos a su dispositivo sin su consentimiento.";
"SaveFilesTo" = "Guardar archivos entrantes en:";
"SaveFilesLocation" = "Ubicación de archivos entrantes";
"SaveFilesToButton" = "Seleccionar…";
"OpenFinderAfterReceiving" = "Mostrar archivos en Finder después de recibirlos";
"DownloadsFolder" = "Descargas";
"SendFiles" = "Enviar archivos";
"ReceiveFiles" = "Recibir archivos";
"DeviceNotShown" = "¿El dispositivo no está visible?";
"Troubleshooting" = "Solución de problemas";
"AndroidApp" = "Aplicación para Android";
"AndroidAppDescription" = "¡QuickDrop ahora también está disponible como una aplicación independiente para tu dispositivo Android! Aunque QuickDrop también puede usarse sin una aplicación de Android, QuickDrop para Android hace que la transferencia de archivos sea aún más fácil y rápida. Con la aplicación de Android, incluso puedes transferir archivos cuando no estás conectado a la red wifi de tu hogar, conectando el Mac al hotspot de tu Android. La aplicación Android sigue proporcionando el mismo cifrado de extremo a extremo al transferir archivos.
QuickDrop para Android está actualmente en versión beta. Para registrarte en la beta, envía un correo a quickdrop-beta@leonboettger.com o utiliza el botón de abajo. ¡Gracias por tu interés!";
"JoinBeta" = "Unirse a la Beta de QuickDrop para Android";
"BetaEmailBottom" = "Hola,
por favor, agrégame a la beta de QuickDrop para Android.
–––––––––––––––
Por favor, proporciona la dirección de correo electrónico de tu cuenta de Google a continuación, para que puedas ser invitado a la beta. Después, recibirás un correo electrónico con un enlace para descargar la aplicación desde Google Play Store.
Tu dirección de correo electrónico solo se utilizará para este propósito y no será almacenada ni compartida.
Tu dirección de correo electrónico: ";
"SendFilesDescription" = "Para enviar un archivo a tu dispositivo Android, haz clic en el icono de Compartir o haz clic derecho sobre el archivo y selecciona \"Compartir\". Luego, selecciona \"QuickDrop\" de la lista de aplicaciones y sigue las instrucciones.
Si no ves QuickDrop en la lista, haz clic en \"Editar extensiones\", desplázate hacia abajo y habilita la opción para QuickDrop.
También puedes enviar el contenido actual del portapapeles de tu Mac a tu dispositivo Android haciendo clic en el icono de QuickDrop en la barra de menú y seleccionando \"Enviar contenido del portapapeles\".";
"WelcomeToQuickDrop" = "Bienvenido a QuickDrop";
"Acknowledgements" = "Agradecimientos";
"SupportQuickDrop" = "Apoyar QuickDrop";
"UserManualDescription" = "Cuando QuickDrop está en ejecución, aparecerá en la barra de menús en la parte superior de tu pantalla. Haz clic en el icono de QuickDrop si deseas dejar de recibir archivos.
Para enviar archivos desde tu dispositivo Android a tu Mac, toca el icono de compartir en tu teléfono y selecciona \"Quick Share\". Tu Mac aparecerá en la lista de dispositivos disponibles. Los archivos se guardarán en la carpeta Descargas.
Importante: tu dispositivo Android y tu Mac deben estar en la misma red wifi para transferir archivos.";
"TroubleshootingDescription" = "Si tu Mac no aparece en tu dispositivo Android, aunque ambos estén conectados a la misma red wifi, o si la transmisión falló, prueba lo siguiente:
1) Abre Configuración del sistema y ve a \"Privacidad y seguridad\" > \"Red local\". Asegúrate de que QuickDrop esté habilitado. Si usas un firewall o antivirus de terceros, consulta su documentación para permitir conexiones entrantes para QuickDrop.
2) Reinicia QuickDrop haciendo clic en el icono de la barra de menús, elige la última opción y abre la aplicación nuevamente. Reinicia tu dispositivo Android.
Nota: Algunas redes no permiten la comunicación entre dispositivos. En ese caso, descarga la aplicación de QuickDrop en tu teléfono y conecta tu Mac al punto de acceso de tu teléfono para transferir archivos. El enlace a la aplicación se encuentra en la barra lateral izquierda.";
"plusview_title" = "¡Gracias por usar QuickDrop!";
"plusview_description" = "Desbloquea QuickDrop+ para seguir intercambiando archivos sin límites. QuickDrop+ es una compra única – no se requiere suscripción. Todas las funciones actuales y futuras están incluidas.";
"plusview_success_title" = "¡Gracias por tu compra!";
"plusview_success_description" = "Ahora puedes seguir recibiendo archivos sin restricciones. Todos los archivos que intentaste enviar antes ya se han guardado en tu dispositivo.";
"plusview_success_button" = "¡Entendido!";
"plusview_unlock_label" = "Apoyar QuickDrop";
"plusview_restorepurchase" = "¿Ya lo compraste?";
"plusview_restorefailed" = "Hubo un problema al restaurar tu compra";
"plusview_restorefailed_proceed" = "Aceptar";
"plusview_purchasefailed" = "La compra ha fallado";
"plusview_purchasefaileddescription" = "Si este mensaje apareció inesperadamente, asegúrate de estar conectado a internet y que las compras dentro de la aplicación estén habilitadas en tu dispositivo, luego intenta nuevamente.";
"plusview_nothingtorestore" = "¡No hay nada que restaurar!";
"plusview_productRequestFailed" = "No se pueden recuperar las compras dentro de la aplicación en este momento.";
"Connecting" = "Conectando...";
"NotEnoughSpace" = "Espacio en disco insuficiente";
"Sending" = "Enviando...";
"TransferCanceled" = "Cancelado";
"TransferDeclined" = "Rechazado";
"TransferFinished" = "Transferencia finalizada";
"TransferTimedOut" = "Tiempo de espera agotado";
"UnsupportedType" = "Tipo de archivo adjunto no compatible";
"QrCodeInstructions" = "Escanea el código QR con tu dispositivo Android para conectarlo a tu Mac. Asegúrate de que tu teléfono esté conectado a la misma red.\n\nSi no deseas escanear el código QR en cada transferencia de archivos, instala la aplicación QuickDrop (ver arriba).";
"ConnectWithQuickDropApp" = "Conectar con la aplicación QuickDrop";
"ConnectWithoutApp" = "Conectar sin aplicación";
"QrCodeInstructionsApp" = "Para conectar tu Mac a tu dispositivo Android, abre la aplicación QuickDrop en tu dispositivo Android. Asegúrate de que tu teléfono esté conectado a la misma red.\n\nPara instalar QuickDrop en tu teléfono, escanea el código QR o busca 'QuickDrop' en Google Play Store.";
"QuickDropAndroidAppAdvertisement" = "¡QuickDrop ahora también está disponible como una aplicación independiente para tu smartphone! La aplicación ofrece funciones adicionales, como la capacidad de transferir archivos cuando no estás conectado a la red wifi de tu hogar, conectando el Mac al punto de acceso de tu smartphone.\n\nPara instalar QuickDrop en tu teléfono, escanea el código QR o busca 'QuickDrop' en Google Play Store.";
"TypeNotSupported" = "Tipo no soportado";
"TypeNotSupportedDescription" = "El tipo de archivo seleccionado no es compatible. Si aún deseas compartirlo, haz clic derecho en el archivo, selecciona \"Comprimir\" y luego envía el archivo comprimido.";
"TypeNotSupportedButton" = "Vale";
"CloseAlert" = "Cerrar";
"InformDeveloper" = "Enviar informe de errores";
"TimeoutTitle" = "Algo salió mal";
"TimeoutDescription" = "QuickDrop no pudo conectarse a tu teléfono. Abre la configuración de tu dispositivo Android y busca 'Quick Share'. Cambia la opción 'Quién puede compartir contigo' a 'Todos' y vuelve a intentarlo.";
"TimeoutButton" = "Aceptar";
"ApIsolationHeader" = "Tu router podría no permitir la comunicación entre dispositivos";
"ApIsolationDescription" = "QuickDrop ha detectado que tu router wifi podría bloquear la comunicación entre dispositivos. Para transferir archivos entre tu teléfono y tu Mac, necesitarás desactivar el aislamiento de clientes (también conocido como aislamiento de AP o aislamiento wifi).
Así es como puedes desactivarlo:
1. Inicia sesión en tu router: Abre un navegador web e ingresa la dirección IP de tu router (normalmente 192.168.0.1 o 192.168.1.1), o utiliza el botón en la parte inferior de esta página. Inicia sesión con tu nombre de usuario y contraseña de administrador. (Consulta la etiqueta o el manual de tu router si no estás seguro.)
2. Encuentra la configuración: Busca en secciones como Configuración inalámbrica, Configuración avanzada o Seguridad. Busca opciones llamadas “Aislamiento de AP”, “Aislamiento de clientes”, “Aislamiento inalámbrico” o “Bloqueo Intra-BSS”.
3. Desactívala: Asegúrate de que esta configuración esté desactivada. Guarda los cambios y reinicia el router si es necesario.
Una vez desactivado, los dispositivos en tu red wifi (como tu teléfono y Mac) podrán descubrirse y conectarse entre sí. Tenga en cuenta: si está en una red de invitados, es posible que esta configuración no esté disponible. En ese caso, utilice una red diferente o descargue la aplicación QuickDrop en su teléfono y conecte su Mac al punto de acceso de su teléfono para transferir archivos. Busque “QuickDrop” en Google Play para descargar la aplicación.";
"ApIsolationAction" = "Abrir configuración del router";
"ApIsolationFindRouterFailedHeader" = "No se pudo encontrar la IP del router";
"ApIsolationFindRouterFailedDescription" = "QuickDrop no pudo determinar la dirección de tu router. Por favor, consulta el manual de usuario de tu router wifi para obtener su dirección.";
"FirewallHeader" = "Un firewall podría bloquear las transferencias de archivos entrantes";
"FirewallDescription" = "QuickDrop ha detectado que un firewall podría impedir que tu dispositivo Android se conecte a tu Mac. Los firewalls se utilizan para bloquear conexiones entrantes por defecto. Para recibir archivos, es necesario permitir las conexiones entrantes, ya que tu teléfono intenta conectarse a tu Mac, creando una conexión entrante.
Es probable que utilices un programa antivirus en tu Mac. Tienes dos opciones:
1. Permitir conexiones entrantes para QuickDrop en la configuración de tu firewall. Este procedimiento varía según el programa, por lo que es posible que debas consultar al fabricante de tu software de firewall/antivirus para saber cómo hacerlo.
2. Desactivar el firewall de tu software antivirus y utilizar en su lugar el firewall integrado del Mac. El firewall de Apple está optimizado para QuickDrop.
Así es como se desactiva el firewall de tu programa antivirus: Abre Configuración del Sistema en tu Mac, navega a Red > VPN y Filtro, y desactiva el filtro de contenido. Luego, ve a Red > Firewall y activa el firewall del Mac.
Una vez hecho esto, tu teléfono podrá conectarse a tu Mac.";
"NetworkFilterHeader" = "Un filtro de paquetes podría impedir la transferencia de archivos";
"NetworkFilterDescription" = "QuickDrop ha detectado que un programa instalado en tu Mac modifica el tráfico de red. Como resultado, el archivo que intentaste enviar desde tu dispositivo Android no pudo ser descifrado.
Es posible que tu Mac tenga habilitado un filtro de paquetes defectuoso, instalado por un antivirus (por ejemplo, ESET). Abre Preferencias del Sistema > Red > VPN y Filtro, y desactiva el filtro de contenido.
Una vez hecho esto, deberías poder transferir archivos correctamente.";
"TransmitUsingCable" = "Transferir con Cable";
"DownloadCableConnectionApp" = "Descargar iDroid";
"DownloadCableConnectionAppDescription" = "Si deseas transferir archivos y explorar el almacenamiento de tu dispositivo Android mediante USB, ahora puedes hacerlo con la nueva aplicación iDroid.\n\niDroid admite la descarga y carga de archivos y carpetas completas mediante arrastrar y soltar.";
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFiles</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@archivos@</string>
<key>file</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d archivo</string>
<key>other</key>
<string>%d archivos</string>
</dict>
</dict>
</dict>
</plist>
-193
View File
@@ -1,193 +0,0 @@
/* No comment provided by engineer. */
"Accept" = "Accepter";
/* No comment provided by engineer. */
"Decline" = "Refuser";
/* No comment provided by engineer. */
"DeviceName" = "Nom de l'appareil : %@";
/* No comment provided by engineer. */
"DeviceSendingFiles" = "%1$@ vous envoie %2$@";
"DeviceCurrentlySendingText" = "%1$@ a inséré \"%2$@\" dans votre presse-papiers.";
"DeviceSendingText" = "%1$@ souhaite insérer \"%2$@\" dans le presse-papiers de votre Mac.";
"DeviceCurrentlySendingUrl" = "%1$@ a ouvert \"%2$@\".";
"DeviceSendingUrl" = "%1$@ souhaite ouvrir \"%2$@\" sur votre Mac.";
"InsertedIntoClipboard" = "Inséré";
"UnsupportedFileType" = "Le fichier que vous avez essayé d'envoyer n'est pas pris en charge actuellement. Veuillez envoyer un rapport d'erreur.";
/* No comment provided by engineer. */
"Error.Crypto" = "Erreur de cryptage";
/* No comment provided by engineer. */
"Error.Protocol" = "Erreur de communication";
/* No comment provided by engineer. */
"NFiles" = "%d fichiers";
/* No comment provided by engineer. */
"NotificationsDenied.Message" = "QuickDrop doit pouvoir afficher des notifications pour les transferts de fichiers entrants. Veuillez autoriser les notifications dans les paramètres système.";
/* No comment provided by engineer. */
"NotificationsDenied.OpenSettings" = "Ouvrir les paramètres";
/* No comment provided by engineer. */
"NotificationsDenied.Title" = "Autorisation de notification requise";
/* No comment provided by engineer. */
"PinCode" = "CODE PIN : %@";
/* No comment provided by engineer. */
"Quit" = "Quitter QuickDrop";
/* No comment provided by engineer. */
"TransferError" = "Échec de la réception des fichiers de %@";
/* No comment provided by engineer. */
"VisibleToEveryone" = "Visible par tout le monde";
"NoNetworkConnection" = "Pas de connexion réseau";
"RecommendedApps" = "Applications recommandées";
"GetSupport" = "Support";
"PrivacyPolicy" = "Politique de confidentialité";
"ReadyToReceive" = "Prêt à recevoir";
"SendClipboard" = "Envoyer le Presse-papiers";
"Clipboard" = "Presse-papiers";
"DeviceCurrentlySendingFiles" = "%1$@ vous envoie %2$@";
"UserManual" = "Manuel d'utilisation et Paramètres";
"Settings" = "Paramètres";
"AutomaticallyAcceptFiles" = "Accepter automatiquement les fichiers entrants";
"AutomaticallyAcceptFilesFooter" = "L'activation de ce paramètre est recommandée uniquement sur votre réseau privé. Toute personne sur le même réseau peut envoyer des fichiers sur votre appareil sans votre consentement.";
"SaveFilesTo" = "Enregistrer les fichiers reçus dans :";
"SaveFilesLocation" = "Emplacement des fichiers reçus";
"SaveFilesToButton" = "Sélectionner…";
"OpenFinderAfterReceiving" = "Afficher les fichiers dans le Finder après réception";
"DownloadsFolder" = "Téléchargements";
"SendFiles" = "Envoyer des fichiers";
"ReceiveFiles" = "Recevoir des fichiers";
"DeviceNotShown" = "Appareil non affiché?";
"Troubleshooting" = "Dépannage";
"AndroidApp" = "Application Android";
"AndroidAppDescription" = "QuickDrop est désormais également disponible sous forme d'application indépendante pour votre appareil Android ! Bien que QuickDrop puisse être utilisé sans application Android, QuickDrop pour Android rend le transfert de fichiers encore plus simple et rapide. Avec l'application Android, vous pouvez même transférer des fichiers lorsque vous n'êtes pas connecté au réseau Wi-Fi de votre domicile, en connectant le Mac au hotspot de votre Android. L'application Android continue de fournir le même chiffrement de bout en bout pour le transfert de fichiers.
QuickDrop pour Android est actuellement en version bêta. Pour vous inscrire à la bêta, veuillez envoyer un e-mail à quickdrop-beta@leonboettger.com ou utiliser le bouton ci-dessous. Merci pour votre intérêt !";
"JoinBeta" = "Rejoindre la bêta de QuickDrop pour Android";
"BetaEmailBottom" = "Bonjour,
veuillez m'ajouter à la bêta de QuickDrop pour Android.
–––––––––––––––
Veuillez fournir ci-dessous l'adresse e-mail de votre compte Google, afin que vous puissiez être invité à la bêta. Ensuite, vous recevrez un e-mail avec un lien pour télécharger l'application depuis le Google Play Store.
Votre adresse e-mail sera utilisée uniquement à cette fin et ne sera ni stockée ni partagée.
Votre adresse e-mail : ";
"SendFilesDescription" = "Pour envoyer un fichier vers votre appareil Android, cliquez sur l'icône de partage ou faites un clic droit sur le fichier et sélectionnez \"Partager\". Ensuite, sélectionnez \"QuickDrop\" dans la liste des applications et suivez les instructions.
Si vous ne voyez pas QuickDrop dans la liste, cliquez sur \"Modifier les extensions\", faites défiler vers le bas et activez l'option pour QuickDrop.
Vous pouvez également envoyer le contenu actuel du presse-papiers de votre Mac à votre appareil Android en cliquant sur l'icône QuickDrop dans la barre de menu et en sélectionnant \"Envoyer le presse-papiers\".";
"WelcomeToQuickDrop" = "Bienvenue sur QuickDrop";
"Acknowledgements" = "Remerciements";
"SupportQuickDrop" = "Soutenir QuickDrop";
"UserManualDescription" = "Lorsque QuickDrop est en cours d'exécution, il apparaît dans la barre de menu en haut de votre écran. Cliquez sur l'icône QuickDrop si vous souhaitez arrêter de recevoir des fichiers.
Pour envoyer des fichiers depuis votre appareil Android vers votre Mac, appuyez sur l'icône de partage sur votre téléphone et sélectionnez \"Quick Share\". Votre Mac apparaîtra comme un appareil disponible. Les fichiers seront enregistrés dans votre dossier Téléchargements.
Important : votre appareil Android et votre Mac doivent être connectés au même réseau WiFi pour transférer des fichiers.";
"TroubleshootingDescription" = "Si votre Mac n'apparaît pas sur votre appareil Android, même si les deux appareils sont connectés au même réseau Wi-Fi, ou si la transmission a échoué, essayez ce qui suit :
1) Ouvrez les Réglages Système et allez dans \"Confidentialité et Sécurité\" > \"Réseau Local\". Assurez-vous que QuickDrop est activé. Si vous utilisez un pare-feu tiers ou un programme antivirus, consultez la documentation de ce programme pour autoriser les connexions entrantes pour QuickDrop.
2) Redémarrez QuickDrop en cliquant sur l'icône de la barre de menus, choisissez la dernière entrée et ouvrez à nouveau l'application. Redémarrez votre appareil Android.
Remarque : Certains réseaux ne permettent pas aux appareils de communiquer entre eux. Dans ce cas, vous devez télécharger l'application QuickDrop pour votre téléphone et connecter votre Mac au point d'accès de votre téléphone pour transférer des fichiers. Le lien vers l'application se trouve dans la barre latérale à gauche.";
"plusview_title" = "Merci d'utiliser QuickDrop !";
"plusview_description" = "Débloquez QuickDrop+ pour continuer à échanger des fichiers sans limites. QuickDrop+ est un achat unique – aucun abonnement requis. Toutes les fonctionnalités actuelles et futures sont incluses.";
"plusview_success_title" = "Merci pour votre achat !";
"plusview_success_description" = "Vous pouvez maintenant continuer à recevoir des fichiers sans restrictions. Tous les fichiers que vous avez essayé d'envoyer précédemment ont déjà été enregistrés sur votre appareil.";
"plusview_success_button" = "D'accord !";
"plusview_unlock_label" = "Soutenir QuickDrop";
"plusview_restorepurchase" = "Déjà acheté ?";
"plusview_restorefailed" = "Un problème est survenu lors de la restauration de votre achat";
"plusview_restorefailed_proceed" = "OK";
"plusview_purchasefailed" = "Échec de l'achat";
"plusview_purchasefaileddescription" = "Si ce message est apparu de manière inattendue, assurez-vous d'être connecté à Internet et que les achats intégrés sont activés sur votre appareil, puis réessayez.";
"plusview_nothingtorestore" = "Aucun achat à restaurer !";
"plusview_productRequestFailed" = "Impossible de récupérer les achats intégrés pour le moment.";
"Connecting" = "Connexion en cours...";
"NotEnoughSpace" = "Espace disque insuffisant";
"Sending" = "Envoi en cours...";
"TransferCanceled" = "Annulé";
"TransferDeclined" = "Refusé";
"TransferFinished" = "Transfert terminé";
"TransferTimedOut" = "Délai dépassé";
"UnsupportedType" = "Type de pièce jointe non pris en charge";
"QrCodeInstructions" = "Scannez le code QR avec votre appareil Android pour le connecter à votre Mac. Assurez-vous que votre téléphone est connecté au même réseau.\n\nSi vous ne souhaitez pas scanner le code QR à chaque transfert de fichier, installez l'application QuickDrop (voir ci-dessus).";
"ConnectWithQuickDropApp" = "Se connecter avec l'app QuickDrop";
"ConnectWithoutApp" = "Se connecter sans app";
"QrCodeInstructionsApp" = "Pour connecter votre Mac à votre appareil Android, ouvrez l'application QuickDrop sur votre appareil Android. Assurez-vous que votre téléphone est connecté au même réseau.\n\nPour installer QuickDrop sur votre téléphone, scannez le code QR ou recherchez 'QuickDrop' sur le Google Play Store.";
"QuickDropAndroidAppAdvertisement" = "QuickDrop est désormais aussi disponible en tant qu'application distincte pour votre smartphone ! L'application offre des fonctionnalités supplémentaires, comme la possibilité de transférer des fichiers même si vous n'êtes pas connecté au réseau Wi-Fi de votre domicile, en connectant le Mac au point d'accès de votre smartphone.\n\nPour installer QuickDrop sur votre téléphone, scannez le code QR ou recherchez 'QuickDrop' sur le Google Play Store.";
"TypeNotSupported" = "Type non pris en charge";
"TypeNotSupportedDescription" = "Le type de fichier sélectionné n'est pas pris en charge. Si vous souhaitez tout de même le partager, faites un clic droit sur le fichier, sélectionnez \"Compresser\" puis envoyez le fichier compressé.";
"TypeNotSupportedButton" = "D'accord";
"CloseAlert" = "Fermer";
"InformDeveloper" = "Envoyer un rapport de bug";
"TimeoutTitle" = "Un problème est survenu";
"TimeoutDescription" = "QuickDrop n’a pas pu se connecter à votre téléphone. Ouvrez les paramètres de votre appareil Android et recherchez 'Quick Share'. Modifiez le paramètre 'Qui peut partager avec vous' sur 'Tout le monde', puis réessayez.";
"TimeoutButton" = "D'accord";
"ApIsolationHeader" = "Votre routeur pourrait ne pas autoriser la communication entre appareils";
"ApIsolationDescription" = "QuickDrop a détecté que votre routeur Wi-Fi pourrait empêcher les appareils de communiquer entre eux. Pour transférer des fichiers entre votre téléphone et votre Mac, vous devez désactiver l’isolation client (également appelée isolation AP ou isolation Wi-Fi).
Voici comment la désactiver :
1. Connectez-vous à votre routeur : Ouvrez un navigateur web et entrez l’adresse IP de votre routeur (généralement 192.168.0.1 ou 192.168.1.1), ou utilisez le bouton en bas de cette page. Connectez-vous avec votre nom d’utilisateur et mot de passe administrateur. (Consultez l’étiquette ou le manuel du routeur si vous ne les connaissez pas.)
2. Trouvez le paramètre : Recherchez dans les sections \"Paramètres sans fil\", \"Paramètres avancés\" ou \"Sécurité\". Recherchez des options comme \"Isolation AP\", \"Isolation client\", \"Isolation sans fil\" ou \"Blocage Intra-BSS\".
3. Désactivez-le : Assurez-vous que ce paramètre est désactivé. Sauvegardez les modifications et redémarrez le routeur si nécessaire.
Une fois désactivée, les appareils de votre réseau Wi-Fi (comme votre téléphone et votre Mac) pourront se détecter et se connecter entre eux. Veuillez noter : si vous êtes sur un réseau invité, ce paramètre peut ne pas être disponible. Dans ce cas, utilisez un autre réseau ou téléchargez l’application QuickDrop sur votre téléphone, puis connectez votre Mac au point d’accès de votre téléphone pour transférer des fichiers. Recherchez “QuickDrop” sur Google Play pour télécharger l’application.";
"ApIsolationAction" = "Ouvrir les paramètres du routeur";
"ApIsolationFindRouterFailedHeader" = "Impossible de trouver l’adresse IP du routeur";
"ApIsolationFindRouterFailedDescription" = "QuickDrop n’a pas pu déterminer l’adresse de votre routeur. Veuillez consulter le manuel d’utilisation de votre routeur Wi-Fi pour l’obtenir.";
"FirewallHeader" = "Un pare-feu peut bloquer les transferts de fichiers entrants";
"FirewallDescription" = "QuickDrop a détecté qu'un pare-feu pourrait empêcher votre appareil Android de se connecter à votre Mac. Les pare-feu bloquent par défaut les connexions entrantes. Pour recevoir des fichiers, vous devez autoriser les connexions entrantes, car votre téléphone tente de se connecter à votre Mac – créant ainsi une connexion entrante.
Il est probable que vous utilisiez un programme antivirus sur votre Mac. Vous avez deux options :
1. Autoriser les connexions entrantes pour QuickDrop dans les paramètres de votre pare-feu. Cette procédure varie selon le programme, vous devrez donc peut-être contacter le fabricant de votre logiciel de pare-feu/antivirus pour savoir comment procéder.
2. Désactiver le pare-feu de votre logiciel antivirus et utiliser le pare-feu intégré du Mac. Le pare-feu d'Apple est optimisé pour QuickDrop.
Voici comment désactiver le pare-feu de votre programme antivirus : Ouvrez les Réglages Système sur votre Mac, allez dans Réseau > VPN et Filtrage, puis désactivez le filtre de contenu. Ensuite, allez dans Réseau > Pare-feu et activez le pare-feu du Mac.
Une fois cela fait, votre téléphone pourra se connecter à votre Mac.";
"NetworkFilterHeader" = "Un filtre de paquets peut empêcher le transfert de fichiers";
"NetworkFilterDescription" = "QuickDrop a détecté qu’un programme installé sur votre Mac modifie le trafic réseau. En conséquence, le fichier que vous avez essayé d’envoyer depuis votre appareil Android n’a pas pu être déchiffré.
Votre Mac pourrait avoir un filtre de paquets défectueux activé, installé par exemple par un antivirus (comme ESET). Ouvrez Réglages Système > Réseau > VPN et Filtrage, puis désactivez le filtre de contenu.
Une fois cela fait, vous devriez pouvoir transférer des fichiers sans problème.";
"TransmitUsingCable" = "Transférer avec Câble";
"DownloadCableConnectionApp" = "Télécharger iDroid";
"DownloadCableConnectionAppDescription" = "Si vous souhaitez transférer des fichiers et explorer le stockage de votre appareil Android via USB, vous pouvez maintenant le faire avec la nouvelle application iDroid.\n\niDroid prend en charge le téléchargement et l'envoi de fichiers et dossiers entiers avec le glisser-déposer.";
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFiles</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@files@</string>
<key>files</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d fichier</string>
<key>other</key>
<string>%d fichiers</string>
</dict>
</dict>
</dict>
</plist>
-194
View File
@@ -1,194 +0,0 @@
/* No comment provided by engineer. */
"Accept" = "Elfogad";
/* No comment provided by engineer. */
"Decline" = "Elutasít";
/* No comment provided by engineer. */
"DeviceName" = "Készülék neve: %@";
/* No comment provided by engineer. */
"DeviceSendingFiles" = "%1$@ a következőt küldi: %2$@";
"DeviceCurrentlySendingText" = "%1$@ beillesztette a(z) \"%2$@\" szöveget a vágólapodra.";
"DeviceSendingText" = "%1$@ be szeretné illeszteni a(z) \"%2$@\" szöveget a Mac-ed vágólapjára.";
"DeviceCurrentlySendingUrl" = "%1$@ megnyitotta a(z) \"%2$@\" hivatkozást.";
"DeviceSendingUrl" = "%1$@ meg szeretné nyitni a(z) \"%2$@\" hivatkozást a Mac-eden.";
"InsertedIntoClipboard" = "Beillesztve";
"UnsupportedFileType" = "A fájl, amelyet megpróbáltál küldeni, jelenleg nem támogatott. Kérjük, küldj hibajelentést.";
/* No comment provided by engineer. */
"Error.Crypto" = "Titkosítási hiba";
/* No comment provided by engineer. */
"Error.Protocol" = "Kommunikációs hiba";
/* No comment provided by engineer. */
"NFiles" = "%d fájl";
/* No comment provided by engineer. */
"NotificationsDenied.Message" = "A QuickDrop-nak szüksége van értesítéseket küldeni a bejövő fájlok fogadásához. Kérlek engedélyezd az értesítésekhez való hozzáférést a Rendszerbeállításokban.";
/* No comment provided by engineer. */
"NotificationsDenied.OpenSettings" = "Beállítások megnyitása";
/* No comment provided by engineer. */
"NotificationsDenied.Title" = "Szükséges az értesítésekhez való hozzáférés";
/* No comment provided by engineer. */
"PinCode" = "JELKÓD: %@";
/* No comment provided by engineer. */
"Quit" = "QuickDrop bezárása";
/* No comment provided by engineer. */
"TransferError" = "Nem sikerült %@-tól/től fájlokat fogadni";
/* No comment provided by engineer. */
"VisibleToEveryone" = "Mindenki számára felfedezhető";
"NoNetworkConnection" = "Nincs hálózati kapcsolat";
"RecommendedApps" = "Ajánlott alkalmazások";
"GetSupport" = "Támogatás";
"PrivacyPolicy" = "Adatvédelmi irányelvek";
"ReadyToReceive" = "Kész a fogadásra";
"SendClipboard" = "Vágólap küldése";
"Clipboard" = "Vágólap";
"DeviceCurrentlySendingFiles" = "%1$@ éppen küldi neked: %2$@";
"UserManual" = "Felhasználói kézikönyv és Beállítások";
"Settings" = "Beállítások";
"AutomaticallyAcceptFiles" = "Bejövő fájlok automatikus elfogadása";
"AutomaticallyAcceptFilesFooter" = "Ennek a beállításnak az engedélyezése csak a saját privát hálózatán ajánlott. Bárki a hálózaton belül fájlokat küldhet az eszközére az ön beleegyezése nélkül.";
"SaveFilesTo" = "Bejövő fájlok mentése ide:";
"SaveFilesLocation" = "Bejövő fájlok helye";
"SaveFilesToButton" = "Kiválasztás…";
"OpenFinderAfterReceiving" = "Fájlok megjelenítése a Finderben fogadás után";
"DownloadsFolder" = "Letöltések";
"SendFiles" = "Fájlok küldése";
"ReceiveFiles" = "Fájlok fogadása";
"DeviceNotShown" = "Az eszköz nem látható?";
"Troubleshooting" = "Hibaelhárítás";
"AndroidApp" = "Android Alkalmazás";
"AndroidAppDescription" = "A QuickDrop mostantól külön alkalmazásként is elérhető Android-eszközödre! Bár a QuickDrop alkalmazás nélkül is használható Androidon, az Androidos QuickDrop még egyszerűbbé és gyorsabbá teszi a fájlátvitelt. Az Android alkalmazással akkor is átvihetsz fájlokat, ha nem vagy csatlakozva az otthoni Wi-Fi hálózatra, csak csatlakoztasd a Mac-et az Android hotspotjához. Az alkalmazás ugyanúgy végponttól végpontig tartó titkosítást használ fájlátvitelkor.
A QuickDrop Androidra jelenleg béta verzióban érhető el. A béta verzióra való regisztráláshoz küldj e-mailt a quickdrop-beta@leonboettger.com címre, vagy használd az alábbi gombot. Köszönjük az érdeklődést!";
"JoinBeta" = "Csatlakozás a QuickDrop Android Béta verziójához";
"BetaEmailBottom" = "Szia,
kérlek, adj hozzá a QuickDrop Android béta verziójához.
–––––––––––––––
Kérlek, add meg alább a Google-fiókod e-mail címét, hogy meghívást kaphass a bétára. Ezt követően egy e-mailt fogsz kapni egy linkkel, ahonnan letöltheted az alkalmazást a Google Play Áruházból.
Az e-mail címedet kizárólag erre a célra használjuk, és nem tároljuk vagy osztjuk meg.
Az e-mail címed: ";
"SendFilesDescription" = "Fájl küldéséhez az Android-eszközödre kattints a Megosztás ikonra, vagy kattints jobb gombbal a fájlra, majd válaszd a \"Megosztás\" lehetőséget. Ezután válaszd a \"QuickDrop\"-ot az alkalmazások listájából, és kövesd az utasításokat.
Ha nem látod a QuickDrop-ot a listában, kattints a \"Bővítmények szerkesztése\" gombra, görgess lefelé, és kapcsold be a QuickDrop-ot.
A Mac vágólapjának aktuális tartalmát is elküldheted az Android-eszközödre a QuickDrop menüsáv ikonjára kattintva, majd a \"Vágólap küldése\" opció kiválasztásával.";
"WelcomeToQuickDrop" = "Üdvözlünk a QuickDropban";
"Acknowledgements" = "Köszönetnyilvánítás";
"SupportQuickDrop" = "Támogasd a QuickDropot";
"UserManualDescription" = "Amikor a QuickDrop fut, megjelenik a képernyő tetején lévő menüsorban. Kattints a QuickDrop ikonra, ha le szeretnéd állítani a fájlok fogadását.
Ha fájlokat szeretnél küldeni Android-eszközödről Mac-re, érintsd meg a megosztás ikont a telefonodon, majd válaszd a \"Quick Share\" lehetőséget. A Mac-ed megjelenik az elérhető eszközök listájában. A fájlok a Letöltések mappába kerülnek.
Fontos: Az Android-eszközödnek és a Mac-ednek ugyanazon a WiFi hálózaton kell lennie a fájlátvitelhez.";
"TroubleshootingDescription" = "Ha a Mac-ed nem jelenik meg az Android-eszközödön, annak ellenére, hogy mindkét eszköz ugyanahhoz a Wi-Fi hálózathoz csatlakozik, vagy a fájlátvitel sikertelen volt, próbáld ki az alábbiakat:
1) Nyisd meg a Rendszerbeállításokat, és menj a \"Adatvédelem és biztonság\" > \"Helyi hálózat\" menüpontra. Győződj meg róla, hogy a QuickDrop engedélyezve van. Ha harmadik féltől származó tűzfalat vagy vírusirtót használsz, ellenőrizd a program dokumentációját, hogy engedélyezve legyenek a bejövő kapcsolatok a QuickDrop számára.
2) Indítsd újra a QuickDrop-ot a menüsor ikonjára kattintva, válaszd az utolsó bejegyzést, majd nyisd meg újra az alkalmazást. Indítsd újra az Android-eszközödet is.
Megjegyzés: Egyes hálózatok nem engedélyezik az eszközök közötti kommunikációt. Ebben az esetben töltsd le a QuickDrop alkalmazást a telefonodra, és csatlakoztasd a Mac-et a telefonod hotspotjára a fájlátvitelhez. Az alkalmazás linkje a bal oldali oldalsávban található.";
"plusview_title" = "Köszönjük, hogy használod a QuickDropot!";
"plusview_description" = "Oldd fel a QuickDrop+-t, hogy korlátlanul folytathasd a fájlcserét. A QuickDrop+ egy egyszeri vásárlás – előfizetés nem szükséges. Az összes jelenlegi és jövőbeli funkciót tartalmazza.";
"plusview_success_title" = "Köszönjük a vásárlást!";
"plusview_success_description" = "Mostantól korlátozások nélkül fogadhat fájlokat. Az összes fájl, amelyet korábban próbált elküldeni, már el lett mentve az eszközére.";
"plusview_success_button" = "Értettem!";
"plusview_unlock_label" = "Támogasd a QuickDropot";
"plusview_restorepurchase" = "Már megvásároltad?";
"plusview_restorefailed" = "Hiba történt a vásárlás visszaállítása során";
"plusview_restorefailed_proceed" = "Rendben";
"plusview_purchasefailed" = "A vásárlás sikertelen";
"plusview_purchasefaileddescription" = "Ha ez az üzenet váratlanul jelent meg, ellenőrizd az internetkapcsolatot, és győződj meg róla, hogy az alkalmazáson belüli vásárlások engedélyezve vannak az eszközödön, majd próbáld újra.";
"plusview_nothingtorestore" = "Nincs visszaállítható vásárlás!";
"plusview_productRequestFailed" = "Jelenleg nem lehet lekérni az alkalmazáson belüli vásárlásokat.";
"Connecting" = "Csatlakozás...";
"NotEnoughSpace" = "Nincs elég lemezterület";
"Sending" = "Küldés...";
"TransferCanceled" = "Megszakítva";
"TransferDeclined" = "Elutasítva";
"TransferFinished" = "Átvitel befejezve";
"TransferTimedOut" = "Időtúllépés";
"UnsupportedType" = "Nem támogatott csatolmánytípus";
"QrCodeInstructions" = "Olvasd be a QR-kódot Android-eszközöddel a Mac-hez való csatlakozáshoz. Győződj meg róla, hogy a telefonod ugyanahhoz a hálózathoz csatlakozik.\n\nHa nem szeretnéd minden fájlátvitel előtt beolvasni a QR-kódot, telepítsd a QuickDrop alkalmazást (lásd fentebb).";
"ConnectWithQuickDropApp" = "Csatlakozás a QuickDrop alkalmazással";
"ConnectWithoutApp" = "Csatlakozás alkalmazás nélkül";
"QrCodeInstructionsApp" = "A Mac és az Android eszköz összekapcsolásához nyisd meg a QuickDrop alkalmazást az Android eszközödön. Győződj meg róla, hogy a telefonod ugyanahhoz a hálózathoz csatlakozik.\n\nA QuickDrop telepítéséhez a telefonodra olvasd be a QR-kódot, vagy keresd meg a 'QuickDrop' alkalmazást a Google Play Áruházban.";
"QuickDropAndroidAppAdvertisement" = "A QuickDrop mostantól különálló alkalmazásként is elérhető az okostelefonodhoz! Az alkalmazás további funkciókat kínál, például a fájlok átvitelének lehetőségét, amikor nem vagy csatlakozva az otthoni Wi-Fi hálózathoz, a Mac és az okostelefonod hotspotjának összekapcsolásával.\n\nA QuickDrop telepítéséhez a telefonodra olvasd be a QR-kódot, vagy keresd meg a 'QuickDrop' alkalmazást a Google Play Áruházban.";
"TypeNotSupported" = "Nem támogatott típus";
"TypeNotSupportedDescription" = "A kiválasztott fájltípus nem támogatott. Ha mégis meg szeretnéd osztani, kattints jobb gombbal a fájlra, válaszd a \"Tömörítés\" lehetőséget, majd küldd el a tömörített fájlt.";
"TypeNotSupportedButton" = "Rendben";
"CloseAlert" = "Bezárás";
"InformDeveloper" = "Hibajelentés küldése";
"TimeoutTitle" = "Valami hiba történt";
"TimeoutDescription" = "A QuickDrop nem tudott csatlakozni a telefonodhoz. Nyisd meg az Android beállításait, és keresd meg a 'Quick Share' beállítást. Állítsd át a 'Ki oszthat meg veled' opciót 'Mindenki'-re, majd próbáld újra.";
"TimeoutButton" = "Rendben";
"ApIsolationHeader" = "Lehet, hogy a routered nem engedélyezi az eszközök közötti kommunikációt";
"ApIsolationDescription" = "A QuickDrop észlelte, hogy a Wi-Fi routered blokkolhatja az eszközök közötti kommunikációt. Ahhoz, hogy fájlokat tudj átküldeni a telefonod és a Mac-ed között, ki kell kapcsolnod a kliens izolációt (más néven AP izoláció vagy Wi-Fi izoláció).
Így tudod kikapcsolni:
1. Jelentkezz be a routeredbe: Nyiss meg egy böngészőt, és írd be a routered IP-címét (általában 192.168.0.1 vagy 192.168.1.1), vagy használd az oldal alján található gombot. Jelentkezz be az adminisztrátori felhasználónévvel és jelszóval. (Ha nem vagy biztos benne, nézd meg a router címkéjét vagy kézikönyvét.)
2. Keresd meg a beállítást: Nézd meg a Vezeték nélküli beállítások, Haladó beállítások vagy Biztonság szekciókat. Keresd az „AP izoláció”, „Kliens izoláció”, „Vezeték nélküli izoláció” vagy „Intra-BSS blokkolás” nevű opciókat.
3. Kapcsold ki: Győződj meg róla, hogy ez a beállítás ki van kapcsolva. Mentsd el a módosításokat, és indítsd újra a routert, ha szükséges.
Miután kikapcsoltad, a Wi-Fi hálózatodon lévő eszközök (például a telefonod és a Mac-ed) képesek lesznek felfedezni és csatlakozni egymáshoz. Kérjük, vegye figyelembe: ha vendéghálózaton van, ez a beállítás nem biztos, hogy elérhető. Ebben az esetben használjon másik hálózatot, vagy töltse le a QuickDrop alkalmazást a telefonjára, és csatlakoztassa a Mac-et a telefon hotspotjához a fájlok átviteléhez. Keresse meg a „QuickDrop” alkalmazást a Google Play Áruházban a letöltéshez.";
"ApIsolationAction" = "Router beállításainak megnyitása";
"ApIsolationFindRouterFailedHeader" = "Nem sikerült megtalálni a router IP-címét";
"ApIsolationFindRouterFailedDescription" = "A QuickDrop nem tudta meghatározni a routered címét. Kérlek, nézd meg a Wi-Fi routered felhasználói kézikönyvét a cím megtudásához.";
"FirewallHeader" = "A tűzfal blokkolhatja a bejövő fájlátvitelek fogadását";
"FirewallDescription" = "A QuickDrop észlelte, hogy egy tűzfal megakadályozhatja, hogy Android-eszköze csatlakozzon a Mac számítógépéhez. A tűzfalak alapértelmezés szerint blokkolják a bejövő kapcsolatokat. A fájlok fogadásához engedélyezni kell a bejövő kapcsolatokat, mivel a telefonja megpróbál csatlakozni a Mac-hez – vagyis bejövő kapcsolatot kezdeményez.
Valószínűleg vírusvédelmi programot használ a Mac-en. Két lehetősége van:
1. Engedélyezze a bejövő kapcsolatokat a QuickDrop számára a tűzfal beállításaiban. Ez az eljárás programonként eltérhet, ezért szükség lehet arra, hogy kapcsolatba lépjen a tűzfalat vagy vírusirtót gyártó céggel a pontos útmutatásért.
2. Tiltsa le a vírusirtó szoftver tűzfalát, és használja a macOS beépített tűzfalát. Az Apple tűzfala optimalizálva van a QuickDrop használatához.
Így kapcsolhatja ki a vírusirtó tűzfalát: Nyissa meg a Rendszerbeállításokat a Mac-en, menjen a Hálózat > VPN és Szűrés menüpontra, és kapcsolja ki a tartalomszűrőt. Ezután menjen a Hálózat > Tűzfal részhez, és kapcsolja be a Mac tűzfalát.
Ezek után a telefonja csatlakozni tud majd a Mac-hez.";
"NetworkFilterHeader" = "Egy csomagszűrő megakadályozhatja a fájlátvitelt";
"NetworkFilterDescription" = "A QuickDrop észlelte, hogy egy, a Mac gépére telepített program módosítja a hálózati forgalmat. Ennek következtében az Android-eszközről küldött fájl nem volt dekódolható.
Lehet, hogy a Mac gépén egy hibás csomagszűrő van engedélyezve, amelyet egy antivírus program (pl. ESET) telepített. Nyissa meg a Rendszerbeállítások > Hálózat > VPN és Szűrés menüpontot, és kapcsolja ki a tartalomszűrőt.
Ezután a fájlátvitelnek sikeresnek kell lennie.";
"TransmitUsingCable" = "Átvitel kábellel";
"DownloadCableConnectionApp" = "iDroid letöltése";
"DownloadCableConnectionAppDescription" = "Ha fájlokat szeretnél áthelyezni és böngészni Android-eszközöd tárhelyét USB-n keresztül, most megteheted az új iDroid alkalmazással.\n\niDroid támogatja a fájlok és teljes mappák letöltését és feltöltését húzással és ejtéssel.";
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFiles</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@files@</string>
<key>files</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d fájl</string>
<key>other</key>
<string>%d fájl</string>
</dict>
</dict>
</dict>
</plist>
-193
View File
@@ -1,193 +0,0 @@
/* No comment provided by engineer. */
"Accept" = "Accetta";
/* No comment provided by engineer. */
"Decline" = "Rifiuta";
/* No comment provided by engineer. */
"DeviceName" = "Nome del dispositivo: %@";
/* No comment provided by engineer. */
"DeviceSendingFiles" = "%1$@ ti sta mandando %2$@";
"DeviceCurrentlySendingText" = "%1$@ ha inserito \"%2$@\" negli appunti.";
"DeviceSendingText" = "%1$@ vuole inserire \"%2$@\" negli appunti del tuo Mac.";
"DeviceCurrentlySendingUrl" = "%1$@ ha aperto \"%2$@\".";
"DeviceSendingUrl" = "%1$@ vuole aprire \"%2$@\" sul tuo Mac.";
"InsertedIntoClipboard" = "Inserito";
"UnsupportedFileType" = "Il file che hai provato a inviare non è attualmente supportato. Invia un rapporto di errore.";
/* No comment provided by engineer. */
"Error.Crypto" = "Errore di crittografia";
/* No comment provided by engineer. */
"Error.Protocol" = "Errore di comunicazione";
/* No comment provided by engineer. */
"NFiles" = "%d files";
/* No comment provided by engineer. */
"NotificationsDenied.Message" = "QuickDrop deve essere in grado di visualizzare le notifiche per i trasferimenti di file in entrata. Consenti le notifiche nelle Impostazioni di sistema.";
/* No comment provided by engineer. */
"NotificationsDenied.OpenSettings" = "Apri le impostazioni";
/* No comment provided by engineer. */
"NotificationsDenied.Title" = "Autorizzazione di notifica richiesta";
/* No comment provided by engineer. */
"PinCode" = "PIN: %@";
/* No comment provided by engineer. */
"Quit" = "Esci da QuickDrop";
/* No comment provided by engineer. */
"TransferError" = "Impossibile ricevere i file da %@";
/* No comment provided by engineer. */
"VisibleToEveryone" = "Visibile a tutti";
"NoNetworkConnection" = "Nessuna connessione di rete";
"RecommendedApps" = "App consigliate";
"GetSupport" = "Supporto";
"ReadyToReceive" = "Pronto a ricevere";
"PrivacyPolicy" = "Informativa sulla privacy";
"SendClipboard" = "Invia Appunti";
"Clipboard" = "Appunti";
"DeviceCurrentlySendingFiles" = "%1$@ ti sta inviando %2$@";
"UserManual" = "Manuale utente e Impostazioni";
"Settings" = "Impostazioni";
"AutomaticallyAcceptFiles" = "Accetta automaticamente i file in arrivo";
"AutomaticallyAcceptFilesFooter" = "L'abilitazione di questa impostazione è consigliata solo nella tua rete privata. Chiunque nella stessa rete può inviare file al tuo dispositivo senza il tuo consenso.";
"SaveFilesTo" = "Salva i file ricevuti in:";
"SaveFilesLocation" = "Posizione dei file ricevuti";
"SaveFilesToButton" = "Seleziona…";
"OpenFinderAfterReceiving" = "Mostra i file nel Finder dopo la ricezione";
"DownloadsFolder" = "Download";
"SendFiles" = "Invia file";
"ReceiveFiles" = "Ricevi file";
"DeviceNotShown" = "Dispositivo non mostrato?";
"Troubleshooting" = "Risoluzione dei problemi";
"AndroidApp" = "App Android";
"AndroidAppDescription" = "QuickDrop è ora disponibile anche come app separata per il tuo dispositivo Android! Anche se QuickDrop può essere utilizzato senza un'app Android, QuickDrop per Android rende il trasferimento dei file ancora più semplice e veloce. Con l'app Android, puoi persino trasferire file quando non sei connesso alla rete Wi-Fi di casa, collegando il Mac all'hotspot del tuo Android. L'app Android garantisce comunque la stessa crittografia end-to-end durante il trasferimento dei file.
QuickDrop per Android è attualmente in versione beta. Per registrarti alla beta, invia un'email a quickdrop-beta@leonboettger.com o utilizza il pulsante qui sotto. Grazie per il tuo interesse!";
"JoinBeta" = "Partecipa alla Beta di QuickDrop per Android";
"BetaEmailBottom" = "Ciao,
per favore aggiungimi alla beta di QuickDrop per Android.
–––––––––––––––
Fornisci l'indirizzo email del tuo account Google qui sotto, così potrai essere invitato alla beta. Successivamente, riceverai un'email con un link per scaricare l'app dal Google Play Store.
Il tuo indirizzo email sarà utilizzato solo per questo scopo e non sarà memorizzato o condiviso.
Il tuo indirizzo email: ";
"SendFilesDescription" = "Per inviare un file al tuo dispositivo Android, fai clic sull'icona Condividi o fai clic con il tasto destro sul file e seleziona \"Condividi\". Successivamente, seleziona \"QuickDrop\" dall'elenco delle app e segui le istruzioni.
Se non vedi QuickDrop nell'elenco, fai clic su \"Modifica Estensioni\", scorri verso il basso e abilita l'interruttore per QuickDrop.
Puoi anche inviare il contenuto attuale degli appunti del tuo Mac al tuo dispositivo Android facendo clic sull'icona della barra dei menu di QuickDrop e selezionando \"Invia Appunti\".";
"WelcomeToQuickDrop" = "Benvenuto su QuickDrop";
"Acknowledgements" = "Ringraziamenti";
"SupportQuickDrop" = "Supporta QuickDrop";
"UserManualDescription" = "Quando QuickDrop è in esecuzione, apparirà nella barra dei menu in alto sullo schermo. Fai clic sull'icona di QuickDrop se desideri interrompere la ricezione dei file.
Per inviare file dal tuo dispositivo Android al Mac, tocca l'icona di condivisione sul telefono e seleziona \"Quick Share\". Il tuo Mac verrà elencato come dispositivo disponibile. I file verranno salvati nella cartella Download.
Importante: il tuo dispositivo Android e il tuo Mac devono essere sulla stessa rete WiFi per trasferire i file.";
"TroubleshootingDescription" = "Se il tuo Mac non viene visualizzato sul dispositivo Android, anche se entrambi sono connessi alla stessa rete Wi-Fi, oppure se la trasmissione è fallita, prova quanto segue:
1) Apri le Impostazioni di Sistema e vai su \"Privacy e Sicurezza\" > \"Rete Locale\". Assicurati che QuickDrop sia abilitato. Se utilizzi un firewall o un programma antivirus di terze parti, consulta la documentazione per consentire le connessioni in entrata per QuickDrop.
2) Riavvia QuickDrop cliccando sull'icona nella barra dei menu, seleziona l'ultima voce e riapri l'app. Riavvia anche il tuo dispositivo Android.
Nota: alcune reti non permettono ai dispositivi di comunicare tra loro. In tal caso, scarica l'app QuickDrop sul telefono e collega il Mac all'hotspot del telefono per trasferire i file. Il link per l'app si trova nella barra laterale a sinistra.";
"plusview_title" = "Grazie per aver usato QuickDrop!";
"plusview_description" = "Sblocca QuickDrop+ per continuare a scambiare file senza limiti. QuickDrop+ è un acquisto unico – nessun abbonamento richiesto. Tutte le funzionalità attuali e future sono incluse.";
"plusview_success_title" = "Grazie per il tuo acquisto!";
"plusview_success_description" = "Ora puoi continuare a ricevere file senza restrizioni. Tutti i file che hai provato a inviare in precedenza sono già stati salvati sul tuo dispositivo.";
"plusview_success_button" = "Capito!";
"plusview_unlock_label" = "Supporta QuickDrop";
"plusview_restorepurchase" = "Già acquistato?";
"plusview_restorefailed" = "Si è verificato un problema durante il ripristino dell'acquisto";
"plusview_restorefailed_proceed" = "Ok";
"plusview_purchasefailed" = "Acquisto non riuscito";
"plusview_purchasefaileddescription" = "Se questo messaggio è apparso inaspettatamente, assicurati di essere connesso a Internet e che gli acquisti in-app siano abilitati sul tuo dispositivo, quindi riprova.";
"plusview_nothingtorestore" = "Non c'è nulla da ripristinare!";
"plusview_productRequestFailed" = "Impossibile recuperare gli acquisti in-app al momento.";
"Connecting" = "Connessione in corso...";
"NotEnoughSpace" = "Spazio su disco insufficiente";
"Sending" = "Invio in corso...";
"TransferCanceled" = "Annullato";
"TransferDeclined" = "Rifiutato";
"TransferFinished" = "Trasferimento completato";
"TransferTimedOut" = "Tempo scaduto";
"UnsupportedType" = "Tipo di allegato non supportato";
"QrCodeInstructions" = "Scansiona il codice QR con il tuo dispositivo Android per collegarlo al tuo Mac. Assicurati che il telefono sia connesso alla stessa rete.\n\nSe non vuoi scansionare il codice QR per ogni trasferimento di file, installa l'app QuickDrop (vedi sopra).";
"ConnectWithQuickDropApp" = "Connetti con l'app QuickDrop";
"ConnectWithoutApp" = "Connetti senza app";
"QrCodeInstructionsApp" = "Per collegare il tuo Mac al dispositivo Android, apri l'app QuickDrop sul tuo dispositivo Android. Assicurati che il telefono sia connesso alla stessa rete.\n\nPer installare QuickDrop sul tuo telefono, scansiona il codice QR o cerca 'QuickDrop' sul Google Play Store.";
"QuickDropAndroidAppAdvertisement" = "QuickDrop è ora disponibile anche come app separata per il tuo smartphone! L'app offre funzionalità aggiuntive, come la possibilità di trasferire file quando non sei connesso alla rete Wi-Fi di casa, collegando il Mac all'hotspot del tuo smartphone.\n\nPer installare QuickDrop sul tuo telefono, scansiona il codice QR o cerca 'QuickDrop' sul Google Play Store.";
"TypeNotSupported" = "Tipo non supportato";
"TypeNotSupportedDescription" = "Il tipo di file selezionato non è supportato. Se desideri comunque condividerlo, fai clic con il tasto destro sul file, seleziona \"Comprimi\" e poi invia il file compresso.";
"TypeNotSupportedButton" = "Ok";
"CloseAlert" = "Chiudi";
"InformDeveloper" = "Invia segnalazione bug";
"TimeoutTitle" = "Qualcosa è andato storto";
"TimeoutDescription" = "QuickDrop non è riuscito a connettersi al tuo telefono. Apri le impostazioni del tuo dispositivo Android e cerca 'Quick Share'. Cambia l'impostazione 'Chi può condividere con te' su 'Tutti' e riprova.";
"TimeoutButton" = "Ok";
"ApIsolationHeader" = "Il tuo router potrebbe non consentire la comunicazione tra dispositivi";
"ApIsolationDescription" = "QuickDrop ha rilevato che il tuo router Wi-Fi potrebbe bloccare la comunicazione tra dispositivi. Per trasferire file tra il tuo telefono e il tuo Mac, dovrai disabilitare l'isolamento dei client (noto anche come isolamento AP o isolamento Wi-Fi).
Ecco come disabilitarlo:
1. Accedi al tuo router: Apri un browser web e inserisci l'indirizzo IP del tuo router (di solito 192.168.0.1 o 192.168.1.1), oppure utilizza il pulsante in fondo a questa pagina. Accedi con il tuo nome utente e password di amministratore. (Controlla l'etichetta o il manuale del router se non sei sicuro.)
2. Trova l'impostazione: Cerca nelle sezioni come Impostazioni wireless, Impostazioni avanzate o Sicurezza. Cerca opzioni denominate “Isolamento AP”, “Isolamento client”, “Isolamento wireless” o “Blocco Intra-BSS”.
3. Disabilitalo: Assicurati che questa impostazione sia disattivata. Salva le modifiche e riavvia il router se necessario.
Una volta disabilitato, i dispositivi sulla tua rete Wi-Fi (come il tuo telefono e Mac) saranno in grado di scoprirsi e connettersi tra loro. Nota: se sei connesso a una rete ospite, questa impostazione potrebbe non essere disponibile. In tal caso, utilizza una rete diversa oppure scarica l’app QuickDrop sul tuo telefono e collega il tuo Mac all’hotspot del telefono per trasferire i file. Cerca “QuickDrop” su Google Play per scaricare l’app.";
"ApIsolationAction" = "Apri le impostazioni del router";
"ApIsolationFindRouterFailedHeader" = "Impossibile trovare l'IP del router";
"ApIsolationFindRouterFailedDescription" = "QuickDrop non è riuscito a determinare l'indirizzo del tuo router. Consulta il manuale utente del tuo router Wi-Fi per ottenere il suo indirizzo.";
"FirewallHeader" = "Un firewall potrebbe bloccare i trasferimenti di file in entrata";
"FirewallDescription" = "QuickDrop ha rilevato che un firewall potrebbe impedire al tuo dispositivo Android di connettersi al tuo Mac. I firewall sono utilizzati per bloccare le connessioni in entrata per impostazione predefinita. Per ricevere file, è necessario che le connessioni in entrata siano consentite. Questo perché il tuo telefono tenta di connettersi al tuo Mac, creando una connessione in entrata.
È probabile che tu stia utilizzando un programma antivirus sul tuo Mac. Hai due opzioni:
1. Consenti le connessioni in entrata per QuickDrop nelle impostazioni del tuo firewall. Questa procedura varia a seconda del programma, quindi potresti dover contattare il produttore del tuo software firewall/antivirus per sapere come procedere.
2. Disattiva il firewall del tuo software antivirus e utilizza invece il firewall integrato del Mac. Il firewall di Apple è ottimizzato per QuickDrop.
Ecco come disattivare il firewall del tuo programma antivirus: Apri le Impostazioni di Sistema sul tuo Mac, vai su Rete > VPN e Filtro, e disattiva il filtro dei contenuti. Poi, vai su Rete > Firewall e attiva il firewall del Mac.
Una volta fatto ciò, il tuo telefono sarà in grado di connettersi al tuo Mac.";
"NetworkFilterHeader" = "Un filtro di pacchetti potrebbe impedire il trasferimento di file";
"NetworkFilterDescription" = "QuickDrop ha rilevato che un programma installato sul tuo Mac modifica il traffico di rete. Di conseguenza, il file che hai provato a inviare dal tuo dispositivo Android non può essere decriptato.
È possibile che sul tuo Mac sia attivo un filtro di pacchetti difettoso, installato ad esempio da un antivirus (es. ESET). Apri Preferenze di Sistema > Rete > VPN e Filtro e disattiva il filtro dei contenuti.
Una volta fatto, dovresti essere in grado di trasferire i file correttamente.";
"TransmitUsingCable" = "Trasferisci con Cavo";
"DownloadCableConnectionApp" = "Scarica iDroid";
"DownloadCableConnectionAppDescription" = "Se desideri trasferire file e navigare nella memoria del tuo dispositivo Android tramite USB, ora puoi farlo con la nuova app iDroid.\n\niDroid supporta il download e il caricamento di file e cartelle intere tramite Drag & Drop.";
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFiles</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@files@</string>
<key>files</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d file</string>
<key>other</key>
<string>%d files</string>
</dict>
</dict>
</dict>
</plist>
-193
View File
@@ -1,193 +0,0 @@
/* No comment provided by engineer. */
"Accept" = "受け入れる";
/* No comment provided by engineer. */
"Decline" = "拒否する";
/* No comment provided by engineer. */
"DeviceName" = "機器名: %@";
/* No comment provided by engineer. */
"DeviceSendingFiles" = "%1$@が%2$@を送信しています";
"DeviceCurrentlySendingText" = "%1$@ が \"%2$@\" をクリップボードに挿入しました。";
"DeviceSendingText" = "%1$@ が \"%2$@\" をあなたの Mac のクリップボードに挿入しようとしています。";
"DeviceCurrentlySendingUrl" = "%1$@ が \"%2$@\" を開きました。";
"DeviceSendingUrl" = "%1$@ があなたの Mac で \"%2$@\" を開こうとしています。";
"InsertedIntoClipboard" = "挿入されました";
"UnsupportedFileType" = "送信しようとしたファイルは現在サポートされていません。エラーレポートを送信してください。";
/* No comment provided by engineer. */
"Error.Crypto" = "暗号化エラー";
/* No comment provided by engineer. */
"Error.Protocol" = "通信エラー";
/* No comment provided by engineer. */
"NFiles" = "%dつのファイル";
/* No comment provided by engineer. */
"NotificationsDenied.Message" = "QuickDropでファイル受信の通知を表示できる必要があります。システム設定で通知を許可してください。";
/* No comment provided by engineer. */
"NotificationsDenied.OpenSettings" = "設定を開く";
/* No comment provided by engineer. */
"NotificationsDenied.Title" = "通知権限が必要です。";
/* No comment provided by engineer. */
"PinCode" = "PIN: %@";
/* No comment provided by engineer. */
"Quit" = "QuickDropを終了";
/* No comment provided by engineer. */
"TransferError" = "%@からのファイル受信に失敗しました。";
/* No comment provided by engineer. */
"VisibleToEveryone" = "すべてのユーザーに公開";
"NoNetworkConnection" = "ネットワークに接続されていません";
"RecommendedApps" = "おすすめアプリ";
"GetSupport" = "サポート";
"PrivacyPolicy" = "プライバシーポリシー";
"ReadyToReceive" = "受信準備完了";
"SendClipboard" = "クリップボードを送信";
"Clipboard" = "クリップボード";
"DeviceCurrentlySendingFiles" = "%1$@ が %2$@ を送信中";
"UserManual" = "ユーザーマニュアルと設定";
"Settings" = "設定";
"AutomaticallyAcceptFiles" = "受信ファイルを自動的に受け入れる";
"AutomaticallyAcceptFilesFooter" = "この設定はプライベートネットワークでのみ有効にすることを推奨します。同じネットワーク上の誰でも、あなたの許可なしにファイルを送信できる可能性があります。";
"SaveFilesTo" = "受信ファイルの保存先:";
"SaveFilesLocation" = "受信ファイルの保存場所";
"SaveFilesToButton" = "選択…";
"OpenFinderAfterReceiving" = "受信後にFinderでファイルを表示";
"DownloadsFolder" = "ダウンロード";
"SendFiles" = "ファイルを送信";
"ReceiveFiles" = "ファイルを受信";
"DeviceNotShown" = "デバイスが表示されない?";
"Troubleshooting" = "トラブルシューティング";
"AndroidApp" = "Androidアプリ";
"AndroidAppDescription" = "QuickDropは、Androidデバイス用の独立したアプリとしても利用できるようになりました!AndroidアプリがなくてもQuickDropは使用できますが、Android用QuickDropを使えばファイル転送がさらに簡単かつ迅速になります。Androidアプリを使用すれば、自宅のWi-Fiに接続していなくても、MacをAndroidのホットスポットに接続してファイルを転送することができます。ファイル転送時には、Androidアプリもエンドツーエンドの暗号化を提供します。
QuickDrop for Androidは現在ベータ版です。ベータ版に登録するには、quickdrop-beta@leonboettger.com にメールを送るか、下のボタンを使用してください。ご関心ありがとうございます!";
"JoinBeta" = "Android用QuickDropベータ版に参加する";
"BetaEmailBottom" = "こんにちは、
Android用QuickDropベータ版に参加させてください。
–––––––––––––––
以下にGoogleアカウントのメールアドレスを入力してください。ベータ版に招待されます。その後、Google Playストアからアプリをダウンロードするためのリンクが記載されたメールが届きます。
あなたのメールアドレスはこの目的のみに使用され、保存または共有されることはありません。
あなたのメールアドレス:";
"SendFilesDescription" = "Androidデバイスにファイルを送信するには、共有アイコンをクリックするか、ファイルを右クリックして「共有」を選択してください。その後、アプリの一覧から「QuickDrop」を選択し、指示に従ってください。
リストにQuickDropが表示されない場合は、「拡張機能を編集」をクリックし、下にスクロールしてQuickDropを有効にしてください。
また、Macのクリップボードの内容をAndroidデバイスに送信するには、メニューバーのQuickDropアイコンをクリックし、「クリップボードを送信」を選択してください。";
"WelcomeToQuickDrop" = "QuickDropへようこそ";
"Acknowledgements" = "謝辞";
"SupportQuickDrop" = "QuickDrop をサポート";
"UserManualDescription" = "QuickDropが実行されていると、画面上部のメニューバーに表示されます。ファイルの受信を停止するには、QuickDropアイコンをクリックしてください。
AndroidデバイスからMacにファイルを送信するには、スマートフォンの共有アイコンをタップし、「Quick Share」を選択します。Macが利用可能なデバイスとして表示されます。ファイルはダウンロードフォルダに保存されます。
重要: AndroidデバイスとMacは同じWiFiネットワークに接続されている必要があります。";
"TroubleshootingDescription" = "MacとAndroidデバイスが同じWi-Fiネットワークに接続されているにもかかわらず、MacがAndroidデバイスに表示されない、または転送が失敗する場合は、以下をお試しください:
1) システム設定を開き、「プライバシーとセキュリティ」>「ローカルネットワーク」に移動します。QuickDropが有効になっていることを確認してください。サードパーティ製のファイアウォールやウイルス対策ソフトを使用している場合は、QuickDropの受信接続を許可するように設定を確認してください。
2) メニューバーのアイコンをクリックしてQuickDropを再起動し、最後の項目を選択してアプリを再度開きます。Androidデバイスも再起動してください。
注意: 一部のネットワークでは、デバイス間の通信が許可されていない場合があります。この場合、スマートフォン用のQuickDropアプリをダウンロードし、Macをスマートフォンのホットスポットに接続してファイルを転送する必要があります。アプリのリンクは左側のサイドバーにあります。";
"plusview_title" = "QuickDropをご利用いただきありがとうございます!";
"plusview_description" = "QuickDrop+ をアンロックして、無制限にファイルを交換できます。QuickDrop+ は買い切り型のアプリです。サブスクリプション不要で、現在および将来のすべての機能が含まれます。";
"plusview_success_title" = "ご購入ありがとうございます!";
"plusview_success_description" = "これで、制限なくファイルを受信し続けることができます。以前送信しようとしたファイルはすでにデバイスに保存されています。";
"plusview_success_button" = "了解!";
"plusview_unlock_label" = "QuickDropをサポート";
"plusview_restorepurchase" = "すでに購入済みですか?";
"plusview_restorefailed" = "購入の復元中に問題が発生しました";
"plusview_restorefailed_proceed" = "OK";
"plusview_purchasefailed" = "購入に失敗しました";
"plusview_purchasefaileddescription" = "このメッセージが予期せず表示された場合、インターネット接続を確認し、アプリ内購入が有効になっていることを確認した上で再試行してください。";
"plusview_nothingtorestore" = "復元するものがありません!";
"plusview_productRequestFailed" = "現在、アプリ内購入情報を取得できません。";
"Connecting" = "接続中...";
"NotEnoughSpace" = "ディスクの空き容量が不足しています";
"Sending" = "送信中...";
"TransferCanceled" = "キャンセルされました";
"TransferDeclined" = "拒否されました";
"TransferFinished" = "転送完了";
"TransferTimedOut" = "タイムアウトしました";
"UnsupportedType" = "添付ファイルの種類はサポートされていません";
"QrCodeInstructions" = "AndroidデバイスでQRコードをスキャンしてMacに接続してください。スマートフォンが同じネットワークに接続されていることを確認してください。\n\n毎回ファイル転送のたびにQRコードをスキャンしたくない場合は、QuickDropアプリをインストールしてください(上記参照)。";
"ConnectWithQuickDropApp" = "QuickDropアプリで接続";
"ConnectWithoutApp" = "アプリなしで接続";
"QrCodeInstructionsApp" = "MacをAndroidデバイスに接続するには、AndroidデバイスでQuickDropアプリを開いてください。携帯電話が同じネットワークに接続されていることを確認してください。\n\n携帯電話にQuickDropをインストールするには、QRコードをスキャンするか、Google Playストアで「QuickDrop」を検索してください。";
"QuickDropAndroidAppAdvertisement" = "QuickDropは、スマートフォン用の独立したアプリとしても利用可能になりました!このアプリは、Macをスマートフォンのホットスポットに接続することで、自宅のWi-Fiネットワークに接続されていない場合でもファイルを転送できるなどの追加機能を提供します。\n\n携帯電話にQuickDropをインストールするには、QRコードをスキャンするか、Google Playストアで「QuickDrop」を検索してください。";
"TypeNotSupported" = "サポートされていない形式";
"TypeNotSupportedDescription" = "選択したファイルの形式はサポートされていません。それでも共有したい場合は、ファイルを右クリックして「圧縮」を選択し、圧縮ファイルを送信してください。";
"TypeNotSupportedButton" = "OK";
"CloseAlert" = "閉じる";
"InformDeveloper" = "バグレポートを送信";
"TimeoutTitle" = "問題が発生しました";
"TimeoutDescription" = "QuickDropが電話に接続できませんでした。Androidの設定を開き、「クイックシェア」を検索してください。「誰が共有できるか」の設定を「全員」に変更し、再度試してください。";
"TimeoutButton" = "OK";
"ApIsolationHeader" = "ルーターがデバイス間の通信を許可していない可能性があります";
"ApIsolationDescription" = "QuickDropは、Wi-Fiルーターがデバイス同士の通信をブロックしている可能性があることを検出しました。電話とMacの間でファイルを転送するには、クライアント分離(AP分離またはWi-Fi分離とも呼ばれます)を無効にする必要があります。
無効にする方法は以下の通りです:
1. ルーターにログインする:ウェブブラウザを開き、ルーターのIPアドレス(通常は192.168.0.1または192.168.1.1)を入力するか、このページの下部にあるボタンを使用してください。管理者のユーザー名とパスワードでログインします。(不明な場合は、ルーターのラベルやマニュアルを確認してください。)
2. 設定を見つける:「ワイヤレス設定」、「詳細設定」または「セキュリティ」などのセクションを探します。「AP分離」、「クライアント分離」、「ワイヤレス分離」または「Intra-BSSブロッキング」といったオプションを探してください。
3. 無効にする:この設定がオフになっていることを確認してください。変更を保存し、必要に応じてルーターを再起動します。
無効にすると、Wi-Fiネットワーク上のデバイス(電話やMacなど)が互いに検出し、接続できるようになります。ご注意ください:ゲストネットワークを使用している場合、この設定は利用できない可能性があります。その場合は、別のネットワークを使用するか、スマートフォンにQuickDropアプリをダウンロードして、Macをスマートフォンのホットスポットに接続してファイルを転送してください。Google Playで「QuickDrop」と検索してアプリをダウンロードしてください。";
"ApIsolationAction" = "ルーター設定を開く";
"ApIsolationFindRouterFailedHeader" = "ルーターのIPアドレスが見つかりませんでした";
"ApIsolationFindRouterFailedDescription" = "QuickDropはルーターのアドレスを特定できませんでした。ルーターのユーザーマニュアルを参照してアドレスを確認してください。";
"FirewallHeader" = "ファイアウォールが受信ファイル転送をブロックする可能性があります";
"FirewallDescription" = "QuickDropは、ファイアウォールがAndroidデバイスのMacへの接続を妨げる可能性があることを検出しました。ファイアウォールはデフォルトで受信接続をブロックします。ファイルを受信するには、受信接続を許可する必要があります。これは、電話がMacに接続しようとするためで、受信接続を作成します。
おそらく、Macでウイルス対策プログラムを使用しています。2つのオプションがあります:
1. ファイアウォール設定でQuickDropの受信接続を許可します。この手順はプログラムによって異なるため、ファイアウォール/ウイルス対策ソフトウェアの製造元に方法を問い合わせる必要があります。
2. ウイルス対策ソフトウェアのファイアウォールを無効にし、代わりにMacの内蔵ファイアウォールを使用します。AppleのファイアウォールはQuickDropに最適化されています。
ウイルス対策プログラムのファイアウォールを無効にする方法は次のとおりです:Macでシステム設定を開き、ネットワーク > VPNとフィルターに移動し、コンテンツフィルターを無効にします。次に、ネットワーク > ファイアウォールに移動し、Macのファイアウォールを有効にします。
これが完了すると、電話はMacに接続できるようになります。";
"NetworkFilterHeader" = "パケットフィルターがファイル転送を妨げる可能性があります";
"NetworkFilterDescription" = "QuickDrop は、Mac にインストールされているプログラムがネットワークトラフィックを変更していることを検出しました。その結果、Android デバイスから送信しようとしたファイルを復号できませんでした。
Mac で不具合のあるパケットフィルターが有効になっている可能性があり、これは ESET などのウイルス対策ソフトによってインストールされている場合があります。システム設定 > ネットワーク > VPNとフィルター を開き、コンテンツフィルターを無効にしてください。
これで、ファイルを正常に転送できるはずです。";
"TransmitUsingCable" = "ケーブルで転送";
"DownloadCableConnectionApp" = "iDroidをダウンロード";
"DownloadCableConnectionAppDescription" = "USB経由でAndroidデバイスのファイルを転送し、ストレージを閲覧したい場合は、新しいiDroidアプリを使って実現できます。\n\niDroidはドラッグ&ドロップによるファイルやフォルダのダウンロードとアップロードをサポートしています。";
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFiles</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@files@</string>
<key>files</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%dつのファイル</string>
<key>other</key>
<string>%dつのファイル</string>
</dict>
</dict>
</dict>
</plist>
-193
View File
@@ -1,193 +0,0 @@
/* No comment provided by engineer. */
"Accept" = "수락";
/* No comment provided by engineer. */
"Decline" = "거절";
/* No comment provided by engineer. */
"DeviceName" = "기기 이름: %@";
/* No comment provided by engineer. */
"DeviceSendingFiles" = "%1$@에서 %2$@을(를) 공유하려고 합니다.";
"DeviceCurrentlySendingText" = "%1$@이(가) \"%2$@\"을(를) 클립보드에 삽입했습니다.";
"DeviceSendingText" = "%1$@이(가) \"%2$@\"을(를) Mac의 클립보드에 삽입하려고 합니다.";
"DeviceCurrentlySendingUrl" = "%1$@이(가) \"%2$@\"을(를) 열었습니다.";
"DeviceSendingUrl" = "%1$@이(가) Mac에서 \"%2$@\"을(를) 열려고 합니다.";
"InsertedIntoClipboard" = "삽입됨";
"UnsupportedFileType" = "보내려는 파일은 현재 지원되지 않습니다. 오류 보고서를 보내주세요.";
/* No comment provided by engineer. */
"Error.Crypto" = "암호화 오류";
/* No comment provided by engineer. */
"Error.Protocol" = "통신 오류";
/* No comment provided by engineer. */
"NFiles" = "%d개의 파일";
/* No comment provided by engineer. */
"NotificationsDenied.Message" = "QuickDrop에서 파일 수신에 대한 알림을 표시할 수 있어야 합니다. 시스템 설정에서 알림을 허용하세요.";
/* No comment provided by engineer. */
"NotificationsDenied.OpenSettings" = "설정 열기";
/* No comment provided by engineer. */
"NotificationsDenied.Title" = "알림 권한이 필요합니다.";
/* No comment provided by engineer. */
"PinCode" = "PIN: %@";
/* No comment provided by engineer. */
"Quit" = "QuickDrop 종료";
/* No comment provided by engineer. */
"TransferError" = "%@에서 전송한 Nearby Share가 실패했습니다.";
/* No comment provided by engineer. */
"VisibleToEveryone" = "모든 사용자에게 공개";
"NoNetworkConnection" = "네트워크에 연결되어 있지 않습니다";
"RecommendedApps" = "추천 앱";
"GetSupport" = "지원";
"PrivacyPolicy" = "개인정보 처리방침";
"ReadyToReceive" = "수신 준비 완료";
"SendClipboard" = "클립보드 보내기";
"Clipboard" = "클립보드";
"DeviceCurrentlySendingFiles" = "%1$@이(가) %2$@을(를) 보내는 중입니다";
"UserManual" = "사용자 매뉴얼 및 설정";
"Settings" = "설정";
"AutomaticallyAcceptFiles" = "수신 파일 자동 수락";
"AutomaticallyAcceptFilesFooter" = "이 설정은 개인 네트워크에서만 활성화하는 것이 좋습니다. 같은 네트워크에 있는 누구나 사용자의 동의 없이 파일을 보낼 수 있습니다.";
"SaveFilesTo" = "수신 파일 저장 위치:";
"SaveFilesLocation" = "수신 파일 위치";
"SaveFilesToButton" = "선택…";
"OpenFinderAfterReceiving" = "수신 후 Finder에서 파일 보기";
"DownloadsFolder" = "다운로드";
"SendFiles" = "파일 보내기";
"ReceiveFiles" = "파일 받기";
"DeviceNotShown" = "장치가 표시되지 않음?";
"Troubleshooting" = "문제 해결";
"AndroidApp" = "Android 앱";
"AndroidAppDescription" = "QuickDrop이 이제 Android 기기용 별도 앱으로도 제공됩니다! Android 앱 없이도 QuickDrop을 사용할 수 있지만, Android용 QuickDrop은 파일 전송을 더욱 쉽고 빠르게 해줍니다. Android 앱을 사용하면, 집의 Wi-Fi에 연결되어 있지 않아도 Android 핫스팟에 Mac을 연결해 파일을 전송할 수 있습니다. Android 앱은 파일 전송 시 동일한 종단 간 암호화를 제공합니다.
Android용 QuickDrop은 현재 베타 버전입니다. 베타에 등록하려면 quickdrop-beta@leonboettger.com 으로 이메일을 보내거나 아래 버튼을 사용하세요. 관심 가져주셔서 감사합니다!";
"JoinBeta" = "Android용 QuickDrop 베타 참여하기";
"BetaEmailBottom" = "안녕하세요,
Android용 QuickDrop 베타에 저를 추가해 주세요.
–––––––––––––––
아래에 Google 계정의 이메일 주소를 제공해 주시면 베타에 초대받을 수 있습니다. 이후 Google Play 스토어에서 앱을 다운로드할 수 있는 링크가 포함된 이메일을 받게 됩니다.
귀하의 이메일 주소는 이 목적에만 사용되며 저장되거나 공유되지 않습니다.
귀하의 이메일 주소: ";
"SendFilesDescription" = "파일을 Android 기기로 보내려면 공유 아이콘을 클릭하거나 파일을 마우스 오른쪽 버튼으로 클릭한 후 \"공유\"를 선택하세요. 그런 다음 앱 목록에서 \"QuickDrop\"을 선택하고 지시에 따라 진행하세요.
목록에 QuickDrop이 보이지 않는 경우 \"확장 프로그램 편집\"을 클릭하고 아래로 스크롤하여 QuickDrop을 활성화하세요.
또한 메뉴 막대의 QuickDrop 아이콘을 클릭하고 \"클립보드 보내기\"를 선택하여 Mac 클립보드의 현재 내용을 Android 기기로 전송할 수도 있습니다.";
"WelcomeToQuickDrop" = "QuickDrop에 오신 것을 환영합니다";
"Acknowledgements" = "감사의 말";
"SupportQuickDrop" = "QuickDrop 지원하기";
"UserManualDescription" = "QuickDrop이 실행 중이면 화면 상단의 메뉴 막대에 표시됩니다. 파일 수신을 중지하려면 QuickDrop 아이콘을 클릭하세요.
Android 기기에서 Mac으로 파일을 보내려면 휴대폰에서 공유 아이콘을 누른 후 \"Quick Share\"를 선택하세요. Mac이 사용 가능한 장치 목록에 표시됩니다. 파일은 다운로드 폴더에 저장됩니다.
중요: Android 기기와 Mac이 동일한 WiFi 네트워크에 있어야 파일을 전송할 수 있습니다.";
"TroubleshootingDescription" = "Mac과 Android 기기가 동일한 Wi-Fi 네트워크에 연결되어 있음에도 불구하고 Mac이 Android 기기에 표시되지 않거나 전송이 실패한 경우, 다음을 시도해 보세요:
1) 시스템 설정을 열고 \"개인정보 보호 및 보안\" > \"로컬 네트워크\"로 이동합니다. QuickDrop이 활성화되어 있는지 확인하세요. 타사 방화벽이나 백신 프로그램을 사용하는 경우, 해당 프로그램의 문서를 확인하여 QuickDrop의 수신 연결을 허용하세요.
2) 메뉴 막대 아이콘을 클릭하여 QuickDrop을 다시 시작하고, 마지막 항목을 선택한 후 앱을 다시 엽니다. Android 기기를 재시작하세요.
참고: 일부 네트워크에서는 기기 간 통신이 허용되지 않을 수 있습니다. 이 경우, 휴대폰용 QuickDrop 앱을 다운로드하고 Mac을 휴대폰의 핫스팟에 연결하여 파일을 전송해야 합니다. 앱 링크는 왼쪽 사이드바에서 찾을 수 있습니다.";
"plusview_title" = "QuickDrop을 사용해 주셔서 감사합니다!";
"plusview_description" = "QuickDrop+를 잠금 해제하여 파일을 제한 없이 교환하세요. QuickDrop+는 일회성 구매이며, 구독이 필요하지 않습니다. 모든 현재 및 미래 기능이 포함됩니다.";
"plusview_success_title" = "구매해 주셔서 감사합니다!";
"plusview_success_description" = "이제 파일을 제한 없이 계속 받을 수 있습니다. 이전에 전송을 시도한 모든 파일이 이미 기기에 저장되었습니다.";
"plusview_success_button" = "알겠습니다!";
"plusview_unlock_label" = "QuickDrop 지원하기";
"plusview_restorepurchase" = "이미 구매하셨나요?";
"plusview_restorefailed" = "구매 복원 중 문제가 발생했습니다.";
"plusview_restorefailed_proceed" = "확인";
"plusview_purchasefailed" = "구매 실패";
"plusview_purchasefaileddescription" = "이 메시지가 예상치 않게 나타난 경우, 인터넷에 연결되어 있는지 확인하고 기기에서 인앱 결제가 활성화되어 있는지 확인한 후 다시 시도해 주세요.";
"plusview_nothingtorestore" = "복원할 항목이 없습니다!";
"plusview_productRequestFailed" = "현재 인앱 구매 정보를 가져올 수 없습니다.";
"Connecting" = "연결 중...";
"NotEnoughSpace" = "디스크 공간 부족";
"Sending" = "보내는 중...";
"TransferCanceled" = "취소됨";
"TransferDeclined" = "거부됨";
"TransferFinished" = "전송 완료";
"TransferTimedOut" = "시간 초과";
"UnsupportedType" = "지원되지 않는 첨부 파일 유형";
"QrCodeInstructions" = "Android 기기로 QR 코드를 스캔하여 Mac에 연결하세요. 휴대폰이 동일한 네트워크에 연결되어 있는지 확인하세요.\n\n파일을 전송할 때마다 QR 코드를 스캔하고 싶지 않다면, QuickDrop 앱을 설치하세요 (위 참조).";
"ConnectWithQuickDropApp" = "QuickDrop 앱으로 연결";
"ConnectWithoutApp" = "앱 없이 연결";
"QrCodeInstructionsApp" = "Mac을 Android 기기와 연결하려면 Android 기기에서 QuickDrop 앱을 열어주세요. 휴대폰이 동일한 네트워크에 연결되어 있는지 확인하세요.\n\n휴대폰에 QuickDrop을 설치하려면 QR 코드를 스캔하거나 Google Play 스토어에서 'QuickDrop'을 검색하세요.";
"QuickDropAndroidAppAdvertisement" = "QuickDrop이 이제 스마트폰용 별도의 앱으로도 제공됩니다! 이 앱은 Mac을 스마트폰의 핫스팟에 연결하여 가정의 Wi-Fi 네트워크에 연결되어 있지 않을 때도 파일을 전송할 수 있는 기능 등 추가 기능을 제공합니다.\n\n휴대폰에 QuickDrop을 설치하려면 QR 코드를 스캔하거나 Google Play 스토어에서 'QuickDrop'을 검색하세요.";
"TypeNotSupported" = "지원되지 않는 유형";
"TypeNotSupportedDescription" = "선택한 파일 형식은 지원되지 않습니다. 그래도 공유하려면 파일을 오른쪽 클릭하여 '압축'을 선택한 후 압축된 파일을 보내세요.";
"TypeNotSupportedButton" = "확인";
"CloseAlert" = "닫기";
"InformDeveloper" = "버그 보고 보내기";
"TimeoutTitle" = "문제가 발생했습니다";
"TimeoutDescription" = "QuickDrop이 휴대전화에 연결할 수 없습니다. Android 설정을 열고 '퀵 공유'를 검색하세요. '누가 나와 공유할 수 있나요?' 설정을 '모두'로 변경하고 다시 시도하세요.";
"TimeoutButton" = "확인";
"ApIsolationHeader" = "라우터가 장치 간 통신을 허용하지 않을 수 있습니다";
"ApIsolationDescription" = "QuickDrop은 Wi-Fi 라우터가 장치 간 통신을 차단할 수 있음을 감지했습니다. 휴대폰과 Mac 간에 파일을 전송하려면 클라이언트 격리(또는 AP 격리, Wi-Fi 격리라고도 함)를 비활성화해야 합니다.
비활성화하는 방법은 다음과 같습니다:
1. 라우터에 로그인: 웹 브라우저를 열고 라우터의 IP 주소(일반적으로 192.168.0.1 또는 192.168.1.1)를 입력하거나 이 페이지 하단의 버튼을 사용하세요. 관리자 사용자 이름과 비밀번호로 로그인합니다. (확실하지 않은 경우 라우터 라벨이나 설명서를 확인하세요.)
2. 설정 찾기: 무선 설정, 고급 설정 또는 보안과 같은 섹션을 찾아보세요. \"AP 격리\", \"클라이언트 격리\", \"무선 격리\" 또는 \"Intra-BSS 차단\"이라는 옵션을 찾으세요.
3. 비활성화: 이 설정이 꺼져 있는지 확인하세요. 변경 사항을 저장하고 필요에 따라 라우터를 재부팅하세요.
비활성화하면 Wi-Fi 네트워크의 장치(예: 휴대폰과 Mac)가 서로를 발견하고 연결할 수 있습니다. 참고: 게스트 네트워크에 연결되어 있는 경우 이 설정을 사용할 수 없을 수 있습니다. 이 경우 다른 네트워크를 사용하거나 휴대폰에 QuickDrop 앱을 다운로드하고 Mac을 휴대폰의 핫스팟에 연결하여 파일을 전송하세요. Google Play에서 “QuickDrop”을 검색하여 앱을 다운로드하세요.";
"ApIsolationAction" = "라우터 설정 열기";
"ApIsolationFindRouterFailedHeader" = "라우터 IP를 찾을 수 없습니다";
"ApIsolationFindRouterFailedDescription" = "QuickDrop이 라우터의 주소를 확인할 수 없습니다. 라우터의 사용자 설명서를 참조하여 주소를 확인하세요.";
"FirewallHeader" = "방화벽이 수신 파일 전송을 차단할 수 있습니다";
"FirewallDescription" = "QuickDrop은 방화벽이 Android 기기가 Mac에 연결하는 것을 방해할 수 있음을 감지했습니다. 방화벽은 기본적으로 수신 연결을 차단합니다. 파일을 수신하려면 수신 연결을 허용해야 합니다. 이는 휴대폰이 Mac에 연결하려고 시도하기 때문입니다 – 수신 연결을 생성합니다.
Mac에서 바이러스 백신 프로그램을 사용하고 있을 가능성이 높습니다. 두 가지 옵션이 있습니다:
1. 방화벽 설정에서 QuickDrop에 대한 수신 연결을 허용하십시오. 이 절차는 프로그램마다 다르므로 방화벽/바이러스 백신 소프트웨어 제조업체에 문의하여 방법을 알아보십시오.
2. 바이러스 백신 소프트웨어의 방화벽을 비활성화하고 대신 Mac의 내장 방화벽을 사용하십시오. Apple의 방화벽은 QuickDrop에 최적화되어 있습니다.
바이러스 백신 프로그램의 방화벽을 비활성화하는 방법은 다음과 같습니다: Mac에서 시스템 설정을 열고 네트워크 > VPN 및 필터로 이동하여 콘텐츠 필터를 비활성화합니다. 그런 다음 네트워크 > 방화벽으로 이동하여 Mac의 방화벽을 활성화합니다.
이 작업이 완료되면 휴대폰이 Mac에 연결할 수 있습니다.";
"NetworkFilterHeader" = "패킷 필터가 파일 전송을 방해할 수 있습니다";
"NetworkFilterDescription" = "QuickDrop은 Mac에 설치된 프로그램이 네트워크 트래픽을 수정하고 있음을 감지했습니다. 이로 인해 Android 기기에서 보낸 파일을 복호화할 수 없습니다.
Mac에 문제가 있는 패킷 필터가 활성화되어 있을 수 있으며, 이는 ESET과 같은 백신 프로그램이 설치했을 수 있습니다. 시스템 설정 > 네트워크 > VPN 및 필터로 이동하여 콘텐츠 필터를 비활성화하세요.
이 작업이 완료되면 파일 전송이 정상적으로 이루어질 것입니다.";
"TransmitUsingCable" = "케이블로 전송";
"DownloadCableConnectionApp" = "iDroid 다운로드";
"DownloadCableConnectionAppDescription" = "USB를 통해 Android 기기의 파일을 전송하고 저장소를 탐색하려면, 이제 새롭게 출시된 iDroid 앱을 사용해보세요.\n\niDroid는 드래그 앤 드롭으로 전체 폴더 및 파일의 다운로드와 업로드를 지원합니다.";
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFiles</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@files@</string>
<key>files</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d개의 파일</string>
<key>other</key>
<string>%d개의 파일</string>
</dict>
</dict>
</dict>
</plist>
-193
View File
@@ -1,193 +0,0 @@
/* No comment provided by engineer. */
"Accept" = "Accepteer";
/* No comment provided by engineer. */
"Decline" = "Afwijzen";
/* No comment provided by engineer. */
"DeviceName" = "Naam apparaat: %@";
/* No comment provided by engineer. */
"DeviceSendingFiles" = "%1$@ stuurt je %2$@";
"DeviceCurrentlySendingText" = "%1$@ heeft \"%2$@\" in je klembord geplaatst.";
"DeviceSendingText" = "%1$@ wil \"%2$@\" in het klembord van je Mac plaatsen.";
"DeviceCurrentlySendingUrl" = "%1$@ heeft \"%2$@\" geopend.";
"DeviceSendingUrl" = "%1$@ wil \"%2$@\" openen op je Mac.";
"InsertedIntoClipboard" = "Ingevoerd";
"UnsupportedFileType" = "Het bestand dat je probeerde te verzenden, wordt momenteel niet ondersteund. Stuur een foutrapport.";
/* No comment provided by engineer. */
"Error.Crypto" = "Fout bij codering";
/* No comment provided by engineer. */
"Error.Protocol" = "Communicatiefout";
/* No comment provided by engineer. */
"NFiles" = "%d bestanden";
/* No comment provided by engineer. */
"NotificationsDenied.Message" = "QuickDrop moet meldingen kunnen weergeven voor inkomende bestandsoverdrachten. Sta meldingen toe in de systeeminstellingen.";
/* No comment provided by engineer. */
"NotificationsDenied.OpenSettings" = "Instellingen openen";
/* No comment provided by engineer. */
"NotificationsDenied.Title" = "Melding Toestemming vereist";
/* No comment provided by engineer. */
"PinCode" = "PIN: %@";
/* No comment provided by engineer. */
"Quit" = "QuickDrop afsluiten";
/* No comment provided by engineer. */
"TransferError" = "Mislukt om bestanden te ontvangen van %@";
/* No comment provided by engineer. */
"VisibleToEveryone" = "Zichtbaar voor iedereen";
"NoNetworkConnection" = "Geen netwerkverbinding";
"RecommendedApps" = "Aanbevolen apps";
"GetSupport" = "Ondersteuning";
"PrivacyPolicy" = "Privacybeleid";
"ReadyToReceive" = "Klaar om te ontvangen";
"SendClipboard" = "Klembord Verzenden";
"Clipboard" = "Klembord";
"DeviceCurrentlySendingFiles" = "%1$@ stuurt je %2$@";
"UserManual" = "Gebruikershandleiding en Instellingen";
"Settings" = "Instellingen";
"AutomaticallyAcceptFiles" = "Inkomende bestanden automatisch accepteren";
"AutomaticallyAcceptFilesFooter" = "Het inschakelen van deze instelling wordt alleen aanbevolen in uw privé-netwerk. Iedereen in hetzelfde netwerk kan bestanden naar uw apparaat sturen zonder uw toestemming.";
"SaveFilesTo" = "Opslaan inkomende bestanden in:";
"SaveFilesLocation" = "Locatie van inkomende bestanden";
"SaveFilesToButton" = "Selecteren…";
"OpenFinderAfterReceiving" = "Toon bestanden in Finder na ontvangst";
"DownloadsFolder" = "Downloads";
"SendFiles" = "Bestanden verzenden";
"ReceiveFiles" = "Bestanden ontvangen";
"DeviceNotShown" = "Apparaat niet weergegeven?";
"Troubleshooting" = "Probleemoplossing";
"AndroidApp" = "Android-app";
"AndroidAppDescription" = "QuickDrop is nu ook beschikbaar als een aparte app voor je Android-apparaat! Hoewel QuickDrop ook zonder Android-app kan worden gebruikt, maakt QuickDrop voor Android het overzetten van bestanden nog gemakkelijker en sneller. Met de Android-app kun je zelfs bestanden overzetten wanneer je niet verbonden bent met je thuisnetwerk, door je Mac te verbinden met de hotspot van je Android. De Android-app gebruikt nog steeds end-to-end encryptie tijdens bestandsoverdracht.
QuickDrop voor Android bevindt zich momenteel in de bètafase. Om je aan te melden voor de bèta, stuur een e-mail naar quickdrop-beta@leonboettger.com of gebruik de knop hieronder. Bedankt voor je interesse!";
"JoinBeta" = "Deelnemen aan QuickDrop voor Android Bèta";
"BetaEmailBottom" = "Hallo,
voeg me alstublieft toe aan de QuickDrop voor Android bèta.
–––––––––––––––
Geef hieronder het e-mailadres van je Google-account op, zodat je kunt worden uitgenodigd voor de bèta. Daarna ontvang je een e-mail met een link om de app te downloaden vanuit de Google Play Store.
Je e-mailadres wordt alleen voor dit doel gebruikt en zal niet worden opgeslagen of gedeeld.
Je e-mailadres: ";
"SendFilesDescription" = "Om een bestand naar je Android-apparaat te sturen, klik op het Deel-icoon of klik met de rechtermuisknop op het bestand en selecteer \"Delen\". Kies vervolgens \"QuickDrop\" uit de lijst met apps en volg de instructies.
Als je QuickDrop niet in de lijst ziet, klik op \"Extensies bewerken\", scroll naar beneden en schakel QuickDrop in.
Je kunt ook de huidige inhoud van het klembord van je Mac naar je Android-apparaat sturen door op het QuickDrop-pictogram in de menubalk te klikken en \"Klembord verzenden\" te selecteren.";
"WelcomeToQuickDrop" = "Welkom bij QuickDrop";
"Acknowledgements" = "Erkenningen";
"SupportQuickDrop" = "Ondersteun QuickDrop";
"UserManualDescription" = "Wanneer QuickDrop actief is, verschijnt het in de menubalk bovenaan je scherm. Klik op het QuickDrop-icoon als je wilt stoppen met het ontvangen van bestanden.
Om bestanden van je Android-apparaat naar je Mac te sturen, tik op het deel-icoon op je telefoon en selecteer \"Quick Share\". Je Mac wordt weergegeven als een beschikbaar apparaat. Bestanden worden opgeslagen in je Downloads-map.
Belangrijk: Je Android-apparaat en Mac moeten op hetzelfde WiFi-netwerk zitten om bestanden over te dragen.";
"TroubleshootingDescription" = "Als je Mac niet verschijnt op je Android-apparaat, zelfs als beide apparaten met hetzelfde Wi-Fi-netwerk zijn verbonden, of als de overdracht is mislukt, probeer dan het volgende:
1) Open Systeeminstellingen en ga naar \"Privacy en beveiliging\" > \"Lokaal netwerk\". Zorg ervoor dat QuickDrop is ingeschakeld. Als je een firewall of antivirusprogramma van derden gebruikt, raadpleeg dan de documentatie van dat programma om inkomende verbindingen voor QuickDrop toe te staan.
2) Herstart QuickDrop door op het menubalkpictogram te klikken, kies de laatste optie en open de app opnieuw. Herstart ook je Android-apparaat.
Opmerking: Sommige netwerken staan niet toe dat apparaten met elkaar communiceren. In dat geval moet je de QuickDrop-app op je telefoon downloaden en je Mac verbinden met de hotspot van je telefoon om bestanden over te dragen. De link naar de app is te vinden in de zijbalk aan de linkerkant.";
"plusview_title" = "Bedankt voor het gebruiken van QuickDrop!";
"plusview_description" = "Ontgrendel QuickDrop+ om bestanden zonder limieten te blijven uitwisselen. QuickDrop+ is een eenmalige aankoop – geen abonnement vereist. Alle huidige en toekomstige functies zijn inbegrepen.";
"plusview_success_title" = "Bedankt voor je aankoop!";
"plusview_success_description" = "Je kunt nu bestanden blijven ontvangen zonder beperkingen. Alle bestanden die je eerder probeerde te verzenden, zijn al opgeslagen op je apparaat.";
"plusview_success_button" = "Begrepen!";
"plusview_unlock_label" = "Ondersteun QuickDrop";
"plusview_restorepurchase" = "Al gekocht?";
"plusview_restorefailed" = "Er was een probleem bij het herstellen van je aankoop";
"plusview_restorefailed_proceed" = "Oké";
"plusview_purchasefailed" = "Aankoop mislukt";
"plusview_purchasefaileddescription" = "Als dit bericht onverwachts verschijnt, controleer dan of je verbonden bent met internet en of in-app aankopen zijn ingeschakeld op je apparaat, en probeer het opnieuw.";
"plusview_nothingtorestore" = "Er is niets om te herstellen!";
"plusview_productRequestFailed" = "In-app aankopen kunnen momenteel niet worden opgehaald.";
"Connecting" = "Verbinden...";
"NotEnoughSpace" = "Onvoldoende schijfruimte";
"Sending" = "Bezig met verzenden...";
"TransferCanceled" = "Geannuleerd";
"TransferDeclined" = "Geweigerd";
"TransferFinished" = "Overdracht voltooid";
"TransferTimedOut" = "Time-out";
"UnsupportedType" = "Bijlagetype niet ondersteund";
"QrCodeInstructions" = "Scan de QR-code met je Android-apparaat om het met je Mac te verbinden. Zorg ervoor dat je telefoon met hetzelfde netwerk is verbonden.\n\nAls je niet bij elke bestandsoverdracht de QR-code wilt scannen, installeer dan de QuickDrop-app (zie hierboven).";
"ConnectWithQuickDropApp" = "Verbind met de QuickDrop-app";
"ConnectWithoutApp" = "Verbind zonder app";
"QrCodeInstructionsApp" = "Om je Mac met je Android-apparaat te verbinden, open je de QuickDrop-app op je Android-apparaat. Zorg ervoor dat je telefoon is verbonden met hetzelfde netwerk.\n\nOm QuickDrop op je telefoon te installeren, scan de QR-code of zoek naar 'QuickDrop' in de Google Play Store.";
"QuickDropAndroidAppAdvertisement" = "QuickDrop is nu ook beschikbaar als een aparte app voor je smartphone! De app biedt extra functies, zoals de mogelijkheid om bestanden over te zetten wanneer je niet bent verbonden met je thuis-Wi-Fi-netwerk, door je Mac te verbinden met de hotspot van je smartphone.\n\nOm QuickDrop op je telefoon te installeren, scan de QR-code of zoek naar 'QuickDrop' in de Google Play Store.";
"TypeNotSupported" = "Type niet ondersteund";
"TypeNotSupportedDescription" = "Het geselecteerde bestandstype wordt niet ondersteund. Als je het toch wilt delen, klik met de rechtermuisknop op het bestand, selecteer \"Comprimeren\" en stuur vervolgens het gecomprimeerde bestand.";
"TypeNotSupportedButton" = "Oké";
"CloseAlert" = "Sluiten";
"InformDeveloper" = "Foutrapport verzenden";
"TimeoutTitle" = "Er is iets misgegaan";
"TimeoutDescription" = "QuickDrop kon geen verbinding maken met je telefoon. Open de instellingen van je Android-apparaat en zoek naar 'Quick Share'. Wijzig de instelling 'Wie kan met je delen' naar 'Iedereen' en probeer het opnieuw.";
"TimeoutButton" = "Oké";
"ApIsolationHeader" = "Je router staat mogelijk geen communicatie tussen apparaten toe";
"ApIsolationDescription" = "QuickDrop heeft vastgesteld dat je Wi-Fi-router mogelijk communicatie tussen apparaten blokkeert. Om bestanden over te zetten tussen je telefoon en je Mac, moet je clientisolatie uitschakelen (ook wel AP-isolatie of Wi-Fi-isolatie genoemd).
Zo schakel je het uit:
1. Log in op je router: Open een webbrowser en voer het IP-adres van je router in (meestal 192.168.0.1 of 192.168.1.1), of gebruik de knop onderaan deze pagina. Log in met je beheerder gebruikersnaam en wachtwoord. (Controleer het etiket op je router of de handleiding als je het niet zeker weet.)
2. Zoek de instelling: Kijk in secties zoals \"Draadloze instellingen\", \"Geavanceerde instellingen\" of \"Beveiliging\". Zoek naar opties zoals \"AP-isolatie\", \"Clientisolatie\", \"Draadloze isolatie\" of \"Intra-BSS-blokkering\".
3. Schakel het uit: Zorg ervoor dat deze instelling is uitgeschakeld. Sla de wijzigingen op en herstart de router indien nodig.
Zodra dit is uitgeschakeld, kunnen apparaten op je Wi-Fi-netwerk (zoals je telefoon en Mac) elkaar vinden en verbinden. Let op: als u zich op een gastnetwerk bevindt, is deze instelling mogelijk niet beschikbaar. Gebruik in dat geval een ander netwerk of download de QuickDrop-app op uw telefoon en verbind uw Mac met de hotspot van uw telefoon om bestanden over te dragen. Zoek naar “QuickDrop” in de Google Play Store om de app te downloaden.";
"ApIsolationAction" = "Routerinstellingen openen";
"ApIsolationFindRouterFailedHeader" = "Kon router-IP niet vinden";
"ApIsolationFindRouterFailedDescription" = "QuickDrop kon het adres van je router niet bepalen. Raadpleeg de gebruikershandleiding van je Wi-Fi-router om het adres te vinden.";
"FirewallHeader" = "Een firewall kan inkomende bestandsoverdrachten blokkeren";
"FirewallDescription" = "QuickDrop heeft vastgesteld dat een firewall mogelijk verhindert dat je Android-apparaat verbinding maakt met je Mac. Firewalls worden standaard gebruikt om inkomende verbindingen te blokkeren. Om bestanden te ontvangen, moeten inkomende verbindingen worden toegestaan, omdat je telefoon probeert verbinding te maken met je Mac – wat een inkomende verbinding creëert.
Je gebruikt waarschijnlijk een antivirusprogramma op je Mac. Je hebt twee opties:
1. Sta inkomende verbindingen toe voor QuickDrop in je firewallinstellingen. Deze procedure varieert per programma, dus je moet mogelijk de fabrikant van je firewall-/antivirussoftware raadplegen over hoe dit moet worden gedaan.
2. Schakel de firewall van je antivirussoftware uit en gebruik in plaats daarvan de ingebouwde firewall van de Mac. De firewall van Apple is geoptimaliseerd voor QuickDrop.
Zo schakel je de firewall van je antivirusprogramma uit: Open Systeeminstellingen op je Mac, navigeer naar Netwerk > VPN en Filter, en schakel het inhoudsfilter uit. Ga vervolgens naar Netwerk > Firewall en schakel de firewall van de Mac in.
Zodra dit is gedaan, kan je telefoon verbinding maken met je Mac.";
"NetworkFilterHeader" = "Een pakketfilter kan bestandoverdracht verhinderen";
"NetworkFilterDescription" = "QuickDrop heeft vastgesteld dat een programma op je Mac het netwerkverkeer wijzigt. Hierdoor kon het bestand dat je probeerde te verzenden vanaf je Android-apparaat niet worden ontsleuteld.
Je Mac heeft mogelijk een defect pakketfilter ingeschakeld, mogelijk geïnstalleerd door antivirussoftware (zoals ESET). Ga naar Systeeminstellingen > Netwerk > VPN en Filter en schakel het inhoudsfilter uit.
Daarna zou je bestanden succesvol moeten kunnen overdragen.";
"TransmitUsingCable" = "Overdragen met Kabel";
"DownloadCableConnectionApp" = "Download iDroid";
"DownloadCableConnectionAppDescription" = "Als je bestanden wilt overzetten en de opslag van je Android-apparaat via USB wilt verkennen, kan dat nu met de nieuwe iDroid-app.\n\niDroid ondersteunt het downloaden en uploaden van bestanden en volledige mappen via slepen en neerzetten.";
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFiles</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@files@</string>
<key>files</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d bestand</string>
<key>other</key>
<string>%d bestanden</string>
</dict>
</dict>
</dict>
</plist>
-193
View File
@@ -1,193 +0,0 @@
/* No comment provided by engineer. */
"Accept" = "Aceitar";
/* No comment provided by engineer. */
"Decline" = "Recusar";
/* No comment provided by engineer. */
"DeviceName" = "Nome do dispositivo: %@";
/* No comment provided by engineer. */
"DeviceSendingFiles" = "%1$@ está te enviando %2$@";
"DeviceCurrentlySendingText" = "%1$@ inseriu \"%2$@\" na sua área de transferência.";
"DeviceSendingText" = "%1$@ deseja inserir \"%2$@\" na área de transferência do seu Mac.";
"DeviceCurrentlySendingUrl" = "%1$@ abriu \"%2$@\".";
"DeviceSendingUrl" = "%1$@ deseja abrir \"%2$@\" no seu Mac.";
"InsertedIntoClipboard" = "Inserido";
"UnsupportedFileType" = "O arquivo que você tentou enviar não é compatível no momento. Por favor, envie um relatório de erro.";
/* No comment provided by engineer. */
"Error.Crypto" = "Erro de encriptação";
/* No comment provided by engineer. */
"Error.Protocol" = "Erro de comunicação";
/* No comment provided by engineer. */
"NFiles" = "%d arquivos";
/* No comment provided by engineer. */
"NotificationsDenied.Message" = "O QuickDrop precisa ser capaz de exibir notificações para o recebimento de arquivos. Por favor, habilite as notificações em Ajustes do Sistema.";
/* No comment provided by engineer. */
"NotificationsDenied.OpenSettings" = "Abrir configurações";
/* No comment provided by engineer. */
"NotificationsDenied.Title" = "Necessário permissão de notificações";
/* No comment provided by engineer. */
"PinCode" = "PIN: %@";
/* No comment provided by engineer. */
"Quit" = "Sair do QuickDrop";
/* No comment provided by engineer. */
"TransferError" = "Falha ao receber arquivos de %@";
/* No comment provided by engineer. */
"VisibleToEveryone" = "Visível para todos";
"NoNetworkConnection" = "Sem conexão com a rede";
"RecommendedApps" = "Apps recomendados";
"GetSupport" = "Suporte";
"PrivacyPolicy" = "Política de Privacidade";
"ReadyToReceive" = "Pronto para receber";
"SendClipboard" = "Enviar Área de Transferência";
"Clipboard" = "Área de Transferência";
"DeviceCurrentlySendingFiles" = "%1$@ está enviando %2$@ para você";
"UserManual" = "Manual do Usuário e Configurações";
"Settings" = "Configurações";
"AutomaticallyAcceptFiles" = "Aceitar automaticamente arquivos recebidos";
"AutomaticallyAcceptFilesFooter" = "Ativar esta configuração é recomendado apenas na sua rede privada. Qualquer pessoa na mesma rede pode enviar arquivos para o seu dispositivo sem o seu consentimento.";
"SaveFilesTo" = "Salvar arquivos recebidos em:";
"SaveFilesLocation" = "Localização dos arquivos recebidos";
"SaveFilesToButton" = "Selecionar…";
"OpenFinderAfterReceiving" = "Mostrar arquivos no Finder após o recebimento";
"DownloadsFolder" = "Downloads";
"SendFiles" = "Enviar arquivos";
"ReceiveFiles" = "Receber arquivos";
"DeviceNotShown" = "Dispositivo não mostrado?";
"Troubleshooting" = "Solução de problemas";
"AndroidApp" = "Aplicativo Android";
"AndroidAppDescription" = "O QuickDrop agora também está disponível como um aplicativo separado para o seu dispositivo Android! Embora o QuickDrop possa ser usado sem um app Android, o QuickDrop para Android torna a transferência de arquivos ainda mais fácil e rápida. Com o app Android, você pode até transferir arquivos quando não estiver conectado à rede Wi-Fi da sua casa, conectando o Mac ao hotspot do seu Android. O app Android ainda oferece a mesma criptografia de ponta a ponta ao transferir arquivos.
O QuickDrop para Android está atualmente em beta. Para se registrar na versão beta, envie um e-mail para quickdrop-beta@leonboettger.com ou use o botão abaixo. Obrigado pelo seu interesse!";
"JoinBeta" = "Participar da Beta do QuickDrop para Android";
"BetaEmailBottom" = "Olá,
por favor, me adicione à beta do QuickDrop para Android.
–––––––––––––––
Por favor, forneça o endereço de e-mail da sua conta do Google abaixo, para que você possa ser convidado para a beta. Depois, você receberá um e-mail com um link para baixar o aplicativo na Google Play Store.
Seu endereço de e-mail será usado apenas para esse propósito e não será armazenado ou compartilhado.
Seu endereço de e-mail: ";
"SendFilesDescription" = "Para enviar um arquivo para seu dispositivo Android, clique no ícone de Compartilhar ou clique com o botão direito no arquivo e selecione \"Compartilhar\". Em seguida, selecione \"QuickDrop\" na lista de aplicativos e siga as instruções.
Se não encontrar o QuickDrop na lista, clique em \"Editar Extensões\", role para baixo e ative a opção para QuickDrop.
Você também pode enviar o conteúdo atual da área de transferência do seu Mac para seu dispositivo Android clicando no ícone do QuickDrop na barra de menu e selecionando \"Enviar Área de Transferência\".";
"WelcomeToQuickDrop" = "Bem-vindo ao QuickDrop";
"Acknowledgements" = "Agradecimentos";
"SupportQuickDrop" = "Apoie o QuickDrop";
"UserManualDescription" = "Quando o QuickDrop estiver em execução, ele aparecerá na barra de menus na parte superior da tela. Clique no ícone do QuickDrop se quiser parar de receber arquivos.
Para enviar arquivos do seu dispositivo Android para o Mac, toque no ícone de compartilhamento no celular e selecione \"Quick Share\". Seu Mac será listado como um dispositivo disponível. Os arquivos serão salvos na pasta Downloads.
Importante: Seu dispositivo Android e seu Mac precisam estar na mesma rede WiFi para transferir arquivos.";
"TroubleshootingDescription" = "Se o seu Mac não aparecer no seu dispositivo Android, mesmo estando ambos conectados à mesma rede Wi-Fi, ou se a transmissão falhar, tente o seguinte:
1) Abra as Configurações do Sistema e vá para \"Privacidade e Segurança\" > \"Rede Local\". Certifique-se de que o QuickDrop está ativado. Se você estiver usando um firewall ou antivírus de terceiros, verifique a documentação do programa para permitir conexões de entrada para o QuickDrop.
2) Reinicie o QuickDrop clicando no ícone da barra de menu, escolha a última opção e abra o aplicativo novamente. Reinicie também o seu dispositivo Android.
Observação: Algumas redes não permitem que os dispositivos se comuniquem entre si. Nesse caso, baixe o aplicativo QuickDrop no seu telefone e conecte seu Mac ao ponto de acesso do telefone para transferir arquivos. O link para o aplicativo pode ser encontrado na barra lateral à esquerda.";
"plusview_title" = "Obrigado por usar o QuickDrop!";
"plusview_description" = "Desbloqueie o QuickDrop+ para continuar trocando arquivos sem limites. QuickDrop+ é uma compra única – sem necessidade de assinatura. Todos os recursos atuais e futuros estão incluídos.";
"plusview_success_title" = "Obrigado pela sua compra!";
"plusview_success_description" = "Agora você pode continuar recebendo arquivos sem restrições. Todos os arquivos que você tentou enviar antes já foram salvos no seu dispositivo.";
"plusview_success_button" = "Entendi!";
"plusview_unlock_label" = "Apoie o QuickDrop";
"plusview_restorepurchase" = "Já comprou?";
"plusview_restorefailed" = "Houve um problema ao restaurar sua compra";
"plusview_restorefailed_proceed" = "OK";
"plusview_purchasefailed" = "Falha na compra";
"plusview_purchasefaileddescription" = "Se esta mensagem apareceu inesperadamente, verifique se você está conectado à internet e se as compras dentro do app estão ativadas no seu dispositivo e tente novamente.";
"plusview_nothingtorestore" = "Nada para restaurar!";
"plusview_productRequestFailed" = "Não foi possível recuperar as compras dentro do aplicativo no momento.";
"Connecting" = "Conectando...";
"NotEnoughSpace" = "Espaço em disco insuficiente";
"Sending" = "Enviando...";
"TransferCanceled" = "Cancelado";
"TransferDeclined" = "Recusado";
"TransferFinished" = "Transferência concluída";
"TransferTimedOut" = "Tempo esgotado";
"UnsupportedType" = "Tipo de anexo não suportado";
"QrCodeInstructions" = "Escaneie o código QR com seu dispositivo Android para conectá-lo ao seu Mac. Certifique-se de que o telefone esteja conectado à mesma rede.\n\nSe não quiser escanear o QR Code em toda transferência, instale o aplicativo QuickDrop (veja acima).";
"ConnectWithQuickDropApp" = "Conectar com o aplicativo QuickDrop";
"ConnectWithoutApp" = "Conectar sem aplicativo";
"QrCodeInstructionsApp" = "Para conectar seu Mac ao dispositivo Android, abra o aplicativo QuickDrop no seu dispositivo Android. Certifique-se de que seu telefone esteja conectado à mesma rede.\n\nPara instalar o QuickDrop no seu telefone, escaneie o código QR ou procure por 'QuickDrop' na Google Play Store.";
"QuickDropAndroidAppAdvertisement" = "O QuickDrop agora também está disponível como um aplicativo separado para o seu smartphone! O aplicativo oferece recursos adicionais, como a capacidade de transferir arquivos quando você não está conectado à rede Wi-Fi da sua casa, conectando o Mac ao hotspot do seu smartphone.\n\nPara instalar o QuickDrop no seu telefone, escaneie o código QR ou procure por 'QuickDrop' na Google Play Store.";
"TypeNotSupported" = "Tipo não suportado";
"TypeNotSupportedDescription" = "O tipo de arquivo selecionado não é suportado. Se quiser compartilhá-lo mesmo assim, clique com o botão direito no arquivo, selecione \"Comprimir\" e envie o arquivo compactado.";
"TypeNotSupportedButton" = "Ok";
"CloseAlert" = "Fechar";
"InformDeveloper" = "Enviar relatório de erro";
"TimeoutTitle" = "Algo deu errado";
"TimeoutDescription" = "O QuickDrop não conseguiu se conectar ao seu telefone. Abra as configurações do seu dispositivo Android e procure por 'Quick Share'. Altere a configuração 'Quem pode compartilhar com você' para 'Todos' e tente novamente.";
"TimeoutButton" = "Ok";
"ApIsolationHeader" = "Seu roteador pode não permitir comunicação entre dispositivos";
"ApIsolationDescription" = "O QuickDrop detectou que seu roteador Wi-Fi pode estar bloqueando a comunicação entre dispositivos. Para transferir arquivos entre seu telefone e seu Mac, será necessário desativar o isolamento de clientes (também conhecido como isolamento AP ou isolamento Wi-Fi).
Veja como desativá-lo:
1. Acesse seu roteador: Abra um navegador da web e digite o endereço IP do seu roteador (geralmente 192.168.0.1 ou 192.168.1.1), ou utilize o botão na parte inferior desta página. Faça login com seu nome de usuário e senha de administrador. (Consulte a etiqueta ou o manual do roteador se não tiver certeza.)
2. Encontre a configuração: Procure em seções como Configurações sem fio, Configurações avançadas ou Segurança. Procure por opções chamadas “Isolamento AP”, “Isolamento de Clientes”, “Isolamento Sem Fio” ou “Bloqueio Intra-BSS”.
3. Desative-a: Certifique-se de que essa configuração esteja desativada. Salve as alterações e reinicie o roteador, se necessário.
Uma vez desativado, os dispositivos na sua rede Wi-Fi (como seu telefone e Mac) poderão se descobrir e se conectar entre si. Observe: se você estiver em uma rede de convidados, essa configuração pode não estar disponível. Nesse caso, use uma rede diferente ou baixe o aplicativo QuickDrop no seu telefone e conecte seu Mac ao ponto de acesso do telefone para transferir arquivos. Procure por “QuickDrop” no Google Play para baixar o aplicativo.";
"ApIsolationAction" = "Abrir configurações do roteador";
"ApIsolationFindRouterFailedHeader" = "Não foi possível encontrar o IP do roteador";
"ApIsolationFindRouterFailedDescription" = "O QuickDrop não conseguiu determinar o endereço do seu roteador. Consulte o manual do usuário do seu roteador Wi-Fi para obter o endereço.";
"FirewallHeader" = "Um firewall pode bloquear transferências de arquivos recebidos";
"FirewallDescription" = "O QuickDrop detectou que um firewall pode impedir que seu dispositivo Android se conecte ao seu Mac. Firewalls são usados para bloquear conexões de entrada por padrão. Para receber arquivos, é necessário permitir conexões de entrada, pois seu telefone tenta se conectar ao seu Mac – criando uma conexão de entrada.
É provável que você esteja usando um programa antivírus no seu Mac. Você tem duas opções:
1. Permitir conexões de entrada para o QuickDrop nas configurações do seu firewall. Esse procedimento varia de acordo com o programa, portanto, você pode precisar consultar o fabricante do seu software de firewall/antivírus sobre como fazer isso.
2. Desativar o firewall do seu software antivírus e usar o firewall integrado do Mac. O firewall da Apple é otimizado para o QuickDrop.
Veja como desativar o firewall do seu programa antivírus: Abra as Configurações do Sistema no seu Mac, vá para Rede > VPN e Filtro, e desative o filtro de conteúdo. Em seguida, vá para Rede > Firewall e ative o firewall do Mac.
Depois de feito isso, seu telefone poderá se conectar ao seu Mac.";
"NetworkFilterHeader" = "Um filtro de pacotes pode impedir a transferência de arquivos";
"NetworkFilterDescription" = "O QuickDrop detectou que um programa instalado no seu Mac está modificando o tráfego de rede. Como resultado, o arquivo que você tentou enviar do seu dispositivo Android não pôde ser descriptografado.
Pode haver um filtro de pacotes com defeito ativado no seu Mac, possivelmente instalado por um antivírus (por exemplo, ESET). Abra Preferências do Sistema > Rede > VPN e Filtro e desative o filtro de conteúdo.
Depois disso, você deve conseguir transferir arquivos com sucesso.";
"TransmitUsingCable" = "Transferir com Cabo";
"DownloadCableConnectionApp" = "Baixar iDroid";
"DownloadCableConnectionAppDescription" = "Se você deseja transferir arquivos e navegar no armazenamento do seu dispositivo Android via USB, agora pode fazer isso com o novo app iDroid.\n\niDroid suporta download e upload de arquivos e pastas inteiras usando Arrastar e Soltar.";
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFiles</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@files@</string>
<key>files</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d file</string>
<key>other</key>
<string>%d files</string>
</dict>
</dict>
</dict>
</plist>
-193
View File
@@ -1,193 +0,0 @@
/* No comment provided by engineer. */
"Accept" = "Aceitar";
/* No comment provided by engineer. */
"Decline" = "Recusar";
/* No comment provided by engineer. */
"DeviceName" = "Nome do dispositivo: %@";
/* No comment provided by engineer. */
"DeviceSendingFiles" = "%1$@ está a enviar %2$@";
"DeviceCurrentlySendingText" = "%1$@ inseriu \"%2$@\" na sua área de transferência.";
"DeviceSendingText" = "%1$@ pretende inserir \"%2$@\" na área de transferência do seu Mac.";
"DeviceCurrentlySendingUrl" = "%1$@ abriu \"%2$@\".";
"DeviceSendingUrl" = "%1$@ pretende abrir \"%2$@\" no seu Mac.";
"InsertedIntoClipboard" = "Inserido";
"UnsupportedFileType" = "O ficheiro que tentou enviar não é atualmente suportado. Por favor, envie um relatório de erro.";
/* No comment provided by engineer. */
"Error.Crypto" = "Erro de encriptação";
/* No comment provided by engineer. */
"Error.Protocol" = "Erro de comunicação";
/* No comment provided by engineer. */
"NFiles" = "%d ficheiros";
/* No comment provided by engineer. */
"NotificationsDenied.Message" = "O QuickDrop precisa ser capaz de mostrar notificações para receber ficheiros. Por favor, active as notificações em Definições do Sistema.";
/* No comment provided by engineer. */
"NotificationsDenied.OpenSettings" = "Abrir definições";
/* No comment provided by engineer. */
"NotificationsDenied.Title" = "Necessário permissão de notificações";
/* No comment provided by engineer. */
"PinCode" = "PIN: %@";
/* No comment provided by engineer. */
"Quit" = "Sair do QuickDrop";
/* No comment provided by engineer. */
"TransferError" = "Falha ao receber ficheiros de %@";
/* No comment provided by engineer. */
"VisibleToEveryone" = "Visível para todos";
"NoNetworkConnection" = "Sem ligação de rede";
"RecommendedApps" = "Aplicativos recomendados";
"GetSupport" = "Suporte";
"PrivacyPolicy" = "Política de privacidade";
"ReadyToReceive" = "Pronto para receber";
"SendClipboard" = "Enviar Área de Transferência";
"Clipboard" = "Área de Transferência";
"DeviceCurrentlySendingFiles" = "%1$@ está a enviar-te %2$@";
"UserManual" = "Manual do Utilizador e Definições";
"Settings" = "Definições";
"AutomaticallyAcceptFiles" = "Aceitar automaticamente ficheiros recebidos";
"AutomaticallyAcceptFilesFooter" = "Ativar esta definição é recomendado apenas na sua rede privada. Qualquer pessoa na mesma rede pode enviar ficheiros para o seu dispositivo sem o seu consentimento.";
"SaveFilesTo" = "Guardar ficheiros recebidos em:";
"SaveFilesLocation" = "Localização dos ficheiros recebidos";
"SaveFilesToButton" = "Selecionar…";
"OpenFinderAfterReceiving" = "Mostrar ficheiros no Finder após a receção";
"DownloadsFolder" = "Transferências";
"SendFiles" = "Enviar ficheiros";
"ReceiveFiles" = "Receber ficheiros";
"DeviceNotShown" = "Dispositivo não mostrado?";
"Troubleshooting" = "Resolução de problemas";
"AndroidApp" = "Aplicação Android";
"AndroidAppDescription" = "O QuickDrop está agora também disponível como uma aplicação separada para o seu dispositivo Android! Embora o QuickDrop possa ser utilizado sem uma app Android, a versão Android torna a transferência de ficheiros ainda mais simples e rápida. Com a app Android, pode até transferir ficheiros quando não está ligado à rede Wi-Fi de casa, ligando o Mac ao hotspot do seu Android. A app Android continua a oferecer a mesma encriptação ponto-a-ponto ao transferir ficheiros.
O QuickDrop para Android está atualmente em versão beta. Para se registar na versão beta, envie um e-mail para quickdrop-beta@leonboettger.com ou use o botão abaixo. Obrigado pelo seu interesse!";
"JoinBeta" = "Participar na Beta do QuickDrop para Android";
"BetaEmailBottom" = "Olá,
por favor, adicione-me à versão beta do QuickDrop para Android.
–––––––––––––––
Por favor, forneça abaixo o endereço de email da sua Conta Google, para que possa ser convidado para a versão beta. Depois, receberá um email com um link para descarregar a aplicação da Google Play Store.
O seu endereço de email será utilizado apenas para este fim e não será armazenado ou partilhado.
O seu endereço de email: ";
"SendFilesDescription" = "Para enviar um ficheiro para o seu dispositivo Android, clique no ícone de Partilhar ou clique com o botão direito no ficheiro e selecione \"Partilhar\". Depois, selecione \"QuickDrop\" na lista de aplicações e siga as instruções.
Se não encontrar o QuickDrop na lista, clique em \"Editar Extensões\", desloque-se para baixo e ative a opção para QuickDrop.
Também pode enviar o conteúdo atual da área de transferência do seu Mac para o seu dispositivo Android clicando no ícone do QuickDrop na barra de menu e selecionando \"Enviar Área de Transferência\".";
"WelcomeToQuickDrop" = "Bem-vindo ao QuickDrop";
"Acknowledgements" = "Agradecimentos";
"SupportQuickDrop" = "Apoie o QuickDrop";
"UserManualDescription" = "Quando o QuickDrop está em execução, aparece na barra de menus na parte superior do ecrã. Clique no ícone do QuickDrop se quiser parar de receber ficheiros.
Para enviar ficheiros do seu dispositivo Android para o Mac, toque no ícone de partilha no telemóvel e selecione \"Quick Share\". O seu Mac será listado como um dispositivo disponível. Os ficheiros serão guardados na pasta Transferências.
Importante: O seu dispositivo Android e o seu Mac precisam de estar na mesma rede WiFi para transferir ficheiros.";
"TroubleshootingDescription" = "Se o seu Mac não aparecer no seu dispositivo Android, mesmo que ambos estejam conectados à mesma rede Wi-Fi, ou se a transmissão falhar, tente o seguinte:
1) Abra as Definições do Sistema e vá a \"Privacidade e Segurança\" > \"Rede Local\". Certifique-se de que o QuickDrop está ativado. Se utilizar um firewall de terceiros ou um programa antivírus, consulte a documentação desse programa para permitir conexões de entrada para o QuickDrop.
2) Reinicie o QuickDrop clicando no ícone da barra de menus, escolha a última opção e abra novamente a aplicação. Reinicie o seu dispositivo Android.
Nota: Algumas redes não permitem que os dispositivos comuniquem entre si. Neste caso, precisa de descarregar a aplicação QuickDrop para o seu telemóvel e ligar o seu Mac ao hotspot do seu telemóvel para transferir ficheiros. O link para a aplicação pode ser encontrado na barra lateral à esquerda.";
"plusview_title" = "Obrigado por usar o QuickDrop!";
"plusview_description" = "Desbloqueia o QuickDrop+ para continuar a trocar ficheiros sem limites. QuickDrop+ é uma compra única – não é necessária subscrição. Todas as funcionalidades atuais e futuras estão incluídas.";
"plusview_success_title" = "Obrigado pela sua compra!";
"plusview_success_description" = "Agora pode continuar a receber ficheiros sem restrições. Todos os ficheiros que tentou enviar antes já foram guardados no seu dispositivo.";
"plusview_success_button" = "Entendido!";
"plusview_unlock_label" = "Apoiar QuickDrop";
"plusview_restorepurchase" = "Já comprou?";
"plusview_restorefailed" = "Houve um problema ao restaurar sua compra";
"plusview_restorefailed_proceed" = "Ok";
"plusview_purchasefailed" = "Falha na compra";
"plusview_purchasefaileddescription" = "Se esta mensagem apareceu inesperadamente, certifique-se de estar conectado à Internet e que as compras dentro do app estejam ativadas no seu dispositivo, depois tente novamente.";
"plusview_nothingtorestore" = "Não há nada para restaurar!";
"plusview_productRequestFailed" = "Não foi possível recuperar as compras dentro do app no momento.";
"Connecting" = "A ligar...";
"NotEnoughSpace" = "Espaço em disco insuficiente";
"Sending" = "A enviar...";
"TransferCanceled" = "Cancelado";
"TransferDeclined" = "Recusado";
"TransferFinished" = "Transferência concluída";
"TransferTimedOut" = "Tempo limite excedido";
"UnsupportedType" = "Tipo de anexo não suportado";
"QrCodeInstructions" = "Digitaliza o código QR com o teu dispositivo Android para o ligares ao teu Mac. Certifica-te de que o teu telefone está ligado à mesma rede.\n\nSe não quiseres digitalizar o código QR em todas as transferências de ficheiros, instala a aplicação QuickDrop (ver acima).";
"ConnectWithQuickDropApp" = "Ligar com a app QuickDrop";
"ConnectWithoutApp" = "Ligar sem app";
"QrCodeInstructionsApp" = "Para ligar o teu Mac ao teu dispositivo Android, abre a app QuickDrop no dispositivo Android. Garante que o teu telemóvel está ligado à mesma rede.\n\nPara instalar o QuickDrop no teu telemóvel, digitaliza o código QR ou procura 'QuickDrop' na Google Play Store.";
"QuickDropAndroidAppAdvertisement" = "O QuickDrop está agora também disponível como uma app separada para o teu smartphone! A app oferece funcionalidades adicionais, como a capacidade de transferir ficheiros mesmo quando não estás ligado à rede Wi-Fi de casa, ligando o Mac ao hotspot do teu smartphone.\n\nPara instalar o QuickDrop no teu telemóvel, digitaliza o código QR ou procura 'QuickDrop' na Google Play Store.";
"TypeNotSupported" = "Tipo não suportado";
"TypeNotSupportedDescription" = "O tipo de ficheiro selecionado não é suportado. Se quiser partilhá-lo mesmo assim, clique com o botão direito no ficheiro, selecione \"Comprimir\" e depois envie o ficheiro comprimido.";
"TypeNotSupportedButton" = "Ok";
"CloseAlert" = "Fechar";
"InformDeveloper" = "Enviar relatório de erro";
"TimeoutTitle" = "Algo correu mal";
"TimeoutDescription" = "O QuickDrop não conseguiu ligar-se ao seu telefone. Abra as definições do seu dispositivo Android e procure por 'Quick Share'. Altere a definição 'Quem pode partilhar consigo' para 'Todos' e tente novamente.";
"TimeoutButton" = "Ok";
"ApIsolationHeader" = "O seu router pode não permitir a comunicação entre dispositivos";
"ApIsolationDescription" = "O QuickDrop detetou que o seu router Wi-Fi pode estar a bloquear a comunicação entre dispositivos. Para transferir ficheiros entre o seu telemóvel e o Mac, terá de desativar o isolamento de cliente (também conhecido como isolamento AP ou isolamento Wi-Fi).
Veja como desativá-lo:
1. Aceda ao seu router: Abra um navegador e introduza o endereço IP do seu router (normalmente 192.168.0.1 ou 192.168.1.1), ou utilize o botão na parte inferior desta página. Inicie sessão com o nome de utilizador e palavra-passe de administrador. (Consulte a etiqueta ou manual do router se não souber.)
2. Encontre a definição: Procure em secções como \"Definições sem fios\", \"Definições avançadas\" ou \"Segurança\". Procure opções com nomes como \"Isolamento AP\", \"Isolamento de cliente\", \"Isolamento sem fios\" ou \"Bloqueio Intra-BSS\".
3. Desative-a: Certifique-se de que esta opção está desativada. Guarde as alterações e reinicie o router, se necessário.
Depois de desativado, os dispositivos na sua rede Wi-Fi (como o seu telemóvel e o Mac) poderão encontrar-se e comunicar entre si. Nota: se estiver numa rede de convidados, esta definição poderá não estar disponível. Nesse caso, utilize uma rede diferente ou descarregue a aplicação QuickDrop no seu telemóvel e ligue o seu Mac ao hotspot do telemóvel para transferir ficheiros. Procure “QuickDrop” na Google Play para descarregar a aplicação.";
"ApIsolationAction" = "Abrir definições do router";
"ApIsolationFindRouterFailedHeader" = "Não foi possível encontrar o IP do router";
"ApIsolationFindRouterFailedDescription" = "O QuickDrop não conseguiu determinar o endereço do seu router. Por favor, consulte o manual do utilizador do seu router Wi-Fi para saber qual é.";
"FirewallHeader" = "Um firewall pode bloquear transferências de ficheiros recebidos";
"FirewallDescription" = "O QuickDrop detectou que um firewall pode impedir que o seu dispositivo Android se conecte ao seu Mac. Os firewalls são utilizados para bloquear conexões de entrada por padrão. Para receber ficheiros, é necessário permitir conexões de entrada, pois o seu telefone tenta conectar-se ao seu Mac – criando uma conexão de entrada.
É provável que esteja a utilizar um programa antivírus no seu Mac. Tem duas opções:
1. Permitir conexões de entrada para o QuickDrop nas configurações do seu firewall. Este procedimento varia conforme o programa, pelo que poderá ter de contactar o fabricante do seu software de firewall/antivírus para saber como proceder.
2. Desativar o firewall do seu software antivírus e utilizar o firewall incorporado do Mac. O firewall da Apple está otimizado para o QuickDrop.
Veja como desativar o firewall do seu programa antivírus: Abra as Definições do Sistema no seu Mac, navegue até Rede > VPN e Filtro, e desative o filtro de conteúdo. Em seguida, vá até Rede > Firewall e ative o firewall do Mac.
Depois de feito isto, o seu telefone poderá conectar-se ao seu Mac.";
"NetworkFilterHeader" = "Um filtro de pacotes pode impedir a transferência de ficheiros";
"NetworkFilterDescription" = "O QuickDrop detetou que um programa instalado no seu Mac está a modificar o tráfego de rede. Como resultado, o ficheiro que tentou enviar a partir do seu dispositivo Android não pôde ser decifrado.
Pode ter um filtro de pacotes com defeito ativado no seu Mac, possivelmente instalado por um antivírus (por exemplo, ESET). Abra Preferências do Sistema > Rede > VPN e Filtro e desative o filtro de conteúdo.
Depois disso, deverá conseguir transferir ficheiros com sucesso.";
"TransmitUsingCable" = "Transferir com Cabo";
"DownloadCableConnectionApp" = "Descarregar iDroid";
"DownloadCableConnectionAppDescription" = "Se pretendes transferir ficheiros e explorar o armazenamento do teu dispositivo Android via USB, agora podes fazê-lo com a nova aplicação iDroid.\n\niDroid suporta transferência e envio de ficheiros e pastas inteiras através de arrastar e largar.";
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFiles</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@files@</string>
<key>files</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d file</string>
<key>other</key>
<string>%d files</string>
</dict>
</dict>
</dict>
</plist>
-192
View File
@@ -1,192 +0,0 @@
/* No comment provided by engineer. */
"Accept" = "Acceptă";
/* No comment provided by engineer. */
"Decline" = "Refuză";
/* No comment provided by engineer. */
"DeviceName" = "Nume dispozitiv : %@";
/* No comment provided by engineer. */
"DeviceSendingFiles" = "%1$@ vă trimite %2$@";
"DeviceCurrentlySendingText" = "%1$@ a introdus \"%2$@\" în clipboard-ul tău.";
"DeviceSendingText" = "%1$@ dorește să introducă \"%2$@\" în clipboard-ul Mac-ului tău.";
"DeviceCurrentlySendingUrl" = "%1$@ a deschis \"%2$@\".";
"DeviceSendingUrl" = "%1$@ dorește să deschidă \"%2$@\" pe Mac-ul tău.";
"InsertedIntoClipboard" = "Inserat";
"UnsupportedFileType" = "Fișierul pe care ai încercat să-l trimiți nu este momentan acceptat. Te rugăm să trimiți un raport de eroare.";
/* No comment provided by engineer. */
"Error.Crypto" = "Eroare de criptare";
/* No comment provided by engineer. */
"Error.Protocol" = "Eroare de comunicare";
/* No comment provided by engineer. */
"NFiles" = "%d fișiere";
/* No comment provided by engineer. */
"NotificationsDenied.Message" = "QuickDrop trebuie să poată afișa notificări pentru transferurile de fișiere primite. Vă rugăm să permiteți notificările în Setările Sistemului.";
/* No comment provided by engineer. */
"NotificationsDenied.OpenSettings" = "Deschide setările";
/* No comment provided by engineer. */
"NotificationsDenied.Title" = "Permisiunea pentru notificări este necesară";
/* No comment provided by engineer. */
"PinCode" = "COD PIN : %@";
/* No comment provided by engineer. */
"Quit" = "Închide QuickDrop";
/* No comment provided by engineer. */
"TransferError" = "Recepționarea fișierelor de la %@ a eșuat";
/* No comment provided by engineer. */
"VisibleToEveryone" = "Vizibil pentru toată lumea";
"NoNetworkConnection" = "Nicio conexiune la rețea";
"RecommendedApps" = "Aplicații recomandate";
"GetSupport" = "Suport";
"PrivacyPolicy" = "Politica de confidențialitate";
"ReadyToReceive" = "Pregătit să primești";
"SendClipboard" = "Trimite Clipboard-ul";
"Clipboard" = "Clipboard";
"DeviceCurrentlySendingFiles" = "%1$@ îți trimite %2$@";
"UserManual" = "Manual utilizator & Setări";
"Settings" = "Setări";
"AutomaticallyAcceptFiles" = "Acceptă automat fișierele primite";
"AutomaticallyAcceptFilesFooter" = "Activarea acestei opțiuni este recomandată doar în rețeaua privată. Oricine din aceeași rețea poate trimite fișiere către dispozitivul dvs. fără consimțământ.";
"SaveFilesTo" = "Salvează fișierele primite în:";
"SaveFilesLocation" = "Locația fișierelor primite";
"SaveFilesToButton" = "Selectează…";
"OpenFinderAfterReceiving" = "Afișează fișierele în Finder după primire";
"DownloadsFolder" = "Descărcări";
"SendFiles" = "Trimite fișiere";
"ReceiveFiles" = "Primește fișiere";
"DeviceNotShown" = "Dispozitivul nu este afișat?";
"Troubleshooting" = "Depanare";
"AndroidApp" = "Aplicație Android";
"AndroidAppDescription" = "QuickDrop este acum disponibil și ca aplicație separată pentru dispozitivul tău Android! Deși QuickDrop poate fi utilizat și fără aplicația Android, QuickDrop pentru Android face transferul de fișiere și mai simplu și mai rapid. Cu aplicația Android, poți transfera fișiere chiar și atunci când nu ești conectat la rețeaua Wi-Fi de acasă, conectând Mac-ul la hotspot-ul Android. Aplicația Android oferă în continuare aceeași criptare end-to-end la transferul fișierelor.
QuickDrop pentru Android este în prezent în versiune beta. Pentru a te înregistra în versiunea beta, trimite un e-mail la quickdrop-beta@leonboettger.com sau folosește butonul de mai jos. Îți mulțumim pentru interes!";
"JoinBeta" = "Participă la versiunea beta QuickDrop pentru Android";
"BetaEmailBottom" = "Bună,
te rog adaugă-mă la versiunea beta a aplicației QuickDrop pentru Android.
–––––––––––––––
Te rugăm să introduci mai jos adresa de e-mail a contului tău Google, pentru a putea fi invitat în versiunea beta. Ulterior, vei primi un e-mail cu un link pentru a descărca aplicația din Google Play Store.
Adresa ta de e-mail va fi folosită doar în acest scop și nu va fi stocată sau partajată.
Adresa ta de e-mail: ";
"SendFilesDescription" = "Pentru a trimite un fișier către dispozitivul dvs. Android, faceți clic pe pictograma Partajare sau faceți clic dreapta pe fișier și selectați \"Partajare\". Apoi, selectați \"QuickDrop\" din lista de aplicații și urmați instrucțiunile.
Dacă nu vedeți QuickDrop în listă, faceți clic pe \"Editare extensii\", derulați în jos și activați opțiunea pentru QuickDrop.
De asemenea, puteți trimite conținutul curent al clipboard-ului Mac-ului dvs. către dispozitivul Android făcând clic pe pictograma QuickDrop din bara de meniu și selectând \"Trimite clipboard\".";
"WelcomeToQuickDrop" = "Bine ai venit la QuickDrop";
"Acknowledgements" = "Mulțumiri";
"SupportQuickDrop" = "Susține QuickDrop";
"UserManualDescription" = "Când QuickDrop rulează, acesta va apărea în bara de meniu din partea de sus a ecranului. Fă clic pe pictograma QuickDrop dacă dorești să oprești primirea fișierelor.
Pentru a trimite fișiere de pe dispozitivul Android pe Mac, atinge pictograma de partajare de pe telefon și selectează \"Quick Share\". Mac-ul tău va fi afișat ca un dispozitiv disponibil. Fișierele vor fi salvate în folderul Descărcări.
Important: Dispozitivul tău Android și Mac-ul trebuie să fie conectate la aceeași rețea WiFi pentru a transfera fișiere.";
"TroubleshootingDescription" = "Dacă Mac-ul tău nu apare pe dispozitivul Android, deși ambele dispozitive sunt conectate la aceeași rețea Wi-Fi, sau dacă transmiterea a eșuat, încearcă următoarele:
1) Deschide Setările Sistemului și mergi la \"Confidențialitate și Securitate\" > \"Rețea Locală\". Asigură-te că QuickDrop este activat. Dacă folosești un firewall terț sau un program antivirus, consultă documentația acestuia pentru a permite conexiunile de intrare pentru QuickDrop.
2) Repornește QuickDrop făcând clic pe pictograma din bara de meniu, alege ultima opțiune și deschide din nou aplicația. Repornește dispozitivul Android.
Notă: Unele rețele nu permit dispozitivelor să comunice între ele. În acest caz, trebuie să descarci aplicația QuickDrop pentru telefonul tău și să conectezi Mac-ul la hotspotul telefonului pentru a transfera fișiere. Linkul către aplicație se găsește în bara laterală din stânga.";
"plusview_title" = "Mulțumim că folosești QuickDrop!";
"plusview_description" = "Deblochează QuickDrop+ pentru a continua schimbul de fișiere fără limite. QuickDrop+ este o achiziție unică – nu este necesar abonament. Toate funcțiile curente și viitoare sunt incluse.";
"plusview_success_title" = "Mulțumim pentru achiziție!";
"plusview_success_description" = "Acum puteți continua să primiți fișiere fără restricții. Toate fișierele pe care ați încercat să le trimiteți anterior au fost deja salvate pe dispozitivul dvs.";
"plusview_success_button" = "Înțeles!";
"plusview_unlock_label" = "Susține QuickDrop";
"plusview_restorepurchase" = "Deja cumpărat?";
"plusview_restorefailed" = "A apărut o problemă la restaurarea achiziției";
"plusview_restorefailed_proceed" = "OK";
"plusview_purchasefailed" = "Achiziția a eșuat";
"plusview_purchasefaileddescription" = "Dacă acest mesaj a apărut neașteptat, asigură-te că ești conectat la internet și că achizițiile în aplicație sunt activate pe dispozitivul tău, apoi încearcă din nou.";
"plusview_nothingtorestore" = "Nu există nimic de restaurat!";
"plusview_productRequestFailed" = "Momentan, nu se pot obține informații despre achizițiile în aplicație.";
"Connecting" = "Se conectează...";
"NotEnoughSpace" = "Spațiu insuficient pe disc";
"Sending" = "Se trimite...";
"TransferCanceled" = "Anulat";
"TransferDeclined" = "Refuzat";
"TransferFinished" = "Transfer finalizat";
"TransferTimedOut" = "Timpul a expirat";
"UnsupportedType" = "Tip de atașament neacceptat";
"QrCodeInstructions" = "Scanează codul QR cu dispozitivul tău Android pentru a-l conecta la Mac. Asigură-te că telefonul este conectat la aceeași rețea.\n\nDacă nu dorești să scanezi codul QR de fiecare dată când transferi fișiere, instalează aplicația QuickDrop (vezi mai sus).";
"ConnectWithQuickDropApp" = "Conectează-te cu aplicația QuickDrop";
"ConnectWithoutApp" = "Conectează-te fără aplicație";
"QrCodeInstructionsApp" = "Pentru a conecta Mac-ul la dispozitivul Android, deschide aplicația QuickDrop pe dispozitivul Android. Asigură-te că telefonul este conectat la aceeași rețea.\n\nPentru a instala QuickDrop pe telefon, scanează codul QR sau caută 'QuickDrop' în Google Play Store.";
"QuickDropAndroidAppAdvertisement" = "QuickDrop este acum disponibil și ca aplicație separată pentru smartphone-ul tău! Aplicația oferă funcții suplimentare, cum ar fi posibilitatea de a transfera fișiere chiar și atunci când nu ești conectat la rețeaua Wi-Fi de acasă, conectând Mac-ul la hotspotul telefonului tău.\n\nPentru a instala QuickDrop pe telefon, scanează codul QR sau caută 'QuickDrop' în Google Play Store.";
"TypeNotSupported" = "Tip nesuportat";
"TypeNotSupportedDescription" = "Tipul de fișier selectat nu este acceptat. Dacă dorești să îl trimiți oricum, fă clic dreapta pe fișier, selectează \"Comprimă\" și apoi trimite fișierul comprimat.";
"TypeNotSupportedButton" = "Ok";
"CloseAlert" = "Închide";
"InformDeveloper" = "Trimite raport de eroare";
"TimeoutTitle" = "Ceva nu a mers bine";
"TimeoutDescription" = "QuickDrop nu a reușit să se conecteze la telefonul tău. Deschide setările dispozitivului Android și caută 'Quick Share'. Schimbă setarea 'Cine poate partaja cu tine' pe 'Toată lumea' și încearcă din nou.";
"TimeoutButton" = "OK";
"ApIsolationHeader" = "Este posibil ca routerul tău să nu permită comunicarea între dispozitive";
"ApIsolationDescription" = "QuickDrop a detectat că routerul tău Wi-Fi ar putea bloca comunicarea între dispozitive. Pentru a transfera fișiere între telefon și Mac, va trebui să dezactivezi izolarea clienților (cunoscută și ca izolare AP sau izolare Wi-Fi).
Iată cum o poți dezactiva:
1. Conectează-te la router: Deschide un browser web și introdu adresa IP a routerului (de obicei 192.168.0.1 sau 192.168.1.1), sau folosește butonul din partea de jos a paginii. Autentifică-te cu numele de utilizator și parola de administrator. (Verifică eticheta routerului sau manualul dacă nu ești sigur.)
2. Găsește setarea: Caută în secțiuni precum \"Setări wireless\", \"Setări avansate\" sau \"Securitate\". Caută opțiuni precum \"Izolare AP\", \"Izolare client\", \"Izolare wireless\" sau \"Blocare Intra-BSS\".
3. Dezactivează: Asigură-te că această setare este dezactivată. Salvează modificările și repornește routerul dacă este necesar.
După dezactivare, dispozitivele din rețeaua Wi-Fi (precum telefonul și Mac-ul) se vor putea detecta și conecta între ele. Vă rugăm să rețineți: dacă sunteți conectat la o rețea pentru oaspeți, această setare ar putea să nu fie disponibilă. În acest caz, folosiți o altă rețea sau descărcați aplicația QuickDrop pe telefonul dvs. și conectați-vă Mac-ul la hotspotul telefonului pentru a transfera fișiere. Căutați “QuickDrop” pe Google Play pentru a descărca aplicația.";
"ApIsolationAction" = "Deschide setările routerului";
"ApIsolationFindRouterFailedHeader" = "Nu s-a putut găsi IP-ul routerului";
"ApIsolationFindRouterFailedDescription" = "QuickDrop nu a putut determina adresa routerului tău. Te rugăm să consulți manualul de utilizare al routerului Wi-Fi pentru a obține această adresă.";
"FirewallHeader" = "Un firewall poate bloca transferurile de fișiere primite";
"FirewallDescription" = "QuickDrop a detectat că un firewall poate împiedica dispozitivul Android să se conecteze la Mac-ul dvs. Firewall-urile sunt utilizate pentru a bloca implicit conexiunile de intrare. Pentru a primi fișiere, trebuie să permiteți conexiunile de intrare, deoarece telefonul dvs. încearcă să se conecteze la Mac – creând o conexiune de intrare.
Este probabil să utilizați un program antivirus pe Mac. Aveți două opțiuni:
1. Permiteți conexiunile de intrare pentru QuickDrop în setările firewall-ului dvs. Procedura variază în funcție de program, așa că poate fi necesar să contactați producătorul software-ului de firewall/antivirus pentru instrucțiuni.
2. Dezactivați firewall-ul din software-ul antivirus și utilizați firewall-ul integrat din macOS. Firewall-ul Apple este optimizat pentru QuickDrop.
Iată cum puteți dezactiva firewall-ul programului antivirus: Deschideți Setările Sistemului pe Mac, mergeți la Rețea > VPN și Filtrare, apoi dezactivați filtrul de conținut. Apoi mergeți la Rețea > Firewall și activați firewall-ul de pe Mac.
După ce faceți acest lucru, telefonul dvs. va putea să se conecteze la Mac.";
"NetworkFilterHeader" = "Un filtru de pachete ar putea împiedica transferul de fișiere";
"NetworkFilterDescription" = "QuickDrop a detectat că un program instalat pe Mac-ul tău modifică traficul de rețea. Din această cauză, fișierul pe care ai încercat să-l trimiți de pe dispozitivul Android nu a putut fi decriptat.
Este posibil ca Mac-ul tău să aibă activat un filtru de pachete defect, instalat de un program antivirus (de exemplu, ESET). Deschide Preferințe Sistem > Rețea > VPN și Filtru și dezactivează filtrul de conținut.
După ce faci acest lucru, ar trebui să poți transfera fișiere cu succes.";
"TransmitUsingCable" = "Transferă cu Cablu";
"DownloadCableConnectionApp" = "Descarcă iDroid";
"DownloadCableConnectionAppDescription" = "Dacă dorești să transferi fișiere și să explorezi stocarea dispozitivului tău Android prin USB, acum poți face acest lucru cu noua aplicație iDroid.\n\niDroid permite descărcarea și încărcarea fișierelor și folderelor întregi prin tragere și plasare.";
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFiles</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@files@</string>
<key>files</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d fișier</string>
<key>other</key>
<string>%d fișiere</string>
</dict>
</dict>
</dict>
</plist>
-195
View File
@@ -1,195 +0,0 @@
/* No comment provided by engineer. */
"Accept" = "Принять";
/* No comment provided by engineer. */
"Decline" = "Отклонить";
/* No comment provided by engineer. */
"DeviceName" = "Имя устройства: %@";
/* No comment provided by engineer. */
"DeviceSendingFiles" = "%1$@ отправляет Вам %2$@";
"DeviceCurrentlySendingText" = "%1$@ вставил \"%2$@\" в ваш буфер обмена.";
"DeviceSendingText" = "%1$@ хочет вставить \"%2$@\" в буфер обмена вашего Mac.";
"DeviceCurrentlySendingUrl" = "%1$@ открыл \"%2$@\".";
"DeviceSendingUrl" = "%1$@ хочет открыть \"%2$@\" на вашем Mac.";
"InsertedIntoClipboard" = "Вставлено";
"UnsupportedFileType" = "Файл, который вы пытались отправить, в настоящее время не поддерживается. Пожалуйста, отправьте отчет об ошибке.";
/* No comment provided by engineer. */
"Error.Crypto" = "Ошибка шифрования";
/* No comment provided by engineer. */
"Error.Protocol" = "Ошибка протокола";
/* No comment provided by engineer. */
/*"NFiles" = "%d файлов";*/
/* No comment provided by engineer. */
"PinCode" = "PIN: %@";
/* No comment provided by engineer. */
"Quit" = "Завершить QuickDrop";
/* No comment provided by engineer. */
"TransferError" = "Не удалость принять файлы от %@";
/* No comment provided by engineer. */
"VisibleToEveryone" = "Видимость: все";
"NoNetworkConnection" = "Нет сетевого подключения";
/* No comment provided by engineer. */
"NotificationsDenied.Message" = "QuickDrop необходима возможность показывать уведомления о входящих файлах. Пожалуйста, разрешите уведомления в Системных настройках.";
/* No comment provided by engineer. */
"NotificationsDenied.OpenSettings" = "Открыть настройки";
/* No comment provided by engineer. */
"NotificationsDenied.Title" = "Требуется доступ к уведомлениям";
"RecommendedApps" = "Рекомендуемые приложения";
"GetSupport" = "Поддержка";
"PrivacyPolicy" = "Политика конфиденциальности";
"ReadyToReceive" = "Готов к получению";
"SendClipboard" = "Отправить буфер обмена";
"Clipboard" = "Буфер обмена";
"DeviceCurrentlySendingFiles" = "%1$@ отправляет вам %2$@";
"UserManual" = "Руководство пользователя и Настройки";
"Settings" = "Настройки";
"AutomaticallyAcceptFiles" = "Автоматически принимать входящие файлы";
"AutomaticallyAcceptFilesFooter" = "Включение этой настройки рекомендуется только в вашей частной сети. Любой пользователь в той же сети может отправлять файлы на ваше устройство без вашего согласия.";
"SaveFilesTo" = "Сохранять входящие файлы в:";
"SaveFilesLocation" = "Местоположение входящих файлов";
"SaveFilesToButton" = "Выбрать…";
"OpenFinderAfterReceiving" = "Показать файлы в Finder после получения";
"DownloadsFolder" = "Загрузки";
"SendFiles" = "Отправить файлы";
"ReceiveFiles" = "Получить файлы";
"DeviceNotShown" = "Устройство не отображается?";
"Troubleshooting" = "Устранение неполадок";
"AndroidApp" = "Приложение Android";
"AndroidAppDescription" = "QuickDrop теперь также доступен как отдельное приложение для вашего устройства Android! Хотя QuickDrop можно использовать и без Android-приложения, версия для Android делает передачу файлов ещё проще и быстрее. С Android-приложением вы даже можете передавать файлы без подключения к домашней Wi-Fi сети, подключив Mac к точке доступа Android. Приложение по-прежнему использует сквозное шифрование при передаче файлов.
QuickDrop для Android сейчас находится в стадии бета-тестирования. Чтобы зарегистрироваться, отправьте письмо на quickdrop-beta@leonboettger.com или используйте кнопку ниже. Спасибо за интерес!";
"JoinBeta" = "Присоединиться к бета-тестированию QuickDrop для Android";
"BetaEmailBottom" = "Здравствуйте,
пожалуйста, добавьте меня в бета-тестирование QuickDrop для Android.
–––––––––––––––
Пожалуйста, укажите адрес электронной почты вашей учетной записи Google ниже, чтобы получить приглашение в бета-тестирование. После этого вы получите письмо со ссылкой для загрузки приложения из Google Play Store.
Ваш адрес электронной почты будет использоваться только для этой цели и не будет сохранен или передан третьим лицам.
Ваш адрес электронной почты: ";
"SendFilesDescription" = "Чтобы отправить файл на ваше устройство Android, нажмите на значок \"Поделиться\" или щелкните файл правой кнопкой мыши и выберите \"Поделиться\". Затем выберите \"QuickDrop\" из списка приложений и следуйте инструкциям.
Если QuickDrop отсутствует в списке, нажмите \"Редактировать расширения\", прокрутите вниз и включите переключатель для QuickDrop.
Вы также можете отправить текущий содержимое буфера обмена вашего Mac на устройство Android, нажав на значок QuickDrop в строке меню и выбрав \"Отправить буфер обмена\".";
"WelcomeToQuickDrop" = "Добро пожаловать в QuickDrop";
"Acknowledgements" = "Благодарности";
"SupportQuickDrop" = "Поддержать QuickDrop";
"UserManualDescription" = "Когда QuickDrop работает, он появится в строке меню в верхней части экрана. Нажмите на иконку QuickDrop, если хотите прекратить прием файлов.
Чтобы отправить файлы с устройства Android на Mac, нажмите значок «Поделиться» на телефоне и выберите «Quick Share». Ваш Mac появится в списке доступных устройств. Файлы будут сохранены в папке «Загрузки».
Важно: ваше устройство Android и Mac должны быть в одной сети WiFi для передачи файлов.";
"TroubleshootingDescription" = "Если ваш Mac не отображается на устройстве Android, даже если оба устройства подключены к одной и той же Wi-Fi сети, или передача не удалась, попробуйте следующее:
1) Откройте \"Системные настройки\" и перейдите в \"Конфиденциальность и безопасность\" > \"Локальная сеть\". Убедитесь, что QuickDrop включён. Если вы используете сторонний брандмауэр или антивирусную программу, ознакомьтесь с их документацией, чтобы разрешить входящие соединения для QuickDrop.
2) Перезапустите QuickDrop, нажав на значок в строке меню, выберите последний пункт и снова откройте приложение. Перезагрузите ваше Android-устройство.
Примечание: Некоторые сети не позволяют устройствам взаимодействовать друг с другом. В этом случае загрузите приложение QuickDrop на телефон и подключите Mac к точке доступа телефона для передачи файлов. Ссылка на приложение доступна в боковой панели слева.";
"plusview_title" = "Спасибо за использование QuickDrop!";
"plusview_description" = "Разблокируйте QuickDrop+, чтобы продолжить обмен файлами без ограничений. QuickDrop+ – это разовая покупка, подписка не требуется. Все текущие и будущие функции включены.";
"plusview_success_title" = "Спасибо за покупку!";
"plusview_success_description" = "Теперь вы можете продолжать получать файлы без ограничений. Все файлы, которые вы пытались отправить ранее, уже сохранены на вашем устройстве.";
"plusview_success_button" = "Понял!";
"plusview_unlock_label" = "Поддержать QuickDrop";
"plusview_restorepurchase" = "Уже куплено?";
"plusview_restorefailed" = "Произошла ошибка при восстановлении покупки";
"plusview_restorefailed_proceed" = "Окей";
"plusview_purchasefailed" = "Ошибка покупки";
"plusview_purchasefaileddescription" = "Если это сообщение появилось неожиданно, убедитесь, что у вас есть доступ к интернету и включены встроенные покупки на вашем устройстве, затем попробуйте снова.";
"plusview_nothingtorestore" = "Нет покупок для восстановления!";
"plusview_productRequestFailed" = "Не удалось получить данные о встроенных покупках.";
"Connecting" = "Подключение...";
"NotEnoughSpace" = "Недостаточно места на диске";
"Sending" = "Отправка...";
"TransferCanceled" = "Отменено";
"TransferDeclined" = "Отклонено";
"TransferFinished" = "Передача завершена";
"TransferTimedOut" = "Время ожидания истекло";
"UnsupportedType" = "Тип вложения не поддерживается";
"QrCodeInstructions" = "Сканируйте QR-код с помощью устройства Android, чтобы подключить его к вашему Mac. Убедитесь, что ваш телефон подключен к той же сети.\n\nЕсли вы не хотите сканировать QR-код при каждой передаче файла, установите приложение QuickDrop (см. выше).";
"ConnectWithQuickDropApp" = "Подключиться с помощью приложения QuickDrop";
"ConnectWithoutApp" = "Подключиться без приложения";
"QrCodeInstructionsApp" = "Чтобы подключить ваш Mac к устройству Android, откройте приложение QuickDrop на устройстве Android. Убедитесь, что ваш телефон подключен к той же сети.\n\nЧтобы установить QuickDrop на телефон, отсканируйте QR-код или найдите 'QuickDrop' в Google Play Store.";
"QuickDropAndroidAppAdvertisement" = "QuickDrop теперь также доступен как отдельное приложение для вашего смартфона! Приложение предоставляет дополнительные функции, такие как возможность передачи файлов, когда вы не подключены к домашней Wi-Fi сети, путем подключения Mac к точке доступа вашего смартфона.\n\nЧтобы установить QuickDrop на телефон, отсканируйте QR-код или найдите 'QuickDrop' в Google Play Store.";
"TypeNotSupported" = "Тип не поддерживается";
"TypeNotSupportedDescription" = "Выбранный тип файла не поддерживается. Если вы все равно хотите его отправить, щелкните файл правой кнопкой мыши, выберите «Сжать», а затем отправьте сжатый файл.";
"TypeNotSupportedButton" = "Окей";
"CloseAlert" = "Закрыть";
"InformDeveloper" = "Отправить отчёт об ошибке";
"TimeoutTitle" = "Что-то пошло не так";
"TimeoutDescription" = "QuickDrop не удалось подключиться к вашему телефону. Откройте настройки Android и найдите 'Quick Share'. Измените параметр 'Кто может делиться с вами' на 'Все' и попробуйте снова.";
"TimeoutButton" = "Ок";
"ApIsolationHeader" = "Ваш маршрутизатор может не разрешать связь между устройствами";
"ApIsolationDescription" = "QuickDrop обнаружил, что ваш Wi-Fi маршрутизатор может блокировать связь между устройствами. Чтобы передавать файлы между телефоном и Mac, необходимо отключить изоляцию клиентов (также известную как изоляция AP или изоляция Wi-Fi).
Вот как это отключить:
1. Войдите в маршрутизатор: Откройте веб-браузер и введите IP-адрес маршрутизатора (обычно 192.168.0.1 или 192.168.1.1), или используйте кнопку внизу этой страницы. Войдите с именем пользователя и паролем администратора. (Проверьте этикетку или руководство маршрутизатора, если не уверены.)
2. Найдите настройку: Ищите в разделах, таких как Беспроводные настройки, Расширенные настройки или Безопасность. Ищите опции с названиями “Изоляция AP”, “Изоляция клиентов”, “Беспроводная изоляция” или “Блокировка Intra-BSS”.
3. Отключите её: Убедитесь, что эта настройка отключена. Сохраните изменения и при необходимости перезагрузите маршрутизатор.
После отключения устройства в вашей Wi-Fi сети (например, телефон и Mac) смогут обнаруживать и подключаться друг к другу. Обратите внимание: если вы используете гостевую сеть, эта настройка может быть недоступна. В этом случае используйте другую сеть или загрузите приложение QuickDrop на свой телефон и подключите Mac к точке доступа телефона для передачи файлов. Найдите “QuickDrop” в Google Play, чтобы загрузить приложение.";
"ApIsolationAction" = "Открыть настройки маршрутизатора";
"ApIsolationFindRouterFailedHeader" = "Не удалось найти IP маршрутизатора";
"ApIsolationFindRouterFailedDescription" = "QuickDrop не удалось определить адрес вашего маршрутизатора. Пожалуйста, обратитесь к руководству пользователя вашего Wi-Fi маршрутизатора, чтобы узнать его адрес.";
"FirewallHeader" = "Брандмауэр может блокировать входящие передачи файлов";
"FirewallDescription" = "QuickDrop обнаружил, что брандмауэр может препятствовать подключению вашего устройства Android к вашему Mac. Брандмауэры по умолчанию блокируют входящие подключения. Для получения файлов необходимо разрешить входящие подключения, поскольку ваш телефон пытается подключиться к вашему Mac, создавая входящее соединение.
Скорее всего, вы используете антивирусную программу на вашем Mac. У вас есть два варианта:
1. Разрешите входящие подключения для QuickDrop в настройках вашего брандмауэра. Эта процедура различается в зависимости от программы, поэтому вам, возможно, придется обратиться к производителю вашего программного обеспечения брандмауэра/антивируса за инструкциями.
2. Отключите брандмауэр вашего антивирусного программного обеспечения и используйте встроенный брандмауэр Mac. Брандмауэр Apple оптимизирован для QuickDrop.
Вот как отключить брандмауэр вашего антивирусного программного обеспечения: Откройте Системные настройки на вашем Mac, перейдите в Сеть > VPN и Фильтр, и отключите фильтр содержимого. Затем перейдите в Сеть > Брандмауэр и включите брандмауэр Mac.
После этого ваш телефон сможет подключиться к вашему Mac.";
"NetworkFilterHeader" = "Фильтр пакетов может препятствовать передаче файлов";
"NetworkFilterDescription" = "QuickDrop обнаружил, что установленная на вашем Mac программа изменяет сетевой трафик. В результате файл, который вы пытались отправить с Android-устройства, не удалось расшифровать.
На вашем Mac может быть включён неисправный фильтр пакетов, возможно установленный антивирусом (например, ESET). Откройте Системные настройки > Сеть > VPN и фильтр и отключите фильтр содержимого.
После этого передача файлов должна пройти успешно.";
"TransmitUsingCable" = "Передача через кабель";
"DownloadCableConnectionApp" = "Скачать iDroid";
"DownloadCableConnectionAppDescription" = "Если вы хотите передавать файлы и просматривать хранилище вашего Android-устройства через USB, теперь вы можете сделать это с помощью нового приложения iDroid.\n\niDroid поддерживает загрузку и передачу файлов и целых папок с помощью перетаскивания.";
@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFiles</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@files@</string>
<key>files</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d файл</string>
<key>few</key>
<string>%d файла</string>
<key>many</key>
<string>%d файлов</string>
<key>other</key>
<string>%d файлов</string>
</dict>
</dict>
</dict>
</plist>
-193
View File
@@ -1,193 +0,0 @@
/* No comment provided by engineer. */
"Accept" = "ยอมรับ";
/* No comment provided by engineer. */
"Decline" = "ปฏิเสธ";
/* No comment provided by engineer. */
"DeviceName" = "ชื่ออุปกรณ์: %@";
/* No comment provided by engineer. */
"DeviceSendingFiles" = "%1$@ กำลังส่งไฟล์ให้คุณ %2$@";
"DeviceCurrentlySendingText" = "%1$@ ได้แทรก \"%2$@\" ไปยังคลิปบอร์ดของคุณแล้ว";
"DeviceSendingText" = "%1$@ ต้องการแทรก \"%2$@\" ไปยังคลิปบอร์ดของ Mac ของคุณ";
"DeviceCurrentlySendingUrl" = "%1$@ ได้เปิด \"%2$@\" แล้ว";
"DeviceSendingUrl" = "%1$@ ต้องการเปิด \"%2$@\" บน Mac ของคุณ";
"InsertedIntoClipboard" = "แทรกแล้ว";
"UnsupportedFileType" = "ไฟล์ที่คุณพยายามส่งยังไม่รองรับในขณะนี้ โปรดส่งรายงานข้อผิดพลาด";
/* No comment provided by engineer. */
"Error.Crypto" = "การเข้ารหัสผิดพลาด";
/* No comment provided by engineer. */
"Error.Protocol" = "การสื่อสารผิดพลาด";
/* No comment provided by engineer. */
"NFiles" = "%d ไฟล์";
/* No comment provided by engineer. */
"NotificationsDenied.Message" = "QuickDrop ไม่สามารถแสดงการแจ้งเตือนไฟล์ที่เข้ามาได้ โปรดให้สิทธิ์การแจ้งเตือน ในการตั้งค่าระบบ";
/* No comment provided by engineer. */
"NotificationsDenied.OpenSettings" = "การตั้งค่า";
/* No comment provided by engineer. */
"NotificationsDenied.Title" = "จำเป็นต้องใช้สิทธิ์การแจ้งเตือน";
/* No comment provided by engineer. */
"PinCode" = "PIN: %@";
/* No comment provided by engineer. */
"Quit" = "ออก QuickDrop";
/* No comment provided by engineer. */
"TransferError" = "เกิดข้อผิดพลาดในการรับไฟล์จาก %@";
/* No comment provided by engineer. */
"VisibleToEveryone" = "เปิดเผยต่อทุกคน";
"NoNetworkConnection" = "ไม่มีการเชื่อมต่อเครือข่าย";
"RecommendedApps" = "แอปแนะนำ";
"GetSupport" = "การสนับสนุน";
"PrivacyPolicy" = "นโยบายความเป็นส่วนตัว";
"ReadyToReceive" = "พร้อมรับข้อมูล";
"SendClipboard" = "ส่งคลิปบอร์ด";
"Clipboard" = "คลิปบอร์ด";
"DeviceCurrentlySendingFiles" = "%1$@ กำลังส่ง %2$@ ให้คุณ";
"UserManual" = "คู่มือผู้ใช้และการตั้งค่า";
"Settings" = "การตั้งค่า";
"AutomaticallyAcceptFiles" = "ยอมรับไฟล์ขาเข้าโดยอัตโนมัติ";
"AutomaticallyAcceptFilesFooter" = "แนะนำให้เปิดใช้งานการตั้งค่านี้เฉพาะในเครือข่ายส่วนตัวของคุณเท่านั้น ใครก็ตามที่อยู่ในเครือข่ายเดียวกันสามารถส่งไฟล์ไปยังอุปกรณ์ของคุณได้โดยไม่ต้องได้รับอนุญาต";
"SaveFilesTo" = "บันทึกไฟล์ขาเข้าไปที่:";
"SaveFilesLocation" = "ตำแหน่งไฟล์ขาเข้า";
"SaveFilesToButton" = "เลือก…";
"OpenFinderAfterReceiving" = "แสดงไฟล์ใน Finder หลังจากรับแล้ว";
"DownloadsFolder" = "ดาวน์โหลด";
"SendFiles" = "ส่งไฟล์";
"ReceiveFiles" = "รับไฟล์";
"DeviceNotShown" = "อุปกรณ์ไม่แสดง?";
"Troubleshooting" = "การแก้ไขปัญหา";
"AndroidApp" = "แอป Android";
"AndroidAppDescription" = "ขณะนี้ QuickDrop พร้อมใช้งานเป็นแอปแยกต่างหากสำหรับอุปกรณ์ Android ของคุณแล้ว! แม้ว่า QuickDrop จะสามารถใช้งานได้โดยไม่ต้องมีแอป Android แต่ QuickDrop สำหรับ Android จะช่วยให้การถ่ายโอนไฟล์ง่ายและรวดเร็วยิ่งขึ้น ด้วยแอป Android คุณสามารถถ่ายโอนไฟล์ได้แม้ในขณะที่ไม่ได้เชื่อมต่อกับเครือข่าย Wi-Fi ที่บ้านของคุณ โดยเชื่อมต่อ Mac กับฮอตสปอตของ Android แอป Android ยังให้การเข้ารหัสแบบ end-to-end เช่นเดียวกันเมื่อถ่ายโอนไฟล์
QuickDrop สำหรับ Android อยู่ในช่วงเบต้า หากต้องการลงทะเบียนเบต้า กรุณาส่งอีเมลไปที่ quickdrop-beta@leonboettger.com หรือใช้ปุ่มด้านล่าง ขอบคุณสำหรับความสนใจ!";
"JoinBeta" = "เข้าร่วมเบต้า QuickDrop สำหรับ Android";
"BetaEmailBottom" = "สวัสดี,
กรุณาเพิ่มฉันเข้าร่วมเบต้า QuickDrop สำหรับ Android
–––––––––––––––
โปรดระบุที่อยู่อีเมลของบัญชี Google ของคุณด้านล่าง เพื่อให้คุณได้รับเชิญเข้าร่วมเบต้า หลังจากนั้นคุณจะได้รับอีเมลพร้อมลิงก์สำหรับดาวน์โหลดแอปจาก Google Play Store
ที่อยู่อีเมลของคุณจะถูกใช้เพื่อวัตถุประสงค์นี้เท่านั้น และจะไม่ถูกจัดเก็บหรือแบ่งปัน
ที่อยู่อีเมลของคุณ: ";
"SendFilesDescription" = "หากต้องการส่งไฟล์ไปยังอุปกรณ์ Android ของคุณ ให้คลิกที่ไอคอนแชร์ หรือคลิกขวาที่ไฟล์แล้วเลือก \"แชร์\" หลังจากนั้นให้เลือก \"QuickDrop\" จากรายการแอปและทำตามคำแนะนำ
หากคุณไม่เห็น QuickDrop ในรายการ ให้คลิกที่ \"แก้ไขส่วนขยาย\" เลื่อนลง และเปิดใช้งาน QuickDrop
คุณยังสามารถส่งเนื้อหาปัจจุบันของคลิปบอร์ดของ Mac ไปยังอุปกรณ์ Android ของคุณโดยคลิกที่ไอคอน QuickDrop บนแถบเมนูและเลือก \"ส่งคลิปบอร์ด\"";
"WelcomeToQuickDrop" = "ยินดีต้อนรับสู่ QuickDrop";
"Acknowledgements" = "คำขอบคุณ";
"SupportQuickDrop" = "สนับสนุน QuickDrop";
"UserManualDescription" = "เมื่อ QuickDrop ทำงานอยู่ จะปรากฏในแถบเมนูที่ด้านบนของหน้าจอ คลิกที่ไอคอน QuickDrop หากต้องการหยุดรับไฟล์
ในการส่งไฟล์จากอุปกรณ์ Android ไปยัง Mac ของคุณ ให้แตะไอคอนแชร์บนโทรศัพท์ แล้วเลือก \"Quick Share\" Mac ของคุณจะปรากฏเป็นอุปกรณ์ที่ใช้ได้ ไฟล์จะถูกบันทึกลงในโฟลเดอร์ดาวน์โหลด
สำคัญ: อุปกรณ์ Android และ Mac ของคุณต้องเชื่อมต่อกับเครือข่าย WiFi เดียวกันเพื่อถ่ายโอนไฟล์";
"TroubleshootingDescription" = "หาก Mac ของคุณไม่ปรากฏบนอุปกรณ์ Android แม้ว่าทั้งสองอุปกรณ์จะเชื่อมต่อกับเครือข่าย Wi-Fi เดียวกัน หรือการส่งล้มเหลว ลองทำตามขั้นตอนต่อไปนี้:
1) เปิดการตั้งค่าระบบ แล้วไปที่ \"ความเป็นส่วนตัวและความปลอดภัย\" > \"เครือข่ายท้องถิ่น\" ตรวจสอบให้แน่ใจว่าเปิดใช้งาน QuickDrop แล้ว หากคุณใช้ไฟร์วอลล์หรือโปรแกรมป้องกันไวรัสของบริษัทอื่น โปรดดูเอกสารประกอบเพื่ออนุญาตการเชื่อมต่อขาเข้าสำหรับ QuickDrop
2) รีสตาร์ท QuickDrop โดยคลิกที่ไอคอนในแถบเมนู เลือกรายการสุดท้าย และเปิดแอปอีกครั้ง รีสตาร์ทอุปกรณ์ Android ของคุณด้วย
หมายเหตุ: เครือข่ายบางเครือข่ายไม่อนุญาตให้อุปกรณ์สื่อสารกันได้ ในกรณีนี้ คุณต้องดาวน์โหลดแอป QuickDrop บนโทรศัพท์ และเชื่อมต่อ Mac ของคุณกับฮอตสปอตของโทรศัพท์เพื่อส่งไฟล์ ลิงก์สำหรับดาวน์โหลดแอปอยู่ในแถบด้านซ้าย";
"plusview_title" = "ขอบคุณที่ใช้ QuickDrop!";
"plusview_description" = "ปลดล็อก QuickDrop+ เพื่อแลกเปลี่ยนไฟล์ได้อย่างไม่จำกัด QuickDrop+ เป็นการซื้อครั้งเดียว – ไม่ต้องสมัครสมาชิก ฟีเจอร์ทั้งหมดทั้งปัจจุบันและอนาคตรวมอยู่ในนี้แล้ว";
"plusview_success_title" = "ขอบคุณสำหรับการซื้อ!";
"plusview_success_description" = "คุณสามารถรับไฟล์ต่อไปได้โดยไม่มีข้อจำกัด ไฟล์ทั้งหมดที่คุณพยายามส่งก่อนหน้านี้ถูกบันทึกไว้ในอุปกรณ์ของคุณแล้ว";
"plusview_success_button" = "รับทราบ!";
"plusview_unlock_label" = "สนับสนุน QuickDrop";
"plusview_restorepurchase" = "เคยซื้อแล้ว?";
"plusview_restorefailed" = "เกิดปัญหาในการกู้คืนการซื้อของคุณ";
"plusview_restorefailed_proceed" = "ตกลง";
"plusview_purchasefailed" = "การซื้อล้มเหลว";
"plusview_purchasefaileddescription" = "หากข้อความนี้ปรากฏขึ้นโดยไม่คาดคิด โปรดตรวจสอบว่าคุณเชื่อมต่ออินเทอร์เน็ตและเปิดใช้งานการซื้อภายในแอปในอุปกรณ์ของคุณแล้วลองอีกครั้ง";
"plusview_nothingtorestore" = "ไม่มีรายการที่ต้องกู้คืน!";
"plusview_productRequestFailed" = "ไม่สามารถดึงข้อมูลการซื้อภายในแอปได้ในขณะนี้";
"Connecting" = "กำลังเชื่อมต่อ...";
"NotEnoughSpace" = "พื้นที่ดิสก์ไม่เพียงพอ";
"Sending" = "กำลังส่ง...";
"TransferCanceled" = "ยกเลิก";
"TransferDeclined" = "ถูกปฏิเสธ";
"TransferFinished" = "การโอนเสร็จสิ้น";
"TransferTimedOut" = "หมดเวลา";
"UnsupportedType" = "ไม่รองรับประเภทไฟล์แนบ";
"QrCodeInstructions" = "สแกนรหัส QR ด้วยอุปกรณ์ Android ของคุณเพื่อเชื่อมต่อกับ Mac ของคุณ ตรวจสอบให้แน่ใจว่าโทรศัพท์ของคุณเชื่อมต่อกับเครือข่ายเดียวกัน\n\nหากคุณไม่ต้องการสแกนรหัส QR ทุกครั้งที่ถ่ายโอนไฟล์ ให้ติดตั้งแอป QuickDrop (ดูด้านบน)";
"ConnectWithQuickDropApp" = "เชื่อมต่อด้วยแอป QuickDrop";
"ConnectWithoutApp" = "เชื่อมต่อโดยไม่ใช้แอป";
"QrCodeInstructionsApp" = "เพื่อเชื่อมต่อ Mac ของคุณกับอุปกรณ์ Android ให้เปิดแอป QuickDrop บนอุปกรณ์ Android ของคุณ ตรวจสอบให้แน่ใจว่าโทรศัพท์ของคุณเชื่อมต่อกับเครือข่ายเดียวกัน\n\nหากต้องการติดตั้ง QuickDrop บนโทรศัพท์ของคุณ ให้สแกนรหัส QR หรือค้นหา 'QuickDrop' ใน Google Play Store";
"QuickDropAndroidAppAdvertisement" = "QuickDrop พร้อมใช้งานแล้วในรูปแบบแอปแยกสำหรับสมาร์ทโฟนของคุณ! แอปนี้มีคุณสมบัติเพิ่มเติม เช่น ความสามารถในการถ่ายโอนไฟล์เมื่อคุณไม่ได้เชื่อมต่อกับเครือข่าย Wi-Fi ที่บ้าน โดยการเชื่อมต่อ Mac กับฮอตสปอตของสมาร์ทโฟนของคุณ\n\nหากต้องการติดตั้ง QuickDrop บนโทรศัพท์ของคุณ ให้สแกนรหัส QR หรือค้นหา 'QuickDrop' ใน Google Play Store";
"TypeNotSupported" = "ไม่รองรับประเภทนี้";
"TypeNotSupportedDescription" = "ประเภทไฟล์ที่เลือกไม่รองรับ หากต้องการแชร์อยู่ดี ให้คลิกขวาที่ไฟล์ เลือก \"บีบอัด\" แล้วส่งไฟล์ที่บีบอัดแล้ว";
"TypeNotSupportedButton" = "ตกลง";
"CloseAlert" = "ปิด";
"InformDeveloper" = "ส่งรายงานข้อบกพร่อง";
"TimeoutTitle" = "เกิดข้อผิดพลาด";
"TimeoutDescription" = "QuickDrop ไม่สามารถเชื่อมต่อกับโทรศัพท์ของคุณได้ โปรดเปิดการตั้งค่าของอุปกรณ์ Android และค้นหา 'Quick Share' จากนั้นเปลี่ยนการตั้งค่า 'ใครสามารถแชร์กับคุณ' เป็น 'ทุกคน' และลองใหม่อีกครั้ง";
"TimeoutButton" = "ตกลง";
"ApIsolationHeader" = "เราเตอร์ของคุณอาจไม่อนุญาตให้มีการสื่อสารระหว่างอุปกรณ์";
"ApIsolationDescription" = "QuickDrop ตรวจพบว่าเราเตอร์ Wi-Fi ของคุณอาจบล็อกการสื่อสารระหว่างอุปกรณ์ เพื่อถ่ายโอนไฟล์ระหว่างโทรศัพท์และ Mac ของคุณ คุณจะต้องปิดการแยกไคลเอนต์ (หรือที่เรียกว่า AP isolation หรือ Wi-Fi isolation)
วิธีปิดการใช้งาน:
1. เข้าสู่ระบบเราเตอร์ของคุณ: เปิดเว็บเบราว์เซอร์และป้อนที่อยู่ IP ของเราเตอร์ของคุณ (โดยปกติคือ 192.168.0.1 หรือ 192.168.1.1) หรือใช้ปุ่มที่ด้านล่างของหน้านี้ เข้าสู่ระบบด้วยชื่อผู้ใช้และรหัสผ่านของผู้ดูแลระบบของคุณ (ตรวจสอบฉลากหรือคู่มือของเราเตอร์หากคุณไม่แน่ใจ)
2. ค้นหาการตั้งค่า: มองหาในส่วนต่างๆ เช่น การตั้งค่าไร้สาย การตั้งค่าขั้นสูง หรือความปลอดภัย มองหาตัวเลือกที่ชื่อว่า “AP Isolation”, “Client Isolation”, “Wireless Isolation” หรือ “Intra-BSS Blocking”
3. ปิดการใช้งาน: ตรวจสอบให้แน่ใจว่าการตั้งค่านี้ถูกปิด บันทึกการเปลี่ยนแปลงและรีบูตเราเตอร์หากจำเป็น
เมื่อปิดใช้งานแล้ว อุปกรณ์ในเครือข่าย Wi-Fi ของคุณ (เช่น โทรศัพท์และ Mac) จะสามารถค้นหาและเชื่อมต่อกันได้ โปรดทราบ: หากคุณอยู่ในเครือข่ายสำหรับแขก การตั้งค่านี้อาจไม่สามารถใช้งานได้ ในกรณีนั้น ให้ใช้เครือข่ายอื่น หรือดาวน์โหลดแอป QuickDrop บนโทรศัพท์ของคุณ และเชื่อมต่อ Mac ของคุณกับฮอตสปอตของโทรศัพท์เพื่อถ่ายโอนไฟล์ ค้นหา “QuickDrop” บน Google Play เพื่อดาวน์โหลดแอป";
"ApIsolationAction" = "เปิดการตั้งค่าเราเตอร์";
"ApIsolationFindRouterFailedHeader" = "ไม่สามารถค้นหา IP ของเราเตอร์";
"ApIsolationFindRouterFailedDescription" = "QuickDrop ไม่สามารถกำหนดที่อยู่ของเราเตอร์ของคุณได้ โปรดดูคู่มือผู้ใช้ของเราเตอร์ Wi-Fi ของคุณเพื่อรับที่อยู่";
"FirewallHeader" = "ไฟร์วอลล์อาจบล็อกการรับส่งไฟล์ขาเข้า";
"FirewallDescription" = "QuickDrop ตรวจพบว่าไฟร์วอลล์อาจป้องกันไม่ให้อุปกรณ์ Android ของคุณเชื่อมต่อกับ Mac ของคุณ โดยปกติไฟร์วอลล์จะบล็อกการเชื่อมต่อขาเข้าตามค่าเริ่มต้น ในการรับไฟล์ จำเป็นต้องอนุญาตการเชื่อมต่อขาเข้า เนื่องจากโทรศัพท์ของคุณพยายามเชื่อมต่อกับ Mac ของคุณ ซึ่งเป็นการสร้างการเชื่อมต่อขาเข้า
คุณอาจใช้โปรแกรมป้องกันไวรัสบน Mac ของคุณ คุณมีสองตัวเลือก:
1. อนุญาตการเชื่อมต่อขาเข้าสำหรับ QuickDrop ในการตั้งค่าไฟร์วอลล์ของคุณ ขั้นตอนนี้แตกต่างกันไปในแต่ละโปรแกรม ดังนั้นคุณอาจต้องสอบถามผู้ผลิตซอฟต์แวร์ไฟร์วอลล์/ป้องกันไวรัสของคุณว่าทำได้อย่างไร
2. ปิดใช้งานไฟร์วอลล์ของซอฟต์แวร์ป้องกันไวรัสของคุณ และใช้ไฟร์วอลล์ในตัวของ Mac แทน ไฟร์วอลล์ของ Apple ได้รับการปรับแต่งให้เหมาะสมสำหรับ QuickDrop
วิธีปิดใช้งานไฟร์วอลล์ของโปรแกรมป้องกันไวรัสของคุณ: เปิดการตั้งค่าระบบบน Mac ของคุณ ไปที่ เครือข่าย > VPN และตัวกรอง และปิดใช้งานตัวกรองเนื้อหา จากนั้นไปที่ เครือข่าย > ไฟร์วอลล์ และเปิดใช้งานไฟร์วอลล์ของ Mac
เมื่อเสร็จสิ้น โทรศัพท์ของคุณจะสามารถเชื่อมต่อกับ Mac ของคุณได้";
"NetworkFilterHeader" = "ตัวกรองแพ็กเก็ตอาจขัดขวางการถ่ายโอนไฟล์";
"NetworkFilterDescription" = "QuickDrop ตรวจพบว่ามีโปรแกรมที่ติดตั้งใน Mac ของคุณแก้ไขทราฟฟิกเครือข่าย ซึ่งทำให้ไฟล์ที่คุณพยายามส่งจากอุปกรณ์ Android ไม่สามารถถอดรหัสได้
Mac ของคุณอาจเปิดใช้งานตัวกรองแพ็กเก็ตที่มีปัญหา ซึ่งอาจติดตั้งโดยโปรแกรมป้องกันไวรัส (เช่น ESET) ให้เปิดการตั้งค่าระบบ > เครือข่าย > VPN และตัวกรอง แล้วปิดตัวกรองเนื้อหา
เมื่อทำเสร็จแล้ว คุณควรสามารถถ่ายโอนไฟล์ได้สำเร็จ";
"TransmitUsingCable" = "ถ่ายโอนด้วยสายเคเบิล";
"DownloadCableConnectionApp" = "ดาวน์โหลด iDroid";
"DownloadCableConnectionAppDescription" = "หากคุณต้องการถ่ายโอนไฟล์และเรียกดูที่เก็บข้อมูลของอุปกรณ์ Android ผ่าน USB ตอนนี้คุณสามารถทำได้ด้วยแอป iDroid ใหม่\n\niDroid รองรับการดาวน์โหลดและอัปโหลดไฟล์และโฟลเดอร์ทั้งหมดด้วยการลากและวาง";
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFiles</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@files@</string>
<key>files</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d ไฟล์</string>
<key>other</key>
<string>%d ไฟล์</string>
</dict>
</dict>
</dict>
</plist>
-193
View File
@@ -1,193 +0,0 @@
/* No comment provided by engineer. */
"Accept" = "Прийняти";
/* No comment provided by engineer. */
"Decline" = "Відхилити";
/* No comment provided by engineer. */
"DeviceName" = "Імʼя пристрою: %@";
/* No comment provided by engineer. */
"DeviceSendingFiles" = "%1$@ відправляє Вам %2$@";
"DeviceCurrentlySendingText" = "%1$@ вставив \"%2$@\" до вашого буфера обміну.";
"DeviceSendingText" = "%1$@ хоче вставити \"%2$@\" до буфера обміну вашого Mac.";
"DeviceCurrentlySendingUrl" = "%1$@ відкрив \"%2$@\".";
"DeviceSendingUrl" = "%1$@ хоче відкрити \"%2$@\" на вашому Mac.";
"InsertedIntoClipboard" = "Вставлено";
"UnsupportedFileType" = "Файл, який ви намагалися надіслати, наразі не підтримується. Будь ласка, надішліть звіт про помилку.";
/* No comment provided by engineer. */
"Error.Crypto" = "Помилка шифрування";
/* No comment provided by engineer. */
"Error.Protocol" = "Помилка протоколу";
/* No comment provided by engineer. */
"NFiles" = "%d files";
/* No comment provided by engineer. */
"NotificationsDenied.Message" = "QuickDrop потрібна можливість показувати сповіщення про вхідні файли. Будь ласка, дозвольте сповіщення у Системних налаштуваннях.";
/* No comment provided by engineer. */
"NotificationsDenied.OpenSettings" = "Відкрити налаштування";
/* No comment provided by engineer. */
"NotificationsDenied.Title" = "Потрібен доступ до сповіщень";
/* No comment provided by engineer. */
"PinCode" = "PIN: %@";
/* No comment provided by engineer. */
"Quit" = "Завершити QuickDrop";
/* No comment provided by engineer. */
"TransferError" = "Не вдалося отримати файли від %@";
/* No comment provided by engineer. */
"VisibleToEveryone" = "Видимість: для всіх";
"NoNetworkConnection" = "Немає мережевого підключення";
"RecommendedApps" = "Рекомендовані додатки";
"GetSupport" = "Підтримка";
"PrivacyPolicy" = "Політика конфіденційності";
"ReadyToReceive" = "Готовий до отримання";
"SendClipboard" = "Надіслати буфер обміну";
"Clipboard" = "Буфер обміну";
"DeviceCurrentlySendingFiles" = "%1$@ надсилає вам %2$@";
"UserManual" = "Керівництво користувача та налаштування";
"Settings" = "Налаштування";
"AutomaticallyAcceptFiles" = "Автоматично приймати вхідні файли";
"AutomaticallyAcceptFilesFooter" = "Увімкнення цього параметра рекомендується лише у вашій приватній мережі. Будь-хто в тій самій мережі може надсилати файли на ваш пристрій без вашої згоди.";
"SaveFilesTo" = "Зберігати вхідні файли у:";
"SaveFilesLocation" = "Розташування вхідних файлів";
"SaveFilesToButton" = "Вибрати…";
"OpenFinderAfterReceiving" = "Показати файли у Finder після отримання";
"DownloadsFolder" = "Завантаження";
"SendFiles" = "Надіслати файли";
"ReceiveFiles" = "Отримати файли";
"DeviceNotShown" = "Пристрій не відображається?";
"Troubleshooting" = "Усунення несправностей";
"AndroidApp" = "Додаток для Android";
"AndroidAppDescription" = "QuickDrop тепер також доступний як окремий додаток для вашого Android-пристрою! Хоча QuickDrop можна використовувати і без Android-додатка, QuickDrop для Android робить передачу файлів ще простішою та швидшою. З Android-додатком ви навіть можете передавати файли, не підключаючись до домашньої Wi-Fi мережі, підключивши Mac до точки доступу Android. Додаток Android також забезпечує шифрування даних від кінця до кінця під час передачі файлів.
QuickDrop для Android наразі знаходиться в бета-версії. Щоб зареєструватися для участі в бета-тестуванні, надішліть електронний лист на quickdrop-beta@leonboettger.com або скористайтеся кнопкою нижче. Дякуємо за інтерес!";
"JoinBeta" = "Приєднатися до бета-версії QuickDrop для Android";
"BetaEmailBottom" = "Привіт,
будь ласка, додайте мене до бета-версії QuickDrop для Android.
–––––––––––––––
Будь ласка, вкажіть нижче адресу електронної пошти вашого облікового запису Google, щоб вас можна було запросити до бета-версії. Після цього ви отримаєте електронного листа з посиланням для завантаження додатка з Google Play Store.
Ваша електронна адреса буде використана лише для цієї мети і не буде збережена або передана іншим.
Ваша електронна адреса: ";
"SendFilesDescription" = "Щоб надіслати файл на ваш Android-пристрій, натисніть на значок \"Поділитися\" або клацніть файл правою кнопкою миші та виберіть \"Поділитися\". Потім виберіть \"QuickDrop\" зі списку програм і дотримуйтесь інструкцій.
Якщо ви не бачите QuickDrop у списку, натисніть \"Редагувати розширення\", прокрутіть вниз і увімкніть QuickDrop.
Ви також можете надіслати поточний вміст буфера обміну вашого Mac на Android-пристрій, натиснувши на значок QuickDrop на панелі меню та вибравши \"Надіслати буфер обміну\".";
"WelcomeToQuickDrop" = "Ласкаво просимо до QuickDrop";
"Acknowledgements" = "Подяки";
"SupportQuickDrop" = "Підтримати QuickDrop";
"UserManualDescription" = "Коли QuickDrop працює, він з'являється в рядку меню у верхній частині екрана. Натисніть на іконку QuickDrop, якщо хочете припинити отримання файлів.
Щоб надіслати файли з вашого пристрою Android на Mac, торкніться значка спільного доступу на телефоні та виберіть \"Quick Share\". Ваш Mac з'явиться у списку доступних пристроїв. Файли будуть збережені в папці «Завантаження».
Важливо: ваш пристрій Android і Mac повинні бути в одній WiFi-мережі для передачі файлів.";
"TroubleshootingDescription" = "Якщо ваш Mac не відображається на вашому Android-пристрої, навіть якщо обидва пристрої підключені до однієї Wi-Fi мережі, або передача не вдалася, спробуйте наступне:
1) Відкрийте Системні налаштування та перейдіть до \"Конфіденційність і безпека\" > \"Локальна мережа\". Переконайтеся, що QuickDrop увімкнено. Якщо ви використовуєте сторонній брандмауер або антивірусну програму, перегляньте документацію цієї програми, щоб дозволити вхідні з'єднання для QuickDrop.
2) Перезапустіть QuickDrop, натиснувши на значок у рядку меню, виберіть останній пункт і знову відкрийте додаток. Перезапустіть ваш Android-пристрій.
Примітка: Деякі мережі не дозволяють пристроям спілкуватися один з одним. У цьому випадку вам потрібно завантажити додаток QuickDrop для вашого телефону та підключити Mac до точки доступу вашого телефону для передачі файлів. Посилання на додаток можна знайти в бічній панелі зліва.";
"plusview_title" = "Дякуємо за використання QuickDrop!";
"plusview_description" = "Розблокуйте QuickDrop+, щоб продовжити обмін файлами без обмежень. QuickDrop+ – це одноразова покупка, підписка не потрібна. Всі поточні та майбутні функції включені.";
"plusview_success_title" = "Дякуємо за покупку!";
"plusview_success_description" = "Тепер ви можете продовжувати отримувати файли без обмежень. Усі файли, які ви намагалися надіслати раніше, вже збережено на вашому пристрої.";
"plusview_success_button" = "Зрозуміло!";
"plusview_unlock_label" = "Підтримати QuickDrop";
"plusview_restorepurchase" = "Вже куплено?";
"plusview_restorefailed" = "Сталася помилка під час відновлення покупки";
"plusview_restorefailed_proceed" = "Гаразд";
"plusview_purchasefailed" = "Помилка покупки";
"plusview_purchasefaileddescription" = "Якщо це повідомлення з’явилося несподівано, переконайтеся, що у вас є підключення до Інтернету, а покупки в додатках увімкнені на вашому пристрої, і спробуйте ще раз.";
"plusview_nothingtorestore" = "Немає покупок для відновлення!";
"plusview_productRequestFailed" = "Наразі неможливо отримати інформацію про покупки в додатку.";
"Connecting" = "Підключення...";
"NotEnoughSpace" = "Недостатньо місця на диску";
"Sending" = "Надсилання...";
"TransferCanceled" = "Скасовано";
"TransferDeclined" = "Відхилено";
"TransferFinished" = "Передача завершена";
"TransferTimedOut" = "Час очікування вийшов";
"UnsupportedType" = "Тип вкладення не підтримується";
"QrCodeInstructions" = "Скануйте QR-код за допомогою вашого Android-пристрою, щоб підключити його до Mac. Переконайтесь, що телефон підключений до тієї ж мережі.\n\nЯкщо ви не хочете сканувати QR-код під час кожної передачі файлів, встановіть додаток QuickDrop (див. вище).";
"ConnectWithQuickDropApp" = "Підключитися за допомогою додатка QuickDrop";
"ConnectWithoutApp" = "Підключитися без додатка";
"QrCodeInstructionsApp" = "Щоб підключити Mac до пристрою Android, відкрийте додаток QuickDrop на вашому Android-пристрої. Переконайтеся, що ваш телефон підключено до тієї ж мережі.\n\nЩоб встановити QuickDrop на телефон, відскануйте QR-код або знайдіть 'QuickDrop' у Google Play Store.";
"QuickDropAndroidAppAdvertisement" = "QuickDrop тепер також доступний як окремий додаток для вашого смартфона! Додаток надає додаткові функції, такі як можливість передавати файли, коли ви не підключені до домашньої Wi-Fi мережі, підключивши Mac до точки доступу вашого смартфона.\n\nЩоб встановити QuickDrop на телефон, відскануйте QR-код або знайдіть 'QuickDrop' у Google Play Store.";
"TypeNotSupported" = "Тип не підтримується";
"TypeNotSupportedDescription" = "Вибраний тип файлу не підтримується. Якщо ви все одно хочете його надіслати, клацніть файл правою кнопкою миші, виберіть \"Стиснути\", а потім надішліть стиснутий файл.";
"TypeNotSupportedButton" = "Гаразд";
"CloseAlert" = "Закрити";
"InformDeveloper" = "Надіслати звіт про помилку";
"TimeoutTitle" = "Щось пішло не так";
"TimeoutDescription" = "QuickDrop не вдалося підключитися до вашого телефону. Відкрийте налаштування Android і знайдіть 'Quick Share'. Змініть параметр 'Хто може ділитися з вами' на 'Усі' і спробуйте знову.";
"TimeoutButton" = "Гаразд";
"ApIsolationHeader" = "Ваш маршрутизатор може забороняти обмін даними між пристроями";
"ApIsolationDescription" = "QuickDrop виявив, що ваш Wi-Fi маршрутизатор може блокувати взаємодію між пристроями. Щоб передавати файли між телефоном і Mac, вам потрібно вимкнути ізоляцію клієнтів (також відому як AP-ізоляція або Wi-Fi ізоляція).
Ось як вимкнути її:
1. Увійдіть до маршрутизатора: Відкрийте веб-браузер і введіть IP-адресу вашого маршрутизатора (зазвичай 192.168.0.1 або 192.168.1.1), або скористайтеся кнопкою внизу сторінки. Увійдіть, використовуючи адміністративне ім’я користувача та пароль. (Перевірте етикетку на маршрутизаторі або інструкцію, якщо ви не впевнені.)
2. Знайдіть відповідне налаштування: Перевірте розділи \"Бездротові налаштування\", \"Розширені налаштування\" або \"Безпека\". Шукайте параметри з назвами \"AP Isolation\", \"Client Isolation\", \"Wireless Isolation\" або \"Intra-BSS Blocking\".
3. Вимкніть її: Переконайтесь, що це налаштування вимкнено. Збережіть зміни й перезавантажте маршрутизатор, якщо це необхідно.
Після вимкнення пристрої у вашій Wi-Fi мережі (наприклад, телефон і Mac) зможуть знаходити одне одного і з’єднуватись. Зверніть увагу: якщо ви перебуваєте в гостьовій мережі, цей параметр може бути недоступний. У такому випадку використовуйте іншу мережу або завантажте додаток QuickDrop на свій телефон і підключіть Mac до точки доступу телефону для передавання файлів. Знайдіть “QuickDrop” у Google Play, щоб завантажити додаток.";
"ApIsolationAction" = "Відкрити налаштування маршрутизатора";
"ApIsolationFindRouterFailedHeader" = "Не вдалося знайти IP-адресу маршрутизатора";
"ApIsolationFindRouterFailedDescription" = "QuickDrop не зміг визначити адресу вашого маршрутизатора. Будь ласка, зверніться до інструкції користувача вашого Wi-Fi маршрутизатора, щоб дізнатися її.";
"FirewallHeader" = "Брандмауер може блокувати вхідні передачі файлів";
"FirewallDescription" = "QuickDrop виявив, що брандмауер може заважати підключенню вашого пристрою Android до вашого Mac. Брандмауери за замовчуванням блокують вхідні з'єднання. Щоб отримувати файли, необхідно дозволити вхідні з'єднання, оскільки ваш телефон намагається підключитися до вашого Mac – створюючи вхідне з'єднання.
Ймовірно, ви використовуєте антивірусну програму на вашому Mac. У вас є два варіанти:
1. Дозвольте вхідні з'єднання для QuickDrop у налаштуваннях брандмауера. Ця процедура відрізняється залежно від програми, тому вам, можливо, доведеться звернутися до виробника вашого програмного забезпечення брандмауера/антивірусу, щоб дізнатися, як це зробити.
2. Вимкніть брандмауер вашого антивірусного програмного забезпечення та використовуйте вбудований брандмауер Mac. Брандмауер Apple оптимізований для QuickDrop.
Ось як вимкнути брандмауер вашої антивірусної програми: Відкрийте Системні налаштування на вашому Mac, перейдіть до Мережа > VPN і Фільтр, і вимкніть фільтр вмісту. Потім перейдіть до Мережа > Брандмауер і увімкніть брандмауер Mac.
Після цього ваш телефон зможе підключитися до вашого Mac.";
"NetworkFilterHeader" = "Фільтр пакетів може заважати передачі файлів";
"NetworkFilterDescription" = "QuickDrop виявив, що програма, встановлена на вашому Mac, змінює мережевий трафік. У результаті цього файл, який ви намагалися надіслати з Android-пристрою, не вдалося розшифрувати.
На вашому Mac може бути активований несправний фільтр пакетів, встановлений антивірусною програмою (наприклад, ESET). Відкрийте Системні налаштування > Мережа > VPN і Фільтр та вимкніть фільтр вмісту.
Після цього передача файлів має працювати коректно.";
"TransmitUsingCable" = "Передача через кабель";
"DownloadCableConnectionApp" = "Завантажити iDroid";
"DownloadCableConnectionAppDescription" = "Якщо ви хочете передавати файли та переглядати сховище вашого Android-пристрою через USB, тепер це можливо з новим додатком iDroid.\n\niDroid підтримує завантаження та надсилання файлів і цілих папок через перетягування.";
@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFiles</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@files@</string>
<key>files</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d файл</string>
<key>few</key>
<string>%d файли</string>
<key>many</key>
<string>%d файлів</string>
<key>other</key>
<string>%d файлів</string>
</dict>
</dict>
</dict>
</plist>
-193
View File
@@ -1,193 +0,0 @@
/* No comment provided by engineer. */
"Accept" = "接受";
/* No comment provided by engineer. */
"Decline" = "拒绝";
/* No comment provided by engineer. */
"DeviceName" = "设备名称: %@";
/* No comment provided by engineer. */
"DeviceSendingFiles" = "%1$@ 正向你发送 %2$@";
"DeviceCurrentlySendingText" = "%1$@ 已将 \"%2$@\" 插入到你的剪贴板。";
"DeviceSendingText" = "%1$@ 想要将 \"%2$@\" 插入到你的 Mac 剪贴板。";
"DeviceCurrentlySendingUrl" = "%1$@ 已打开 \"%2$@\"。";
"DeviceSendingUrl" = "%1$@ 想要在你的 Mac 上打开 \"%2$@\"。";
"InsertedIntoClipboard" = "已插入";
"UnsupportedFileType" = "您尝试发送的文件当前不受支持。请发送错误报告。";
/* No comment provided by engineer. */
"Error.Crypto" = "加密出错";
/* No comment provided by engineer. */
"Error.Protocol" = "通信出错";
/* No comment provided by engineer. */
"NFiles" = "%d files";
/* No comment provided by engineer. */
"NotificationsDenied.Message" = "QuickDrop 需显示是否接收文件传输的通知。请在 系统设置 中允许通知。";
/* No comment provided by engineer. */
"NotificationsDenied.OpenSettings" = "打开设置";
/* No comment provided by engineer. */
"NotificationsDenied.Title" = "需要通知权限";
/* No comment provided by engineer. */
"PinCode" = "PIN: %@";
/* No comment provided by engineer. */
"Quit" = "退出 QuickDrop";
/* No comment provided by engineer. */
"TransferError" = "无法从 %@ 接收文件";
/* No comment provided by engineer. */
"VisibleToEveryone" = "对所有人可见";
"NoNetworkConnection" = "无网络连接";
"RecommendedApps" = "推荐应用";
"GetSupport" = "支持";
"PrivacyPolicy" = "隐私政策";
"ReadyToReceive" = "准备接收";
"SendClipboard" = "发送剪贴板";
"Clipboard" = "剪贴板";
"DeviceCurrentlySendingFiles" = "%1$@ 正在向你发送 %2$@";
"UserManual" = "用户手册与设置";
"Settings" = "设置";
"AutomaticallyAcceptFiles" = "自动接受传入文件";
"AutomaticallyAcceptFilesFooter" = "仅建议在您的私人网络中启用此设置。同一网络中的任何人都可以在未经您同意的情况下向您的设备发送文件。";
"SaveFilesTo" = "保存传入文件到:";
"SaveFilesLocation" = "传入文件位置";
"SaveFilesToButton" = "选择…";
"OpenFinderAfterReceiving" = "接收后在访达中显示文件";
"DownloadsFolder" = "下载";
"SendFiles" = "发送文件";
"ReceiveFiles" = "接收文件";
"DeviceNotShown" = "设备未显示?";
"Troubleshooting" = "故障排除";
"AndroidApp" = "Android 应用程序";
"AndroidAppDescription" = "QuickDrop 现在也作为适用于 Android 设备的独立应用提供!虽然无需 Android 应用也能使用 QuickDrop,但 QuickDrop for Android 让文件传输更加简单快捷。使用 Android 应用,即使你未连接家庭 Wi-Fi 网络,也可以通过将 Mac 连接到 Android 热点来传输文件。Android 应用在传输文件时仍然提供端到端加密。
QuickDrop for Android 当前处于测试阶段。要注册测试版,请发送电子邮件至 quickdrop-beta@leonboettger.com,或点击下面的按钮。感谢你的关注!";
"JoinBeta" = "加入 QuickDrop for Android 测试版";
"BetaEmailBottom" = "您好,
请将我添加到 QuickDrop for Android 测试版中。
–––––––––––––––
请在下方提供您的 Google 帐户的电子邮件地址,以便您可以被邀请加入测试版。之后,您将收到一封电子邮件,其中包含从 Google Play 商店下载该应用程序的链接。
您的电子邮件地址仅用于此目的,不会被存储或共享。
您的电子邮件地址:";
"SendFilesDescription" = "要将文件发送到您的 Android 设备,请点击共享图标或右键点击文件并选择“共享”。然后,从应用列表中选择“QuickDrop”,并按照说明进行操作。
如果列表中没有 QuickDrop,请点击“编辑扩展”,向下滚动并启用 QuickDrop。
您还可以通过点击菜单栏中的 QuickDrop 图标并选择“发送剪贴板”,将 Mac 剪贴板的当前内容发送到您的 Android 设备。";
"WelcomeToQuickDrop" = "欢迎使用 QuickDrop";
"Acknowledgements" = "致谢";
"SupportQuickDrop" = "支持 QuickDrop";
"UserManualDescription" = "当 QuickDrop 运行时,它会显示在屏幕顶部的菜单栏中。如果您想停止接收文件,请点击 QuickDrop 图标。
要将文件从 Android 设备发送到 Mac,请点击手机上的共享图标,然后选择“Quick Share”。您的 Mac 将列为可用设备。文件将保存在“下载”文件夹中。
重要:您的 Android 设备和 Mac 需要连接到同一个 WiFi 网络才能传输文件。";
"TroubleshootingDescription" = "如果您的 Mac 没有显示在 Android 设备上,即使两个设备已连接到同一个 Wi-Fi 网络,或者传输失败,请尝试以下操作:
1)打开系统设置,前往“隐私与安全性”>“本地网络”。确保已启用 QuickDrop。如果您使用的是第三方防火墙或防病毒程序,请查阅相关文档,允许 QuickDrop 的传入连接。
2)点击菜单栏图标重新启动 QuickDrop,选择最后一个选项并重新打开应用。重启您的 Android 设备。
注意:某些网络不允许设备之间通信。在这种情况下,请在手机上下载 QuickDrop 应用,并将 Mac 连接到手机的热点以进行文件传输。应用下载链接位于左侧边栏中。";
"plusview_title" = "感谢使用 QuickDrop!";
"plusview_description" = "解锁 QuickDrop+ 以无限制地交换文件。QuickDrop+ 是一次性购买 – 无需订阅。所有当前和未来的功能均包含在内。";
"plusview_success_title" = "感谢您的购买!";
"plusview_success_description" = "您现在可以继续无限制地接收文件。您之前尝试发送的所有文件已保存到您的设备上。";
"plusview_success_button" = "知道了!";
"plusview_unlock_label" = "支持 QuickDrop";
"plusview_restorepurchase" = "已购买?";
"plusview_restorefailed" = "恢复购买时出现问题";
"plusview_restorefailed_proceed" = "确定";
"plusview_purchasefailed" = "购买失败";
"plusview_purchasefaileddescription" = "如果此消息意外出现,请确保您已连接到互联网,并且设备已启用应用内购买功能,然后重试。";
"plusview_nothingtorestore" = "没有可恢复的购买!";
"plusview_productRequestFailed" = "暂时无法获取应用内购买信息。";
"Connecting" = "正在连接...";
"NotEnoughSpace" = "磁盘空间不足";
"Sending" = "正在发送...";
"TransferCanceled" = "已取消";
"TransferDeclined" = "已拒绝";
"TransferFinished" = "传输完成";
"TransferTimedOut" = "超时";
"UnsupportedType" = "不支持的附件类型";
"QrCodeInstructions" = "使用您的 Android 设备扫描二维码以将其连接到您的 Mac。请确保您的手机连接到同一网络。\n\n如果您不想每次传输文件都扫描二维码,请安装 QuickDrop 应用(见上方)。";
"ConnectWithQuickDropApp" = "使用 QuickDrop 应用连接";
"ConnectWithoutApp" = "无需应用连接";
"QrCodeInstructionsApp" = "要将您的 Mac 连接到 Android 设备,请在 Android 设备上打开 QuickDrop 应用。确保您的手机连接到同一网络。\n\n要在手机上安装 QuickDrop,请扫描二维码或在 Google Play 商店中搜索“QuickDrop”。";
"QuickDropAndroidAppAdvertisement" = "QuickDrop 现已作为独立应用提供,适用于您的智能手机!该应用提供了额外功能,例如在未连接到家庭 Wi-Fi 网络时,通过将 Mac 连接到智能手机的热点来传输文件。\n\n要在手机上安装 QuickDrop,请扫描二维码或在 Google Play 商店中搜索“QuickDrop”。";
"TypeNotSupported" = "不支持的类型";
"TypeNotSupportedDescription" = "所选文件类型不受支持。如果仍要分享,请右键点击文件,选择“压缩”,然后发送压缩后的文件。";
"TypeNotSupportedButton" = "好的";
"CloseAlert" = "关闭";
"InformDeveloper" = "发送错误报告";
"TimeoutTitle" = "出现问题";
"TimeoutDescription" = "QuickDrop 无法连接到您的手机。请打开 Android 设备的设置并搜索“快速分享”(Quick Share)。将“谁可以与您分享”更改为“所有人”,然后重试。";
"TimeoutButton" = "确定";
"ApIsolationHeader" = "您的路由器可能不允许设备之间的通信";
"ApIsolationDescription" = "QuickDrop检测到您的Wi-Fi路由器可能阻止设备之间的通信。要在手机和Mac之间传输文件,您需要禁用客户端隔离(也称为AP隔离或Wi-Fi隔离)。
以下是禁用的方法:
1. 登录您的路由器:打开网页浏览器,输入您的路由器IP地址(通常是192.168.0.1或192.168.1.1),或使用本页面底部的按钮。使用管理员用户名和密码登录。(如果不确定,请查看路由器标签或手册。)
2. 查找设置:查看“无线设置”、“高级设置”或“安全”等部分。查找名为“AP隔离”、“客户端隔离”、“无线隔离”或“Intra-BSS阻止”的选项。
3. 禁用它:确保该设置已关闭。保存更改并在必要时重新启动路由器。
禁用后,您的Wi-Fi网络中的设备(如手机和Mac)将能够相互发现并连接。请注意:如果您使用的是访客网络,则此设置可能不可用。在这种情况下,请使用其他网络,或在手机上下载 QuickDrop 应用,并将您的 Mac 连接到手机的热点以传输文件。在 Google Play 上搜索“QuickDrop”以下载该应用。";
"ApIsolationAction" = "打开路由器设置";
"ApIsolationFindRouterFailedHeader" = "无法找到路由器IP";
"ApIsolationFindRouterFailedDescription" = "QuickDrop无法确定您的路由器地址。请参考您的Wi-Fi路由器用户手册以获取其地址。";
"FirewallHeader" = "防火墙可能会阻止传入的文件传输";
"FirewallDescription" = "QuickDrop 检测到防火墙可能会阻止您的 Android 设备连接到您的 Mac。防火墙默认用于阻止传入连接。为了接收文件,必须允许传入连接,因为您的手机尝试连接到您的 Mac – 创建一个传入连接。
您很可能在 Mac 上使用了防病毒程序。您有两个选项:
1. 在防火墙设置中允许 QuickDrop 的传入连接。此过程因程序而异,因此您可能需要咨询防火墙/防病毒软件制造商,了解如何操作。
2. 禁用防病毒软件的防火墙,改用 Mac 的内置防火墙。Apple 的防火墙针对 QuickDrop 进行了优化。
以下是禁用防病毒程序防火墙的方法:在 Mac 上打开系统设置,导航到网络 > VPN 和过滤器,禁用内容过滤器。然后,导航到网络 > 防火墙,点击切换以启用 Mac 的防火墙。
完成后,您的手机将能够连接到您的 Mac。";
"NetworkFilterHeader" = "数据包过滤器可能会阻止文件传输";
"NetworkFilterDescription" = "QuickDrop 检测到您的 Mac 上安装的某个程序修改了网络流量。因此,您尝试从 Android 设备发送的文件无法解密。
您的 Mac 上可能启用了有问题的数据包过滤器,该过滤器可能由杀毒软件(如 ESET)安装。请打开系统设置 > 网络 > VPN 与过滤器,并关闭内容过滤器。
完成这些操作后,您应该可以成功传输文件。";
"TransmitUsingCable" = "通过数据线传输";
"DownloadCableConnectionApp" = "下载 iDroid";
"DownloadCableConnectionAppDescription" = "如果您想通过 USB 传输文件并浏览 Android 设备的存储空间,现在可以使用全新的 iDroid 应用程序。\n\niDroid 支持通过拖放下载和上传文件及整个文件夹。";
@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFiles</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@files@</string>
<key>files</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>%d 个文件</string>
</dict>
</dict>
</dict>
</plist>
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loaded 100 of 131 files, more files were not shown because too many files have changed in this diff. Show more