mirror of
https://github.com/leonboe1/QuickDrop.git
synced 2026-11-11 02:51:11 +00:00
Add sending files, images, and clipboard to iOS app
This commit is contained in:
1 parent
045725072b
commit
c781af554f
14 files changed
+802
-231
No files matched your search
@@ -16,7 +16,7 @@ import SwiftECC
|
||||
import CryptoKit
|
||||
|
||||
public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearbyConnectionDelegate, OutboundNearbyConnectionDelegate {
|
||||
|
||||
|
||||
private let sleepManager = SleepManager.shared
|
||||
private var tcpListener: NWListener
|
||||
private var mdnsServices: [NetService] = []
|
||||
@@ -30,11 +30,11 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
private var qrCodePrivateKey: ECPrivateKey?
|
||||
private var qrCodeAdvertisingToken: Data?
|
||||
private var qrCodeNameEncryptionKey: SymmetricKey?
|
||||
|
||||
|
||||
public let endpointID: [UInt8] = generateEndpointID()
|
||||
public var mainAppDelegate: (any MainAppDelegate)?
|
||||
public static let shared = NearbyConnectionManager()
|
||||
|
||||
|
||||
|
||||
override init() {
|
||||
tcpListener = try! NWListener(using: NWParameters(tls: .none))
|
||||
@@ -46,7 +46,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
startTCPListener()
|
||||
}
|
||||
|
||||
|
||||
|
||||
private func startTCPListener() {
|
||||
tcpListener.stateUpdateHandler = { (state: NWListener.State) in
|
||||
if case .ready = state {
|
||||
@@ -62,32 +62,32 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
}
|
||||
tcpListener.start(queue: .global(qos: .utility))
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static func generateEndpointID() -> [UInt8] {
|
||||
let userDefaultsKey = UserDefaultsKeys.endpointID.rawValue
|
||||
|
||||
|
||||
// Try to retrieve from UserDefaults
|
||||
if let savedString = UserDefaults.standard.string(forKey: userDefaultsKey),
|
||||
let savedData = savedString.data(using: .utf8)
|
||||
{
|
||||
return [UInt8](savedData)
|
||||
}
|
||||
|
||||
|
||||
// Generate a new random ID
|
||||
var id: [UInt8] = []
|
||||
let alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".compactMap { UInt8($0.asciiValue!) }
|
||||
for _ in 0 ..< 4 {
|
||||
id.append(alphabet[Int.random(in: 0 ..< alphabet.count)])
|
||||
}
|
||||
|
||||
|
||||
// Save to UserDefaults as String
|
||||
let idString = String(bytes: id, encoding: .utf8) ?? ""
|
||||
UserDefaults.standard.set(idString, forKey: userDefaultsKey)
|
||||
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
|
||||
|
||||
private func initMDNS() {
|
||||
let nameBytes: [UInt8] = [
|
||||
@@ -99,7 +99,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
|
||||
let name = Data(nameBytes).urlSafeBase64EncodedString()
|
||||
let port = Int32(tcpListener.port!.rawValue)
|
||||
|
||||
|
||||
mdnsServices = serviceTypes.map { serviceType in
|
||||
let service = NetService(domain: "", type: serviceType, name: name, port: port)
|
||||
service.delegate = self
|
||||
@@ -113,15 +113,15 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
|
||||
|
||||
func getEndpointInfo() -> EndpointInfo {
|
||||
#if os(macOS)
|
||||
#if os(macOS)
|
||||
let endpointInfo = EndpointInfo(name: Host.current().localizedName ?? "Mac", deviceType: .computer)
|
||||
#else
|
||||
#else
|
||||
let endpointInfo = EndpointInfo(name: UIDevice.current.name, deviceType: .phone)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
return endpointInfo
|
||||
}
|
||||
|
||||
|
||||
|
||||
func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo, connection _: InboundNearbyConnection) {
|
||||
mainAppDelegate?.obtainUserConsent(for: transfer, from: device)
|
||||
@@ -135,7 +135,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
mainAppDelegate?.incomingTransfer(id: connection.id, didFinishWith: error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public func submitUserConsent(transferID: String, accept: Bool, storeInTemp: Bool = false) {
|
||||
guard let conn = incomingConnections[transferID] else { return }
|
||||
@@ -143,20 +143,20 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
log("[NearbyConnectionManager] Submitting user consent for transfer \(transferID), accepted: \(accept), store in temp: \(storeInTemp)")
|
||||
conn.submitUserConsent(accepted: accept, storeInTemp: storeInTemp)
|
||||
}
|
||||
|
||||
|
||||
|
||||
public func startDeviceDiscovery() {
|
||||
|
||||
if !startedDeviceDiscovery {
|
||||
startedDeviceDiscovery = true
|
||||
foundServices.removeAll()
|
||||
|
||||
|
||||
log("[NearbyConnectionManager] Starting device discovery")
|
||||
|
||||
|
||||
if browsers.isEmpty {
|
||||
for type in serviceTypes {
|
||||
log("[NearbyConnectionManager] Starting browser for type \(type)")
|
||||
|
||||
|
||||
let browser = NWBrowser(for: .bonjourWithTXTRecord(type: type, domain: nil), using: .tcp)
|
||||
browser.browseResultsChangedHandler = { _, changes in
|
||||
for change in changes {
|
||||
@@ -176,7 +176,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public func stopDeviceDiscovery() {
|
||||
if startedDeviceDiscovery {
|
||||
@@ -186,12 +186,12 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
for browser in browsers {
|
||||
browser.cancel()
|
||||
}
|
||||
|
||||
|
||||
browsers.removeAll()
|
||||
startedDeviceDiscovery = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public func addShareExtensionDelegate(_ delegate: ShareExtensionDelegate) {
|
||||
shareExtensionDelegates.append(delegate)
|
||||
@@ -200,18 +200,18 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
delegate.addDevice(device: device)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public func removeShareExtensionDelegate(_ delegate: ShareExtensionDelegate) {
|
||||
shareExtensionDelegates.removeAll(where: { $0 === delegate })
|
||||
}
|
||||
|
||||
|
||||
|
||||
public func cancelOutgoingTransfer(id: String) {
|
||||
guard let transfer = outgoingTransfers[id] else { return }
|
||||
transfer.connection.cancel()
|
||||
}
|
||||
|
||||
|
||||
|
||||
private func endpointID(for service: NWBrowser.Result) -> String? {
|
||||
guard case let NWEndpoint.service(name: serviceName, type: _, domain: _, interface: _) = service.endpoint else { return nil }
|
||||
@@ -224,7 +224,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
guard serviceIDHash == Data([0xFC, 0x9F, 0x5E]) else { return nil }
|
||||
return endpointID
|
||||
}
|
||||
|
||||
|
||||
|
||||
private func addFoundDevice(service: NWBrowser.Result) {
|
||||
log("[NearbyConnectionManager] Found Service \(service)")
|
||||
@@ -286,7 +286,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
|
||||
return deviceInfo
|
||||
}
|
||||
|
||||
|
||||
|
||||
private func removeFoundDevice(service: NWBrowser.Result) {
|
||||
guard let endpointID = endpointID(for: service) else { return }
|
||||
@@ -313,21 +313,33 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
|
||||
return keyData.urlSafeBase64EncodedString()
|
||||
}
|
||||
|
||||
|
||||
|
||||
public func startOutgoingTransfer(deviceID: String, delegate: ShareExtensionDelegate, urls: [URL], textToSend: String?) {
|
||||
log("Starting outgoing transfer to \(deviceID)")
|
||||
guard let info = foundServices[deviceID] else { return }
|
||||
let tcp = NWProtocolTCP.Options()
|
||||
tcp.noDelay = true
|
||||
let nwconn = NWConnection(to: info.service.endpoint, using: NWParameters(tls: .none, tcp: tcp))
|
||||
let conn = OutboundNearbyConnection(connection: nwconn, id: deviceID, urlsToSend: urls, textToSend: textToSend)
|
||||
conn.delegate = self
|
||||
conn.qrCodePrivateKey = qrCodePrivateKey
|
||||
let transfer = OutgoingTransferInfo(service: info.service, device: info.device!, connection: conn, delegate: delegate)
|
||||
outgoingTransfers[deviceID] = transfer
|
||||
conn.start()
|
||||
|
||||
do {
|
||||
let localUrls = try saveFilesToTemp(urls: urls)
|
||||
|
||||
let tcp = NWProtocolTCP.Options()
|
||||
tcp.noDelay = true
|
||||
let nwconn = NWConnection(to: info.service.endpoint, using: NWParameters(tls: .none, tcp: tcp))
|
||||
|
||||
let conn = OutboundNearbyConnection(connection: nwconn, id: deviceID, urlsToSend: localUrls, textToSend: textToSend)
|
||||
conn.delegate = self
|
||||
conn.qrCodePrivateKey = qrCodePrivateKey
|
||||
let transfer = OutgoingTransferInfo(service: info.service, device: info.device!, connection: conn, delegate: delegate)
|
||||
outgoingTransfers[deviceID] = transfer
|
||||
conn.start()
|
||||
} catch {
|
||||
log("[NearbyConnectionManager] Error zipping URLs: \(error)")
|
||||
shareExtensionDelegates.forEach { delegate in
|
||||
delegate.connectionFailed(with: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func outboundConnectionWasEstablished(connection: OutboundNearbyConnection) {
|
||||
guard let transfer = outgoingTransfers[connection.id] else { return }
|
||||
@@ -336,14 +348,14 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func outboundConnectionTransferAccepted(connection: OutboundNearbyConnection) {
|
||||
guard let transfer = outgoingTransfers[connection.id] else { return }
|
||||
DispatchQueue.main.async {
|
||||
transfer.delegate.transferAccepted()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func outboundConnection(connection: OutboundNearbyConnection, transferProgress: Double) {
|
||||
guard let transfer = outgoingTransfers[connection.id] else { return }
|
||||
@@ -351,7 +363,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
transfer.delegate.transferProgress(progress: transferProgress)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func outboundConnection(connection: OutboundNearbyConnection, failedWithError: Error) {
|
||||
guard let transfer = outgoingTransfers[connection.id] else { return }
|
||||
@@ -361,7 +373,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
outgoingTransfers.removeValue(forKey: connection.id)
|
||||
}
|
||||
|
||||
|
||||
|
||||
func outboundConnectionTransferFinished(connection: OutboundNearbyConnection) {
|
||||
guard let transfer = outgoingTransfers[connection.id] else { return }
|
||||
DispatchQueue.main.async {
|
||||
@@ -369,13 +381,61 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
}
|
||||
outgoingTransfers.removeValue(forKey: connection.id)
|
||||
}
|
||||
|
||||
|
||||
|
||||
public func getActiveConnectionsCount() -> Int {
|
||||
return incomingConnections.count + outgoingTransfers.count
|
||||
}
|
||||
|
||||
|
||||
private func saveFilesToTemp(urls: [URL]) throws -> [URL] {
|
||||
|
||||
var modifiedUrls = [URL]()
|
||||
|
||||
for url in urls {
|
||||
|
||||
// if url is no file url or already in inside temp directory, do nothing
|
||||
if url.isFileURL && !url.standardizedFileURL.path.contains(FileManager.default.temporaryDirectory.standardizedFileURL.path) {
|
||||
|
||||
let accessSuccess = url.startAccessingSecurityScopedResource()
|
||||
|
||||
if !accessSuccess {
|
||||
log("[NearbyConnectionManager] Could not access security scoped resource at \(url)")
|
||||
}
|
||||
|
||||
var isDirectory: ObjCBool = false
|
||||
if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) && isDirectory.boolValue {
|
||||
let zipUrl = try Zip.createAtTemporaryDirectory(zipFilename: url.lastPathComponent, fromDirectory: url)
|
||||
|
||||
modifiedUrls.append(zipUrl)
|
||||
}
|
||||
else {
|
||||
// Copy single file to temp directory
|
||||
let tempUrl = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(url.lastPathComponent)
|
||||
// Remove if already exists
|
||||
if FileManager.default.fileExists(atPath: tempUrl.path) {
|
||||
try FileManager.default.removeItem(at: tempUrl)
|
||||
}
|
||||
try FileManager.default.copyItem(at: url, to: tempUrl)
|
||||
modifiedUrls.append(tempUrl)
|
||||
}
|
||||
|
||||
if accessSuccess {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
log("[NearbyConnectionManager] File URL \(url) does not point to a security scoped resource")
|
||||
modifiedUrls.append(url)
|
||||
}
|
||||
}
|
||||
|
||||
return modifiedUrls
|
||||
}
|
||||
|
||||
|
||||
// -- MARK: - Internal Data Model
|
||||
|
||||
private struct FoundServiceInfo {
|
||||
|
||||
@@ -93,6 +93,10 @@
|
||||
EB886E552E4E50F40052B7D9 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */; };
|
||||
EB886E582E4E66420052B7D9 /* LUI in Frameworks */ = {isa = PBXBuildFile; productRef = EB886E572E4E66420052B7D9 /* LUI */; };
|
||||
EB886E592E4F056B0052B7D9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 69DA9A1329E0BF5200A442DA /* Assets.xcassets */; };
|
||||
EB886E5D2E4F13EB0052B7D9 /* Zip.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBCC27F82D80C9F70079C478 /* Zip.swift */; };
|
||||
EB886E5E2E4F13EB0052B7D9 /* Zip.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBCC27F82D80C9F70079C478 /* Zip.swift */; };
|
||||
EB886E672E4F2F800052B7D9 /* License.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E662E4F2F7C0052B7D9 /* License.swift */; };
|
||||
EB886E682E4F2F800052B7D9 /* License.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E662E4F2F7C0052B7D9 /* License.swift */; };
|
||||
EB96ED8B2D7F7CF7004B3FF1 /* SmallSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8A2D7F7CF4004B3FF1 /* SmallSheetView.swift */; };
|
||||
EB96ED8C2D7F7E7B004B3FF1 /* SmallSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8A2D7F7CF4004B3FF1 /* SmallSheetView.swift */; };
|
||||
EB96ED8D2D7F7EA3004B3FF1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 69DA9A1329E0BF5200A442DA /* Assets.xcassets */; };
|
||||
@@ -223,6 +227,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
EB96ED8A2D7F7CF4004B3FF1 /* SmallSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmallSheetView.swift; sourceTree = "<group>"; };
|
||||
EB96ED8E2D7F8103004B3FF1 /* Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localized.swift; sourceTree = "<group>"; };
|
||||
EB99257F2E45D53E00357055 /* sharing_enums.pb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = sharing_enums.pb.swift; sourceTree = "<group>"; };
|
||||
@@ -434,6 +439,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */,
|
||||
EB886E662E4F2F7C0052B7D9 /* License.swift */,
|
||||
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */,
|
||||
EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */,
|
||||
);
|
||||
@@ -712,6 +718,7 @@
|
||||
EB5C37BA2E377A6F0052EFD7 /* Helpers.swift in Sources */,
|
||||
EBC05C5B2E33937200EECC18 /* ErrorAlertHandler.swift in Sources */,
|
||||
EBC05C712E33B57D00EECC18 /* Converter.swift in Sources */,
|
||||
EB886E5D2E4F13EB0052B7D9 /* Zip.swift in Sources */,
|
||||
EBC05C722E33B57D00EECC18 /* Helper.swift in Sources */,
|
||||
EBC05C732E33B57D00EECC18 /* PlusView.swift in Sources */,
|
||||
EB5C37BF2E377A780052EFD7 /* ukey.pb.swift in Sources */,
|
||||
@@ -730,6 +737,7 @@
|
||||
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 */,
|
||||
69DA9A1229E0BF5100A442DA /* AppDelegate.swift in Sources */,
|
||||
EB6F62D72D91F4AF00534EC2 /* SettingsView.swift in Sources */,
|
||||
@@ -752,6 +760,7 @@
|
||||
EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */,
|
||||
EB5C37F22E377CE70052EFD7 /* NearbyConnection.swift in Sources */,
|
||||
EB5C37F32E377CE70052EFD7 /* SaveFilesManager.swift in Sources */,
|
||||
EB886E672E4F2F800052B7D9 /* License.swift in Sources */,
|
||||
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
||||
EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */,
|
||||
EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */,
|
||||
@@ -766,6 +775,7 @@
|
||||
EB5C37FC2E377CE70052EFD7 /* OutboundNearbyConnection.swift in Sources */,
|
||||
EB5C37FD2E377CE70052EFD7 /* Logger.swift in Sources */,
|
||||
EB5C37FE2E377CE70052EFD7 /* offline_wire_formats.pb.swift in Sources */,
|
||||
EB886E5E2E4F13EB0052B7D9 /* Zip.swift in Sources */,
|
||||
EB5C383D2E38C2D90052EFD7 /* AudioManager.swift in Sources */,
|
||||
EB5C38402E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */,
|
||||
EB5C37FF2E377CE70052EFD7 /* wire_format.pb.swift in Sources */,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "dc2ed9804dbbcffdc405b2edfb84282a3cc98d80b17b0d759f119e81dde31eba",
|
||||
"originHash" : "053324b22e9d13d48c4bf9290d746eb2c1a2e80e5900f9dde545bb150c5eff6a",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "applewatchscreensize",
|
||||
@@ -43,7 +43,7 @@
|
||||
"location" : "https://github.com/leonboe1/LUI.git",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "a4a9e2fb64cd3e928a00bddf3b401bf1e56d69b6"
|
||||
"revision" : "88c015024986364070bf127f156ca9b9f2705533"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1603,6 +1603,113 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ClipboardEmptyMessage" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "يرجى نسخ النص إلى الحافظة، وحاول مرة أخرى."
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Bitte kopiere Text in die Zwischenablage und versuche es erneut."
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Please copy text to the clipboard, and try again."
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Por favor, copie el texto al portapapeles y vuelva a intentarlo."
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Veuillez copier le texte dans le presse-papiers, puis réessayez."
|
||||
}
|
||||
},
|
||||
"hu" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Kérem, másolja a szöveget a vágólapra, majd próbálja újra."
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Copia il testo negli appunti e riprova."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "テキストをクリップボードにコピーして、もう一度お試しください。"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "클립보드에 텍스트를 복사하고 다시 시도하세요."
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Kopieer de tekst naar het klembord en probeer opnieuw."
|
||||
}
|
||||
},
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Por favor, copie o texto para a área de transferência e tente novamente."
|
||||
}
|
||||
},
|
||||
"pt-PT" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Por favor, copie o texto para a área de transferência e tente novamente."
|
||||
}
|
||||
},
|
||||
"ro" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Te rog copiază textul în clipboard și încearcă din nou."
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Пожалуйста, скопируйте текст в буфер обмена и попробуйте снова."
|
||||
}
|
||||
},
|
||||
"th" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "โปรดคัดลอกข้อความไปยังคลิปบอร์ด แล้วลองอีกครั้ง"
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Скопіюйте текст у буфер обміну та спробуйте ще раз."
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "请将文本复制到剪贴板,然后重试。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CloseAlert" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -6201,6 +6308,113 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"QuickDropWelcomeDescription" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "يتيح لك تطبيق QuickDrop نقل الملفات بسهولة وسرعة بين أجهزة iPhone وiPad وMac وAndroid."
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "QuickDrop macht es einfach und schnell, Dateien zwischen iPhone, iPad, Mac und Android-Geräten zu übertragen."
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "QuickDrop makes it easy and quick to transfer files between iPhone, iPad, Mac, and Android devices."
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "QuickDrop facilita y agiliza la transferencia de archivos entre dispositivos iPhone, iPad, Mac y Android."
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "QuickDrop facilite et accélère le transfert de fichiers entre iPhone, iPad, Mac et appareils Android."
|
||||
}
|
||||
},
|
||||
"hu" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "A QuickDrop megkönnyíti és felgyorsítja a fájlok átvitelét iPhone, iPad, Mac és Android eszközök között."
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "QuickDrop rende facile e veloce il trasferimento di file tra iPhone, iPad, Mac e dispositivi Android."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "QuickDropは、iPhone、iPad、Mac、およびAndroidデバイス間でのファイル転送を簡単かつ迅速に行えます。"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "QuickDrop은 iPhone, iPad, Mac, Android 기기 간 파일 전송을 쉽고 빠르게 만들어줍니다."
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "QuickDrop maakt het eenvoudig en snel om bestanden over te zetten tussen iPhone-, iPad-, Mac- en Android-apparaten."
|
||||
}
|
||||
},
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "QuickDrop torna fácil e rápido transferir arquivos entre dispositivos iPhone, iPad, Mac e Android."
|
||||
}
|
||||
},
|
||||
"pt-PT" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "O QuickDrop facilita e acelera a transferência de ficheiros entre iPhone, iPad, Mac e dispositivos Android."
|
||||
}
|
||||
},
|
||||
"ro" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "QuickDrop face ușor și rapid transferul de fișiere între dispozitive iPhone, iPad, Mac și Android."
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "QuickDrop позволяет легко и быстро передавать файлы между iPhone, iPad, Mac и Android-устройствами."
|
||||
}
|
||||
},
|
||||
"th" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "QuickDrop ทำให้การถ่ายโอนไฟล์ระหว่าง iPhone, iPad, Mac และอุปกรณ์ Android ง่ายและรวดเร็ว"
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "QuickDrop дозволяє легко та швидко передавати файли між пристроями iPhone, iPad, Mac та Android."
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "QuickDrop让您可以轻松快捷地在iPhone、iPad、Mac和Android设备之间传输文件。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Quit" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -7483,6 +7697,113 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SendImages" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "إرسال الصور"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Bilder senden"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Send Images"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Enviar imágenes"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Envoyer des images"
|
||||
}
|
||||
},
|
||||
"hu" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Képek küldése"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Invia immagini"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "画像を送信"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "이미지 보내기"
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Afbeeldingen verzenden"
|
||||
}
|
||||
},
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Enviar Imagens"
|
||||
}
|
||||
},
|
||||
"pt-PT" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Enviar Imagens"
|
||||
}
|
||||
},
|
||||
"ro" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Trimite Imagini"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Отправить изображения"
|
||||
}
|
||||
},
|
||||
"th" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ส่งรูปภาพ"
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Надіслати зображення"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "发送图片"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Sending" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// License.swift
|
||||
// QuickDrop
|
||||
//
|
||||
// Created by Leon Böttger on 15.08.25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
var licenseText: String {
|
||||
if let path = Bundle.main.path(forResource: "License", ofType: "txt"),
|
||||
let licenseText = try? String(contentsOfFile: path) {
|
||||
return licenseText
|
||||
}
|
||||
else {
|
||||
return Bundle.main.path(forResource: "License", ofType: "txt") ?? "License file not found"
|
||||
}
|
||||
}
|
||||
@@ -15,14 +15,7 @@ struct LicensePage: View {
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
if let path = Bundle.main.path(forResource: "License", ofType: "txt"),
|
||||
let licenseText = try? String(contentsOfFile: path) {
|
||||
Text(licenseText)
|
||||
.padding()
|
||||
}
|
||||
else {
|
||||
Text(Bundle.main.path(forResource: "License", ofType: "txt") ?? "License file not found")
|
||||
}
|
||||
Text(licenseText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// Configuration.swift
|
||||
// QuickDrop
|
||||
//
|
||||
// Created by Leon Böttger on 15.08.25.
|
||||
//
|
||||
|
||||
import LUI
|
||||
|
||||
let configuration = LUIConfiguration(
|
||||
appName: "QuickDrop",
|
||||
|
||||
hasRoundedFont: false,
|
||||
|
||||
copyrightInformation: licenseText,
|
||||
|
||||
introductionViewInformation: "QuickDropWelcomeDescription",
|
||||
|
||||
usesPagedIntroduction: false,
|
||||
|
||||
supportsSplitView: false,
|
||||
|
||||
supportsPortraitSplitView: false,
|
||||
|
||||
supportsDarkModeShadow: false
|
||||
)
|
||||
@@ -13,7 +13,11 @@ struct ContentView: View {
|
||||
@StateObject var model = ShareViewModel()
|
||||
|
||||
var body: some View {
|
||||
DeviceListView(model: model)
|
||||
AppRootView(isPlus: .constant(true), phoneView: {
|
||||
DeviceListView(model: model)
|
||||
}, settingsView: {
|
||||
EmptyView()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,112 +27,100 @@ struct DeviceListView: View {
|
||||
|
||||
var body: some View {
|
||||
|
||||
NavigationView {
|
||||
NavigationSubView(header: "QuickDrop ") {
|
||||
NavigationSubView(header: "QuickDrop ") {
|
||||
|
||||
// "You're visible as"
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
FooterView(text: "YouAreVisibleAs")
|
||||
|
||||
// "You're visible as"
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
FooterView(text: "YouAreVisibleAs")
|
||||
Text(NearbyConnectionManager.shared.getEndpointInfo().name ?? "QuickDrop")
|
||||
.fontWeight(.bold)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color.defaultForegroundColor)
|
||||
.cornerRadius(24)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.padding(.top, 8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
|
||||
if model.foundDevices.isEmpty {
|
||||
|
||||
CustomSection(header: "NoDevicesFound") {
|
||||
|
||||
Text(NearbyConnectionManager.shared.getEndpointInfo().name ?? "QuickDrop")
|
||||
.fontWeight(.bold)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color.defaultForegroundColor)
|
||||
.cornerRadius(24)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.padding(.top, 8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
|
||||
if model.foundDevices.isEmpty {
|
||||
|
||||
CustomSection(header: "NoDevicesFound") {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "square.and.arrow.down")
|
||||
.font(.system(size: 30))
|
||||
.foregroundColor(.gray)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "square.and.arrow.down")
|
||||
.font(.system(size: 30))
|
||||
.foregroundColor(.gray)
|
||||
|
||||
Text("DownloadQuickDropOnPlayStore")
|
||||
}
|
||||
.padding(.vertical)
|
||||
Text("DownloadQuickDropOnPlayStore")
|
||||
}
|
||||
} else {
|
||||
|
||||
CustomSection(header: "AvailableDevices") {
|
||||
ForEach(model.foundDevices) { device in
|
||||
|
||||
LUIButton {
|
||||
model.selectDevice(device: device)
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
|
||||
let name = device.name ?? "Android"
|
||||
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [Color(red: 0.623, green: 0.659, blue: 0.855),
|
||||
Color(red: 0.474, green: 0.525, blue: 0.796)],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
.padding(.vertical)
|
||||
}
|
||||
} else {
|
||||
|
||||
CustomSection(header: "AvailableDevices") {
|
||||
ForEach(model.foundDevices) { device in
|
||||
|
||||
SendPickerButton {
|
||||
HStack(spacing: 12) {
|
||||
|
||||
let name = device.name ?? "Android"
|
||||
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [Color(red: 0.623, green: 0.659, blue: 0.855),
|
||||
Color(red: 0.474, green: 0.525, blue: 0.796)],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
.frame(width: 40, height: 40)
|
||||
|
||||
Image(systemName: device.icon)
|
||||
.foregroundColor(.white)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
)
|
||||
.frame(width: 40, height: 40)
|
||||
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(name)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(Color.primary)
|
||||
|
||||
Text(model.selectedDevice == device ? model.progressState ?? "..." : "Available".localized())
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(Color.primary.opacity(0.6))
|
||||
}
|
||||
Image(systemName: device.icon)
|
||||
.foregroundColor(.white)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(name)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(Color.primary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
Text(model.selectedDevice == device ? model.progressState ?? "..." : "Available".localized())
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(Color.primary.opacity(0.6))
|
||||
}
|
||||
.padding(.vertical, 13)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(Color.primary.opacity(0.6))
|
||||
}
|
||||
.padding(.vertical, 13)
|
||||
} onResult: { urls, text in
|
||||
model.urls = urls ?? []
|
||||
model.textToSend = text
|
||||
model.selectDevice(device: device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Text("SearchingForDevices")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(Color.primary.opacity(0.6))
|
||||
|
||||
ProgressView()
|
||||
.frame(width: 12, height: 12)
|
||||
}
|
||||
.padding(.vertical, 16)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Privacy policy
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://leonboettger.com/quickdrop-privacy") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
Text("PrivacyPolicy")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(Color.accentColor.opacity(0.8))
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Text("SearchingForDevices")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(Color.primary.opacity(0.6))
|
||||
|
||||
ProgressView()
|
||||
.frame(width: 12, height: 12)
|
||||
}
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
.navigationBarItems(trailing: LUISettingsButton())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,5 +138,7 @@ extension DeviceListView {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
DeviceListView.preview
|
||||
NavigationView {
|
||||
DeviceListView.preview
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
//
|
||||
// FilePicker.swift
|
||||
// QuickDrop
|
||||
//
|
||||
// Created by Leon Böttger on 15.08.25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
import LUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct SendPickerButton<Label: View>: View {
|
||||
var label: () -> Label
|
||||
var onResult: (_ urls: [URL]?, _ text: String?) -> Void
|
||||
|
||||
@State private var showPhotoPicker = false
|
||||
@State private var selectedPhotoItems: [PhotosPickerItem] = []
|
||||
|
||||
@State private var showFilePicker = false
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
Button("SendImages", systemImage: "photo") {
|
||||
showPhotoPicker = true
|
||||
}
|
||||
Button("SendFiles", systemImage: "folder") {
|
||||
showFilePicker = true
|
||||
}
|
||||
Button("SendClipboard", systemImage: "doc.on.clipboard") {
|
||||
if let clipboardText = UIPasteboard.general.string, !clipboardText.isEmpty {
|
||||
onResult(nil, clipboardText)
|
||||
} else {
|
||||
showAlert(title: "ClipboardEmpty", message: "ClipboardEmptyMessage")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
label()
|
||||
}
|
||||
.photosPicker(
|
||||
isPresented: $showPhotoPicker,
|
||||
selection: $selectedPhotoItems,
|
||||
maxSelectionCount: nil,
|
||||
matching: .any(of: [.images, .videos])
|
||||
)
|
||||
.onChange(of: selectedPhotoItems) { newItems in
|
||||
guard !newItems.isEmpty else { return }
|
||||
updatedPhotoSelection(content: newItems)
|
||||
}
|
||||
// File picker
|
||||
.fileImporter(isPresented: $showFilePicker,
|
||||
allowedContentTypes: [.item, .folder], // any file
|
||||
allowsMultipleSelection: true) { result in
|
||||
switch result {
|
||||
case .success(let urls):
|
||||
onResult(urls, nil)
|
||||
case .failure:
|
||||
onResult(nil, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updatedPhotoSelection(content: [PhotosPickerItem]) {
|
||||
Task {
|
||||
do {
|
||||
let urls = try await fetchContentUrls(content: content)
|
||||
onResult(urls, nil)
|
||||
selectedPhotoItems = []
|
||||
} catch {
|
||||
log("Could not fetch photo URLs: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func fetchContentUrls(content: [PhotosPickerItem]) async throws -> [URL] {
|
||||
try await withThrowingTaskGroup(of: URL?.self, returning: [URL].self) { group in
|
||||
let fileManager = FileManager.default
|
||||
|
||||
for item in content {
|
||||
group.addTask {
|
||||
if let dataUrl = try await item.loadTransferable(type: Data.self) {
|
||||
|
||||
if let contentType = item.supportedContentTypes.first {
|
||||
// Step 2: make the URL file name and a get a file extention.
|
||||
let url = fileManager.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).\(contentType.preferredFilenameExtension ?? "")")
|
||||
|
||||
do {
|
||||
// Step 3: write to temp App file directory and return in completionHandler
|
||||
try dataUrl.write(to: url)
|
||||
return url
|
||||
} catch {
|
||||
log("Faied to write data to file: \(error)")
|
||||
throw error
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// let destination = fileManager.temporaryDirectory.appendingPathComponent(dataUrl.url.lastPathComponent)
|
||||
//
|
||||
// // overwrite if file exists
|
||||
// if fileManager.fileExists(atPath: destination.path) {
|
||||
// try fileManager.removeItem(at: destination)
|
||||
// }
|
||||
//
|
||||
// log("Copying photo at \(dataUrl.url) to \(destination)")
|
||||
//
|
||||
// do {
|
||||
// try fileManager.copyItem(at: dataUrl.url, to: destination)
|
||||
// }
|
||||
// catch {
|
||||
// log("Failed to copy photo: \(error)")
|
||||
// throw error
|
||||
// }
|
||||
// return destination
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var finalUrls = [URL]()
|
||||
for try await result in group {
|
||||
if let url = result {
|
||||
finalUrls.append(url)
|
||||
}
|
||||
}
|
||||
|
||||
return finalUrls
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// File representation
|
||||
fileprivate struct DataUrl: Transferable {
|
||||
let url: URL
|
||||
|
||||
static var transferRepresentation: some TransferRepresentation {
|
||||
FileRepresentation(contentType: .data) { data in
|
||||
|
||||
log("Received data: \(data.url)")
|
||||
return SentTransferredFile(data.url)
|
||||
} importing: { received in
|
||||
|
||||
log("Importing file: \(received.file)")
|
||||
return Self(url: received.file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
SendPickerButton {
|
||||
Text("Send Files")
|
||||
.font(.headline)
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(8)
|
||||
} onResult: { urls, text in
|
||||
if let urls = urls {
|
||||
print("Selected URLs: \(urls)")
|
||||
}
|
||||
if let text = text {
|
||||
print("Clipboard text: \(text)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,8 @@ class ShareViewModel: ObservableObject, ShareExtensionDelegate {
|
||||
@Published var progressState: String? = nil
|
||||
@Published var progressValue: Double? = nil
|
||||
|
||||
private var urls: [URL] = []
|
||||
private var textToSend = "Test from QuickDrop iOS"
|
||||
var urls: [URL] = []
|
||||
var textToSend: String? = nil
|
||||
//private var errorAlertHandler = ErrorAlertHandler.shared
|
||||
|
||||
private var connectionEstablished = false
|
||||
@@ -70,8 +70,6 @@ class ShareViewModel: ObservableObject, ShareExtensionDelegate {
|
||||
}
|
||||
|
||||
func selectDevice(device: RemoteDeviceInfo) {
|
||||
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
||||
|
||||
progressValue = 0
|
||||
progressState = "Connecting".localized()
|
||||
selectedDevice = device
|
||||
|
||||
@@ -4,5 +4,9 @@
|
||||
<dict>
|
||||
<key>com.apple.developer.networking.multicast</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.leonboettger.neardrop</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -6,9 +6,19 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import LUI
|
||||
|
||||
@main
|
||||
struct QuickDropiOSApp: App {
|
||||
|
||||
#if !os(macOS)
|
||||
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
#endif
|
||||
|
||||
init() {
|
||||
LUIInit(configuration: configuration)
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
|
||||
@@ -56,7 +56,7 @@ class ShareViewController: NSViewController, ShareExtensionDelegate {
|
||||
textToSend = text
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.zipFolderAndSetUpIcon()
|
||||
self.setUpIcon()
|
||||
}
|
||||
} else {
|
||||
for attachment in attachments {
|
||||
@@ -72,7 +72,7 @@ class ShareViewController: NSViewController, ShareExtensionDelegate {
|
||||
|
||||
if self.urls.count == attachments.count {
|
||||
DispatchQueue.main.async {
|
||||
self.zipFolderAndSetUpIcon()
|
||||
self.setUpIcon()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,42 +209,7 @@ class ShareViewController: NSViewController, ShareExtensionDelegate {
|
||||
}
|
||||
|
||||
|
||||
private func zipFolderAndSetUpIcon() {
|
||||
for url in urls {
|
||||
if url.isFileURL {
|
||||
let isDirectory = UnsafeMutablePointer<ObjCBool>.allocate(capacity: 1)
|
||||
if FileManager.default.fileExists(atPath: url.path, isDirectory: isDirectory) && isDirectory.pointee.boolValue {
|
||||
do {
|
||||
let zipUrl = try createZipAtTmp(zipFilename: url.lastPathComponent, fromDirectory: url)
|
||||
|
||||
let index = urls.firstIndex(of: url)
|
||||
urls[index!] = zipUrl
|
||||
} catch {
|
||||
log("Error creating zip file: \(error)")
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
log("Canceling share request because URL \(url) is a directory")
|
||||
|
||||
AudioManager.playErrorSound()
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .critical
|
||||
|
||||
alert.messageText = "TypeNotSupported".localized()
|
||||
alert.informativeText = "TypeNotSupportedDescription".localized()
|
||||
alert.addButton(withTitle: "TypeNotSupportedButton".localized())
|
||||
|
||||
alert.beginSheetModal(for: self.view.window!) { _ in
|
||||
let cancelError = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil)
|
||||
self.extensionContext!.cancelRequest(withError: cancelError)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setUpIcon() {
|
||||
if let textToSend = textToSend {
|
||||
let maxLength = 50
|
||||
let cleanedText = textToSend.replacingOccurrences(of: "\n", with: " ").replacingOccurrences(of: "\r", with: "")
|
||||
|
||||
@@ -7,41 +7,57 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum CreateZipError: Swift.Error {
|
||||
case urlNotADirectory(URL)
|
||||
case failedToCreateZIP(Swift.Error)
|
||||
}
|
||||
|
||||
|
||||
func createZip(zipFinalURL: URL, fromDirectory directoryURL: URL) throws -> URL {
|
||||
|
||||
guard directoryURL.isDirectory else {
|
||||
throw CreateZipError.urlNotADirectory(directoryURL)
|
||||
class Zip {
|
||||
|
||||
static func createAtTemporaryDirectory(zipFilename: String, zipExtension: String = "zip", fromDirectory directoryURL: URL) throws -> URL {
|
||||
|
||||
let finalURL = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(zipFilename)
|
||||
.appendingPathExtension(zipExtension)
|
||||
|
||||
return try createAt(
|
||||
zipFinalURL: finalURL,
|
||||
fromDirectory: directoryURL
|
||||
)
|
||||
}
|
||||
|
||||
var fileManagerError: Swift.Error?
|
||||
var coordinatorError: NSError?
|
||||
let coordinator = NSFileCoordinator()
|
||||
|
||||
coordinator.coordinate(readingItemAt: directoryURL, options: .forUploading, error: &coordinatorError) { zipCreatedURL in
|
||||
do {
|
||||
// clean if file exists
|
||||
if FileManager.default.fileExists(atPath: zipFinalURL.path) {
|
||||
try FileManager.default.removeItem(at: zipFinalURL)
|
||||
}
|
||||
|
||||
try FileManager.default.moveItem(at: zipCreatedURL, to: zipFinalURL)
|
||||
|
||||
} catch {
|
||||
fileManagerError = error
|
||||
|
||||
static func createAt(zipFinalURL: URL, fromDirectory directoryURL: URL) throws -> URL {
|
||||
|
||||
guard directoryURL.isDirectory else {
|
||||
throw CreateZipError.urlNotADirectory(directoryURL)
|
||||
}
|
||||
|
||||
var fileManagerError: Swift.Error?
|
||||
var coordinatorError: NSError?
|
||||
let coordinator = NSFileCoordinator()
|
||||
|
||||
coordinator.coordinate(readingItemAt: directoryURL, options: .forUploading, error: &coordinatorError) { zipCreatedURL in
|
||||
do {
|
||||
// clean if file exists
|
||||
if FileManager.default.fileExists(atPath: zipFinalURL.path) {
|
||||
try FileManager.default.removeItem(at: zipFinalURL)
|
||||
}
|
||||
|
||||
try FileManager.default.moveItem(at: zipCreatedURL, to: zipFinalURL)
|
||||
|
||||
} catch {
|
||||
fileManagerError = error
|
||||
}
|
||||
}
|
||||
|
||||
if let error = coordinatorError ?? fileManagerError {
|
||||
throw CreateZipError.failedToCreateZIP(error)
|
||||
}
|
||||
|
||||
return zipFinalURL
|
||||
}
|
||||
|
||||
if let error = coordinatorError ?? fileManagerError {
|
||||
throw CreateZipError.failedToCreateZIP(error)
|
||||
}
|
||||
|
||||
return zipFinalURL
|
||||
enum CreateZipError: Swift.Error {
|
||||
case urlNotADirectory(URL)
|
||||
case failedToCreateZIP(Swift.Error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,16 +66,3 @@ extension URL {
|
||||
(try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func createZipAtTmp(zipFilename: String, zipExtension: String = "zip", fromDirectory directoryURL: URL) throws -> URL {
|
||||
|
||||
let finalURL = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(zipFilename)
|
||||
.appendingPathExtension(zipExtension)
|
||||
|
||||
return try createZip(
|
||||
zipFinalURL: finalURL,
|
||||
fromDirectory: directoryURL
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user