Refactoring

This commit is contained in:
Leon Böttger committed 2026-01-25 20:31:34 +01:00
1 parent 02e8af22ab
commit d748167f0e
5 files changed
+18 -156

No files matched your search

@@ -169,7 +169,7 @@ class InboundNearbyConnection: NearbyConnection {
fileInfo.progress?.unpublish()
#endif
downloadedFiles.append(fileInfo.destinationURL)
EXIFUtils.applyEXIFTimestamps(at: fileInfo.destinationURL)
EXIFUtils.applyTimestamps(at: fileInfo.destinationURL)
filesToBeReceived.removeValue(forKey: id)
@@ -223,7 +223,7 @@ class InboundNearbyConnection: NearbyConnection {
#endif
filesToBeReceived.removeValue(forKey: id)
downloadedFiles.append(fileInfo.destinationURL)
EXIFUtils.applyEXIFTimestamps(at: fileInfo.destinationURL)
EXIFUtils.applyTimestamps(at: fileInfo.destinationURL)
if filesToBeReceived.isEmpty {
log("[InboundNearbyConnection \(self.id)] Received file payload. Disconnecting...")
-90
View File
@@ -1,90 +0,0 @@
//
// EXIFUtils.swift
// QuickDrop
//
// Created by Leon Böttger on 10.11.25.
//
import Foundation
import ImageIO
import LUI
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
}
do {
// Set the "creation date"
try FileManager.default.setAttributes([.creationDate: exifDate], ofItemAtPath: fileURL.path)
}
catch {
log("[EXIFUtils] Failed to set file attributes for \(fileURL.path): \(error)")
}
}
/// Returns the EXIF original date of an image file, adjusted to the current time zone.
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 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"
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)
}
}
-14
View File
@@ -76,7 +76,6 @@
EB3B9F1C2EF7150400D6704E /* SmallSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8A2D7F7CF4004B3FF1 /* SmallSheetView.swift */; };
EB3B9F1D2EF7150400D6704E /* License.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E662E4F2F7C0052B7D9 /* License.swift */; };
EB3B9F1E2EF7150400D6704E /* LicenseWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7F54132D78E9E400989564 /* LicenseWindow.swift */; };
EB3B9F1F2EF7150400D6704E /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; };
EB3B9F202EF7150400D6704E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1129E0BF5100A442DA /* AppDelegate.swift */; };
EB3B9F212EF7150400D6704E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */; };
EB3B9F222EF7150400D6704E /* TutorialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7F54112D78E95C00989564 /* TutorialView.swift */; };
@@ -123,7 +122,6 @@
EB3B9F5B2EF7151900D6704E /* wire_format.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2929E18CB500A442DA /* wire_format.pb.swift */; };
EB3B9F5C2EF7151900D6704E /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
EB3B9F5D2EF7151900D6704E /* device_to_device_messages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2A29E18CB500A442DA /* device_to_device_messages.pb.swift */; };
EB3B9F5E2EF7151900D6704E /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; };
EB3B9F5F2EF7151900D6704E /* Attachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB950B732E520954002D68FA /* Attachments.swift */; };
EB3B9F602EF7151900D6704E /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
EB3B9F612EF7151900D6704E /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B7762E59BA9F007D9792 /* AppGroup.swift */; };
@@ -160,10 +158,6 @@
EB3B9F952EF715A000D6704E /* QRCode in Frameworks */ = {isa = PBXBuildFile; productRef = EB3B9F942EF715A000D6704E /* QRCode */; };
EB3B9F972EF715A000D6704E /* LUI in Frameworks */ = {isa = PBXBuildFile; productRef = EB3B9F962EF715A000D6704E /* LUI */; };
EB3B9F992EF715A000D6704E /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = EB3B9F982EF715A000D6704E /* ZIPFoundation */; };
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 */; };
EB53D8C42EF496AB004C95BA /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = EB53D8C32EF496AB004C95BA /* ZIPFoundation */; };
@@ -514,7 +508,6 @@
EB3B9F492EF7150400D6704E /* QuickDrop copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "QuickDrop copy-Info.plist"; path = "/Users/leonboettger/QuickDrop Backup with git/QuickDrop copy-Info.plist"; sourceTree = "<absolute>"; };
EB3B9F892EF7151900D6704E /* QuickDropShareExtensionGitHub.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = QuickDropShareExtensionGitHub.appex; sourceTree = BUILT_PRODUCTS_DIR; };
EB3B9F8A2EF7151900D6704E /* QuickDropShareExtension copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "QuickDropShareExtension copy-Info.plist"; path = "/Users/leonboettger/QuickDrop Backup with git/QuickDropShareExtension copy-Info.plist"; sourceTree = "<absolute>"; };
EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXIFUtils.swift; sourceTree = "<group>"; };
EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+DocumentsOpener.swift"; sourceTree = "<group>"; };
EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
EB5C36952E34EBBD0052EFD7 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/ShareViewController.xcstrings; sourceTree = "<group>"; };
@@ -762,7 +755,6 @@
EB16B7762E59BA9F007D9792 /* AppGroup.swift */,
EB886E512E4E19390052B7D9 /* HKDF.swift */,
EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */,
EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */,
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */,
);
path = Util;
@@ -1246,7 +1238,6 @@
EB5C37C72E377A780052EFD7 /* wire_format.pb.swift in Sources */,
EB5C37C82E377A780052EFD7 /* ukey.pb.swift in Sources */,
EB5C37C92E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */,
EB4F9D222EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
EB950B742E520957002D68FA /* Attachments.swift in Sources */,
EB1325D32E701D8E00FD749C /* Settings.swift in Sources */,
EB16B7782E59BAA1007D9792 /* AppGroup.swift in Sources */,
@@ -1306,7 +1297,6 @@
EB96ED8C2D7F7E7B004B3FF1 /* SmallSheetView.swift in Sources */,
EB886E682E4F2F800052B7D9 /* License.swift in Sources */,
EB7F54142D78E9E700989564 /* LicenseWindow.swift in Sources */,
EB4F9D202EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
69DA9A1229E0BF5100A442DA /* AppDelegate.swift in Sources */,
EB6F62D72D91F4AF00534EC2 /* SettingsView.swift in Sources */,
EB7F54122D78E96000989564 /* TutorialView.swift in Sources */,
@@ -1361,7 +1351,6 @@
EB3B9F1C2EF7150400D6704E /* SmallSheetView.swift in Sources */,
EB3B9F1D2EF7150400D6704E /* License.swift in Sources */,
EB3B9F1E2EF7150400D6704E /* LicenseWindow.swift in Sources */,
EB3B9F1F2EF7150400D6704E /* EXIFUtils.swift in Sources */,
EB3B9F202EF7150400D6704E /* AppDelegate.swift in Sources */,
EB3B9F212EF7150400D6704E /* SettingsView.swift in Sources */,
EB3B9F222EF7150400D6704E /* TutorialView.swift in Sources */,
@@ -1403,7 +1392,6 @@
EB3B9F5B2EF7151900D6704E /* wire_format.pb.swift in Sources */,
EB3B9F5C2EF7151900D6704E /* ukey.pb.swift in Sources */,
EB3B9F5D2EF7151900D6704E /* device_to_device_messages.pb.swift in Sources */,
EB3B9F5E2EF7151900D6704E /* EXIFUtils.swift in Sources */,
EB3B9F5F2EF7151900D6704E /* Attachments.swift in Sources */,
EB3B9F602EF7151900D6704E /* Settings.swift in Sources */,
EB3B9F612EF7151900D6704E /* AppGroup.swift in Sources */,
@@ -1453,7 +1441,6 @@
EBBC93B32EC8B76600F541A1 /* LocalNetworkPermissionView.swift in Sources */,
EB97A00F2E511124008B8C5C /* ReceiveModel.swift in Sources */,
EB5C37FE2E377CE70052EFD7 /* offline_wire_formats.pb.swift in Sources */,
EB4F9D1F2EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
EB886E6A2E4F37B90052B7D9 /* ErrorAlertHandler.swift in Sources */,
EB886E5E2E4F13EB0052B7D9 /* Zip.swift in Sources */,
EB5C383D2E38C2D90052EFD7 /* AudioManager.swift in Sources */,
@@ -1489,7 +1476,6 @@
EB950B4B2E520436002D68FA /* offline_wire_formats.pb.swift in Sources */,
EB950B4C2E520436002D68FA /* InboundNearbyConnection.swift in Sources */,
EB16B6132E58B4B9007D9792 /* IdentityManager.swift in Sources */,
EB4F9D212EC26C8100A74A0F /* EXIFUtils.swift in Sources */,
EB950B4D2E520436002D68FA /* NearbyConnection.swift in Sources */,
EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */,
EB950B582E5205B4002D68FA /* Zip.swift in Sources */,
@@ -52,7 +52,7 @@
"location" : "https://github.com/leonboe1/LUI.git",
"state" : {
"branch" : "main",
"revision" : "e7bb652c44d38f64d7d056723aa5cd01cb3e0a8d"
"revision" : "b58913bbbb648ed24babf30bd8bcdff2157d8d84"
}
},
{
+15 -49
View File
@@ -19,7 +19,7 @@ struct PhotoManager {
/// Returns if there are photos or videos among the given URLs.
static func hasPhotosOrVideos(at urls: [URL]) -> Bool {
urls.contains(where: { isImageFile(at: $0) || isVideoFile(at: $0) })
urls.contains(where: { EXIFUtils.isImageFile(at: $0) || EXIFUtils.isVideoFile(at: $0) })
}
@@ -30,9 +30,6 @@ struct PhotoManager {
/// - 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
@@ -41,16 +38,15 @@ struct PhotoManager {
var imageItems: [ImageItem] = []
var videoItems: [VideoItem] = []
// Extract creation date for image and video
for sourceURL in candidates {
if isImageFile(at: sourceURL) {
let exifDate = EXIFUtils.exifOriginalDate(from: sourceURL)
imageItems.append(ImageItem(fileURL: sourceURL, creationDate: exifDate))
} else {
let videoDate = getVideoCreationDate(from: sourceURL)
videoItems.append(VideoItem(fileURL: sourceURL, creationDate: videoDate))
for sourceURL in urls {
if let (type, date) = EXIFUtils.originalDate(from: sourceURL) {
if type == .image {
imageItems.append(ImageItem(fileURL: sourceURL, creationDate: date))
}
if type == .video {
videoItems.append(VideoItem(fileURL: sourceURL, creationDate: date))
}
}
}
@@ -116,40 +112,6 @@ struct PhotoManager {
}
private static func isImageFile(at url: URL) -> Bool {
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) -> Date? {
let asset = AVURLAsset(url: url)
// AVMetadataItem that typically contains an ISO‑8601 string
guard let item = asset.creationDate,
let dateString = item.stringValue else {
return nil
}
let formatter = ISO8601DateFormatter()
return formatter.date(from: dateString)
}
/// Requests add-only Photos permission
private static func requestAddOnlyPhotoAuthorization() async -> PHAuthorizationStatus {
await withCheckedContinuation { continuation in
@@ -158,8 +120,12 @@ struct PhotoManager {
}
}
}
}
// MARK: - Private types
private extension PhotoManager {
private enum SaveToPhotosError: Error, LocalizedError {
case noSavableContent
case permissionDenied