mirror of
https://github.com/leonboe1/QuickDrop.git
synced 2026-11-11 02:51:11 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fd07e9326 | ||
|
|
70a6b5ff3e | ||
|
|
9536854fa2 | ||
|
|
39ea48fc80 | ||
|
|
4e73fced74 | ||
|
|
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.
@@ -22,6 +22,7 @@ import UIKit
|
|||||||
class InboundNearbyConnection: NearbyConnection {
|
class InboundNearbyConnection: NearbyConnection {
|
||||||
|
|
||||||
private var filesToBeReceived: [Int64: InternalFileInfo] = [:]
|
private var filesToBeReceived: [Int64: InternalFileInfo] = [:]
|
||||||
|
private var downloadedFiles = [URL]()
|
||||||
private var currentState: State = .initial
|
private var currentState: State = .initial
|
||||||
private var cipherCommitment: Data?
|
private var cipherCommitment: Data?
|
||||||
private var textPayloadID: Int64 = 0
|
private var textPayloadID: Int64 = 0
|
||||||
@@ -51,8 +52,17 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.delegate?.connectionWasTerminated(connection: self, error: self.lastError)
|
self.delegate?.connectionWasTerminated(connection: self, error: self.lastError)
|
||||||
|
|
||||||
SaveFilesManager.shared.movePendingFilesToTarget()
|
if !self.downloadedFiles.isEmpty {
|
||||||
SaveFilesManager.shared.stopAccessingSecurityScopedResource()
|
|
||||||
|
#if os(macOS)
|
||||||
|
if Settings.shared.openFinderAfterReceiving {
|
||||||
|
log("[SaveFilesManager] Opening \(self.downloadedFiles.count) file(s) in Finder.")
|
||||||
|
NSWorkspace.shared.activateFileViewerSelecting(self.downloadedFiles)
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
FileManager.default.openDocumentFolder()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +179,8 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
fileInfo.progress?.unpublish()
|
fileInfo.progress?.unpublish()
|
||||||
#endif
|
#endif
|
||||||
SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL)
|
downloadedFiles.append(fileInfo.destinationURL)
|
||||||
|
EXIFUtils.applyEXIFTimestamps(at: fileInfo.destinationURL)
|
||||||
|
|
||||||
filesToBeReceived.removeValue(forKey: id)
|
filesToBeReceived.removeValue(forKey: id)
|
||||||
|
|
||||||
@@ -222,7 +233,8 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
fileInfo.progress?.unpublish()
|
fileInfo.progress?.unpublish()
|
||||||
#endif
|
#endif
|
||||||
filesToBeReceived.removeValue(forKey: id)
|
filesToBeReceived.removeValue(forKey: id)
|
||||||
SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL)
|
downloadedFiles.append(fileInfo.destinationURL)
|
||||||
|
EXIFUtils.applyEXIFTimestamps(at: fileInfo.destinationURL)
|
||||||
|
|
||||||
if filesToBeReceived.isEmpty {
|
if filesToBeReceived.isEmpty {
|
||||||
log("[InboundNearbyConnection \(self.id)] Received file payload. Disconnecting...")
|
log("[InboundNearbyConnection \(self.id)] Received file payload. Disconnecting...")
|
||||||
@@ -495,7 +507,7 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
currentState = .waitingForUserConsent
|
currentState = .waitingForUserConsent
|
||||||
|
|
||||||
if frame.v1.introduction.fileMetadata.count > 0 && frame.v1.introduction.textMetadata.isEmpty {
|
if frame.v1.introduction.fileMetadata.count > 0 && frame.v1.introduction.textMetadata.isEmpty {
|
||||||
let saveDirectory = SaveFilesManager.shared.getSaveDirectory()
|
let saveDirectory = NearbyConnectionManager.shared.getSaveDirectory()
|
||||||
var usedDestinations = Set<URL>()
|
var usedDestinations = Set<URL>()
|
||||||
|
|
||||||
for file in frame.v1.introduction.fileMetadata {
|
for file in frame.v1.introduction.fileMetadata {
|
||||||
@@ -515,23 +527,16 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
checkIfCanProceed(metadata: metadata)
|
checkIfCanProceed(metadata: metadata)
|
||||||
}
|
}
|
||||||
else if let textMetadata = frame.v1.introduction.textMetadata.first {
|
else if let textMetadata = frame.v1.introduction.textMetadata.first {
|
||||||
|
let isURL = textMetadata.type == .url
|
||||||
|
|
||||||
if textMetadata.type == .url || textMetadata.type == .text {
|
let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: textMetadata.textTitle, transferType: isURL ? .url : .text, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil)
|
||||||
|
textPayloadID = textMetadata.payloadID
|
||||||
|
|
||||||
let isClipboardText = textMetadata.type == .text
|
if !isURL {
|
||||||
|
isPlainTextTransfer = true
|
||||||
let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: textMetadata.textTitle, transferType: isClipboardText ? .text : .url, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil)
|
|
||||||
textPayloadID = textMetadata.payloadID
|
|
||||||
|
|
||||||
if isClipboardText {
|
|
||||||
isPlainTextTransfer = true
|
|
||||||
}
|
|
||||||
|
|
||||||
checkIfCanProceed(metadata: metadata)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
rejectDueToUnsupportedFileType(frame)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkIfCanProceed(metadata: metadata)
|
||||||
} else {
|
} else {
|
||||||
rejectDueToUnsupportedFileType(frame)
|
rejectDueToUnsupportedFileType(frame)
|
||||||
}
|
}
|
||||||
@@ -542,7 +547,13 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
let acceptAutomatically = isAuthenticated || Settings.shared.automaticallyAcceptFiles
|
let acceptAutomatically = isAuthenticated || Settings.shared.automaticallyAcceptFiles
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
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 +568,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 {
|
if trustDevice, let peerCertificate = peerCertificate, let remoteDeviceInfo = remoteDeviceInfo {
|
||||||
TrustStore.shared.addTrusted(certificate: peerCertificate, device: remoteDeviceInfo)
|
TrustStore.shared.addTrusted(certificate: peerCertificate, device: remoteDeviceInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.global(qos: .utility).async {
|
DispatchQueue.global(qos: .utility).async {
|
||||||
if accepted {
|
if accepted {
|
||||||
self.acceptTransfer(storeInTemp: storeInTemp)
|
self.acceptTransfer()
|
||||||
} else {
|
} else {
|
||||||
self.rejectTransfer()
|
self.rejectTransfer()
|
||||||
}
|
}
|
||||||
@@ -572,19 +583,28 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func acceptTransfer(storeInTemp: Bool) {
|
private func acceptTransfer() {
|
||||||
if currentState == .disconnected {
|
if currentState == .disconnected {
|
||||||
log("[InboundNearbyConnection \(self.id)] Detected timeout, not accepting transfer")
|
log("[InboundNearbyConnection \(self.id)] Detected timeout, not accepting transfer")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isFileTransferRestricted() {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] File transfer restrictions detected.")
|
||||||
|
delegate?.showPlusScreen()
|
||||||
|
rejectTransfer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
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 {
|
for (id, file) in filesToBeReceived {
|
||||||
let targetURL = storeInTemp ? SaveFilesManager.shared.tempDirectory.appendingPathComponent(file.destinationURL.lastPathComponent) : file.destinationURL
|
let targetURL = file.destinationURL
|
||||||
|
|
||||||
FileManager.default.createFile(atPath: targetURL.path, contents: nil)
|
FileManager.default.createFile(atPath: targetURL.path, contents: nil)
|
||||||
let handle = try FileHandle(forWritingTo: targetURL)
|
let handle = try FileHandle(forWritingTo: targetURL)
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ class NearbyConnection {
|
|||||||
private func receiveFrameAsync() {
|
private func receiveFrameAsync() {
|
||||||
connection.receive(minimumIncompleteLength: 4, maximumLength: 4) { content, _, isComplete, error in
|
connection.receive(minimumIncompleteLength: 4, maximumLength: 4) { content, _, isComplete, error in
|
||||||
if self.connectionClosed {
|
if self.connectionClosed {
|
||||||
|
log("[NearbyConnection \(self.id)] Received FIN from peer, connection closed.")
|
||||||
|
self.connection.cancel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if isComplete {
|
if isComplete {
|
||||||
@@ -171,6 +173,7 @@ class NearbyConnection {
|
|||||||
self.connectionClosedByPeer {
|
self.connectionClosedByPeer {
|
||||||
self.lastError = NearbyError.protocolError("Error.ClosedByPeer".localized())
|
self.lastError = NearbyError.protocolError("Error.ClosedByPeer".localized())
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
|
self.connection.cancel()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -201,6 +204,8 @@ class NearbyConnection {
|
|||||||
private func receiveFrameAsync(length: UInt32) {
|
private func receiveFrameAsync(length: UInt32) {
|
||||||
connection.receive(minimumIncompleteLength: Int(length), maximumLength: Int(length)) { [self] content, _, isComplete, _ in
|
connection.receive(minimumIncompleteLength: Int(length), maximumLength: Int(length)) { [self] content, _, isComplete, _ in
|
||||||
if self.connectionClosed {
|
if self.connectionClosed {
|
||||||
|
log("[NearbyConnection \(self.id)] Received FIN from peer, connection closed.")
|
||||||
|
connection.cancel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if isComplete {
|
if isComplete {
|
||||||
@@ -216,6 +221,7 @@ class NearbyConnection {
|
|||||||
self.connectionClosedByPeer {
|
self.connectionClosedByPeer {
|
||||||
self.lastError = NearbyError.protocolError("Error.ClosedByPeer".localized())
|
self.lastError = NearbyError.protocolError("Error.ClosedByPeer".localized())
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
|
self.connection.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
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() {
|
func disconnect() {
|
||||||
log("[NearbyConnection \(self.id)] Disconnecting from connection.")
|
log("[NearbyConnection \(self.id)] Disconnecting from connection.")
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
private var mdnsServices: [NetService] = []
|
private var mdnsServices: [NetService] = []
|
||||||
private var incomingConnections: [String: InboundNearbyConnection] = [:]
|
private var incomingConnections: [String: InboundNearbyConnection] = [:]
|
||||||
private var foundServices: [String: FoundServiceInfo] = [:]
|
private var foundServices: [String: FoundServiceInfo] = [:]
|
||||||
private var shareExtensionDelegates: [OutboundAppDelegate] = []
|
private var outboundAppDelegates: [OutboundAppDelegate] = []
|
||||||
|
private var inboundAppDelegates: [InboundAppDelegate] = []
|
||||||
private var outgoingTransfers: [String: OutgoingTransferInfo] = [:]
|
private var outgoingTransfers: [String: OutgoingTransferInfo] = [:]
|
||||||
private var startedDeviceDiscovery = false
|
private var startedDeviceDiscovery = false
|
||||||
private var startedAdvertising = false
|
private var startedAdvertising = false
|
||||||
@@ -38,10 +39,11 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
private var qrCodeNameEncryptionKey: SymmetricKey?
|
private var qrCodeNameEncryptionKey: SymmetricKey?
|
||||||
private let hasConnectionMonitor = NWPathMonitor()
|
private let hasConnectionMonitor = NWPathMonitor()
|
||||||
private let connectionMonitorQueue = DispatchQueue(label: "NetworkConnectionMonitorQueue")
|
private let connectionMonitorQueue = DispatchQueue(label: "NetworkConnectionMonitorQueue")
|
||||||
|
private var securityScopeUrl: URL?
|
||||||
|
|
||||||
public let deviceInfo: EndpointInfo
|
public let deviceInfo: EndpointInfo
|
||||||
public let endpointID: [UInt8] = generateEndpointID()
|
public let endpointID: [UInt8] = generateEndpointID()
|
||||||
public var mainAppDelegate: (any InboundAppDelegate)?
|
|
||||||
public static let shared = NearbyConnectionManager()
|
public static let shared = NearbyConnectionManager()
|
||||||
|
|
||||||
@Published var attachments: AttachmentDetails? = nil
|
@Published var attachments: AttachmentDetails? = nil
|
||||||
@@ -91,6 +93,28 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasConnectionMonitor.start(queue: connectionMonitorQueue)
|
hasConnectionMonitor.start(queue: connectionMonitorQueue)
|
||||||
|
|
||||||
|
|
||||||
|
// remove old temp directory
|
||||||
|
let tempPath = FileManager.default.temporaryDirectory
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
|
||||||
|
do {
|
||||||
|
let contents = try fileManager.contentsOfDirectory(at: tempPath, includingPropertiesForKeys: nil)
|
||||||
|
|
||||||
|
var didSomething = false
|
||||||
|
|
||||||
|
for item in contents {
|
||||||
|
didSomething = true
|
||||||
|
try fileManager.removeItem(at: item)
|
||||||
|
}
|
||||||
|
|
||||||
|
if didSomething {
|
||||||
|
log("[SaveFilesManager] Temporary directory cleared.")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
log("[SaveFilesManager] Failed to list contents of temp directory: \(error)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -123,7 +147,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
|
|
||||||
public func becomeInvisible() {
|
public func becomeInvisible() {
|
||||||
if !startedAdvertising {
|
if !startedAdvertising {
|
||||||
print("[NearbyConnectionManager] Already invisible, ignoring becomeInvisible()")
|
log("[NearbyConnectionManager] Already invisible, ignoring becomeInvisible()")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,30 +214,55 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, connection _: InboundNearbyConnection, acceptAutomatically: Bool) {
|
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, connection _: InboundNearbyConnection) {
|
||||||
mainAppDelegate?.obtainUserConsent(transfer: transfer, device: device, acceptAutomatically: acceptAutomatically)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?) {
|
func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?) {
|
||||||
incomingConnections.removeValue(forKey: connection.id)
|
incomingConnections.removeValue(forKey: connection.id)
|
||||||
|
|
||||||
|
// If there are no more incoming connections, we can stop accessing the save directory
|
||||||
|
if incomingConnections.isEmpty {
|
||||||
|
self.stopAccessingSaveDirectory()
|
||||||
|
}
|
||||||
|
|
||||||
if !connection.wasUserRejected {
|
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) {
|
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 }
|
guard let conn = incomingConnections[transferID] else { return }
|
||||||
|
|
||||||
log("[NearbyConnectionManager] Submitting user consent for transfer \(transferID), accepted: \(accept), store in temp: \(storeInTemp)")
|
log("[NearbyConnectionManager] Submitting user consent for transfer \(transferID), accepted: \(accept)")
|
||||||
conn.submitUserConsent(accepted: accept, trustDevice: trustDevice, storeInTemp: storeInTemp)
|
conn.submitUserConsent(accepted: accept, trustDevice: trustDevice)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -283,7 +332,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
startedDeviceDiscovery = false
|
startedDeviceDiscovery = false
|
||||||
|
|
||||||
|
|
||||||
for delegate in shareExtensionDelegates {
|
for delegate in outboundAppDelegates {
|
||||||
|
|
||||||
foundServices.values.forEach { service in
|
foundServices.values.forEach { service in
|
||||||
guard let device = service.device, let id = device.id else { return }
|
guard let device = service.device, let id = device.id else { return }
|
||||||
@@ -294,8 +343,8 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public func addShareExtensionDelegate(_ delegate: OutboundAppDelegate) {
|
public func addOutboundAppDelegate(_ delegate: OutboundAppDelegate) {
|
||||||
shareExtensionDelegates.append(delegate)
|
outboundAppDelegates.append(delegate)
|
||||||
for service in foundServices.values {
|
for service in foundServices.values {
|
||||||
guard let device = service.device else { continue }
|
guard let device = service.device else { continue }
|
||||||
delegate.addDevice(device: device)
|
delegate.addDevice(device: device)
|
||||||
@@ -303,14 +352,28 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public func removeShareExtensionDelegate(_ delegate: OutboundAppDelegate) {
|
public func addInboundAppDelegate(_ delegate: InboundAppDelegate) {
|
||||||
shareExtensionDelegates.removeAll(where: { $0 === delegate })
|
inboundAppDelegates.append(delegate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public func cancelOutgoingTransfer(id: String) {
|
public func removeOutboundAppDelegate(_ delegate: OutboundAppDelegate) {
|
||||||
guard let transfer = outgoingTransfers[id] else { return }
|
outboundAppDelegates.removeAll(where: { $0 === delegate })
|
||||||
transfer.connection.cancel()
|
}
|
||||||
|
|
||||||
|
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 +416,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
|
|
||||||
if qrData == qrCodeAdvertisingToken {
|
if qrData == qrCodeAdvertisingToken {
|
||||||
if let deviceInfo = deviceInfo {
|
if let deviceInfo = deviceInfo {
|
||||||
for delegate in shareExtensionDelegates {
|
for delegate in outboundAppDelegates {
|
||||||
delegate.startTransferWithQrCode(device: deviceInfo)
|
delegate.startTransferWithQrCode(device: deviceInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,7 +428,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
guard let name = String.init(data: decryptedName, encoding: .utf8) else { return }
|
guard let name = String.init(data: decryptedName, encoding: .utf8) else { return }
|
||||||
endpointInfo.name = name
|
endpointInfo.name = name
|
||||||
let deviceInfo = addFoundDevice(foundService: &foundService, endpointInfo: endpointInfo, endpointID: endpointID)
|
let deviceInfo = addFoundDevice(foundService: &foundService, endpointInfo: endpointInfo, endpointID: endpointID)
|
||||||
for delegate in shareExtensionDelegates {
|
for delegate in outboundAppDelegates {
|
||||||
delegate.startTransferWithQrCode(device: deviceInfo)
|
delegate.startTransferWithQrCode(device: deviceInfo)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -381,7 +444,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
foundService.device = deviceInfo
|
foundService.device = deviceInfo
|
||||||
foundServices[endpointID] = foundService
|
foundServices[endpointID] = foundService
|
||||||
|
|
||||||
for delegate in shareExtensionDelegates {
|
for delegate in outboundAppDelegates {
|
||||||
delegate.addDevice(device: deviceInfo)
|
delegate.addDevice(device: deviceInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,7 +455,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
private func removeFoundDevice(service: NWBrowser.Result) {
|
private func removeFoundDevice(service: NWBrowser.Result) {
|
||||||
guard let endpointID = endpointID(for: service) else { return }
|
guard let endpointID = endpointID(for: service) else { return }
|
||||||
guard let _ = foundServices.removeValue(forKey: endpointID) else { return }
|
guard let _ = foundServices.removeValue(forKey: endpointID) else { return }
|
||||||
for delegate in shareExtensionDelegates {
|
for delegate in outboundAppDelegates {
|
||||||
delegate.removeDevice(id: endpointID)
|
delegate.removeDevice(id: endpointID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -457,8 +520,8 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
outgoingTransfers[deviceID] = transfer
|
outgoingTransfers[deviceID] = transfer
|
||||||
conn.start()
|
conn.start()
|
||||||
} catch {
|
} catch {
|
||||||
log("[NearbyConnectionManager] Error zipping URLs: \(error)")
|
log("[NearbyConnectionManager] Error storing URLs: \(error)")
|
||||||
shareExtensionDelegates.forEach { delegate in
|
outboundAppDelegates.forEach { delegate in
|
||||||
delegate.connectionFailed(error: error)
|
delegate.connectionFailed(error: error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -560,6 +623,68 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -- MARK: - Inbound Connection Data Store
|
||||||
|
|
||||||
|
public func getSaveDirectory() -> URL {
|
||||||
|
|
||||||
|
// Not supported on iOS
|
||||||
|
#if os(macOS)
|
||||||
|
if let securityScopeUrl = securityScopeUrl {
|
||||||
|
log("[SaveFilesManager] Using existing security scope URL: \(securityScopeUrl)")
|
||||||
|
return securityScopeUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
if let bookmarkData = Settings.shared.saveFolderBookmark {
|
||||||
|
var isStale = false
|
||||||
|
|
||||||
|
do {
|
||||||
|
let url = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
|
||||||
|
|
||||||
|
if !isStale {
|
||||||
|
if url.startAccessingSecurityScopedResource() {
|
||||||
|
log("[SaveFilesManager] Successfully accessed security scoped resource: \(url)")
|
||||||
|
|
||||||
|
securityScopeUrl = url
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log("[SaveFilesManager] Bookmark is stale, using default downloads folder.")
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
log("[SaveFilesManager] Failed to resolve bookmark: \(error), using default downloads folder.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
return try FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: true).resolvingSymlinksInPath()
|
||||||
|
} catch {
|
||||||
|
fatalError("[SaveFilesManager] Failed to get downloads directory: \(error)")
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// Return the documents directory for iOS
|
||||||
|
do {
|
||||||
|
return try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).resolvingSymlinksInPath()
|
||||||
|
} catch {
|
||||||
|
fatalError("Failed to get documents directory: \(error)")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func stopAccessingSaveDirectory() {
|
||||||
|
// Clean up security scoped resource access
|
||||||
|
guard let url = securityScopeUrl else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log("[SaveFilesManager] Stopping access to security scoped resource: \(url)")
|
||||||
|
url.stopAccessingSecurityScopedResource()
|
||||||
|
securityScopeUrl = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// -- MARK: - Internal Data Model
|
// -- MARK: - Internal Data Model
|
||||||
|
|
||||||
private struct FoundServiceInfo {
|
private struct FoundServiceInfo {
|
||||||
|
|||||||
@@ -70,19 +70,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() {
|
override func connectionReady() {
|
||||||
super.connectionReady()
|
super.connectionReady()
|
||||||
do {
|
do {
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ import SwiftECC
|
|||||||
|
|
||||||
|
|
||||||
protocol InboundNearbyConnectionDelegate {
|
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 updatedTransferProgress(connection: InboundNearbyConnection, progress: Double)
|
||||||
func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?)
|
func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?)
|
||||||
|
func showPlusScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -28,9 +30,11 @@ protocol OutboundNearbyConnectionDelegate {
|
|||||||
|
|
||||||
|
|
||||||
public protocol InboundAppDelegate: AnyObject {
|
public protocol InboundAppDelegate: AnyObject {
|
||||||
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, acceptAutomatically: Bool)
|
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo)
|
||||||
func connectionWasTerminated(from device: RemoteDeviceInfo, wasPlainTextTransfer: Bool, error: Error?)
|
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo)
|
||||||
func transferProgress(progress: Double)
|
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 text
|
||||||
case url
|
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 ?? "?"])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Contains information about the app group used to share data between main app and widget
|
/// Contains information about the app group used to share data between main app and extensions
|
||||||
struct AppGroup {
|
struct AppGroup {
|
||||||
|
|
||||||
/// Identifier of the app group
|
/// Identifier of the app group
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
//
|
||||||
|
// EXIFUtils.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 10.11.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import ImageIO
|
||||||
|
|
||||||
|
struct EXIFUtils {
|
||||||
|
|
||||||
|
/// Applies EXIF timestamps to a given file or directory.
|
||||||
|
static func applyEXIFTimestamps(at url: URL) {
|
||||||
|
let fm = FileManager.default
|
||||||
|
var isDir: ObjCBool = false
|
||||||
|
guard fm.fileExists(atPath: url.path, isDirectory: &isDir) else { return }
|
||||||
|
|
||||||
|
if isDir.boolValue {
|
||||||
|
// It's a directory — enumerate files
|
||||||
|
if let enumerator = fm.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey]) {
|
||||||
|
for case let fileURL as URL in enumerator {
|
||||||
|
guard (try? fileURL.resourceValues(forKeys: [.isRegularFileKey]))?.isRegularFile == true else { continue }
|
||||||
|
applyEXIFTimestamp(to: fileURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// It's a single file
|
||||||
|
applyEXIFTimestamp(to: url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Applies EXIF timestamp (if available) to a single file
|
||||||
|
private static func applyEXIFTimestamp(to fileURL: URL) {
|
||||||
|
guard let exifDate = exifOriginalDate(from: fileURL) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the "creation date"
|
||||||
|
try? FileManager.default.setAttributes([.creationDate: exifDate], ofItemAtPath: fileURL.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Returns the EXIF original date of an image file, adjusted to the current time zone.
|
||||||
|
private static func exifOriginalDate(from url: URL) -> Date? {
|
||||||
|
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
|
||||||
|
let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
|
||||||
|
let exif = properties[kCGImagePropertyExifDictionary] as? [CFString: Any],
|
||||||
|
let dateString = exif[kCGImagePropertyExifDateTimeOriginal] as? String
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "yyyy:MM:dd HH:mm:ss"
|
||||||
|
formatter.timeZone = TimeZone.current // Use the current device time zone
|
||||||
|
return formatter.date(from: dateString)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// FileManager+DocumentsOpener.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 10.11.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import LUI
|
||||||
|
|
||||||
|
extension FileManager {
|
||||||
|
public func openDocumentFolder() {
|
||||||
|
#if !EXTENSION
|
||||||
|
let documentsUrl = self.urls(for: .documentDirectory, in: .userDomainMask).first
|
||||||
|
|
||||||
|
if let documentsUrl = documentsUrl, let sharedUrl = URL(string: "shareddocuments://\(documentsUrl.path)"), UIApplication.shared.canOpenURL(sharedUrl) {
|
||||||
|
UIApplication.shared.open(sharedUrl, options: [:])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
showAlert(title: "CouldNotOpenDocumentsFolder", message: "CouldNotOpenDocumentsFolderDescription")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -111,14 +111,8 @@ extension String {
|
|||||||
|
|
||||||
|
|
||||||
public func fullVersion() -> Bool {
|
public func fullVersion() -> Bool {
|
||||||
|
|
||||||
#if os(macOS)
|
|
||||||
// Enable full functionality if app distributed directly
|
// Enable full functionality if app distributed directly
|
||||||
return Settings.shared.gotPlus || DistributionDetector.isDirectDistributionEnabled
|
return Settings.shared.gotPlus || DistributionDetector.isDirectDistributionEnabled
|
||||||
#else
|
|
||||||
// iOS
|
|
||||||
return true
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,259 +0,0 @@
|
|||||||
//
|
|
||||||
// SaveFilesManager.swift
|
|
||||||
// QuickDrop
|
|
||||||
//
|
|
||||||
// Created by Leon Böttger on 26.07.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import ImageIO
|
|
||||||
#if os(macOS)
|
|
||||||
import AppKit
|
|
||||||
#else
|
|
||||||
import LUI
|
|
||||||
import UIKit
|
|
||||||
#endif
|
|
||||||
|
|
||||||
public class SaveFilesManager {
|
|
||||||
|
|
||||||
private init() {
|
|
||||||
// remove old temp directory
|
|
||||||
|
|
||||||
let tempPath = FileManager.default.temporaryDirectory
|
|
||||||
let fileManager = FileManager.default
|
|
||||||
|
|
||||||
do {
|
|
||||||
let contents = try fileManager.contentsOfDirectory(at: tempPath, includingPropertiesForKeys: nil)
|
|
||||||
|
|
||||||
var didSomething = false
|
|
||||||
|
|
||||||
for item in contents {
|
|
||||||
didSomething = true
|
|
||||||
try fileManager.removeItem(at: item)
|
|
||||||
}
|
|
||||||
|
|
||||||
if didSomething {
|
|
||||||
log("[SaveFilesManager] Temporary directory cleared.")
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
log("[SaveFilesManager] Failed to list contents of temp directory: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static let shared = SaveFilesManager()
|
|
||||||
|
|
||||||
let tempDirectory: URL = FileManager.default.temporaryDirectory.appendingPathComponent("Pending")
|
|
||||||
private var securityScopeUrl: URL?
|
|
||||||
|
|
||||||
private var filesFinishedDownloading = [URL]()
|
|
||||||
private var filesFinishedDownloadingSinceLastRun = [URL]()
|
|
||||||
|
|
||||||
public func registerFileFinishedDownloading(_ fileURL: URL) {
|
|
||||||
filesFinishedDownloading.append(fileURL)
|
|
||||||
filesFinishedDownloadingSinceLastRun.append(fileURL)
|
|
||||||
|
|
||||||
// read out EXIF timestamp and manually set file creation date to it
|
|
||||||
applyEXIFTimestamps(at: fileURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public func movePendingFilesToTarget() {
|
|
||||||
|
|
||||||
if fullVersion() {
|
|
||||||
do {
|
|
||||||
let fileManager = FileManager.default
|
|
||||||
let files = try fileManager.contentsOfDirectory(at: tempDirectory, includingPropertiesForKeys: nil)
|
|
||||||
|
|
||||||
let target = getSaveDirectory()
|
|
||||||
var loggedExecution = false
|
|
||||||
|
|
||||||
for file in files {
|
|
||||||
|
|
||||||
if !loggedExecution {
|
|
||||||
log("[SaveFilesManager] Moving pending files to target directory")
|
|
||||||
loggedExecution = true
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileName = file.lastPathComponent
|
|
||||||
let destinationURL = target.appendingPathComponent(fileName)
|
|
||||||
|
|
||||||
if !filesFinishedDownloading.contains(destinationURL) {
|
|
||||||
log("[SaveFilesManager] File \(file) not finished downloading, skipping")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log("[SaveFilesManager] Moving file: \(file.lastPathComponent) to \(destinationURL.lastPathComponent)")
|
|
||||||
|
|
||||||
do {
|
|
||||||
try fileManager.copyItem(at: file, to: destinationURL)
|
|
||||||
|
|
||||||
let progress = Progress()
|
|
||||||
progress.fileURL = destinationURL
|
|
||||||
progress.totalUnitCount = 10
|
|
||||||
progress.kind = .file
|
|
||||||
progress.isPausable = false
|
|
||||||
#if os(macOS)
|
|
||||||
progress.publish()
|
|
||||||
#endif
|
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
||||||
progress.completedUnitCount = 10
|
|
||||||
#if os(macOS)
|
|
||||||
progress.unpublish()
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
try fileManager.removeItem(at: file)
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
log("[SaveFilesManager] Error moving file: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log("[SaveFilesManager] Moved all pending files to target directory")
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
// Pending directory doesn't exist or is empty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !filesFinishedDownloadingSinceLastRun.isEmpty && !isFileTransferRestricted() {
|
|
||||||
|
|
||||||
#if os(macOS)
|
|
||||||
if Settings.shared.openFinderAfterReceiving {
|
|
||||||
log("[SaveFilesManager] Opening \(filesFinishedDownloadingSinceLastRun.count) file(s) in Finder.")
|
|
||||||
NSWorkspace.shared.activateFileViewerSelecting(filesFinishedDownloadingSinceLastRun)
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
openDownloadedFilesFolder()
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Clear the list of finished files
|
|
||||||
filesFinishedDownloadingSinceLastRun.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
stopAccessingSecurityScopedResource()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#if !os(macOS)
|
|
||||||
public func openDownloadedFilesFolder() {
|
|
||||||
#if !EXTENSION
|
|
||||||
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
|
|
||||||
|
|
||||||
if let documentsUrl = documentsUrl, let sharedUrl = URL(string: "shareddocuments://\(documentsUrl.path)"), UIApplication.shared.canOpenURL(sharedUrl) {
|
|
||||||
UIApplication.shared.open(sharedUrl, options: [:])
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
showAlert(title: "CouldNotOpenDocumentsFolder", message: "CouldNotOpenDocumentsFolderDescription")
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
public func stopAccessingSecurityScopedResource() {
|
|
||||||
guard let url = securityScopeUrl else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log("[SaveFilesManager] Stopping access to security scoped resource: \(url)")
|
|
||||||
url.stopAccessingSecurityScopedResource()
|
|
||||||
securityScopeUrl = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public func getSaveDirectory() -> URL {
|
|
||||||
|
|
||||||
// Not supported on iOS
|
|
||||||
#if os(macOS)
|
|
||||||
if let securityScopeUrl = securityScopeUrl {
|
|
||||||
log("[SaveFilesManager] Using existing security scope URL: \(securityScopeUrl)")
|
|
||||||
return securityScopeUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
if let bookmarkData = Settings.shared.saveFolderBookmark {
|
|
||||||
var isStale = false
|
|
||||||
|
|
||||||
do {
|
|
||||||
let url = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
|
|
||||||
|
|
||||||
if !isStale {
|
|
||||||
if url.startAccessingSecurityScopedResource() {
|
|
||||||
log("[SaveFilesManager] Successfully accessed security scoped resource: \(url)")
|
|
||||||
|
|
||||||
securityScopeUrl = url
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log("[SaveFilesManager] Bookmark is stale, using default downloads folder.")
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
log("[SaveFilesManager] Failed to resolve bookmark: \(error), using default downloads folder.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
return try FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: true).resolvingSymlinksInPath()
|
|
||||||
} catch {
|
|
||||||
fatalError("[SaveFilesManager] Failed to get downloads directory: \(error)")
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
// Return the documents directory for iOS
|
|
||||||
do {
|
|
||||||
return try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).resolvingSymlinksInPath()
|
|
||||||
} catch {
|
|
||||||
fatalError("Failed to get documents directory: \(error)")
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Returns the EXIF original date of an image file, adjusted to the current time zone.
|
|
||||||
private func exifOriginalDate(from url: URL) -> Date? {
|
|
||||||
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
|
|
||||||
let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
|
|
||||||
let exif = properties[kCGImagePropertyExifDictionary] as? [CFString: Any],
|
|
||||||
let dateString = exif[kCGImagePropertyExifDateTimeOriginal] as? String
|
|
||||||
else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateFormat = "yyyy:MM:dd HH:mm:ss"
|
|
||||||
formatter.timeZone = TimeZone.current // Use the current device time zone
|
|
||||||
return formatter.date(from: dateString)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Applies EXIF timestamps to a given file or directory.
|
|
||||||
private func applyEXIFTimestamps(at url: URL) {
|
|
||||||
let fm = FileManager.default
|
|
||||||
var isDir: ObjCBool = false
|
|
||||||
guard fm.fileExists(atPath: url.path, isDirectory: &isDir) else { return }
|
|
||||||
|
|
||||||
if isDir.boolValue {
|
|
||||||
// It's a directory — enumerate files
|
|
||||||
if let enumerator = fm.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey]) {
|
|
||||||
for case let fileURL as URL in enumerator {
|
|
||||||
guard (try? fileURL.resourceValues(forKeys: [.isRegularFileKey]))?.isRegularFile == true else { continue }
|
|
||||||
applyEXIFTimestamp(to: fileURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// It's a single file
|
|
||||||
applyEXIFTimestamp(to: url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Applies EXIF timestamp (if available) to a single file
|
|
||||||
private func applyEXIFTimestamp(to fileURL: URL) {
|
|
||||||
guard let exifDate = exifOriginalDate(from: fileURL) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the "creation date"
|
|
||||||
try? FileManager.default.setAttributes([.creationDate: exifDate], ofItemAtPath: fileURL.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -45,16 +45,21 @@
|
|||||||
EB16B77A2E59BAA1007D9792 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B7762E59BA9F007D9792 /* AppGroup.swift */; };
|
EB16B77A2E59BAA1007D9792 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B7762E59BA9F007D9792 /* AppGroup.swift */; };
|
||||||
EB2F9BFE2DA05F48004010A2 /* BezelNotification in Frameworks */ = {isa = PBXBuildFile; productRef = EB2F9BFD2DA05F48004010A2 /* BezelNotification */; };
|
EB2F9BFE2DA05F48004010A2 /* BezelNotification in Frameworks */ = {isa = PBXBuildFile; productRef = EB2F9BFD2DA05F48004010A2 /* BezelNotification */; };
|
||||||
EB3643602D73A2A2009CDFF8 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB36435F2D73A29F009CDFF8 /* AudioManager.swift */; };
|
EB3643602D73A2A2009CDFF8 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB36435F2D73A29F009CDFF8 /* AudioManager.swift */; };
|
||||||
|
EB4F9D1D2EC113EA00A74A0F /* DistributionDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB704A812E4A7BAE008E608F /* DistributionDetector.swift */; };
|
||||||
|
EB4F9D1F2EC26C8100A74A0F /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; };
|
||||||
|
EB4F9D202EC26C8100A74A0F /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; };
|
||||||
|
EB4F9D212EC26C8100A74A0F /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; };
|
||||||
|
EB4F9D222EC26C8100A74A0F /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; };
|
||||||
|
EB4F9D252EC27CFB00A74A0F /* FileManager+DocumentsOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */; };
|
||||||
|
EB4F9D272EC27CFB00A74A0F /* FileManager+DocumentsOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */; };
|
||||||
EB5C36932E34EBBD0052EFD7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */; };
|
EB5C36932E34EBBD0052EFD7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */; };
|
||||||
EB5C36942E34EBBD0052EFD7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */; };
|
EB5C36942E34EBBD0052EFD7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */; };
|
||||||
EB5C36E42E34EC0E0052EFD7 /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36E32E34EC0E0052EFD7 /* License.txt */; };
|
EB5C36E42E34EC0E0052EFD7 /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36E32E34EC0E0052EFD7 /* License.txt */; };
|
||||||
EB5C36E52E34EC0E0052EFD7 /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36E32E34EC0E0052EFD7 /* License.txt */; };
|
EB5C36E52E34EC0E0052EFD7 /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36E32E34EC0E0052EFD7 /* License.txt */; };
|
||||||
EB5C37B82E377A6F0052EFD7 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; };
|
EB5C37B82E377A6F0052EFD7 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; };
|
||||||
EB5C37B92E377A6F0052EFD7 /* SaveFilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */; };
|
|
||||||
EB5C37BA2E377A6F0052EFD7 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; };
|
EB5C37BA2E377A6F0052EFD7 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; };
|
||||||
EB5C37BC2E377A700052EFD7 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; };
|
EB5C37BC2E377A700052EFD7 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; };
|
||||||
EB5C37BD2E377A700052EFD7 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; };
|
EB5C37BD2E377A700052EFD7 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; };
|
||||||
EB5C37BE2E377A700052EFD7 /* SaveFilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */; };
|
|
||||||
EB5C37BF2E377A780052EFD7 /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
EB5C37BF2E377A780052EFD7 /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
||||||
EB5C37C02E377A780052EFD7 /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
|
EB5C37C02E377A780052EFD7 /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
|
||||||
EB5C37C12E377A780052EFD7 /* offline_wire_formats.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2B29E18CB500A442DA /* offline_wire_formats.pb.swift */; };
|
EB5C37C12E377A780052EFD7 /* offline_wire_formats.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2B29E18CB500A442DA /* offline_wire_formats.pb.swift */; };
|
||||||
@@ -85,7 +90,6 @@
|
|||||||
EB5C37F02E377C3F0052EFD7 /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37EF2E377C3F0052EFD7 /* SwiftECC */; };
|
EB5C37F02E377C3F0052EFD7 /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37EF2E377C3F0052EFD7 /* SwiftECC */; };
|
||||||
EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
||||||
EB5C37F22E377CE70052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
|
EB5C37F22E377CE70052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
|
||||||
EB5C37F32E377CE70052EFD7 /* SaveFilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */; };
|
|
||||||
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
||||||
EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
|
EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
|
||||||
EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2A29E18CB500A442DA /* device_to_device_messages.pb.swift */; };
|
EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2A29E18CB500A442DA /* device_to_device_messages.pb.swift */; };
|
||||||
@@ -123,6 +127,7 @@
|
|||||||
EB7F54142D78E9E700989564 /* LicenseWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7F54132D78E9E400989564 /* LicenseWindow.swift */; };
|
EB7F54142D78E9E700989564 /* LicenseWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7F54132D78E9E400989564 /* LicenseWindow.swift */; };
|
||||||
EB821C992E76FE5300036963 /* SmallSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8A2D7F7CF4004B3FF1 /* SmallSheetView.swift */; };
|
EB821C992E76FE5300036963 /* SmallSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8A2D7F7CF4004B3FF1 /* SmallSheetView.swift */; };
|
||||||
EB821C9A2E76FE5300036963 /* 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 */; };
|
EB886E522E4E19390052B7D9 /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
|
||||||
EB886E532E4E19390052B7D9 /* 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 */; };
|
EB886E542E4E19390052B7D9 /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
|
||||||
@@ -155,7 +160,6 @@
|
|||||||
EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
||||||
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
||||||
EB950B542E520436002D68FA /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
|
EB950B542E520436002D68FA /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
|
||||||
EB950B552E520436002D68FA /* SaveFilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */; };
|
|
||||||
EB950B562E520436002D68FA /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
|
EB950B562E520436002D68FA /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
|
||||||
EB950B572E520591002D68FA /* ErrorAlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */; };
|
EB950B572E520591002D68FA /* ErrorAlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */; };
|
||||||
EB950B582E5205B4002D68FA /* Zip.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBCC27F82D80C9F70079C478 /* Zip.swift */; };
|
EB950B582E5205B4002D68FA /* Zip.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBCC27F82D80C9F70079C478 /* Zip.swift */; };
|
||||||
@@ -326,10 +330,11 @@
|
|||||||
EB2B41E32DC63AC80070FC39 /* SleepManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SleepManager.swift; sourceTree = "<group>"; };
|
EB2B41E32DC63AC80070FC39 /* SleepManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SleepManager.swift; sourceTree = "<group>"; };
|
||||||
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
|
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
|
||||||
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = "<group>"; };
|
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = "<group>"; };
|
||||||
|
EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXIFUtils.swift; sourceTree = "<group>"; };
|
||||||
|
EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+DocumentsOpener.swift"; sourceTree = "<group>"; };
|
||||||
EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||||
EB5C36952E34EBBD0052EFD7 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/ShareViewController.xcstrings; sourceTree = "<group>"; };
|
EB5C36952E34EBBD0052EFD7 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/ShareViewController.xcstrings; sourceTree = "<group>"; };
|
||||||
EB5C36E32E34EC0E0052EFD7 /* License.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = License.txt; sourceTree = "<group>"; };
|
EB5C36E32E34EC0E0052EFD7 /* License.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = License.txt; sourceTree = "<group>"; };
|
||||||
EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveFilesManager.swift; sourceTree = "<group>"; };
|
|
||||||
EB5C36E82E3546100052EFD7 /* DataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataModel.swift; sourceTree = "<group>"; };
|
EB5C36E82E3546100052EFD7 /* DataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataModel.swift; sourceTree = "<group>"; };
|
||||||
EB5C37E22E377C2E0052EFD7 /* QuickDropiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickDropiOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
EB5C37E22E377C2E0052EFD7 /* QuickDropiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickDropiOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
EB5C38382E38C1F60052EFD7 /* justMaybe.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = justMaybe.mp3; sourceTree = "<group>"; };
|
EB5C38382E38C1F60052EFD7 /* justMaybe.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = justMaybe.mp3; sourceTree = "<group>"; };
|
||||||
@@ -341,6 +346,7 @@
|
|||||||
EB7CD1442D6FB0C100CB0CB5 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
EB950B2D2E52027F002D68FA /* iOSShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = iOSShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -437,7 +443,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
699DEBA52AB0573200115D22 /* ShareViewController.swift */,
|
699DEBA52AB0573200115D22 /* ShareViewController.swift */,
|
||||||
EB950B732E520954002D68FA /* Attachments.swift */,
|
EB644B872EA50827006C2E6C /* Helpers */,
|
||||||
EB950B762E521712002D68FA /* AttachmentDetails.swift */,
|
EB950B762E521712002D68FA /* AttachmentDetails.swift */,
|
||||||
EB5C371E2E36693D0052EFD7 /* View */,
|
EB5C371E2E36693D0052EFD7 /* View */,
|
||||||
699DEBAA2AB0573200115D22 /* Info.plist */,
|
699DEBAA2AB0573200115D22 /* Info.plist */,
|
||||||
@@ -477,7 +483,7 @@
|
|||||||
children = (
|
children = (
|
||||||
69DA9A1129E0BF5100A442DA /* AppDelegate.swift */,
|
69DA9A1129E0BF5100A442DA /* AppDelegate.swift */,
|
||||||
EB1325D22E701D8900FD749C /* Settings.swift */,
|
EB1325D22E701D8900FD749C /* Settings.swift */,
|
||||||
EB1325EA2E7025D900FD749C /* SelfTest */,
|
EB644B852EA507CF006C2E6C /* ViewModel */,
|
||||||
EBC05C6A2E33B55100EECC18 /* View */,
|
EBC05C6A2E33B55100EECC18 /* View */,
|
||||||
EBC05C6B2E33B57100EECC18 /* Util */,
|
EBC05C6B2E33B57100EECC18 /* Util */,
|
||||||
EBC05C702E33B57D00EECC18 /* Plus */,
|
EBC05C702E33B57D00EECC18 /* Plus */,
|
||||||
@@ -532,8 +538,9 @@
|
|||||||
EBCC27F82D80C9F70079C478 /* Zip.swift */,
|
EBCC27F82D80C9F70079C478 /* Zip.swift */,
|
||||||
EB16B7762E59BA9F007D9792 /* AppGroup.swift */,
|
EB16B7762E59BA9F007D9792 /* AppGroup.swift */,
|
||||||
EB886E512E4E19390052B7D9 /* HKDF.swift */,
|
EB886E512E4E19390052B7D9 /* HKDF.swift */,
|
||||||
|
EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */,
|
||||||
|
EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */,
|
||||||
EB7CD1442D6FB0C100CB0CB5 /* Logger.swift */,
|
EB7CD1442D6FB0C100CB0CB5 /* Logger.swift */,
|
||||||
EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */,
|
|
||||||
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */,
|
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */,
|
||||||
);
|
);
|
||||||
path = Util;
|
path = Util;
|
||||||
@@ -570,6 +577,34 @@
|
|||||||
path = Model;
|
path = Model;
|
||||||
sourceTree = "<group>";
|
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 */ = {
|
EB950B6C2E5206E2002D68FA /* iOSShareExtension */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -586,12 +621,7 @@
|
|||||||
EB97A0162E51DF4F008B8C5C /* Info.plist */,
|
EB97A0162E51DF4F008B8C5C /* Info.plist */,
|
||||||
EB1325FC2E7034B000FD749C /* InfoPlist.xcstrings */,
|
EB1325FC2E7034B000FD749C /* InfoPlist.xcstrings */,
|
||||||
EB979F8D2E50CA23008B8C5C /* Configuration.swift */,
|
EB979F8D2E50CA23008B8C5C /* Configuration.swift */,
|
||||||
EB979F8E2E50CA23008B8C5C /* ContentView.swift */,
|
EB644B862EA50807006C2E6C /* View */,
|
||||||
EB979F8F2E50CA23008B8C5C /* FilePicker.swift */,
|
|
||||||
EB979F902E50CA23008B8C5C /* SendModel.swift */,
|
|
||||||
EB97A00E2E511120008B8C5C /* ReceiveModel.swift */,
|
|
||||||
EB97A0192E51EA9C008B8C5C /* UIAlert.swift */,
|
|
||||||
EB979F992E50CAA6008B8C5C /* PieProgressView.swift */,
|
|
||||||
EB979F912E50CA23008B8C5C /* QuickDropiOS.entitlements */,
|
EB979F912E50CA23008B8C5C /* QuickDropiOS.entitlements */,
|
||||||
EB979F922E50CA23008B8C5C /* QuickDropiOSApp.swift */,
|
EB979F922E50CA23008B8C5C /* QuickDropiOSApp.swift */,
|
||||||
);
|
);
|
||||||
@@ -605,6 +635,7 @@
|
|||||||
EBD2A2162D2860BE00BCB8E2 /* WelcomeScreen.swift */,
|
EBD2A2162D2860BE00BCB8E2 /* WelcomeScreen.swift */,
|
||||||
EB7F54112D78E95C00989564 /* TutorialView.swift */,
|
EB7F54112D78E95C00989564 /* TutorialView.swift */,
|
||||||
EB7F54132D78E9E400989564 /* LicenseWindow.swift */,
|
EB7F54132D78E9E400989564 /* LicenseWindow.swift */,
|
||||||
|
EB869DB42E92AA33009AAAD4 /* QuickDropToastView.swift */,
|
||||||
EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */,
|
EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */,
|
||||||
EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */,
|
EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */,
|
||||||
EBC05C812E33B58300EECC18 /* IssueViews */,
|
EBC05C812E33B58300EECC18 /* IssueViews */,
|
||||||
@@ -620,6 +651,7 @@
|
|||||||
EB886E662E4F2F7C0052B7D9 /* License.swift */,
|
EB886E662E4F2F7C0052B7D9 /* License.swift */,
|
||||||
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */,
|
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */,
|
||||||
EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */,
|
EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */,
|
||||||
|
EB1325EA2E7025D900FD749C /* SelfTest */,
|
||||||
);
|
);
|
||||||
path = Util;
|
path = Util;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -905,6 +937,7 @@
|
|||||||
EB5C37C72E377A780052EFD7 /* wire_format.pb.swift in Sources */,
|
EB5C37C72E377A780052EFD7 /* wire_format.pb.swift in Sources */,
|
||||||
EB5C37C82E377A780052EFD7 /* ukey.pb.swift in Sources */,
|
EB5C37C82E377A780052EFD7 /* ukey.pb.swift in Sources */,
|
||||||
EB5C37C92E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */,
|
EB5C37C92E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */,
|
||||||
|
EB4F9D222EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
|
||||||
EB950B742E520957002D68FA /* Attachments.swift in Sources */,
|
EB950B742E520957002D68FA /* Attachments.swift in Sources */,
|
||||||
EB1325D32E701D8E00FD749C /* Settings.swift in Sources */,
|
EB1325D32E701D8E00FD749C /* Settings.swift in Sources */,
|
||||||
EB16B7782E59BAA1007D9792 /* AppGroup.swift in Sources */,
|
EB16B7782E59BAA1007D9792 /* AppGroup.swift in Sources */,
|
||||||
@@ -925,7 +958,6 @@
|
|||||||
EB1325EB2E7025D900FD749C /* ApIsolationTester.swift in Sources */,
|
EB1325EB2E7025D900FD749C /* ApIsolationTester.swift in Sources */,
|
||||||
EB1325EC2E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */,
|
EB1325EC2E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */,
|
||||||
EB1325ED2E7025D900FD749C /* ApIsolation6Tester.swift in Sources */,
|
EB1325ED2E7025D900FD749C /* ApIsolation6Tester.swift in Sources */,
|
||||||
EB5C37BE2E377A700052EFD7 /* SaveFilesManager.swift in Sources */,
|
|
||||||
EB9925822E45D53E00357055 /* sharing_enums.pb.swift in Sources */,
|
EB9925822E45D53E00357055 /* sharing_enums.pb.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -938,7 +970,7 @@
|
|||||||
EB3643602D73A2A2009CDFF8 /* AudioManager.swift in Sources */,
|
EB3643602D73A2A2009CDFF8 /* AudioManager.swift in Sources */,
|
||||||
EB5C38412E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */,
|
EB5C38412E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */,
|
||||||
EB5C37B82E377A6F0052EFD7 /* SleepManager.swift in Sources */,
|
EB5C37B82E377A6F0052EFD7 /* SleepManager.swift in Sources */,
|
||||||
EB5C37B92E377A6F0052EFD7 /* SaveFilesManager.swift in Sources */,
|
EB869DB52E92AA3A009AAAD4 /* QuickDropToastView.swift in Sources */,
|
||||||
EB5C37BA2E377A6F0052EFD7 /* Helpers.swift in Sources */,
|
EB5C37BA2E377A6F0052EFD7 /* Helpers.swift in Sources */,
|
||||||
EB05DFBE2E59FC8900EF567A /* TrustedDevicesView.swift in Sources */,
|
EB05DFBE2E59FC8900EF567A /* TrustedDevicesView.swift in Sources */,
|
||||||
EB16B6182E58B4C8007D9792 /* TrustStore.swift in Sources */,
|
EB16B6182E58B4C8007D9792 /* TrustStore.swift in Sources */,
|
||||||
@@ -966,6 +998,7 @@
|
|||||||
EB96ED8C2D7F7E7B004B3FF1 /* SmallSheetView.swift in Sources */,
|
EB96ED8C2D7F7E7B004B3FF1 /* SmallSheetView.swift in Sources */,
|
||||||
EB886E682E4F2F800052B7D9 /* License.swift in Sources */,
|
EB886E682E4F2F800052B7D9 /* License.swift in Sources */,
|
||||||
EB7F54142D78E9E700989564 /* LicenseWindow.swift in Sources */,
|
EB7F54142D78E9E700989564 /* LicenseWindow.swift in Sources */,
|
||||||
|
EB4F9D202EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
|
||||||
69DA9A1229E0BF5100A442DA /* AppDelegate.swift in Sources */,
|
69DA9A1229E0BF5100A442DA /* AppDelegate.swift in Sources */,
|
||||||
EB6F62D72D91F4AF00534EC2 /* SettingsView.swift in Sources */,
|
EB6F62D72D91F4AF00534EC2 /* SettingsView.swift in Sources */,
|
||||||
EB7F54122D78E96000989564 /* TutorialView.swift in Sources */,
|
EB7F54122D78E96000989564 /* TutorialView.swift in Sources */,
|
||||||
@@ -989,11 +1022,11 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
EB4F9D272EC27CFB00A74A0F /* FileManager+DocumentsOpener.swift in Sources */,
|
||||||
EB821C992E76FE5300036963 /* SmallSheetView.swift in Sources */,
|
EB821C992E76FE5300036963 /* SmallSheetView.swift in Sources */,
|
||||||
EB05DFBF2E59FD0A00EF567A /* TrustedDevicesView.swift in Sources */,
|
EB05DFBF2E59FD0A00EF567A /* TrustedDevicesView.swift in Sources */,
|
||||||
EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */,
|
EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */,
|
||||||
EB5C37F22E377CE70052EFD7 /* NearbyConnection.swift in Sources */,
|
EB5C37F22E377CE70052EFD7 /* NearbyConnection.swift in Sources */,
|
||||||
EB5C37F32E377CE70052EFD7 /* SaveFilesManager.swift in Sources */,
|
|
||||||
EB16B6142E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
EB16B6142E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
||||||
EB886E672E4F2F800052B7D9 /* License.swift in Sources */,
|
EB886E672E4F2F800052B7D9 /* License.swift in Sources */,
|
||||||
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
||||||
@@ -1012,6 +1045,7 @@
|
|||||||
EB5C37FD2E377CE70052EFD7 /* Logger.swift in Sources */,
|
EB5C37FD2E377CE70052EFD7 /* Logger.swift in Sources */,
|
||||||
EB97A00F2E511124008B8C5C /* ReceiveModel.swift in Sources */,
|
EB97A00F2E511124008B8C5C /* ReceiveModel.swift in Sources */,
|
||||||
EB5C37FE2E377CE70052EFD7 /* offline_wire_formats.pb.swift in Sources */,
|
EB5C37FE2E377CE70052EFD7 /* offline_wire_formats.pb.swift in Sources */,
|
||||||
|
EB4F9D1F2EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
|
||||||
EB886E6A2E4F37B90052B7D9 /* ErrorAlertHandler.swift in Sources */,
|
EB886E6A2E4F37B90052B7D9 /* ErrorAlertHandler.swift in Sources */,
|
||||||
EB886E5E2E4F13EB0052B7D9 /* Zip.swift in Sources */,
|
EB886E5E2E4F13EB0052B7D9 /* Zip.swift in Sources */,
|
||||||
EB5C383D2E38C2D90052EFD7 /* AudioManager.swift in Sources */,
|
EB5C383D2E38C2D90052EFD7 /* AudioManager.swift in Sources */,
|
||||||
@@ -1045,6 +1079,7 @@
|
|||||||
EB950B4B2E520436002D68FA /* offline_wire_formats.pb.swift in Sources */,
|
EB950B4B2E520436002D68FA /* offline_wire_formats.pb.swift in Sources */,
|
||||||
EB950B4C2E520436002D68FA /* InboundNearbyConnection.swift in Sources */,
|
EB950B4C2E520436002D68FA /* InboundNearbyConnection.swift in Sources */,
|
||||||
EB16B6132E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
EB16B6132E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
||||||
|
EB4F9D212EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
|
||||||
EB950B4D2E520436002D68FA /* NearbyConnection.swift in Sources */,
|
EB950B4D2E520436002D68FA /* NearbyConnection.swift in Sources */,
|
||||||
EB950B4F2E520436002D68FA /* Logger.swift in Sources */,
|
EB950B4F2E520436002D68FA /* Logger.swift in Sources */,
|
||||||
EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */,
|
EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */,
|
||||||
@@ -1054,7 +1089,6 @@
|
|||||||
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */,
|
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */,
|
||||||
EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */,
|
EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */,
|
||||||
EB950B542E520436002D68FA /* HKDF.swift in Sources */,
|
EB950B542E520436002D68FA /* HKDF.swift in Sources */,
|
||||||
EB950B552E520436002D68FA /* SaveFilesManager.swift in Sources */,
|
|
||||||
EB1325D52E701D8E00FD749C /* Settings.swift in Sources */,
|
EB1325D52E701D8E00FD749C /* Settings.swift in Sources */,
|
||||||
EB950B562E520436002D68FA /* DataModel.swift in Sources */,
|
EB950B562E520436002D68FA /* DataModel.swift in Sources */,
|
||||||
EB821C9A2E76FE5300036963 /* SmallSheetView.swift in Sources */,
|
EB821C9A2E76FE5300036963 /* SmallSheetView.swift in Sources */,
|
||||||
@@ -1063,6 +1097,7 @@
|
|||||||
EB16B6192E58B4C8007D9792 /* TrustStore.swift in Sources */,
|
EB16B6192E58B4C8007D9792 /* TrustStore.swift in Sources */,
|
||||||
EB16B7772E59BAA1007D9792 /* AppGroup.swift in Sources */,
|
EB16B7772E59BAA1007D9792 /* AppGroup.swift in Sources */,
|
||||||
EB950B3F2E52038A002D68FA /* FilePicker.swift in Sources */,
|
EB950B3F2E52038A002D68FA /* FilePicker.swift in Sources */,
|
||||||
|
EB4F9D252EC27CFB00A74A0F /* FileManager+DocumentsOpener.swift in Sources */,
|
||||||
EB950B402E52038A002D68FA /* Configuration.swift in Sources */,
|
EB950B402E52038A002D68FA /* Configuration.swift in Sources */,
|
||||||
EB950B412E52038A002D68FA /* PieProgressView.swift in Sources */,
|
EB950B412E52038A002D68FA /* PieProgressView.swift in Sources */,
|
||||||
EB950B592E520649002D68FA /* AudioManager.swift in Sources */,
|
EB950B592E520649002D68FA /* AudioManager.swift in Sources */,
|
||||||
@@ -1070,6 +1105,7 @@
|
|||||||
EB950B6F2E5206E2002D68FA /* ShareViewController.swift in Sources */,
|
EB950B6F2E5206E2002D68FA /* ShareViewController.swift in Sources */,
|
||||||
EB950B432E52038A002D68FA /* ContentView.swift in Sources */,
|
EB950B432E52038A002D68FA /* ContentView.swift in Sources */,
|
||||||
EB950B442E520414002D68FA /* License.swift in Sources */,
|
EB950B442E520414002D68FA /* License.swift in Sources */,
|
||||||
|
EB4F9D1D2EC113EA00A74A0F /* DistributionDetector.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -1239,8 +1275,8 @@
|
|||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SHARED_BUILD = 74;
|
SHARED_BUILD = 84;
|
||||||
SHARED_VERSION = 2.4.1;
|
SHARED_VERSION = 2.5.4;
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@@ -1302,8 +1338,8 @@
|
|||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SHARED_BUILD = 74;
|
SHARED_BUILD = 84;
|
||||||
SHARED_VERSION = 2.4.1;
|
SHARED_VERSION = 2.5.4;
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
|||||||
@@ -33,8 +33,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/leif-ibsen/BigInt",
|
"location" : "https://github.com/leif-ibsen/BigInt",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "afb70a0038bfbba845271b60fa9a58d5840f8017",
|
"revision" : "8c6f93aa37504b7b1ba3954335b5548a19fbbd82",
|
||||||
"version" : "1.21.0"
|
"version" : "1.22.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"location" : "https://github.com/leonboe1/LUI.git",
|
"location" : "https://github.com/leonboe1/LUI.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "main",
|
"branch" : "main",
|
||||||
"revision" : "123192312165c23271cd49492ae7e4fc676d7711"
|
"revision" : "0b862decf853e1f542eeab1f8d548f8ff629deca"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -87,8 +87,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/apple/swift-protobuf.git",
|
"location" : "https://github.com/apple/swift-protobuf.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "e3f69fd321d0c9fcdc16fb576a0cdd956675face",
|
"revision" : "c169a5744230951031770e27e475ff6eefe51f9d",
|
||||||
"version" : "1.31.0"
|
"version" : "1.33.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
"location" : "https://github.com/siteline/swiftui-introspect",
|
"location" : "https://github.com/siteline/swiftui-introspect",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "main",
|
"branch" : "main",
|
||||||
"revision" : "d620dacd1423d9545635eee3fc42c1fa1ab2f665"
|
"revision" : "11d9782bea80656e8a65a643d6ac6396e4a8662e"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = ""
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
|||||||
@@ -48,14 +48,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
let sendClipboardItem = NSMenuItem(title: "SendClipboard".localized(), action: #selector(sendClipboard), keyEquivalent: "")
|
let sendClipboardItem = NSMenuItem(title: "SendClipboard".localized(), action: #selector(sendClipboard), keyEquivalent: "")
|
||||||
|
sendClipboardItem.image = NSImage(systemSymbolName: "doc.on.clipboard", accessibilityDescription: nil)
|
||||||
menu.addItem(sendClipboardItem)
|
menu.addItem(sendClipboardItem)
|
||||||
|
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
let userManualItem = NSMenuItem(title: "UserManual".localized(), action: #selector(openWelcomeScreen), keyEquivalent: "")
|
let userManualItem = NSMenuItem(title: "UserManual".localized(), action: #selector(openWelcomeScreen), keyEquivalent: "")
|
||||||
|
userManualItem.image = NSImage(systemSymbolName: "gear", accessibilityDescription: nil)
|
||||||
menu.addItem(userManualItem)
|
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 = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||||
statusItem?.button?.image = NSImage(named: "MenuBarIcon")
|
statusItem?.button?.image = NSImage(named: "MenuBarIcon")
|
||||||
@@ -65,7 +69,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
iapManager = IAPManager.sharedInstance
|
iapManager = IAPManager.sharedInstance
|
||||||
iapManager?.startObserving()
|
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
|
// app did not lauch before
|
||||||
if !Settings.shared.appLaunchedBefore {
|
if !Settings.shared.appLaunchedBefore {
|
||||||
|
|||||||
+385
-152
File diff suppressed because it is too large.
Load diff
@@ -27,15 +27,7 @@ struct PlusView: View {
|
|||||||
@State var warning = false
|
@State var warning = false
|
||||||
@State var buying = false
|
@State var buying = false
|
||||||
@State var errorText = ""
|
@State var errorText = ""
|
||||||
|
@State var boughtSuccessAlert = false
|
||||||
@State var boughtSuccessAlert = false {
|
|
||||||
didSet {
|
|
||||||
if boughtSuccessAlert {
|
|
||||||
log("User bought plus version. Moving pending files...")
|
|
||||||
SaveFilesManager.shared.movePendingFilesToTarget()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ class ErrorAlertHandler {
|
|||||||
self.isAlertShown = false
|
self.isAlertShown = false
|
||||||
|
|
||||||
if result == .alertFirstButtonReturn {
|
if result == .alertFirstButtonReturn {
|
||||||
log("Sending logging string")
|
//log("Sending logging string")
|
||||||
sendLoggingString()
|
//sendLoggingString()
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
showAlert(title: title, message: description)
|
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(controlPlusScreen: { _ in })
|
||||||
|
|
||||||
|
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())
|
.listStyle(SidebarListStyle())
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
.opacity(colorScheme == .light ? 1 : 0)
|
.opacity(colorScheme == .light ? 1 : 0.5)
|
||||||
.edgesIgnoringSafeArea(.vertical)
|
.edgesIgnoringSafeArea(.vertical)
|
||||||
|
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|||||||
@@ -0,0 +1,318 @@
|
|||||||
|
//
|
||||||
|
// 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) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#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)
|
||||||
|
}
|
||||||
|
#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, onCancel: {
|
||||||
|
NearbyConnectionManager.shared.cancelTransfer(id: connectionID)
|
||||||
|
})
|
||||||
|
#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() {
|
init() {
|
||||||
NearbyConnectionManager.shared.startDeviceDiscovery()
|
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()
|
errorVibration()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
ErrorAlertHandler.shared.showErrorAlert(for: selectedDevice?.name ?? "?", error: error)
|
ErrorAlertHandler.shared.showErrorAlert(for: selectedDevice?.name ?? "UnknownDevice".localized(), error: error)
|
||||||
|
|
||||||
progressValue = nil
|
progressValue = nil
|
||||||
selectedDevice = nil
|
selectedDevice = nil
|
||||||
@@ -116,7 +121,7 @@ class SendModel: ObservableObject, OutboundAppDelegate {
|
|||||||
progressValue = 0
|
progressValue = 0
|
||||||
progressState = nil
|
progressState = nil
|
||||||
selectedDevice = nil
|
selectedDevice = nil
|
||||||
NearbyConnectionManager.shared.cancelOutgoingTransfer(id: device.id!)
|
NearbyConnectionManager.shared.cancelTransfer(id: device.id!)
|
||||||
}
|
}
|
||||||
else {
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,7 @@ struct ContentView: View {
|
|||||||
}, settingsView: {
|
}, settingsView: {
|
||||||
CustomSection(header: settings.gotPlus ? "" : "General", footer: "TrustedDevicesFooterShort") {
|
CustomSection(header: settings.gotPlus ? "" : "General", footer: "TrustedDevicesFooterShort") {
|
||||||
LUIButton {
|
LUIButton {
|
||||||
SaveFilesManager.shared.openDownloadedFilesFolder()
|
FileManager.default.openDocumentFolder()
|
||||||
} label: {
|
} label: {
|
||||||
NavigationLinkLabel(imageName: "folder.fill", text: "BrowseDownloadedFiles")
|
NavigationLinkLabel(imageName: "folder.fill", text: "BrowseDownloadedFiles")
|
||||||
}
|
}
|
||||||
@@ -41,9 +41,14 @@ struct DeviceListView: View {
|
|||||||
@State private var qrCode: Image? = nil
|
@State private var qrCode: Image? = nil
|
||||||
@StateObject var sendModel = SendModel()
|
@StateObject var sendModel = SendModel()
|
||||||
|
|
||||||
#if !EXTENSION
|
#if !EXTENSION
|
||||||
@StateObject var receiveModel = ReceiveModel()
|
@StateObject var receiveModel = ReceiveModel(controlPlusScreen: { show in
|
||||||
#endif
|
log("[DeviceListView] Setting showPlusScreen to \(show)")
|
||||||
|
DeviceListViewModel.sharedInstance.showPlusScreen = show
|
||||||
|
})
|
||||||
|
#endif
|
||||||
|
|
||||||
|
@ObservedObject var model = DeviceListViewModel.sharedInstance
|
||||||
|
|
||||||
@Environment(\.scenePhase) var scenePhase
|
@Environment(\.scenePhase) var scenePhase
|
||||||
@Environment(\.sheetActive) var sheetActive
|
@Environment(\.sheetActive) var sheetActive
|
||||||
@@ -54,7 +59,7 @@ struct DeviceListView: View {
|
|||||||
|
|
||||||
let showsLoadingIndicator = nearbyConnectionManager.hasLocalNetworkPermission && nearbyConnectionManager.isConnectedToLocalNetwork
|
let showsLoadingIndicator = nearbyConnectionManager.hasLocalNetworkPermission && nearbyConnectionManager.isConnectedToLocalNetwork
|
||||||
|
|
||||||
BottomBarView(header: "QuickDrop ", navigationBarLayout: isShareExtension() ? .SmallOnlyAlways : .Default, bottomViewHeight: 30) {
|
BottomBarView(header: "QuickDrop", navigationBarLayout: isShareExtension() ? .SmallOnlyAlways : .Default, bottomViewHeight: 30) {
|
||||||
VStack {
|
VStack {
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
@@ -185,7 +190,7 @@ struct DeviceListView: View {
|
|||||||
|
|
||||||
log("[ScenePhase] App became active")
|
log("[ScenePhase] App became active")
|
||||||
NearbyConnectionManager.shared.startDeviceDiscovery()
|
NearbyConnectionManager.shared.startDeviceDiscovery()
|
||||||
#if !EXTENSION
|
#if !EXTENSION
|
||||||
NearbyConnectionManager.shared.becomeVisible()
|
NearbyConnectionManager.shared.becomeVisible()
|
||||||
|
|
||||||
if Settings.shared.incomingTransmissionCount > 0 {
|
if Settings.shared.incomingTransmissionCount > 0 {
|
||||||
@@ -193,18 +198,21 @@ struct DeviceListView: View {
|
|||||||
requestReviewOnce()
|
requestReviewOnce()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
if newValue == .background {
|
if newValue == .background {
|
||||||
|
|
||||||
log("[ScenePhase] App went to background")
|
log("[ScenePhase] App went to background")
|
||||||
#if !EXTENSION
|
#if !EXTENSION
|
||||||
NearbyConnectionManager.shared.becomeInvisible()
|
NearbyConnectionManager.shared.becomeInvisible()
|
||||||
#endif
|
#endif
|
||||||
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if !EXTENSION
|
||||||
|
.modifier(PlusVersionSheetModifier(sheetActive: $model.showPlusScreen, isPlusVersion: $settings.gotPlus))
|
||||||
|
#endif
|
||||||
.animation(.smooth, value: nearbyConnectionManager.hasLocalNetworkPermission)
|
.animation(.smooth, value: nearbyConnectionManager.hasLocalNetworkPermission)
|
||||||
.animation(.smooth, value: nearbyConnectionManager.isConnectedToLocalNetwork)
|
.animation(.smooth, value: nearbyConnectionManager.isConnectedToLocalNetwork)
|
||||||
.navigationBarItems(trailing: ZStack {
|
.navigationBarItems(trailing: ZStack {
|
||||||
@@ -218,6 +226,12 @@ struct DeviceListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class DeviceListViewModel: SharedInstance {
|
||||||
|
static var sharedInstance = DeviceListViewModel()
|
||||||
|
|
||||||
|
@Published var showPlusScreen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
struct DeviceFilePickerButton: View {
|
struct DeviceFilePickerButton: View {
|
||||||
|
|
||||||
@@ -297,11 +311,11 @@ struct DeviceButtonLabel: View {
|
|||||||
|
|
||||||
|
|
||||||
func isShareExtension() -> Bool {
|
func isShareExtension() -> Bool {
|
||||||
#if EXTENSION
|
#if EXTENSION
|
||||||
return true
|
return true
|
||||||
#else
|
#else
|
||||||
return false
|
return false
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -162,10 +162,10 @@ fileprivate struct DataUrl: Transferable {
|
|||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
} onResult: { urls, text in
|
} onResult: { urls, text in
|
||||||
if let urls = urls {
|
if let urls = urls {
|
||||||
print("Selected URLs: \(urls)")
|
log("Selected URLs: \(urls)")
|
||||||
}
|
}
|
||||||
if let text = text {
|
if let text = text {
|
||||||
print("Clipboard text: \(text)")
|
log("Clipboard text: \(text)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File renamed without changes.
@@ -18,41 +18,36 @@ final class ProgressAlert {
|
|||||||
private weak var progressView: UIProgressView?
|
private weak var progressView: UIProgressView?
|
||||||
|
|
||||||
// MARK: - Initial Accept / Decline
|
// 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) {
|
||||||
guard let vc = topMostViewController() else { return }
|
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)
|
onAccept(.Accept)
|
||||||
self.showProgressAlert(on: vc)
|
}))
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
let alert = UIAlertController(title: title,
|
if let acceptAlways = acceptAlwaysLabel {
|
||||||
message: message,
|
alert.addAction(UIAlertAction(title: acceptAlways, style: .default, handler: { _ in
|
||||||
preferredStyle: .alert)
|
onAccept(.AcceptAlways)
|
||||||
|
|
||||||
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)
|
|
||||||
}))
|
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Progress Alert
|
// MARK: - Progress Alert
|
||||||
private func showProgressAlert(on vc: UIViewController) {
|
private func showProgressAlert(onCancel: @escaping () -> Void) {
|
||||||
|
|
||||||
|
guard let vc = topMostViewController() else { return }
|
||||||
|
|
||||||
let alert = UIAlertController(title: "Receiving".localized(),
|
let alert = UIAlertController(title: "Receiving".localized(),
|
||||||
message: "\n\n",
|
message: "\n\n",
|
||||||
preferredStyle: .alert)
|
preferredStyle: .alert)
|
||||||
@@ -67,7 +62,8 @@ final class ProgressAlert {
|
|||||||
])
|
])
|
||||||
|
|
||||||
let cancel = UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: { _ in
|
let cancel = UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: { _ in
|
||||||
print("Cancel during progress")
|
log("Cancel during progress")
|
||||||
|
onCancel()
|
||||||
})
|
})
|
||||||
alert.addAction(cancel)
|
alert.addAction(cancel)
|
||||||
|
|
||||||
@@ -77,10 +73,24 @@ final class ProgressAlert {
|
|||||||
vc.present(alert, animated: true)
|
vc.present(alert, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Update Progress
|
// MARK: - Update Progress
|
||||||
// progress: 0…1 → update progress, nil → finished
|
// progress: 0…1 → update progress, nil → finished
|
||||||
func updateProgress(_ progress: Double?, completion: @escaping () -> Void = {}) {
|
func updateProgress(_ progress: Double?, onCancel: @escaping () -> Void, completion: @escaping () -> Void = {}) {
|
||||||
|
|
||||||
|
// No progress to show, nothing shown currently → just complete
|
||||||
|
if progressAlert == nil && progress == nil {
|
||||||
|
completion()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|
||||||
|
// Show progress if not shown yet
|
||||||
|
if self.progressAlert == nil {
|
||||||
|
self.showProgressAlert(onCancel: onCancel)
|
||||||
|
}
|
||||||
|
|
||||||
if let p = progress {
|
if let p = progress {
|
||||||
self.progressView?.setProgress(Float(p), animated: true)
|
self.progressView?.setProgress(Float(p), animated: true)
|
||||||
completion()
|
completion()
|
||||||
@@ -24,31 +24,8 @@ func loadAttachments(with extensionContext: NSExtensionContext?, loadedItems: @e
|
|||||||
let item = extensionContext.inputItems[0] as! NSExtensionItem
|
let item = extensionContext.inputItems[0] as! NSExtensionItem
|
||||||
|
|
||||||
if let attachments = item.attachments {
|
if let attachments = item.attachments {
|
||||||
if let text = item.attributedContentText?.string {
|
|
||||||
|
|
||||||
log("Found text content: \(text)")
|
if !attachments.isEmpty {
|
||||||
|
|
||||||
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 {
|
|
||||||
for attachment in attachments {
|
for attachment in attachments {
|
||||||
|
|
||||||
if attachment.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
|
if attachment.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
|
||||||
@@ -59,6 +36,10 @@ func loadAttachments(with extensionContext: NSExtensionContext?, loadedItems: @e
|
|||||||
} else if let url = data as? NSURL {
|
} else if let url = data as? NSURL {
|
||||||
result.urls += [(url as URL)]
|
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)
|
checkIfAttachmentsLoaded(result: result, attachmentCount: attachments.count, ignoredAttachments: ignoredAttachments)
|
||||||
}
|
}
|
||||||
@@ -82,6 +63,9 @@ func loadAttachments(with extensionContext: NSExtensionContext?, loadedItems: @e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
loadTextIfPossible()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log("No attachments found in extension context. Exiting.")
|
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) {
|
func checkIfAttachmentsLoaded(result: AttachmentDetails, attachmentCount: Int, ignoredAttachments: Int = 0) {
|
||||||
|
|
||||||
var result = result
|
var result = result
|
||||||
|
|
||||||
if result.urls.count == attachmentCount-ignoredAttachments {
|
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 {
|
if result.urls[0].isFileURL {
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
|
|||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
dicoverDevices()
|
dicoverDevices()
|
||||||
NearbyConnectionManager.shared.addShareExtensionDelegate(self)
|
NearbyConnectionManager.shared.addOutboundAppDelegate(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -111,13 +111,13 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
|
|||||||
if chosenDevice == nil {
|
if chosenDevice == nil {
|
||||||
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
||||||
}
|
}
|
||||||
NearbyConnectionManager.shared.removeShareExtensionDelegate(self)
|
NearbyConnectionManager.shared.removeOutboundAppDelegate(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@IBAction func cancel(_: AnyObject?) {
|
@IBAction func cancel(_: AnyObject?) {
|
||||||
if let device = chosenDevice {
|
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)
|
let cancelError = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil)
|
||||||
extensionContext!.cancelRequest(withError: cancelError)
|
extensionContext!.cancelRequest(withError: cancelError)
|
||||||
|
|||||||
@@ -32,11 +32,7 @@ class ShareViewController: UIViewController {
|
|||||||
LUIInit(configuration: configuration)
|
LUIInit(configuration: configuration)
|
||||||
LUISettings.sharedInstance.appLaunchedBefore = true
|
LUISettings.sharedInstance.appLaunchedBefore = true
|
||||||
|
|
||||||
// Replace with your app’s SwiftUI entry view
|
let hostingController = UIHostingController(rootView: ContentView())
|
||||||
let contentView = ContentView()
|
|
||||||
|
|
||||||
// Embed SwiftUI inside the extension’s controller
|
|
||||||
let hostingController = UIHostingController(rootView: contentView)
|
|
||||||
|
|
||||||
addChild(hostingController)
|
addChild(hostingController)
|
||||||
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|||||||
Reference in new issue
Block a user