diff --git a/NearbyShare/Connection/InboundNearbyConnection.swift b/NearbyShare/Connection/InboundNearbyConnection.swift index 0acaee6..e74092f 100644 --- a/NearbyShare/Connection/InboundNearbyConnection.swift +++ b/NearbyShare/Connection/InboundNearbyConnection.swift @@ -50,19 +50,7 @@ class InboundNearbyConnection: NearbyConnection { deletePartiallyReceivedFiles() DispatchQueue.main.async { - self.delegate?.connectionWasTerminated(connection: self, error: self.lastError) - - if !self.downloadedFiles.isEmpty { - - #if os(macOS) - if Settings.shared.openFinderAfterReceiving { - log("[SaveFilesManager] Opening \(self.downloadedFiles.count) file(s) in Finder.") - NSWorkspace.shared.activateFileViewerSelecting(self.downloadedFiles) - } - #else - FileManager.default.openDocumentFolder() - #endif - } + self.delegate?.connectionWasTerminated(connection: self, savedFiles: self.downloadedFiles, error: self.lastError) } } diff --git a/NearbyShare/Connection/NearbyConnectionManager.swift b/NearbyShare/Connection/NearbyConnectionManager.swift index 4e96f13..8416ff3 100644 --- a/NearbyShare/Connection/NearbyConnectionManager.swift +++ b/NearbyShare/Connection/NearbyConnectionManager.swift @@ -228,7 +228,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb } - func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?) { + func connectionWasTerminated(connection: InboundNearbyConnection, savedFiles: [URL], error: Error?) { incomingConnections.removeValue(forKey: connection.id) // If there are no more incoming connections, we can stop accessing the save directory @@ -238,7 +238,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb if !connection.wasUserRejected { 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), savedFiles: savedFiles, wasPlainTextTransfer: connection.isPlainTextTransfer, error: error) } } } diff --git a/NearbyShare/Model/DataModel.swift b/NearbyShare/Model/DataModel.swift index 9210d9e..2444e44 100644 --- a/NearbyShare/Model/DataModel.swift +++ b/NearbyShare/Model/DataModel.swift @@ -15,7 +15,7 @@ protocol InboundNearbyConnectionDelegate { func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, connection: InboundNearbyConnection) func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo, connection: InboundNearbyConnection) func updatedTransferProgress(connection: InboundNearbyConnection, progress: Double) - func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?) + func connectionWasTerminated(connection: InboundNearbyConnection, savedFiles: [URL], error: Error?) func showPlusScreen() } @@ -32,7 +32,7 @@ protocol OutboundNearbyConnectionDelegate { public protocol InboundAppDelegate: AnyObject { func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo) func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo) - func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo, wasPlainTextTransfer: Bool, error: Error?) + func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo, savedFiles: [URL], wasPlainTextTransfer: Bool, error: Error?) func transferProgress(connectionID: String, progress: Double) func showPlusScreen() } diff --git a/NearbyShare/Util/EXIFUtils.swift b/NearbyShare/Util/EXIFUtils.swift index 3772c1c..901f366 100644 --- a/NearbyShare/Util/EXIFUtils.swift +++ b/NearbyShare/Util/EXIFUtils.swift @@ -48,7 +48,7 @@ struct EXIFUtils { /// Returns the EXIF original date of an image file, adjusted to the current time zone. - private static func exifOriginalDate(from url: URL) -> Date? { + 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], @@ -56,10 +56,34 @@ struct EXIFUtils { else { return nil } - + + let offsetString = exif[kCGImagePropertyExifOffsetTimeOriginal] as? String + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.calendar = Calendar(identifier: .gregorian) formatter.dateFormat = "yyyy:MM:dd HH:mm:ss" - formatter.timeZone = TimeZone.current // Use the current device time zone + + let offsetSeconds = offsetString.flatMap(secondsFromOffsetString) + if let seconds = offsetSeconds { + formatter.timeZone = TimeZone(secondsFromGMT: seconds) + } else { + // Fallback: treat as device local + formatter.timeZone = TimeZone.current + } + return formatter.date(from: dateString) } + + + static private func secondsFromOffsetString(_ s: String) -> Int? { + // Expect formats like "+02:00" or "-05:30" + guard s.count == 6, (s.first == "+" || s.first == "-") else { return nil } + let sign = s.first == "-" ? -1 : 1 + let parts = s.dropFirst().split(separator: ":") + guard parts.count == 2, + let hours = Int(parts[0]), + let minutes = Int(parts[1]) else { return nil } + return sign * (hours * 3600 + minutes * 60) + } } diff --git a/QuickDrop.xcodeproj/project.pbxproj b/QuickDrop.xcodeproj/project.pbxproj index 5ae7947..cda9785 100644 --- a/QuickDrop.xcodeproj/project.pbxproj +++ b/QuickDrop.xcodeproj/project.pbxproj @@ -205,6 +205,7 @@ EBADEB912E802A830021B5E4 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = EBADEB902E802A830021B5E4 /* DeviceKit */; }; EBBC93B22EC8B76600F541A1 /* LocalNetworkPermissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBBC93B12EC8B75F00F541A1 /* LocalNetworkPermissionView.swift */; }; EBBC93B32EC8B76600F541A1 /* LocalNetworkPermissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBBC93B12EC8B75F00F541A1 /* LocalNetworkPermissionView.swift */; }; + EBBEE1462EC9D93400AA7831 /* PhotoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBBEE1452EC9D92F00AA7831 /* PhotoManager.swift */; }; EBC05C5B2E33937200EECC18 /* ErrorAlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */; }; EBC05C5C2E3394EF00EECC18 /* ErrorAlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */; }; EBC05C712E33B57D00EECC18 /* Converter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C6D2E33B57D00EECC18 /* Converter.swift */; }; @@ -371,6 +372,7 @@ EB97A0192E51EA9C008B8C5C /* UIAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlert.swift; sourceTree = ""; }; EB99257F2E45D53E00357055 /* sharing_enums.pb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = sharing_enums.pb.swift; sourceTree = ""; }; EBBC93B12EC8B75F00F541A1 /* LocalNetworkPermissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNetworkPermissionView.swift; sourceTree = ""; }; + EBBEE1452EC9D92F00AA7831 /* PhotoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoManager.swift; sourceTree = ""; }; EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlertHandler.swift; sourceTree = ""; }; EBC05C6D2E33B57D00EECC18 /* Converter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Converter.swift; sourceTree = ""; }; EBC05C6E2E33B57D00EECC18 /* Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helper.swift; sourceTree = ""; }; @@ -595,6 +597,7 @@ EB979F8E2E50CA23008B8C5C /* ContentView.swift */, EB979F8F2E50CA23008B8C5C /* FilePicker.swift */, EB97A0192E51EA9C008B8C5C /* UIAlert.swift */, + EBBEE1452EC9D92F00AA7831 /* PhotoManager.swift */, EBBC93B12EC8B75F00F541A1 /* LocalNetworkPermissionView.swift */, EB979F992E50CAA6008B8C5C /* PieProgressView.swift */, ); @@ -1056,6 +1059,7 @@ EB5C383D2E38C2D90052EFD7 /* AudioManager.swift in Sources */, EB979F942E50CA23008B8C5C /* Configuration.swift in Sources */, EB979F952E50CA23008B8C5C /* ContentView.swift in Sources */, + EBBEE1462EC9D93400AA7831 /* PhotoManager.swift in Sources */, EB97A01A2E51EAA2008B8C5C /* UIAlert.swift in Sources */, EB979F962E50CA23008B8C5C /* FilePicker.swift in Sources */, EB979F9A2E50CAA6008B8C5C /* PieProgressView.swift in Sources */, @@ -1462,6 +1466,7 @@ INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Leon Böttger"; INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network."; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; @@ -1506,6 +1511,7 @@ INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Leon Böttger"; INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network."; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; diff --git a/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3c05d6e..57d5dd3 100644 --- a/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -52,7 +52,7 @@ "location" : "https://github.com/leonboe1/LUI.git", "state" : { "branch" : "main", - "revision" : "c971b0a42e299c6ad78e933db473342e11fd9456" + "revision" : "a005a2241d55fe42cf7e5f6e2bcd7d4714fc8c21" } }, { diff --git a/QuickDrop/Localizable.xcstrings b/QuickDrop/Localizable.xcstrings index cc389e4..cf46cfd 100644 --- a/QuickDrop/Localizable.xcstrings +++ b/QuickDrop/Localizable.xcstrings @@ -6055,6 +6055,239 @@ } } }, + "CouldNotSaveMediaToPhotoLibrary" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تعذر حفظ الوسائط" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "No s'ha pogut desar el contingut multimèdia" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nelze uložit médium" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunne ikke gemme medier" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Medien konnten nicht gespeichert werden" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αδυναμία αποθήκευσης πολυμέσων" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Could Not Save Media" + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "Could Not Save Media" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Could Not Save Media" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Could Not Save Media" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudo guardar el contenido multimedia" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudo guardar el archivo multimedia" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiedostoa ei voitu tallentaa" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossible de sauvegarder le média" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossible d’enregistrer le média" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "לא ניתן לשמור מדיה" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "मीडिया सहेजा नहीं जा सका" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nije moguće spremiti medij" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Média mentése sikertelen" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tidak Dapat Menyimpan Media" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossibile salvare contenuti multimediali" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "メディアを保存できませんでした" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "미디어를 저장할 수 없음" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tidak Dapat Simpan Media" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunne ikke lagre medier" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kan media niet opslaan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie można zapisać multimediów" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foi possível salvar a mídia" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não Foi Possível Guardar o Conteúdo Multimédia" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nu s-a putut salva media" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось сохранить медиа" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunde inte spara media" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ไม่สามารถบันทึกสื่อได้" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Medya Kaydedilemedi" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не вдалося зберегти медіа" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể lưu phương tiện" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法保存媒体" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法儲存媒體" + } + } + } + }, "Decline" : { "extractionState" : "manual", "localizations" : { @@ -7919,6 +8152,239 @@ } } }, + "Done" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تم" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fet" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hotovo" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Færdig" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fertig" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Τέλος" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Done" + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "Done" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Done" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Done" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Listo" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Listo" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Valmis" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terminé" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terminé" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "סיום" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "हो गया" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gotovo" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kész" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selesai" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fatto" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "完了" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "완료" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selesai" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ferdig" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gereed" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gotowe" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Concluído" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Concluído" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gata" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Готово" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Klar" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "เสร็จสิ้น" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bitti" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Готово" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xong" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "完成" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "完成" + } + } + } + }, "DontSeeYourDevice" : { "extractionState" : "manual", "localizations" : { @@ -10248,6 +10714,472 @@ } } }, + "FileTransferCompleted" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "اكتمل نقل الملف" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transferència de fitxers completada" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Přenos souborů dokončen" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Filoverførsel fuldført" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dateiübertragung abgeschlossen" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Η μεταφορά αρχείων ολοκληρώθηκε" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "File Transfer Completed" + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "File Transfer Completed" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "File Transfer Completed" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "File Transfer Completed" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transferencia de archivos completada" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transferencia de archivos completada" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiedostonsiirto valmis" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transfert de fichier terminé" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transfert de fichier terminé" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "העברת הקובץ הושלמה" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "फ़ाइल ट्रांसफर पूरा हुआ" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prijenos datoteka završen" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fájlátvitel befejezve" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transfer File Selesai" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trasferimento file completato" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ファイル転送が完了しました" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "파일 전송 완료" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pemindahan Fail Selesai" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Filoverføring fullført" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bestandsoverdracht voltooid" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transfer plików zakończony" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transferência de arquivo concluída" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transferência de ficheiros concluída" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Transferul de fișiere a fost finalizat" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Передача файла завершена" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Filöverföring slutförd" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "การถ่ายโอนไฟล์เสร็จสิ้นแล้ว" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dosya Transferi Tamamlandı" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Передача файлів завершена" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển tệp thành công" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "文件传输完成" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "檔案傳輸完成" + } + } + } + }, + "FileTransferCompletedMessage" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تم حفظ جميع الملفات في تطبيق الملفات على هذا الجهاز." + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tots els fitxers s'han desat a l'app Fitxers d'aquest dispositiu." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Všechny soubory byly uloženy do aplikace Soubory na tomto zařízení." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle filer er gemt i appen Filer på denne enhed." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle Dateien wurden in der Dateien-App auf diesem Gerät gespeichert." + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Όλα τα αρχεία αποθηκεύτηκαν στην εφαρμογή Αρχεία αυτής της συσκευής." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "All files were stored in the Files app on this device." + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "All files were stored in the Files app on this device." + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "All files were stored in the Files app on this device." + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "All files were stored in the Files app on this device." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Todos los archivos se almacenaron en la app Archivos de este dispositivo." + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Todos los archivos se guardaron en la app Archivos de este dispositivo." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kaikki tiedostot tallennettiin tämän laitteen Files-sovellukseen." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tous les fichiers ont été enregistrés dans l’app Fichiers sur cet appareil." + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tous les fichiers ont été enregistrés dans l’app Fichiers sur cet appareil." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "כל הקבצים נשמרו באפליקציית הקבצים במכשיר זה." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सभी फाइलें इस डिवाइस की फ़ाइल्स ऐप में संग्रहीत की गई हैं।" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sve datoteke su spremljene u aplikaciju Datoteke na ovom uređaju." + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Minden fájl el lett mentve a Fájlok alkalmazásba ezen az eszközön." + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Semua file disimpan di aplikasi File di perangkat ini." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tutti i file sono stati salvati nell'app File su questo dispositivo." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "すべてのファイルはこのデバイスの「ファイル」アプリに保存されました。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "모든 파일이 이 기기의 파일 앱에 저장되었습니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Semua fail telah disimpan dalam aplikasi Fail pada peranti ini." + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle filer ble lagret i Filer-appen på denne enheten." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alle bestanden zijn opgeslagen in de Bestanden-app op dit apparaat." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wszystkie pliki zostały zapisane w aplikacji Pliki na tym urządzeniu." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Todos os arquivos foram salvos no app Arquivos deste dispositivo." + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Todos os ficheiros foram guardados na aplicação Ficheiros neste dispositivo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toate fișierele au fost stocate în aplicația Fișiere pe acest dispozitiv." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Все файлы были сохранены в приложении \"Файлы\" на этом устройстве." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alla filer har sparats i appen Filer på den här enheten." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ไฟล์ทั้งหมดถูกบันทึกไว้ในแอปไฟล์บนอุปกรณ์นี้" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tüm dosyalar bu cihazdaki Dosyalar uygulamasında saklandı." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Усі файли збережено в застосунку «Файли» на цьому пристрої." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tất cả tệp đã được lưu trong ứng dụng Tệp trên thiết bị này." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "所有文件已存储在此设备的文件App中。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "所有檔案已儲存在此裝置的檔案 App 中。" + } + } + } + }, "FirewallDescription" : { "extractionState" : "manual", "localizations" : { @@ -11646,6 +12578,239 @@ } } }, + "ImportToPhotos" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "استيراد إلى الصور" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importa a Fotos" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importovat do Fotek" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importer til Fotos" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "In Fotos importieren" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Εισαγωγή στις Φωτογραφίες" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Import to Photos" + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "Import to Photos" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Import to Photos" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Import to Photos" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importar a Fotos" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importar a Fotos" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tuo Kuvat-sovellukseen" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importer dans Photos" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importer dans Photos" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "ייבוא לתמונות" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "फ़ोटो में आयात करें" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uvezi u Fotografije" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importálás a Fotókba" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impor ke Foto" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importa in Foto" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "写真に追加" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "사진에 가져오기" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Import ke Foto" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importer til Bilder" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importeren naar Foto's" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importuj do Zdjęć" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importar para Fotos" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importar para Fotografias" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importă în Poze" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Импортировать в Фото" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Importera till Bilder" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "นำเข้าไปยังรูปภาพ" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fotoğraflara İçe Aktar" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Імпортувати в Фото" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập vào Ảnh" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入到照片" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "匯入到相片" + } + } + } + }, "InformDeveloper" : { "extractionState" : "manual", "localizations" : { @@ -19796,6 +20961,705 @@ } } }, + "SaveToPhotosErrorNoSavableContent" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لم يتم العثور على صور أو فيديوهات مدعومة للحفظ." + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "No s'ha trobat cap foto ni vídeo compatible per desar." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nebyly nalezeny žádné podporované fotografie nebo videa k uložení." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen understøttede billeder eller videoer fundet til at gemme." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Es wurden keine unterstützten Fotos oder Videos zum Speichern gefunden." + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Δεν βρέθηκαν υποστηριζόμενες φωτογραφίες ή βίντεο για αποθήκευση." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No supported photos or videos were found to be saved." + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "No supported photos or videos were found to be saved." + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "No supported photos or videos were found to be saved." + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "No supported photos or videos were found to be saved." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se encontraron fotos o videos compatibles para guardar." + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se encontraron fotos o videos compatibles para guardar." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tuettavia kuvia tai videoita ei löytynyt tallennettavaksi." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucune photo ou vidéo compatible n’a été trouvée à sauvegarder." + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucune photo ou vidéo prise en charge n'a été trouvée à enregistrer." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "לא נמצאו תמונות או סרטונים נתמכים לשמירה." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "कोई समर्थित फोटो या वीडियो सहेजने के लिए नहीं मिले।" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nisu pronađene podržane fotografije ili videozapisi za spremanje." + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nem találhatók menthető, támogatott fotók vagy videók." + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tidak ada foto atau video yang didukung untuk disimpan." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Non sono state trovate foto o video supportati da salvare." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存できる対応写真や動画が見つかりませんでした。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "저장할 수 있는 지원되는 사진이나 비디오가 없습니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiada foto atau video yang disokong ditemui untuk disimpan." + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen støttede bilder eller videoer ble funnet for lagring." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Er zijn geen ondersteunde foto's of video's gevonden om op te slaan." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie znaleziono obsługiwanych zdjęć ani filmów do zapisania." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nenhuma foto ou vídeo compatível foi encontrado para salvar." + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foram encontradas fotos ou vídeos suportados para guardar." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nu s-au găsit fotografii sau videoclipuri compatibile de salvat." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не найдено поддерживаемых фото или видео для сохранения." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inga kompatibla bilder eller videor hittades att spara." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ไม่พบรูปภาพหรือวิดีโอที่รองรับเพื่อบันทึก" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kaydedilecek desteklenen fotoğraf veya video bulunamadı." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не знайдено підтримуваних фото чи відео для збереження." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không tìm thấy ảnh hoặc video được hỗ trợ để lưu." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未找到可保存的支持格式的照片或视频。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "找不到可儲存的相片或影片。" + } + } + } + }, + "SaveToPhotosErrorPermissionDenied" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لقد منعت تطبيق QuickDrop من حفظ المحتوى في معرض الصور الخاص بك. لتغيير ذلك، افتح تطبيق الإعدادات، وابحث عن \"QuickDrop\"، وغيّر إعداد \"الصور\" إلى \"إضافة الصور فقط\"." + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Has denegat a QuickDrop l'accés per desar contingut a la galeria. Per canviar-ho, obre l'app Configuració, busca \"QuickDrop\" i canvia l'opció \"Fotos\" a \"Només afegir fotos\"." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odepřeli jste aplikaci QuickDrop přístup k ukládání obsahu do vaší galerie. Chcete-li to změnit, otevřete aplikaci Nastavení, vyhledejte \"QuickDrop\" a změňte nastavení \"Fotky\" na \"Přidávat pouze fotky\"." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du har nægtet QuickDrop adgang til at gemme indhold i dit galleri. For at ændre dette, åbn appen Indstillinger, søg efter \"QuickDrop\", og skift indstillingen \"Fotos\" til \"Tilføj kun fotos\"." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du hast QuickDrop den Zugriff verweigert, Inhalte in deiner Galerie zu speichern. Um dies zu ändern, öffne die Einstellungen, suche nach „QuickDrop“ und setze die Einstellung „Fotos“ auf „Nur Fotos hinzufügen“." + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αρνηθήκατε στην εφαρμογή QuickDrop πρόσβαση για αποθήκευση περιεχομένου στη συλλογή σας. Για να το αλλάξετε, ανοίξτε την εφαρμογή Ρυθμίσεις, βρείτε το \"QuickDrop\" και αλλάξτε τη ρύθμιση \"Φωτογραφίες\" σε \"Μόνο προσθήκη φωτογραφιών\"." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You denied QuickDrop access to save content to your gallery. To change this, open the Settings app, search for \"QuickDrop\", and change the setting \"Photos\" to \"Add Photos Only\"." + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "You denied QuickDrop access to save content to your gallery. To change this, open the Settings app, search for \"QuickDrop\", and change the setting \"Photos\" to \"Add Photos Only\"." + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "You denied QuickDrop access to save content to your gallery. To change this, open the Settings app, search for \"QuickDrop,\" and change the setting \"Photos\" to \"Add Photos Only.\"" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "You denied QuickDrop access to save content to your gallery. To change this, open the Settings app, search for \"QuickDrop\", and change the setting \"Photos\" to \"Add Photos Only\"." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Denegaste a QuickDrop el acceso para guardar contenido en tu galería. Para cambiarlo, abre la app de Configuración, busca \"QuickDrop\" y cambia el ajuste de \"Fotos\" a \"Solo añadir fotos\"." + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Denegaste el acceso de QuickDrop para guardar contenido en tu galería. Para cambiar esto, abre la app de Configuración, busca \"QuickDrop\" y cambia la opción de \"Fotos\" a \"Solo agregar fotos\"." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kieltäydyit antamasta QuickDropille lupaa tallentaa sisältöä galleriaasi. Muuttaaksesi tätä, avaa Asetukset-sovellus, etsi \"QuickDrop\" ja muuta \"Kuvat\"-asetukseksi \"Lisää vain kuvia\"." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous avez refusé à QuickDrop l'accès pour enregistrer du contenu dans votre galerie. Pour changer cela, ouvrez l'application Réglages, cherchez « QuickDrop », puis changez l'option « Photos » en « Ajouter des photos uniquement »." + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous avez refusé à QuickDrop l'accès pour enregistrer du contenu dans votre galerie. Pour modifier cela, ouvrez l'application Réglages, cherchez « QuickDrop », puis changez l'option « Photos » à « Ajouter des photos seulement »." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "סירבת לאפליקציית QuickDrop גישה לשמירת תוכן בגלריה שלך. כדי לשנות זאת, פתח את אפליקציית ההגדרות, חפש \"QuickDrop\", ושנה את ההגדרה \"תמונות\" ל-\"הוספת תמונות בלבד\"." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "आपने QuickDrop को अपनी गैलरी में सामग्री सुरक्षित करने की अनुमति नहीं दी है। इसे बदलने के लिए, सेटिंग्स ऐप खोलें, \"QuickDrop\" सर्च करें, और \"Photos\" सेटिंग को \"Add Photos Only\" पर बदलें।" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odbili ste dopuštenje QuickDropu za spremanje sadržaja u vašu galeriju. Da to promijenite, otvorite aplikaciju Postavke, potražite \"QuickDrop\" i postavite opciju \"Fotografije\" na \"Samo dodaj fotografije\"." + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Megtagadtad a QuickDrop számára a galéria elérését. A módosításhoz nyisd meg a Beállítások alkalmazást, keresd meg a „QuickDrop”-ot, majd a „Fotók” beállítást állítsd „Csak fotók hozzáadása” lehetőségre." + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anda menolak izin QuickDrop untuk menyimpan konten ke galeri Anda. Untuk mengubahnya, buka aplikasi Pengaturan, cari \"QuickDrop\", dan ubah pengaturan \"Foto\" menjadi \"Hanya Tambah Foto\"." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hai negato a QuickDrop l'accesso per salvare contenuti nella tua galleria. Per modificare questa impostazione, apri l'app Impostazioni, cerca \"QuickDrop\" e cambia l'impostazione \"Foto\" su \"Solo aggiunta di foto\"." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "QuickDropがギャラリーにコンテンツを保存するアクセス権を拒否しました。これを変更するには、設定アプリを開き、「QuickDrop」を検索し、「写真」の設定を「写真の追加のみに許可」に変更してください。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "QuickDrop이 갤러리에 콘텐츠를 저장하는 접근 권한이 거부되었습니다. 이를 변경하려면 설정 앱을 열고 \"QuickDrop\"을 검색한 후 \"사진\" 설정을 \"사진 추가만 허용\"으로 변경하세요." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anda telah menolak akses QuickDrop untuk menyimpan kandungan ke galeri anda. Untuk mengubah ini, buka aplikasi Tetapan, cari \"QuickDrop\", dan tukar tetapan \"Foto\" kepada \"Tambah Foto Sahaja\"." + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du har nektet QuickDrop tilgang til å lagre innhold i galleriet. For å endre dette, åpne Innstillinger-appen, søk etter \"QuickDrop\", og endre innstillingen \"Bilder\" til \"Legg til bilder kun\"." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je hebt QuickDrop geen toegang gegeven om inhoud in je galerij op te slaan. Open de app Instellingen, zoek naar \"QuickDrop\" en wijzig de instelling \"Foto's\" naar \"Alleen foto's toevoegen\"." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odmówiłeś aplikacji QuickDrop dostępu do zapisywania zawartości w galerii. Aby to zmienić, otwórz aplikację Ustawienia, wyszukaj \"QuickDrop\" i ustaw opcję \"Zdjęcia\" na \"Dodaj tylko zdjęcia\"." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Você negou ao QuickDrop acesso para salvar conteúdo na sua galeria. Para alterar isso, abra o app Ajustes, procure por \"QuickDrop\" e altere a configuração \"Fotos\" para \"Adicionar Fotos Apenas\"." + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Negaste ao QuickDrop o acesso para guardar conteúdos na tua galeria. Para alterar isto, abre a aplicação Definições, procura por \"QuickDrop\" e muda a definição \"Fotografias\" para \"Apenas Adicionar Fotografias\"." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ai refuzat accesul QuickDrop pentru a salva conținut în galerie. Pentru a schimba acest lucru, deschide aplicația Configurări, caută „QuickDrop” și setează „Fotografii” pe „Doar adăugare fotografii”." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы запретили QuickDrop сохранять контент в вашу галерею. Чтобы изменить это, откройте приложение \"Настройки\", найдите \"QuickDrop\" и измените настройку \"Фото\" на \"Только добавление\"." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Du har nekat QuickDrop åtkomst att spara innehåll i ditt galleri. För att ändra detta, öppna appen Inställningar, sök efter \"QuickDrop\" och ändra inställningen \"Bilder\" till \"Lägg till endast\"." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "คุณได้ปฏิเสธการเข้าถึงแกลเลอรี่ของ QuickDrop ในการเปลี่ยนแปลง ให้เปิดแอปการตั้งค่า ค้นหา \"QuickDrop\" และเปลี่ยนการตั้งค่า \"รูปภาพ\" เป็น \"เพิ่มรูปภาพเท่านั้น\"" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "QuickDrop'un galerinize içerik kaydetme iznini reddettiniz. Bunu değiştirmek için Ayarlar uygulamasını açın, \"QuickDrop\"u arayın ve \"Fotoğraflar\" ayarını \"Yalnızca Fotoğraf Ekle\" olarak değiştirin." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ви заборонили QuickDrop зберігати контент у вашу галерею. Щоб змінити це, відкрийте додаток Налаштування, знайдіть \"QuickDrop\" і змініть опцію \"Фото\" на \"Додавати лише фото\"." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bạn đã từ chối quyền cho QuickDrop lưu nội dung vào thư viện của mình. Để thay đổi, hãy mở ứng dụng Cài đặt, tìm \"QuickDrop\" và chuyển tùy chọn \"Ảnh\" thành \"Chỉ thêm ảnh\"." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "你已拒绝QuickDrop保存内容到图库的权限。要更改此设置,请打开“设置”应用,搜索“QuickDrop”,并将“照片”权限设置为“仅添加照片”。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "你已拒絕QuickDrop存取相簿的權限。若要更改,請開啟「設定」App,搜尋「QuickDrop」,然後將「相片」權限設為「僅加入相片」。" + } + } + } + }, + "SaveToPhotosErrorSaveFailed" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "حدث خطأ غير معروف أثناء محاولة حفظ الصور أو الفيديوهات في معرضك. يرجى التفكير في الإبلاغ عن هذا الخطأ." + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "S'ha produït un error desconegut en intentar desar fotos o vídeos a la galeria. Si us plau, considera informar d'aquest error." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Při pokusu o uložení fotografií nebo videí do galerie došlo k neznámé chybě. Zvažte prosím nahlášení této chyby." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "En ukendt fejl opstod under forsøg på at gemme billeder eller videoer i dit galleri. Overvej venligst at rapportere denne fejl." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beim Versuch, Fotos oder Videos in deiner Galerie zu speichern, ist ein unbekannter Fehler aufgetreten. Bitte erwäge, diesen Fehler zu melden." + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Παρουσιάστηκε ένα άγνωστο σφάλμα κατά την αποθήκευση φωτογραφιών ή βίντεο στη συλλογή σας. Παρακαλούμε σκεφτείτε να αναφέρετε αυτό το σφάλμα." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "An unknown error occurred when trying to save photos or videos to your gallery. Please consider reporting this error." + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "An unknown error occurred when trying to save photos or videos to your gallery. Please consider reporting this error." + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "An unknown error occurred when trying to save photos or videos to your gallery. Please consider reporting this error." + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "An unknown error occurred when trying to save photos or videos to your gallery. Please consider reporting this error." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ocurrió un error desconocido al intentar guardar fotos o videos en tu galería. Por favor, considera informar de este error." + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ocurrió un error desconocido al intentar guardar fotos o videos en tu galería. Por favor, considera reportar este error." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tuntematon virhe tapahtui, kun yritettiin tallentaa kuvia tai videoita galleriaasi. Harkitse virheen ilmoittamista." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Une erreur inconnue s'est produite lors de la sauvegarde des photos ou vidéos dans votre galerie. Merci d'envisager de signaler cette erreur." + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Une erreur inconnue s'est produite lors de l'enregistrement des photos ou vidéos dans votre galerie. Veuillez envisager de signaler cette erreur." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אירעה שגיאה לא מוכרת בעת ניסיון לשמור תמונות או סרטונים לגלריה שלך. אנא שקול לדווח על שגיאה זו." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "फोटो या वीडियो गैलरी में सहेजने की कोशिश में अज्ञात त्रुटि हुई। कृपया इस त्रुटि की रिपोर्ट करने पर विचार करें।" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Došlo je do nepoznate pogreške prilikom spremanja fotografija ili videozapisa u vašu galeriju. Molimo razmislite o prijavljivanju ove pogreške." + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ismeretlen hiba történt a fotók vagy videók galériába mentésekor. Kérjük, fontold meg a hiba jelentését." + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terjadi kesalahan tidak dikenal saat mencoba menyimpan foto atau video ke galeri Anda. Silakan pertimbangkan untuk melaporkan kesalahan ini." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si è verificato un errore sconosciuto durante il salvataggio di foto o video nella tua galleria. Ti invitiamo a segnalare questo errore." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "写真や動画をギャラリーに保存中に不明なエラーが発生しました。このエラーの報告をご検討ください。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "사진 또는 비디오를 갤러리에 저장하는 중 알 수 없는 오류가 발생했습니다. 이 오류를 보고해 주세요." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ralat tidak diketahui berlaku semasa cuba menyimpan foto atau video ke galeri anda. Sila pertimbangkan untuk melaporkan ralat ini." + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "En ukjent feil oppstod ved lagring av bilder eller videoer i galleriet ditt. Vurder å rapportere denne feilen." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Er is een onbekende fout opgetreden bij het opslaan van foto's of video's in je galerij. Overweeg dit probleem te melden." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wystąpił nieznany błąd podczas zapisywania zdjęć lub filmów w galerii. Prosimy o rozważenie zgłoszenia tego błędu." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ocorreu um erro desconhecido ao tentar salvar fotos ou vídeos na sua galeria. Considere relatar esse erro." + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ocorreu um erro desconhecido ao tentar guardar fotos ou vídeos na tua galeria. Por favor, considera reportar este erro." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "A apărut o eroare necunoscută la încercarea de a salva fotografiile sau videoclipurile în galerie. Te rugăm să raportezi această eroare." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Произошла неизвестная ошибка при попытке сохранить фото или видео в галерею. Пожалуйста, сообщите об этой ошибке." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ett okänt fel uppstod när bilder eller videor skulle sparas i ditt galleri. Vänligen överväg att rapportera detta fel." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "เกิดข้อผิดพลาดที่ไม่ทราบสาเหตุขณะพยายามบันทึกรูปภาพหรือวิดีโอลงในแกลเลอรี่ของคุณ กรุณารายงานข้อผิดพลาดนี้" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fotoğraf veya videolar galerinize kaydedilmeye çalışılırken bilinmeyen bir hata oluştu. Lütfen bu hatayı bildirmeniz gerektiğini düşünün." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сталася невідома помилка під час спроби зберегти фото або відео до вашої галереї. Будь ласка, розгляньте можливість повідомити про цю помилку." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã xảy ra lỗi không xác định khi cố lưu ảnh hoặc video vào thư viện của bạn. Vui lòng cân nhắc báo cáo lỗi này." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在尝试将照片或视频保存到图库时发生未知错误。请考虑报告此错误。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "嘗試將照片或影片儲存至相簿時發生未知錯誤。請考慮回報此錯誤。" + } + } + } + }, "SearchingForDevices" : { "extractionState" : "manual", "localizations" : { diff --git a/QuickDrop/ViewModel/ReceiveModel.swift b/QuickDrop/ViewModel/ReceiveModel.swift index 2c40513..08a1576 100644 --- a/QuickDrop/ViewModel/ReceiveModel.swift +++ b/QuickDrop/ViewModel/ReceiveModel.swift @@ -170,7 +170,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate { } - func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo, wasPlainTextTransfer: Bool, error: (any Error)?) { + func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo, savedFiles: [URL], wasPlainTextTransfer: Bool, error: (any Error)?) { processes.removeValue(forKey: connectionID) @@ -204,11 +204,35 @@ class ReceiveModel: ObservableObject, InboundAppDelegate { #if os(iOS) doubleVibration() + if !savedFiles.isEmpty { + + let hasPhotoOrVideos = PhotoManager.hasPhotosOrVideos(at: savedFiles) + let photoSaveAction: (() -> ())? = hasPhotoOrVideos ? { + Task { + do { + try await PhotoManager.saveMediaToPhotoLibrary(from: savedFiles) + } + catch { + showAlert(title: "CouldNotSaveMediaToPhotoLibrary", message: error.localizedDescription) + } + } + } : nil + + ProgressAlert.shared.presentIncomingTransferDoneAlert(title: "FileTransferCompleted".localized(), message: "FileTransferCompletedMessage".localized(), onImportToPhotos: photoSaveAction) + } + #endif let currentCount = Settings.shared.incomingTransmissionCount #if os(macOS) + + // Show received files if wanted + if !savedFiles.isEmpty, Settings.shared.openFinderAfterReceiving { + log("[SaveFilesManager] Opening \(self.downloadedFiles.count) file(s) in Finder.") + NSWorkspace.shared.activateFileViewerSelecting(self.downloadedFiles) + } + // If distributed directly, do not request review here as it does not work if currentCount == 0 && !DistributionDetector.isDirectDistributionEnabled { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { diff --git a/QuickDropiOS/InfoPlist.xcstrings b/QuickDropiOS/InfoPlist.xcstrings index d9b4325..dccd489 100644 --- a/QuickDropiOS/InfoPlist.xcstrings +++ b/QuickDropiOS/InfoPlist.xcstrings @@ -5,11 +5,233 @@ "comment" : "Bundle display name", "extractionState" : "extracted_with_value", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "ca" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "cs" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "da" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "de" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "el" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "QuickDrop" } + }, + "en-AU" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "es" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "fi" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "fr" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "he" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "hi" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "hr" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "hu" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "id" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "it" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "ja" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "ko" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "ms" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "nb" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "pl" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "ro" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "ru" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "sv" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "th" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "tr" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "uk" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "vi" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } } } }, @@ -17,11 +239,233 @@ "comment" : "Bundle name", "extractionState" : "extracted_with_value", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "ca" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "cs" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "da" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "de" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "el" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "QuickDropiOS" } + }, + "en-AU" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "es" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "fi" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "fr" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "he" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "hi" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "hr" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "hu" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "id" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "it" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "ja" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "ko" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "ms" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "nb" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "pl" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "ro" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "ru" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "sv" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "th" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "tr" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "uk" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "vi" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } } } }, @@ -29,11 +473,233 @@ "comment" : "Copyright (human-readable)", "extractionState" : "extracted_with_value", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "ca" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "cs" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "da" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "de" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "el" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "" } + }, + "en-AU" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "es" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "fi" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "fr" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "he" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "hi" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "hr" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "hu" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "id" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "it" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "ja" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "ko" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "ms" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "nb" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "nl" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "pl" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "ro" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "ru" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "sv" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "th" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "tr" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "uk" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "vi" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } } } }, @@ -269,6 +935,240 @@ } } } + }, + "NSPhotoLibraryAddUsageDescription" : { + "comment" : "Privacy - Photo Library Additions Usage Description", + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "يرغب QuickDrop في إضافة الصور ومقاطع الفيديو المستلمة إلى مكتبة صورك." + } + }, + "ca" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop vol afegir les fotos i vídeos rebuts a la teva biblioteca de fotos." + } + }, + "cs" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop si přeje přidat přijaté fotky a videa do vaší knihovny fotografií." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "QuickDrop vil gerne tilføje modtagne billeder og videoer til dit fotobibliotek." + } + }, + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop möchte empfangene Fotos und Videos deiner Mediathek hinzufügen." + } + }, + "el" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Το QuickDrop θέλει να προσθέσει τις ληφθείσες φωτογραφίες και βίντεο στη βιβλιοθήκη φωτογραφιών σας." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The add photos permission is required to save received photos and videos to your photo library." + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop would like to add received photos and videos to your photo library." + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop would like to add received photos and videos to your photo library." + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop would like to add received photos and videos to your photo library." + } + }, + "es" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop quiere añadir las fotos y videos recibidos a tu galería." + } + }, + "es-419" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop quiere agregar las fotos y videos recibidos a tu galería." + } + }, + "fi" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop haluaa lisätä vastaanotetut kuvat ja videot kuvakirjastoosi." + } + }, + "fr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop souhaite ajouter les photos et vidéos reçues à votre bibliothèque." + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop souhaite ajouter les photos et vidéos reçues à votre bibliothèque de photos." + } + }, + "he" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop מבקשת להוסיף תמונות וסרטונים שהתקבלו לספריית התמונות שלך." + } + }, + "hi" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop प्राप्त तस्वीरें और वीडियो आपकी फोटो लाइब्रेरी में जोड़ना चाहता है।" + } + }, + "hr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop želi dodati primljene fotografije i videozapise u vašu galeriju." + } + }, + "hu" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "A QuickDrop hozzá szeretné adni a fogadott fotókat és videókat a fotókönyvtáradhoz." + } + }, + "id" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop ingin menambahkan foto dan video yang diterima ke perpustakaan foto Anda." + } + }, + "it" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop desidera aggiungere le foto e i video ricevuti alla tua libreria fotografica." + } + }, + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDropが受信した写真とビデオをフォトライブラリに追加しようとしています。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop이(가) 받은 사진과 동영상을 사진 보관함에 추가하려고 합니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop ingin menambah foto dan video yang diterima ke pustaka foto anda." + } + }, + "nb" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop vil legge til mottatte bilder og videoer i bildebiblioteket ditt." + } + }, + "nl" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop wil ontvangen foto's en video's toevoegen aan je fotobibliotheek." + } + }, + "pl" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop chce dodać odebrane zdjęcia i filmy do Twojej biblioteki zdjęć." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "O QuickDrop gostaria de adicionar fotos e vídeos recebidos à sua biblioteca de fotos." + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "O QuickDrop gostaria de adicionar as fotos e vídeos recebidos à sua biblioteca de fotografias." + } + }, + "ro" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop dorește să adauge fotografiile și videoclipurile primite în biblioteca ta foto." + } + }, + "ru" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop хочет добавить полученные фото и видео в вашу медиатеку." + } + }, + "sv" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop vill lägga till mottagna foton och videor i ditt bildbibliotek." + } + }, + "th" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop ต้องการเพิ่มรูปภาพและวิดีโอที่ได้รับไปยังคลังรูปภาพของคุณ" + } + }, + "tr" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop, alınan fotoğraf ve videoları fotoğraf kitaplığınıza eklemek istiyor." + } + }, + "uk" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop хоче додати отримані фото та відео до вашої фототеки." + } + }, + "vi" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop muốn thêm ảnh và video nhận được vào thư viện ảnh của bạn." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop 希望将接收的照片和视频添加到您的照片库。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "QuickDrop 想將收到的照片和影片加入你的相簿。" + } + } + } } }, "version" : "1.0" diff --git a/QuickDropiOS/View/PhotoManager.swift b/QuickDropiOS/View/PhotoManager.swift new file mode 100644 index 0000000..4495ab4 --- /dev/null +++ b/QuickDropiOS/View/PhotoManager.swift @@ -0,0 +1,174 @@ +// +// PhotoManager.swift +// QuickDrop +// +// Created by Leon Böttger on 16.11.25. +// + +import UIKit +import Photos +import UniformTypeIdentifiers + + +struct PhotoManager { + + static func hasPhotosOrVideos(at urls: [URL]) -> Bool { + urls.contains(where: { isImageFile(at: $0) || isVideoFile(at: $0) }) + } + + + /// Saves any image/video URLs to the user's Photos library. + /// - Parameters: + /// - urls: Remote or local URLs to inspect. + /// - openPhotosOnSuccess: If true, will try to open the Photos app after saving. + /// - Throws: `SaveToPhotosError` or underlying URLSession/Photos errors. + static func saveMediaToPhotoLibrary( + from urls: [URL], + openPhotosOnSuccess: Bool = true + ) async throws { + let candidates = urls.filter { isImageFile(at: $0) || isVideoFile(at: $0) } + guard !candidates.isEmpty else { throw SaveToPhotosError.noSavableContent } + + let authStatus = await requestAddOnlyPhotoAuthorization() + guard authStatus == .authorized || authStatus == .limited else { + throw SaveToPhotosError.permissionDenied + } + + struct ImageItem { + let fileURL: URL + let creationDate: Date? + } + + struct VideoItem { + let fileURL: URL + let creationDate: Date? + } + + var imageItems: [ImageItem] = [] + var videoItems: [VideoItem] = [] + + // 1. Download and write to temp files, extracting EXIF date for images. + for sourceURL in candidates { + + if isImageFile(at: sourceURL) { + let exifDate = EXIFUtils.exifOriginalDate(from: sourceURL) + imageItems.append(ImageItem(fileURL: sourceURL, creationDate: exifDate)) + } else { + // For videos you may want to parse container metadata; as a fallback use file creation date. + let videoDate = await getVideoCreationDate(from: sourceURL) + videoItems.append(VideoItem(fileURL: sourceURL, creationDate: videoDate)) + } + } + + guard !imageItems.isEmpty || !videoItems.isEmpty else { + throw SaveToPhotosError.noSavableContent + } + + // 2. Import into Photos; explicitly set creationDate so it doesn't default to "now". + try await withCheckedThrowingContinuation { continuation in + PHPhotoLibrary.shared().performChanges({ + for item in imageItems { + if let request = PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: item.fileURL) { + if let date = item.creationDate { + request.creationDate = date + } + } + } + + for item in videoItems { + if let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: item.fileURL) { + if let date = item.creationDate { + request.creationDate = date + } + } + } + }, completionHandler: { success, error in + if success { + continuation.resume() + } else { + continuation.resume(throwing: error ?? SaveToPhotosError.saveFailed) + } + }) + } + + + // 5. Open the Photos app + if openPhotosOnSuccess { + if let url = URL(string: "photos-redirect://") { + DispatchQueue.main.async { + UIApplication.shared.open(url) + } + } + } + } + + + private static func isImageFile(at url: URL) -> Bool { + // Assumes there *is* an extension, as per your requirement + let ext = url.pathExtension.lowercased() + guard let type = UTType(filenameExtension: ext) else { + return false + } + + return type.conforms(to: .image) + } + + + private static func isVideoFile(at url: URL) -> Bool { + let ext = url.pathExtension.lowercased() + guard let type = UTType(filenameExtension: ext) else { + return false + } + // `.movie` is the canonical file-based video type; `.audiovisualContent` + // catches some broader cases if needed. + return type.conforms(to: .movie) || type.conforms(to: .audiovisualContent) + } + + + private static func getVideoCreationDate(from url: URL) async -> Date? { + let asset = AVURLAsset(url: url) + + do { + // AVMetadataItem that typically contains an ISO‑8601 string + guard let item = try await asset.load(.creationDate), + let dateString = try await item.load(.stringValue) else { + return nil + } + + let formatter = ISO8601DateFormatter() + return formatter.date(from: dateString) + } + catch { + log("Failed to load creation date from \(url): \(error)") + return nil + } + } + + + /// Requests add-only Photos permission + private static func requestAddOnlyPhotoAuthorization() async -> PHAuthorizationStatus { + await withCheckedContinuation { continuation in + PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in + continuation.resume(returning: status) + } + } + } + + + enum SaveToPhotosError: Error, LocalizedError { + case noSavableContent + case permissionDenied + case saveFailed + + var errorDescription: String? { + switch self { + case .noSavableContent: + return "SaveToPhotosErrorNoSavableContent".localized() + case .permissionDenied: + return "SaveToPhotosErrorPermissionDenied".localized() + case .saveFailed: + return "SaveToPhotosErrorSaveFailed".localized() + } + } + } +} diff --git a/QuickDropiOS/View/UIAlert.swift b/QuickDropiOS/View/UIAlert.swift index 0f73825..1301947 100644 --- a/QuickDropiOS/View/UIAlert.swift +++ b/QuickDropiOS/View/UIAlert.swift @@ -42,6 +42,42 @@ final class ProgressAlert { vc.present(alert, animated: true) } + + func presentIncomingTransferDoneAlert( + title: String, + message: String, + onImportToPhotos: (() -> Void)? = nil, + onDone: (() -> Void)? = nil + ) { + guard let vc = topMostViewController() else { return } + + let alert = UIAlertController( + title: title, + message: message, + preferredStyle: .alert + ) + + // "Show in Files" + alert.addAction(UIAlertAction(title: "BrowseDownloadedFiles".localized(), style: .default) { _ in + FileManager.default.openDocumentFolder() + }) + + // Optional "Import to Photos" + if let importHandler = onImportToPhotos { + alert.addAction(UIAlertAction(title: "ImportToPhotos".localized(), style: .default) { _ in + importHandler() + }) + } + + // "Done" + alert.addAction(UIAlertAction(title: "Done".localized(), style: .cancel) { _ in + onDone?() + }) + + vc.present(alert, animated: true) + } + + // MARK: - Progress Alert private func showProgressAlert(onCancel: @escaping () -> Void) {