macOS: Add ability to save images/videos directly to photos app

This commit is contained in:
Leon Böttger committed 2025-11-18 19:16:07 +01:00
1 parent 4ddf39817c
commit 658a2ff28e
7 files changed
+2251 -867

No files matched your search

+74 -6
View File
@@ -111,6 +111,9 @@
EB5C383F2E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
EB5C38402E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
EB5C38412E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
EB6E6AB42ECBA46D00E71D57 /* PhotoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBBEE1452EC9D92F00AA7831 /* PhotoManager.swift */; };
EB6E6B212ECBA95000E71D57 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB6E6B202ECBA95000E71D57 /* InfoPlist.xcstrings */; };
EB6E6B222ECBA95000E71D57 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB6E6B202ECBA95000E71D57 /* InfoPlist.xcstrings */; };
EB6F62D72D91F4AF00534EC2 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */; };
EB704A822E4A7BB3008E608F /* DistributionDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB704A812E4A7BAE008E608F /* DistributionDetector.swift */; };
EB704A832E4B10D4008E608F /* DistributionDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB704A812E4A7BAE008E608F /* DistributionDetector.swift */; };
@@ -342,6 +345,7 @@
EB5C37E22E377C2E0052EFD7 /* QuickDropiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickDropiOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
EB5C38382E38C1F60052EFD7 /* justMaybe.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = justMaybe.mp3; sourceTree = "<group>"; };
EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLogin.swift; sourceTree = "<group>"; };
EB6E6B202ECBA95000E71D57 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
EB704A812E4A7BAE008E608F /* DistributionDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DistributionDetector.swift; sourceTree = "<group>"; };
EB721F232E75AFA900F2099F /* QuickDropIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = QuickDropIcon.icon; sourceTree = "<group>"; };
@@ -493,6 +497,7 @@
EBC05C6B2E33B57100EECC18 /* Util */,
EBC05C702E33B57D00EECC18 /* Plus */,
69DA9A1329E0BF5200A442DA /* Assets.xcassets */,
EB6E6B202ECBA95000E71D57 /* InfoPlist.xcstrings */,
EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */,
69DA9A1829E0BF5200A442DA /* QuickDrop.entitlements */,
);
@@ -874,6 +879,7 @@
699DEBA92AB0573200115D22 /* ShareViewController.xib in Resources */,
EB96ED8D2D7F7EA3004B3FF1 /* Assets.xcassets in Resources */,
EB5C383A2E38C1F60052EFD7 /* justMaybe.mp3 in Resources */,
EB6E6B212ECBA95000E71D57 /* InfoPlist.xcstrings in Resources */,
EB950B5A2E52065A002D68FA /* timeIsNow.mp3 in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -888,6 +894,7 @@
EBC05C8C2E33B59E00EECC18 /* timeIsNow.mp3 in Resources */,
EB721F252E75AFA900F2099F /* QuickDropIcon.icon in Resources */,
EB5C38392E38C1F60052EFD7 /* justMaybe.mp3 in Resources */,
EB6E6B222ECBA95000E71D57 /* InfoPlist.xcstrings in Resources */,
EB7654782E525ACC006B4AA3 /* MainMenu.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1011,6 +1018,7 @@
EB7F54122D78E96000989564 /* TutorialView.swift in Sources */,
EB704A822E4A7BB3008E608F /* DistributionDetector.swift in Sources */,
EB9925812E45D53E00357055 /* sharing_enums.pb.swift in Sources */,
EB6E6AB42ECBA46D00E71D57 /* PhotoManager.swift in Sources */,
EB96ED8F2D7F8105004B3FF1 /* Localized.swift in Sources */,
EB7CD1452D6FB0C300CB0CB5 /* Logger.swift in Sources */,
EB16B77A2E59BAA1007D9792 /* AppGroup.swift in Sources */,
@@ -1150,6 +1158,7 @@
699DEBAF2AB0573200115D22 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
@@ -1158,9 +1167,15 @@
CURRENT_PROJECT_VERSION = "${SHARED_BUILD}";
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_HARDENED_RUNTIME = NO;
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
ENABLE_USER_SELECTED_FILES = readonly;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShareExtension/Info.plist;
@@ -1178,6 +1193,12 @@
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
REGISTER_APP_GROUPS = YES;
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) EXTENSION";
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -1188,6 +1209,7 @@
699DEBB02AB0573200115D22 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
@@ -1196,9 +1218,15 @@
CURRENT_PROJECT_VERSION = "${SHARED_BUILD}";
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_HARDENED_RUNTIME = NO;
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
ENABLE_USER_SELECTED_FILES = readonly;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShareExtension/Info.plist;
@@ -1216,6 +1244,12 @@
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
REGISTER_APP_GROUPS = YES;
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = EXTENSION;
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*]" = "";
@@ -1362,6 +1396,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = QuickDropIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
AUTOMATION_APPLE_EVENTS = NO;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = QuickDrop/QuickDrop.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
@@ -1372,18 +1407,28 @@
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_HARDENED_RUNTIME = NO;
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
ENABLE_USER_SELECTED_FILES = readwrite;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = QuickDrop/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = QuickDrop;
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Leon Böttger";
INFOPLIST_KEY_NSMainNibFile = MainMenu;
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "The photo library permission is required to save received photos and videos to your photo library.";
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -1396,6 +1441,12 @@
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
REGISTER_APP_GROUPS = YES;
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -1408,6 +1459,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = QuickDropIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
AUTOMATION_APPLE_EVENTS = NO;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = QuickDrop/QuickDrop.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
@@ -1418,18 +1470,28 @@
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_HARDENED_RUNTIME = NO;
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
ENABLE_USER_SELECTED_FILES = readwrite;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = QuickDrop/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = QuickDrop;
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Leon Böttger";
INFOPLIST_KEY_NSMainNibFile = MainMenu;
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "The photo library permission is required to save received photos and videos to your photo library.";
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -1442,6 +1504,12 @@
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
REGISTER_APP_GROUPS = YES;
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_VERSION = 5.0;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+85 -18
View File
@@ -8,21 +8,29 @@
import SwiftUI
import AppKit
let toastViewSize = CGSize(width: 400, height: 100)
struct QuickDropToastView: View {
@ObservedObject var settings = Settings.shared
@ObservedObject var receiveModel: ReceiveModel
public var onCancel: () -> Void = {}
@State var autoHider: DispatchWorkItem? = nil
init(receiveModel: ReceiveModel,
onCancel: @escaping () -> Void = {}) {
/// Cancel while receiving.
public var onCancel: () -> Void
init(
receiveModel: ReceiveModel,
onCancel: @escaping () -> Void = {}
) {
self.receiveModel = receiveModel
self.onCancel = onCancel
}
public var body: some View {
let isDone = receiveModel.toastActions != nil
VStack(spacing: 12) {
HStack(spacing: 12) {
ZStack(alignment: .bottomLeading) {
@@ -36,7 +44,8 @@ struct QuickDropToastView: View {
VStack(alignment: .leading, spacing: 2) {
Text(String("QuickDrop"))
.font(.system(size: 16, weight: .semibold))
Text("Receiving")
Text(isDone ? "FileTransferCompleted" : "Receiving")
.font(.system(size: 13))
.foregroundColor(.secondary)
.lineLimit(1)
@@ -46,17 +55,54 @@ struct QuickDropToastView: View {
Spacer(minLength: 12)
}
HStack() {
CapsuleProgress(value: receiveModel.progress ?? 1.0)
ZStack {
if let actions = receiveModel.toastActions {
HStack(spacing: 8) {
QuickDropToastViewButton(title: "OpenInFinder") {
actions.openFilesAction()
actions.closeToastAction()
}
Button(action: onCancel) {
Image(systemName: "xmark")
.font(.system(size: 10, weight: .semibold))
.foregroundColor(.secondary)
.frame(width: 18, height: 18)
if let onImportToPhotos = actions.importPhotosAction {
QuickDropToastViewButton(title: "ImportToPhotos") {
onImportToPhotos()
actions.closeToastAction()
}
}
Spacer()
QuickDropToastViewButton(title: "Done", action: actions.closeToastAction)
.onAppear {
self.autoHider = DispatchWorkItem(block: {
actions.closeToastAction()
})
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
self.autoHider?.perform()
}
}
.onDisappear {
self.autoHider?.cancel()
}
}
.buttonStyle(.bordered)
.controlSize(.small)
}
.buttonStyle(PlainButtonStyle())
HStack {
CapsuleProgress(value: receiveModel.progress ?? 1.0)
Button(action: onCancel) {
Image(systemName: "xmark")
.font(.system(size: 10, weight: .semibold))
.foregroundColor(.secondary)
.frame(width: 18, height: 18)
}
.buttonStyle(PlainButtonStyle())
}
.opacity(isDone ? 0 : 1)
}
}
.padding(16)
@@ -75,6 +121,25 @@ struct QuickDropToastView: View {
}
fileprivate struct QuickDropToastViewButton: View {
let title: String
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title.localized())
.padding(.horizontal, 12)
.padding(.vertical, 4)
.background(
Color.gray.opacity(0.15)
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
)
}
.buttonStyle(.plain) // prevents blue macOS button look
}
}
// NSVisualEffectView wrapper for macOS 11 compatible blur/vibrancy
struct VisualEffectView: NSViewRepresentable {
var material: NSVisualEffectView.Material = .hudWindow
@@ -110,7 +175,7 @@ struct CapsuleProgress: View {
ZStack(alignment: .leading) {
Capsule()
.fill(track)
Capsule() // Use a plain rectangle
Capsule()
.fill(fill)
.mask(
Capsule()
@@ -128,20 +193,22 @@ struct CapsuleProgress: View {
// MARK: - Preview
struct QuickDropToastView_Previews: PreviewProvider {
struct Demo: View {
@State var model = ReceiveModel(controlPlusScreen: { _ in })
@State var done = false
var body: some View {
QuickDropToastView(
receiveModel: model,
onCancel: { }
)
.padding()
.frame(width: toastViewSize.width, height: toastViewSize.height)
.padding(100)
.background(Color.white.opacity(0.2))
.onAppear {
model.progress = 0.01
}
}
}
static var previews: some View { Demo() }
}
+51 -7
View File
@@ -22,6 +22,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
#if os(macOS)
@Published var progress: Double? = nil
@Published var toastActions: ToastViewAction? = nil
private var toastWindow: NSWindow?
private var toastHosting: NSHostingView<QuickDropToastView>?
#endif
@@ -163,6 +164,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
}, completion: {})
#else
DispatchQueue.main.async {
self.toastActions = nil
self.progress = averageProgress
self.showQuickDropToast(for: connectionID)
}
@@ -176,8 +178,6 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
#if os(macOS)
DispatchQueue.main.async {
self.hideQuickDropToast()
if self.processes.isEmpty {
self.progress = nil
}
@@ -200,8 +200,12 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
controlPlusScreen(false)
ErrorAlertHandler.shared.showErrorAlert(for: device.name ?? "Android", error: error)
} else {
DispatchQueue.main.async {
self.hideQuickDropToast()
}
} else {
#if os(iOS)
doubleVibration()
if !savedFiles.isEmpty {
@@ -227,10 +231,42 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
#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)
let openFilesAction = {
if !savedFiles.isEmpty {
log("[SaveFilesManager] Opening \(savedFiles.count) file(s) in Finder.")
NSWorkspace.shared.activateFileViewerSelecting(savedFiles)
}
}
let hasPhotoOrVideos = PhotoManager.hasPhotosOrVideos(at: savedFiles)
let importPhotosAction: (()->())? = hasPhotoOrVideos ? {
Task {
do {
try await PhotoManager.saveMediaToPhotoLibrary(from: savedFiles)
}
catch {
DispatchQueue.main.async {
let alert = NSAlert()
alert.alertStyle = .critical
alert.messageText = "CouldNotSaveMediaToPhotoLibrary".localized()
alert.informativeText = error.localizedDescription
alert.addButton(withTitle: "CloseAlert".localized())
alert.runModal()
}
}
}
} : nil
DispatchQueue.main.async {
withAnimation {
self.toastActions = ToastViewAction(openFilesAction: openFilesAction, importPhotosAction: importPhotosAction, closeToastAction: self.hideQuickDropToast)
}
// Show received files if wanted
if Settings.shared.openFinderAfterReceiving {
self.toastActions?.openFilesAction()
}
}
// If distributed directly, do not request review here as it does not work
@@ -336,7 +372,15 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
window.orderOut(nil)
self.toastWindow = nil
self.toastHosting = nil
self.toastActions = nil
})
}
struct ToastViewAction {
let openFilesAction: () -> ()
let importPhotosAction: (() -> ())?
let closeToastAction: () -> ()
}
#endif
}
-666
View File
@@ -5,233 +5,11 @@
"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" : ""
}
}
}
},
@@ -239,233 +17,11 @@
"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" : ""
}
}
}
},
@@ -473,233 +29,11 @@
"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" : ""
}
}
}
},
+21 -18
View File
@@ -5,11 +5,15 @@
// Created by Leon Böttger on 16.11.25.
//
#if os(iOS)
import UIKit
#else
import AppKit
#endif
import Photos
import UniformTypeIdentifiers
struct PhotoManager {
/// Returns if there are photos or videos among the given URLs.
@@ -44,7 +48,7 @@ struct PhotoManager {
let exifDate = EXIFUtils.exifOriginalDate(from: sourceURL)
imageItems.append(ImageItem(fileURL: sourceURL, creationDate: exifDate))
} else {
let videoDate = await getVideoCreationDate(from: sourceURL)
let videoDate = getVideoCreationDate(from: sourceURL)
videoItems.append(VideoItem(fileURL: sourceURL, creationDate: videoDate))
}
}
@@ -96,10 +100,16 @@ struct PhotoManager {
// Open the Photos app
if openPhotosOnSuccess {
if let url = URL(string: "photos-redirect://") {
DispatchQueue.main.async {
DispatchQueue.main.async {
#if os(iOS)
if let url = URL(string: "photos-redirect://") {
UIApplication.shared.open(url)
}
#else
if let url = URL(string: "photos-navigation://album?name=recently-saved") {
NSWorkspace.shared.open(url)
}
#endif
}
}
}
@@ -126,23 +136,16 @@ struct PhotoManager {
}
private static func getVideoCreationDate(from url: URL) async -> Date? {
private static func getVideoCreationDate(from url: URL) -> 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)")
// 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)
}