20 Commits
Author SHA1 Message Date
Leon Böttger 21f0f865b6 Bump version 2025-11-08 09:50:14 +01:00
Leon Böttger 026ebc33db Do not show review on GitHub release 2025-11-08 09:49:22 +01:00
Leon Böttger d01e1d7c34 Refactoring 2025-11-06 20:57:46 +01:00
Leon Böttger 14390035a3 Refactoring 2025-11-06 19:17:23 +01:00
Leon Böttger a3d295cd38 UI improvements 2025-11-06 19:03:16 +01:00
Leon Böttger 0ca2a13d66 Improve Toast View UI 2025-11-05 15:50:12 +01:00
Leon Böttger 05321adb6d Fix: progress bar not shown immediately when receiving many files 2025-11-05 15:09:07 +01:00
Leon Böttger 31b93a34a2 Fix incoming transmission capsule shape 2025-11-05 11:16:39 +01:00
Leon Böttger 261f91bc94 Add menu bar menu icons 2025-11-05 10:18:52 +01:00
Leon Böttger 646da767a7 Show average process for multiple incoming connnections 2025-10-19 14:39:25 +02:00
Leon Böttger 9d1e698d9b Refactoring 2025-10-19 14:20:44 +02:00
Leon Böttger a69a75cb07 Refactoring 2025-10-19 14:01:02 +02:00
Leon Böttger 1ac988a4fc Add progress notification when receiving files 2025-10-06 19:42:06 +02:00
Leon Böttger 673d4418f8 Better localizations 2025-10-06 14:09:47 +02:00
Leon Böttger 779144d26a Fix: iOS app was unable to send URLs 2025-10-06 14:09:10 +02:00
Leon Böttger 0fa585652d Update error handling instructions 2025-10-05 14:16:53 +02:00
Leon Böttger 1b1206d0b8 Comments 2025-10-05 14:01:37 +02:00
Leon Böttger 86afa8a206 Fix: Connection not cleaned up after unexpected close be peer 2025-10-05 13:07:10 +02:00
Leon Böttger dda001cb1a Fix: Connection not cleaned up after FIN was sent 2025-10-05 12:17:34 +02:00
Leon Böttger 4ae1663779 Update Strings 2025-09-23 12:38:07 +02:00
28 changed files with 1066 additions and 423 deletions

