mirror of
https://github.com/leonboe1/QuickDrop.git
synced 2026-11-11 02:51:11 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21f0f865b6 | ||
|
|
026ebc33db | ||
|
|
d01e1d7c34 | ||
|
|
14390035a3 | ||
|
|
a3d295cd38 | ||
|
|
0ca2a13d66 | ||
|
|
05321adb6d | ||
|
|
31b93a34a2 | ||
|
|
261f91bc94 | ||
|
|
646da767a7 | ||
|
|
9d1e698d9b | ||
|
|
a69a75cb07 | ||
|
|
1ac988a4fc | ||
|
|
673d4418f8 | ||
|
|
779144d26a | ||
|
|
0fa585652d | ||
|
|
1b1206d0b8 | ||
|
|
86afa8a206 | ||
|
|
dda001cb1a | ||
|
|
4ae1663779 |
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() {
|
||||
|
||||
@@ -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 ?? "?"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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
@@ -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 應用程式。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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() }
|
||||
}
|
||||
@@ -58,7 +58,7 @@ struct WelcomeScreen: View {
|
||||
.listStyle(SidebarListStyle())
|
||||
|
||||
Divider()
|
||||
.opacity(colorScheme == .light ? 1 : 0)
|
||||
.opacity(colorScheme == .light ? 1 : 0.5)
|
||||
.edgesIgnoringSafeArea(.vertical)
|
||||
|
||||
ZStack {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user