mirror of
https://github.com/leonboe1/QuickDrop.git
synced 2026-11-11 02:51:11 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fd07e9326 | ||
|
|
70a6b5ff3e | ||
|
|
9536854fa2 | ||
|
|
39ea48fc80 | ||
|
|
4e73fced74 |
No files matched your search
@@ -22,6 +22,7 @@ import UIKit
|
|||||||
class InboundNearbyConnection: NearbyConnection {
|
class InboundNearbyConnection: NearbyConnection {
|
||||||
|
|
||||||
private var filesToBeReceived: [Int64: InternalFileInfo] = [:]
|
private var filesToBeReceived: [Int64: InternalFileInfo] = [:]
|
||||||
|
private var downloadedFiles = [URL]()
|
||||||
private var currentState: State = .initial
|
private var currentState: State = .initial
|
||||||
private var cipherCommitment: Data?
|
private var cipherCommitment: Data?
|
||||||
private var textPayloadID: Int64 = 0
|
private var textPayloadID: Int64 = 0
|
||||||
@@ -51,8 +52,17 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.delegate?.connectionWasTerminated(connection: self, error: self.lastError)
|
self.delegate?.connectionWasTerminated(connection: self, error: self.lastError)
|
||||||
|
|
||||||
SaveFilesManager.shared.movePendingFilesToTarget()
|
if !self.downloadedFiles.isEmpty {
|
||||||
SaveFilesManager.shared.stopAccessingSecurityScopedResource()
|
|
||||||
|
#if os(macOS)
|
||||||
|
if Settings.shared.openFinderAfterReceiving {
|
||||||
|
log("[SaveFilesManager] Opening \(self.downloadedFiles.count) file(s) in Finder.")
|
||||||
|
NSWorkspace.shared.activateFileViewerSelecting(self.downloadedFiles)
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
FileManager.default.openDocumentFolder()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +179,8 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
fileInfo.progress?.unpublish()
|
fileInfo.progress?.unpublish()
|
||||||
#endif
|
#endif
|
||||||
SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL)
|
downloadedFiles.append(fileInfo.destinationURL)
|
||||||
|
EXIFUtils.applyEXIFTimestamps(at: fileInfo.destinationURL)
|
||||||
|
|
||||||
filesToBeReceived.removeValue(forKey: id)
|
filesToBeReceived.removeValue(forKey: id)
|
||||||
|
|
||||||
@@ -222,7 +233,8 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
fileInfo.progress?.unpublish()
|
fileInfo.progress?.unpublish()
|
||||||
#endif
|
#endif
|
||||||
filesToBeReceived.removeValue(forKey: id)
|
filesToBeReceived.removeValue(forKey: id)
|
||||||
SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL)
|
downloadedFiles.append(fileInfo.destinationURL)
|
||||||
|
EXIFUtils.applyEXIFTimestamps(at: fileInfo.destinationURL)
|
||||||
|
|
||||||
if filesToBeReceived.isEmpty {
|
if filesToBeReceived.isEmpty {
|
||||||
log("[InboundNearbyConnection \(self.id)] Received file payload. Disconnecting...")
|
log("[InboundNearbyConnection \(self.id)] Received file payload. Disconnecting...")
|
||||||
@@ -495,7 +507,7 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
currentState = .waitingForUserConsent
|
currentState = .waitingForUserConsent
|
||||||
|
|
||||||
if frame.v1.introduction.fileMetadata.count > 0 && frame.v1.introduction.textMetadata.isEmpty {
|
if frame.v1.introduction.fileMetadata.count > 0 && frame.v1.introduction.textMetadata.isEmpty {
|
||||||
let saveDirectory = SaveFilesManager.shared.getSaveDirectory()
|
let saveDirectory = NearbyConnectionManager.shared.getSaveDirectory()
|
||||||
var usedDestinations = Set<URL>()
|
var usedDestinations = Set<URL>()
|
||||||
|
|
||||||
for file in frame.v1.introduction.fileMetadata {
|
for file in frame.v1.introduction.fileMetadata {
|
||||||
@@ -515,23 +527,16 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
checkIfCanProceed(metadata: metadata)
|
checkIfCanProceed(metadata: metadata)
|
||||||
}
|
}
|
||||||
else if let textMetadata = frame.v1.introduction.textMetadata.first {
|
else if let textMetadata = frame.v1.introduction.textMetadata.first {
|
||||||
|
let isURL = textMetadata.type == .url
|
||||||
|
|
||||||
if textMetadata.type == .url || textMetadata.type == .text {
|
let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: textMetadata.textTitle, transferType: isURL ? .url : .text, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil)
|
||||||
|
textPayloadID = textMetadata.payloadID
|
||||||
let isClipboardText = textMetadata.type == .text
|
|
||||||
|
if !isURL {
|
||||||
let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: textMetadata.textTitle, transferType: isClipboardText ? .text : .url, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil)
|
isPlainTextTransfer = true
|
||||||
textPayloadID = textMetadata.payloadID
|
|
||||||
|
|
||||||
if isClipboardText {
|
|
||||||
isPlainTextTransfer = true
|
|
||||||
}
|
|
||||||
|
|
||||||
checkIfCanProceed(metadata: metadata)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
rejectDueToUnsupportedFileType(frame)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkIfCanProceed(metadata: metadata)
|
||||||
} else {
|
} else {
|
||||||
rejectDueToUnsupportedFileType(frame)
|
rejectDueToUnsupportedFileType(frame)
|
||||||
}
|
}
|
||||||
@@ -584,17 +589,14 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let storeInTemp = isFileTransferRestricted()
|
if isFileTransferRestricted() {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] File transfer restrictions detected.")
|
||||||
if storeInTemp {
|
|
||||||
log("[InboundNearbyConnection \(self.id)] File transfer restrictions detected, storing files in temporary directory.")
|
|
||||||
delegate?.showPlusScreen()
|
delegate?.showPlusScreen()
|
||||||
|
rejectTransfer()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
if storeInTemp {
|
|
||||||
try FileManager.default.createDirectory(at: SaveFilesManager.shared.tempDirectory, withIntermediateDirectories: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if filesToBeReceived.count >= 1 {
|
if filesToBeReceived.count >= 1 {
|
||||||
// Show progress bar for file transfers immediately
|
// Show progress bar for file transfers immediately
|
||||||
@@ -602,7 +604,7 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (id, file) in filesToBeReceived {
|
for (id, file) in filesToBeReceived {
|
||||||
let targetURL = storeInTemp ? SaveFilesManager.shared.tempDirectory.appendingPathComponent(file.destinationURL.lastPathComponent) : file.destinationURL
|
let targetURL = file.destinationURL
|
||||||
|
|
||||||
FileManager.default.createFile(atPath: targetURL.path, contents: nil)
|
FileManager.default.createFile(atPath: targetURL.path, contents: nil)
|
||||||
let handle = try FileHandle(forWritingTo: targetURL)
|
let handle = try FileHandle(forWritingTo: targetURL)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
private var qrCodeNameEncryptionKey: SymmetricKey?
|
private var qrCodeNameEncryptionKey: SymmetricKey?
|
||||||
private let hasConnectionMonitor = NWPathMonitor()
|
private let hasConnectionMonitor = NWPathMonitor()
|
||||||
private let connectionMonitorQueue = DispatchQueue(label: "NetworkConnectionMonitorQueue")
|
private let connectionMonitorQueue = DispatchQueue(label: "NetworkConnectionMonitorQueue")
|
||||||
|
private var securityScopeUrl: URL?
|
||||||
|
|
||||||
public let deviceInfo: EndpointInfo
|
public let deviceInfo: EndpointInfo
|
||||||
public let endpointID: [UInt8] = generateEndpointID()
|
public let endpointID: [UInt8] = generateEndpointID()
|
||||||
@@ -92,6 +93,28 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasConnectionMonitor.start(queue: connectionMonitorQueue)
|
hasConnectionMonitor.start(queue: connectionMonitorQueue)
|
||||||
|
|
||||||
|
|
||||||
|
// remove old temp directory
|
||||||
|
let tempPath = FileManager.default.temporaryDirectory
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
|
||||||
|
do {
|
||||||
|
let contents = try fileManager.contentsOfDirectory(at: tempPath, includingPropertiesForKeys: nil)
|
||||||
|
|
||||||
|
var didSomething = false
|
||||||
|
|
||||||
|
for item in contents {
|
||||||
|
didSomething = true
|
||||||
|
try fileManager.removeItem(at: item)
|
||||||
|
}
|
||||||
|
|
||||||
|
if didSomething {
|
||||||
|
log("[SaveFilesManager] Temporary directory cleared.")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
log("[SaveFilesManager] Failed to list contents of temp directory: \(error)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -208,6 +231,11 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?) {
|
func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?) {
|
||||||
incomingConnections.removeValue(forKey: connection.id)
|
incomingConnections.removeValue(forKey: connection.id)
|
||||||
|
|
||||||
|
// If there are no more incoming connections, we can stop accessing the save directory
|
||||||
|
if incomingConnections.isEmpty {
|
||||||
|
self.stopAccessingSaveDirectory()
|
||||||
|
}
|
||||||
|
|
||||||
if !connection.wasUserRejected {
|
if !connection.wasUserRejected {
|
||||||
inboundAppDelegates.forEach { delegate in
|
inboundAppDelegates.forEach { delegate in
|
||||||
delegate.connectionWasTerminated(connectionID: connection.id, from: connection.remoteDeviceInfo ?? RemoteDeviceInfo(name: "UnknownDevice".localized(), type: .unknown), wasPlainTextTransfer: connection.isPlainTextTransfer, error: error)
|
delegate.connectionWasTerminated(connectionID: connection.id, from: connection.remoteDeviceInfo ?? RemoteDeviceInfo(name: "UnknownDevice".localized(), type: .unknown), wasPlainTextTransfer: connection.isPlainTextTransfer, error: error)
|
||||||
@@ -492,7 +520,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
outgoingTransfers[deviceID] = transfer
|
outgoingTransfers[deviceID] = transfer
|
||||||
conn.start()
|
conn.start()
|
||||||
} catch {
|
} catch {
|
||||||
log("[NearbyConnectionManager] Error zipping URLs: \(error)")
|
log("[NearbyConnectionManager] Error storing URLs: \(error)")
|
||||||
outboundAppDelegates.forEach { delegate in
|
outboundAppDelegates.forEach { delegate in
|
||||||
delegate.connectionFailed(error: error)
|
delegate.connectionFailed(error: error)
|
||||||
}
|
}
|
||||||
@@ -595,6 +623,68 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -- MARK: - Inbound Connection Data Store
|
||||||
|
|
||||||
|
public func getSaveDirectory() -> URL {
|
||||||
|
|
||||||
|
// Not supported on iOS
|
||||||
|
#if os(macOS)
|
||||||
|
if let securityScopeUrl = securityScopeUrl {
|
||||||
|
log("[SaveFilesManager] Using existing security scope URL: \(securityScopeUrl)")
|
||||||
|
return securityScopeUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
if let bookmarkData = Settings.shared.saveFolderBookmark {
|
||||||
|
var isStale = false
|
||||||
|
|
||||||
|
do {
|
||||||
|
let url = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
|
||||||
|
|
||||||
|
if !isStale {
|
||||||
|
if url.startAccessingSecurityScopedResource() {
|
||||||
|
log("[SaveFilesManager] Successfully accessed security scoped resource: \(url)")
|
||||||
|
|
||||||
|
securityScopeUrl = url
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log("[SaveFilesManager] Bookmark is stale, using default downloads folder.")
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
log("[SaveFilesManager] Failed to resolve bookmark: \(error), using default downloads folder.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
return try FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: true).resolvingSymlinksInPath()
|
||||||
|
} catch {
|
||||||
|
fatalError("[SaveFilesManager] Failed to get downloads directory: \(error)")
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// Return the documents directory for iOS
|
||||||
|
do {
|
||||||
|
return try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).resolvingSymlinksInPath()
|
||||||
|
} catch {
|
||||||
|
fatalError("Failed to get documents directory: \(error)")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func stopAccessingSaveDirectory() {
|
||||||
|
// Clean up security scoped resource access
|
||||||
|
guard let url = securityScopeUrl else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log("[SaveFilesManager] Stopping access to security scoped resource: \(url)")
|
||||||
|
url.stopAccessingSecurityScopedResource()
|
||||||
|
securityScopeUrl = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// -- MARK: - Internal Data Model
|
// -- MARK: - Internal Data Model
|
||||||
|
|
||||||
private struct FoundServiceInfo {
|
private struct FoundServiceInfo {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Contains information about the app group used to share data between main app and widget
|
/// Contains information about the app group used to share data between main app and extensions
|
||||||
struct AppGroup {
|
struct AppGroup {
|
||||||
|
|
||||||
/// Identifier of the app group
|
/// Identifier of the app group
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
//
|
||||||
|
// EXIFUtils.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 10.11.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import ImageIO
|
||||||
|
|
||||||
|
struct EXIFUtils {
|
||||||
|
|
||||||
|
/// Applies EXIF timestamps to a given file or directory.
|
||||||
|
static func applyEXIFTimestamps(at url: URL) {
|
||||||
|
let fm = FileManager.default
|
||||||
|
var isDir: ObjCBool = false
|
||||||
|
guard fm.fileExists(atPath: url.path, isDirectory: &isDir) else { return }
|
||||||
|
|
||||||
|
if isDir.boolValue {
|
||||||
|
// It's a directory — enumerate files
|
||||||
|
if let enumerator = fm.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey]) {
|
||||||
|
for case let fileURL as URL in enumerator {
|
||||||
|
guard (try? fileURL.resourceValues(forKeys: [.isRegularFileKey]))?.isRegularFile == true else { continue }
|
||||||
|
applyEXIFTimestamp(to: fileURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// It's a single file
|
||||||
|
applyEXIFTimestamp(to: url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Applies EXIF timestamp (if available) to a single file
|
||||||
|
private static func applyEXIFTimestamp(to fileURL: URL) {
|
||||||
|
guard let exifDate = exifOriginalDate(from: fileURL) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the "creation date"
|
||||||
|
try? FileManager.default.setAttributes([.creationDate: exifDate], ofItemAtPath: fileURL.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Returns the EXIF original date of an image file, adjusted to the current time zone.
|
||||||
|
private static func exifOriginalDate(from url: URL) -> Date? {
|
||||||
|
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
|
||||||
|
let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
|
||||||
|
let exif = properties[kCGImagePropertyExifDictionary] as? [CFString: Any],
|
||||||
|
let dateString = exif[kCGImagePropertyExifDateTimeOriginal] as? String
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "yyyy:MM:dd HH:mm:ss"
|
||||||
|
formatter.timeZone = TimeZone.current // Use the current device time zone
|
||||||
|
return formatter.date(from: dateString)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// FileManager+DocumentsOpener.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 10.11.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import LUI
|
||||||
|
|
||||||
|
extension FileManager {
|
||||||
|
public func openDocumentFolder() {
|
||||||
|
#if !EXTENSION
|
||||||
|
let documentsUrl = self.urls(for: .documentDirectory, in: .userDomainMask).first
|
||||||
|
|
||||||
|
if let documentsUrl = documentsUrl, let sharedUrl = URL(string: "shareddocuments://\(documentsUrl.path)"), UIApplication.shared.canOpenURL(sharedUrl) {
|
||||||
|
UIApplication.shared.open(sharedUrl, options: [:])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
showAlert(title: "CouldNotOpenDocumentsFolder", message: "CouldNotOpenDocumentsFolderDescription")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -111,14 +111,8 @@ extension String {
|
|||||||
|
|
||||||
|
|
||||||
public func fullVersion() -> Bool {
|
public func fullVersion() -> Bool {
|
||||||
|
|
||||||
#if os(macOS)
|
|
||||||
// Enable full functionality if app distributed directly
|
// Enable full functionality if app distributed directly
|
||||||
return Settings.shared.gotPlus || DistributionDetector.isDirectDistributionEnabled
|
return Settings.shared.gotPlus || DistributionDetector.isDirectDistributionEnabled
|
||||||
#else
|
|
||||||
// iOS
|
|
||||||
return true
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,259 +0,0 @@
|
|||||||
//
|
|
||||||
// SaveFilesManager.swift
|
|
||||||
// QuickDrop
|
|
||||||
//
|
|
||||||
// Created by Leon Böttger on 26.07.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import ImageIO
|
|
||||||
#if os(macOS)
|
|
||||||
import AppKit
|
|
||||||
#else
|
|
||||||
import LUI
|
|
||||||
import UIKit
|
|
||||||
#endif
|
|
||||||
|
|
||||||
public class SaveFilesManager {
|
|
||||||
|
|
||||||
private init() {
|
|
||||||
// remove old temp directory
|
|
||||||
|
|
||||||
let tempPath = FileManager.default.temporaryDirectory
|
|
||||||
let fileManager = FileManager.default
|
|
||||||
|
|
||||||
do {
|
|
||||||
let contents = try fileManager.contentsOfDirectory(at: tempPath, includingPropertiesForKeys: nil)
|
|
||||||
|
|
||||||
var didSomething = false
|
|
||||||
|
|
||||||
for item in contents {
|
|
||||||
didSomething = true
|
|
||||||
try fileManager.removeItem(at: item)
|
|
||||||
}
|
|
||||||
|
|
||||||
if didSomething {
|
|
||||||
log("[SaveFilesManager] Temporary directory cleared.")
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
log("[SaveFilesManager] Failed to list contents of temp directory: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static let shared = SaveFilesManager()
|
|
||||||
|
|
||||||
let tempDirectory: URL = FileManager.default.temporaryDirectory.appendingPathComponent("Pending")
|
|
||||||
private var securityScopeUrl: URL?
|
|
||||||
|
|
||||||
private var filesFinishedDownloading = [URL]()
|
|
||||||
private var filesFinishedDownloadingSinceLastRun = [URL]()
|
|
||||||
|
|
||||||
public func registerFileFinishedDownloading(_ fileURL: URL) {
|
|
||||||
filesFinishedDownloading.append(fileURL)
|
|
||||||
filesFinishedDownloadingSinceLastRun.append(fileURL)
|
|
||||||
|
|
||||||
// read out EXIF timestamp and manually set file creation date to it
|
|
||||||
applyEXIFTimestamps(at: fileURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public func movePendingFilesToTarget() {
|
|
||||||
|
|
||||||
if fullVersion() {
|
|
||||||
do {
|
|
||||||
let fileManager = FileManager.default
|
|
||||||
let files = try fileManager.contentsOfDirectory(at: tempDirectory, includingPropertiesForKeys: nil)
|
|
||||||
|
|
||||||
let target = getSaveDirectory()
|
|
||||||
var loggedExecution = false
|
|
||||||
|
|
||||||
for file in files {
|
|
||||||
|
|
||||||
if !loggedExecution {
|
|
||||||
log("[SaveFilesManager] Moving pending files to target directory")
|
|
||||||
loggedExecution = true
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileName = file.lastPathComponent
|
|
||||||
let destinationURL = target.appendingPathComponent(fileName)
|
|
||||||
|
|
||||||
if !filesFinishedDownloading.contains(destinationURL) {
|
|
||||||
log("[SaveFilesManager] File \(file) not finished downloading, skipping")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log("[SaveFilesManager] Moving file: \(file.lastPathComponent) to \(destinationURL.lastPathComponent)")
|
|
||||||
|
|
||||||
do {
|
|
||||||
try fileManager.copyItem(at: file, to: destinationURL)
|
|
||||||
|
|
||||||
let progress = Progress()
|
|
||||||
progress.fileURL = destinationURL
|
|
||||||
progress.totalUnitCount = 10
|
|
||||||
progress.kind = .file
|
|
||||||
progress.isPausable = false
|
|
||||||
#if os(macOS)
|
|
||||||
progress.publish()
|
|
||||||
#endif
|
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
||||||
progress.completedUnitCount = 10
|
|
||||||
#if os(macOS)
|
|
||||||
progress.unpublish()
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
try fileManager.removeItem(at: file)
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
log("[SaveFilesManager] Error moving file: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log("[SaveFilesManager] Moved all pending files to target directory")
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
// Pending directory doesn't exist or is empty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !filesFinishedDownloadingSinceLastRun.isEmpty && !isFileTransferRestricted() {
|
|
||||||
|
|
||||||
#if os(macOS)
|
|
||||||
if Settings.shared.openFinderAfterReceiving {
|
|
||||||
log("[SaveFilesManager] Opening \(filesFinishedDownloadingSinceLastRun.count) file(s) in Finder.")
|
|
||||||
NSWorkspace.shared.activateFileViewerSelecting(filesFinishedDownloadingSinceLastRun)
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
openDownloadedFilesFolder()
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Clear the list of finished files
|
|
||||||
filesFinishedDownloadingSinceLastRun.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
stopAccessingSecurityScopedResource()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#if !os(macOS)
|
|
||||||
public func openDownloadedFilesFolder() {
|
|
||||||
#if !EXTENSION
|
|
||||||
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
|
|
||||||
|
|
||||||
if let documentsUrl = documentsUrl, let sharedUrl = URL(string: "shareddocuments://\(documentsUrl.path)"), UIApplication.shared.canOpenURL(sharedUrl) {
|
|
||||||
UIApplication.shared.open(sharedUrl, options: [:])
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
showAlert(title: "CouldNotOpenDocumentsFolder", message: "CouldNotOpenDocumentsFolderDescription")
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
public func stopAccessingSecurityScopedResource() {
|
|
||||||
guard let url = securityScopeUrl else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log("[SaveFilesManager] Stopping access to security scoped resource: \(url)")
|
|
||||||
url.stopAccessingSecurityScopedResource()
|
|
||||||
securityScopeUrl = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public func getSaveDirectory() -> URL {
|
|
||||||
|
|
||||||
// Not supported on iOS
|
|
||||||
#if os(macOS)
|
|
||||||
if let securityScopeUrl = securityScopeUrl {
|
|
||||||
log("[SaveFilesManager] Using existing security scope URL: \(securityScopeUrl)")
|
|
||||||
return securityScopeUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
if let bookmarkData = Settings.shared.saveFolderBookmark {
|
|
||||||
var isStale = false
|
|
||||||
|
|
||||||
do {
|
|
||||||
let url = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
|
|
||||||
|
|
||||||
if !isStale {
|
|
||||||
if url.startAccessingSecurityScopedResource() {
|
|
||||||
log("[SaveFilesManager] Successfully accessed security scoped resource: \(url)")
|
|
||||||
|
|
||||||
securityScopeUrl = url
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log("[SaveFilesManager] Bookmark is stale, using default downloads folder.")
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
log("[SaveFilesManager] Failed to resolve bookmark: \(error), using default downloads folder.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
return try FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: true).resolvingSymlinksInPath()
|
|
||||||
} catch {
|
|
||||||
fatalError("[SaveFilesManager] Failed to get downloads directory: \(error)")
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
// Return the documents directory for iOS
|
|
||||||
do {
|
|
||||||
return try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).resolvingSymlinksInPath()
|
|
||||||
} catch {
|
|
||||||
fatalError("Failed to get documents directory: \(error)")
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Returns the EXIF original date of an image file, adjusted to the current time zone.
|
|
||||||
private func exifOriginalDate(from url: URL) -> Date? {
|
|
||||||
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
|
|
||||||
let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
|
|
||||||
let exif = properties[kCGImagePropertyExifDictionary] as? [CFString: Any],
|
|
||||||
let dateString = exif[kCGImagePropertyExifDateTimeOriginal] as? String
|
|
||||||
else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateFormat = "yyyy:MM:dd HH:mm:ss"
|
|
||||||
formatter.timeZone = TimeZone.current // Use the current device time zone
|
|
||||||
return formatter.date(from: dateString)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Applies EXIF timestamps to a given file or directory.
|
|
||||||
private func applyEXIFTimestamps(at url: URL) {
|
|
||||||
let fm = FileManager.default
|
|
||||||
var isDir: ObjCBool = false
|
|
||||||
guard fm.fileExists(atPath: url.path, isDirectory: &isDir) else { return }
|
|
||||||
|
|
||||||
if isDir.boolValue {
|
|
||||||
// It's a directory — enumerate files
|
|
||||||
if let enumerator = fm.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey]) {
|
|
||||||
for case let fileURL as URL in enumerator {
|
|
||||||
guard (try? fileURL.resourceValues(forKeys: [.isRegularFileKey]))?.isRegularFile == true else { continue }
|
|
||||||
applyEXIFTimestamp(to: fileURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// It's a single file
|
|
||||||
applyEXIFTimestamp(to: url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Applies EXIF timestamp (if available) to a single file
|
|
||||||
private func applyEXIFTimestamp(to fileURL: URL) {
|
|
||||||
guard let exifDate = exifOriginalDate(from: fileURL) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the "creation date"
|
|
||||||
try? FileManager.default.setAttributes([.creationDate: exifDate], ofItemAtPath: fileURL.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -45,16 +45,21 @@
|
|||||||
EB16B77A2E59BAA1007D9792 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B7762E59BA9F007D9792 /* AppGroup.swift */; };
|
EB16B77A2E59BAA1007D9792 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B7762E59BA9F007D9792 /* AppGroup.swift */; };
|
||||||
EB2F9BFE2DA05F48004010A2 /* BezelNotification in Frameworks */ = {isa = PBXBuildFile; productRef = EB2F9BFD2DA05F48004010A2 /* BezelNotification */; };
|
EB2F9BFE2DA05F48004010A2 /* BezelNotification in Frameworks */ = {isa = PBXBuildFile; productRef = EB2F9BFD2DA05F48004010A2 /* BezelNotification */; };
|
||||||
EB3643602D73A2A2009CDFF8 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB36435F2D73A29F009CDFF8 /* AudioManager.swift */; };
|
EB3643602D73A2A2009CDFF8 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB36435F2D73A29F009CDFF8 /* AudioManager.swift */; };
|
||||||
|
EB4F9D1D2EC113EA00A74A0F /* DistributionDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB704A812E4A7BAE008E608F /* DistributionDetector.swift */; };
|
||||||
|
EB4F9D1F2EC26C8100A74A0F /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; };
|
||||||
|
EB4F9D202EC26C8100A74A0F /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; };
|
||||||
|
EB4F9D212EC26C8100A74A0F /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; };
|
||||||
|
EB4F9D222EC26C8100A74A0F /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; };
|
||||||
|
EB4F9D252EC27CFB00A74A0F /* FileManager+DocumentsOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */; };
|
||||||
|
EB4F9D272EC27CFB00A74A0F /* FileManager+DocumentsOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */; };
|
||||||
EB5C36932E34EBBD0052EFD7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */; };
|
EB5C36932E34EBBD0052EFD7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */; };
|
||||||
EB5C36942E34EBBD0052EFD7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */; };
|
EB5C36942E34EBBD0052EFD7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */; };
|
||||||
EB5C36E42E34EC0E0052EFD7 /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36E32E34EC0E0052EFD7 /* License.txt */; };
|
EB5C36E42E34EC0E0052EFD7 /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36E32E34EC0E0052EFD7 /* License.txt */; };
|
||||||
EB5C36E52E34EC0E0052EFD7 /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36E32E34EC0E0052EFD7 /* License.txt */; };
|
EB5C36E52E34EC0E0052EFD7 /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36E32E34EC0E0052EFD7 /* License.txt */; };
|
||||||
EB5C37B82E377A6F0052EFD7 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; };
|
EB5C37B82E377A6F0052EFD7 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; };
|
||||||
EB5C37B92E377A6F0052EFD7 /* SaveFilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */; };
|
|
||||||
EB5C37BA2E377A6F0052EFD7 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; };
|
EB5C37BA2E377A6F0052EFD7 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; };
|
||||||
EB5C37BC2E377A700052EFD7 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; };
|
EB5C37BC2E377A700052EFD7 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; };
|
||||||
EB5C37BD2E377A700052EFD7 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; };
|
EB5C37BD2E377A700052EFD7 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; };
|
||||||
EB5C37BE2E377A700052EFD7 /* SaveFilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */; };
|
|
||||||
EB5C37BF2E377A780052EFD7 /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
EB5C37BF2E377A780052EFD7 /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
||||||
EB5C37C02E377A780052EFD7 /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
|
EB5C37C02E377A780052EFD7 /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
|
||||||
EB5C37C12E377A780052EFD7 /* offline_wire_formats.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2B29E18CB500A442DA /* offline_wire_formats.pb.swift */; };
|
EB5C37C12E377A780052EFD7 /* offline_wire_formats.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2B29E18CB500A442DA /* offline_wire_formats.pb.swift */; };
|
||||||
@@ -85,7 +90,6 @@
|
|||||||
EB5C37F02E377C3F0052EFD7 /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37EF2E377C3F0052EFD7 /* SwiftECC */; };
|
EB5C37F02E377C3F0052EFD7 /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37EF2E377C3F0052EFD7 /* SwiftECC */; };
|
||||||
EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
||||||
EB5C37F22E377CE70052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
|
EB5C37F22E377CE70052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
|
||||||
EB5C37F32E377CE70052EFD7 /* SaveFilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */; };
|
|
||||||
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
||||||
EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
|
EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
|
||||||
EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2A29E18CB500A442DA /* device_to_device_messages.pb.swift */; };
|
EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2A29E18CB500A442DA /* device_to_device_messages.pb.swift */; };
|
||||||
@@ -156,7 +160,6 @@
|
|||||||
EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
||||||
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
||||||
EB950B542E520436002D68FA /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
|
EB950B542E520436002D68FA /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
|
||||||
EB950B552E520436002D68FA /* SaveFilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */; };
|
|
||||||
EB950B562E520436002D68FA /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
|
EB950B562E520436002D68FA /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
|
||||||
EB950B572E520591002D68FA /* ErrorAlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */; };
|
EB950B572E520591002D68FA /* ErrorAlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */; };
|
||||||
EB950B582E5205B4002D68FA /* Zip.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBCC27F82D80C9F70079C478 /* Zip.swift */; };
|
EB950B582E5205B4002D68FA /* Zip.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBCC27F82D80C9F70079C478 /* Zip.swift */; };
|
||||||
@@ -327,10 +330,11 @@
|
|||||||
EB2B41E32DC63AC80070FC39 /* SleepManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SleepManager.swift; sourceTree = "<group>"; };
|
EB2B41E32DC63AC80070FC39 /* SleepManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SleepManager.swift; sourceTree = "<group>"; };
|
||||||
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
|
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
|
||||||
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = "<group>"; };
|
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = "<group>"; };
|
||||||
|
EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXIFUtils.swift; sourceTree = "<group>"; };
|
||||||
|
EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+DocumentsOpener.swift"; sourceTree = "<group>"; };
|
||||||
EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||||
EB5C36952E34EBBD0052EFD7 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/ShareViewController.xcstrings; sourceTree = "<group>"; };
|
EB5C36952E34EBBD0052EFD7 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/ShareViewController.xcstrings; sourceTree = "<group>"; };
|
||||||
EB5C36E32E34EC0E0052EFD7 /* License.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = License.txt; sourceTree = "<group>"; };
|
EB5C36E32E34EC0E0052EFD7 /* License.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = License.txt; sourceTree = "<group>"; };
|
||||||
EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveFilesManager.swift; sourceTree = "<group>"; };
|
|
||||||
EB5C36E82E3546100052EFD7 /* DataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataModel.swift; sourceTree = "<group>"; };
|
EB5C36E82E3546100052EFD7 /* DataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataModel.swift; sourceTree = "<group>"; };
|
||||||
EB5C37E22E377C2E0052EFD7 /* QuickDropiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickDropiOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
EB5C37E22E377C2E0052EFD7 /* QuickDropiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickDropiOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
EB5C38382E38C1F60052EFD7 /* justMaybe.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = justMaybe.mp3; sourceTree = "<group>"; };
|
EB5C38382E38C1F60052EFD7 /* justMaybe.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = justMaybe.mp3; sourceTree = "<group>"; };
|
||||||
@@ -534,8 +538,9 @@
|
|||||||
EBCC27F82D80C9F70079C478 /* Zip.swift */,
|
EBCC27F82D80C9F70079C478 /* Zip.swift */,
|
||||||
EB16B7762E59BA9F007D9792 /* AppGroup.swift */,
|
EB16B7762E59BA9F007D9792 /* AppGroup.swift */,
|
||||||
EB886E512E4E19390052B7D9 /* HKDF.swift */,
|
EB886E512E4E19390052B7D9 /* HKDF.swift */,
|
||||||
|
EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */,
|
||||||
|
EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */,
|
||||||
EB7CD1442D6FB0C100CB0CB5 /* Logger.swift */,
|
EB7CD1442D6FB0C100CB0CB5 /* Logger.swift */,
|
||||||
EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */,
|
|
||||||
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */,
|
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */,
|
||||||
);
|
);
|
||||||
path = Util;
|
path = Util;
|
||||||
@@ -932,6 +937,7 @@
|
|||||||
EB5C37C72E377A780052EFD7 /* wire_format.pb.swift in Sources */,
|
EB5C37C72E377A780052EFD7 /* wire_format.pb.swift in Sources */,
|
||||||
EB5C37C82E377A780052EFD7 /* ukey.pb.swift in Sources */,
|
EB5C37C82E377A780052EFD7 /* ukey.pb.swift in Sources */,
|
||||||
EB5C37C92E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */,
|
EB5C37C92E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */,
|
||||||
|
EB4F9D222EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
|
||||||
EB950B742E520957002D68FA /* Attachments.swift in Sources */,
|
EB950B742E520957002D68FA /* Attachments.swift in Sources */,
|
||||||
EB1325D32E701D8E00FD749C /* Settings.swift in Sources */,
|
EB1325D32E701D8E00FD749C /* Settings.swift in Sources */,
|
||||||
EB16B7782E59BAA1007D9792 /* AppGroup.swift in Sources */,
|
EB16B7782E59BAA1007D9792 /* AppGroup.swift in Sources */,
|
||||||
@@ -952,7 +958,6 @@
|
|||||||
EB1325EB2E7025D900FD749C /* ApIsolationTester.swift in Sources */,
|
EB1325EB2E7025D900FD749C /* ApIsolationTester.swift in Sources */,
|
||||||
EB1325EC2E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */,
|
EB1325EC2E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */,
|
||||||
EB1325ED2E7025D900FD749C /* ApIsolation6Tester.swift in Sources */,
|
EB1325ED2E7025D900FD749C /* ApIsolation6Tester.swift in Sources */,
|
||||||
EB5C37BE2E377A700052EFD7 /* SaveFilesManager.swift in Sources */,
|
|
||||||
EB9925822E45D53E00357055 /* sharing_enums.pb.swift in Sources */,
|
EB9925822E45D53E00357055 /* sharing_enums.pb.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -966,7 +971,6 @@
|
|||||||
EB5C38412E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */,
|
EB5C38412E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */,
|
||||||
EB5C37B82E377A6F0052EFD7 /* SleepManager.swift in Sources */,
|
EB5C37B82E377A6F0052EFD7 /* SleepManager.swift in Sources */,
|
||||||
EB869DB52E92AA3A009AAAD4 /* QuickDropToastView.swift in Sources */,
|
EB869DB52E92AA3A009AAAD4 /* QuickDropToastView.swift in Sources */,
|
||||||
EB5C37B92E377A6F0052EFD7 /* SaveFilesManager.swift in Sources */,
|
|
||||||
EB5C37BA2E377A6F0052EFD7 /* Helpers.swift in Sources */,
|
EB5C37BA2E377A6F0052EFD7 /* Helpers.swift in Sources */,
|
||||||
EB05DFBE2E59FC8900EF567A /* TrustedDevicesView.swift in Sources */,
|
EB05DFBE2E59FC8900EF567A /* TrustedDevicesView.swift in Sources */,
|
||||||
EB16B6182E58B4C8007D9792 /* TrustStore.swift in Sources */,
|
EB16B6182E58B4C8007D9792 /* TrustStore.swift in Sources */,
|
||||||
@@ -994,6 +998,7 @@
|
|||||||
EB96ED8C2D7F7E7B004B3FF1 /* SmallSheetView.swift in Sources */,
|
EB96ED8C2D7F7E7B004B3FF1 /* SmallSheetView.swift in Sources */,
|
||||||
EB886E682E4F2F800052B7D9 /* License.swift in Sources */,
|
EB886E682E4F2F800052B7D9 /* License.swift in Sources */,
|
||||||
EB7F54142D78E9E700989564 /* LicenseWindow.swift in Sources */,
|
EB7F54142D78E9E700989564 /* LicenseWindow.swift in Sources */,
|
||||||
|
EB4F9D202EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
|
||||||
69DA9A1229E0BF5100A442DA /* AppDelegate.swift in Sources */,
|
69DA9A1229E0BF5100A442DA /* AppDelegate.swift in Sources */,
|
||||||
EB6F62D72D91F4AF00534EC2 /* SettingsView.swift in Sources */,
|
EB6F62D72D91F4AF00534EC2 /* SettingsView.swift in Sources */,
|
||||||
EB7F54122D78E96000989564 /* TutorialView.swift in Sources */,
|
EB7F54122D78E96000989564 /* TutorialView.swift in Sources */,
|
||||||
@@ -1017,11 +1022,11 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
EB4F9D272EC27CFB00A74A0F /* FileManager+DocumentsOpener.swift in Sources */,
|
||||||
EB821C992E76FE5300036963 /* SmallSheetView.swift in Sources */,
|
EB821C992E76FE5300036963 /* SmallSheetView.swift in Sources */,
|
||||||
EB05DFBF2E59FD0A00EF567A /* TrustedDevicesView.swift in Sources */,
|
EB05DFBF2E59FD0A00EF567A /* TrustedDevicesView.swift in Sources */,
|
||||||
EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */,
|
EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */,
|
||||||
EB5C37F22E377CE70052EFD7 /* NearbyConnection.swift in Sources */,
|
EB5C37F22E377CE70052EFD7 /* NearbyConnection.swift in Sources */,
|
||||||
EB5C37F32E377CE70052EFD7 /* SaveFilesManager.swift in Sources */,
|
|
||||||
EB16B6142E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
EB16B6142E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
||||||
EB886E672E4F2F800052B7D9 /* License.swift in Sources */,
|
EB886E672E4F2F800052B7D9 /* License.swift in Sources */,
|
||||||
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
||||||
@@ -1040,6 +1045,7 @@
|
|||||||
EB5C37FD2E377CE70052EFD7 /* Logger.swift in Sources */,
|
EB5C37FD2E377CE70052EFD7 /* Logger.swift in Sources */,
|
||||||
EB97A00F2E511124008B8C5C /* ReceiveModel.swift in Sources */,
|
EB97A00F2E511124008B8C5C /* ReceiveModel.swift in Sources */,
|
||||||
EB5C37FE2E377CE70052EFD7 /* offline_wire_formats.pb.swift in Sources */,
|
EB5C37FE2E377CE70052EFD7 /* offline_wire_formats.pb.swift in Sources */,
|
||||||
|
EB4F9D1F2EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
|
||||||
EB886E6A2E4F37B90052B7D9 /* ErrorAlertHandler.swift in Sources */,
|
EB886E6A2E4F37B90052B7D9 /* ErrorAlertHandler.swift in Sources */,
|
||||||
EB886E5E2E4F13EB0052B7D9 /* Zip.swift in Sources */,
|
EB886E5E2E4F13EB0052B7D9 /* Zip.swift in Sources */,
|
||||||
EB5C383D2E38C2D90052EFD7 /* AudioManager.swift in Sources */,
|
EB5C383D2E38C2D90052EFD7 /* AudioManager.swift in Sources */,
|
||||||
@@ -1073,6 +1079,7 @@
|
|||||||
EB950B4B2E520436002D68FA /* offline_wire_formats.pb.swift in Sources */,
|
EB950B4B2E520436002D68FA /* offline_wire_formats.pb.swift in Sources */,
|
||||||
EB950B4C2E520436002D68FA /* InboundNearbyConnection.swift in Sources */,
|
EB950B4C2E520436002D68FA /* InboundNearbyConnection.swift in Sources */,
|
||||||
EB16B6132E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
EB16B6132E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
||||||
|
EB4F9D212EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
|
||||||
EB950B4D2E520436002D68FA /* NearbyConnection.swift in Sources */,
|
EB950B4D2E520436002D68FA /* NearbyConnection.swift in Sources */,
|
||||||
EB950B4F2E520436002D68FA /* Logger.swift in Sources */,
|
EB950B4F2E520436002D68FA /* Logger.swift in Sources */,
|
||||||
EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */,
|
EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */,
|
||||||
@@ -1082,7 +1089,6 @@
|
|||||||
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */,
|
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */,
|
||||||
EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */,
|
EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */,
|
||||||
EB950B542E520436002D68FA /* HKDF.swift in Sources */,
|
EB950B542E520436002D68FA /* HKDF.swift in Sources */,
|
||||||
EB950B552E520436002D68FA /* SaveFilesManager.swift in Sources */,
|
|
||||||
EB1325D52E701D8E00FD749C /* Settings.swift in Sources */,
|
EB1325D52E701D8E00FD749C /* Settings.swift in Sources */,
|
||||||
EB950B562E520436002D68FA /* DataModel.swift in Sources */,
|
EB950B562E520436002D68FA /* DataModel.swift in Sources */,
|
||||||
EB821C9A2E76FE5300036963 /* SmallSheetView.swift in Sources */,
|
EB821C9A2E76FE5300036963 /* SmallSheetView.swift in Sources */,
|
||||||
@@ -1091,6 +1097,7 @@
|
|||||||
EB16B6192E58B4C8007D9792 /* TrustStore.swift in Sources */,
|
EB16B6192E58B4C8007D9792 /* TrustStore.swift in Sources */,
|
||||||
EB16B7772E59BAA1007D9792 /* AppGroup.swift in Sources */,
|
EB16B7772E59BAA1007D9792 /* AppGroup.swift in Sources */,
|
||||||
EB950B3F2E52038A002D68FA /* FilePicker.swift in Sources */,
|
EB950B3F2E52038A002D68FA /* FilePicker.swift in Sources */,
|
||||||
|
EB4F9D252EC27CFB00A74A0F /* FileManager+DocumentsOpener.swift in Sources */,
|
||||||
EB950B402E52038A002D68FA /* Configuration.swift in Sources */,
|
EB950B402E52038A002D68FA /* Configuration.swift in Sources */,
|
||||||
EB950B412E52038A002D68FA /* PieProgressView.swift in Sources */,
|
EB950B412E52038A002D68FA /* PieProgressView.swift in Sources */,
|
||||||
EB950B592E520649002D68FA /* AudioManager.swift in Sources */,
|
EB950B592E520649002D68FA /* AudioManager.swift in Sources */,
|
||||||
@@ -1098,6 +1105,7 @@
|
|||||||
EB950B6F2E5206E2002D68FA /* ShareViewController.swift in Sources */,
|
EB950B6F2E5206E2002D68FA /* ShareViewController.swift in Sources */,
|
||||||
EB950B432E52038A002D68FA /* ContentView.swift in Sources */,
|
EB950B432E52038A002D68FA /* ContentView.swift in Sources */,
|
||||||
EB950B442E520414002D68FA /* License.swift in Sources */,
|
EB950B442E520414002D68FA /* License.swift in Sources */,
|
||||||
|
EB4F9D1D2EC113EA00A74A0F /* DistributionDetector.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = ""
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
|||||||
@@ -28421,229 +28421,229 @@
|
|||||||
"ar" : {
|
"ar" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "افتح QuickDrop+ لتبادل ملفات غير محدودة بين جهاز Android وجهاز Mac لديك. جميع المنصات والميزات القادمة مشمولة."
|
"value" : "افتح QuickDrop+ لمتابعة تبادل الملفات بدون حدود. QuickDrop+ هو شراء لمرة واحدة فقط – لا حاجة لاشتراك. يتضمن جميع الميزات الحالية والمستقبلية، بالإضافة إلى تطبيقات macOS وAndroid."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ca" : {
|
"ca" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Desbloqueja QuickDrop+ per intercanviar arxius il·limitats entre el teu dispositiu Android i el Mac. Totes les plataformes i futures funcionalitats incloses."
|
"value" : "Desbloqueja QuickDrop+ per seguir intercanviant fitxers sense límits. QuickDrop+ és un pagament únic – no cal subscripció. Totes les funcionalitats actuals i futures estan incloses, així com les aplicacions per a macOS i Android."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cs" : {
|
"cs" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Odemkněte QuickDrop+ a vyměňujte si neomezeně soubory mezi svým zařízením s Androidem a Macem. Zahrnuje všechny platformy i budoucí funkce."
|
"value" : "Odemkněte QuickDrop+ pro neomezenou výměnu souborů. QuickDrop+ je jednorázový nákup – žádné předplatné není potřeba. Zahrnuje všechny aktuální i budoucí funkce a aplikace pro macOS i Android."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"da" : {
|
"da" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Lås op for QuickDrop+ for at udveksle ubegrænsede filer mellem din Android-enhed og Mac. Alle platforme og kommende funktioner er inkluderet."
|
"value" : "Lås QuickDrop+ op for at fortsætte filudveksling uden begrænsninger. QuickDrop+ er et engangskøb – intet abonnement kræves. Alle nuværende og fremtidige funktioner er inkluderet, samt macOS- og Android-apps."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Schalte QuickDrop+ frei, um unbegrenzt Dateien zwischen deinem Android- und Mac-Gerät zu übertragen. Alle Plattformen und kommende Funktionen sind enthalten."
|
"value" : "Schalte QuickDrop+ frei, um unbegrenzt Dateien auszutauschen. QuickDrop+ ist ein einmaliger Kauf – kein Abo nötig. Alle aktuellen und zukünftigen Funktionen sind enthalten, ebenso die macOS- und Android-Apps."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"el" : {
|
"el" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Ξεκλειδώστε το QuickDrop+ για να ανταλλάσσετε απεριόριστα αρχεία ανάμεσα σε Android και Mac συσκευές. Περιλαμβάνονται όλες οι πλατφόρμες και μελλοντικές λειτουργίες."
|
"value" : "Ξεκλειδώστε το QuickDrop+ για να συνεχίσετε την ανταλλαγή αρχείων χωρίς περιορισμούς. Το QuickDrop+ είναι μια εφάπαξ αγορά – δεν απαιτείται συνδρομή. Περιλαμβάνει όλες τις τρέχουσες και μελλοντικές λειτουργίες, καθώς και τις εφαρμογές για macOS και Android."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Unlock QuickDrop+ to exchange unlimited files with your Android device and Mac device. All platforms and upcoming features are included."
|
"value" : "Unlock QuickDrop+ to continue exchanging files without limits. QuickDrop+ is a one-time purchase – no subscription required. All current and future features are included, plus the macOS and Android apps."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en-AU" : {
|
"en-AU" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Unlock QuickDrop+ to swap unlimited files between your Android and Mac. All platforms and upcoming features included."
|
"value" : "Unlock QuickDrop+ to keep exchanging files without limits. QuickDrop+ is a one-off purchase – no subscription needed. All current and future features are included, as well as the macOS and Android apps."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en-CA" : {
|
"en-CA" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Unlock QuickDrop+ to exchange unlimited files between your Android device and Mac. Includes all platforms and future features."
|
"value" : "Unlock QuickDrop+ to keep sharing files without limits. QuickDrop+ is a one-time purchase—no subscription needed. All current and upcoming features are included, as well as the macOS and Android apps."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en-GB" : {
|
"en-GB" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Unlock QuickDrop+ to exchange unlimited files between your Android and Mac devices. All platforms and future features included."
|
"value" : "Unlock QuickDrop+ to continue exchanging files without limits. QuickDrop+ is a one-off purchase – no subscription required. All current and future features are included, plus the macOS and Android apps."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Desbloquea QuickDrop+ para intercambiar archivos ilimitados entre tu dispositivo Android y Mac. Todas las plataformas y próximas funciones están incluidas."
|
"value" : "Desbloquea QuickDrop+ para seguir intercambiando archivos sin límites. QuickDrop+ es una compra única, sin suscripción. Incluye todas las funciones actuales y futuras, además de las apps para macOS y Android."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"es-419" : {
|
"es-419" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Desbloquea QuickDrop+ para intercambiar archivos ilimitados entre tu dispositivo Android y Mac. Incluye todas las plataformas y funciones futuras."
|
"value" : "Desbloquea QuickDrop+ para seguir intercambiando archivos sin límites. QuickDrop+ es una compra única, sin suscripción. Incluye todas las funciones actuales y futuras, además de las apps para macOS y Android."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fi" : {
|
"fi" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Avaa QuickDrop+, jotta voit vaihtaa rajattomasti tiedostoja Android-laitteesi ja Macisi välillä. Kaikki alustat ja tulevat ominaisuudet sisältyvät."
|
"value" : "Avaa QuickDrop+ jatkaaksesi tiedostojen rajatonta vaihtoa. QuickDrop+ on kertamaksu – ei tilausta. Sisältää kaikki nykyiset ja tulevat ominaisuudet sekä macOS- ja Android-sovellukset."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Débloquez QuickDrop+ pour échanger des fichiers illimités entre votre appareil Android et votre Mac. Toutes les plateformes et futures fonctionnalités sont incluses."
|
"value" : "Débloquez QuickDrop+ pour continuer à échanger des fichiers sans limites. QuickDrop+ est un achat unique – aucun abonnement requis. Toutes les fonctionnalités actuelles et à venir sont incluses, ainsi que les applications macOS et Android."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fr-CA" : {
|
"fr-CA" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Débloquez QuickDrop+ pour échanger un nombre illimité de fichiers entre votre appareil Android et votre Mac. Toutes les plateformes et les prochaines fonctionnalités sont incluses."
|
"value" : "Déverrouillez QuickDrop+ pour continuer d’échanger des fichiers sans limite. QuickDrop+ est un achat unique — aucun abonnement requis. Toutes les fonctionnalités actuelles et à venir sont incluses, ainsi que les apps macOS et Android."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"he" : {
|
"he" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "פתח את QuickDrop+ כדי להחליף קבצים ללא הגבלה בין מכשיר האנדרואיד ל-Mac שלך. כל הפלטפורמות והתכונות הקרובות כלולות."
|
"value" : "פתח את QuickDrop+ כדי להמשיך להעביר קבצים ללא הגבלות. QuickDrop+ הוא תשלום חד-פעמי – ללא מנוי. כל הפיצ'רים הנוכחיים והעתידיים כלולים, כולל האפליקציות ל-macOS ולאנדרואיד."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hi" : {
|
"hi" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "अनलिमिटेड फाइल ट्रांसफर के लिए QuickDrop+ अनलॉक करें और अपने Android व Mac डिवाइस के बीच फ़ाइलें भेजें। सभी प्लेटफ़ॉर्म व आने वाली सुविधाएँ शामिल हैं।"
|
"value" : "QuickDrop+ अनलॉक करें ताकि आप बिना किसी सीमा के फाइलें भेज और प्राप्त कर सकें। QuickDrop+ एक बार की खरीद है – कोई सदस्यता आवश्यक नहीं है। इसमें सभी मौजूदा और भविष्य की सुविधाएँ शामिल हैं, साथ ही macOS और Android ऐप्स भी।"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hr" : {
|
"hr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Otključajte QuickDrop+ za neograničenu razmjenu datoteka između Android i Mac uređaja. Sve platforme i nadolazeće značajke su uključene."
|
"value" : "Otključajte QuickDrop+ za neograničenu razmjenu datoteka. QuickDrop+ je jednokratna kupnja – nema pretplate. Sve sadašnje i buduće značajke su uključene, kao i macOS i Android aplikacije."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hu" : {
|
"hu" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Oldja fel a QuickDrop+ funkciót, hogy korlátlanul cserélhessen fájlokat Android és Mac eszköze között. Minden platform és közelgő funkció benne van."
|
"value" : "Oldja fel a QuickDrop+ szolgáltatást, hogy korlátlanul cserélhessen fájlokat. A QuickDrop+ egyszeri vásárlás – előfizetés nem szükséges. Minden jelenlegi és jövőbeli funkció, valamint a macOS és Android alkalmazások benne vannak."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id" : {
|
"id" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Buka QuickDrop+ untuk bertukar file tanpa batas antara perangkat Android dan Mac Anda. Semua platform dan fitur terbaru sudah termasuk."
|
"value" : "Buka QuickDrop+ untuk terus bertukar file tanpa batas. QuickDrop+ adalah pembelian sekali saja – tanpa langganan. Semua fitur saat ini dan mendatang sudah termasuk, serta aplikasi macOS dan Android."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"it" : {
|
"it" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Sblocca QuickDrop+ per scambiare file illimitati tra il tuo dispositivo Android e Mac. Tutte le piattaforme e le prossime funzionalità sono incluse."
|
"value" : "Sblocca QuickDrop+ per continuare a scambiare file senza limiti. QuickDrop+ è un acquisto una tantum: nessun abbonamento richiesto. Include tutte le funzionalità attuali e future, oltre alle app per macOS e Android."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ja" : {
|
"ja" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "QuickDrop+ をアンロックして、Android デバイスと Mac デバイス間で無制限にファイル転送。全プラットフォームと今後の機能も利用可能です。"
|
"value" : "制限なくファイル交換を続けるにはQuickDrop+をアンロックしてください。QuickDrop+は買い切り型で、サブスクリプションは不要です。現在および今後の機能、さらにmacOS・Androidアプリもすべて含まれます。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ko" : {
|
"ko" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "QuickDrop+를 잠금 해제하여 Android와 Mac 기기 간에 무제한 파일을 전송하세요. 모든 플랫폼 및 향후 기능이 포함됩니다."
|
"value" : "QuickDrop+를 잠금 해제하여 파일을 무제한으로 자유롭게 전송하세요. QuickDrop+는 일회성 구매이며, 구독이 필요하지 않습니다. 현재와 미래의 모든 기능이 포함되어 있으며, macOS와 Android 앱도 함께 제공됩니다."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ms" : {
|
"ms" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Buka QuickDrop+ untuk tukar fail tanpa had antara peranti Android dan Mac anda. Semua platform dan ciri akan datang disertakan."
|
"value" : "Buka kunci QuickDrop+ untuk terus berkongsi fail tanpa had. QuickDrop+ ialah pembelian sekali sahaja – tiada langganan diperlukan. Semua ciri kini dan akan datang disertakan, termasuk aplikasi macOS dan Android."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nb" : {
|
"nb" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Lås opp QuickDrop+ for å overføre ubegrenset med filer mellom Android- og Mac-enheten din. Alle plattformer og kommende funksjoner er inkludert."
|
"value" : "Lås opp QuickDrop+ for å fortsette å utveksle filer uten begrensninger. QuickDrop+ er et engangskjøp – ingen abonnement kreves. Alle nåværende og fremtidige funksjoner er inkludert, samt macOS- og Android-appene."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nl" : {
|
"nl" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Ontgrendel QuickDrop+ om onbeperkt bestanden uit te wisselen tussen je Android- en Mac-apparaat. Alle platforms en toekomstige functies zijn inbegrepen."
|
"value" : "Ontgrendel QuickDrop+ om onbeperkt bestanden te blijven uitwisselen. QuickDrop+ is een eenmalige aankoop – geen abonnement nodig. Alle huidige en toekomstige functies zijn inbegrepen, evenals de macOS- en Android-apps."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pl" : {
|
"pl" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Odblokuj QuickDrop+, aby wymieniać nieograniczoną liczbę plików między urządzeniem z Androidem a Maciem. Wszystkie platformy i nadchodzące funkcje w cenie."
|
"value" : "Odblokuj QuickDrop+, aby dalej wymieniać pliki bez ograniczeń. QuickDrop+ to jednorazowy zakup – bez abonamentu. Obejmuje wszystkie obecne i przyszłe funkcje oraz aplikacje na macOS i Androida."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Desbloqueie o QuickDrop+ para trocar arquivos ilimitados entre seu dispositivo Android e Mac. Todas as plataformas e recursos futuros incluídos."
|
"value" : "Desbloqueie o QuickDrop+ para continuar trocando arquivos sem limites. QuickDrop+ é uma compra única – sem assinatura. Inclui todos os recursos atuais e futuros, além dos apps para macOS e Android."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pt-PT" : {
|
"pt-PT" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Desbloqueie o QuickDrop+ para trocar ficheiros ilimitados entre o seu dispositivo Android e Mac. Todas as plataformas e futuras funcionalidades incluídas."
|
"value" : "Desbloqueie o QuickDrop+ para continuar a trocar ficheiros sem limites. O QuickDrop+ é uma compra única – sem subscrição necessária. Inclui todas as funcionalidades atuais e futuras, bem como as apps para macOS e Android."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Deblochează QuickDrop+ pentru a transfera fișiere nelimitat între dispozitivul tău Android și Mac. Toate platformele și funcțiile viitoare sunt incluse."
|
"value" : "Deblochează QuickDrop+ pentru a continua schimbul de fișiere fără limite. QuickDrop+ este o achiziție unică – nu este nevoie de abonament. Toate funcțiile actuale și viitoare sunt incluse, precum și aplicațiile pentru macOS și Android."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Откройте QuickDrop+, чтобы обмениваться неограниченным количеством файлов между Android и Mac. Все платформы и будущие функции включены."
|
"value" : "Разблокируйте QuickDrop+, чтобы продолжать обмениваться файлами без ограничений. QuickDrop+ — это единовременная покупка, подписка не требуется. Включены все текущие и будущие функции, а также приложения для macOS и Android."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sv" : {
|
"sv" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Lås upp QuickDrop+ för att utbyta obegränsat med filer mellan din Android- och Mac-enhet. Alla plattformar och kommande funktioner ingår."
|
"value" : "Lås upp QuickDrop+ för att fortsätta utbyta filer utan begränsningar. QuickDrop+ är ett engångsköp – ingen prenumeration krävs. Alla nuvarande och framtida funktioner ingår, samt macOS- och Android-apparna."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"th" : {
|
"th" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "ปลดล็อก QuickDrop+ เพื่อแลกเปลี่ยนไฟล์ไม่จำกัดระหว่างอุปกรณ์ Android และ Mac ของคุณ รองรับทุกแพลตฟอร์มและฟีเจอร์ใหม่ในอนาคต"
|
"value" : "ปลดล็อก QuickDrop+ เพื่อแลกเปลี่ยนไฟล์ได้ไม่จำกัด QuickDrop+ ซื้อครั้งเดียว ไม่มีค่าสมัครรายเดือน รวมฟีเจอร์ปัจจุบันและอนาคตทั้งหมด พร้อมแอป macOS และ Android"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tr" : {
|
"tr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "QuickDrop+’ı açarak Android ve Mac cihazların arasında sınırsız dosya aktarımı yapın. Tüm platformlar ve gelecek özellikler dahildir."
|
"value" : "Sınırsız dosya paylaşımı için QuickDrop+’ı etkinleştirin. QuickDrop+ tek seferlik bir satın almadır – abonelik gerektirmez. Tüm mevcut ve gelecekteki özellikler ile macOS ve Android uygulamaları dahildir."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uk" : {
|
"uk" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Відкрийте QuickDrop+, щоб обмінюватися необмеженою кількістю файлів між вашим Android і Mac. Усі платформи та майбутні функції включені."
|
"value" : "Розблокуйте QuickDrop+, щоб продовжити обмін файлами без обмежень. QuickDrop+ — одноразова покупка, без підписки. Усі поточні й майбутні функції включені, а також додатки для macOS і Android."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vi" : {
|
"vi" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Mở khóa QuickDrop+ để trao đổi không giới hạn tệp giữa thiết bị Android và Mac của bạn. Bao gồm tất cả nền tảng và tính năng sắp ra mắt."
|
"value" : "Mở khóa QuickDrop+ để tiếp tục trao đổi tệp không giới hạn. QuickDrop+ là gói mua một lần – không cần đăng ký. Bao gồm tất cả các tính năng hiện tại và tương lai, cùng với ứng dụng macOS và Android."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-Hans" : {
|
"zh-Hans" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "解锁 QuickDrop+,即可在 Android 设备和 Mac 设备间无限制传输文件。支持所有平台及即将推出的功能。"
|
"value" : "解锁 QuickDrop+,即可无限制传输文件。QuickDrop+为一次性购买,无需订阅。包含所有当前与未来功能,以及 macOS 和 Android 应用。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-Hant" : {
|
"zh-Hant" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "解鎖 QuickDrop+,即可在 Android 與 Mac 裝置間無限傳輸檔案。所有平台和即將推出的功能均包含在內。"
|
"value" : "解鎖 QuickDrop+,即可無限制地傳輸檔案。QuickDrop+ 為一次性購買,無需訂閱。包含所有現有與未來功能,以及 macOS 和 Android 應用程式。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30745,229 +30745,229 @@
|
|||||||
"ar" : {
|
"ar" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "يمكنك الآن متابعة استلام الملفات بدون قيود. أي ملفات حاولت إرسالها سابقًا تم حفظها بالفعل على جهازك."
|
"value" : "يمكنك الآن استقبال الملفات بدون أي قيود. يرجى إعادة إرسال الملف أو الملفات التي حاولت إرسالها سابقًا."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ca" : {
|
"ca" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Ara pots continuar rebent arxius sense restriccions. Qualsevol arxiu que hagis intentat enviar ja s'ha desat al teu dispositiu."
|
"value" : "Ara pots rebre fitxers sense cap restricció. Torna a enviar el(s) fitxer(s) que vas intentar enviar abans."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cs" : {
|
"cs" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Nyní můžete pokračovat v přijímání souborů bez omezení. Všechny soubory, které jste se pokusili odeslat, byly již uloženy na vašem zařízení."
|
"value" : "Nyní můžete přijímat soubory bez omezení. Prosím, znovu odešlete soubory, které jste se pokusili poslat dříve."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"da" : {
|
"da" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Du kan nu fortsætte med at modtage filer uden restriktioner. Alle filer, du forsøgte at sende, blev allerede gemt på din enhed."
|
"value" : "Du kan nu modtage filer uden begrænsninger. Send venligst filerne igen, som du tidligere forsøgte at sende."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Du kannst nun weiterhin Dateien ohne Einschränkungen empfangen. Alle Dateien, welche gerade gesendet wurden, wurden bereits gespeichert."
|
"value" : "Du kannst jetzt Dateien ohne Einschränkungen empfangen. Bitte sende die Datei(en), die du zuvor senden wolltest, erneut."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"el" : {
|
"el" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Μπορείτε τώρα να συνεχίσετε να λαμβάνετε αρχεία χωρίς περιορισμούς. Τυχόν αρχεία που δοκιμάσατε να στείλετε έχουν ήδη αποθηκευτεί στη συσκευή σας."
|
"value" : "Τώρα μπορείτε να λαμβάνετε αρχεία χωρίς περιορισμούς. Παρακαλούμε στείλτε ξανά το(α) αρχείο(α) που προσπαθήσατε να στείλετε νωρίτερα."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "You can now continue receiving files without restrictions. Any files you tried to send were already saved on your device."
|
"value" : "You can now receive files without any restrictions. Please resend any files you tried to send earlier."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en-AU" : {
|
"en-AU" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Continue unlimited file reception. Attempts were saved."
|
"value" : "You can now receive files without any restrictions. Please resend the file(s) you tried to send earlier."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en-CA" : {
|
"en-CA" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "File reception unrestricted. Previously sent files were saved on your device."
|
"value" : "You can now receive files without any restrictions. Please resend the file(s) you tried to send earlier."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en-GB" : {
|
"en-GB" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "You can now continue receiving files unrestricted. Any sent files have already been saved."
|
"value" : "You can now receive files without any restrictions. Please resend the file(s) you tried to send earlier."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Ahora puedes seguir recibiendo archivos sin restricciones. Todos los archivos que intentaste enviar antes ya se han guardado en tu dispositivo."
|
"value" : "Ahora puedes recibir archivos sin restricciones. Por favor, vuelve a enviar el/los archivo(s) que intentaste enviar antes."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"es-419" : {
|
"es-419" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Ahora puedes continuar recibiendo archivos sin restricciones. Cualquier archivo que intentaste enviar ya se guardó en tu dispositivo."
|
"value" : "Ahora puedes recibir archivos sin restricciones. Por favor, vuelve a enviar el/los archivo(s) que intentaste enviar antes."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fi" : {
|
"fi" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Voit nyt jatkaa tiedostojen vastaanottamista ilman rajoituksia. Kaikki lähetettävät tiedostot on jo tallennettu laitteellesi."
|
"value" : "Voit nyt vastaanottaa tiedostoja ilman rajoituksia. Lähetäthän aiemmin yrittämäsi tiedoston/tiedostot uudelleen."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Vous pouvez maintenant continuer à recevoir des fichiers sans restrictions. Tous les fichiers que vous avez essayé d'envoyer précédemment ont déjà été enregistrés sur votre appareil."
|
"value" : "Vous pouvez maintenant recevoir des fichiers sans aucune restriction. Veuillez renvoyer le(s) fichier(s) que vous avez tenté d’envoyer plus tôt."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fr-CA" : {
|
"fr-CA" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Vous pouvez maintenant continuer à recevoir des fichiers sans restrictions. Tous les fichiers que vous avez essayé d'envoyer ont déjà été enregistrés sur votre appareil."
|
"value" : "Vous pouvez maintenant recevoir des fichiers sans aucune restriction. Veuillez renvoyer le(s) fichier(s) que vous avez essayé d’envoyer plus tôt."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"he" : {
|
"he" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "ניתן להמשיך לקבל קבצים ללא מגבלות. כל הקבצים שניסית לשלוח נשמרו כבר במכשיר שלך."
|
"value" : "ניתן כעת לקבל קבצים ללא מגבלות. אנא שלח שוב את הקובץ או הקבצים שניסית לשלוח קודם."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hi" : {
|
"hi" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "आप अब बिना किसी प्रतिबंध के फ़ाइलें प्राप्त कर सकते हैं। आपने जो फ़ाइलें भेजने की कोशिश की थीं, वे आपकी डिवाइस पर पहले ही सहेजी जा चुकी हैं।"
|
"value" : "अब आप बिना किसी प्रतिबंध के फाइलें प्राप्त कर सकते हैं। कृपया वे फाइलें फिर से भेजें जिन्हें आपने पहले भेजने की कोशिश की थी।"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hr" : {
|
"hr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Sada možete nastaviti primati datoteke bez ograničenja. Sve datoteke koje ste pokušali poslati već su spremljene na vaš uređaj."
|
"value" : "Sada možete primati datoteke bez ikakvih ograničenja. Molimo ponovno pošaljite datoteke koje ste ranije pokušali poslati."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hu" : {
|
"hu" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Mostantól korlátozások nélkül fogadhat fájlokat. Az összes fájl, amelyet korábban próbált elküldeni, már el lett mentve az eszközére."
|
"value" : "Mostantól korlátozás nélkül fogadhat fájlokat. Kérjük, küldje el újra a korábban próbált fájl(oka)t."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id" : {
|
"id" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Anda sekarang dapat terus menerima file tanpa batasan. File apa pun yang Anda coba kirim telah disimpan di perangkat Anda."
|
"value" : "Anda sekarang dapat menerima file tanpa batasan. Silakan kirim ulang file yang sebelumnya Anda coba kirim."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"it" : {
|
"it" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Ora puoi continuare a ricevere file senza restrizioni. Tutti i file che hai provato a inviare in precedenza sono già stati salvati sul tuo dispositivo."
|
"value" : "Ora puoi ricevere file senza restrizioni. Invia di nuovo il/i file che hai provato a mandare prima."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ja" : {
|
"ja" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "これで、制限なくファイルを受信し続けることができます。以前送信しようとしたファイルはすでにデバイスに保存されています。"
|
"value" : "これで制限なくファイルを受信できます。先ほど送信しようとしたファイルをもう一度送信してください。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ko" : {
|
"ko" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "이제 파일을 제한 없이 계속 받을 수 있습니다. 이전에 전송을 시도한 모든 파일이 이미 기기에 저장되었습니다."
|
"value" : "이제 아무 제한 없이 파일을 받을 수 있습니다. 이전에 보내려던 파일을 다시 보내주세요."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ms" : {
|
"ms" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Anda kini boleh terus menerima fail tanpa sekatan. Sebarang fail yang anda cuba hantar telah disimpan pada peranti anda."
|
"value" : "Anda kini boleh menerima fail tanpa sebarang sekatan. Sila hantar semula fail yang anda cuba hantar sebelum ini."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nb" : {
|
"nb" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Du kan nå fortsette å motta filer uten begrensninger. Alle filer du prøvde å sende ble allerede lagret på enheten din."
|
"value" : "Du kan nå motta filer uten begrensninger. Vennligst send filene du prøvde å sende tidligere på nytt."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nl" : {
|
"nl" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Je kunt nu bestanden blijven ontvangen zonder beperkingen. Alle bestanden die je eerder probeerde te verzenden, zijn al opgeslagen op je apparaat."
|
"value" : "Je kunt nu bestanden zonder beperkingen ontvangen. Stuur het bestand of de bestanden die je eerder probeerde te verzenden opnieuw."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pl" : {
|
"pl" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Możesz teraz kontynuować odbieranie plików bez ograniczeń. Wszystkie pliki, które próbowałeś wysłać, zostały już zapisane na Twoim urządzeniu."
|
"value" : "Możesz teraz odbierać pliki bez ograniczeń. Wyślij ponownie plik(i), które próbowałeś wcześniej wysłać."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Agora você pode continuar recebendo arquivos sem restrições. Todos os arquivos que você tentou enviar antes já foram salvos no seu dispositivo."
|
"value" : "Agora você pode receber arquivos sem restrições. Por favor, envie novamente o(s) arquivo(s) que tentou enviar anteriormente."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pt-PT" : {
|
"pt-PT" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Agora pode continuar a receber ficheiros sem restrições. Todos os ficheiros que tentou enviar antes já foram guardados no seu dispositivo."
|
"value" : "Agora pode receber ficheiros sem quaisquer restrições. Por favor, reenvie os ficheiros que tentou enviar anteriormente."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro" : {
|
"ro" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Acum puteți continua să primiți fișiere fără restricții. Toate fișierele pe care ați încercat să le trimiteți anterior au fost deja salvate pe dispozitivul dvs."
|
"value" : "Acum poți primi fișiere fără restricții. Te rugăm să retrimiți fișierul(ele) pe care ai încercat să le trimiți mai devreme."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Теперь вы можете продолжать получать файлы без ограничений. Все файлы, которые вы пытались отправить ранее, уже сохранены на вашем устройстве."
|
"value" : "Теперь вы можете получать файлы без ограничений. Пожалуйста, отправьте файл(ы) заново."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sv" : {
|
"sv" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Du kan nu fortsätta ta emot filer utan begränsningar. Alla filer du försökte skicka sparades redan på din enhet."
|
"value" : "Du kan nu ta emot filer utan några begränsningar. Vänligen skicka filen/filerna igen som du försökte skicka tidigare."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"th" : {
|
"th" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "คุณสามารถรับไฟล์ต่อไปได้โดยไม่มีข้อจำกัด ไฟล์ทั้งหมดที่คุณพยายามส่งก่อนหน้านี้ถูกบันทึกไว้ในอุปกรณ์ของคุณแล้ว"
|
"value" : "คุณสามารถรับไฟล์ได้โดยไม่มีข้อจำกัดแล้ว โปรดส่งไฟล์ที่คุณพยายามส่งมาก่อนหน้านี้อีกครั้ง"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tr" : {
|
"tr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Artık sınırsız dosya alımına devam edebilirsiniz. Göndermeye çalıştığınız tüm dosyalar zaten cihazınıza kaydedildi."
|
"value" : "Artık herhangi bir kısıtlama olmadan dosya alabilirsiniz. Lütfen daha önce göndermeye çalıştığınız dosyaları tekrar gönderin."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uk" : {
|
"uk" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Тепер ви можете продовжувати отримувати файли без обмежень. Усі файли, які ви намагалися надіслати раніше, вже збережено на вашому пристрої."
|
"value" : "Тепер ви можете отримувати файли без обмежень. Будь ласка, надішліть файл(и) ще раз."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vi" : {
|
"vi" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Bạn có thể tiếp tục nhận tệp không giới hạn. Bất kỳ tệp nào bạn đã cố gắng gửi đều đã được lưu vào thiết bị của bạn."
|
"value" : "Bạn hiện có thể nhận tệp mà không bị hạn chế. Vui lòng gửi lại tệp mà bạn đã cố gửi trước đó."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-Hans" : {
|
"zh-Hans" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "您现在可以继续无限制地接收文件。您之前尝试发送的所有文件已保存到您的设备上。"
|
"value" : "您现在可以无限制地接收文件了。请重新发送您之前尝试发送的文件。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-Hant" : {
|
"zh-Hant" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "您現在可以繼續無限制地接收檔案。您嘗試傳送的所有檔案均已保存到裝置上。"
|
"value" : "您現在可以無限制地接收檔案。請重新傳送您先前嘗試傳送的檔案。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,15 +27,7 @@ struct PlusView: View {
|
|||||||
@State var warning = false
|
@State var warning = false
|
||||||
@State var buying = false
|
@State var buying = false
|
||||||
@State var errorText = ""
|
@State var errorText = ""
|
||||||
|
@State var boughtSuccessAlert = false
|
||||||
@State var boughtSuccessAlert = false {
|
|
||||||
didSet {
|
|
||||||
if boughtSuccessAlert {
|
|
||||||
log("User bought plus version. Moving pending files...")
|
|
||||||
SaveFilesManager.shared.movePendingFilesToTarget()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ struct CapsuleProgress: View {
|
|||||||
struct QuickDropToastView_Previews: PreviewProvider {
|
struct QuickDropToastView_Previews: PreviewProvider {
|
||||||
struct Demo: View {
|
struct Demo: View {
|
||||||
|
|
||||||
@State var model = ReceiveModel()
|
@State var model = ReceiveModel(controlPlusScreen: { _ in })
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
QuickDropToastView(
|
QuickDropToastView(
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import LUI
|
|||||||
|
|
||||||
class ReceiveModel: ObservableObject, InboundAppDelegate {
|
class ReceiveModel: ObservableObject, InboundAppDelegate {
|
||||||
|
|
||||||
|
|
||||||
/// For each connection ID, store the last reported progress value
|
/// For each connection ID, store the last reported progress value
|
||||||
private var processes: [String: Double] = [:]
|
private var processes: [String: Double] = [:]
|
||||||
|
|
||||||
@@ -29,7 +28,8 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
|
|
||||||
let controlPlusScreen: (Bool) -> Void
|
let controlPlusScreen: (Bool) -> Void
|
||||||
|
|
||||||
init(controlPlusScreen: @escaping (Bool) -> Void = { _ in }) {
|
|
||||||
|
init(controlPlusScreen: @escaping (Bool) -> Void) {
|
||||||
self.controlPlusScreen = controlPlusScreen
|
self.controlPlusScreen = controlPlusScreen
|
||||||
NearbyConnectionManager.shared.addInboundAppDelegate(self)
|
NearbyConnectionManager.shared.addInboundAppDelegate(self)
|
||||||
NearbyConnectionManager.shared.becomeVisible()
|
NearbyConnectionManager.shared.becomeVisible()
|
||||||
@@ -103,8 +103,6 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
case .Decline:
|
case .Decline:
|
||||||
secondaryButtonAction()
|
secondaryButtonAction()
|
||||||
}
|
}
|
||||||
} onCancel: {
|
|
||||||
NearbyConnectionManager.shared.cancelTransfer(id: transferID)
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -146,11 +144,6 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
if transfer.type == .text {
|
if transfer.type == .text {
|
||||||
showAlert(title: "QuickDrop", message: mainMessage)
|
showAlert(title: "QuickDrop", message: mainMessage)
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
ProgressAlert.shared.showProgressAlert(onCancel: {
|
|
||||||
NearbyConnectionManager.shared.cancelTransfer(id: transfer.id)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +158,9 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
let averageProgress = totalProgress / Double(processes.count)
|
let averageProgress = totalProgress / Double(processes.count)
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
ProgressAlert.shared.updateProgress(averageProgress)
|
ProgressAlert.shared.updateProgress(averageProgress, onCancel: {
|
||||||
|
NearbyConnectionManager.shared.cancelTransfer(id: connectionID)
|
||||||
|
})
|
||||||
#else
|
#else
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.progress = averageProgress
|
self.progress = averageProgress
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ struct ContentView: View {
|
|||||||
}, settingsView: {
|
}, settingsView: {
|
||||||
CustomSection(header: settings.gotPlus ? "" : "General", footer: "TrustedDevicesFooterShort") {
|
CustomSection(header: settings.gotPlus ? "" : "General", footer: "TrustedDevicesFooterShort") {
|
||||||
LUIButton {
|
LUIButton {
|
||||||
SaveFilesManager.shared.openDownloadedFilesFolder()
|
FileManager.default.openDocumentFolder()
|
||||||
} label: {
|
} label: {
|
||||||
NavigationLinkLabel(imageName: "folder.fill", text: "BrowseDownloadedFiles")
|
NavigationLinkLabel(imageName: "folder.fill", text: "BrowseDownloadedFiles")
|
||||||
}
|
}
|
||||||
@@ -41,9 +41,14 @@ struct DeviceListView: View {
|
|||||||
@State private var qrCode: Image? = nil
|
@State private var qrCode: Image? = nil
|
||||||
@StateObject var sendModel = SendModel()
|
@StateObject var sendModel = SendModel()
|
||||||
|
|
||||||
#if !EXTENSION
|
#if !EXTENSION
|
||||||
@StateObject var receiveModel = ReceiveModel()
|
@StateObject var receiveModel = ReceiveModel(controlPlusScreen: { show in
|
||||||
#endif
|
log("[DeviceListView] Setting showPlusScreen to \(show)")
|
||||||
|
DeviceListViewModel.sharedInstance.showPlusScreen = show
|
||||||
|
})
|
||||||
|
#endif
|
||||||
|
|
||||||
|
@ObservedObject var model = DeviceListViewModel.sharedInstance
|
||||||
|
|
||||||
@Environment(\.scenePhase) var scenePhase
|
@Environment(\.scenePhase) var scenePhase
|
||||||
@Environment(\.sheetActive) var sheetActive
|
@Environment(\.sheetActive) var sheetActive
|
||||||
@@ -54,7 +59,7 @@ struct DeviceListView: View {
|
|||||||
|
|
||||||
let showsLoadingIndicator = nearbyConnectionManager.hasLocalNetworkPermission && nearbyConnectionManager.isConnectedToLocalNetwork
|
let showsLoadingIndicator = nearbyConnectionManager.hasLocalNetworkPermission && nearbyConnectionManager.isConnectedToLocalNetwork
|
||||||
|
|
||||||
BottomBarView(header: "QuickDrop ", navigationBarLayout: isShareExtension() ? .SmallOnlyAlways : .Default, bottomViewHeight: 30) {
|
BottomBarView(header: "QuickDrop", navigationBarLayout: isShareExtension() ? .SmallOnlyAlways : .Default, bottomViewHeight: 30) {
|
||||||
VStack {
|
VStack {
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
@@ -185,7 +190,7 @@ struct DeviceListView: View {
|
|||||||
|
|
||||||
log("[ScenePhase] App became active")
|
log("[ScenePhase] App became active")
|
||||||
NearbyConnectionManager.shared.startDeviceDiscovery()
|
NearbyConnectionManager.shared.startDeviceDiscovery()
|
||||||
#if !EXTENSION
|
#if !EXTENSION
|
||||||
NearbyConnectionManager.shared.becomeVisible()
|
NearbyConnectionManager.shared.becomeVisible()
|
||||||
|
|
||||||
if Settings.shared.incomingTransmissionCount > 0 {
|
if Settings.shared.incomingTransmissionCount > 0 {
|
||||||
@@ -193,18 +198,21 @@ struct DeviceListView: View {
|
|||||||
requestReviewOnce()
|
requestReviewOnce()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
if newValue == .background {
|
if newValue == .background {
|
||||||
|
|
||||||
log("[ScenePhase] App went to background")
|
log("[ScenePhase] App went to background")
|
||||||
#if !EXTENSION
|
#if !EXTENSION
|
||||||
NearbyConnectionManager.shared.becomeInvisible()
|
NearbyConnectionManager.shared.becomeInvisible()
|
||||||
#endif
|
#endif
|
||||||
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if !EXTENSION
|
||||||
|
.modifier(PlusVersionSheetModifier(sheetActive: $model.showPlusScreen, isPlusVersion: $settings.gotPlus))
|
||||||
|
#endif
|
||||||
.animation(.smooth, value: nearbyConnectionManager.hasLocalNetworkPermission)
|
.animation(.smooth, value: nearbyConnectionManager.hasLocalNetworkPermission)
|
||||||
.animation(.smooth, value: nearbyConnectionManager.isConnectedToLocalNetwork)
|
.animation(.smooth, value: nearbyConnectionManager.isConnectedToLocalNetwork)
|
||||||
.navigationBarItems(trailing: ZStack {
|
.navigationBarItems(trailing: ZStack {
|
||||||
@@ -218,6 +226,12 @@ struct DeviceListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class DeviceListViewModel: SharedInstance {
|
||||||
|
static var sharedInstance = DeviceListViewModel()
|
||||||
|
|
||||||
|
@Published var showPlusScreen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
struct DeviceFilePickerButton: View {
|
struct DeviceFilePickerButton: View {
|
||||||
|
|
||||||
@@ -297,11 +311,11 @@ struct DeviceButtonLabel: View {
|
|||||||
|
|
||||||
|
|
||||||
func isShareExtension() -> Bool {
|
func isShareExtension() -> Bool {
|
||||||
#if EXTENSION
|
#if EXTENSION
|
||||||
return true
|
return true
|
||||||
#else
|
#else
|
||||||
return false
|
return false
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ final class ProgressAlert {
|
|||||||
private weak var progressView: UIProgressView?
|
private weak var progressView: UIProgressView?
|
||||||
|
|
||||||
// MARK: - Initial Accept / Decline
|
// MARK: - Initial Accept / Decline
|
||||||
func askForUserPermission(title: String, message: String, acceptLabel: String, acceptAlwaysLabel: String?, rejectLabel: String, onAccept: @escaping (AcceptAction) -> Void, onCancel: @escaping () -> Void) {
|
func askForUserPermission(title: String, message: String, acceptLabel: String, acceptAlwaysLabel: String?, rejectLabel: String, onAccept: @escaping (AcceptAction) -> Void) {
|
||||||
guard let vc = topMostViewController() else { return }
|
guard let vc = topMostViewController() else { return }
|
||||||
|
|
||||||
let alert = UIAlertController(title: title,
|
let alert = UIAlertController(title: title,
|
||||||
@@ -31,28 +31,23 @@ final class ProgressAlert {
|
|||||||
|
|
||||||
alert.addAction(UIAlertAction(title: acceptLabel, style: .default, handler: { _ in
|
alert.addAction(UIAlertAction(title: acceptLabel, style: .default, handler: { _ in
|
||||||
onAccept(.Accept)
|
onAccept(.Accept)
|
||||||
self.showProgressAlert(on: vc, onCancel: onCancel)
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if let acceptAlways = acceptAlwaysLabel {
|
if let acceptAlways = acceptAlwaysLabel {
|
||||||
alert.addAction(UIAlertAction(title: acceptAlways, style: .default, handler: { _ in
|
alert.addAction(UIAlertAction(title: acceptAlways, style: .default, handler: { _ in
|
||||||
onAccept(.AcceptAlways)
|
onAccept(.AcceptAlways)
|
||||||
self.showProgressAlert(on: vc, onCancel: onCancel)
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
vc.present(alert, animated: true)
|
vc.present(alert, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func showProgressAlert(onCancel: @escaping () -> Void) {
|
|
||||||
guard let vc = topMostViewController() else { return }
|
|
||||||
self.showProgressAlert(on: vc, onCancel: onCancel)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Progress Alert
|
// MARK: - Progress Alert
|
||||||
private func showProgressAlert(on vc: UIViewController, onCancel: @escaping () -> Void) {
|
private func showProgressAlert(onCancel: @escaping () -> Void) {
|
||||||
|
|
||||||
|
guard let vc = topMostViewController() else { return }
|
||||||
|
|
||||||
let alert = UIAlertController(title: "Receiving".localized(),
|
let alert = UIAlertController(title: "Receiving".localized(),
|
||||||
message: "\n\n",
|
message: "\n\n",
|
||||||
preferredStyle: .alert)
|
preferredStyle: .alert)
|
||||||
@@ -77,11 +72,25 @@ final class ProgressAlert {
|
|||||||
|
|
||||||
vc.present(alert, animated: true)
|
vc.present(alert, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Update Progress
|
// MARK: - Update Progress
|
||||||
// progress: 0…1 → update progress, nil → finished
|
// progress: 0…1 → update progress, nil → finished
|
||||||
func updateProgress(_ progress: Double?, completion: @escaping () -> Void = {}) {
|
func updateProgress(_ progress: Double?, onCancel: @escaping () -> Void, completion: @escaping () -> Void = {}) {
|
||||||
|
|
||||||
|
// No progress to show, nothing shown currently → just complete
|
||||||
|
if progressAlert == nil && progress == nil {
|
||||||
|
completion()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|
||||||
|
// Show progress if not shown yet
|
||||||
|
if self.progressAlert == nil {
|
||||||
|
self.showProgressAlert(onCancel: onCancel)
|
||||||
|
}
|
||||||
|
|
||||||
if let p = progress {
|
if let p = progress {
|
||||||
self.progressView?.setProgress(Float(p), animated: true)
|
self.progressView?.setProgress(Float(p), animated: true)
|
||||||
completion()
|
completion()
|
||||||
|
|||||||
Reference in new issue
Block a user