No files matched your search

Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -542,7 +542,13 @@ class InboundNearbyConnection: NearbyConnection {
let acceptAutomatically = isAuthenticated || Settings.shared.automaticallyAcceptFiles
DispatchQueue.main.async {
self.delegate?.obtainUserConsent(transfer: metadata, device: self.remoteDeviceInfo!, connection: self, acceptAutomatically: acceptAutomatically)
if acceptAutomatically {
self.delegate?.obtainedUserConsentAutomatically(transfer: metadata, device: self.remoteDeviceInfo!, connection: self)
self.submitUserConsent(accepted: true, trustDevice: false)
}
else {
self.delegate?.obtainUserConsent(transfer: metadata, device: self.remoteDeviceInfo!, connection: self)
}
}
}
}
@@ -557,14 +563,14 @@ class InboundNearbyConnection: NearbyConnection {
}
func submitUserConsent(accepted: Bool, trustDevice: Bool, storeInTemp: Bool = false) {
func submitUserConsent(accepted: Bool, trustDevice: Bool) {
if trustDevice, let peerCertificate = peerCertificate, let remoteDeviceInfo = remoteDeviceInfo {
TrustStore.shared.addTrusted(certificate: peerCertificate, device: remoteDeviceInfo)
}
DispatchQueue.global(qos: .utility).async {
if accepted {
self.acceptTransfer(storeInTemp: storeInTemp)
self.acceptTransfer()
} else {
self.rejectTransfer()
}
@@ -572,16 +578,28 @@ class InboundNearbyConnection: NearbyConnection {
}
private func acceptTransfer(storeInTemp: Bool) {
private func acceptTransfer() {
if currentState == .disconnected {
log("[InboundNearbyConnection \(self.id)] Detected timeout, not accepting transfer")
return
}
let storeInTemp = isFileTransferRestricted()
if storeInTemp {
log("[InboundNearbyConnection \(self.id)] File transfer restrictions detected, storing files in temporary directory.")
delegate?.showPlusScreen()
}
do {
if storeInTemp {
try FileManager.default.createDirectory(at: SaveFilesManager.shared.tempDirectory, withIntermediateDirectories: true)
}
if filesToBeReceived.count >= 1 {
// Show progress bar for file transfers immediately
NearbyConnectionManager.shared.updatedTransferProgress(connection: self, progress: 0)
}
for (id, file) in filesToBeReceived {
let targetURL = storeInTemp ? SaveFilesManager.shared.tempDirectory.appendingPathComponent(file.destinationURL.lastPathComponent) : file.destinationURL
@@ -164,6 +164,8 @@ class NearbyConnection {
private func receiveFrameAsync() {
connection.receive(minimumIncompleteLength: 4, maximumLength: 4) { content, _, isComplete, error in
if self.connectionClosed {
log("[NearbyConnection \(self.id)] Received FIN from peer, connection closed.")
self.connection.cancel()
return
}
if isComplete {
@@ -171,6 +173,7 @@ class NearbyConnection {
self.connectionClosedByPeer {
self.lastError = NearbyError.protocolError("Error.ClosedByPeer".localized())
self.disconnect()
self.connection.cancel()
}
return
}
@@ -201,6 +204,8 @@ class NearbyConnection {
private func receiveFrameAsync(length: UInt32) {
connection.receive(minimumIncompleteLength: Int(length), maximumLength: Int(length)) { [self] content, _, isComplete, _ in
if self.connectionClosed {
log("[NearbyConnection \(self.id)] Received FIN from peer, connection closed.")
connection.cancel()
return
}
if isComplete {
@@ -216,6 +221,7 @@ class NearbyConnection {
self.connectionClosedByPeer {
self.lastError = NearbyError.protocolError("Error.ClosedByPeer".localized())
self.disconnect()
self.connection.cancel()
}
return
@@ -515,6 +521,19 @@ class NearbyConnection {
}
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()
}
func disconnect() {
log("[NearbyConnection \(self.id)] Disconnecting from connection.")
@@ -27,7 +27,8 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
private var mdnsServices: [NetService] = []
private var incomingConnections: [String: InboundNearbyConnection] = [:]
private var foundServices: [String: FoundServiceInfo] = [:]
private var shareExtensionDelegates: [OutboundAppDelegate] = []
private var outboundAppDelegates: [OutboundAppDelegate] = []
private var inboundAppDelegates: [InboundAppDelegate] = []
private var outgoingTransfers: [String: OutgoingTransferInfo] = [:]
private var startedDeviceDiscovery = false
private var startedAdvertising = false
@@ -41,7 +42,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
public let deviceInfo: EndpointInfo
public let endpointID: [UInt8] = generateEndpointID()
public var mainAppDelegate: (any InboundAppDelegate)?
public static let shared = NearbyConnectionManager()
@Published var attachments: AttachmentDetails? = nil
@@ -123,7 +124,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
public func becomeInvisible() {
if !startedAdvertising {
print("[NearbyConnectionManager] Already invisible, ignoring becomeInvisible()")
log("[NearbyConnectionManager] Already invisible, ignoring becomeInvisible()")
return
}
@@ -190,8 +191,17 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
}
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, connection _: InboundNearbyConnection, acceptAutomatically: Bool) {
mainAppDelegate?.obtainUserConsent(transfer: transfer, device: device, acceptAutomatically: acceptAutomatically)
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, connection _: InboundNearbyConnection) {
inboundAppDelegates.forEach { delegate in
delegate.obtainUserConsent(transfer: transfer, device: device)
}
}
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo, connection: InboundNearbyConnection) {
inboundAppDelegates.forEach { delegate in
delegate.obtainedUserConsentAutomatically(transfer: transfer, device: device)
}
}
@@ -199,21 +209,32 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
incomingConnections.removeValue(forKey: connection.id)
if !connection.wasUserRejected {
mainAppDelegate?.connectionWasTerminated(from: connection.remoteDeviceInfo ?? RemoteDeviceInfo(name: "??", type: .unknown), wasPlainTextTransfer: connection.isPlainTextTransfer, error: error)
inboundAppDelegates.forEach { delegate in
delegate.connectionWasTerminated(connectionID: connection.id, from: connection.remoteDeviceInfo ?? RemoteDeviceInfo(name: "UnknownDevice".localized(), type: .unknown), wasPlainTextTransfer: connection.isPlainTextTransfer, error: error)
}
}
}
func showPlusScreen() {
inboundAppDelegates.forEach { delegate in
delegate.showPlusScreen()
}
}
func updatedTransferProgress(connection: InboundNearbyConnection, progress: Double) {
mainAppDelegate?.transferProgress(progress: progress)
inboundAppDelegates.forEach { delegate in
delegate.transferProgress(connectionID: connection.id, progress: progress)
}
}
public func submitUserConsent(transferID: String, accept: Bool, trustDevice: Bool, storeInTemp: Bool = false) {
public func submitUserConsent(transferID: String, accept: Bool, trustDevice: Bool) {
guard let conn = incomingConnections[transferID] else { return }
log("[NearbyConnectionManager] Submitting user consent for transfer \(transferID), accepted: \(accept), store in temp: \(storeInTemp)")
conn.submitUserConsent(accepted: accept, trustDevice: trustDevice, storeInTemp: storeInTemp)
log("[NearbyConnectionManager] Submitting user consent for transfer \(transferID), accepted: \(accept)")
conn.submitUserConsent(accepted: accept, trustDevice: trustDevice)
}
@@ -283,7 +304,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
startedDeviceDiscovery = false
for delegate in shareExtensionDelegates {
for delegate in outboundAppDelegates {
foundServices.values.forEach { service in
guard let device = service.device, let id = device.id else { return }
@@ -294,8 +315,8 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
}
public func addShareExtensionDelegate(_ delegate: OutboundAppDelegate) {
shareExtensionDelegates.append(delegate)
public func addOutboundAppDelegate(_ delegate: OutboundAppDelegate) {
outboundAppDelegates.append(delegate)
for service in foundServices.values {
guard let device = service.device else { continue }
delegate.addDevice(device: device)
@@ -303,14 +324,28 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
}
public func removeShareExtensionDelegate(_ delegate: OutboundAppDelegate) {
shareExtensionDelegates.removeAll(where: { $0 === delegate })
public func addInboundAppDelegate(_ delegate: InboundAppDelegate) {
inboundAppDelegates.append(delegate)
}
public func cancelOutgoingTransfer(id: String) {
guard let transfer = outgoingTransfers[id] else { return }
transfer.connection.cancel()
public func removeOutboundAppDelegate(_ delegate: OutboundAppDelegate) {
outboundAppDelegates.removeAll(where: { $0 === delegate })
}
public func removeInboundAppDelegate(_ delegate: InboundAppDelegate) {
inboundAppDelegates.removeAll(where: { $0 === delegate })
}
public func cancelTransfer(id: String) {
if let transfer = incomingConnections[id] {
transfer.cancel()
}
if let transfer = outgoingTransfers[id] {
transfer.connection.cancel()
}
}
@@ -353,7 +388,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
if qrData == qrCodeAdvertisingToken {
if let deviceInfo = deviceInfo {
for delegate in shareExtensionDelegates {
for delegate in outboundAppDelegates {
delegate.startTransferWithQrCode(device: deviceInfo)
}
}
@@ -365,7 +400,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
guard let name = String.init(data: decryptedName, encoding: .utf8) else { return }
endpointInfo.name = name
let deviceInfo = addFoundDevice(foundService: &foundService, endpointInfo: endpointInfo, endpointID: endpointID)
for delegate in shareExtensionDelegates {
for delegate in outboundAppDelegates {
delegate.startTransferWithQrCode(device: deviceInfo)
}
} catch {
@@ -381,7 +416,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
foundService.device = deviceInfo
foundServices[endpointID] = foundService
for delegate in shareExtensionDelegates {
for delegate in outboundAppDelegates {
delegate.addDevice(device: deviceInfo)
}
@@ -392,7 +427,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 }
for delegate in shareExtensionDelegates {
for delegate in outboundAppDelegates {
delegate.removeDevice(id: endpointID)
}
}
@@ -458,7 +493,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
conn.start()
} catch {
log("[NearbyConnectionManager] Error zipping URLs: \(error)")
shareExtensionDelegates.forEach { delegate in
outboundAppDelegates.forEach { delegate in
delegate.connectionFailed(error: error)
}
}
@@ -68,19 +68,6 @@ class OutboundNearbyConnection: NearbyConnection {
}
}
}
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() {
+38 -4
View File
@@ -12,9 +12,11 @@ import SwiftECC
protocol InboundNearbyConnectionDelegate {
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, connection: InboundNearbyConnection, acceptAutomatically: Bool)
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, connection: InboundNearbyConnection)
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo, connection: InboundNearbyConnection)
func updatedTransferProgress(connection: InboundNearbyConnection, progress: Double)
func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?)
func showPlusScreen()
}
@@ -28,9 +30,11 @@ protocol OutboundNearbyConnectionDelegate {
public protocol InboundAppDelegate: AnyObject {
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, acceptAutomatically: Bool)
func connectionWasTerminated(from device: RemoteDeviceInfo, wasPlainTextTransfer: Bool, error: Error?)
func transferProgress(progress: Double)
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo)
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo)
func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo, wasPlainTextTransfer: Bool, error: Error?)
func transferProgress(connectionID: String, progress: Double)
func showPlusScreen()
}
@@ -133,6 +137,36 @@ public struct TransferMetadata {
case text
case url
}
private func getSummary() -> String {
if let textTitle = textDescription {
return textTitle
} else if files.count == 1 {
return files[0].name
} else {
return String.localizedStringWithFormat("NFiles".localized(), files.count)
}
}
public func getDescription(deviceName: String, alreadyAccepted: Bool) -> String {
let fileStr = getSummary()
switch type {
case .file:
return String(format: (alreadyAccepted ? "DeviceCurrentlySendingFiles" : "DeviceSendingFiles").localized(), arguments: [deviceName, fileStr])
case .text:
return String(format: (alreadyAccepted ? "DeviceCurrentlySendingText" : "DeviceSendingText").localized(), arguments: [deviceName, fileStr])
case .url:
return String(format: (alreadyAccepted ? "DeviceCurrentlySendingUrl" : "DeviceSendingUrl").localized(), arguments: [deviceName, fileStr])
}
}
public func getPinCodeMessage() -> String {
return String(format: "PinCode".localized(), arguments: [pinCode ?? "?"])
}
}
+40 -12
View File
@@ -123,6 +123,7 @@
EB7F54142D78E9E700989564 /* LicenseWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7F54132D78E9E400989564 /* LicenseWindow.swift */; };
EB821C992E76FE5300036963 /* SmallSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8A2D7F7CF4004B3FF1 /* SmallSheetView.swift */; };
EB821C9A2E76FE5300036963 /* SmallSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8A2D7F7CF4004B3FF1 /* SmallSheetView.swift */; };
EB869DB52E92AA3A009AAAD4 /* QuickDropToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB869DB42E92AA33009AAAD4 /* QuickDropToastView.swift */; };
EB886E522E4E19390052B7D9 /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
EB886E532E4E19390052B7D9 /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
EB886E542E4E19390052B7D9 /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
@@ -341,6 +342,7 @@
EB7CD1442D6FB0C100CB0CB5 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
EB7F54112D78E95C00989564 /* TutorialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialView.swift; sourceTree = "<group>"; };
EB7F54132D78E9E400989564 /* LicenseWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicenseWindow.swift; sourceTree = "<group>"; };
EB869DB42E92AA33009AAAD4 /* QuickDropToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickDropToastView.swift; sourceTree = "<group>"; };
EB886E512E4E19390052B7D9 /* HKDF.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKDF.swift; sourceTree = "<group>"; };
EB886E662E4F2F7C0052B7D9 /* License.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = License.swift; sourceTree = "<group>"; };
EB950B2D2E52027F002D68FA /* iOSShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = iOSShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -437,7 +439,7 @@
isa = PBXGroup;
children = (
699DEBA52AB0573200115D22 /* ShareViewController.swift */,
EB950B732E520954002D68FA /* Attachments.swift */,
EB644B872EA50827006C2E6C /* Helpers */,
EB950B762E521712002D68FA /* AttachmentDetails.swift */,
EB5C371E2E36693D0052EFD7 /* View */,
699DEBAA2AB0573200115D22 /* Info.plist */,
@@ -477,7 +479,7 @@
children = (
69DA9A1129E0BF5100A442DA /* AppDelegate.swift */,
EB1325D22E701D8900FD749C /* Settings.swift */,
EB1325EA2E7025D900FD749C /* SelfTest */,
EB644B852EA507CF006C2E6C /* ViewModel */,
EBC05C6A2E33B55100EECC18 /* View */,
EBC05C6B2E33B57100EECC18 /* Util */,
EBC05C702E33B57D00EECC18 /* Plus */,
@@ -570,6 +572,34 @@
path = Model;
sourceTree = "<group>";
};
EB644B852EA507CF006C2E6C /* ViewModel */ = {
isa = PBXGroup;
children = (
EB979F902E50CA23008B8C5C /* SendModel.swift */,
EB97A00E2E511120008B8C5C /* ReceiveModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
};
EB644B862EA50807006C2E6C /* View */ = {
isa = PBXGroup;
children = (
EB979F8E2E50CA23008B8C5C /* ContentView.swift */,
EB979F8F2E50CA23008B8C5C /* FilePicker.swift */,
EB97A0192E51EA9C008B8C5C /* UIAlert.swift */,
EB979F992E50CAA6008B8C5C /* PieProgressView.swift */,
);
path = View;
sourceTree = "<group>";
};
EB644B872EA50827006C2E6C /* Helpers */ = {
isa = PBXGroup;
children = (
EB950B732E520954002D68FA /* Attachments.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
EB950B6C2E5206E2002D68FA /* iOSShareExtension */ = {
isa = PBXGroup;
children = (
@@ -586,12 +616,7 @@
EB97A0162E51DF4F008B8C5C /* Info.plist */,
EB1325FC2E7034B000FD749C /* InfoPlist.xcstrings */,
EB979F8D2E50CA23008B8C5C /* Configuration.swift */,
EB979F8E2E50CA23008B8C5C /* ContentView.swift */,
EB979F8F2E50CA23008B8C5C /* FilePicker.swift */,
EB979F902E50CA23008B8C5C /* SendModel.swift */,
EB97A00E2E511120008B8C5C /* ReceiveModel.swift */,
EB97A0192E51EA9C008B8C5C /* UIAlert.swift */,
EB979F992E50CAA6008B8C5C /* PieProgressView.swift */,
EB644B862EA50807006C2E6C /* View */,
EB979F912E50CA23008B8C5C /* QuickDropiOS.entitlements */,
EB979F922E50CA23008B8C5C /* QuickDropiOSApp.swift */,
);
@@ -605,6 +630,7 @@
EBD2A2162D2860BE00BCB8E2 /* WelcomeScreen.swift */,
EB7F54112D78E95C00989564 /* TutorialView.swift */,
EB7F54132D78E9E400989564 /* LicenseWindow.swift */,
EB869DB42E92AA33009AAAD4 /* QuickDropToastView.swift */,
EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */,
EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */,
EBC05C812E33B58300EECC18 /* IssueViews */,
@@ -620,6 +646,7 @@
EB886E662E4F2F7C0052B7D9 /* License.swift */,
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */,
EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */,
EB1325EA2E7025D900FD749C /* SelfTest */,
);
path = Util;
sourceTree = "<group>";
@@ -938,6 +965,7 @@
EB3643602D73A2A2009CDFF8 /* AudioManager.swift in Sources */,
EB5C38412E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */,
EB5C37B82E377A6F0052EFD7 /* SleepManager.swift in Sources */,
EB869DB52E92AA3A009AAAD4 /* QuickDropToastView.swift in Sources */,
EB5C37B92E377A6F0052EFD7 /* SaveFilesManager.swift in Sources */,
EB5C37BA2E377A6F0052EFD7 /* Helpers.swift in Sources */,
EB05DFBE2E59FC8900EF567A /* TrustedDevicesView.swift in Sources */,
@@ -1239,8 +1267,8 @@
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SHARED_BUILD = 74;
SHARED_VERSION = 2.4.1;
SHARED_BUILD = 84;
SHARED_VERSION = 2.5.4;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -1302,8 +1330,8 @@
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
SHARED_BUILD = 74;
SHARED_VERSION = 2.4.1;
SHARED_BUILD = 84;
SHARED_VERSION = 2.5.4;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -33,8 +33,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/leif-ibsen/BigInt",
"state" : {
"revision" : "afb70a0038bfbba845271b60fa9a58d5840f8017",
"version" : "1.21.0"
"revision" : "8c6f93aa37504b7b1ba3954335b5548a19fbbd82",
"version" : "1.22.0"
}
},
{
@@ -52,7 +52,7 @@
"location" : "https://github.com/leonboe1/LUI.git",
"state" : {
"branch" : "main",
"revision" : "123192312165c23271cd49492ae7e4fc676d7711"
"revision" : "0b862decf853e1f542eeab1f8d548f8ff629deca"
}
},
{
@@ -87,8 +87,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "e3f69fd321d0c9fcdc16fb576a0cdd956675face",
"version" : "1.31.0"
"revision" : "c169a5744230951031770e27e475ff6eefe51f9d",
"version" : "1.33.3"
}
},
{
@@ -124,7 +124,7 @@
"location" : "https://github.com/siteline/swiftui-introspect",
"state" : {
"branch" : "main",
"revision" : "d620dacd1423d9545635eee3fc42c1fa1ab2f665"
"revision" : "11d9782bea80656e8a65a643d6ac6396e4a8662e"
}
}
],
+11 -2
View File
@@ -48,14 +48,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
menu.addItem(NSMenuItem.separator())
let sendClipboardItem = NSMenuItem(title: "SendClipboard".localized(), action: #selector(sendClipboard), keyEquivalent: "")
sendClipboardItem.image = NSImage(systemSymbolName: "doc.on.clipboard", accessibilityDescription: nil)
menu.addItem(sendClipboardItem)
menu.addItem(NSMenuItem.separator())
let userManualItem = NSMenuItem(title: "UserManual".localized(), action: #selector(openWelcomeScreen), keyEquivalent: "")
userManualItem.image = NSImage(systemSymbolName: "gear", accessibilityDescription: nil)
menu.addItem(userManualItem)
menu.addItem(withTitle: "Quit".localized(), action: #selector(NSApplication.terminate(_:)), keyEquivalent: "")
let quitItem = NSMenuItem(title: "Quit".localized(), action: #selector(NSApplication.terminate(_:)), keyEquivalent: "")
quitItem.image = NSImage(systemSymbolName: "xmark", accessibilityDescription: nil)
menu.addItem(quitItem)
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem?.button?.image = NSImage(named: "MenuBarIcon")
@@ -65,7 +69,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
iapManager = IAPManager.sharedInstance
iapManager?.startObserving()
log("[AppDelegate] Downloaded directly: \(DistributionDetector.isDirectDistributionEnabled)")
if DistributionDetector.isDirectDistributionEnabled {
log("[AppDelegate] Downloaded from GitHub.")
}
else {
log("[AppDelegate] Downloaded from App Store.")
}
// app did not lauch before
if !Settings.shared.appLaunchedBefore {
+309 -76
View File
@@ -9555,229 +9555,229 @@
"ar" : {
"stringUnit" : {
"state" : "translated",
"value" : "إذا حدث ذلك مرارًا، يرجى تعطيل وتمكين الواي فاي على هاتفك، أو إعادة تشغيل هاتفك."
"value" : "إذا تكرر هذا الأمر، يرجى إعادة تشغيل هاتفك أو محاولة تنزيل تطبيق QuickDrop للأندرويد من متجر Google Play."
}
},
"ca" : {
"stringUnit" : {
"state" : "translated",
"value" : "Si això succeeix repetidament, desactiva i torna a activar el Wi-Fi al teu telèfon o reinicia el telèfon."
"value" : "Si això passa repetidament, reinicia el telèfon o prova de descarregar l'app QuickDrop per a Android des de Google Play."
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pokud se toto stále opakuje, vypněte a znova aktivujte Wi-Fi na telefonu nebo telefon restartujte."
"value" : "Pokud se to opakuje, restartujte prosím svůj telefon nebo zkuste stáhnout aplikaci QuickDrop pro Android z Google Play."
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hvis dette gentager sig, skal du slå Wi-Fi fra og til på din telefon eller genstarte din telefon."
"value" : "Hvis dette sker gentagne gange, så genstart venligst din telefon eller prøv at downloade QuickDrop Android-appen fra Google Play."
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Wenn dies wiederholt passiert, deaktiviere bitte das WLAN auf deinem Telefon und aktiviere es erneut oder starte dein Telefon neu."
"value" : "Wenn dies wiederholt passiert, starte bitte dein Handy neu oder lade die QuickDrop Android-App aus dem Google Play Store herunter."
}
},
"el" : {
"stringUnit" : {
"state" : "translated",
"value" : "Εάν αυτό συμβαίνει επανειλημμένα, απενεργοποιήστε και ενεργοποιήστε ξανά το Wi-Fi στο τηλέφωνό σας, ή επανεκκινήστε το τηλέφωνό σας."
"value" : "Αν αυτό συμβαίνει επανειλημμένα, επανεκκινήστε το τηλέφωνό σας ή δοκιμάστε να κατεβάσετε την εφαρμογή QuickDrop για Android από το Google Play."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "If this happens repeatedly, please disable and re-enable Wi-Fi on your phone, or restart your phone."
"value" : "If this happens repeatedly, please restart your phone or try downloading the QuickDrop Android app from Google Play."
}
},
"en-AU" : {
"stringUnit" : {
"state" : "translated",
"value" : "If this happens often, try turning Wi-Fi off and on or restarting your phone."
"value" : "If this happens repeatedly, please restart your phone or try downloading the QuickDrop Android app from Google Play Store."
}
},
"en-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "If persistent, disable/re-enable Wi-Fi on your phone or restart the device."
"value" : "If this keeps happening, please restart your phone or try downloading the QuickDrop Android app from Google Play."
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "If this persists, toggle Wi-Fi on/off on your phone or restart your device."
"value" : "If this happens repeatedly, please restart your phone or try downloading the QuickDrop Android app from Google Play Store."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Si esto ocurre repetidamente, desactiva y vuelve a activar el Wi-Fi en tu teléfono, o reinicia tu teléfono."
"value" : "Si esto ocurre repetidamente, reinicia tu teléfono o intenta descargar la app QuickDrop para Android desde Google Play."
}
},
"es-419" : {
"stringUnit" : {
"state" : "translated",
"value" : "Si esto ocurre repetidamente, por favor desactiva y reactiva el Wi-Fi en tu teléfono, o reinicia tu teléfono."
"value" : "Si esto ocurre repetidamente, reinicia tu teléfono o intenta descargar la app QuickDrop para Android desde Google Play."
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Jos tämä toistuu, poista Wi-Fi käytöstä ja ota se uudelleen käyttöön puhelimessasi tai käynnistä puhelimesi uudelleen."
"value" : "Jos tämä tapahtuu toistuvasti, käynnistä puhelin uudelleen tai kokeile ladata QuickDrop Android -sovellus Google Playsta."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Si cela se produit plusieurs fois, veuillez désactiver et réactiver le Wi-Fi sur votre téléphone ou redémarrez votre téléphone."
"value" : "Si cela se produit à plusieurs reprises, veuillez redémarrer votre téléphone ou essayer de télécharger l'application QuickDrop pour Android depuis Google Play."
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Si cela se produit de manière répétée, veuillez désactiver puis réactiver le Wi-Fi sur votre téléphone, ou redémarrez votre téléphone."
"value" : "Si cela se produit à plusieurs reprises, veuillez redémarrer votre téléphone ou essayer de télécharger l'application QuickDrop pour Android depuis Google Play."
}
},
"he" : {
"stringUnit" : {
"state" : "translated",
"value" : "אם זה קורה שוב ושוב, השבת והפעל מחדש את ה-Wi-Fi בטלפון שלך, או אתחל את הטלפון."
"value" : "אם זה קורה שוב ושוב, נא להפעיל את הטלפון מחדש או לנסות להוריד את אפליקציית QuickDrop לאנדרואיד מגוגל פליי."
}
},
"hi" : {
"stringUnit" : {
"state" : "translated",
"value" : "यदि यह बार-बार होता है, तो कृपया अपने फोन पर वाई-फाई को अक्षम और पुन: सक्षम करें, या अपने फोन को पुनरारंभ करें।"
"value" : "यदि ऐसा बार-बार होता है, तो कृपया अपने फोन को रीस्टार्ट करें या Google Play से QuickDrop Android ऐप डाउनलोड करने की कोशिश करें।"
}
},
"hr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ako se ovo često ponavlja, onemogućite i ponovno omogućite Wi-Fi na svom telefonu ili ponovno pokrenite telefon."
"value" : "Ako se ovo ponavlja, ponovno pokrenite svoj telefon ili pokušajte preuzeti QuickDrop Android aplikaciju s Google Playa."
}
},
"hu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ha ez ismétlődik, kapcsold ki majd be újra a Wi-Fi-t a telefonodon, vagy indítsd újra a telefonod."
"value" : "Ha ez ismétlődően előfordul, indítsd újra a telefonodat, vagy próbáld meg letölteni a QuickDrop Android alkalmazást a Google Play-ről."
}
},
"id" : {
"stringUnit" : {
"state" : "translated",
"value" : "Jika ini terjadi berulang kali, harap matikan dan nyalakan kembali Wi-Fi di ponsel Anda, atau mulai ulang ponsel Anda."
"value" : "Jika ini terjadi berulang kali, silakan mulai ulang ponsel Anda atau coba unduh aplikasi QuickDrop untuk Android dari Google Play."
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Se ciò accade ripetutamente, disattiva e riattiva il Wi-Fi sul tuo telefono o riavvia il tuo telefono."
"value" : "Se questo accade ripetutamente, riavvia il telefono o prova a scaricare l'app QuickDrop per Android da Google Play."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "これが繰り返し起こる場合は、お使いの電話でWi-Fiを無効にして再度有効にするか、電話を再起動してください。"
"value" : "この問題が繰り返し発生する場合は、スマートフォンを再起動するか、Google Play から QuickDrop Android アプリをダウンロードしてください。"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "이 문제가 반복되면 휴대폰에서 Wi-Fi를 비활성화하고 다시 활성화하거나 휴대폰을 재시작하세요."
"value" : "이 현상이 반복되면 휴대폰을 재시작하거나 Google Play에서 QuickDrop Android 앱을 다운로드해 보세요."
}
},
"ms" : {
"stringUnit" : {
"state" : "translated",
"value" : "Jika ini berlaku berulang kali, sila matikan dan hidupkan semula Wi-Fi pada telefon anda, atau mulakan semula telefon anda."
"value" : "Jika ini berlaku berulang kali, sila mulakan semula telefon anda atau cuba muat turun aplikasi QuickDrop Android dari Google Play."
}
},
"nb" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hvis dette skjer gjentatte ganger, vennligst deaktiver og aktiver Wi-Fi på telefonen din, eller start telefonen på nytt."
"value" : "Hvis dette skjer gjentatte ganger, start telefonen på nytt eller prøv å laste ned QuickDrop-appen for Android fra Google Play."
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Als dit herhaaldelijk gebeurt, schakel dan Wi-Fi op uw telefoon uit en weer in, of start uw telefoon opnieuw op."
"value" : "Als dit herhaaldelijk gebeurt, start dan je telefoon opnieuw op of download de QuickDrop Android-app via Google Play."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Jeśli to zdarza się wielokrotnie, wyłącz i ponownie włącz Wi-Fi na telefonie lub zrestartuj urządzenie."
"value" : "Jeśli to się powtarza, uruchom ponownie telefon lub spróbuj pobrać aplikację QuickDrop na Androida z Google Play."
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
"value" : "Se isso acontecer repetidamente, desative e reative o Wi-Fi no seu telefone, ou reinicie o seu telefone."
"value" : "Se isso acontecer repetidamente, reinicie seu telefone ou tente baixar o aplicativo QuickDrop para Android na Google Play."
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
"value" : "Se isso acontecer repetidamente, por favor, desative e ative novamente o Wi-Fi do seu telemóvel ou reinicie o seu telemóvel."
"value" : "Se isto acontecer repetidamente, reinicie o seu telemóvel ou tente instalar a aplicação QuickDrop para Android a partir da Google Play."
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dacă se întâmplă în mod repetat, vă rugăm să dezactivați și să reactivați Wi-Fi pe telefonul dvs. sau să reporniți telefonul."
"value" : "Dacă se întâmplă în mod repetat, repornește telefonul sau încearcă să descarci aplicația QuickDrop pentru Android din Google Play."
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "Если это повторяется, пожалуйста, отключите и снова включите Wi-Fi на вашем телефоне или перезагрузите его."
"value" : "Если это происходит неоднократно, перезапустите телефон или попробуйте установить приложение QuickDrop для Android из Google Play."
}
},
"sv" : {
"stringUnit" : {
"state" : "translated",
"value" : "Om detta händer upprepade gånger, inaktivera och återaktivera Wi-Fi på din telefon eller starta om din telefon."
"value" : "Om detta händer upprepade gånger, starta om din telefon eller ladda ner QuickDrop-appen för Android från Google Play."
}
},
"th" : {
"stringUnit" : {
"state" : "translated",
"value" : "ถ้าเกิดเหตุการณ์นี้บ่อย ๆ กรุณาปิดและเปิด Wi-Fi ในโทรศัพท์ของคุณใหม่ หรือรีสตาร์ทโทรศัพท์ของคุณ"
"value" : "หากเกิดเหตุการณ์นี้ซ้ำ โปรดรีสตาร์ทโทรศัพท์หรือดาวน์โหลดแอป QuickDrop สำหรับ Android จาก Google Play"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Eğer bu tekrar ederse, lütfen telefonunuzda Wi-Fi'yi devre dışı bırakıp tekrar etkinleştirin veya telefonunuzu yeniden başlatın."
"value" : "Bu tekrar ederse, lütfen telefonunuzu yeniden başlatın veya QuickDrop Android uygulamasını Google Play'den indirmeyi deneyin."
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
"value" : "Якщо це повторюється, будь ласка, вимкніть і знову увімкніть Wi-Fi на вашому телефоні або перезавантажте телефон."
"value" : "Якщо це трапляється повторно, перезавантажте телефон або спробуйте завантажити додаток QuickDrop для Android з Google Play."
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nếu điều này xảy ra lặp lại, vui lòng tắt và bật lại Wi-Fi trên điện thoại hoặc khởi động lại điện thoại."
"value" : "Nếu điều này xảy ra liên tục, vui lòng khởi động lại điện thoại hoặc thử tải ứng dụng QuickDrop cho Android từ Google Play."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "如果这种情况反复发生,请禁用并重新启用手机上的Wi-Fi,或重启手机。"
"value" : "如果此问题反复出现,请重启手机或尝试从 Google Play 下载 QuickDrop 安卓应用。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "如果此情況反覆發生,請停用並重新啟用手機上的 Wi-Fi,或重啟手機。"
"value" : "如果這種情況重複發生,請重啟您的手機,或嘗試從 Google Play 下載 QuickDrop Android 應用程式。"
}
}
}
@@ -26319,6 +26319,239 @@
}
}
},
"UnknownDevice" : {
"extractionState" : "manual",
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
"value" : "جهاز غير معروف"
}
},
"ca" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dispositiu desconegut"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
"value" : "Neznámé zařízení"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ukendt enhed"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unbekanntes Gerät"
}
},
"el" : {
"stringUnit" : {
"state" : "translated",
"value" : "Άγνωστη συσκευή"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unknown Device"
}
},
"en-AU" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unknown Device"
}
},
"en-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unknown Device"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unknown Device"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dispositivo desconocido"
}
},
"es-419" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dispositivo desconocido"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tuntematon laite"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Appareil inconnu"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Appareil inconnu"
}
},
"he" : {
"stringUnit" : {
"state" : "translated",
"value" : "מכשיר לא מזוהה"
}
},
"hi" : {
"stringUnit" : {
"state" : "translated",
"value" : "अज्ञात डिवाइस"
}
},
"hr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nepoznat uređaj"
}
},
"hu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ismeretlen eszköz"
}
},
"id" : {
"stringUnit" : {
"state" : "translated",
"value" : "Perangkat Tidak Dikenal"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dispositivo sconosciuto"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "不明なデバイス"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "알 수 없는 기기"
}
},
"ms" : {
"stringUnit" : {
"state" : "translated",
"value" : "Peranti Tidak Dikenali"
}
},
"nb" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ukjent enhet"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Onbekend apparaat"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nieznane urządzenie"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dispositivo desconhecido"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dispositivo desconhecido"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dispozitiv necunoscut"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "Неизвестное устройство"
}
},
"sv" : {
"stringUnit" : {
"state" : "translated",
"value" : "Okänd enhet"
}
},
"th" : {
"stringUnit" : {
"state" : "translated",
"value" : "อุปกรณ์ที่ไม่รู้จัก"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bilinmeyen Cihaz"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
"value" : "Невідомий пристрій"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Thiết bị không xác định"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "未知设备"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "未知裝置"
}
}
}
},
"UnsupportedType" : {
"extractionState" : "manual",
"localizations" : {
@@ -27955,229 +28188,229 @@
"ar" : {
"stringUnit" : {
"state" : "translated",
"value" : "قم بفتح QuickDrop+ لمواصلة تبادل الملفات بلا حدود. QuickDrop+ هو شراء لمرة واحدة – لا حاجة للاشتراك. تشمل جميع الميزات الحالية والمستقبلية."
"value" : "افتح QuickDrop+ لمواصلة تبادل الملفات بلا حدود. QuickDrop+ هو عملية شراء لمرة واحدة فقط – لا تحتاج لاشتراك. جميع الميزات الحالية والمستقبلية متضمنة، بما في ذلك تطبيقات iOS وAndroid."
}
},
"ca" : {
"stringUnit" : {
"state" : "translated",
"value" : "Desbloqueja QuickDrop+ per continuar l'intercanvi d'arxius sense límits. QuickDrop+ és una compra única: no requereix subscripció. Totes les funcions actuals i futures estan incloses."
"value" : "Desbloqueja QuickDrop+ per continuar intercanviant fitxers sense límits. QuickDrop+ és una compra única – sense subscripció. Inclou totes les funcions actuals i futures, també les aplicacions per a iOS i Android."
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
"value" : "Odemkněte QuickDrop+, abyste mohli dále sdílet soubory bez omezení. QuickDrop+ je jednorázový nákup – žádné předplatné. Zahrnuje všechny současné i budoucí funkce."
"value" : "Odemkněte QuickDrop+, abyste mohli nadále neomezeně sdílet soubory. QuickDrop+ je jednorázový nákup – žádné předplatné. Získáte všechny aktuální i budoucí funkce, včetně aplikací pro iOS a Android."
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
"value" : "Opgrader til QuickDrop+ for ubegrænset filudveksling. QuickDrop+ er en engangskøb – ingen abonnement kræves. Alle nuværende og fremtidige funktioner er inkluderet."
"value" : "Lås op for QuickDrop+ for at fortsætte filudveksling uden begrænsninger. QuickDrop+ er et engangskøb – intet abonnement kræves. Alle nuværende og fremtidige funktioner er inkluderet, inklusive iOS- og Android-apps."
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "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."
"value" : "Schalte QuickDrop+ frei, um Dateien weiterhin unbegrenzt auszutauschen. QuickDrop+ ist ein Einmalkauf – ein Abo ist nicht nötig. Alle aktuellen und zukünftigen Funktionen sind enthalten, auch die iOS- und Android-Apps."
}
},
"el" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ξεκλειδώστε το QuickDrop+ για να συνεχίσετε την ανταλλαγή αρχείων χωρίς περιορισμούς. Το QuickDrop+ είναι μια εφάπαξ αγορά — χωρίς συνδρομή. Όλες οι τρέχουσες και μελλοντικές δυνατότητες περιλαμβάνονται."
"value" : "Ξεκλειδώστε το QuickDrop+ για να συνεχίσετε να ανταλλάσσετε αρχεία χωρίς περιορισμούς. Το QuickDrop+ είναι μια εφάπαξ αγορά – δεν απαιτείται συνδρομή. Όλα τα τρέχοντα και μελλοντικά χαρακτηριστικά περιλαμβάνονται, καθώς και οι εφαρμογές iOS και Android."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unlock QuickDrop+ to continue exchanging files without limits. QuickDrop+ is a one-time purchase – no subscription required. All current and future features are included."
"value" : "Unlock QuickDrop+ to continue exchanging files without limits. QuickDrop+ is a one-time purchase – no subscription required. All current and future features are included, plus the iOS and Android apps."
}
},
"en-AU" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unlock QuickDrop+ to enjoy unlimited file sharing. It’s a one-off purchase – no subscriptions needed. Includes current and future features."
"value" : "Unlock QuickDrop+ to continue exchanging files without limits. QuickDrop+ is a one-time purchase – no subscription required. All current and future features are included, plus the iOS and Android apps."
}
},
"en-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unlock QuickDrop+ for unlimited file exchanges. QuickDrop+ is a one-time purchase with no subscription needed. It includes all current and future features."
"value" : "Unlock QuickDrop+ to continue exchanging files without limits. QuickDrop+ is a one-time purchase – no subscription required. All current and future features are included, plus the iOS and Android apps."
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unlock QuickDrop+ to continue transferring files without restrictions. QuickDrop+ is a one-time purchase, no subscription necessary. It includes all current and future features."
"value" : "Unlock QuickDrop+ to continue exchanging files without limits. QuickDrop+ is a one-time purchase – no subscription required. All current and future features are included, plus the iOS and Android apps."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "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."
"value" : "Desbloquea QuickDrop+ para seguir intercambiando archivos sin límites. QuickDrop+ es una compra única, sin suscripción. Incluye todas las funciones actuales y futuras, incluidas las apps para iOS y Android."
}
},
"es-419" : {
"stringUnit" : {
"state" : "translated",
"value" : "Desbloquea QuickDrop+ para continuar intercambiando archivos sin límites. QuickDrop+ es una compra única, sin suscripción requerida. Todas las funciones actuales y futuras están incluidas."
"value" : "Desbloquea QuickDrop+ para seguir intercambiando archivos sin límites. QuickDrop+ es una compra única, no requiere suscripción. Incluye todas las funciones actuales y futuras, además de las apps para iOS y Android."
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Avaa QuickDrop+ jatkaaksesi tiedostojen vaihtamista rajoituksitta. QuickDrop+ on kertaluonteinen ostos - ei tilauksia. Kaikki nykyiset ja tulevat toiminnot sisältyvät hintaan."
"value" : "Avaa QuickDrop+ jatkaaksesi tiedostojen vaihtoa ilman rajoituksia. QuickDrop+ on kertaluonteinen osto – ei tilausta tarvita. Kaikki nykyiset ja tulevat ominaisuudet sisältyvät, mukaan lukien iOS- ja Android-sovellukset."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "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."
"value" : "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, y compris les applications iOS et Android."
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Déverrouillez 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."
"value" : "Déverrouillez 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, y compris les applis iOS et Android."
}
},
"he" : {
"stringUnit" : {
"state" : "translated",
"value" : "בטל מגבלות קבצים עם QuickDrop+. QuickDrop+ הוא רכישה חד-פעמית – ללא מנוי. כל התכונות הנוכחיות והעתידיות כלולות."
"value" : "פתח את QuickDrop+ כדי להמשיך להחליף קבצים ללא הגבלה. QuickDrop+ היא רכישה חד-פעמית – אין צורך במנוי. כל התכונות הנוכחיות והעתידיות כלולות, כולל אפליקציות ל-iOS ולאנדרואיד."
}
},
"hi" : {
"stringUnit" : {
"state" : "translated",
"value" : "फ़ाइलों का आदान-प्रदान जारी रखने के लिए QuickDrop+ को अनलॉक करें। QuickDrop+ एक बार की खरीदारी है – कोई सब्सक्रिप्शन आवश्यक नहीं। इसमें सभी वर्तमान और भविष्य की विशेषताएं शामिल हैं।"
"value" : "फाइलों का आदान-प्रदान बिना किसी सीमा के जारी रखने के लिए QuickDrop+ अनलॉक करें। QuickDrop+ एक बार की खरीदारी है – कोई सदस्यता नहीं चाहिए। सभी वर्तमान और भविष्य की सुविधाएँ शामिल हैं, जैसे iOS और Android ऐप्स।"
}
},
"hr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Otključajte QuickDrop+ za nastavak razmjene datoteka bez ograničenja. QuickDrop+ je jednokratna kupnja – bez pretplate. Uključene su sve trenutne i buduće značajke."
"value" : "Otključajte QuickDrop+ za neograničenu razmjenu datoteka. QuickDrop+ je jednokratna kupnja – nema pretplate. Sve sadašnje i buduće značajke su uključene, uključujući iOS i Android aplikacije."
}
},
"hu" : {
"stringUnit" : {
"state" : "translated",
"value" : "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."
"value" : "Oldd fel a QuickDrop+ szolgáltatást, hogy korlátlanul cserélhess fájlokat. A QuickDrop+ egyszeri vásárlás – nincs előfizetés. Minden jelenlegi és jövőbeli funkciót tartalmaz, beleértve az iOS és Android alkalmazásokat is."
}
},
"id" : {
"stringUnit" : {
"state" : "translated",
"value" : "Buka kunci QuickDrop+ untuk terus bertukar file tanpa batas. QuickDrop+ adalah pembelian satu kali – tanpa diperlukan langganan. Semua fitur saat ini dan mendatang termasuk."
"value" : "Buka QuickDrop+ untuk terus bertukar file tanpa batas. QuickDrop+ adalah pembelian sekali bayar – tidak perlu berlangganan. Semua fitur saat ini dan mendatang termasuk, juga aplikasi iOS dan Android."
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sblocca QuickDrop+ per continuare a scambiare file senza limiti. QuickDrop+ è un acquisto unico – nessun abbonamento richiesto. Tutte le funzionalità attuali e future sono incluse."
"value" : "Sblocca QuickDrop+ per continuare a scambiare file senza limiti. QuickDrop+ è un acquisto unico – nessun abbonamento richiesto. Sono incluse tutte le funzionalità attuali e future, comprese le app per iOS e Android."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop+ をアンロックして、無制限にファイルを交換できます。QuickDrop+ は買い切り型のアプリです。サブスクリプション不要で、現在および将来のすべての機能が含まれます。"
"value" : "QuickDrop+をアンロックして、無制限でファイル交換を続けましょう。QuickDrop+は買い切り型で、サブスクリプションは不要です。iOSやAndroidアプリを含む、現在および今後のすべての機能が含まれます。"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop+를 잠금 해제하여 파일을 제한 없이 교환하세요. QuickDrop+는 일회성 구매이며, 구독이 필요하지 않습니다. 모든 현재 및 미래 기능이 포함됩니다."
"value" : "QuickDrop+를 잠금 해제하여 파일을 무제한으로 주고받으세요. QuickDrop+는 일회성 구매이며, 구독이 필요하지 않습니다. 현재 및 향후 모든 기능이 포함되어 있으며, iOS 및 Android 앱도 포함됩니다."
}
},
"ms" : {
"stringUnit" : {
"state" : "translated",
"value" : "Buka QuickDrop+ untuk terus menukar fail tanpa had. QuickDrop+ adalah pembelian sekali sahaja – tiada langganan diperlukan. Semua ciri semasa dan masa depan disertakan."
"value" : "Buka kunci QuickDrop+ untuk terus berkongsi fail tanpa had. QuickDrop+ adalah pembelian sekali sahaja – tiada langganan diperlukan. Semua ciri semasa dan akan datang disertakan, termasuk aplikasi iOS dan Android."
}
},
"nb" : {
"stringUnit" : {
"state" : "translated",
"value" : "Lås opp QuickDrop+ for å fortsette å dele filer uten begrensninger. QuickDrop+ er et engangskjøp – ingen abonnement er nødvendig. Alle nåværende og fremtidige funksjoner er inkludert."
"value" : "Lås opp QuickDrop+ for å fortsette å utveksle filer uten begrensninger. QuickDrop+ er et engangskjøp – ingen abonnement kreves. Alle nåværende og fremtidige funksjoner er inkludert, inkludert iOS- og Android-appene."
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ontgrendel QuickDrop+ om bestanden zonder limieten te blijven uitwisselen. QuickDrop+ is een eenmalige aankoop – geen abonnement vereist. Alle huidige en toekomstige functies zijn inbegrepen."
"value" : "Ontgrendel QuickDrop+ om bestanden onbeperkt te blijven uitwisselen. QuickDrop+ is een eenmalige aankoop – geen abonnement nodig. Alle huidige en toekomstige functies zijn inbegrepen, ook de iOS- en Android-apps."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Odkryj QuickDrop+, aby kontynuować wymianę plików bez ograniczeń. QuickDrop+ to jednorazowy zakup – bez subskrypcji. Wszystkie obecne i przyszłe funkcje są w zestawie."
"value" : "Odblokuj QuickDrop+, aby dalej wymieniać pliki bez ograniczeń. QuickDrop+ to jednorazowy zakup – bez subskrypcji. Obejmuje wszystkie obecne i przyszłe funkcje, w tym aplikacje na iOS i Androida."
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
"value" : "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."
"value" : "Desbloqueie o QuickDrop+ para continuar trocando arquivos sem limites. O QuickDrop+ é uma compra única – sem assinatura. Todos os recursos atuais e futuros estão incluídos, incluindo os apps para iOS e Android."
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
"value" : "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."
"value" : "Desbloqueie o QuickDrop+ para continuar a trocar ficheiros sem limites. O QuickDrop+ é uma compra única – sem necessidade de subscrição. Todas as funcionalidades atuais e futuras estão incluídas, incluindo as apps para iOS e Android."
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
"value" : "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."
"value" : "Deblochează QuickDrop+ pentru a continua schimbul de fișiere fără limite. QuickDrop+ este o achiziție unică – fără abonament. Toate funcțiile actuale și viitoare sunt incluse, inclusiv aplicațiile iOS și Android."
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "Разблокируйте QuickDrop+, чтобы продолжить обмен файлами без ограничений. QuickDrop+ – это разовая покупка, подписка не требуется. Все текущие и будущие функции включены."
"value" : "Разблокируйте QuickDrop+, чтобы продолжать обмен файлами без ограничений. QuickDrop+ — это единоразовая покупка, подписка не требуется. Включены все текущие и будущие функции, включая приложения для iOS и Android."
}
},
"sv" : {
"stringUnit" : {
"state" : "translated",
"value" : "Lås upp QuickDrop+ för att fortsätta utbyta filer utan begränsningar. QuickDrop+ är ett engångsköp - ingen prenumeration krävs. Alla nuvarande och framtida funktioner inkluderas."
"value" : "Lås upp QuickDrop+ för att fortsätta utbyta filer utan begränsningar. QuickDrop+ är ett engångsköp – inget abonnemang krävs. Alla nuvarande och framtida funktioner ingår, inklusive appar för iOS och Android."
}
},
"th" : {
"stringUnit" : {
"state" : "translated",
"value" : "ปลดล็อก QuickDrop+ เพื่อแลกเปลี่ยนไฟล์ได้อย่างไม่จำกัด QuickDrop+ เป็นการซื้อครั้งเดียว – ไม่ต้องสมัครสมาชิก ฟีเจอร์ทั้งหมดทั้งปัจจุบันและอนาคตรวมอยู่ในนี้แล้ว"
"value" : "ปลดล็อก QuickDrop+ เพื่อแลกเปลี่ยนไฟล์ได้ไม่จำกัด QuickDrop+ ซื้อครั้งเดียว ไม่มีค่าบริการรายเดือน รวมฟีเจอร์ปัจจุบันและอนาคตทั้งหมด ทั้งแอป iOS และ Android"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dosyaları limitsiz değiştirmeye devam etmek için QuickDrop+ kilidini açın. QuickDrop+, tek seferlik bir satın alımdır – abonelik gerekmez. Tüm mevcut ve gelecekteki özellikler dahildir."
"value" : "Dosya alışverişine sınırsız devam etmek için QuickDrop+’ı açın. QuickDrop+ tek seferlik bir satın almadır – abonelik gerekmez. iOS ve Android uygulamaları da dahil olmak üzere mevcut ve gelecekteki tüm özellikler dahildir."
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
"value" : "Розблокуйте QuickDrop+, щоб продовжити обмін файлами без обмежень. QuickDrop+ – це одноразова покупка, підписка не потрібна. Всі поточні та майбутні функції включені."
"value" : "Розблокуйте QuickDrop+, щоб і надалі обмінюватися файлами без обмежень. QuickDrop+ купується один раз — без підписок. Усі поточні та майбутні функції входять у покупку, включаючи додатки для iOS і Android."
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Mở khóa QuickDrop+ để tiếp tục chuyển tệp không giới hạn. QuickDrop+ là mua một lần duy nhất – không yêu cầu đăng ký. Tất cả tính năng hiện tại và tương lai đều được bao gồm."
"value" : "Mở khóa QuickDrop+ để tiếp tục trao đổi tệp không giới hạn. QuickDrop+ là mua một lần – không cần đăng ký. Bao gồm tất cả tính năng hiện tại và tương lai, cùng với ứng dụng iOS và Android."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "解锁 QuickDrop+ 以无限制地交换文件。QuickDrop+ 是一次性购买 – 无需订阅。所有当前和未来的功能均包含在内。"
"value" : "解锁 QuickDrop+,即可无限制传输文件。QuickDrop+ 为一次性购买,无需订阅。所有当前和未来功能均已包含,包括 iOS 和 Android 应用。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "解鎖 QuickDrop+ 以繼續無限制地交換文件。QuickDrop+ 是一次性購買——不需要訂閱。包含所有當前和未來的功能。"
"value" : "解鎖 QuickDrop+ 以無限交換檔案。QuickDrop+ 僅需一次性購買,無需訂閱。所有現有和未來功能均包含在內,包括 iOS 和 Android 應用程式。"
}
}
}
+2 -2
View File
@@ -103,8 +103,8 @@ class ErrorAlertHandler {
self.isAlertShown = false
if result == .alertFirstButtonReturn {
log("Sending logging string")
sendLoggingString()
//log("Sending logging string")
//sendLoggingString()
}
#else
showAlert(title: title, message: description)
+147
View File
@@ -0,0 +1,147 @@
//
// IncomingFileTransmissionView.swift
// QuickDrop
//
// Created by Leon Böttger on 05.10.25.
//
import SwiftUI
import AppKit
let toastViewSize = CGSize(width: 400, height: 100)
struct QuickDropToastView: View {
@ObservedObject var settings = Settings.shared
@ObservedObject var receiveModel: ReceiveModel
public var onCancel: () -> Void = {}
init(receiveModel: ReceiveModel,
onCancel: @escaping () -> Void = {}) {
self.receiveModel = receiveModel
self.onCancel = onCancel
}
public var body: some View {
VStack(spacing: 12) {
HStack(spacing: 12) {
ZStack(alignment: .bottomLeading) {
Image(.quickDropIcon)
.resizable()
.scaledToFill()
.frame(width: 44, height: 44)
.clipShape(Circle())
}
VStack(alignment: .leading, spacing: 2) {
Text(String("QuickDrop"))
.font(.system(size: 16, weight: .semibold))
Text("Receiving")
.font(.system(size: 13))
.foregroundColor(.secondary)
.lineLimit(1)
.truncationMode(.tail)
}
Spacer(minLength: 12)
}
HStack() {
CapsuleProgress(value: receiveModel.progress ?? 1.0)
Button(action: onCancel) {
Image(systemName: "xmark")
.font(.system(size: 10, weight: .semibold))
.foregroundColor(.secondary)
.frame(width: 18, height: 18)
}
.buttonStyle(PlainButtonStyle())
}
}
.padding(16)
.background(
VisualEffectView(material: .hudWindow)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
)
.overlay(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.stroke(Color.black.opacity(0.06), lineWidth: 1)
)
.shadow(color: Color.black.opacity(0.1), radius: 20)
.frame(maxWidth: .infinity)
.opacity(isFileTransferRestricted() ? 0 : 1)
}
}
// NSVisualEffectView wrapper for macOS 11 compatible blur/vibrancy
struct VisualEffectView: NSViewRepresentable {
var material: NSVisualEffectView.Material = .hudWindow
var blendingMode: NSVisualEffectView.BlendingMode = .withinWindow
var state: NSVisualEffectView.State = .active
func makeNSView(context: Context) -> NSVisualEffectView {
let v = NSVisualEffectView()
v.material = material
v.blendingMode = blendingMode
v.state = state
v.isEmphasized = true
return v
}
func updateNSView(_ v: NSVisualEffectView, context: Context) {
v.material = material
v.blendingMode = blendingMode
v.state = state
}
}
// Capsule-style linear progress (0...1)
struct CapsuleProgress: View {
var value: Double
var track = Color(NSColor.separatorColor).opacity(0.28)
var fill = Color(NSColor.systemBlue)
var body: some View {
GeometryReader { geo in
let progress = max(0, min(1, value))
ZStack(alignment: .leading) {
Capsule()
.fill(track)
Capsule() // Use a plain rectangle
.fill(fill)
.mask(
Capsule()
.frame(width: progress * geo.size.width)
.frame(maxWidth: .infinity, alignment: .leading)
)
}
}
.frame(height: 8)
.animation(.easeInOut(duration: 0.25), value: value)
}
}
// MARK: - Preview
struct QuickDropToastView_Previews: PreviewProvider {
struct Demo: View {
@State var model = ReceiveModel()
var body: some View {
QuickDropToastView(
receiveModel: model,
onCancel: { }
)
.padding()
.frame(width: toastViewSize.width, height: toastViewSize.height)
.onAppear {
model.progress = 0.01
}
}
}
static var previews: some View { Demo() }
}
+1 -1
View File
@@ -58,7 +58,7 @@ struct WelcomeScreen: View {
.listStyle(SidebarListStyle())
Divider()
.opacity(colorScheme == .light ? 1 : 0)
.opacity(colorScheme == .light ? 1 : 0.5)
.edgesIgnoringSafeArea(.vertical)
ZStack {
+323
View File
@@ -0,0 +1,323 @@
//
// ReceiveModel.swift
// QuickDrop
//
// Created by Leon Böttger on 16.08.25.
//
import SwiftUI
import StoreKit
import UserNotifications
#if os(macOS)
import BezelNotification
#else
import LUI
#endif
class ReceiveModel: ObservableObject, InboundAppDelegate {
/// For each connection ID, store the last reported progress value
private var processes: [String: Double] = [:]
#if os(macOS)
@Published var progress: Double? = nil
private var toastWindow: NSWindow?
private var toastHosting: NSHostingView<QuickDropToastView>?
#endif
let controlPlusScreen: (Bool) -> Void
init(controlPlusScreen: @escaping (Bool) -> Void = { _ in }) {
self.controlPlusScreen = controlPlusScreen
NearbyConnectionManager.shared.addInboundAppDelegate(self)
NearbyConnectionManager.shared.becomeVisible()
}
deinit {
NearbyConnectionManager.shared.removeInboundAppDelegate(self)
}
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo) {
#if os(macOS)
NSApp.activate(ignoringOtherApps: true)
AudioManager.playIncomingFileSound()
#endif
let mainMessage = transfer.getDescription(deviceName: device.name ?? "Android", alreadyAccepted: false)
let pinCodeMessage = transfer.getPinCodeMessage()
let transferID = transfer.id
let title = "QuickDrop - \(pinCodeMessage)"
let primaryButtonTitle = "Accept".localized()
let primaryButtonAction = { (trustDevice: Bool) in
NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: true, trustDevice: trustDevice)
#if os(macOS)
if transfer.type == .text {
DispatchQueue.main.async {
BezelNotification.show(messageText: "InsertedIntoClipboard".localized(), icon: .clipboard)
}
}
#endif
}
let secondaryButtonTitle = "Decline".localized()
let secondaryButtonAction = { NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: false, trustDevice: false) }
#if os(macOS)
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = title
alert.informativeText = mainMessage
alert.addButton(withTitle: primaryButtonTitle)
alert.addButton(withTitle: secondaryButtonTitle)
if transfer.allowsToBeAddedAsTrustedDevice {
alert.showsSuppressionButton = true
alert.suppressionButton?.title = "AutoAcceptFromThisDevice".localized()
}
let result = alert.runModal()
if result == .alertFirstButtonReturn {
primaryButtonAction(alert.suppressionButton?.state == .on)
} else if result == .alertSecondButtonReturn {
secondaryButtonAction()
}
#else
// iOS
let alwaysAcceptLabel = transfer.allowsToBeAddedAsTrustedDevice ? "AlwaysAccept".localized() : nil
ProgressAlert.shared.askForUserPermission(title: title, message: mainMessage, acceptLabel: primaryButtonTitle, acceptAlwaysLabel: alwaysAcceptLabel, rejectLabel: secondaryButtonTitle) { accepted in
switch accepted {
case .Accept:
primaryButtonAction(false)
case .AcceptAlways:
primaryButtonAction(true)
case .Decline:
secondaryButtonAction()
}
} onCancel: {
NearbyConnectionManager.shared.cancelTransfer(id: transferID)
}
#endif
}
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo) {
let mainMessage = transfer.getDescription(deviceName: device.name ?? "Android", alreadyAccepted: true)
#if os(macOS)
NSApp.activate(ignoringOtherApps: true)
AudioManager.playIncomingFileSound()
// If file -> no notification, as there is the QuickDrop progress toast
if transfer.type != .file {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, _ in
if granted {
log("[ReceiveModel] User granted notification permissions")
let notificationContent = UNMutableNotificationContent()
notificationContent.title = "QuickDrop"
notificationContent.body = mainMessage
notificationContent.sound = nil
let notificationId = UUID().uuidString
let notificationReq = UNNotificationRequest(identifier: notificationId, content: notificationContent, trigger: nil)
UNUserNotificationCenter.current().add(notificationReq)
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [notificationId])
}
}
}
}
#else
// If text is received, nothing is shown, as it is directly inserted into clipboard automatically
// Therefore, give a feedback dialog in this case
if transfer.type == .text {
showAlert(title: "QuickDrop", message: mainMessage)
}
else {
ProgressAlert.shared.showProgressAlert(onCancel: {
NearbyConnectionManager.shared.cancelTransfer(id: transfer.id)
})
}
#endif
}
func transferProgress(connectionID: String, progress: Double) {
// Update process dictionary
processes[connectionID] = progress
// Calcualate average value for all ongoing transfers
let totalProgress = processes.values.reduce(0, +)
let averageProgress = totalProgress / Double(processes.count)
#if os(iOS)
ProgressAlert.shared.updateProgress(averageProgress)
#else
DispatchQueue.main.async {
self.progress = averageProgress
self.showQuickDropToast(for: connectionID)
}
#endif
}
func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo, wasPlainTextTransfer: Bool, error: (any Error)?) {
processes.removeValue(forKey: connectionID)
#if os(macOS)
DispatchQueue.main.async {
self.hideQuickDropToast()
if self.processes.isEmpty {
self.progress = nil
}
}
finish()
#else
if self.processes.isEmpty {
ProgressAlert.shared.updateProgress(nil) {
finish()
}
}
#endif
func finish() {
if let error = error {
#if os(iOS)
errorVibration()
#endif
controlPlusScreen(false)
ErrorAlertHandler.shared.showErrorAlert(for: device.name ?? "Android", error: error)
} else {
#if os(iOS)
doubleVibration()
#endif
let currentCount = Settings.shared.incomingTransmissionCount
#if os(macOS)
// If distributed directly, do not request review here as it does not work
if currentCount == 0 && !DistributionDetector.isDirectDistributionEnabled {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
SKStoreReviewController.requestReview()
}
}
#endif
Settings.shared.incomingTransmissionCount = currentCount + 1
log("[ReceiveModel] Successful transmission. Current count: \(currentCount)")
}
}
}
func showPlusScreen() {
DispatchQueue.main.async {
self.controlPlusScreen(true)
}
}
// MARK: - macOS Progress Overlay
#if os(macOS)
func showQuickDropToast(for connectionID: String) {
guard toastWindow == nil else { return }
let contentView = QuickDropToastView(
receiveModel: self,
onCancel: {
NearbyConnectionManager.shared.cancelTransfer(id: connectionID)
}
)
let hostingView = NSHostingView(rootView: contentView)
hostingView.frame.size = toastViewSize
// Prefer the screen under the mouse, then the key window’s screen, then main
let screen = NSScreen.screens.first { $0.frame.contains(NSEvent.mouseLocation) }
?? NSApp.keyWindow?.screen
?? NSScreen.main
guard let screen else { return }
let frame = screen.frame
let visible = screen.visibleFrame
let marginX: CGFloat = 24
let marginFromTop: CGFloat = 30
// If the menu bar is auto-hidden, visible.maxY == frame.maxY; subtract the status bar thickness to stay clear
let menuBarHidden = abs(frame.maxY - visible.maxY) < 0.5
let extraTopInset = menuBarHidden ? NSStatusBar.system.thickness : 0
let targetX = visible.maxX - toastViewSize.width - marginX
let targetY = visible.maxY - toastViewSize.height - marginFromTop - extraTopInset
// Start slightly above the final spot for a smooth slide-in
let startY = min(frame.maxY + 10, targetY + 10)
let startFrame = CGRect(x: targetX, y: startY, width: toastViewSize.width, height: toastViewSize.height)
let window = NSWindow(contentRect: startFrame,
styleMask: [.borderless],
backing: .buffered,
defer: false)
window.isOpaque = false
window.backgroundColor = .clear
window.hasShadow = true
window.level = .statusBar
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
window.contentView = hostingView
window.ignoresMouseEvents = false
window.alphaValue = 0
window.makeKeyAndOrderFront(nil)
self.toastWindow = window
self.toastHosting = hostingView
NSAnimationContext.runAnimationGroup { ctx in
ctx.duration = 0.35
ctx.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
window.animator().setFrameOrigin(CGPoint(x: targetX, y: targetY))
window.animator().alphaValue = 1
}
}
func hideQuickDropToast() {
guard let window = toastWindow,
let screen = NSScreen.main else { return }
let offscreenY = screen.visibleFrame.origin.y + screen.visibleFrame.height + 20
NSAnimationContext.runAnimationGroup({ ctx in
ctx.duration = 0.35
ctx.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
window.animator().setFrameOrigin(CGPoint(x: window.frame.origin.x, y: offscreenY))
window.animator().alphaValue = 0
}, completionHandler: {
window.orderOut(nil)
self.toastWindow = nil
self.toastHosting = nil
})
}
#endif
}
@@ -29,7 +29,12 @@ class SendModel: ObservableObject, OutboundAppDelegate {
init() {
NearbyConnectionManager.shared.startDeviceDiscovery()
NearbyConnectionManager.shared.addShareExtensionDelegate(self)
NearbyConnectionManager.shared.addOutboundAppDelegate(self)
}
deinit {
NearbyConnectionManager.shared.removeOutboundAppDelegate(self)
}
@@ -60,7 +65,7 @@ class SendModel: ObservableObject, OutboundAppDelegate {
errorVibration()
#endif
ErrorAlertHandler.shared.showErrorAlert(for: selectedDevice?.name ?? "?", error: error)
ErrorAlertHandler.shared.showErrorAlert(for: selectedDevice?.name ?? "UnknownDevice".localized(), error: error)
progressValue = nil
selectedDevice = nil
@@ -116,7 +121,7 @@ class SendModel: ObservableObject, OutboundAppDelegate {
progressValue = 0
progressState = nil
selectedDevice = nil
NearbyConnectionManager.shared.cancelOutgoingTransfer(id: device.id!)
NearbyConnectionManager.shared.cancelTransfer(id: device.id!)
}
else {
-212
View File
@@ -1,212 +0,0 @@
//
// ReceiveModel.swift
// QuickDrop
//
// Created by Leon Böttger on 16.08.25.
//
import SwiftUI
import StoreKit
import UserNotifications
#if os(macOS)
import BezelNotification
#else
import LUI
#endif
class ReceiveModel: ObservableObject, InboundAppDelegate {
let controlPlusScreen: (Bool) -> Void
init(controlPlusScreen: @escaping (Bool) -> Void = { _ in }) {
self.controlPlusScreen = controlPlusScreen
NearbyConnectionManager.shared.mainAppDelegate = self
NearbyConnectionManager.shared.becomeVisible()
}
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, acceptAutomatically: Bool) {
#if os(macOS)
NSApp.activate(ignoringOtherApps: true)
AudioManager.playIncomingFileSound()
#endif
let fileStr: String
if let textTitle = transfer.textDescription {
fileStr = textTitle
} else if transfer.files.count == 1 {
fileStr = transfer.files[0].name
} else {
fileStr = String.localizedStringWithFormat("NFiles".localized(), transfer.files.count)
}
let mainMessage: String
let name = device.name ?? "Android"
switch transfer.type {
case .file:
mainMessage = String(format: (acceptAutomatically ? "DeviceCurrentlySendingFiles" : "DeviceSendingFiles").localized(), arguments: [name, fileStr])
case .text:
mainMessage = String(format: (acceptAutomatically ? "DeviceCurrentlySendingText" : "DeviceSendingText").localized(), arguments: [name, fileStr])
case .url:
mainMessage = String(format: (acceptAutomatically ? "DeviceCurrentlySendingUrl" : "DeviceSendingUrl").localized(), arguments: [name, fileStr])
}
let pinCodeMessage = String(format: "PinCode".localized(), arguments: [transfer.pinCode ?? "?"])
let transferID = transfer.id
#if os(macOS)
let isMac = true
#else
let isMac = false
#endif
if acceptAutomatically && isMac {
pressAcceptButton(transferID: transfer.id, trustDevice: false)
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, _ in
if granted {
log("[ReceiveModel] User granted notification permissions")
let notificationContent = UNMutableNotificationContent()
notificationContent.title = "QuickDrop"
notificationContent.body = mainMessage
notificationContent.sound = nil
let notificationId = UUID().uuidString
let notificationReq = UNNotificationRequest(identifier: notificationId, content: notificationContent, trigger: nil)
UNUserNotificationCenter.current().add(notificationReq)
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [notificationId])
}
}
}
} else {
let title = "QuickDrop - \(pinCodeMessage)"
let primaryButtonTitle = "Accept".localized()
let primaryButtonAction = { (trustDevice: Bool) in self.pressAcceptButton(transferID: transferID, trustDevice: trustDevice) }
let secondaryButtonTitle = "Decline".localized()
let secondaryButtonAction = { NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: false, trustDevice: false, storeInTemp: false) }
#if os(macOS)
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = title
alert.informativeText = mainMessage
alert.addButton(withTitle: primaryButtonTitle)
alert.addButton(withTitle: secondaryButtonTitle)
if transfer.allowsToBeAddedAsTrustedDevice {
alert.showsSuppressionButton = true
alert.suppressionButton?.title = "AutoAcceptFromThisDevice".localized()
}
let result = alert.runModal()
if result == .alertFirstButtonReturn {
primaryButtonAction(alert.suppressionButton?.state == .on)
} else if result == .alertSecondButtonReturn {
secondaryButtonAction()
}
#else
// iOS
let alwaysAcceptLabel = transfer.allowsToBeAddedAsTrustedDevice ? "AlwaysAccept".localized() : nil
ProgressAlert.shared.askForUserPermission(title: title, message: mainMessage, acceptLabel: primaryButtonTitle, acceptAlwaysLabel: alwaysAcceptLabel, rejectLabel: secondaryButtonTitle, acceptAutomatically: acceptAutomatically) { accepted in
switch accepted {
case .Accept:
primaryButtonAction(false)
case .AcceptAlways:
primaryButtonAction(true)
case .Decline:
secondaryButtonAction()
}
}
#endif
}
}
func transferProgress(progress: Double) {
#if os(iOS)
ProgressAlert.shared.updateProgress(progress)
#endif
}
func connectionWasTerminated(from device: RemoteDeviceInfo, wasPlainTextTransfer: Bool, error: (any Error)?) {
#if os(macOS)
finish()
#else
ProgressAlert.shared.updateProgress(nil) {
finish()
}
#endif
func finish() {
if let error = error {
#if os(iOS)
errorVibration()
#endif
controlPlusScreen(false)
ErrorAlertHandler.shared.showErrorAlert(for: device.name ?? "Android", error: error)
} else {
#if os(iOS)
doubleVibration()
#endif
if wasPlainTextTransfer {
showCopiedToClipboardAlert()
}
let currentCount = Settings.shared.incomingTransmissionCount
#if os(macOS)
if currentCount == 0 {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
SKStoreReviewController.requestReview()
}
}
#endif
Settings.shared.incomingTransmissionCount = currentCount + 1
log("[ReceiveModel] Successful transmission. Current count: \(currentCount)")
}
}
}
private func showCopiedToClipboardAlert() {
#if os(macOS)
DispatchQueue.main.async {
BezelNotification.show(messageText: "InsertedIntoClipboard".localized(), icon: .clipboard)
}
#endif
}
private func pressAcceptButton(transferID: String, trustDevice: Bool) {
if isFileTransferRestricted() {
log("[ReceiveModel] Showing plus screen...")
NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: true, trustDevice: trustDevice, storeInTemp: true)
controlPlusScreen(true)
} else {
NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: true, trustDevice: trustDevice, storeInTemp: false)
}
}
}
File renamed without changes.
@@ -162,10 +162,10 @@ fileprivate struct DataUrl: Transferable {
.cornerRadius(8)
} onResult: { urls, text in
if let urls = urls {
print("Selected URLs: \(urls)")
log("Selected URLs: \(urls)")
}
if let text = text {
print("Clipboard text: \(text)")
log("Clipboard text: \(text)")
}
}
}
File renamed without changes.
@@ -18,41 +18,41 @@ final class ProgressAlert {
private weak var progressView: UIProgressView?
// MARK: - Initial Accept / Decline
func askForUserPermission(title: String, message: String, acceptLabel: String, acceptAlwaysLabel: String?, rejectLabel: String, acceptAutomatically: Bool, onAccept: @escaping (AcceptAction) -> Void) {
func askForUserPermission(title: String, message: String, acceptLabel: String, acceptAlwaysLabel: String?, rejectLabel: String, onAccept: @escaping (AcceptAction) -> Void, onCancel: @escaping () -> Void) {
guard let vc = topMostViewController() else { return }
if acceptAutomatically {
let alert = UIAlertController(title: title,
message: message,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: rejectLabel, style: .cancel, handler: { _ in
onAccept(.Decline)
}))
alert.addAction(UIAlertAction(title: acceptLabel, style: .default, handler: { _ in
onAccept(.Accept)
self.showProgressAlert(on: vc)
}
else {
let alert = UIAlertController(title: title,
message: message,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: rejectLabel, style: .cancel, handler: { _ in
onAccept(.Decline)
self.showProgressAlert(on: vc, onCancel: onCancel)
}))
if let acceptAlways = acceptAlwaysLabel {
alert.addAction(UIAlertAction(title: acceptAlways, style: .default, handler: { _ in
onAccept(.AcceptAlways)
self.showProgressAlert(on: vc, onCancel: onCancel)
}))
alert.addAction(UIAlertAction(title: acceptLabel, style: .default, handler: { _ in
onAccept(.Accept)
self.showProgressAlert(on: vc)
}))
if let acceptAlways = acceptAlwaysLabel {
alert.addAction(UIAlertAction(title: acceptAlways, style: .default, handler: { _ in
onAccept(.AcceptAlways)
self.showProgressAlert(on: vc)
}))
}
vc.present(alert, animated: true)
}
vc.present(alert, animated: true)
}
func showProgressAlert(onCancel: @escaping () -> Void) {
guard let vc = topMostViewController() else { return }
self.showProgressAlert(on: vc, onCancel: onCancel)
}
// MARK: - Progress Alert
private func showProgressAlert(on vc: UIViewController) {
private func showProgressAlert(on vc: UIViewController, onCancel: @escaping () -> Void) {
let alert = UIAlertController(title: "Receiving".localized(),
message: "\n\n",
preferredStyle: .alert)
@@ -67,7 +67,8 @@ final class ProgressAlert {
])
let cancel = UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: { _ in
print("Cancel during progress")
log("Cancel during progress")
onCancel()
})
alert.addAction(cancel)
@@ -24,31 +24,8 @@ func loadAttachments(with extensionContext: NSExtensionContext?, loadedItems: @e
let item = extensionContext.inputItems[0] as! NSExtensionItem
if let attachments = item.attachments {
if let text = item.attributedContentText?.string {
log("Found text content: \(text)")
result.textToSend = text
#if os(macOS)
result.previewImage = NSImage(named: NSImage.multipleDocumentsName)
#else
result.previewImage = Image(systemName: "text.document")
#endif
let maxLength = 50
let cleanedText = text.replacingOccurrences(of: "\n", with: " ").replacingOccurrences(of: "\r", with: "")
if cleanedText.count > maxLength {
let index = cleanedText.index(cleanedText.startIndex, offsetBy: maxLength)
result.shortDescription = String(cleanedText[..<index]) + "..."
} else {
result.shortDescription = cleanedText
}
loadedItems(result)
} else {
if !attachments.isEmpty {
for attachment in attachments {
if attachment.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
@@ -59,6 +36,10 @@ func loadAttachments(with extensionContext: NSExtensionContext?, loadedItems: @e
} else if let url = data as? NSURL {
result.urls += [(url as URL)]
}
else {
log("Attachment is not a URL, ignoring. All attachments: \(attachments.description)")
ignoredAttachments += 1
}
checkIfAttachmentsLoaded(result: result, attachmentCount: attachments.count, ignoredAttachments: ignoredAttachments)
}
@@ -82,6 +63,9 @@ func loadAttachments(with extensionContext: NSExtensionContext?, loadedItems: @e
}
}
}
else {
loadTextIfPossible()
}
}
else {
log("No attachments found in extension context. Exiting.")
@@ -91,12 +75,48 @@ func loadAttachments(with extensionContext: NSExtensionContext?, loadedItems: @e
}
func loadTextIfPossible() {
if let text = item.attributedContentText?.string {
log("Found text content: \(text)")
result.textToSend = text
#if os(macOS)
result.previewImage = NSImage(named: NSImage.multipleDocumentsName)
#else
result.previewImage = Image(systemName: "text.document")
#endif
let maxLength = 50
let cleanedText = text.replacingOccurrences(of: "\n", with: " ").replacingOccurrences(of: "\r", with: "")
if cleanedText.count > maxLength {
let index = cleanedText.index(cleanedText.startIndex, offsetBy: maxLength)
result.shortDescription = String(cleanedText[..<index]) + "..."
} else {
result.shortDescription = cleanedText
}
DispatchQueue.main.async {
loadedItems(result)
}
}
}
func checkIfAttachmentsLoaded(result: AttachmentDetails, attachmentCount: Int, ignoredAttachments: Int = 0) {
var result = result
if result.urls.count == attachmentCount-ignoredAttachments {
if result.urls.count == 1 {
// Unable to load anything useful, check if there is text as last resort
if result.urls.count == 0 {
loadTextIfPossible()
return
}
else if result.urls.count == 1 {
if result.urls[0].isFileURL {
+3 -3
View File
@@ -99,7 +99,7 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
super.viewDidLoad()
dicoverDevices()
NearbyConnectionManager.shared.addShareExtensionDelegate(self)
NearbyConnectionManager.shared.addOutboundAppDelegate(self)
}
@@ -111,13 +111,13 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
if chosenDevice == nil {
NearbyConnectionManager.shared.stopDeviceDiscovery()
}
NearbyConnectionManager.shared.removeShareExtensionDelegate(self)
NearbyConnectionManager.shared.removeOutboundAppDelegate(self)
}
@IBAction func cancel(_: AnyObject?) {
if let device = chosenDevice {
NearbyConnectionManager.shared.cancelOutgoingTransfer(id: device.id!)
NearbyConnectionManager.shared.cancelTransfer(id: device.id!)
}
let cancelError = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil)
extensionContext!.cancelRequest(withError: cancelError)
+2 -6
View File
@@ -31,12 +31,8 @@ class ShareViewController: UIViewController {
// skip introduction for share extension
LUIInit(configuration: configuration)
LUISettings.sharedInstance.appLaunchedBefore = true
// Replace with your app’s SwiftUI entry view
let contentView = ContentView()
// Embed SwiftUI inside the extension’s controller
let hostingController = UIHostingController(rootView: contentView)
let hostingController = UIHostingController(rootView: ContentView())
addChild(hostingController)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false