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 | ||
|
|
66acbf25a3 | ||
|
|
93537094e4 | ||
|
|
b6b11c00c0 | ||
|
|
92671d8983 | ||
|
|
bfc23fde15 | ||
|
|
a39a5a324a | ||
|
|
fd7dfd959b | ||
|
|
bd4b181a49 | ||
|
|
f066a11608 | ||
|
|
5429126c1e | ||
|
|
4bf8a9052d | ||
|
|
f786af9d12 | ||
|
|
6def7dbc9f | ||
|
|
fcc0ce0818 | ||
|
|
fd6978cc90 | ||
|
|
12ef87c4da | ||
|
|
518fabcd33 | ||
|
|
eb6bb8fc4d | ||
|
|
d443412697 | ||
|
|
110b0a4ae7 | ||
|
|
2b272c81c1 | ||
|
|
11afc232ab | ||
|
|
943414db58 | ||
|
|
b135229d30 | ||
|
|
6126d9428d |
No files matched your search
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 488 KiB |
|
Before Width: | Height: | Size: 492 KiB |
|
Before Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 488 KiB |
|
Before Width: | Height: | Size: 492 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
@@ -22,6 +22,7 @@ import UIKit
|
||||
class InboundNearbyConnection: NearbyConnection {
|
||||
|
||||
private var filesToBeReceived: [Int64: InternalFileInfo] = [:]
|
||||
private var downloadedFiles = [URL]()
|
||||
private var currentState: State = .initial
|
||||
private var cipherCommitment: Data?
|
||||
private var textPayloadID: Int64 = 0
|
||||
@@ -51,8 +52,17 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.connectionWasTerminated(connection: self, error: self.lastError)
|
||||
|
||||
SaveFilesManager.shared.movePendingFilesToTarget()
|
||||
SaveFilesManager.shared.stopAccessingSecurityScopedResource()
|
||||
if !self.downloadedFiles.isEmpty {
|
||||
|
||||
#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)
|
||||
fileInfo.progress?.unpublish()
|
||||
#endif
|
||||
SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL)
|
||||
downloadedFiles.append(fileInfo.destinationURL)
|
||||
EXIFUtils.applyEXIFTimestamps(at: fileInfo.destinationURL)
|
||||
|
||||
filesToBeReceived.removeValue(forKey: id)
|
||||
|
||||
@@ -222,7 +233,8 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
fileInfo.progress?.unpublish()
|
||||
#endif
|
||||
filesToBeReceived.removeValue(forKey: id)
|
||||
SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL)
|
||||
downloadedFiles.append(fileInfo.destinationURL)
|
||||
EXIFUtils.applyEXIFTimestamps(at: fileInfo.destinationURL)
|
||||
|
||||
if filesToBeReceived.isEmpty {
|
||||
log("[InboundNearbyConnection \(self.id)] Received file payload. Disconnecting...")
|
||||
@@ -392,6 +404,13 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
|
||||
let pkeFrame = frame.v1.pairedKeyEncryption
|
||||
|
||||
if frame.v1.certificateInfo.publicCertificate.isEmpty {
|
||||
log("[InboundNearbyConnection \(self.id)] Detected legacy peer.")
|
||||
}
|
||||
else {
|
||||
log("[InboundNearbyConnection \(self.id)] Detected QuickDrop peer.")
|
||||
}
|
||||
|
||||
// Check if we can accept the connection automatically
|
||||
if !pkeFrame.secretIDHash.isEmpty,
|
||||
let trustedCertData = TrustStore.shared.findTrustedKey(for: pkeFrame.secretIDHash),
|
||||
@@ -461,9 +480,11 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
|
||||
|
||||
private func makeFileDestinationURL(_ initialDest: URL) -> URL {
|
||||
private func makeFileDestinationURL(_ initialDest: URL, usedDestinations: Set<URL>) -> URL {
|
||||
var dest = initialDest
|
||||
if FileManager.default.fileExists(atPath: dest.path) {
|
||||
let fm = FileManager.default
|
||||
|
||||
if fm.fileExists(atPath: dest.path) || usedDestinations.contains(dest) {
|
||||
var counter = 1
|
||||
var path: String
|
||||
let ext = dest.pathExtension
|
||||
@@ -474,8 +495,8 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
path += ".\(ext)"
|
||||
}
|
||||
counter += 1
|
||||
} while FileManager.default.fileExists(atPath: path)
|
||||
dest = URL(fileURLWithPath: path)
|
||||
dest = URL(fileURLWithPath: path)
|
||||
} while fm.fileExists(atPath: dest.path) || usedDestinations.contains(dest)
|
||||
}
|
||||
return dest
|
||||
}
|
||||
@@ -486,13 +507,19 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
currentState = .waitingForUserConsent
|
||||
|
||||
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>()
|
||||
|
||||
for file in frame.v1.introduction.fileMetadata {
|
||||
let dest = makeFileDestinationURL(saveDirectory.appendingPathComponent(file.name))
|
||||
let info = InternalFileInfo(meta: FileMetadata(name: file.name, size: file.size, mimeType: file.mimeType),
|
||||
payloadID: file.payloadID,
|
||||
destinationURL: dest)
|
||||
let initialDest = saveDirectory.appendingPathComponent(file.name)
|
||||
let dest = makeFileDestinationURL(initialDest, usedDestinations: usedDestinations)
|
||||
usedDestinations.insert(dest)
|
||||
|
||||
let info = InternalFileInfo(
|
||||
meta: FileMetadata(name: file.name, size: file.size, mimeType: file.mimeType),
|
||||
payloadID: file.payloadID,
|
||||
destinationURL: dest
|
||||
)
|
||||
filesToBeReceived[file.payloadID] = info
|
||||
bytesToBeTransferred += file.size
|
||||
}
|
||||
@@ -500,23 +527,16 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
checkIfCanProceed(metadata: metadata)
|
||||
}
|
||||
else if let textMetadata = frame.v1.introduction.textMetadata.first {
|
||||
let isURL = textMetadata.type == .url
|
||||
|
||||
if textMetadata.type == .url || textMetadata.type == .text {
|
||||
|
||||
let isClipboardText = textMetadata.type == .text
|
||||
|
||||
let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: textMetadata.textTitle, transferType: isClipboardText ? .text : .url, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil)
|
||||
textPayloadID = textMetadata.payloadID
|
||||
|
||||
if isClipboardText {
|
||||
isPlainTextTransfer = true
|
||||
}
|
||||
|
||||
checkIfCanProceed(metadata: metadata)
|
||||
}
|
||||
else {
|
||||
rejectDueToUnsupportedFileType(frame)
|
||||
let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: textMetadata.textTitle, transferType: isURL ? .url : .text, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil)
|
||||
textPayloadID = textMetadata.payloadID
|
||||
|
||||
if !isURL {
|
||||
isPlainTextTransfer = true
|
||||
}
|
||||
|
||||
checkIfCanProceed(metadata: metadata)
|
||||
} else {
|
||||
rejectDueToUnsupportedFileType(frame)
|
||||
}
|
||||
@@ -524,10 +544,16 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
|
||||
func checkIfCanProceed(metadata: TransferMetadata) {
|
||||
|
||||
let acceptAutomatically = isAuthenticated || UserDefaults.standard.bool(forKey: UserDefaultsKeys.automaticallyAcceptFiles.rawValue)
|
||||
let acceptAutomatically = isAuthenticated || Settings.shared.automaticallyAcceptFiles
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.obtainUserConsent(transfer: metadata, device: self.remoteDeviceInfo!, connection: self, acceptAutomatically: acceptAutomatically)
|
||||
if acceptAutomatically {
|
||||
self.delegate?.obtainedUserConsentAutomatically(transfer: metadata, device: self.remoteDeviceInfo!, connection: self)
|
||||
self.submitUserConsent(accepted: true, trustDevice: false)
|
||||
}
|
||||
else {
|
||||
self.delegate?.obtainUserConsent(transfer: metadata, device: self.remoteDeviceInfo!, connection: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -542,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 {
|
||||
TrustStore.shared.addTrusted(certificate: peerCertificate, device: remoteDeviceInfo)
|
||||
}
|
||||
|
||||
DispatchQueue.global(qos: .utility).async {
|
||||
if accepted {
|
||||
self.acceptTransfer(storeInTemp: storeInTemp)
|
||||
self.acceptTransfer()
|
||||
} else {
|
||||
self.rejectTransfer()
|
||||
}
|
||||
@@ -557,19 +583,28 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
|
||||
|
||||
private func acceptTransfer(storeInTemp: Bool) {
|
||||
private func acceptTransfer() {
|
||||
if currentState == .disconnected {
|
||||
log("[InboundNearbyConnection \(self.id)] Detected timeout, not accepting transfer")
|
||||
return
|
||||
}
|
||||
|
||||
if isFileTransferRestricted() {
|
||||
log("[InboundNearbyConnection \(self.id)] File transfer restrictions detected.")
|
||||
delegate?.showPlusScreen()
|
||||
rejectTransfer()
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
if storeInTemp {
|
||||
try FileManager.default.createDirectory(at: SaveFilesManager.shared.tempDirectory, withIntermediateDirectories: true)
|
||||
|
||||
if filesToBeReceived.count >= 1 {
|
||||
// Show progress bar for file transfers immediately
|
||||
NearbyConnectionManager.shared.updatedTransferProgress(connection: self, progress: 0)
|
||||
}
|
||||
|
||||
for (id, file) in filesToBeReceived {
|
||||
let targetURL = storeInTemp ? SaveFilesManager.shared.tempDirectory.appendingPathComponent(file.destinationURL.lastPathComponent) : file.destinationURL
|
||||
let targetURL = file.destinationURL
|
||||
|
||||
FileManager.default.createFile(atPath: targetURL.path, contents: nil)
|
||||
let handle = try FileHandle(forWritingTo: targetURL)
|
||||
@@ -638,12 +673,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
#endif
|
||||
|
||||
guard file.created else { continue }
|
||||
do {
|
||||
try FileManager.default.removeItem(at: file.destinationURL)
|
||||
}
|
||||
catch {
|
||||
// if it fails, we don't care. Could be because file was not created yet
|
||||
}
|
||||
try? FileManager.default.removeItem(at: file.destinationURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,6 +164,8 @@ class NearbyConnection {
|
||||
private func receiveFrameAsync() {
|
||||
connection.receive(minimumIncompleteLength: 4, maximumLength: 4) { content, _, isComplete, error in
|
||||
if self.connectionClosed {
|
||||
log("[NearbyConnection \(self.id)] Received FIN from peer, connection closed.")
|
||||
self.connection.cancel()
|
||||
return
|
||||
}
|
||||
if isComplete {
|
||||
@@ -171,6 +173,7 @@ class NearbyConnection {
|
||||
self.connectionClosedByPeer {
|
||||
self.lastError = NearbyError.protocolError("Error.ClosedByPeer".localized())
|
||||
self.disconnect()
|
||||
self.connection.cancel()
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -201,6 +204,8 @@ class NearbyConnection {
|
||||
private func receiveFrameAsync(length: UInt32) {
|
||||
connection.receive(minimumIncompleteLength: Int(length), maximumLength: Int(length)) { [self] content, _, isComplete, _ in
|
||||
if self.connectionClosed {
|
||||
log("[NearbyConnection \(self.id)] Received FIN from peer, connection closed.")
|
||||
connection.cancel()
|
||||
return
|
||||
}
|
||||
if isComplete {
|
||||
@@ -216,6 +221,7 @@ class NearbyConnection {
|
||||
self.connectionClosedByPeer {
|
||||
self.lastError = NearbyError.protocolError("Error.ClosedByPeer".localized())
|
||||
self.disconnect()
|
||||
self.connection.cancel()
|
||||
}
|
||||
|
||||
return
|
||||
@@ -515,6 +521,19 @@ class NearbyConnection {
|
||||
}
|
||||
|
||||
|
||||
func cancel() {
|
||||
cancelled = true
|
||||
if encryptionDone {
|
||||
var cancel = Sharing_Nearby_Frame()
|
||||
cancel.version = .v1
|
||||
cancel.v1 = Sharing_Nearby_V1Frame()
|
||||
cancel.v1.type = .cancel
|
||||
try? sendTransferSetupFrame(cancel)
|
||||
}
|
||||
try? sendDisconnectionAndDisconnect()
|
||||
}
|
||||
|
||||
|
||||
func disconnect() {
|
||||
log("[NearbyConnection \(self.id)] Disconnecting from connection.")
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
import RNDeviceName
|
||||
import DeviceKit
|
||||
#endif
|
||||
|
||||
import Foundation
|
||||
@@ -16,6 +16,7 @@ import System
|
||||
import SwiftECC
|
||||
import CryptoKit
|
||||
import SwiftUI
|
||||
import QRCode
|
||||
|
||||
public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearbyConnectionDelegate, OutboundNearbyConnectionDelegate, ObservableObject {
|
||||
|
||||
@@ -26,7 +27,8 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
private var mdnsServices: [NetService] = []
|
||||
private var incomingConnections: [String: InboundNearbyConnection] = [:]
|
||||
private var foundServices: [String: FoundServiceInfo] = [:]
|
||||
private var shareExtensionDelegates: [OutboundAppDelegate] = []
|
||||
private var outboundAppDelegates: [OutboundAppDelegate] = []
|
||||
private var inboundAppDelegates: [InboundAppDelegate] = []
|
||||
private var outgoingTransfers: [String: OutgoingTransferInfo] = [:]
|
||||
private var startedDeviceDiscovery = false
|
||||
private var startedAdvertising = false
|
||||
@@ -37,19 +39,20 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
private var qrCodeNameEncryptionKey: SymmetricKey?
|
||||
private let hasConnectionMonitor = NWPathMonitor()
|
||||
private let connectionMonitorQueue = DispatchQueue(label: "NetworkConnectionMonitorQueue")
|
||||
private var securityScopeUrl: URL?
|
||||
|
||||
public let deviceInfo: EndpointInfo
|
||||
public let endpointID: [UInt8] = generateEndpointID()
|
||||
public var mainAppDelegate: (any InboundAppDelegate)?
|
||||
|
||||
public static let shared = NearbyConnectionManager()
|
||||
|
||||
@Published var attachments: AttachmentDetails? = nil
|
||||
@Published var hasLocalNetworkAccess = true
|
||||
@Published var isConnectedViaWiFi = true
|
||||
@Published var hasLocalNetworkPermission = true
|
||||
@Published var isConnectedToLocalNetwork = true
|
||||
|
||||
public var connectionUpdateCallback: (Bool) -> Void = { _ in } {
|
||||
didSet {
|
||||
connectionUpdateCallback(isConnectedViaWiFi)
|
||||
connectionUpdateCallback(isConnectedToLocalNetwork)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +61,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
#if os(macOS)
|
||||
self.deviceInfo = EndpointInfo(name: Host.current().localizedName ?? "Mac", deviceType: .computer)
|
||||
#else
|
||||
let marketingName = UIDevice.current.marketingName.withoutBracketedContent
|
||||
let marketingName = Device.current.description.withoutBracketedContent
|
||||
let isiPad = UIDevice.current.model == "iPad"
|
||||
|
||||
self.deviceInfo = EndpointInfo(name: marketingName, deviceType: isiPad ? .tablet : .phone)
|
||||
@@ -69,22 +72,49 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
super.init()
|
||||
|
||||
hasConnectionMonitor.pathUpdateHandler = { path in
|
||||
if path.usesInterfaceType(.wifi) {
|
||||
log("[NearbyConnectionManager] Connected via WiFi.")
|
||||
DispatchQueue.main.async {
|
||||
self.isConnectedViaWiFi = true
|
||||
self.connectionUpdateCallback(true)
|
||||
}
|
||||
} else {
|
||||
log("[NearbyConnectionManager] WiFi connection lost.")
|
||||
DispatchQueue.main.async {
|
||||
self.isConnectedViaWiFi = false
|
||||
self.connectionUpdateCallback(false)
|
||||
|
||||
#if os(macOS)
|
||||
// On Mac, the local network is available as long as the path is satisfied, since Macs do not have cellular modems
|
||||
let isConnected = path.status == .satisfied
|
||||
#else
|
||||
// On iPhone and iPad, the local network is only available using Wi-Fi or Ethernet
|
||||
let isConnected = path.usesInterfaceType(.wifi) || path.usesInterfaceType(.wiredEthernet)
|
||||
#endif
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if isConnected {
|
||||
log("[NearbyConnectionManager] Connected to local network.")
|
||||
} else {
|
||||
log("[NearbyConnectionManager] Local network lost.")
|
||||
}
|
||||
self.isConnectedToLocalNetwork = isConnected
|
||||
self.connectionUpdateCallback(isConnected)
|
||||
}
|
||||
}
|
||||
|
||||
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)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +147,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
|
||||
public func becomeInvisible() {
|
||||
if !startedAdvertising {
|
||||
print("[NearbyConnectionManager] Already invisible, ignoring becomeInvisible()")
|
||||
log("[NearbyConnectionManager] Already invisible, ignoring becomeInvisible()")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -130,12 +160,10 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
|
||||
|
||||
private static func generateEndpointID() -> [UInt8] {
|
||||
let userDefaultsKey = UserDefaultsKeys.endpointID.rawValue
|
||||
|
||||
// Try to retrieve from UserDefaults
|
||||
if let savedString = UserDefaults.standard.string(forKey: userDefaultsKey),
|
||||
let savedData = savedString.data(using: .utf8)
|
||||
{
|
||||
if let savedString = Settings.shared.endpointID,
|
||||
let savedData = savedString.data(using: .utf8) {
|
||||
return [UInt8](savedData)
|
||||
}
|
||||
|
||||
@@ -148,7 +176,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
|
||||
// Save to UserDefaults as String
|
||||
let idString = String(bytes: id, encoding: .utf8) ?? ""
|
||||
UserDefaults.standard.set(idString, forKey: userDefaultsKey)
|
||||
Settings.shared.endpointID = idString
|
||||
|
||||
return id
|
||||
}
|
||||
@@ -186,30 +214,55 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
}
|
||||
|
||||
|
||||
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, connection _: InboundNearbyConnection, acceptAutomatically: Bool) {
|
||||
mainAppDelegate?.obtainUserConsent(transfer: transfer, device: device, acceptAutomatically: acceptAutomatically)
|
||||
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, connection _: InboundNearbyConnection) {
|
||||
inboundAppDelegates.forEach { delegate in
|
||||
delegate.obtainUserConsent(transfer: transfer, device: device)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo, connection: InboundNearbyConnection) {
|
||||
inboundAppDelegates.forEach { delegate in
|
||||
delegate.obtainedUserConsentAutomatically(transfer: transfer, device: device)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?) {
|
||||
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 {
|
||||
mainAppDelegate?.connectionWasTerminated(from: connection.remoteDeviceInfo ?? RemoteDeviceInfo(name: "??", type: .unknown), wasPlainTextTransfer: connection.isPlainTextTransfer, error: error)
|
||||
inboundAppDelegates.forEach { delegate in
|
||||
delegate.connectionWasTerminated(connectionID: connection.id, from: connection.remoteDeviceInfo ?? RemoteDeviceInfo(name: "UnknownDevice".localized(), type: .unknown), wasPlainTextTransfer: connection.isPlainTextTransfer, error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func showPlusScreen() {
|
||||
inboundAppDelegates.forEach { delegate in
|
||||
delegate.showPlusScreen()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updatedTransferProgress(connection: InboundNearbyConnection, progress: Double) {
|
||||
mainAppDelegate?.transferProgress(progress: progress)
|
||||
inboundAppDelegates.forEach { delegate in
|
||||
delegate.transferProgress(connectionID: connection.id, progress: progress)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func submitUserConsent(transferID: String, accept: Bool, trustDevice: Bool, storeInTemp: Bool = false) {
|
||||
public func submitUserConsent(transferID: String, accept: Bool, trustDevice: Bool) {
|
||||
guard let conn = incomingConnections[transferID] else { return }
|
||||
|
||||
log("[NearbyConnectionManager] Submitting user consent for transfer \(transferID), accepted: \(accept), store in temp: \(storeInTemp)")
|
||||
conn.submitUserConsent(accepted: accept, trustDevice: trustDevice, storeInTemp: storeInTemp)
|
||||
log("[NearbyConnectionManager] Submitting user consent for transfer \(transferID), accepted: \(accept)")
|
||||
conn.submitUserConsent(accepted: accept, trustDevice: trustDevice)
|
||||
}
|
||||
|
||||
|
||||
@@ -245,13 +298,13 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
log("[NearbyConnectionManager] Browser failed: \(error)")
|
||||
case .ready:
|
||||
log("[NearbyConnectionManager] Browser ready")
|
||||
self.hasLocalNetworkAccess = true
|
||||
self.hasLocalNetworkPermission = true
|
||||
case .waiting(let error):
|
||||
log("[NearbyConnectionManager] Browser waiting: \(error)")
|
||||
|
||||
if error == NWError.dns(DNSServiceErrorType(kDNSServiceErr_PolicyDenied)) {
|
||||
log("[NearbyConnectionManager] Local network access not granted.")
|
||||
self.hasLocalNetworkAccess = false
|
||||
self.hasLocalNetworkPermission = false
|
||||
}
|
||||
default:
|
||||
break
|
||||
@@ -279,7 +332,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
startedDeviceDiscovery = false
|
||||
|
||||
|
||||
for delegate in shareExtensionDelegates {
|
||||
for delegate in outboundAppDelegates {
|
||||
|
||||
foundServices.values.forEach { service in
|
||||
guard let device = service.device, let id = device.id else { return }
|
||||
@@ -290,8 +343,8 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
}
|
||||
|
||||
|
||||
public func addShareExtensionDelegate(_ delegate: OutboundAppDelegate) {
|
||||
shareExtensionDelegates.append(delegate)
|
||||
public func addOutboundAppDelegate(_ delegate: OutboundAppDelegate) {
|
||||
outboundAppDelegates.append(delegate)
|
||||
for service in foundServices.values {
|
||||
guard let device = service.device else { continue }
|
||||
delegate.addDevice(device: device)
|
||||
@@ -299,14 +352,28 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
}
|
||||
|
||||
|
||||
public func removeShareExtensionDelegate(_ delegate: OutboundAppDelegate) {
|
||||
shareExtensionDelegates.removeAll(where: { $0 === delegate })
|
||||
public func addInboundAppDelegate(_ delegate: InboundAppDelegate) {
|
||||
inboundAppDelegates.append(delegate)
|
||||
}
|
||||
|
||||
|
||||
public func cancelOutgoingTransfer(id: String) {
|
||||
guard let transfer = outgoingTransfers[id] else { return }
|
||||
transfer.connection.cancel()
|
||||
public func removeOutboundAppDelegate(_ delegate: OutboundAppDelegate) {
|
||||
outboundAppDelegates.removeAll(where: { $0 === delegate })
|
||||
}
|
||||
|
||||
public func removeInboundAppDelegate(_ delegate: InboundAppDelegate) {
|
||||
inboundAppDelegates.removeAll(where: { $0 === delegate })
|
||||
}
|
||||
|
||||
|
||||
public func cancelTransfer(id: String) {
|
||||
|
||||
if let transfer = incomingConnections[id] {
|
||||
transfer.cancel()
|
||||
}
|
||||
if let transfer = outgoingTransfers[id] {
|
||||
transfer.connection.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -349,7 +416,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
|
||||
if qrData == qrCodeAdvertisingToken {
|
||||
if let deviceInfo = deviceInfo {
|
||||
for delegate in shareExtensionDelegates {
|
||||
for delegate in outboundAppDelegates {
|
||||
delegate.startTransferWithQrCode(device: deviceInfo)
|
||||
}
|
||||
}
|
||||
@@ -361,7 +428,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
guard let name = String.init(data: decryptedName, encoding: .utf8) else { return }
|
||||
endpointInfo.name = name
|
||||
let deviceInfo = addFoundDevice(foundService: &foundService, endpointInfo: endpointInfo, endpointID: endpointID)
|
||||
for delegate in shareExtensionDelegates {
|
||||
for delegate in outboundAppDelegates {
|
||||
delegate.startTransferWithQrCode(device: deviceInfo)
|
||||
}
|
||||
} catch {
|
||||
@@ -377,7 +444,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
foundService.device = deviceInfo
|
||||
foundServices[endpointID] = foundService
|
||||
|
||||
for delegate in shareExtensionDelegates {
|
||||
for delegate in outboundAppDelegates {
|
||||
delegate.addDevice(device: deviceInfo)
|
||||
}
|
||||
|
||||
@@ -388,19 +455,23 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
private func removeFoundDevice(service: NWBrowser.Result) {
|
||||
guard let endpointID = endpointID(for: service) else { return }
|
||||
guard let _ = foundServices.removeValue(forKey: endpointID) else { return }
|
||||
for delegate in shareExtensionDelegates {
|
||||
for delegate in outboundAppDelegates {
|
||||
delegate.removeDevice(id: endpointID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func generateQrCodeKey() -> String{
|
||||
public func generateQrCodeKey() -> Image? {
|
||||
|
||||
log("[NearbyConnectionManager] Generating new QR code key")
|
||||
|
||||
let domain = Domain.instance(curve: .EC256r1)
|
||||
let (pubKey, privKey) = domain.makeKeyPair()
|
||||
qrCodePrivateKey = privKey
|
||||
var keyData = Data()
|
||||
keyData.append(contentsOf: [0, 0, 2])
|
||||
let keyBytes = Data(pubKey.w.x.asSignedBytes())
|
||||
|
||||
// Sometimes, for some keys, there will be a leading zero byte. Strip that, Android really hates it (it breaks the endpoint info)
|
||||
keyData.append(keyBytes.suffixOfAtMost(numBytes: 32))
|
||||
|
||||
@@ -408,7 +479,26 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
qrCodeAdvertisingToken = HKDF.deriveKey(ikm: ikm, salt: Data(), info: "advertisingContext".data(using: .utf8)!, outputLength: 16).data()
|
||||
qrCodeNameEncryptionKey = HKDF.deriveKey(ikm: ikm, salt: Data(), info: "encryptionKey".data(using: .utf8)!, outputLength: 16)
|
||||
|
||||
return keyData.urlSafeBase64EncodedString()
|
||||
|
||||
do {
|
||||
let qrKey = keyData.urlSafeBase64EncodedString()
|
||||
let qrCodeImage = try QRCode.build
|
||||
.text("https://quickshare.google/qrcode#key=\(qrKey)")
|
||||
.backgroundColor(CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 0))
|
||||
.quietZonePixelCount(3)
|
||||
.onPixels.shape(.circle())
|
||||
.eye.shape(.squircle())
|
||||
.errorCorrection(.low)
|
||||
.generate
|
||||
.image(dimension: 1000)
|
||||
|
||||
return Image(decorative: qrCodeImage, scale: 1.0, orientation: .up)
|
||||
}
|
||||
catch {
|
||||
log("Error generating QR code: \(error)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -430,8 +520,8 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
outgoingTransfers[deviceID] = transfer
|
||||
conn.start()
|
||||
} catch {
|
||||
log("[NearbyConnectionManager] Error zipping URLs: \(error)")
|
||||
shareExtensionDelegates.forEach { delegate in
|
||||
log("[NearbyConnectionManager] Error storing URLs: \(error)")
|
||||
outboundAppDelegates.forEach { delegate in
|
||||
delegate.connectionFailed(error: error)
|
||||
}
|
||||
}
|
||||
@@ -533,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
|
||||
|
||||
private struct FoundServiceInfo {
|
||||
|
||||
@@ -68,19 +68,6 @@ class OutboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func cancel() {
|
||||
cancelled = true
|
||||
if encryptionDone {
|
||||
var cancel = Sharing_Nearby_Frame()
|
||||
cancel.version = .v1
|
||||
cancel.v1 = Sharing_Nearby_V1Frame()
|
||||
cancel.v1.type = .cancel
|
||||
try? sendTransferSetupFrame(cancel)
|
||||
}
|
||||
try? sendDisconnectionAndDisconnect()
|
||||
}
|
||||
|
||||
|
||||
override func connectionReady() {
|
||||
|
||||
@@ -12,9 +12,11 @@ import SwiftECC
|
||||
|
||||
|
||||
protocol InboundNearbyConnectionDelegate {
|
||||
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, connection: InboundNearbyConnection, acceptAutomatically: Bool)
|
||||
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, connection: InboundNearbyConnection)
|
||||
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo, connection: InboundNearbyConnection)
|
||||
func updatedTransferProgress(connection: InboundNearbyConnection, progress: Double)
|
||||
func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?)
|
||||
func showPlusScreen()
|
||||
}
|
||||
|
||||
|
||||
@@ -28,9 +30,11 @@ protocol OutboundNearbyConnectionDelegate {
|
||||
|
||||
|
||||
public protocol InboundAppDelegate: AnyObject {
|
||||
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, acceptAutomatically: Bool)
|
||||
func connectionWasTerminated(from device: RemoteDeviceInfo, wasPlainTextTransfer: Bool, error: Error?)
|
||||
func transferProgress(progress: Double)
|
||||
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo)
|
||||
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo)
|
||||
func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo, wasPlainTextTransfer: Bool, error: Error?)
|
||||
func transferProgress(connectionID: String, progress: Double)
|
||||
func showPlusScreen()
|
||||
}
|
||||
|
||||
|
||||
@@ -133,6 +137,36 @@ public struct TransferMetadata {
|
||||
case text
|
||||
case url
|
||||
}
|
||||
|
||||
private func getSummary() -> String {
|
||||
if let textTitle = textDescription {
|
||||
return textTitle
|
||||
} else if files.count == 1 {
|
||||
return files[0].name
|
||||
} else {
|
||||
return String.localizedStringWithFormat("NFiles".localized(), files.count)
|
||||
}
|
||||
}
|
||||
|
||||
public func getDescription(deviceName: String, alreadyAccepted: Bool) -> String {
|
||||
|
||||
let fileStr = getSummary()
|
||||
|
||||
switch type {
|
||||
case .file:
|
||||
return String(format: (alreadyAccepted ? "DeviceCurrentlySendingFiles" : "DeviceSendingFiles").localized(), arguments: [deviceName, fileStr])
|
||||
|
||||
case .text:
|
||||
return String(format: (alreadyAccepted ? "DeviceCurrentlySendingText" : "DeviceSendingText").localized(), arguments: [deviceName, fileStr])
|
||||
|
||||
case .url:
|
||||
return String(format: (alreadyAccepted ? "DeviceCurrentlySendingUrl" : "DeviceSendingUrl").localized(), arguments: [deviceName, fileStr])
|
||||
}
|
||||
}
|
||||
|
||||
public func getPinCodeMessage() -> String {
|
||||
return String(format: "PinCode".localized(), arguments: [pinCode ?? "?"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
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 {
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
@@ -110,35 +110,12 @@ extension String {
|
||||
}
|
||||
|
||||
|
||||
public enum UserDefaultsKeys: String, CaseIterable {
|
||||
case isEligibleForIap = "isEligibleForIap"
|
||||
case appLaunchedBefore = "ShowedWelcomeScreen"
|
||||
case plusVersion = "isPlusVersion"
|
||||
case transmissionCount = "reviewRequestCountKey"
|
||||
case automaticallyAcceptFiles = "automaticallyAcceptFiles"
|
||||
case saveFolderBookmark = "saveFolderBookmark"
|
||||
case openFinderAfterReceiving = "openFinderAfterReceiving"
|
||||
case endpointID = "endpointID"
|
||||
}
|
||||
|
||||
|
||||
public func isPlusVersion() -> Bool {
|
||||
|
||||
#if os(macOS)
|
||||
public func fullVersion() -> Bool {
|
||||
// Enable full functionality if app distributed directly
|
||||
return DistributionDetector.isDirectDistributionEnabled || UserDefaults.standard.bool(forKey: UserDefaultsKeys.plusVersion.rawValue)
|
||||
#else
|
||||
// iOS
|
||||
return true
|
||||
#endif
|
||||
return Settings.shared.gotPlus || DistributionDetector.isDirectDistributionEnabled
|
||||
}
|
||||
|
||||
|
||||
public func isFileTransferRestricted() -> Bool {
|
||||
(!isPlusVersion() && incomingTransmissionCount() > 1)
|
||||
}
|
||||
|
||||
|
||||
public func incomingTransmissionCount() -> Int {
|
||||
return UserDefaults.standard.integer(forKey: UserDefaultsKeys.transmissionCount.rawValue)
|
||||
(!fullVersion() && Settings.shared.incomingTransmissionCount > 1)
|
||||
}
|
||||
@@ -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 isPlusVersion() {
|
||||
do {
|
||||
let fileManager = FileManager.default
|
||||
let files = try fileManager.contentsOfDirectory(at: tempDirectory, includingPropertiesForKeys: nil)
|
||||
|
||||
let target = getSaveDirectory()
|
||||
var loggedExecution = false
|
||||
|
||||
for file in files {
|
||||
|
||||
if !loggedExecution {
|
||||
log("[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 UserDefaults.standard.bool(forKey: UserDefaultsKeys.openFinderAfterReceiving.rawValue) {
|
||||
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 = UserDefaults.standard.data(forKey: UserDefaultsKeys.saveFolderBookmark.rawValue) {
|
||||
var isStale = false
|
||||
|
||||
do {
|
||||
let url = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
|
||||
|
||||
if !isStale {
|
||||
if url.startAccessingSecurityScopedResource() {
|
||||
log("[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)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,20 @@
|
||||
EB05DFBE2E59FC8900EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
|
||||
EB05DFBF2E59FD0A00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
|
||||
EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
|
||||
EB1325D32E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
|
||||
EB1325D42E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
|
||||
EB1325D52E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
|
||||
EB1325D62E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
|
||||
EB1325EB2E7025D900FD749C /* ApIsolationTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E92E7025D900FD749C /* ApIsolationTester.swift */; };
|
||||
EB1325EC2E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */; };
|
||||
EB1325ED2E7025D900FD749C /* ApIsolation6Tester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E82E7025D900FD749C /* ApIsolation6Tester.swift */; };
|
||||
EB1325F42E7025D900FD749C /* ApIsolationTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E92E7025D900FD749C /* ApIsolationTester.swift */; };
|
||||
EB1325F52E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */; };
|
||||
EB1325F62E7025D900FD749C /* ApIsolation6Tester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E82E7025D900FD749C /* ApIsolation6Tester.swift */; };
|
||||
EB1325F82E70288300FD749C /* SendToExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325F72E70287C00FD749C /* SendToExtension.swift */; };
|
||||
EB1325FA2E70288300FD749C /* SendToExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325F72E70287C00FD749C /* SendToExtension.swift */; };
|
||||
EB1325FD2E7034B000FD749C /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB1325FC2E7034B000FD749C /* InfoPlist.xcstrings */; };
|
||||
EB1325FE2E7034B000FD749C /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB1325FC2E7034B000FD749C /* InfoPlist.xcstrings */; };
|
||||
EB16B6122E58B4B9007D9792 /* IdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B6112E58B4B9007D9792 /* IdentityManager.swift */; };
|
||||
EB16B6132E58B4B9007D9792 /* IdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B6112E58B4B9007D9792 /* IdentityManager.swift */; };
|
||||
EB16B6142E58B4B9007D9792 /* IdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B6112E58B4B9007D9792 /* IdentityManager.swift */; };
|
||||
@@ -31,16 +45,21 @@
|
||||
EB16B77A2E59BAA1007D9792 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B7762E59BA9F007D9792 /* AppGroup.swift */; };
|
||||
EB2F9BFE2DA05F48004010A2 /* BezelNotification in Frameworks */ = {isa = PBXBuildFile; productRef = EB2F9BFD2DA05F48004010A2 /* BezelNotification */; };
|
||||
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 */; };
|
||||
EB5C36942E34EBBD0052EFD7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */; };
|
||||
EB5C36E42E34EC0E0052EFD7 /* 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 */; };
|
||||
EB5C37B92E377A6F0052EFD7 /* SaveFilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */; };
|
||||
EB5C37BA2E377A6F0052EFD7 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; };
|
||||
EB5C37BC2E377A700052EFD7 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.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 */; };
|
||||
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 */; };
|
||||
@@ -71,7 +90,6 @@
|
||||
EB5C37F02E377C3F0052EFD7 /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37EF2E377C3F0052EFD7 /* SwiftECC */; };
|
||||
EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.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 */; };
|
||||
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 */; };
|
||||
@@ -97,16 +115,19 @@
|
||||
EB704A822E4A7BB3008E608F /* DistributionDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB704A812E4A7BAE008E608F /* DistributionDetector.swift */; };
|
||||
EB704A832E4B10D4008E608F /* DistributionDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB704A812E4A7BAE008E608F /* DistributionDetector.swift */; };
|
||||
EB704A842E4B10D4008E608F /* DistributionDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB704A812E4A7BAE008E608F /* DistributionDetector.swift */; };
|
||||
EB721F242E75AFA900F2099F /* QuickDropIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = EB721F232E75AFA900F2099F /* QuickDropIcon.icon */; };
|
||||
EB721F252E75AFA900F2099F /* QuickDropIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = EB721F232E75AFA900F2099F /* QuickDropIcon.icon */; };
|
||||
EB7654782E525ACC006B4AA3 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = EB7654772E525ACC006B4AA3 /* MainMenu.xib */; };
|
||||
EB7654A12E53252D006B4AA3 /* AttachmentDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB950B762E521712002D68FA /* AttachmentDetails.swift */; };
|
||||
EB7654C02E534A8E006B4AA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 69DA9A1329E0BF5200A442DA /* Assets.xcassets */; };
|
||||
EB7654C12E534A8E006B4AA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 69DA9A1329E0BF5200A442DA /* Assets.xcassets */; };
|
||||
EB7654C42E534E89006B4AA3 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = EB7654C32E534E89006B4AA3 /* InfoPlist.strings */; };
|
||||
EB7654C62E534E89006B4AA3 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = EB7654C32E534E89006B4AA3 /* InfoPlist.strings */; };
|
||||
EB7CD1452D6FB0C300CB0CB5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7CD1442D6FB0C100CB0CB5 /* Logger.swift */; };
|
||||
EB7CD1462D6FB0E800CB0CB5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7CD1442D6FB0C100CB0CB5 /* Logger.swift */; };
|
||||
EB7F54122D78E96000989564 /* TutorialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7F54112D78E95C00989564 /* TutorialView.swift */; };
|
||||
EB7F54142D78E9E700989564 /* LicenseWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7F54132D78E9E400989564 /* LicenseWindow.swift */; };
|
||||
EB821C992E76FE5300036963 /* SmallSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8A2D7F7CF4004B3FF1 /* SmallSheetView.swift */; };
|
||||
EB821C9A2E76FE5300036963 /* SmallSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8A2D7F7CF4004B3FF1 /* SmallSheetView.swift */; };
|
||||
EB869DB52E92AA3A009AAAD4 /* QuickDropToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB869DB42E92AA33009AAAD4 /* QuickDropToastView.swift */; };
|
||||
EB886E522E4E19390052B7D9 /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
|
||||
EB886E532E4E19390052B7D9 /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
|
||||
EB886E542E4E19390052B7D9 /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
|
||||
@@ -139,7 +160,6 @@
|
||||
EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.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 */; };
|
||||
EB950B552E520436002D68FA /* SaveFilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */; };
|
||||
EB950B562E520436002D68FA /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
|
||||
EB950B572E520591002D68FA /* ErrorAlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */; };
|
||||
EB950B582E5205B4002D68FA /* Zip.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBCC27F82D80C9F70079C478 /* Zip.swift */; };
|
||||
@@ -152,7 +172,6 @@
|
||||
EB950B612E520674002D68FA /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = EB950B602E520674002D68FA /* SwiftECC */; };
|
||||
EB950B632E52067A002D68FA /* QRCode in Frameworks */ = {isa = PBXBuildFile; productRef = EB950B622E52067A002D68FA /* QRCode */; };
|
||||
EB950B652E52067D002D68FA /* LUI in Frameworks */ = {isa = PBXBuildFile; productRef = EB950B642E52067D002D68FA /* LUI */; };
|
||||
EB950B672E520680002D68FA /* RNDeviceName in Frameworks */ = {isa = PBXBuildFile; productRef = EB950B662E520680002D68FA /* RNDeviceName */; };
|
||||
EB950B6F2E5206E2002D68FA /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB950B6B2E5206E2002D68FA /* ShareViewController.swift */; };
|
||||
EB950B702E520734002D68FA /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */; };
|
||||
EB950B742E520957002D68FA /* Attachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB950B732E520954002D68FA /* Attachments.swift */; };
|
||||
@@ -173,7 +192,6 @@
|
||||
EB979F9A2E50CAA6008B8C5C /* PieProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F992E50CAA6008B8C5C /* PieProgressView.swift */; };
|
||||
EB97A00F2E511124008B8C5C /* ReceiveModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB97A00E2E511120008B8C5C /* ReceiveModel.swift */; };
|
||||
EB97A0102E51CD13008B8C5C /* ReceiveModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB97A00E2E511120008B8C5C /* ReceiveModel.swift */; };
|
||||
EB97A0132E51D6B2008B8C5C /* RNDeviceName in Frameworks */ = {isa = PBXBuildFile; productRef = EB97A0122E51D6B2008B8C5C /* RNDeviceName */; };
|
||||
EB97A0142E51DD00008B8C5C /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36E32E34EC0E0052EFD7 /* License.txt */; };
|
||||
EB97A0152E51DD00008B8C5C /* timeIsNow.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = EBC05C8A2E33B59E00EECC18 /* timeIsNow.mp3 */; };
|
||||
EB97A01A2E51EAA2008B8C5C /* UIAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB97A0192E51EA9C008B8C5C /* UIAlert.swift */; };
|
||||
@@ -183,16 +201,13 @@
|
||||
EB9925852E45EF0900357055 /* QRCode in Frameworks */ = {isa = PBXBuildFile; productRef = EB9925842E45EF0900357055 /* QRCode */; };
|
||||
EB9925872E45F12F00357055 /* QRCode in Frameworks */ = {isa = PBXBuildFile; productRef = EB9925862E45F12F00357055 /* QRCode */; };
|
||||
EB9925892E45F13500357055 /* QRCode in Frameworks */ = {isa = PBXBuildFile; productRef = EB9925882E45F13500357055 /* QRCode */; };
|
||||
EBADEB8F2E8029750021B5E4 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = EBADEB8E2E8029750021B5E4 /* DeviceKit */; };
|
||||
EBADEB912E802A830021B5E4 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = EBADEB902E802A830021B5E4 /* DeviceKit */; };
|
||||
EBC05C5B2E33937200EECC18 /* ErrorAlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */; };
|
||||
EBC05C5C2E3394EF00EECC18 /* ErrorAlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */; };
|
||||
EBC05C712E33B57D00EECC18 /* Converter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C6D2E33B57D00EECC18 /* Converter.swift */; };
|
||||
EBC05C722E33B57D00EECC18 /* Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C6E2E33B57D00EECC18 /* Helper.swift */; };
|
||||
EBC05C732E33B57D00EECC18 /* PlusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C6F2E33B57D00EECC18 /* PlusView.swift */; };
|
||||
EBC05C782E33B58100EECC18 /* ActiveNetworkInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C742E33B58100EECC18 /* ActiveNetworkInterface.swift */; };
|
||||
EBC05C792E33B58100EECC18 /* ApIsolation6Tester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C752E33B58100EECC18 /* ApIsolation6Tester.swift */; };
|
||||
EBC05C7A2E33B58100EECC18 /* ApIsolationTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C762E33B58100EECC18 /* ApIsolationTester.swift */; };
|
||||
EBC05C7B2E33B58100EECC18 /* ActiveNetworkInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C742E33B58100EECC18 /* ActiveNetworkInterface.swift */; };
|
||||
EBC05C7C2E33B58100EECC18 /* ApIsolationTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C762E33B58100EECC18 /* ApIsolationTester.swift */; };
|
||||
EBC05C822E33B58300EECC18 /* ApIsolationIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C7D2E33B58300EECC18 /* ApIsolationIssueView.swift */; };
|
||||
EBC05C832E33B58300EECC18 /* FirewallIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C7E2E33B58300EECC18 /* FirewallIssueView.swift */; };
|
||||
EBC05C842E33B58300EECC18 /* IssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C7F2E33B58300EECC18 /* IssueView.swift */; };
|
||||
@@ -303,42 +318,35 @@
|
||||
69DA9A2C29E18CB500A442DA /* securegcm.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securegcm.pb.swift; sourceTree = "<group>"; };
|
||||
69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securemessage.pb.swift; sourceTree = "<group>"; };
|
||||
EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedDevicesView.swift; sourceTree = "<group>"; };
|
||||
EB1325D22E701D8900FD749C /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
|
||||
EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveNetworkInterface.swift; sourceTree = "<group>"; };
|
||||
EB1325E82E7025D900FD749C /* ApIsolation6Tester.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApIsolation6Tester.swift; sourceTree = "<group>"; };
|
||||
EB1325E92E7025D900FD749C /* ApIsolationTester.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApIsolationTester.swift; sourceTree = "<group>"; };
|
||||
EB1325F72E70287C00FD749C /* SendToExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendToExtension.swift; sourceTree = "<group>"; };
|
||||
EB1325FC2E7034B000FD749C /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
|
||||
EB16B6112E58B4B9007D9792 /* IdentityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityManager.swift; sourceTree = "<group>"; };
|
||||
EB16B6162E58B4C8007D9792 /* TrustStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustStore.swift; sourceTree = "<group>"; };
|
||||
EB16B7762E59BA9F007D9792 /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLogin.swift; sourceTree = "<group>"; };
|
||||
EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
EB704A812E4A7BAE008E608F /* DistributionDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DistributionDetector.swift; sourceTree = "<group>"; };
|
||||
EB721F232E75AFA900F2099F /* QuickDropIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = QuickDropIcon.icon; sourceTree = "<group>"; };
|
||||
EB7654772E525ACC006B4AA3 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
|
||||
EB7654C22E534E89006B4AA3 /* InfoPlist.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = InfoPlist.strings; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
EB7654C82E534F2C006B4AA3 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
EB7654C92E534F2D006B4AA3 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
EB7654CA2E534F31006B4AA3 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
EB7654CB2E534F32006B4AA3 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
EB7654CC2E534F33006B4AA3 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
EB7654CD2E534F33006B4AA3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
EB7654CE2E534F34006B4AA3 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
EB7654CF2E534F35006B4AA3 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
EB7654D02E534F35006B4AA3 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
EB7654D12E534F41006B4AA3 /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
EB7654D22E534F42006B4AA3 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
EB7654D32E534F43006B4AA3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
EB7654D42E534F43006B4AA3 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
EB7654D52E534F44006B4AA3 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
EB7654D62E534F45006B4AA3 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; 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>"; };
|
||||
EB7F54132D78E9E400989564 /* LicenseWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicenseWindow.swift; sourceTree = "<group>"; };
|
||||
EB869DB42E92AA33009AAAD4 /* QuickDropToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickDropToastView.swift; sourceTree = "<group>"; };
|
||||
EB886E512E4E19390052B7D9 /* HKDF.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKDF.swift; sourceTree = "<group>"; };
|
||||
EB886E662E4F2F7C0052B7D9 /* License.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = License.swift; sourceTree = "<group>"; };
|
||||
EB950B2D2E52027F002D68FA /* iOSShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = iOSShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -364,9 +372,6 @@
|
||||
EBC05C6D2E33B57D00EECC18 /* Converter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Converter.swift; sourceTree = "<group>"; };
|
||||
EBC05C6E2E33B57D00EECC18 /* Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helper.swift; sourceTree = "<group>"; };
|
||||
EBC05C6F2E33B57D00EECC18 /* PlusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlusView.swift; sourceTree = "<group>"; };
|
||||
EBC05C742E33B58100EECC18 /* ActiveNetworkInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveNetworkInterface.swift; sourceTree = "<group>"; };
|
||||
EBC05C752E33B58100EECC18 /* ApIsolation6Tester.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApIsolation6Tester.swift; sourceTree = "<group>"; };
|
||||
EBC05C762E33B58100EECC18 /* ApIsolationTester.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApIsolationTester.swift; sourceTree = "<group>"; };
|
||||
EBC05C7D2E33B58300EECC18 /* ApIsolationIssueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApIsolationIssueView.swift; sourceTree = "<group>"; };
|
||||
EBC05C7E2E33B58300EECC18 /* FirewallIssueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirewallIssueView.swift; sourceTree = "<group>"; };
|
||||
EBC05C7F2E33B58300EECC18 /* IssueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueView.swift; sourceTree = "<group>"; };
|
||||
@@ -402,8 +407,8 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
EB97A0132E51D6B2008B8C5C /* RNDeviceName in Frameworks */,
|
||||
EB5C37F02E377C3F0052EFD7 /* SwiftECC in Frameworks */,
|
||||
EBADEB8F2E8029750021B5E4 /* DeviceKit in Frameworks */,
|
||||
EB886E582E4E66420052B7D9 /* LUI in Frameworks */,
|
||||
EB5C37EE2E377C3F0052EFD7 /* SwiftProtobuf in Frameworks */,
|
||||
EB9925892E45F13500357055 /* QRCode in Frameworks */,
|
||||
@@ -414,8 +419,8 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
EB950B672E520680002D68FA /* RNDeviceName in Frameworks */,
|
||||
EB950B612E520674002D68FA /* SwiftECC in Frameworks */,
|
||||
EBADEB912E802A830021B5E4 /* DeviceKit in Frameworks */,
|
||||
EB950B652E52067D002D68FA /* LUI in Frameworks */,
|
||||
EB950B5F2E520671002D68FA /* SwiftProtobuf in Frameworks */,
|
||||
EB950B632E52067A002D68FA /* QRCode in Frameworks */,
|
||||
@@ -438,7 +443,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
699DEBA52AB0573200115D22 /* ShareViewController.swift */,
|
||||
EB950B732E520954002D68FA /* Attachments.swift */,
|
||||
EB644B872EA50827006C2E6C /* Helpers */,
|
||||
EB950B762E521712002D68FA /* AttachmentDetails.swift */,
|
||||
EB5C371E2E36693D0052EFD7 /* View */,
|
||||
699DEBAA2AB0573200115D22 /* Info.plist */,
|
||||
@@ -450,6 +455,7 @@
|
||||
69DA9A0529E0BF5100A442DA = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EB721F232E75AFA900F2099F /* QuickDropIcon.icon */,
|
||||
69DA9A1029E0BF5100A442DA /* QuickDrop */,
|
||||
699DEBA22AB0573200115D22 /* ShareExtension */,
|
||||
69DCF4822AB70D0600CBE2CC /* NearbyShare */,
|
||||
@@ -476,8 +482,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
69DA9A1129E0BF5100A442DA /* AppDelegate.swift */,
|
||||
EB1325D22E701D8900FD749C /* Settings.swift */,
|
||||
EB644B852EA507CF006C2E6C /* ViewModel */,
|
||||
EBC05C6A2E33B55100EECC18 /* View */,
|
||||
EBC05C772E33B58100EECC18 /* SelfTest */,
|
||||
EBC05C6B2E33B57100EECC18 /* Util */,
|
||||
EBC05C702E33B57D00EECC18 /* Plus */,
|
||||
69DA9A1329E0BF5200A442DA /* Assets.xcassets */,
|
||||
@@ -511,6 +518,16 @@
|
||||
path = NearbyShare;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EB1325EA2E7025D900FD749C /* SelfTest */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */,
|
||||
EB1325E82E7025D900FD749C /* ApIsolation6Tester.swift */,
|
||||
EB1325E92E7025D900FD749C /* ApIsolationTester.swift */,
|
||||
);
|
||||
path = SelfTest;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EB5C371C2E3669010052EFD7 /* Util */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -521,8 +538,9 @@
|
||||
EBCC27F82D80C9F70079C478 /* Zip.swift */,
|
||||
EB16B7762E59BA9F007D9792 /* AppGroup.swift */,
|
||||
EB886E512E4E19390052B7D9 /* HKDF.swift */,
|
||||
EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */,
|
||||
EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */,
|
||||
EB7CD1442D6FB0C100CB0CB5 /* Logger.swift */,
|
||||
EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */,
|
||||
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */,
|
||||
);
|
||||
path = Util;
|
||||
@@ -559,6 +577,34 @@
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EB644B852EA507CF006C2E6C /* ViewModel */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EB979F902E50CA23008B8C5C /* SendModel.swift */,
|
||||
EB97A00E2E511120008B8C5C /* ReceiveModel.swift */,
|
||||
);
|
||||
path = ViewModel;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EB644B862EA50807006C2E6C /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EB979F8E2E50CA23008B8C5C /* ContentView.swift */,
|
||||
EB979F8F2E50CA23008B8C5C /* FilePicker.swift */,
|
||||
EB97A0192E51EA9C008B8C5C /* UIAlert.swift */,
|
||||
EB979F992E50CAA6008B8C5C /* PieProgressView.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EB644B872EA50827006C2E6C /* Helpers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EB950B732E520954002D68FA /* Attachments.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EB950B6C2E5206E2002D68FA /* iOSShareExtension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -573,14 +619,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EB97A0162E51DF4F008B8C5C /* Info.plist */,
|
||||
EB7654C32E534E89006B4AA3 /* InfoPlist.strings */,
|
||||
EB1325FC2E7034B000FD749C /* InfoPlist.xcstrings */,
|
||||
EB979F8D2E50CA23008B8C5C /* Configuration.swift */,
|
||||
EB979F8E2E50CA23008B8C5C /* ContentView.swift */,
|
||||
EB979F8F2E50CA23008B8C5C /* FilePicker.swift */,
|
||||
EB979F902E50CA23008B8C5C /* SendModel.swift */,
|
||||
EB97A00E2E511120008B8C5C /* ReceiveModel.swift */,
|
||||
EB97A0192E51EA9C008B8C5C /* UIAlert.swift */,
|
||||
EB979F992E50CAA6008B8C5C /* PieProgressView.swift */,
|
||||
EB644B862EA50807006C2E6C /* View */,
|
||||
EB979F912E50CA23008B8C5C /* QuickDropiOS.entitlements */,
|
||||
EB979F922E50CA23008B8C5C /* QuickDropiOSApp.swift */,
|
||||
);
|
||||
@@ -594,6 +635,7 @@
|
||||
EBD2A2162D2860BE00BCB8E2 /* WelcomeScreen.swift */,
|
||||
EB7F54112D78E95C00989564 /* TutorialView.swift */,
|
||||
EB7F54132D78E9E400989564 /* LicenseWindow.swift */,
|
||||
EB869DB42E92AA33009AAAD4 /* QuickDropToastView.swift */,
|
||||
EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */,
|
||||
EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */,
|
||||
EBC05C812E33B58300EECC18 /* IssueViews */,
|
||||
@@ -604,10 +646,12 @@
|
||||
EBC05C6B2E33B57100EECC18 /* Util */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EB1325F72E70287C00FD749C /* SendToExtension.swift */,
|
||||
EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */,
|
||||
EB886E662E4F2F7C0052B7D9 /* License.swift */,
|
||||
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */,
|
||||
EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */,
|
||||
EB1325EA2E7025D900FD749C /* SelfTest */,
|
||||
);
|
||||
path = Util;
|
||||
sourceTree = "<group>";
|
||||
@@ -623,16 +667,6 @@
|
||||
path = Plus;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EBC05C772E33B58100EECC18 /* SelfTest */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EBC05C742E33B58100EECC18 /* ActiveNetworkInterface.swift */,
|
||||
EBC05C752E33B58100EECC18 /* ApIsolation6Tester.swift */,
|
||||
EBC05C762E33B58100EECC18 /* ApIsolationTester.swift */,
|
||||
);
|
||||
path = SelfTest;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EBC05C812E33B58300EECC18 /* IssueViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -722,7 +756,7 @@
|
||||
EB5C37EF2E377C3F0052EFD7 /* SwiftECC */,
|
||||
EB9925882E45F13500357055 /* QRCode */,
|
||||
EB886E572E4E66420052B7D9 /* LUI */,
|
||||
EB97A0122E51D6B2008B8C5C /* RNDeviceName */,
|
||||
EBADEB8E2E8029750021B5E4 /* DeviceKit */,
|
||||
);
|
||||
productName = QuickDropiOS;
|
||||
productReference = EB5C37E22E377C2E0052EFD7 /* QuickDropiOS.app */;
|
||||
@@ -746,7 +780,7 @@
|
||||
EB950B602E520674002D68FA /* SwiftECC */,
|
||||
EB950B622E52067A002D68FA /* QRCode */,
|
||||
EB950B642E52067D002D68FA /* LUI */,
|
||||
EB950B662E520680002D68FA /* RNDeviceName */,
|
||||
EBADEB902E802A830021B5E4 /* DeviceKit */,
|
||||
);
|
||||
productName = iOSShareExtension;
|
||||
productReference = EB950B2D2E52027F002D68FA /* iOSShareExtension.appex */;
|
||||
@@ -760,7 +794,7 @@
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1640;
|
||||
LastUpgradeCheck = 1640;
|
||||
LastUpgradeCheck = 2600;
|
||||
TargetAttributes = {
|
||||
699DEBA02AB0573200115D22 = {
|
||||
CreatedOnToolsVersion = 14.3.1;
|
||||
@@ -808,7 +842,7 @@
|
||||
EB2F9BFC2DA05F48004010A2 /* XCRemoteSwiftPackageReference "BezelNotification" */,
|
||||
EB9925832E45EF0900357055 /* XCRemoteSwiftPackageReference "qrcode" */,
|
||||
EB886E562E4E66420052B7D9 /* XCRemoteSwiftPackageReference "LUI" */,
|
||||
EB97A0112E51D6B2008B8C5C /* XCRemoteSwiftPackageReference "RNDeviceName" */,
|
||||
EBADEB8B2E8029420021B5E4 /* XCRemoteSwiftPackageReference "DeviceKit" */,
|
||||
);
|
||||
productRefGroup = 69DA9A0F29E0BF5100A442DA /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -845,6 +879,7 @@
|
||||
EB5C36E42E34EC0E0052EFD7 /* License.txt in Resources */,
|
||||
EB5C36932E34EBBD0052EFD7 /* Localizable.xcstrings in Resources */,
|
||||
EBC05C8C2E33B59E00EECC18 /* timeIsNow.mp3 in Resources */,
|
||||
EB721F252E75AFA900F2099F /* QuickDropIcon.icon in Resources */,
|
||||
EB5C38392E38C1F60052EFD7 /* justMaybe.mp3 in Resources */,
|
||||
EB7654782E525ACC006B4AA3 /* MainMenu.xib in Resources */,
|
||||
);
|
||||
@@ -855,9 +890,10 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
EB5C383B2E38C1F60052EFD7 /* justMaybe.mp3 in Resources */,
|
||||
EB7654C42E534E89006B4AA3 /* InfoPlist.strings in Resources */,
|
||||
EB1325FD2E7034B000FD749C /* InfoPlist.xcstrings in Resources */,
|
||||
EB97A0142E51DD00008B8C5C /* License.txt in Resources */,
|
||||
EB97A0152E51DD00008B8C5C /* timeIsNow.mp3 in Resources */,
|
||||
EB721F242E75AFA900F2099F /* QuickDropIcon.icon in Resources */,
|
||||
EB886E552E4E50F40052B7D9 /* Localizable.xcstrings in Resources */,
|
||||
EB7654C02E534A8E006B4AA3 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
@@ -868,7 +904,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
EB950B5B2E52065A002D68FA /* timeIsNow.mp3 in Resources */,
|
||||
EB7654C62E534E89006B4AA3 /* InfoPlist.strings in Resources */,
|
||||
EB1325FE2E7034B000FD749C /* InfoPlist.xcstrings in Resources */,
|
||||
EB950B702E520734002D68FA /* Localizable.xcstrings in Resources */,
|
||||
EB950B5C2E52065A002D68FA /* justMaybe.mp3 in Resources */,
|
||||
EB950B5D2E52065A002D68FA /* License.txt in Resources */,
|
||||
@@ -885,6 +921,7 @@
|
||||
files = (
|
||||
EB704A832E4B10D4008E608F /* DistributionDetector.swift in Sources */,
|
||||
EB96ED902D7F8114004B3FF1 /* Localized.swift in Sources */,
|
||||
EB1325F82E70288300FD749C /* SendToExtension.swift in Sources */,
|
||||
EB16B6172E58B4C8007D9792 /* TrustStore.swift in Sources */,
|
||||
EB886E542E4E19390052B7D9 /* HKDF.swift in Sources */,
|
||||
EBC05C862E33B58300EECC18 /* NetworkFilterIssueView.swift in Sources */,
|
||||
@@ -900,14 +937,14 @@
|
||||
EB5C37C72E377A780052EFD7 /* wire_format.pb.swift in Sources */,
|
||||
EB5C37C82E377A780052EFD7 /* ukey.pb.swift in Sources */,
|
||||
EB5C37C92E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */,
|
||||
EB4F9D222EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
|
||||
EB950B742E520957002D68FA /* Attachments.swift in Sources */,
|
||||
EB1325D32E701D8E00FD749C /* Settings.swift in Sources */,
|
||||
EB16B7782E59BAA1007D9792 /* AppGroup.swift in Sources */,
|
||||
EB5C383C2E38C2D90052EFD7 /* AudioManager.swift in Sources */,
|
||||
EB5C37CA2E377A780052EFD7 /* offline_wire_formats.pb.swift in Sources */,
|
||||
EB16B6122E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
||||
EBC05C5C2E3394EF00EECC18 /* ErrorAlertHandler.swift in Sources */,
|
||||
EBC05C7B2E33B58100EECC18 /* ActiveNetworkInterface.swift in Sources */,
|
||||
EBC05C7C2E33B58100EECC18 /* ApIsolationTester.swift in Sources */,
|
||||
691F53BB2ABB70840089FD92 /* DeviceListCell.swift in Sources */,
|
||||
EB5C383F2E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */,
|
||||
EB5C37BC2E377A700052EFD7 /* SleepManager.swift in Sources */,
|
||||
@@ -918,7 +955,9 @@
|
||||
EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */,
|
||||
EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
||||
EB5C37BD2E377A700052EFD7 /* Helpers.swift in Sources */,
|
||||
EB5C37BE2E377A700052EFD7 /* SaveFilesManager.swift in Sources */,
|
||||
EB1325EB2E7025D900FD749C /* ApIsolationTester.swift in Sources */,
|
||||
EB1325EC2E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */,
|
||||
EB1325ED2E7025D900FD749C /* ApIsolation6Tester.swift in Sources */,
|
||||
EB9925822E45D53E00357055 /* sharing_enums.pb.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -931,11 +970,12 @@
|
||||
EB3643602D73A2A2009CDFF8 /* AudioManager.swift in Sources */,
|
||||
EB5C38412E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */,
|
||||
EB5C37B82E377A6F0052EFD7 /* SleepManager.swift in Sources */,
|
||||
EB5C37B92E377A6F0052EFD7 /* SaveFilesManager.swift in Sources */,
|
||||
EB869DB52E92AA3A009AAAD4 /* QuickDropToastView.swift in Sources */,
|
||||
EB5C37BA2E377A6F0052EFD7 /* Helpers.swift in Sources */,
|
||||
EB05DFBE2E59FC8900EF567A /* TrustedDevicesView.swift in Sources */,
|
||||
EB16B6182E58B4C8007D9792 /* TrustStore.swift in Sources */,
|
||||
EBC05C5B2E33937200EECC18 /* ErrorAlertHandler.swift in Sources */,
|
||||
EB1325D42E701D8E00FD749C /* Settings.swift in Sources */,
|
||||
EBC05C712E33B57D00EECC18 /* Converter.swift in Sources */,
|
||||
EB886E5D2E4F13EB0052B7D9 /* Zip.swift in Sources */,
|
||||
EBC05C722E33B57D00EECC18 /* Helper.swift in Sources */,
|
||||
@@ -948,19 +988,17 @@
|
||||
EB16B6152E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
||||
EB5C37C42E377A780052EFD7 /* securegcm.pb.swift in Sources */,
|
||||
EB97A0102E51CD13008B8C5C /* ReceiveModel.swift in Sources */,
|
||||
EBC05C782E33B58100EECC18 /* ActiveNetworkInterface.swift in Sources */,
|
||||
EB5C37CB2E377A7F0052EFD7 /* NearbyConnection.swift in Sources */,
|
||||
EB5C37CC2E377A7F0052EFD7 /* DataModel.swift in Sources */,
|
||||
EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */,
|
||||
EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */,
|
||||
EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
||||
EBC05C792E33B58100EECC18 /* ApIsolation6Tester.swift in Sources */,
|
||||
EB7654A12E53252D006B4AA3 /* AttachmentDetails.swift in Sources */,
|
||||
EBC05C7A2E33B58100EECC18 /* ApIsolationTester.swift in Sources */,
|
||||
EB886E522E4E19390052B7D9 /* HKDF.swift in Sources */,
|
||||
EB96ED8C2D7F7E7B004B3FF1 /* SmallSheetView.swift in Sources */,
|
||||
EB886E682E4F2F800052B7D9 /* License.swift in Sources */,
|
||||
EB7F54142D78E9E700989564 /* LicenseWindow.swift in Sources */,
|
||||
EB4F9D202EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
|
||||
69DA9A1229E0BF5100A442DA /* AppDelegate.swift in Sources */,
|
||||
EB6F62D72D91F4AF00534EC2 /* SettingsView.swift in Sources */,
|
||||
EB7F54122D78E96000989564 /* TutorialView.swift in Sources */,
|
||||
@@ -971,6 +1009,10 @@
|
||||
EB16B77A2E59BAA1007D9792 /* AppGroup.swift in Sources */,
|
||||
EBC05C822E33B58300EECC18 /* ApIsolationIssueView.swift in Sources */,
|
||||
EBC05C832E33B58300EECC18 /* FirewallIssueView.swift in Sources */,
|
||||
EB1325FA2E70288300FD749C /* SendToExtension.swift in Sources */,
|
||||
EB1325F42E7025D900FD749C /* ApIsolationTester.swift in Sources */,
|
||||
EB1325F52E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */,
|
||||
EB1325F62E7025D900FD749C /* ApIsolation6Tester.swift in Sources */,
|
||||
EBC05C842E33B58300EECC18 /* IssueView.swift in Sources */,
|
||||
EBC05C852E33B58300EECC18 /* NetworkFilterIssueView.swift in Sources */,
|
||||
);
|
||||
@@ -980,10 +1022,11 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
EB4F9D272EC27CFB00A74A0F /* FileManager+DocumentsOpener.swift in Sources */,
|
||||
EB821C992E76FE5300036963 /* SmallSheetView.swift in Sources */,
|
||||
EB05DFBF2E59FD0A00EF567A /* TrustedDevicesView.swift in Sources */,
|
||||
EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */,
|
||||
EB5C37F22E377CE70052EFD7 /* NearbyConnection.swift in Sources */,
|
||||
EB5C37F32E377CE70052EFD7 /* SaveFilesManager.swift in Sources */,
|
||||
EB16B6142E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
||||
EB886E672E4F2F800052B7D9 /* License.swift in Sources */,
|
||||
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
||||
@@ -998,9 +1041,11 @@
|
||||
EB5C37FB2E377CE70052EFD7 /* InboundNearbyConnection.swift in Sources */,
|
||||
EB9925802E45D53E00357055 /* sharing_enums.pb.swift in Sources */,
|
||||
EB5C37FC2E377CE70052EFD7 /* OutboundNearbyConnection.swift in Sources */,
|
||||
EB1325D62E701D8E00FD749C /* Settings.swift in Sources */,
|
||||
EB5C37FD2E377CE70052EFD7 /* Logger.swift in Sources */,
|
||||
EB97A00F2E511124008B8C5C /* ReceiveModel.swift in Sources */,
|
||||
EB5C37FE2E377CE70052EFD7 /* offline_wire_formats.pb.swift in Sources */,
|
||||
EB4F9D1F2EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
|
||||
EB886E6A2E4F37B90052B7D9 /* ErrorAlertHandler.swift in Sources */,
|
||||
EB886E5E2E4F13EB0052B7D9 /* Zip.swift in Sources */,
|
||||
EB5C383D2E38C2D90052EFD7 /* AudioManager.swift in Sources */,
|
||||
@@ -1034,6 +1079,7 @@
|
||||
EB950B4B2E520436002D68FA /* offline_wire_formats.pb.swift in Sources */,
|
||||
EB950B4C2E520436002D68FA /* InboundNearbyConnection.swift in Sources */,
|
||||
EB16B6132E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
||||
EB4F9D212EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
|
||||
EB950B4D2E520436002D68FA /* NearbyConnection.swift in Sources */,
|
||||
EB950B4F2E520436002D68FA /* Logger.swift in Sources */,
|
||||
EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */,
|
||||
@@ -1043,13 +1089,15 @@
|
||||
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */,
|
||||
EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */,
|
||||
EB950B542E520436002D68FA /* HKDF.swift in Sources */,
|
||||
EB950B552E520436002D68FA /* SaveFilesManager.swift in Sources */,
|
||||
EB1325D52E701D8E00FD749C /* Settings.swift in Sources */,
|
||||
EB950B562E520436002D68FA /* DataModel.swift in Sources */,
|
||||
EB821C9A2E76FE5300036963 /* SmallSheetView.swift in Sources */,
|
||||
EB950B3E2E52038A002D68FA /* SendModel.swift in Sources */,
|
||||
EB950B772E521712002D68FA /* AttachmentDetails.swift in Sources */,
|
||||
EB16B6192E58B4C8007D9792 /* TrustStore.swift in Sources */,
|
||||
EB16B7772E59BAA1007D9792 /* AppGroup.swift in Sources */,
|
||||
EB950B3F2E52038A002D68FA /* FilePicker.swift in Sources */,
|
||||
EB4F9D252EC27CFB00A74A0F /* FileManager+DocumentsOpener.swift in Sources */,
|
||||
EB950B402E52038A002D68FA /* Configuration.swift in Sources */,
|
||||
EB950B412E52038A002D68FA /* PieProgressView.swift in Sources */,
|
||||
EB950B592E520649002D68FA /* AudioManager.swift in Sources */,
|
||||
@@ -1057,6 +1105,7 @@
|
||||
EB950B6F2E5206E2002D68FA /* ShareViewController.swift in Sources */,
|
||||
EB950B432E52038A002D68FA /* ContentView.swift in Sources */,
|
||||
EB950B442E520414002D68FA /* License.swift in Sources */,
|
||||
EB4F9D1D2EC113EA00A74A0F /* DistributionDetector.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -1085,29 +1134,6 @@
|
||||
name = ShareViewController.xib;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EB7654C32E534E89006B4AA3 /* InfoPlist.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
EB7654C22E534E89006B4AA3 /* InfoPlist.strings */,
|
||||
EB7654C82E534F2C006B4AA3 /* ar */,
|
||||
EB7654C92E534F2D006B4AA3 /* zh-Hans */,
|
||||
EB7654CA2E534F31006B4AA3 /* nl */,
|
||||
EB7654CB2E534F32006B4AA3 /* fr */,
|
||||
EB7654CC2E534F33006B4AA3 /* hu */,
|
||||
EB7654CD2E534F33006B4AA3 /* it */,
|
||||
EB7654CE2E534F34006B4AA3 /* ja */,
|
||||
EB7654CF2E534F35006B4AA3 /* ko */,
|
||||
EB7654D02E534F35006B4AA3 /* pt-BR */,
|
||||
EB7654D12E534F41006B4AA3 /* pt-PT */,
|
||||
EB7654D22E534F42006B4AA3 /* ro */,
|
||||
EB7654D32E534F43006B4AA3 /* ru */,
|
||||
EB7654D42E534F43006B4AA3 /* es */,
|
||||
EB7654D52E534F44006B4AA3 /* th */,
|
||||
EB7654D62E534F45006B4AA3 /* uk */,
|
||||
);
|
||||
name = InfoPlist.strings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
@@ -1121,7 +1147,11 @@
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = "${SHARED_BUILD}";
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = QuickDrop;
|
||||
@@ -1155,7 +1185,11 @@
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = "${SHARED_BUILD}";
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = QuickDrop;
|
||||
@@ -1241,8 +1275,9 @@
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
SHARED_BUILD = 68;
|
||||
SHARED_VERSION = 2.2.1;
|
||||
SHARED_BUILD = 84;
|
||||
SHARED_VERSION = 2.5.4;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -1303,8 +1338,9 @@
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = macosx;
|
||||
SHARED_BUILD = 68;
|
||||
SHARED_VERSION = 2.2.1;
|
||||
SHARED_BUILD = 84;
|
||||
SHARED_VERSION = 2.5.4;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
@@ -1314,7 +1350,7 @@
|
||||
69DA9A1C29E0BF5200A442DA /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = QuickDropIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QuickDrop/QuickDrop.entitlements;
|
||||
@@ -1324,7 +1360,12 @@
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = "${SHARED_BUILD}";
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readwrite;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = QuickDrop/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = QuickDrop;
|
||||
@@ -1355,7 +1396,7 @@
|
||||
69DA9A1D29E0BF5200A442DA /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = QuickDropIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = QuickDrop/QuickDrop.entitlements;
|
||||
@@ -1365,7 +1406,12 @@
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = "${SHARED_BUILD}";
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readwrite;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = QuickDrop/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = QuickDrop;
|
||||
@@ -1395,7 +1441,7 @@
|
||||
EB5C37EB2E377C310052EFD7 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = QuickDropIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = QuickDropiOS/QuickDropiOS.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -1408,6 +1454,7 @@
|
||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Leon Böttger";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
@@ -1438,7 +1485,7 @@
|
||||
EB5C37EC2E377C310052EFD7 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = QuickDropIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = QuickDropiOS/QuickDropiOS.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -1451,6 +1498,7 @@
|
||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Leon Böttger";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
@@ -1622,14 +1670,6 @@
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
EB97A0112E51D6B2008B8C5C /* XCRemoteSwiftPackageReference "RNDeviceName" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/souzainf3/RNDeviceName";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 2.1.0;
|
||||
};
|
||||
};
|
||||
EB9925832E45EF0900357055 /* XCRemoteSwiftPackageReference "qrcode" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/dagronf/qrcode.git";
|
||||
@@ -1638,6 +1678,14 @@
|
||||
minimumVersion = 26.10.0;
|
||||
};
|
||||
};
|
||||
EBADEB8B2E8029420021B5E4 /* XCRemoteSwiftPackageReference "DeviceKit" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/devicekit/DeviceKit";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 5.7.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@@ -1701,16 +1749,6 @@
|
||||
package = EB886E562E4E66420052B7D9 /* XCRemoteSwiftPackageReference "LUI" */;
|
||||
productName = LUI;
|
||||
};
|
||||
EB950B662E520680002D68FA /* RNDeviceName */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = EB97A0112E51D6B2008B8C5C /* XCRemoteSwiftPackageReference "RNDeviceName" */;
|
||||
productName = RNDeviceName;
|
||||
};
|
||||
EB97A0122E51D6B2008B8C5C /* RNDeviceName */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = EB97A0112E51D6B2008B8C5C /* XCRemoteSwiftPackageReference "RNDeviceName" */;
|
||||
productName = RNDeviceName;
|
||||
};
|
||||
EB9925842E45EF0900357055 /* QRCode */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = EB9925832E45EF0900357055 /* XCRemoteSwiftPackageReference "qrcode" */;
|
||||
@@ -1726,6 +1764,16 @@
|
||||
package = EB9925832E45EF0900357055 /* XCRemoteSwiftPackageReference "qrcode" */;
|
||||
productName = QRCode;
|
||||
};
|
||||
EBADEB8E2E8029750021B5E4 /* DeviceKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = EBADEB8B2E8029420021B5E4 /* XCRemoteSwiftPackageReference "DeviceKit" */;
|
||||
productName = DeviceKit;
|
||||
};
|
||||
EBADEB902E802A830021B5E4 /* DeviceKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = EBADEB8B2E8029420021B5E4 /* XCRemoteSwiftPackageReference "DeviceKit" */;
|
||||
productName = DeviceKit;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 69DA9A0629E0BF5100A442DA /* Project object */;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "212cf044c2b0bab89578ca93a3666a656e0c44fb54a69f813c4cc4fae07827d7",
|
||||
"originHash" : "270e8bfd245fae589b12faf08b86042cbbe7c809d59c5b0b8b735c7fb3a8d4bd",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "applewatchscreensize",
|
||||
@@ -33,8 +33,17 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/leif-ibsen/BigInt",
|
||||
"state" : {
|
||||
"revision" : "afb70a0038bfbba845271b60fa9a58d5840f8017",
|
||||
"version" : "1.21.0"
|
||||
"revision" : "8c6f93aa37504b7b1ba3954335b5548a19fbbd82",
|
||||
"version" : "1.22.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "devicekit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/devicekit/DeviceKit",
|
||||
"state" : {
|
||||
"revision" : "581df61650bc457ec00373a592a84be3e7468eb1",
|
||||
"version" : "5.7.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -43,7 +52,7 @@
|
||||
"location" : "https://github.com/leonboe1/LUI.git",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "5b2fe7161069c2314c5be17810ec7471b9decc8a"
|
||||
"revision" : "0b862decf853e1f542eeab1f8d548f8ff629deca"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -55,15 +64,6 @@
|
||||
"version" : "26.10.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "rndevicename",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/souzainf3/RNDeviceName",
|
||||
"state" : {
|
||||
"revision" : "68ada02a7135381425b01778aeb8719ce3bf8f62",
|
||||
"version" : "2.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-cross-kit-types",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -87,8 +87,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-protobuf.git",
|
||||
"state" : {
|
||||
"revision" : "e3f69fd321d0c9fcdc16fb576a0cdd956675face",
|
||||
"version" : "1.31.0"
|
||||
"revision" : "c169a5744230951031770e27e475ff6eefe51f9d",
|
||||
"version" : "1.33.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -124,7 +124,7 @@
|
||||
"location" : "https://github.com/siteline/swiftui-introspect",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "9bedf1e79c7b7c34b35b74bef126f56d2400ef7f"
|
||||
"revision" : "11d9782bea80656e8a65a643d6ac6396e4a8662e"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1640"
|
||||
LastUpgradeVersion = "2600"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1640"
|
||||
LastUpgradeVersion = "2600"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1640"
|
||||
LastUpgradeVersion = "2600"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1640"
|
||||
LastUpgradeVersion = "2600"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
|
||||
@@ -38,26 +38,28 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
let menu = NSMenu()
|
||||
|
||||
let visibleItem = NSMenuItem(
|
||||
title: "VisibleToEveryone".localized(),
|
||||
title: getDefaultVisibleLabel(),
|
||||
action: nil,
|
||||
keyEquivalent: ""
|
||||
)
|
||||
self.visibleItem = visibleItem
|
||||
menu.addItem(visibleItem)
|
||||
|
||||
menu.addItem(withTitle: String(format: "DeviceName".localized(), arguments: [Host.current().localizedName ?? "Mac"]), action: nil, keyEquivalent: "")
|
||||
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
let sendClipboardItem = NSMenuItem(title: "SendClipboard".localized(), action: #selector(sendClipboard), keyEquivalent: "")
|
||||
sendClipboardItem.image = NSImage(systemSymbolName: "doc.on.clipboard", accessibilityDescription: nil)
|
||||
menu.addItem(sendClipboardItem)
|
||||
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
let userManualItem = NSMenuItem(title: "UserManual".localized(), action: #selector(openWelcomeScreen), keyEquivalent: "")
|
||||
userManualItem.image = NSImage(systemSymbolName: "gear", accessibilityDescription: nil)
|
||||
menu.addItem(userManualItem)
|
||||
|
||||
menu.addItem(withTitle: "Quit".localized(), action: #selector(NSApplication.terminate(_:)), keyEquivalent: "")
|
||||
let quitItem = NSMenuItem(title: "Quit".localized(), action: #selector(NSApplication.terminate(_:)), keyEquivalent: "")
|
||||
quitItem.image = NSImage(systemSymbolName: "xmark", accessibilityDescription: nil)
|
||||
menu.addItem(quitItem)
|
||||
|
||||
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||
statusItem?.button?.image = NSImage(named: "MenuBarIcon")
|
||||
@@ -67,26 +69,31 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
iapManager = IAPManager.sharedInstance
|
||||
iapManager?.startObserving()
|
||||
|
||||
log("[AppDelegate] Downloaded directly: \(DistributionDetector.isDirectDistributionEnabled)")
|
||||
if DistributionDetector.isDirectDistributionEnabled {
|
||||
log("[AppDelegate] Downloaded from GitHub.")
|
||||
}
|
||||
else {
|
||||
log("[AppDelegate] Downloaded from App Store.")
|
||||
}
|
||||
|
||||
// app did not lauch before
|
||||
if !UserDefaults.standard.bool(forKey: UserDefaultsKeys.appLaunchedBefore.rawValue) {
|
||||
if !Settings.shared.appLaunchedBefore {
|
||||
log("[AppDelegate] Opening Welcome Screen")
|
||||
// open welcome screen
|
||||
openWelcomeScreen()
|
||||
UserDefaults.standard.set(true, forKey: UserDefaultsKeys.appLaunchedBefore.rawValue)
|
||||
Settings.shared.appLaunchedBefore = true
|
||||
|
||||
// user installed the app after the IAP was implemented, set the user as eligible for IAP
|
||||
UserDefaults.standard.set(true, forKey: UserDefaultsKeys.isEligibleForIap.rawValue)
|
||||
Settings.shared.isEligibleForIap = true
|
||||
} else {
|
||||
// app launched before
|
||||
|
||||
// user installed the app before the IAP was implemented, grant the plus version
|
||||
if !UserDefaults.standard.bool(forKey: UserDefaultsKeys.isEligibleForIap.rawValue) {
|
||||
if !Settings.shared.isEligibleForIap {
|
||||
log("[AppDelegate] Granting QuickDrop+ for old user")
|
||||
UserDefaults.standard.set(true, forKey: UserDefaultsKeys.plusVersion.rawValue)
|
||||
Settings.shared.gotPlus = true
|
||||
} else {
|
||||
if !isPlusVersion() {
|
||||
if !fullVersion() {
|
||||
log("[AppDelegate] New user - QuickDrop+ not available")
|
||||
}
|
||||
else {
|
||||
@@ -116,7 +123,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
NearbyConnectionManager.shared.connectionUpdateCallback = { isConnected in
|
||||
if isConnected {
|
||||
self.statusItem?.button?.image = NSImage(named: "MenuBarIcon")
|
||||
self.visibleItem?.title = "VisibleToEveryone".localized()
|
||||
self.visibleItem?.title = self.getDefaultVisibleLabel()
|
||||
}
|
||||
else {
|
||||
self.statusItem?.button?.image = NSImage(named: "MenuBarIconSlash")
|
||||
@@ -124,7 +131,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
}
|
||||
}
|
||||
|
||||
log("Application did finish launching")
|
||||
log("[AppDelegate] Application did finish launching")
|
||||
}
|
||||
|
||||
|
||||
@@ -191,8 +198,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
}
|
||||
}
|
||||
else {
|
||||
let sharingService = NSSharingService(named: NSSharingService.Name("com.leonboettger.neardrop.ShareExtension"))
|
||||
sharingService?.perform(withItems: [clipboardString])
|
||||
sendToSharingService(items: [clipboardString])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,6 +344,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func getDefaultVisibleLabel() -> String {
|
||||
let name = NearbyConnectionManager.shared.deviceInfo.name ?? "Unknown".localized()
|
||||
return String(format: "VisibleAs".localized(), name)
|
||||
}
|
||||
}
|
||||
|
||||
struct TransferInfo {
|
||||
|
||||
|
After Width: | Height: | Size: 585 KiB |
|
Before Width: | Height: | Size: 1016 KiB |
|
Before Width: | Height: | Size: 752 KiB After Width: | Height: | Size: 672 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "1024-1024 1.png",
|
||||
"filename" : "1024-1024 1 2.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 752 KiB After Width: | Height: | Size: 672 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "FileTransfer Kopie-1024.png",
|
||||
"filename" : "FileTransfer Kopie-512.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 647 KiB |
|
After Width: | Height: | Size: 111 KiB |
@@ -15,5 +15,7 @@
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UIDesignRequiresCompatibility</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -13,7 +13,7 @@ let plusViewHeight: CGFloat = 200
|
||||
|
||||
struct PlusView: View {
|
||||
|
||||
@AppStorage(UserDefaultsKeys.plusVersion.rawValue) var isPlusVersion = false
|
||||
@ObservedObject private var settings = Settings.shared
|
||||
@ObservedObject private var iaphelper = IAPManager.sharedInstance
|
||||
let closeView: () -> Void
|
||||
|
||||
@@ -27,15 +27,7 @@ struct PlusView: View {
|
||||
@State var warning = false
|
||||
@State var buying = false
|
||||
@State var errorText = ""
|
||||
|
||||
@State var boughtSuccessAlert = false {
|
||||
didSet {
|
||||
if boughtSuccessAlert {
|
||||
log("User bought plus version. Moving pending files...")
|
||||
SaveFilesManager.shared.movePendingFilesToTarget()
|
||||
}
|
||||
}
|
||||
}
|
||||
@State var boughtSuccessAlert = false
|
||||
|
||||
|
||||
var body: some View {
|
||||
@@ -115,7 +107,7 @@ struct PlusView: View {
|
||||
if success {
|
||||
log("[LUI] Restored purchases successfully")
|
||||
withAnimation {
|
||||
isPlusVersion = true
|
||||
settings.gotPlus = true
|
||||
}
|
||||
boughtSuccessAlert = true
|
||||
} else {
|
||||
@@ -146,7 +138,7 @@ struct PlusView: View {
|
||||
|
||||
if success {
|
||||
withAnimation {
|
||||
isPlusVersion = true
|
||||
settings.gotPlus = true
|
||||
}
|
||||
|
||||
boughtSuccessAlert = true
|
||||
@@ -193,7 +185,7 @@ struct PlusView: View {
|
||||
case .success(_):
|
||||
|
||||
withAnimation {
|
||||
isPlusVersion = true
|
||||
settings.gotPlus = true
|
||||
boughtSuccessAlert = true
|
||||
warning = false
|
||||
}
|
||||
|
||||
@@ -2,19 +2,9 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.leonboettger.neardrop</string>
|
||||
</array>
|
||||
<key>com.apple.security.files.downloads.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// Settings.swift
|
||||
// QuickDrop
|
||||
//
|
||||
// Created by Leon Böttger on 09.09.25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
class Settings: ObservableObject {
|
||||
|
||||
private init() {}
|
||||
|
||||
static var shared = Settings()
|
||||
|
||||
|
||||
// MARK: - Published UserDefaults Backed Properties
|
||||
|
||||
@Published var gotPlus = UserDefaults.standard.bool(forKey: UserDefaultsKeys.plusVersion.rawValue)
|
||||
{ didSet { UserDefaults.standard.set(gotPlus, forKey: UserDefaultsKeys.plusVersion.rawValue) }}
|
||||
|
||||
|
||||
@Published var automaticallyAcceptFiles: Bool = UserDefaults.standard.bool(forKey: UserDefaultsKeys.automaticallyAcceptFiles.rawValue)
|
||||
{ didSet { UserDefaults.standard.set(automaticallyAcceptFiles, forKey: UserDefaultsKeys.automaticallyAcceptFiles.rawValue) }}
|
||||
|
||||
|
||||
@Published var openFinderAfterReceiving: Bool = UserDefaults.standard.bool(forKey: UserDefaultsKeys.openFinderAfterReceiving.rawValue)
|
||||
{ didSet { UserDefaults.standard.set(openFinderAfterReceiving, forKey: UserDefaultsKeys.openFinderAfterReceiving.rawValue) }}
|
||||
|
||||
|
||||
@Published var saveFolderBookmark: Data? = UserDefaults.standard.data(forKey: UserDefaultsKeys.saveFolderBookmark.rawValue)
|
||||
{ didSet { UserDefaults.standard.set(saveFolderBookmark, forKey: UserDefaultsKeys.saveFolderBookmark.rawValue) }}
|
||||
|
||||
|
||||
|
||||
// MARK: - Non-Published UserDefaults Backed Properties
|
||||
|
||||
var incomingTransmissionCount: Int = UserDefaults.standard.integer(forKey: UserDefaultsKeys.transmissionCount.rawValue)
|
||||
{ didSet { UserDefaults.standard.set(incomingTransmissionCount, forKey: UserDefaultsKeys.transmissionCount.rawValue) }}
|
||||
|
||||
|
||||
var endpointID: String? = UserDefaults.standard.string(forKey: UserDefaultsKeys.endpointID.rawValue)
|
||||
{ didSet { UserDefaults.standard.set(endpointID, forKey: UserDefaultsKeys.endpointID.rawValue) }}
|
||||
|
||||
|
||||
var isEligibleForIap: Bool = UserDefaults.standard.bool(forKey: UserDefaultsKeys.isEligibleForIap.rawValue)
|
||||
{ didSet { UserDefaults.standard.set(isEligibleForIap, forKey: UserDefaultsKeys.isEligibleForIap.rawValue) }}
|
||||
|
||||
|
||||
var appLaunchedBefore: Bool = UserDefaults.standard.bool(forKey: UserDefaultsKeys.appLaunchedBefore.rawValue)
|
||||
{ didSet { UserDefaults.standard.set(appLaunchedBefore, forKey: UserDefaultsKeys.appLaunchedBefore.rawValue) }}
|
||||
|
||||
|
||||
|
||||
// MARK: - Debug
|
||||
|
||||
func deleteAllUserDefaults() {
|
||||
for key in UserDefaultsKeys.allCases {
|
||||
UserDefaults.standard.removeObject(forKey: key.rawValue)
|
||||
}
|
||||
|
||||
Settings.shared = Settings()
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - UserDefaults Keys
|
||||
|
||||
public enum UserDefaultsKeys: String, CaseIterable {
|
||||
case isEligibleForIap = "isEligibleForIap"
|
||||
case appLaunchedBefore = "ShowedWelcomeScreen"
|
||||
case plusVersion = "isPlusVersion"
|
||||
case transmissionCount = "reviewRequestCountKey"
|
||||
case automaticallyAcceptFiles = "automaticallyAcceptFiles"
|
||||
case saveFolderBookmark = "saveFolderBookmark"
|
||||
case openFinderAfterReceiving = "openFinderAfterReceiving"
|
||||
case endpointID = "endpointID"
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ class ErrorAlertHandler {
|
||||
|
||||
let title = String(format: "TransferError".localized(), arguments: [deviceName])
|
||||
log("Showing alert with title: \"\(title)\" and description: \"\(description)\"")
|
||||
log("Unsuccessful transmission. Already successful transmissions: \(incomingTransmissionCount())")
|
||||
log("Unsuccessful transmission. Already successful transmissions: \(Settings.shared.incomingTransmissionCount)")
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
@@ -103,8 +103,8 @@ class ErrorAlertHandler {
|
||||
self.isAlertShown = false
|
||||
|
||||
if result == .alertFirstButtonReturn {
|
||||
log("Sending logging string")
|
||||
sendLoggingString()
|
||||
//log("Sending logging string")
|
||||
//sendLoggingString()
|
||||
}
|
||||
#else
|
||||
showAlert(title: title, message: description)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// SendToExtension.swift
|
||||
// QuickDrop
|
||||
//
|
||||
// Created by Leon Böttger on 09.09.25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
func sendToSharingService(items: [Any]) {
|
||||
if let sharingService = NSSharingService(named: NSSharingService.Name("com.leonboettger.neardrop.ShareExtension")) {
|
||||
sharingService.perform(withItems: items)
|
||||
}
|
||||
else {
|
||||
DispatchQueue.main.async {
|
||||
// show alert
|
||||
AudioManager.playErrorSound()
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .critical
|
||||
alert.messageText = "ShareExtensionDisabledTitle".localized()
|
||||
alert.informativeText = "ShareExtensionDisabledDescription".localized()
|
||||
alert.addButton(withTitle: "CloseAlert".localized())
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -10,15 +10,13 @@ import AppKit
|
||||
|
||||
struct SettingsView: View {
|
||||
|
||||
@AppStorage(UserDefaultsKeys.automaticallyAcceptFiles.rawValue) private var automaticallyAcceptFiles = false
|
||||
@AppStorage(UserDefaultsKeys.openFinderAfterReceiving.rawValue) private var openFinderAfterReceiving = false
|
||||
@AppStorage(UserDefaultsKeys.saveFolderBookmark.rawValue) private var saveFolderPath: Data = Data()
|
||||
@ObservedObject var settings = Settings.shared
|
||||
|
||||
var body: some View {
|
||||
|
||||
LargeAppIconView(title: "Settings") {
|
||||
Form {
|
||||
Toggle("AutomaticallyAcceptFiles", isOn: $automaticallyAcceptFiles)
|
||||
Toggle("AutomaticallyAcceptFiles", isOn: $settings.automaticallyAcceptFiles)
|
||||
.padding(.top, 10)
|
||||
|
||||
Text("AutomaticallyAcceptFilesFooter")
|
||||
@@ -33,7 +31,7 @@ struct SettingsView: View {
|
||||
Text("SaveFilesTo")
|
||||
Spacer()
|
||||
Text(getSavedFolderPath() ?? "DownloadsFolder".localized())
|
||||
.foregroundColor(saveFolderPath.isEmpty ? .gray : .primary)
|
||||
.foregroundColor((settings.saveFolderBookmark ?? Data()).isEmpty ? .gray : .primary)
|
||||
Button("SaveFilesToButton") {
|
||||
selectFolder()
|
||||
}
|
||||
@@ -42,7 +40,7 @@ struct SettingsView: View {
|
||||
Divider()
|
||||
.padding(.vertical, 10)
|
||||
|
||||
Toggle("OpenFinderAfterReceiving", isOn: $openFinderAfterReceiving)
|
||||
Toggle("OpenFinderAfterReceiving", isOn: $settings.openFinderAfterReceiving)
|
||||
.padding(.top, 5)
|
||||
|
||||
if #available(macOS 13.0, *) {
|
||||
@@ -72,10 +70,10 @@ struct SettingsView: View {
|
||||
|
||||
|
||||
private func getSavedFolderPath() -> String? {
|
||||
if !saveFolderPath.isEmpty {
|
||||
if let bookmark = settings.saveFolderBookmark, !bookmark.isEmpty {
|
||||
var isStale = false
|
||||
do {
|
||||
let url = try URL(resolvingBookmarkData: saveFolderPath, options: [], relativeTo: nil, bookmarkDataIsStale: &isStale)
|
||||
let url = try URL(resolvingBookmarkData: bookmark, options: [], relativeTo: nil, bookmarkDataIsStale: &isStale)
|
||||
return url.lastPathComponent // Returns the folder path as a string
|
||||
} catch {
|
||||
log("Failed to resolve bookmark: \(error)")
|
||||
@@ -96,11 +94,30 @@ struct SettingsView: View {
|
||||
do {
|
||||
log("Selected folder: \(url)")
|
||||
|
||||
if url.description == "file:///" {
|
||||
throw NSError(domain: "InvalidSelection", code: 0, userInfo: [NSLocalizedDescriptionKey: "Cannot select root directory. Please choose a specific folder."])
|
||||
}
|
||||
|
||||
let bookmarkData = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
|
||||
|
||||
UserDefaults.standard.set(bookmarkData, forKey: UserDefaultsKeys.saveFolderBookmark.rawValue)
|
||||
Settings.shared.saveFolderBookmark = bookmarkData
|
||||
Settings.shared.openFinderAfterReceiving = true
|
||||
} catch {
|
||||
log("Failed to save security-scoped bookmark: \(error)")
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now()) {
|
||||
|
||||
AudioManager.playErrorSound()
|
||||
|
||||
// show alert
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .critical
|
||||
alert.messageText = "FolderSelectionFailedTitle".localized()
|
||||
alert.informativeText = "FolderSelectionFailedMessage".localized()
|
||||
alert.addButton(withTitle: "CloseAlert".localized())
|
||||
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ struct TutorialView: View {
|
||||
let openPlus: () -> Void
|
||||
|
||||
@State private var licenseWindow: NSWindow?
|
||||
@AppStorage(UserDefaultsKeys.plusVersion.rawValue) var isPlusVersion = false
|
||||
@ObservedObject var settings = Settings.shared
|
||||
|
||||
var body: some View {
|
||||
|
||||
@@ -40,7 +40,7 @@ struct TutorialView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
if !isPlusVersion {
|
||||
if !settings.gotPlus {
|
||||
Button {
|
||||
openPlus()
|
||||
} label: {
|
||||
@@ -54,11 +54,9 @@ struct TutorialView: View {
|
||||
|
||||
#if DEBUG
|
||||
Button {
|
||||
for key in UserDefaultsKeys.allCases {
|
||||
UserDefaults.standard.removeObject(forKey: key.rawValue)
|
||||
}
|
||||
Settings.shared.deleteAllUserDefaults()
|
||||
} label: {
|
||||
Text("Reset UD")
|
||||
Text("ResetAllSettings")
|
||||
.underline()
|
||||
.font(.footnote)
|
||||
.opacity(0.5)
|
||||
|
||||
@@ -58,7 +58,7 @@ struct WelcomeScreen: View {
|
||||
.listStyle(SidebarListStyle())
|
||||
|
||||
Divider()
|
||||
.opacity(colorScheme == .light ? 1 : 0)
|
||||
.opacity(colorScheme == .light ? 1 : 0.5)
|
||||
.edgesIgnoringSafeArea(.vertical)
|
||||
|
||||
ZStack {
|
||||
@@ -138,12 +138,6 @@ struct WelcomeScreen: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendToSharingService(items: [Any]) {
|
||||
if let sharingService = NSSharingService(named: NSSharingService.Name("com.leonboettger.neardrop.ShareExtension")) {
|
||||
sharingService.perform(withItems: items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import LUI
|
||||
|
||||
class SendModel: ObservableObject, OutboundAppDelegate {
|
||||
|
||||
@Published var showQrCodeView: Bool = false
|
||||
|
||||
@Published var foundDevices: [RemoteDeviceInfo] = []
|
||||
@Published var selectedDevice: RemoteDeviceInfo?
|
||||
|
||||
@@ -24,49 +26,62 @@ class SendModel: ObservableObject, OutboundAppDelegate {
|
||||
private var connectionEstablished = false
|
||||
private var timeoutDispatchWorkItem: DispatchWorkItem? = nil
|
||||
|
||||
|
||||
init() {
|
||||
NearbyConnectionManager.shared.startDeviceDiscovery()
|
||||
NearbyConnectionManager.shared.addShareExtensionDelegate(self)
|
||||
NearbyConnectionManager.shared.addOutboundAppDelegate(self)
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
NearbyConnectionManager.shared.removeOutboundAppDelegate(self)
|
||||
}
|
||||
|
||||
|
||||
func addDevice(device: RemoteDeviceInfo) {
|
||||
withAnimation(.smooth) {
|
||||
foundDevices.append(device)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func removeDevice(id: String) {
|
||||
withAnimation(.smooth) {
|
||||
foundDevices.removeAll { $0.id == id }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func connectionWasEstablished(pinCode: String) {
|
||||
connectionEstablished = true
|
||||
progressState = String(format: "PinCode".localized(), arguments: [pinCode])
|
||||
progressValue = 0
|
||||
}
|
||||
|
||||
|
||||
func connectionFailed(error: any Error) {
|
||||
|
||||
#if os(iOS)
|
||||
errorVibration()
|
||||
#endif
|
||||
|
||||
ErrorAlertHandler.shared.showErrorAlert(for: selectedDevice?.name ?? "?", error: error)
|
||||
ErrorAlertHandler.shared.showErrorAlert(for: selectedDevice?.name ?? "UnknownDevice".localized(), error: error)
|
||||
|
||||
progressValue = nil
|
||||
selectedDevice = nil
|
||||
}
|
||||
|
||||
|
||||
func transferAccepted() {
|
||||
progressState = "Sending".localized()
|
||||
}
|
||||
|
||||
|
||||
func transferProgress(progress: Double) {
|
||||
progressValue = progress
|
||||
}
|
||||
|
||||
|
||||
func transferFinished() {
|
||||
|
||||
#if os(iOS)
|
||||
@@ -78,22 +93,38 @@ class SendModel: ObservableObject, OutboundAppDelegate {
|
||||
progressState = "TransferFinished".localized()
|
||||
|
||||
NearbyConnectionManager.shared.attachments?.closeView?()
|
||||
|
||||
#if !EXTENSION
|
||||
requestReviewOnce()
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
func startTransferWithQrCode(device: RemoteDeviceInfo) {
|
||||
selectDevice(device: device)
|
||||
showQrCodeView = false
|
||||
|
||||
#if EXTENSION
|
||||
if let attachments = NearbyConnectionManager.shared.attachments {
|
||||
self.selectDevice(device:device, with: attachments)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func selectDevice(device: RemoteDeviceInfo) {
|
||||
|
||||
func selectDevice(device: RemoteDeviceInfo, with attachments: AttachmentDetails) {
|
||||
|
||||
self.urls = attachments.urls
|
||||
self.textToSend = attachments.textToSend
|
||||
|
||||
// already selected, cancel transfer
|
||||
if device == selectedDevice {
|
||||
progressValue = 0
|
||||
progressState = nil
|
||||
selectedDevice = nil
|
||||
NearbyConnectionManager.shared.cancelOutgoingTransfer(id: device.id!)
|
||||
NearbyConnectionManager.shared.cancelTransfer(id: device.id!)
|
||||
}
|
||||
else {
|
||||
|
||||
progressValue = 0
|
||||
progressState = "Connecting".localized()
|
||||
selectedDevice = device
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"fill" : {
|
||||
"linear-gradient" : [
|
||||
"display-p3:0.31273,0.68878,0.97563,1.00000",
|
||||
"display-p3:0.22962,0.42793,0.95758,1.00000"
|
||||
]
|
||||
},
|
||||
"groups" : [
|
||||
{
|
||||
"layers" : [
|
||||
{
|
||||
"image-name" : "QuickDropArrows.png",
|
||||
"name" : "QuickDropArrows"
|
||||
},
|
||||
{
|
||||
"image-name" : "QuickDropCircle.png",
|
||||
"name" : "QuickDropCircle",
|
||||
"opacity" : 0.5
|
||||
}
|
||||
],
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"translucency" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.52
|
||||
}
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"circles" : [
|
||||
"watchOS"
|
||||
],
|
||||
"squares" : "shared"
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,10 @@ import LUI
|
||||
let configuration = LUIConfiguration(
|
||||
appName: "QuickDrop",
|
||||
|
||||
inAppPurchaseName: "plusversion",
|
||||
|
||||
plusVersionInfo: GetPlusViewInformation(description: "plusview_description_phone", plusVersionFeatures: [], settingsLabelColor: .orange, settingsFooter: "plusview_footer_phone"),
|
||||
|
||||
hasRoundedFont: false,
|
||||
|
||||
copyrightInformation: licenseText,
|
||||
@@ -24,6 +28,8 @@ let configuration = LUIConfiguration(
|
||||
|
||||
supportsSplitView: false,
|
||||
|
||||
supportsLiquidGlass: true,
|
||||
|
||||
supportsPortraitSplitView: false,
|
||||
|
||||
supportsDarkModeShadow: false
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
{
|
||||
"sourceLanguage" : "en",
|
||||
"strings" : {
|
||||
"CFBundleDisplayName" : {
|
||||
"comment" : "Bundle display name",
|
||||
"extractionState" : "extracted_with_value",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "QuickDrop"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CFBundleName" : {
|
||||
"comment" : "Bundle name",
|
||||
"extractionState" : "extracted_with_value",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "QuickDropiOS"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"NSHumanReadableCopyright" : {
|
||||
"comment" : "Copyright (human-readable)",
|
||||
"extractionState" : "extracted_with_value",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"NSLocalNetworkUsageDescription" : {
|
||||
"comment" : "Privacy - Local Network Usage Description",
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "الوصول إلى الشبكة المحلية مطلوب لاكتشاف الأجهزة ونقل الملفات عبر شبكة Wi-Fi الخاصة بك."
|
||||
}
|
||||
},
|
||||
"ca" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cal accés a la xarxa local per detectar dispositius i transferir fitxers a través de la teva xarxa Wi-Fi."
|
||||
}
|
||||
},
|
||||
"cs" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pro přenos souborů a detekci zařízení přes vaši Wi-Fi síť je třeba povolit přístup k místní síti."
|
||||
}
|
||||
},
|
||||
"da" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Adgang til lokalt netværk er nødvendig for at finde enheder og overføre filer via dit Wi-Fi-netværk."
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Der Zugriff auf das lokale Netzwerk ist erforderlich, um Geräte zu erkennen und Dateien über WLAN zu übertragen."
|
||||
}
|
||||
},
|
||||
"el" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Απαιτείται πρόσβαση στο τοπικό δίκτυο για ανίχνευση συσκευών και μεταφορά αρχείων μέσω του Wi-Fi δικτύου σας."
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Local network access is needed to detect devices and transfer files over your Wi-Fi network."
|
||||
}
|
||||
},
|
||||
"en-AU" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Local network access is required to find devices and transfer files over your Wi-Fi network."
|
||||
}
|
||||
},
|
||||
"en-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Local network access is required to find devices and transfer files over your Wi-Fi network."
|
||||
}
|
||||
},
|
||||
"en-GB" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Local network access is required to discover devices and transfer files via your Wi-Fi network."
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Se necesita acceso a la red local para detectar dispositivos y transferir archivos a través de su red Wi-Fi."
|
||||
}
|
||||
},
|
||||
"es-419" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Se necesita acceso a la red local para detectar dispositivos y transferir archivos por tu red Wi-Fi."
|
||||
}
|
||||
},
|
||||
"fi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Lähiverkkoyhteys tarvitaan laitteiden tunnistamiseen ja tiedostojen siirtoon Wi-Fi-verkossasi."
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "L’accès au réseau local est nécessaire pour détecter les appareils et transférer des fichiers via votre réseau Wi-Fi."
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "L’accès au réseau local est nécessaire pour détecter les appareils et transférer des fichiers sur votre réseau Wi-Fi."
|
||||
}
|
||||
},
|
||||
"he" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "נדרש גישה לרשת המקומית כדי לזהות מכשירים ולהעביר קבצים דרך רשת ה-Wi-Fi שלך."
|
||||
}
|
||||
},
|
||||
"hi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "डिवाइस का पता लगाने और अपने Wi-Fi नेटवर्क पर फाइल ट्रांसफर के लिए लोकल नेटवर्क एक्सेस आवश्यक है।"
|
||||
}
|
||||
},
|
||||
"hr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pristup lokalnoj mreži potreban je za pronalaženje uređaja i prijenos datoteka putem vaše Wi-Fi mreže."
|
||||
}
|
||||
},
|
||||
"hu" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Helyi hálózati hozzáférés szükséges az eszközök felismeréséhez és a fájlok átviteléhez a Wi-Fi hálózatán keresztül."
|
||||
}
|
||||
},
|
||||
"id" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Akses jaringan lokal diperlukan untuk mendeteksi perangkat dan mentransfer file melalui jaringan Wi-Fi Anda."
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "È necessario l’accesso alla rete locale per rilevare i dispositivi e trasferire file tramite la tua rete Wi-Fi."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ローカルネットワークへのアクセスは、デバイスを検出し、Wi-Fi ネットワーク経由でファイルを転送するために必要です。"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "장치를 감지하고 Wi-Fi 네트워크를 통해 파일을 전송하려면 로컬 네트워크 접근이 필요합니다."
|
||||
}
|
||||
},
|
||||
"ms" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Akses rangkaian tempatan diperlukan untuk mengesan peranti dan memindahkan fail melalui rangkaian Wi-Fi anda."
|
||||
}
|
||||
},
|
||||
"nb" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Tilgang til lokalt nettverk er nødvendig for å oppdage enheter og overføre filer via Wi-Fi-nettverket ditt."
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Lokale netwerktoegang is nodig om apparaten te detecteren en bestanden over uw Wi-Fi-netwerk te verzenden."
|
||||
}
|
||||
},
|
||||
"pl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Dostęp do sieci lokalnej jest potrzebny, aby wykrywać urządzenia i przesyłać pliki przez Twoją sieć Wi-Fi."
|
||||
}
|
||||
},
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "O acesso à rede local é necessário para detectar dispositivos e transferir arquivos pela sua rede Wi-Fi."
|
||||
}
|
||||
},
|
||||
"pt-PT" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "O acesso à rede local é necessário para detetar dispositivos e transferir ficheiros através da sua rede Wi-Fi."
|
||||
}
|
||||
},
|
||||
"ro" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Accesul la rețeaua locală este necesar pentru a detecta dispozitivele și a transfera fișiere prin rețeaua Wi-Fi."
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Доступ к локальной сети необходим для обнаружения устройств и передачи файлов через вашу сеть Wi-Fi."
|
||||
}
|
||||
},
|
||||
"sv" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Åtkomst till lokalt nätverk krävs för att upptäcka enheter och överföra filer via ditt Wi-Fi-nätverk."
|
||||
}
|
||||
},
|
||||
"th" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "การเข้าถึงเครือข่ายภายในเครื่องจำเป็นสำหรับการตรวจหาดีไวซ์และการถ่ายโอนไฟล์ผ่านเครือข่าย Wi-Fi ของคุณ"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cihazları algılamak ve dosya aktarmak için yerel ağa erişim gereklidir."
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Доступ до локальної мережі потрібен для виявлення пристроїв та передавання файлів через вашу мережу Wi-Fi."
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cần truy cập mạng nội bộ để phát hiện thiết bị và truyền tệp qua Wi-Fi của bạn."
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "需要本地网络访问以检测设备并通过您的 Wi-Fi 网络传输文件。"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "需要本機網路存取權才能偵測裝置並透過 Wi-Fi 網路傳輸檔案。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -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 = incomingTransmissionCount()
|
||||
|
||||
#if os(macOS)
|
||||
if currentCount == 0 {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
SKStoreReviewController.requestReview()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
UserDefaults.standard.set(currentCount + 1, forKey: UserDefaultsKeys.transmissionCount.rawValue)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,15 +12,16 @@ import StoreKit
|
||||
struct ContentView: View {
|
||||
|
||||
@ObservedObject var luiSettings = LUISettings.sharedInstance
|
||||
@ObservedObject var settings = Settings.shared
|
||||
|
||||
var body: some View {
|
||||
AppRootView(isPlus: .constant(true), phoneView: {
|
||||
AppRootView(isPlus: $settings.gotPlus, phoneView: {
|
||||
DeviceListView()
|
||||
.environment(\.sheetActive, isShareExtension())
|
||||
}, settingsView: {
|
||||
CustomSection {
|
||||
CustomSection(header: settings.gotPlus ? "" : "General", footer: "TrustedDevicesFooterShort") {
|
||||
LUIButton {
|
||||
SaveFilesManager.shared.openDownloadedFilesFolder()
|
||||
FileManager.default.openDocumentFolder()
|
||||
} label: {
|
||||
NavigationLinkLabel(imageName: "folder.fill", text: "BrowseDownloadedFiles")
|
||||
}
|
||||
@@ -37,19 +38,28 @@ struct ContentView: View {
|
||||
|
||||
struct DeviceListView: View {
|
||||
|
||||
@State private var qrCode: Image? = nil
|
||||
@StateObject var sendModel = SendModel()
|
||||
|
||||
#if !EXTENSION
|
||||
@StateObject var receiveModel = ReceiveModel()
|
||||
#endif
|
||||
#if !EXTENSION
|
||||
@StateObject var receiveModel = ReceiveModel(controlPlusScreen: { show in
|
||||
log("[DeviceListView] Setting showPlusScreen to \(show)")
|
||||
DeviceListViewModel.sharedInstance.showPlusScreen = show
|
||||
})
|
||||
#endif
|
||||
|
||||
@ObservedObject var model = DeviceListViewModel.sharedInstance
|
||||
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@AppStorage("requestedReview") private var requestedReview = false
|
||||
@Environment(\.sheetActive) var sheetActive
|
||||
@ObservedObject var nearbyConnectionManager = NearbyConnectionManager.shared
|
||||
@ObservedObject var settings = Settings.shared
|
||||
|
||||
var body: some View {
|
||||
|
||||
NavigationSubView(header: "QuickDrop ", navigationBarLayout: isShareExtension() ? .SmallOnlyAlways : .Default) {
|
||||
let showsLoadingIndicator = nearbyConnectionManager.hasLocalNetworkPermission && nearbyConnectionManager.isConnectedToLocalNetwork
|
||||
|
||||
BottomBarView(header: "QuickDrop", navigationBarLayout: isShareExtension() ? .SmallOnlyAlways : .Default, bottomViewHeight: 30) {
|
||||
VStack {
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
@@ -72,14 +82,14 @@ struct DeviceListView: View {
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color.defaultForegroundColor)
|
||||
.background(sheetActive ? Color.sheetForegroundColor : Color.defaultForegroundColor)
|
||||
.cornerRadius(24)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
if !nearbyConnectionManager.hasLocalNetworkAccess {
|
||||
if !nearbyConnectionManager.hasLocalNetworkPermission {
|
||||
CardView(backgroundColor: .red, title: "NoNetworkAccess", titleSymbol: "network.slash") {
|
||||
CardSubView(symbol: "exclamationmark.triangle.fill", text: "NoLocalNetworkAccessDescription")
|
||||
}
|
||||
@@ -93,16 +103,22 @@ struct DeviceListView: View {
|
||||
|
||||
CustomSection(header: "NoDevicesFound") {
|
||||
|
||||
let hasWifi = nearbyConnectionManager.isConnectedViaWiFi
|
||||
let hasWifi = nearbyConnectionManager.isConnectedToLocalNetwork
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: hasWifi ? "arrow.down.circle.fill" : "wifi.slash")
|
||||
.font(.system(size: 30))
|
||||
.foregroundColor(.blue)
|
||||
.padding(.leading, -10)
|
||||
.frame(width: 30)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(Color.white)
|
||||
.padding(3)
|
||||
.opacity(hasWifi ? 1 : 0)
|
||||
)
|
||||
.frame(width: 32)
|
||||
|
||||
Text(hasWifi ? "DownloadQuickDropOnPlayStore" : "NoWiFiConnectionDescription")
|
||||
LUIText(hasWifi ? "DownloadQuickDropOnPlayStore" : "NoWiFiConnectionDescription")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.animation(.default, value: hasWifi)
|
||||
.padding(.vertical)
|
||||
@@ -119,9 +135,7 @@ struct DeviceListView: View {
|
||||
if let attachments = NearbyConnectionManager.shared.attachments {
|
||||
LUIButton {
|
||||
lightVibration()
|
||||
sendModel.urls = attachments.urls
|
||||
sendModel.textToSend = attachments.textToSend
|
||||
sendModel.selectDevice(device: device)
|
||||
sendModel.selectDevice(device: device, with: attachments)
|
||||
} label: {
|
||||
DeviceButtonLabel(device: device, isSelected: isSelected, progress: sendModel.progressState, progressValue: sendModel.progressValue)
|
||||
}
|
||||
@@ -137,7 +151,7 @@ struct DeviceListView: View {
|
||||
}
|
||||
|
||||
|
||||
if nearbyConnectionManager.hasLocalNetworkAccess && nearbyConnectionManager.isConnectedViaWiFi {
|
||||
if showsLoadingIndicator {
|
||||
HStack(spacing: 8) {
|
||||
Text("SearchingForDevices")
|
||||
.font(.system(size: 14))
|
||||
@@ -151,43 +165,56 @@ struct DeviceListView: View {
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: 700)
|
||||
} bottomView: {
|
||||
|
||||
LUIButton {
|
||||
qrCode = NearbyConnectionManager.shared.generateQrCodeKey()
|
||||
sendModel.showQrCodeView = true
|
||||
} label: {
|
||||
UnderlineText(label: "DeviceNotShown")
|
||||
.padding()
|
||||
}
|
||||
.opacity(showsLoadingIndicator ? 1 : 0)
|
||||
}
|
||||
.luiSheet(isPresented: $sendModel.showQrCodeView, content: {
|
||||
NavigationView {
|
||||
NavigationSubView(header: "ConnectDevice", navigationBarLayout: .SmallOnlyAlways) {
|
||||
SmallSheetView(type: .sendToDeviceQrCode, dynamicQrCode: qrCode, closeView: {})
|
||||
}
|
||||
.navigationBarItems(trailing: XButton(action: { sendModel.showQrCodeView = false }))
|
||||
}
|
||||
})
|
||||
.onChange(of: scenePhase) { newValue in
|
||||
|
||||
if newValue == .active {
|
||||
|
||||
log("App became active")
|
||||
log("[ScenePhase] App became active")
|
||||
NearbyConnectionManager.shared.startDeviceDiscovery()
|
||||
#if !EXTENSION
|
||||
#if !EXTENSION
|
||||
NearbyConnectionManager.shared.becomeVisible()
|
||||
|
||||
if incomingTransmissionCount() > 0 && !requestedReview {
|
||||
if Settings.shared.incomingTransmissionCount > 0 {
|
||||
runAfter(seconds: 0.3) {
|
||||
if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
|
||||
|
||||
log("Requesting Review...")
|
||||
|
||||
SKStoreReviewController.requestReview(in: scene)
|
||||
requestedReview = true
|
||||
}
|
||||
else {
|
||||
log("Error Requesting Review!")
|
||||
}
|
||||
requestReviewOnce()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
if newValue == .background {
|
||||
|
||||
log("App went to background")
|
||||
#if !EXTENSION
|
||||
log("[ScenePhase] App went to background")
|
||||
#if !EXTENSION
|
||||
NearbyConnectionManager.shared.becomeInvisible()
|
||||
#endif
|
||||
#endif
|
||||
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
||||
}
|
||||
}
|
||||
.animation(.smooth, value: nearbyConnectionManager.hasLocalNetworkAccess)
|
||||
#if !EXTENSION
|
||||
.modifier(PlusVersionSheetModifier(sheetActive: $model.showPlusScreen, isPlusVersion: $settings.gotPlus))
|
||||
#endif
|
||||
.animation(.smooth, value: nearbyConnectionManager.hasLocalNetworkPermission)
|
||||
.animation(.smooth, value: nearbyConnectionManager.isConnectedToLocalNetwork)
|
||||
.navigationBarItems(trailing: ZStack {
|
||||
if isShareExtension() {
|
||||
XButton(action: { NearbyConnectionManager.shared.attachments?.closeView?() })
|
||||
@@ -199,6 +226,12 @@ struct DeviceListView: View {
|
||||
}
|
||||
}
|
||||
|
||||
final class DeviceListViewModel: SharedInstance {
|
||||
static var sharedInstance = DeviceListViewModel()
|
||||
|
||||
@Published var showPlusScreen = false
|
||||
}
|
||||
|
||||
|
||||
struct DeviceFilePickerButton: View {
|
||||
|
||||
@@ -215,9 +248,7 @@ struct DeviceFilePickerButton: View {
|
||||
DeviceButtonLabel(device: device, isSelected: isPreparing || isSelected, progress: isPreparing ? "Preparing".localized() : progressState, progressValue: progressValue)
|
||||
} onResult: { urls, text in
|
||||
isPreparing = false
|
||||
sendModel.urls = urls ?? []
|
||||
sendModel.textToSend = text
|
||||
sendModel.selectDevice(device: device)
|
||||
sendModel.selectDevice(device: device, with: AttachmentDetails(urls: urls ?? [], textToSend: text, shortDescription: ""))
|
||||
} onPrepare: {
|
||||
isPreparing = true
|
||||
}
|
||||
@@ -280,11 +311,11 @@ struct DeviceButtonLabel: View {
|
||||
|
||||
|
||||
func isShareExtension() -> Bool {
|
||||
#if EXTENSION
|
||||
#if EXTENSION
|
||||
return true
|
||||
#else
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -162,10 +162,10 @@ fileprivate struct DataUrl: Transferable {
|
||||
.cornerRadius(8)
|
||||
} onResult: { urls, text in
|
||||
if let urls = urls {
|
||||
print("Selected URLs: \(urls)")
|
||||
log("Selected URLs: \(urls)")
|
||||
}
|
||||
if let text = text {
|
||||
print("Clipboard text: \(text)")
|
||||
log("Clipboard text: \(text)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,41 +18,36 @@ final class ProgressAlert {
|
||||
private weak var progressView: UIProgressView?
|
||||
|
||||
// MARK: - Initial Accept / Decline
|
||||
func askForUserPermission(title: String, message: String, acceptLabel: String, acceptAlwaysLabel: String?, rejectLabel: String, acceptAutomatically: Bool, onAccept: @escaping (AcceptAction) -> Void) {
|
||||
func askForUserPermission(title: String, message: String, acceptLabel: String, acceptAlwaysLabel: String?, rejectLabel: String, onAccept: @escaping (AcceptAction) -> Void) {
|
||||
guard let vc = topMostViewController() else { return }
|
||||
|
||||
if acceptAutomatically {
|
||||
let alert = UIAlertController(title: title,
|
||||
message: message,
|
||||
preferredStyle: .alert)
|
||||
|
||||
alert.addAction(UIAlertAction(title: rejectLabel, style: .cancel, handler: { _ in
|
||||
onAccept(.Decline)
|
||||
}))
|
||||
|
||||
alert.addAction(UIAlertAction(title: acceptLabel, style: .default, handler: { _ in
|
||||
onAccept(.Accept)
|
||||
self.showProgressAlert(on: vc)
|
||||
}
|
||||
else {
|
||||
|
||||
let alert = UIAlertController(title: title,
|
||||
message: message,
|
||||
preferredStyle: .alert)
|
||||
|
||||
alert.addAction(UIAlertAction(title: rejectLabel, style: .cancel, handler: { _ in
|
||||
onAccept(.Decline)
|
||||
}))
|
||||
|
||||
if let acceptAlways = acceptAlwaysLabel {
|
||||
alert.addAction(UIAlertAction(title: acceptAlways, style: .default, handler: { _ in
|
||||
onAccept(.AcceptAlways)
|
||||
}))
|
||||
|
||||
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
|
||||
private func showProgressAlert(on vc: UIViewController) {
|
||||
private func showProgressAlert(onCancel: @escaping () -> Void) {
|
||||
|
||||
guard let vc = topMostViewController() else { return }
|
||||
|
||||
let alert = UIAlertController(title: "Receiving".localized(),
|
||||
message: "\n\n",
|
||||
preferredStyle: .alert)
|
||||
@@ -67,7 +62,8 @@ final class ProgressAlert {
|
||||
])
|
||||
|
||||
let cancel = UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: { _ in
|
||||
print("Cancel during progress")
|
||||
log("Cancel during progress")
|
||||
onCancel()
|
||||
})
|
||||
alert.addAction(cancel)
|
||||
|
||||
@@ -76,11 +72,25 @@ final class ProgressAlert {
|
||||
|
||||
vc.present(alert, animated: true)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Update Progress
|
||||
// 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 {
|
||||
|
||||
// Show progress if not shown yet
|
||||
if self.progressAlert == nil {
|
||||
self.showProgressAlert(onCancel: onCancel)
|
||||
}
|
||||
|
||||
if let p = progress {
|
||||
self.progressView?.setProgress(Float(p), animated: true)
|
||||
completion()
|
||||
@@ -1,10 +0,0 @@
|
||||
/*
|
||||
InfoPlist.strings
|
||||
AirGuard
|
||||
|
||||
Created by Leon Böttger on 23.08.22.
|
||||
|
||||
*/
|
||||
|
||||
|
||||
NSLocalNetworkUsageDescription = "الوصول إلى الشبكة المحلية مطلوب لاكتشاف الأجهزة ونقل الملفات عبر شبكة Wi-Fi الخاصة بك.";
|
||||
@@ -1,10 +0,0 @@
|
||||
/*
|
||||
InfoPlist.strings
|
||||
AirGuard
|
||||
|
||||
Created by Leon Böttger on 23.08.22.
|
||||
|
||||
*/
|
||||
|
||||
|
||||
NSLocalNetworkUsageDescription = "Der Zugriff auf das lokale Netzwerk ist erforderlich, um Geräte zu erkennen und Dateien über WLAN zu übertragen.";
|
||||
@@ -1,10 +0,0 @@
|
||||
/*
|
||||
InfoPlist.strings
|
||||
AirGuard
|
||||
|
||||
Created by Leon Böttger on 23.08.22.
|
||||
|
||||
*/
|
||||
|
||||
|
||||
NSLocalNetworkUsageDescription = "Se necesita acceso a la red local para detectar dispositivos y transferir archivos a través de su red Wi-Fi.";
|
||||
@@ -1,10 +0,0 @@
|
||||
/*
|
||||
InfoPlist.strings
|
||||
AirGuard
|
||||
|
||||
Created by Leon Böttger on 23.08.22.
|
||||
|
||||
*/
|
||||
|
||||
|
||||
NSLocalNetworkUsageDescription = "L’accès au réseau local est nécessaire pour détecter les appareils et transférer des fichiers via votre réseau Wi-Fi.";
|
||||