15 Commits
34 changed files with 14811 additions and 6950 deletions

No files matched your search

+1
View File
@@ -2,3 +2,4 @@ DerivedData/
.DerivedData/ .DerivedData/
.DS_Store .DS_Store
xcuserdata/ xcuserdata/
/research/
+7 -7
View File
@@ -254,27 +254,27 @@ public struct TransferMetadata {
let type = UTType(mimeType: mimeType) ?? UTType(filenameExtension: ext) let type = UTType(mimeType: mimeType) ?? UTType(filenameExtension: ext)
if let type, type.conforms(to: .pdf) { if let type, type.conforms(to: .pdf) {
return UTType.pdf.localizedDescription ?? "PDF" return "PDF".localized()
} }
if let type, type.conforms(to: .image) { if let type, type.conforms(to: .image) {
return UTType.image.localizedDescription ?? "Image" return "Image".localized()
} }
if let type, type.conforms(to: .video) || type.conforms(to: .movie) { if let type, type.conforms(to: .video) || type.conforms(to: .movie) {
return UTType.video.localizedDescription ?? "Video" return "Video".localized()
} }
if let type, type.conforms(to: .audio) { if let type, type.conforms(to: .audio) {
return UTType.audio.localizedDescription ?? "Audio" return "Audio".localized()
} }
if let type, type.conforms(to: .text) { if let type, type.conforms(to: .text) {
return UTType.text.localizedDescription ?? "Text" return "Text".localized()
} }
if let type, type.conforms(to: .archive) { if let type, type.conforms(to: .archive) {
return UTType.archive.localizedDescription ?? "Archive" return "Archive".localized()
} }
if let type, let description = type.localizedDescription { if let type, let description = type.localizedDescription {
return description return description
} }
return "File" return "File".localized()
} }
public func getDescription(deviceName: String) -> String { public func getDescription(deviceName: String) -> String {
+182 -8
View File
@@ -28,6 +28,19 @@
A1F5B3DC2F3A1A0100A1B2C7 /* PairingToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */; }; A1F5B3DC2F3A1A0100A1B2C7 /* PairingToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */; };
A1F5B3DD2F3A1A0100A1B2C8 /* PairingQrSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */; }; A1F5B3DD2F3A1A0100A1B2C8 /* PairingQrSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */; };
A1F5B3DE2F3A1A0100A1B2C9 /* PairingQrSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */; }; A1F5B3DE2F3A1A0100A1B2C9 /* PairingQrSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */; };
A3D000000000000000000202 /* BluetoothPermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3D000000000000000000002 /* BluetoothPermissionManager.swift */; };
A3D000000000000000000203 /* BluetoothPermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3D000000000000000000002 /* BluetoothPermissionManager.swift */; };
A6D000000000000000000101 /* AndroidAirDropModeDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6D000000000000000000001 /* AndroidAirDropModeDetector.swift */; };
A6D000000000000000000102 /* AndroidAirDropModeDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6D000000000000000000001 /* AndroidAirDropModeDetector.swift */; };
A6D000000000000000000201 /* AndroidAirDropModeDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6D000000000000000000002 /* AndroidAirDropModeDetectorTests.swift */; };
A7D000000000000000000101 /* AndroidAirDropModeIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D000000000000000000001 /* AndroidAirDropModeIssueView.swift */; };
A7D000000000000000000102 /* AndroidAirDropModeIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D000000000000000000001 /* AndroidAirDropModeIssueView.swift */; };
A7D000000000000000000103 /* AndroidAirDropModeIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D000000000000000000001 /* AndroidAirDropModeIssueView.swift */; };
A7D000000000000000000104 /* AndroidAirDropModeIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D000000000000000000001 /* AndroidAirDropModeIssueView.swift */; };
A8D000000000000000000101 /* BluetoothPermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3D000000000000000000002 /* BluetoothPermissionManager.swift */; };
A8D000000000000000000102 /* AndroidAirDropModeDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6D000000000000000000001 /* AndroidAirDropModeDetector.swift */; };
A8D000000000000000000103 /* AndroidAirDropModeIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D000000000000000000001 /* AndroidAirDropModeIssueView.swift */; };
A8D000000000000000000104 /* IssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C7F2E33B58300EECC18 /* IssueView.swift */; };
EB05DFBE2E59FC8900EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; }; EB05DFBE2E59FC8900EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
EB05DFBF2E59FD0A00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; }; EB05DFBF2E59FD0A00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; }; EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
@@ -246,6 +259,10 @@
EB5C383F2E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; }; EB5C383F2E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
EB5C38402E38C49D0052EFD7 /* 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 */; }; EB5C38412E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
EB63EDB72FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB63EDB62FF42390000CC61F /* ModelessAlertPresenter.swift */; };
EB63EDB92FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB63EDB62FF42390000CC61F /* ModelessAlertPresenter.swift */; };
EB63EDBA2FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB63EDB62FF42390000CC61F /* ModelessAlertPresenter.swift */; };
EB63EDBB2FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB63EDB62FF42390000CC61F /* ModelessAlertPresenter.swift */; };
EB6E51A52F2F9370006D9812 /* IntroductionSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6E51A42F2F9370006D9812 /* IntroductionSheetManager.swift */; }; EB6E51A52F2F9370006D9812 /* IntroductionSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6E51A42F2F9370006D9812 /* IntroductionSheetManager.swift */; };
EB6E51A62F2F9370006D9812 /* IntroductionSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6E51A42F2F9370006D9812 /* IntroductionSheetManager.swift */; }; EB6E51A62F2F9370006D9812 /* IntroductionSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6E51A42F2F9370006D9812 /* IntroductionSheetManager.swift */; };
EB6E51AB2F311ED4006D9812 /* QuickDropIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = EB721F232E75AFA900F2099F /* QuickDropIcon.icon */; }; EB6E51AB2F311ED4006D9812 /* QuickDropIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = EB721F232E75AFA900F2099F /* QuickDropIcon.icon */; };
@@ -382,6 +399,13 @@
remoteGlobalIDString = 699DEBA02AB0573200115D22; remoteGlobalIDString = 699DEBA02AB0573200115D22;
remoteInfo = ShareExtension; remoteInfo = ShareExtension;
}; };
E14627D44A6E1CA296A9A53D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 69DA9A0629E0BF5100A442DA /* Project object */;
proxyType = 1;
remoteGlobalIDString = 69DA9A0D29E0BF5100A442DA;
remoteInfo = QuickDrop;
};
EB3B9F8C2EF7153000D6704E /* PBXContainerItemProxy */ = { EB3B9F8C2EF7153000D6704E /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
containerPortal = 69DA9A0629E0BF5100A442DA /* Project object */; containerPortal = 69DA9A0629E0BF5100A442DA /* Project object */;
@@ -495,6 +519,7 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
156115A05E122702111639A0 /* QuickDropTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = QuickDropTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
691F53B92ABB70840089FD92 /* DeviceListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceListCell.swift; sourceTree = "<group>"; }; 691F53B92ABB70840089FD92 /* DeviceListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceListCell.swift; sourceTree = "<group>"; };
691F53BA2ABB70840089FD92 /* DeviceListCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DeviceListCell.xib; sourceTree = "<group>"; }; 691F53BA2ABB70840089FD92 /* DeviceListCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DeviceListCell.xib; sourceTree = "<group>"; };
691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutboundNearbyConnection.swift; sourceTree = "<group>"; }; 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutboundNearbyConnection.swift; sourceTree = "<group>"; };
@@ -523,6 +548,10 @@
A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardSyncNotificationPresenter.swift; sourceTree = "<group>"; }; A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardSyncNotificationPresenter.swift; sourceTree = "<group>"; };
A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingToken.swift; sourceTree = "<group>"; }; A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingToken.swift; sourceTree = "<group>"; };
A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingQrSheetManager.swift; sourceTree = "<group>"; }; A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingQrSheetManager.swift; sourceTree = "<group>"; };
A3D000000000000000000002 /* BluetoothPermissionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothPermissionManager.swift; sourceTree = "<group>"; };
A6D000000000000000000001 /* AndroidAirDropModeDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AndroidAirDropModeDetector.swift; sourceTree = "<group>"; };
A6D000000000000000000002 /* AndroidAirDropModeDetectorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AndroidAirDropModeDetectorTests.swift; sourceTree = "<group>"; };
A7D000000000000000000001 /* AndroidAirDropModeIssueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AndroidAirDropModeIssueView.swift; sourceTree = "<group>"; };
EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedDevicesView.swift; sourceTree = "<group>"; }; EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedDevicesView.swift; sourceTree = "<group>"; };
EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyBonjourServiceResolver.swift; sourceTree = "<group>"; }; EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyBonjourServiceResolver.swift; sourceTree = "<group>"; };
EB1325D22E701D8900FD749C /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; }; EB1325D22E701D8900FD749C /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
@@ -550,6 +579,7 @@
EB5C37E22E377C2E0052EFD7 /* QuickDropiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickDropiOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; EB5C37E22E377C2E0052EFD7 /* QuickDropiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickDropiOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
EB5C38382E38C1F60052EFD7 /* justMaybe.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = justMaybe.mp3; sourceTree = "<group>"; }; EB5C38382E38C1F60052EFD7 /* justMaybe.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = justMaybe.mp3; sourceTree = "<group>"; };
EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLogin.swift; sourceTree = "<group>"; }; EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLogin.swift; sourceTree = "<group>"; };
EB63EDB62FF42390000CC61F /* ModelessAlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelessAlertPresenter.swift; sourceTree = "<group>"; };
EB6E51A42F2F9370006D9812 /* IntroductionSheetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroductionSheetManager.swift; sourceTree = "<group>"; }; EB6E51A42F2F9370006D9812 /* IntroductionSheetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroductionSheetManager.swift; sourceTree = "<group>"; };
EB6E6B202ECBA95000E71D57 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; 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>"; }; EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
@@ -673,6 +703,13 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
F5F5F63DAC90375051BA9F4E /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
@@ -713,6 +750,7 @@
EBC05C8B2E33B59E00EECC18 /* Resources */, EBC05C8B2E33B59E00EECC18 /* Resources */,
EB3B9F492EF7150400D6704E /* QuickDrop copy-Info.plist */, EB3B9F492EF7150400D6704E /* QuickDrop copy-Info.plist */,
EB3B9F8A2EF7151900D6704E /* QuickDropShareExtension copy-Info.plist */, EB3B9F8A2EF7151900D6704E /* QuickDropShareExtension copy-Info.plist */,
9E6A957675D279417AA88926 /* QuickDropTests */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -725,6 +763,7 @@
EB950B2D2E52027F002D68FA /* iOSShareExtension.appex */, EB950B2D2E52027F002D68FA /* iOSShareExtension.appex */,
EB3B9F482EF7150400D6704E /* QuickDropGitHub.app */, EB3B9F482EF7150400D6704E /* QuickDropGitHub.app */,
EB3B9F892EF7151900D6704E /* QuickDropShareExtensionGitHub.appex */, EB3B9F892EF7151900D6704E /* QuickDropShareExtensionGitHub.appex */,
156115A05E122702111639A0 /* QuickDropTests.xctest */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -770,6 +809,14 @@
path = NearbyShare; path = NearbyShare;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
9E6A957675D279417AA88926 /* QuickDropTests */ = {
isa = PBXGroup;
children = (
A6D000000000000000000002 /* AndroidAirDropModeDetectorTests.swift */,
);
path = QuickDropTests;
sourceTree = "<group>";
};
A1F5B3DA2F3A1A0100A1B2C5 /* Pairing */ = { A1F5B3DA2F3A1A0100A1B2C5 /* Pairing */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -944,14 +991,17 @@
EBC05C6B2E33B57100EECC18 /* Util */ = { EBC05C6B2E33B57100EECC18 /* Util */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A3D000000000000000000002 /* BluetoothPermissionManager.swift */,
A1C0DE102F4C000100ABC001 /* ClipboardSyncCoordinator.swift */, A1C0DE102F4C000100ABC001 /* ClipboardSyncCoordinator.swift */,
A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */, A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */,
EB1325F72E70287C00FD749C /* SendToExtension.swift */, EB1325F72E70287C00FD749C /* SendToExtension.swift */,
EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */, EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */,
EB63EDB62FF42390000CC61F /* ModelessAlertPresenter.swift */,
EB886E662E4F2F7C0052B7D9 /* License.swift */, EB886E662E4F2F7C0052B7D9 /* License.swift */,
EB6FE7D12EFEFB2700FF2BCF /* AllowedWorkMonitor.swift */, EB6FE7D12EFEFB2700FF2BCF /* AllowedWorkMonitor.swift */,
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */, EB36435F2D73A29F009CDFF8 /* AudioManager.swift */,
EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */, EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */,
A6D000000000000000000001 /* AndroidAirDropModeDetector.swift */,
EB1325EA2E7025D900FD749C /* SelfTest */, EB1325EA2E7025D900FD749C /* SelfTest */,
); );
path = Util; path = Util;
@@ -964,6 +1014,7 @@
EBC05C7E2E33B58300EECC18 /* FirewallIssueView.swift */, EBC05C7E2E33B58300EECC18 /* FirewallIssueView.swift */,
EBC05C7F2E33B58300EECC18 /* IssueView.swift */, EBC05C7F2E33B58300EECC18 /* IssueView.swift */,
EBC05C802E33B58300EECC18 /* NetworkFilterIssueView.swift */, EBC05C802E33B58300EECC18 /* NetworkFilterIssueView.swift */,
A7D000000000000000000001 /* AndroidAirDropModeIssueView.swift */,
); );
path = IssueViews; path = IssueViews;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -981,6 +1032,24 @@
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
32A98F34E7D10C21DC2EEA19 /* QuickDropTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = B473F6C65C8455203EA5FD18 /* Build configuration list for PBXNativeTarget "QuickDropTests" */;
buildPhases = (
BECDC1522AF83179791FA522 /* Sources */,
F5F5F63DAC90375051BA9F4E /* Frameworks */,
36CE9ED35EE7975FAB802535 /* Resources */,
);
buildRules = (
);
dependencies = (
C4F698F843B0C498BFC216AB /* PBXTargetDependency */,
);
name = QuickDropTests;
productName = QuickDropTests;
productReference = 156115A05E122702111639A0 /* QuickDropTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
699DEBA02AB0573200115D22 /* QuickDropShareExtension */ = { 699DEBA02AB0573200115D22 /* QuickDropShareExtension */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 699DEBB12AB0573200115D22 /* Build configuration list for PBXNativeTarget "QuickDropShareExtension" */; buildConfigurationList = 699DEBB12AB0573200115D22 /* Build configuration list for PBXNativeTarget "QuickDropShareExtension" */;
@@ -1137,6 +1206,9 @@
LastSwiftUpdateCheck = 1640; LastSwiftUpdateCheck = 1640;
LastUpgradeCheck = 2600; LastUpgradeCheck = 2600;
TargetAttributes = { TargetAttributes = {
32A98F34E7D10C21DC2EEA19 = {
CreatedOnToolsVersion = 26.0;
};
699DEBA02AB0573200115D22 = { 699DEBA02AB0573200115D22 = {
CreatedOnToolsVersion = 14.3.1; CreatedOnToolsVersion = 14.3.1;
}; };
@@ -1196,11 +1268,19 @@
EB950B2C2E52027F002D68FA /* iOSShareExtension */, EB950B2C2E52027F002D68FA /* iOSShareExtension */,
EB3B9EF02EF7150400D6704E /* QuickDropGitHub */, EB3B9EF02EF7150400D6704E /* QuickDropGitHub */,
EB3B9F4A2EF7151900D6704E /* QuickDropShareExtensionGitHub */, EB3B9F4A2EF7151900D6704E /* QuickDropShareExtensionGitHub */,
32A98F34E7D10C21DC2EEA19 /* QuickDropTests */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */ /* Begin PBXResourcesBuildPhase section */
36CE9ED35EE7975FAB802535 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
699DEB9F2AB0573200115D22 /* Resources */ = { 699DEB9F2AB0573200115D22 /* Resources */ = {
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -1312,8 +1392,10 @@
EBE5FF432EDB71810019B380 /* Configuration.swift in Sources */, EBE5FF432EDB71810019B380 /* Configuration.swift in Sources */,
EB6FE7D42EFEFB2700FF2BCF /* AllowedWorkMonitor.swift in Sources */, EB6FE7D42EFEFB2700FF2BCF /* AllowedWorkMonitor.swift in Sources */,
EBC05C872E33B58300EECC18 /* IssueView.swift in Sources */, EBC05C872E33B58300EECC18 /* IssueView.swift in Sources */,
A7D000000000000000000103 /* AndroidAirDropModeIssueView.swift in Sources */,
EBC05C882E33B58300EECC18 /* FirewallIssueView.swift in Sources */, EBC05C882E33B58300EECC18 /* FirewallIssueView.swift in Sources */,
EBC05C892E33B58300EECC18 /* ApIsolationIssueView.swift in Sources */, EBC05C892E33B58300EECC18 /* ApIsolationIssueView.swift in Sources */,
EB63EDBB2FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */,
699DEBA62AB0573200115D22 /* ShareViewController.swift in Sources */, 699DEBA62AB0573200115D22 /* ShareViewController.swift in Sources */,
EB96ED8B2D7F7CF7004B3FF1 /* SmallSheetView.swift in Sources */, EB96ED8B2D7F7CF7004B3FF1 /* SmallSheetView.swift in Sources */,
EBCC27F92D80C9F90079C478 /* Zip.swift in Sources */, EBCC27F92D80C9F90079C478 /* Zip.swift in Sources */,
@@ -1363,6 +1445,8 @@
EB16B6182E58B4C8007D9792 /* TrustStore.swift in Sources */, EB16B6182E58B4C8007D9792 /* TrustStore.swift in Sources */,
EBC05C5B2E33937200EECC18 /* ErrorAlertHandler.swift in Sources */, EBC05C5B2E33937200EECC18 /* ErrorAlertHandler.swift in Sources */,
EB1325D42E701D8E00FD749C /* Settings.swift in Sources */, EB1325D42E701D8E00FD749C /* Settings.swift in Sources */,
A3D000000000000000000202 /* BluetoothPermissionManager.swift in Sources */,
A6D000000000000000000101 /* AndroidAirDropModeDetector.swift in Sources */,
EB886E5D2E4F13EB0052B7D9 /* Zip.swift in Sources */, EB886E5D2E4F13EB0052B7D9 /* Zip.swift in Sources */,
EB5C37BF2E377A780052EFD7 /* ukey.pb.swift in Sources */, EB5C37BF2E377A780052EFD7 /* ukey.pb.swift in Sources */,
EB5C37C02E377A780052EFD7 /* securemessage.pb.swift in Sources */, EB5C37C02E377A780052EFD7 /* securemessage.pb.swift in Sources */,
@@ -1370,6 +1454,7 @@
EB5C37C22E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */, EB5C37C22E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */,
EB5C37C32E377A780052EFD7 /* wire_format.pb.swift in Sources */, EB5C37C32E377A780052EFD7 /* wire_format.pb.swift in Sources */,
EB16B6152E58B4B9007D9792 /* IdentityManager.swift in Sources */, EB16B6152E58B4B9007D9792 /* IdentityManager.swift in Sources */,
EB63EDB92FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */,
EB5C37C42E377A780052EFD7 /* securegcm.pb.swift in Sources */, EB5C37C42E377A780052EFD7 /* securegcm.pb.swift in Sources */,
EB97A0102E51CD13008B8C5C /* ReceiveModel.swift in Sources */, EB97A0102E51CD13008B8C5C /* ReceiveModel.swift in Sources */,
A1F5B3DD2F3A1A0100A1B2C8 /* PairingQrSheetManager.swift in Sources */, A1F5B3DD2F3A1A0100A1B2C8 /* PairingQrSheetManager.swift in Sources */,
@@ -1398,6 +1483,7 @@
EB6FE7D62EFEFB2700FF2BCF /* AllowedWorkMonitor.swift in Sources */, EB6FE7D62EFEFB2700FF2BCF /* AllowedWorkMonitor.swift in Sources */,
EBC05C822E33B58300EECC18 /* ApIsolationIssueView.swift in Sources */, EBC05C822E33B58300EECC18 /* ApIsolationIssueView.swift in Sources */,
EBC05C832E33B58300EECC18 /* FirewallIssueView.swift in Sources */, EBC05C832E33B58300EECC18 /* FirewallIssueView.swift in Sources */,
A7D000000000000000000101 /* AndroidAirDropModeIssueView.swift in Sources */,
EB1325FA2E70288300FD749C /* SendToExtension.swift in Sources */, EB1325FA2E70288300FD749C /* SendToExtension.swift in Sources */,
EB0A22E72FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */, EB0A22E72FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
EB1325F42E7025D900FD749C /* ApIsolationTester.swift in Sources */, EB1325F42E7025D900FD749C /* ApIsolationTester.swift in Sources */,
@@ -1410,6 +1496,14 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
BECDC1522AF83179791FA522 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A6D000000000000000000201 /* AndroidAirDropModeDetectorTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
EB3B9EFF2EF7150400D6704E /* Sources */ = { EB3B9EFF2EF7150400D6704E /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -1424,6 +1518,8 @@
EB3B9F082EF7150400D6704E /* TrustStore.swift in Sources */, EB3B9F082EF7150400D6704E /* TrustStore.swift in Sources */,
EB3B9F092EF7150400D6704E /* ErrorAlertHandler.swift in Sources */, EB3B9F092EF7150400D6704E /* ErrorAlertHandler.swift in Sources */,
EB3B9F0A2EF7150400D6704E /* Settings.swift in Sources */, EB3B9F0A2EF7150400D6704E /* Settings.swift in Sources */,
A3D000000000000000000203 /* BluetoothPermissionManager.swift in Sources */,
A6D000000000000000000102 /* AndroidAirDropModeDetector.swift in Sources */,
EB3B9F0B2EF7150400D6704E /* Zip.swift in Sources */, EB3B9F0B2EF7150400D6704E /* Zip.swift in Sources */,
EB3B9F0C2EF7150400D6704E /* ukey.pb.swift in Sources */, EB3B9F0C2EF7150400D6704E /* ukey.pb.swift in Sources */,
EB3B9F0D2EF7150400D6704E /* securemessage.pb.swift in Sources */, EB3B9F0D2EF7150400D6704E /* securemessage.pb.swift in Sources */,
@@ -1431,6 +1527,7 @@
EB3B9F0F2EF7150400D6704E /* device_to_device_messages.pb.swift in Sources */, EB3B9F0F2EF7150400D6704E /* device_to_device_messages.pb.swift in Sources */,
EB3B9F102EF7150400D6704E /* wire_format.pb.swift in Sources */, EB3B9F102EF7150400D6704E /* wire_format.pb.swift in Sources */,
EB3B9F112EF7150400D6704E /* IdentityManager.swift in Sources */, EB3B9F112EF7150400D6704E /* IdentityManager.swift in Sources */,
EB63EDB72FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */,
EB3B9F122EF7150400D6704E /* securegcm.pb.swift in Sources */, EB3B9F122EF7150400D6704E /* securegcm.pb.swift in Sources */,
EB3B9F132EF7150400D6704E /* ReceiveModel.swift in Sources */, EB3B9F132EF7150400D6704E /* ReceiveModel.swift in Sources */,
A1F5B3DE2F3A1A0100A1B2C9 /* PairingQrSheetManager.swift in Sources */, A1F5B3DE2F3A1A0100A1B2C9 /* PairingQrSheetManager.swift in Sources */,
@@ -1459,6 +1556,7 @@
EB6FE7D52EFEFB2700FF2BCF /* AllowedWorkMonitor.swift in Sources */, EB6FE7D52EFEFB2700FF2BCF /* AllowedWorkMonitor.swift in Sources */,
EB3B9F272EF7150400D6704E /* ApIsolationIssueView.swift in Sources */, EB3B9F272EF7150400D6704E /* ApIsolationIssueView.swift in Sources */,
EB3B9F282EF7150400D6704E /* FirewallIssueView.swift in Sources */, EB3B9F282EF7150400D6704E /* FirewallIssueView.swift in Sources */,
A7D000000000000000000102 /* AndroidAirDropModeIssueView.swift in Sources */,
EB3B9F292EF7150400D6704E /* SendToExtension.swift in Sources */, EB3B9F292EF7150400D6704E /* SendToExtension.swift in Sources */,
EB0A22E82FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */, EB0A22E82FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
EB3B9F2A2EF7150400D6704E /* ApIsolationTester.swift in Sources */, EB3B9F2A2EF7150400D6704E /* ApIsolationTester.swift in Sources */,
@@ -1483,8 +1581,10 @@
EB3B9F522EF7151900D6704E /* Configuration.swift in Sources */, EB3B9F522EF7151900D6704E /* Configuration.swift in Sources */,
EB6FE7D22EFEFB2700FF2BCF /* AllowedWorkMonitor.swift in Sources */, EB6FE7D22EFEFB2700FF2BCF /* AllowedWorkMonitor.swift in Sources */,
EB3B9F532EF7151900D6704E /* IssueView.swift in Sources */, EB3B9F532EF7151900D6704E /* IssueView.swift in Sources */,
A7D000000000000000000104 /* AndroidAirDropModeIssueView.swift in Sources */,
EB3B9F542EF7151900D6704E /* FirewallIssueView.swift in Sources */, EB3B9F542EF7151900D6704E /* FirewallIssueView.swift in Sources */,
EB3B9F552EF7151900D6704E /* ApIsolationIssueView.swift in Sources */, EB3B9F552EF7151900D6704E /* ApIsolationIssueView.swift in Sources */,
EB63EDBA2FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */,
EB3B9F562EF7151900D6704E /* ShareViewController.swift in Sources */, EB3B9F562EF7151900D6704E /* ShareViewController.swift in Sources */,
EB3B9F572EF7151900D6704E /* SmallSheetView.swift in Sources */, EB3B9F572EF7151900D6704E /* SmallSheetView.swift in Sources */,
EB3B9F582EF7151900D6704E /* Zip.swift in Sources */, EB3B9F582EF7151900D6704E /* Zip.swift in Sources */,
@@ -1538,6 +1638,8 @@
EB886E532E4E19390052B7D9 /* HKDF.swift in Sources */, EB886E532E4E19390052B7D9 /* HKDF.swift in Sources */,
EB5C37F82E377CE70052EFD7 /* SleepManager.swift in Sources */, EB5C37F82E377CE70052EFD7 /* SleepManager.swift in Sources */,
EB5C37F92E377CE70052EFD7 /* securegcm.pb.swift in Sources */, EB5C37F92E377CE70052EFD7 /* securegcm.pb.swift in Sources */,
A8D000000000000000000101 /* BluetoothPermissionManager.swift in Sources */,
A8D000000000000000000102 /* AndroidAirDropModeDetector.swift in Sources */,
EB5C37FA2E377CE70052EFD7 /* Helpers.swift in Sources */, EB5C37FA2E377CE70052EFD7 /* Helpers.swift in Sources */,
EB5C37FB2E377CE70052EFD7 /* InboundNearbyConnection.swift in Sources */, EB5C37FB2E377CE70052EFD7 /* InboundNearbyConnection.swift in Sources */,
EB0A22E62FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */, EB0A22E62FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
@@ -1554,6 +1656,8 @@
EB979F942E50CA23008B8C5C /* Configuration.swift in Sources */, EB979F942E50CA23008B8C5C /* Configuration.swift in Sources */,
EB979F952E50CA23008B8C5C /* ContentView.swift in Sources */, EB979F952E50CA23008B8C5C /* ContentView.swift in Sources */,
EB6FE7CF2EFEB42800FF2BCF /* NoLocalNetworkAccessAlertView.swift in Sources */, EB6FE7CF2EFEB42800FF2BCF /* NoLocalNetworkAccessAlertView.swift in Sources */,
A8D000000000000000000103 /* AndroidAirDropModeIssueView.swift in Sources */,
A8D000000000000000000104 /* IssueView.swift in Sources */,
EBBEE1462EC9D93400AA7831 /* PhotoManager.swift in Sources */, EBBEE1462EC9D93400AA7831 /* PhotoManager.swift in Sources */,
EB97A01A2E51EAA2008B8C5C /* UIAlert.swift in Sources */, EB97A01A2E51EAA2008B8C5C /* UIAlert.swift in Sources */,
EB979F962E50CA23008B8C5C /* FilePicker.swift in Sources */, EB979F962E50CA23008B8C5C /* FilePicker.swift in Sources */,
@@ -1622,6 +1726,12 @@
target = 699DEBA02AB0573200115D22 /* QuickDropShareExtension */; target = 699DEBA02AB0573200115D22 /* QuickDropShareExtension */;
targetProxy = 699DEBAC2AB0573200115D22 /* PBXContainerItemProxy */; targetProxy = 699DEBAC2AB0573200115D22 /* PBXContainerItemProxy */;
}; };
C4F698F843B0C498BFC216AB /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = QuickDrop;
target = 69DA9A0D29E0BF5100A442DA /* QuickDrop */;
targetProxy = E14627D44A6E1CA296A9A53D /* PBXContainerItemProxy */;
};
EB3B9F8D2EF7153000D6704E /* PBXTargetDependency */ = { EB3B9F8D2EF7153000D6704E /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
target = EB3B9F4A2EF7151900D6704E /* QuickDropShareExtensionGitHub */; target = EB3B9F4A2EF7151900D6704E /* QuickDropShareExtensionGitHub */;
@@ -1812,8 +1922,8 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx; SDKROOT = macosx;
SHARED_BUILD = 149; SHARED_BUILD = 154;
SHARED_VERSION = 4.0; SHARED_VERSION = 4.2.1;
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -1875,8 +1985,8 @@
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
SDKROOT = macosx; SDKROOT = macosx;
SHARED_BUILD = 149; SHARED_BUILD = 154;
SHARED_VERSION = 4.0; SHARED_VERSION = 4.2.1;
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -1904,7 +2014,7 @@
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; ENABLE_RESOURCE_ACCESS_BLUETOOTH = YES;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO; ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO; ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO; ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
@@ -1919,6 +2029,8 @@
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger"; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network."; INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
INFOPLIST_KEY_NSMainNibFile = MainMenu; INFOPLIST_KEY_NSMainNibFile = MainMenu;
@@ -1969,7 +2081,7 @@
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; ENABLE_RESOURCE_ACCESS_BLUETOOTH = YES;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO; ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO; ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO; ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
@@ -1984,6 +2096,8 @@
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger"; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network."; INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
INFOPLIST_KEY_NSMainNibFile = MainMenu; INFOPLIST_KEY_NSMainNibFile = MainMenu;
@@ -2013,6 +2127,49 @@
}; };
name = Release; name = Release;
}; };
9A4E1D6A03B4FDA5B2074DF0 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 92SDAWLN76;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
PRODUCT_BUNDLE_IDENTIFIER = com.leonboettger.neardrop.tests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/QuickDrop.app/Contents/MacOS/QuickDrop";
};
name = Release;
};
A8C87F63E411A0C639408AC5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 92SDAWLN76;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
PRODUCT_BUNDLE_IDENTIFIER = com.leonboettger.neardrop.tests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/QuickDrop.app/Contents/MacOS/QuickDrop";
};
name = Debug;
};
EB3B9F462EF7150400D6704E /* Debug */ = { EB3B9F462EF7150400D6704E /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
@@ -2033,7 +2190,7 @@
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; ENABLE_RESOURCE_ACCESS_BLUETOOTH = YES;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO; ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO; ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO; ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
@@ -2048,6 +2205,8 @@
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger"; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network."; INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
INFOPLIST_KEY_NSMainNibFile = MainMenu; INFOPLIST_KEY_NSMainNibFile = MainMenu;
@@ -2099,7 +2258,7 @@
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; ENABLE_RESOURCE_ACCESS_BLUETOOTH = YES;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO; ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO; ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO; ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
@@ -2114,6 +2273,8 @@
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger"; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network."; INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
INFOPLIST_KEY_NSMainNibFile = MainMenu; INFOPLIST_KEY_NSMainNibFile = MainMenu;
@@ -2264,6 +2425,8 @@
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger"; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network."; 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_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
@@ -2309,6 +2472,8 @@
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger"; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network."; 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_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
@@ -2431,6 +2596,15 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
B473F6C65C8455203EA5FD18 /* Build configuration list for PBXNativeTarget "QuickDropTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
9A4E1D6A03B4FDA5B2074DF0 /* Release */,
A8C87F63E411A0C639408AC5 /* Debug */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
EB3B9F452EF7150400D6704E /* Build configuration list for PBXNativeTarget "QuickDropGitHub" */ = { EB3B9F452EF7150400D6704E /* Build configuration list for PBXNativeTarget "QuickDropGitHub" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
@@ -33,8 +33,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/leif-ibsen/BigInt", "location" : "https://github.com/leif-ibsen/BigInt",
"state" : { "state" : {
"revision" : "c2f49bee454760e433bafff17bfdc5b96f75d7c8", "revision" : "7c7b1be2f6714ce03833879b0aab4b20293c0207",
"version" : "1.23.0" "version" : "1.24.0"
} }
}, },
{ {
@@ -52,7 +52,7 @@
"location" : "https://github.com/leonboe1/LUI.git", "location" : "https://github.com/leonboe1/LUI.git",
"state" : { "state" : {
"branch" : "main", "branch" : "main",
"revision" : "3af5f2e26d102887980957b2d88a38ea8604b8d8" "revision" : "44013e8ef77d8e0d18e13f449cfe3775f1abb31a"
} }
}, },
{ {
@@ -87,8 +87,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git", "location" : "https://github.com/apple/swift-protobuf.git",
"state" : { "state" : {
"revision" : "f6506eaa86ed2e01cb0ae14a75035b7fdbf0918f", "revision" : "55d7a1cc5666b85c13464aea1c4b4a90feccb4c8",
"version" : "1.38.0" "version" : "1.38.1"
} }
}, },
{ {
@@ -124,7 +124,7 @@
"location" : "https://github.com/siteline/swiftui-introspect", "location" : "https://github.com/siteline/swiftui-introspect",
"state" : { "state" : {
"branch" : "main", "branch" : "main",
"revision" : "c3db5696c625f8571f5f26fb292568db517374cc" "revision" : "aee624c87acee41e1a421f7825b468d38bbfbd6c"
} }
}, },
{ {
@@ -40,6 +40,24 @@
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
debugServiceExtension = "internal" debugServiceExtension = "internal"
allowLocationSimulation = "YES"> allowLocationSimulation = "YES">
<PostActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Register dev QuickDrop for Finder Share"
scriptText = "&quot;${SRCROOT}/scripts/use-dev-share-extension.sh&quot;&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "69DA9A0D29E0BF5100A442DA"
BuildableName = "QuickDrop.app"
BlueprintName = "QuickDrop"
ReferencedContainer = "container:QuickDrop.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PostActions>
<BuildableProductRunnable <BuildableProductRunnable
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">
<BuildableReference <BuildableReference
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2650"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "32A98F34E7D10C21DC2EEA19"
BuildableName = "QuickDropTests.xctest"
BlueprintName = "QuickDropTests"
ReferencedContainer = "container:QuickDrop.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
queueDebuggingEnableBacktraceRecording = "Yes">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+82 -2
View File
@@ -7,6 +7,7 @@
import AudioToolbox import AudioToolbox
import BezelNotification import BezelNotification
import Combine
import Cocoa import Cocoa
import Network import Network
import StoreKit import StoreKit
@@ -27,14 +28,22 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
private var errorAlertHandler = ErrorAlertHandler.shared private var errorAlertHandler = ErrorAlertHandler.shared
private var receiveModel: ReceiveModel? private var receiveModel: ReceiveModel?
private var bluetoothAuthorizationObserver: AnyCancellable?
private let welcomeNavigationState = WelcomeScreenNavigationState()
var showsFirewallAlert = false var showsFirewallAlert = false
var visibleItem: NSMenuItem? = nil var visibleItem: NSMenuItem? = nil
private var didShowAndroidAirDropModeAlert = false
// MARK: NSApplicationDelegate functions // MARK: NSApplicationDelegate functions
func applicationDidFinishLaunching(_: Notification) { func applicationDidFinishLaunching(_: Notification) {
if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil {
LUIInit(configuration: configuration)
return
}
UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().delegate = self
// app did not lauch before // app did not lauch before
@@ -74,9 +83,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
let visibleItem = NSMenuItem( let visibleItem = NSMenuItem(
title: getDefaultVisibleLabel(), title: getDefaultVisibleLabel(),
action: nil, action: #selector(visibleMenuItemSelected),
keyEquivalent: "" keyEquivalent: ""
) )
visibleItem.target = self
self.visibleItem = visibleItem self.visibleItem = visibleItem
menu.addItem(visibleItem) menu.addItem(visibleItem)
@@ -119,6 +129,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
self.visibleItem?.title = self.getDefaultVisibleLabel() self.visibleItem?.title = self.getDefaultVisibleLabel()
} }
setupAndroidAirDropModeDetection()
requestBluetoothPermissionForExistingUserIfNeeded()
// MARK: Logging // MARK: Logging
@@ -181,6 +194,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
func applicationWillTerminate(_: Notification) { func applicationWillTerminate(_: Notification) {
UNUserNotificationCenter.current().removeAllDeliveredNotifications() UNUserNotificationCenter.current().removeAllDeliveredNotifications()
ClipboardSyncCoordinator.shared.stop() ClipboardSyncCoordinator.shared.stop()
AndroidAirDropModeDetector.shared.stop()
NearbyConnectionManager.shared.stopDeviceDiscovery() NearbyConnectionManager.shared.stopDeviceDiscovery()
NearbyConnectionManager.shared.becomeInvisible() NearbyConnectionManager.shared.becomeInvisible()
} }
@@ -232,6 +246,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
// MARK: - Button Actions // MARK: - Button Actions
@objc private func visibleMenuItemSelected() {
showMainWindow(selectedTab: .settings)
}
@objc func sendClipboard() { @objc func sendClipboard() {
let pasteboard = NSPasteboard.general let pasteboard = NSPasteboard.general
@@ -249,6 +267,15 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
@objc func openMainWindow() { @objc func openMainWindow() {
showMainWindow()
}
private func showMainWindow(selectedTab: Tab? = nil) {
if let selectedTab {
welcomeNavigationState.select(selectedTab)
}
defer { defer {
if let welcomeWindow, !Settings.sharedInstance.appLaunchedBefore { if let welcomeWindow, !Settings.sharedInstance.appLaunchedBefore {
runAfter(seconds: 0.2) { runAfter(seconds: 0.2) {
@@ -275,7 +302,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
openPlusScreen: openPlusScreen, openPlusScreen: openPlusScreen,
openAppAdvertisementView: { self.openSheetView(type: .downloadAndroidApp) }, openAppAdvertisementView: { self.openSheetView(type: .downloadAndroidApp) },
openCableTransmissionView: { self.openSheetView(type: .downloadCableConnectionApp) }, openCableTransmissionView: { self.openSheetView(type: .downloadCableConnectionApp) },
checkForNetworkIssues: performDeviceToDeviceCheck checkForNetworkIssues: performDeviceToDeviceCheck,
navigationState: welcomeNavigationState
) )
welcomeWindow = NSWindow( welcomeWindow = NSWindow(
@@ -404,6 +432,58 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
} }
} }
private func setupAndroidAirDropModeDetection() {
log("[AppDelegate] Setting up Android AirDrop mode detection. authorization=\(BluetoothPermissionManager.shared.authorization.rawValue)")
bluetoothAuthorizationObserver = BluetoothPermissionManager.shared.$authorization
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] authorization in
guard let self else { return }
log("[AppDelegate] Bluetooth authorization changed for Android AirDrop mode detection: \(authorization.rawValue)")
if authorization == .allowedAlways {
self.startAndroidAirDropModeDetection()
}
else {
AndroidAirDropModeDetector.shared.stop()
}
}
BluetoothPermissionManager.shared.refresh()
}
private func requestBluetoothPermissionForExistingUserIfNeeded() {
let bluetoothPermission = BluetoothPermissionManager.shared
bluetoothPermission.refresh()
guard Settings.sharedInstance.appLaunchedBefore, bluetoothPermission.canRequestPermission else { return }
log("[AppDelegate] Requesting Bluetooth permission for existing user.")
bluetoothPermission.requestPermission { allowed in
log("[AppDelegate] Existing user Bluetooth permission request completed. allowed=\(allowed)")
}
}
private func startAndroidAirDropModeDetection() {
guard !didShowAndroidAirDropModeAlert else { return }
AndroidAirDropModeDetector.shared.start { [weak self] _ in
DispatchQueue.main.async {
self?.showAndroidAirDropModeAlertIfNeeded()
}
}
}
private func showAndroidAirDropModeAlertIfNeeded() {
guard !didShowAndroidAirDropModeAlert else { return }
didShowAndroidAirDropModeAlert = true
ErrorAlertHandler.shared.openAlert(type: .AndroidAirDropMode)
}
private func getDefaultVisibleLabel() -> String { private func getDefaultVisibleLabel() -> String {
let name = NearbyConnectionManager.shared.deviceInfo.name ?? "Unknown".localized() let name = NearbyConnectionManager.shared.deviceInfo.name ?? "Unknown".localized()
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "Smartphone.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

+466
View File
@@ -27,6 +27,472 @@
}, },
"shouldTranslate" : false "shouldTranslate" : false
}, },
"NSBluetoothAlwaysUsageDescription" : {
"comment" : "Privacy - Bluetooth Always Usage Description",
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
"value" : "يستخدم QuickDrop تقنية Bluetooth لتحسين اكتشاف الأجهزة القريبة."
}
},
"ca" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop utilitza Bluetooth per millorar la detecció de dispositius propers."
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop používá Bluetooth pro vylepšenou detekci zařízení v okolí."
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop bruger Bluetooth til forbedret registrering af enheder i nærheden."
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop verwendet Bluetooth für eine verbesserte Erkennung von Geräten in der Nähe."
}
},
"el" : {
"stringUnit" : {
"state" : "translated",
"value" : "Το QuickDrop χρησιμοποιεί Bluetooth για βελτιωμένο εντοπισμό κοντινών συσκευών."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
}
},
"en-AU" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
}
},
"en-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop usa Bluetooth para mejorar la detección de dispositivos cercanos."
}
},
"es-419" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop usa Bluetooth para mejorar la detección de dispositivos cercanos."
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop käyttää Bluetoothia läheisten laitteiden parempaan havaitsemiseen."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop utilise le Bluetooth pour améliorer la détection des appareils à proximité."
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop utilise le Bluetooth pour améliorer la détection des appareils à proximité."
}
},
"he" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop משתמש ב-Bluetooth לזיהוי משופר של מכשירים בקרבת מקום."
}
},
"hi" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop आस-पास के डिवाइस की बेहतर पहचान के लिए Bluetooth का उपयोग करता है।"
}
},
"hr" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop koristi Bluetooth za poboljšano otkrivanje uređaja u blizini."
}
},
"hu" : {
"stringUnit" : {
"state" : "translated",
"value" : "A QuickDrop Bluetoothot használ a közeli eszközök fejlettebb felismeréséhez."
}
},
"id" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop menggunakan Bluetooth untuk deteksi perangkat terdekat yang lebih baik."
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop usa il Bluetooth per un rilevamento migliorato dei dispositivi nelle vicinanze."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDropは、近くのデバイスをより正確に検出するためにBluetoothを使用します。"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop은 주변 기기를 더 잘 감지하기 위해 Bluetooth를 사용합니다."
}
},
"ms" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop menggunakan Bluetooth untuk pengesanan peranti berdekatan yang dipertingkatkan."
}
},
"nb" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop bruker Bluetooth for forbedret oppdagelse av enheter i nærheten."
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop gebruikt Bluetooth voor verbeterde detectie van apparaten in de buurt."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop używa Bluetooth do ulepszonego wykrywania pobliskich urządzeń."
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
"value" : "O QuickDrop usa Bluetooth para melhorar a detecção de dispositivos próximos."
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
"value" : "O QuickDrop usa Bluetooth para melhorar a deteção de dispositivos próximos."
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop folosește Bluetooth pentru detectarea îmbunătățită a dispozitivelor din apropiere."
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop использует Bluetooth для улучшенного обнаружения устройств поблизости."
}
},
"sv" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop använder Bluetooth för förbättrad upptäckt av enheter i närheten."
}
},
"th" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop ใช้ Bluetooth เพื่อการตรวจจับอุปกรณ์ใกล้เคียงที่ดียิ่งขึ้น"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop, yakındaki cihazları daha iyi algılamak için Bluetooth kullanır."
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop використовує Bluetooth для покращеного виявлення пристроїв поблизу."
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop sử dụng Bluetooth để phát hiện thiết bị lân cận tốt hơn."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop 使用蓝牙来增强附近设备检测。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop 使用藍牙來增強附近裝置偵測。"
}
}
}
},
"NSBluetoothPeripheralUsageDescription" : {
"comment" : "Privacy - Bluetooth Peripheral Usage Description",
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
"value" : "يستخدم QuickDrop تقنية Bluetooth لتحسين اكتشاف الأجهزة القريبة."
}
},
"ca" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop utilitza Bluetooth per millorar la detecció de dispositius propers."
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop používá Bluetooth pro vylepšenou detekci zařízení v okolí."
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop bruger Bluetooth til forbedret registrering af enheder i nærheden."
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop verwendet Bluetooth für eine verbesserte Erkennung von Geräten in der Nähe."
}
},
"el" : {
"stringUnit" : {
"state" : "translated",
"value" : "Το QuickDrop χρησιμοποιεί Bluetooth για βελτιωμένο εντοπισμό κοντινών συσκευών."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
}
},
"en-AU" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
}
},
"en-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop usa Bluetooth para mejorar la detección de dispositivos cercanos."
}
},
"es-419" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop usa Bluetooth para mejorar la detección de dispositivos cercanos."
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop käyttää Bluetoothia läheisten laitteiden parempaan havaitsemiseen."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop utilise le Bluetooth pour améliorer la détection des appareils à proximité."
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop utilise le Bluetooth pour améliorer la détection des appareils à proximité."
}
},
"he" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop משתמש ב-Bluetooth לזיהוי משופר של מכשירים בקרבת מקום."
}
},
"hi" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop आस-पास के डिवाइस की बेहतर पहचान के लिए Bluetooth का उपयोग करता है।"
}
},
"hr" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop koristi Bluetooth za poboljšano otkrivanje uređaja u blizini."
}
},
"hu" : {
"stringUnit" : {
"state" : "translated",
"value" : "A QuickDrop Bluetoothot használ a közeli eszközök fejlettebb felismeréséhez."
}
},
"id" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop menggunakan Bluetooth untuk deteksi perangkat terdekat yang lebih baik."
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop usa il Bluetooth per un rilevamento migliorato dei dispositivi nelle vicinanze."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDropは、近くのデバイスをより正確に検出するためにBluetoothを使用します。"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop은 주변 기기를 더 잘 감지하기 위해 Bluetooth를 사용합니다."
}
},
"ms" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop menggunakan Bluetooth untuk pengesanan peranti berdekatan yang dipertingkatkan."
}
},
"nb" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop bruker Bluetooth for forbedret oppdagelse av enheter i nærheten."
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop gebruikt Bluetooth voor verbeterde detectie van apparaten in de buurt."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop używa Bluetooth do ulepszonego wykrywania pobliskich urządzeń."
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
"value" : "O QuickDrop usa Bluetooth para melhorar a detecção de dispositivos próximos."
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
"value" : "O QuickDrop usa Bluetooth para melhorar a deteção de dispositivos próximos."
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop folosește Bluetooth pentru detectarea îmbunătățită a dispozitivelor din apropiere."
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop использует Bluetooth для улучшенного обнаружения устройств поблизости."
}
},
"sv" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop använder Bluetooth för förbättrad upptäckt av enheter i närheten."
}
},
"th" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop ใช้ Bluetooth เพื่อการตรวจจับอุปกรณ์ใกล้เคียงที่ดียิ่งขึ้น"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop, yakındaki cihazları daha iyi algılamak için Bluetooth kullanır."
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop використовує Bluetooth для покращеного виявлення пристроїв поблизу."
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop sử dụng Bluetooth để phát hiện thiết bị lân cận tốt hơn."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop 使用蓝牙来增强附近设备检测。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop 使用藍牙來增強附近裝置偵測。"
}
}
}
},
"NSHumanReadableCopyright" : { "NSHumanReadableCopyright" : {
"comment" : "Copyright (human-readable)", "comment" : "Copyright (human-readable)",
"extractionState" : "extracted_with_value", "extractionState" : "extracted_with_value",
+12348 -6816
View File
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,259 @@
//
// AndroidAirDropModeDetector.swift
// QuickDrop
//
// Created by Leon Böttger on 10.06.26.
//
import CoreBluetooth
import Foundation
import LUI
struct AndroidAirDropAdvertisement: Equatable {
let rssi: Int
let manufacturerDataHex: String?
let serviceUUIDs: Set<String>
let serviceDataHexByUUID: [String: String]
let isConnectable: Bool?
init(
rssi: Int,
manufacturerDataHex: String?,
serviceUUIDs: Set<String>,
serviceDataHexByUUID: [String: String] = [:],
isConnectable: Bool? = nil
) {
self.rssi = rssi
self.manufacturerDataHex = manufacturerDataHex?.lowercased()
self.serviceUUIDs = Set(serviceUUIDs.map { $0.uppercased() })
self.serviceDataHexByUUID = Dictionary(
uniqueKeysWithValues: serviceDataHexByUUID.map { ($0.key.uppercased(), $0.value.lowercased()) }
)
self.isConnectable = isConnectable
}
}
struct AndroidAirDropModeObservation: Equatable {
let airDropModePacketCount: Int
let strongestAirDropModeRSSI: Int?
let minimumRSSI: Int
let requiredPacketsPerSignal: Int
var isDetected: Bool {
airDropModePacketCount >= requiredPacketsPerSignal
}
}
struct AndroidAirDropModeClassifier {
static let defaultMinimumRSSI = -70
static let defaultRequiredPacketsPerSignal = 2
static let airDropModeManufacturerPrefix = "4c000512000000000000000001"
static let airDropModeServiceUUID = "FCF1"
let minimumRSSI: Int
let requiredPacketsPerSignal: Int
private(set) var airDropModePacketCount = 0
private(set) var strongestAirDropModeRSSI: Int?
init(
minimumRSSI: Int = Self.defaultMinimumRSSI,
requiredPacketsPerSignal: Int = Self.defaultRequiredPacketsPerSignal
) {
self.minimumRSSI = minimumRSSI
self.requiredPacketsPerSignal = requiredPacketsPerSignal
}
var observation: AndroidAirDropModeObservation {
AndroidAirDropModeObservation(
airDropModePacketCount: airDropModePacketCount,
strongestAirDropModeRSSI: strongestAirDropModeRSSI,
minimumRSSI: minimumRSSI,
requiredPacketsPerSignal: requiredPacketsPerSignal
)
}
mutating func reset() {
airDropModePacketCount = 0
strongestAirDropModeRSSI = nil
}
@discardableResult
mutating func record(_ advertisement: AndroidAirDropAdvertisement) -> AndroidAirDropModeObservation {
guard advertisement.rssi >= minimumRSSI else {
return observation
}
if Self.isAirDropModeAdvertisement(advertisement) {
airDropModePacketCount += 1
strongestAirDropModeRSSI = Self.strongerRSSI(strongestAirDropModeRSSI, advertisement.rssi)
}
return observation
}
static func isAirDropModeAdvertisement(_ advertisement: AndroidAirDropAdvertisement) -> Bool {
#if os(iOS)
// iOS exposes the AirDrop-compatible Android share-mode service UUID,
// but hides the Apple manufacturer payload from third-party apps. The
// share sheet emits a non-connectable FCF1 packet without service data;
// background AirDrop-compatible discovery emits connectable FCF1 packets
// with FCF1 service data.
advertisement.serviceUUIDs.contains(airDropModeServiceUUID) &&
advertisement.isConnectable == false &&
advertisement.serviceDataHexByUUID[airDropModeServiceUUID] == nil
#else
advertisement.manufacturerDataHex?.hasPrefix(airDropModeManufacturerPrefix) == true &&
advertisement.serviceUUIDs.contains(airDropModeServiceUUID)
#endif
}
private static func strongerRSSI(_ current: Int?, _ candidate: Int) -> Int {
guard let current else { return candidate }
return max(current, candidate)
}
}
final class AndroidAirDropModeDetector: NSObject, CBCentralManagerDelegate {
static let shared = AndroidAirDropModeDetector()
static let scanServiceUUIDs = [CBUUID(string: AndroidAirDropModeClassifier.airDropModeServiceUUID)]
static let scanOptions: [String: Any] = [CBCentralManagerScanOptionAllowDuplicatesKey: true]
private var centralManager: CBCentralManager?
private var classifier = AndroidAirDropModeClassifier()
private var onDetected: ((AndroidAirDropModeObservation) -> Void)?
private var isScanning = false
private var hasDeliveredDetection = false
func start(onDetected: @escaping (AndroidAirDropModeObservation) -> Void) {
guard BluetoothPermissionManager.shared.isAllowed else {
log("[AndroidAirDropModeDetector] Bluetooth permission is not granted; skipping scan. authorization=\(BluetoothPermissionManager.shared.authorization.rawValue)")
return
}
self.onDetected = onDetected
guard !hasDeliveredDetection else { return }
guard !isScanning else { return }
log("[AndroidAirDropModeDetector] Starting detector.")
classifier.reset()
if centralManager == nil {
log("[AndroidAirDropModeDetector] Creating central manager.")
centralManager = CBCentralManager(delegate: self, queue: .main)
}
else if centralManager?.state == .poweredOn {
beginScanning()
}
}
func stop() {
log("[AndroidAirDropModeDetector] Stopping detector.")
centralManager?.stopScan()
classifier.reset()
onDetected = nil
hasDeliveredDetection = false
isScanning = false
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
log("[AndroidAirDropModeDetector] Central state changed: \(central.state.rawValue)")
guard central.state == .poweredOn else {
if isScanning {
central.stopScan()
isScanning = false
}
return
}
if onDetected != nil {
beginScanning()
}
}
func centralManager(
_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String: Any],
rssi RSSI: NSNumber
) {
guard !hasDeliveredDetection else { return }
let advertisement = Self.advertisement(from: advertisementData, rssi: RSSI.intValue)
let observation = classifier.record(advertisement)
guard observation.isDetected else { return }
hasDeliveredDetection = true
isScanning = false
central.stopScan()
let callback = onDetected
onDetected = nil
log(
"[AndroidAirDropModeDetector] Detected Android AirDrop-compatible sharing mode. " +
"airDropModePackets=\(observation.airDropModePacketCount), " +
"minimumRSSI=\(observation.minimumRSSI)"
)
callback?(observation)
}
private func beginScanning() {
guard !isScanning, let centralManager, centralManager.state == .poweredOn else { return }
let scanTargetDescription = Self.scanServiceUUIDs.map(\.uuidString).joined(separator: ", ")
log("[AndroidAirDropModeDetector] BLE scan started for \(scanTargetDescription).")
// Duplicate delivery is intentional: the classifier waits for repeated
// matching packets so one transient advertisement does not trigger the alert.
centralManager.scanForPeripherals(
withServices: Self.scanServiceUUIDs,
options: Self.scanOptions
)
isScanning = true
}
private static func advertisement(
from advertisementData: [String: Any],
rssi: Int
) -> AndroidAirDropAdvertisement {
var serviceUUIDs = Set<String>()
var serviceDataHexByUUID: [String: String] = [:]
[
CBAdvertisementDataServiceUUIDsKey,
CBAdvertisementDataOverflowServiceUUIDsKey,
CBAdvertisementDataSolicitedServiceUUIDsKey
].forEach { key in
guard let uuids = advertisementData[key] as? [CBUUID] else { return }
uuids.forEach { serviceUUIDs.insert($0.uuidString) }
}
if let serviceData = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] {
serviceData.forEach { uuid, data in
serviceUUIDs.insert(uuid.uuidString)
serviceDataHexByUUID[uuid.uuidString] = data.androidAirDropModeHexString
}
}
return AndroidAirDropAdvertisement(
rssi: rssi,
manufacturerDataHex: (advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data)?.androidAirDropModeHexString,
serviceUUIDs: serviceUUIDs,
serviceDataHexByUUID: serviceDataHexByUUID,
isConnectable: advertisementData[CBAdvertisementDataIsConnectable] as? Bool
)
}
}
private extension Data {
var androidAirDropModeHexString: String {
map { String(format: "%02x", $0) }.joined()
}
}
@@ -0,0 +1,85 @@
//
// BluetoothPermissionManager.swift
// QuickDrop
//
// Created by Leon Böttger on 31.05.26.
//
import Combine
import CoreBluetooth
import Foundation
import LUI
#if os(iOS) && !EXTENSION
import UIKit
#endif
#if os(macOS)
import AppKit
#endif
final class BluetoothPermissionManager: NSObject, ObservableObject, CBCentralManagerDelegate {
static let shared = BluetoothPermissionManager()
@Published private(set) var authorization: CBManagerAuthorization = CBManager.authorization
private var centralManager: CBCentralManager?
private var completion: ((Bool) -> Void)?
var isAllowed: Bool {
authorization == .allowedAlways
}
var canRequestPermission: Bool {
authorization == .notDetermined
}
private override init() {
super.init()
}
func refresh() {
let currentAuthorization = CBManager.authorization
guard authorization != currentAuthorization else { return }
authorization = currentAuthorization
}
func requestPermission(completion: ((Bool) -> Void)? = nil) {
refresh()
guard authorization == .notDetermined else {
completion?(isAllowed)
return
}
self.completion = completion
centralManager = CBCentralManager(delegate: self, queue: .main)
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
refresh()
guard authorization != .notDetermined else { return }
let completion = completion
self.completion = nil
completion?(isAllowed)
}
func openSystemBluetoothSettings() {
#if os(iOS) && !EXTENSION
openAppSettings()
#elseif os(macOS)
let urls = [
URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Bluetooth"),
URL(string: "x-apple.systempreferences:com.apple.BluetoothSettings")
].compactMap { $0 }
if let url = urls.first {
NSWorkspace.shared.open(url)
}
#endif
}
}
+102 -26
View File
@@ -27,15 +27,35 @@ class ErrorAlertHandler {
private var firewallAlertWindow: NSWindow? private var firewallAlertWindow: NSWindow?
private var apIsolationAlertWindow: NSWindow? private var apIsolationAlertWindow: NSWindow?
private var networkFilterAlertWindow: NSWindow? private var networkFilterAlertWindow: NSWindow?
private var androidAirDropModeAlertWindow: NSWindow?
#endif #endif
func showErrorAlert(for deviceName: String, error: Error) { /// Resource key for the recoverable-error suffix ("if this keeps happening…") appended to most
/// transfer errors.
///
/// `Error.FixInstructions` suggests downloading the QuickDrop Android app from Google Play, which
/// is pointless for a peer that already runs QuickDrop. For such a peer (`isQuickDropPeer == true`)
/// the `Error.FixInstructionsAppInstalled` variant — the same advice without the download
/// suggestion — is used instead.
static func fixInstructionsKey(isQuickDropPeer: Bool) -> String {
isQuickDropPeer ? "Error.FixInstructionsAppInstalled" : "Error.FixInstructions"
}
/// Presents the transfer-error alert for `deviceName`.
///
/// - Parameter isQuickDropPeer: whether the other device runs the QuickDrop app. When `true`, the
/// "download the app from Google Play" suggestion is omitted from the fix instructions (see
/// `fixInstructionsKey(isQuickDropPeer:)`). Defaults to `false` so callers without device context
/// keep the full hint.
/// - Parameter completion: called after the macOS transfer alert is dismissed. For issue-specific
/// alerts and iOS alerts, it is called after presentation.
func showErrorAlert(for deviceName: String, error: Error, isQuickDropPeer: Bool = false, completion: (() -> Void)? = nil) {
#if os(macOS) #if os(macOS)
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
#endif #endif
var description = "" var description = ""
let fixInstructions = " " + "Error.FixInstructions".localized() let fixInstructions = " " + Self.fixInstructionsKey(isQuickDropPeer: isQuickDropPeer).localized()
if let ne = (error as? NearbyError) { if let ne = (error as? NearbyError) {
switch ne { switch ne {
case .inputOutput: case .inputOutput:
@@ -45,6 +65,7 @@ class ErrorAlertHandler {
case .packetFilterError: case .packetFilterError:
#if os(macOS) #if os(macOS)
openAlert(type: .NetworkFilter) openAlert(type: .NetworkFilter)
completion?()
return return
#else #else
description = error.localizedDescription description = error.localizedDescription
@@ -52,6 +73,7 @@ class ErrorAlertHandler {
case .firewallError: case .firewallError:
#if os(macOS) #if os(macOS)
openAlert(type: .Firewall) openAlert(type: .Firewall)
completion?()
return return
#else #else
description = error.localizedDescription description = error.localizedDescription
@@ -77,6 +99,7 @@ class ErrorAlertHandler {
// Prevent multiple alerts at the same time // Prevent multiple alerts at the same time
if self.isAlertShown { if self.isAlertShown {
log("Skipping alert for error \(error.localizedDescription) because one is already shown") log("Skipping alert for error \(error.localizedDescription) because one is already shown")
completion?()
return return
} }
else { else {
@@ -88,31 +111,14 @@ class ErrorAlertHandler {
#if os(macOS) #if os(macOS)
let primaryButtonTitle = "InformDeveloper".localized() // Shown modelessly (the native NSAlert panel via makeKeyAndOrderFront) rather than
let secondaryButtonTitle = "CloseAlert".localized() // runModal(), which blocks the main run loop and would freeze the next incoming transfer's
// consent prompt until dismissed. Same window runModal() uses, so it looks identical.
self.isAlertShown = true self.isAlertShown = true
let alert = NSAlert() presentTransferErrorAlert(title: title, description: description, completion: completion)
alert.alertStyle = .critical
alert.messageText = title
alert.informativeText = description
alert.addButton(withTitle: primaryButtonTitle)
alert.addButton(withTitle: secondaryButtonTitle)
let result = alert.runModal()
self.isAlertShown = false
if result == .alertFirstButtonReturn {
if Bundle.main.bundlePath.hasSuffix(".appex") {
if let url = URL(string: "quickdrop://sendLog") {
NSWorkspace.shared.open(url)
}
} else {
LogExportPresenter.showUploadLogsAlert(openSupportMailAfterUpload: false)
}
}
#else #else
showAlert(title: title, message: description) showAlert(title: title, message: description)
completion?()
#endif #endif
} }
} }
@@ -124,6 +130,11 @@ class ErrorAlertHandler {
AudioManager.playErrorSound() AudioManager.playErrorSound()
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
if let existingWindow = alertWindow(for: type), existingWindow.isVisible {
bringAlertWindowToFront(existingWindow)
return
}
// Create an NSWindow to host the SwiftUI view // Create an NSWindow to host the SwiftUI view
let alertWindow = NSWindow( let alertWindow = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: issueViewWidth, height: issueViewHeight), contentRect: NSRect(x: 0, y: 0, width: issueViewWidth, height: issueViewHeight),
@@ -142,6 +153,9 @@ class ErrorAlertHandler {
case .Firewall: case .Firewall:
firewallAlertWindow = alertWindow firewallAlertWindow = alertWindow
alertWindow.contentView = NSHostingView(rootView: FirewallIssueView()) alertWindow.contentView = NSHostingView(rootView: FirewallIssueView())
case .AndroidAirDropMode:
androidAirDropModeAlertWindow = alertWindow
alertWindow.contentView = NSHostingView(rootView: AndroidAirDropModeIssueView())
} }
alertWindow.title = "QuickDrop" alertWindow.title = "QuickDrop"
@@ -149,9 +163,8 @@ class ErrorAlertHandler {
alertWindow.isReleasedWhenClosed = false alertWindow.isReleasedWhenClosed = false
alertWindow.setFrameAutosaveName(type.rawValue) alertWindow.setFrameAutosaveName(type.rawValue)
NSApp.activate(ignoringOtherApps: true)
alertWindow.makeKeyAndOrderFront(nil)
alertWindow.level = .normal alertWindow.level = .normal
bringAlertWindowToFront(alertWindow)
} }
} }
@@ -163,6 +176,68 @@ class ErrorAlertHandler {
self.apIsolationAlertWindow = nil self.apIsolationAlertWindow = nil
} }
} }
private func alertWindow(for type: AlertType) -> NSWindow? {
switch type {
case .ApIsolation:
return apIsolationAlertWindow
case .NetworkFilter:
return networkFilterAlertWindow
case .Firewall:
return firewallAlertWindow
case .AndroidAirDropMode:
return androidAirDropModeAlertWindow
}
}
private func bringAlertWindowToFront(_ window: NSWindow) {
// Keep the app a menu-bar (.accessory) app: do NOT switch to .regular,
// which would add a dock icon that lingers after the alert closes.
// orderFrontRegardless() brings the window to the front even though the
// app stays inactive in the background.
NSApp.activate(ignoringOtherApps: true)
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless()
}
/// Shows the transfer-error alert as the native `NSAlert` panel, but presented modelessly
/// (`makeKeyAndOrderFront`) instead of `runModal()`, so it does NOT block the main run loop — an
/// incoming transfer's consent prompt can still appear while it's on screen. It's the same
/// `_NSAlertPanel` window `runModal()` would show, so it looks identical; we just route the button
/// clicks ourselves (there's no modal session to end) and reset state on dismissal.
private func presentTransferErrorAlert(title: String, description: String, completion: (() -> Void)? = nil) {
let alert = NSAlert()
alert.alertStyle = .critical
alert.messageText = title
alert.informativeText = description
alert.addButton(withTitle: "InformDeveloper".localized()) // tag == .alertFirstButtonReturn
alert.addButton(withTitle: "CloseAlert".localized()) // tag == .alertSecondButtonReturn
alert.buttons.first?.keyEquivalent = "\r" // default button: Return
if alert.buttons.count > 1 { alert.buttons[1].keyEquivalent = "\u{1b}" } // Close: Escape
ModelessAlertPresenter.present(alert) { [weak self] response in
self?.isAlertShown = false
if response == .alertFirstButtonReturn {
Self.presentLogUpload()
}
completion?()
}
}
/// The "Inform developer" action: from the share extension, hand off to the host app via the
/// `quickdrop://sendLog` URL; otherwise upload logs directly. Mirrors the previous alert's primary
/// button.
private static func presentLogUpload() {
if Bundle.main.bundlePath.hasSuffix(".appex") {
if let url = URL(string: "quickdrop://sendLog") {
NSWorkspace.shared.open(url)
}
} else {
LogExportPresenter.showUploadLogsAlert(openSupportMailAfterUpload: false)
}
}
#endif #endif
} }
@@ -171,4 +246,5 @@ enum AlertType: String {
case ApIsolation case ApIsolation
case NetworkFilter case NetworkFilter
case Firewall case Firewall
case AndroidAirDropMode
} }
@@ -0,0 +1,78 @@
//
// ModelessAlertPresenter.swift
// QuickDrop
//
// Created by Leon Böttger on 30.06.26.
//
import AppKit
/// Presents an `NSAlert` as its own modeless panel (non-blocking) instead of `runModal()`. It's the
/// exact `_NSAlertPanel` window `runModal()` would show, so it looks identical — but it does not spin a
/// nested modal run loop, so the app keeps working (e.g. can still receive the next transfer) while the
/// alert is on screen.
///
/// Use this in place of `alert.runModal()` for any alert that may appear while the app is busy. The
/// clicked button is reported to `completion` as the `NSApplication.ModalResponse` that `runModal()`
/// would have returned (first button = `.alertFirstButtonReturn`, second = `.alertSecondButtonReturn`,
/// …). Must be called on the main thread.
final class ModelessAlertPresenter: NSObject {
/// Keeps each live presenter — and thus its `NSAlert` and panel — retained while on screen; the
/// entry is removed when the alert is dismissed. `NSButton.target` is weak and the panel is owned by
/// the `NSAlert`, so without this the alert would deallocate immediately.
private static var active: Set<ModelessAlertPresenter> = []
private let alert: NSAlert
private let completion: (NSApplication.ModalResponse) -> Void
private init(alert: NSAlert, completion: @escaping (NSApplication.ModalResponse) -> Void) {
self.alert = alert
self.completion = completion
}
static func present(_ alert: NSAlert,
completion: @escaping (NSApplication.ModalResponse) -> Void = { _ in }) {
if let window = NSApp.mainWindow ?? NSApp.keyWindow {
alert.beginSheetModal(for: window, completionHandler: completion)
}
else {
let presenter = ModelessAlertPresenter(alert: alert, completion: completion)
active.insert(presenter)
presenter.show()
}
}
private func show() {
alert.layout()
// runModal() makes the default button respond to Return; modelessly we set it ourselves — but
// only if the caller specified no key equivalents, so explicit ones (e.g. Escape) are kept.
if alert.buttons.allSatisfy({ $0.keyEquivalent.isEmpty }) {
alert.buttons.first?.keyEquivalent = "\r"
}
for button in alert.buttons {
button.target = self
button.action = #selector(buttonClicked(_:))
}
let window = alert.window
window.center()
NSApp.activate(ignoringOtherApps: true)
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless()
}
@objc private func buttonClicked(_ sender: NSButton) {
let response = NSApplication.ModalResponse(rawValue: sender.tag)
alert.window.orderOut(nil)
// Defer teardown so the alert and its panel aren't deallocated while this button's action is
// still on the stack.
DispatchQueue.main.async {
self.completion(response)
ModelessAlertPresenter.active.remove(self)
}
}
}
+1 -1
View File
@@ -26,7 +26,7 @@ func sendToSharingService(items: [Any]) {
alert.informativeText = "ShareExtensionDisabledDescriptionLegacy".localized() alert.informativeText = "ShareExtensionDisabledDescriptionLegacy".localized()
} }
alert.addButton(withTitle: "CloseAlert".localized()) alert.addButton(withTitle: "CloseAlert".localized())
alert.runModal() ModelessAlertPresenter.present(alert)
} }
} }
} }
@@ -14,6 +14,8 @@ struct IntroductionView: View {
static let height: CGFloat = 540 static let height: CGFloat = 540
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
@Environment(\.scenePhase) private var scenePhase
@ObservedObject private var bluetooth = BluetoothPermissionManager.shared
@State private var currentPage: IntroductionPage = .splash @State private var currentPage: IntroductionPage = .splash
@State private var pollingPage: IntroductionPage? = nil @State private var pollingPage: IntroductionPage? = nil
@@ -120,6 +122,10 @@ struct IntroductionView: View {
.onChange(of: currentPage) { newValue in .onChange(of: currentPage) { newValue in
pageDidChange(to: newValue) pageDidChange(to: newValue)
} }
.onChange(of: scenePhase) { newValue in
guard newValue == .active, currentPage == .bluetoothAccess else { return }
refreshBluetoothPermissionAndAdvanceIfAllowed()
}
.task(id: pollingPage) { .task(id: pollingPage) {
guard let page = pollingPage else { return } guard let page = pollingPage else { return }
@@ -181,6 +187,11 @@ struct IntroductionView: View {
private func continueTapped() { private func continueTapped() {
if currentPage == .bluetoothAccess {
continueBluetoothPermission()
return
}
// If already allowed, advance immediately // If already allowed, advance immediately
if currentPage.canContinue() { if currentPage.canContinue() {
advance() advance()
@@ -197,6 +208,50 @@ struct IntroductionView: View {
} }
private func continueBluetoothPermission() {
bluetooth.refresh()
if bluetooth.isAllowed {
advance()
return
}
pollingPage = .bluetoothAccess
guard bluetooth.canRequestPermission else {
bluetooth.openSystemBluetoothSettings()
return
}
bluetooth.requestPermission { allowed in
DispatchQueue.main.async {
guard currentPage == .bluetoothAccess else {
stopPolling()
return
}
if allowed {
stopPolling()
advance()
}
else {
pollingPage = .bluetoothAccess
}
}
}
}
private func refreshBluetoothPermissionAndAdvanceIfAllowed() {
bluetooth.refresh()
guard currentPage == .bluetoothAccess, bluetooth.isAllowed else { return }
stopPolling()
advance()
}
private func advance() { private func advance() {
if currentPage == .localNetworkAccess { if currentPage == .localNetworkAccess {
@@ -223,6 +278,7 @@ enum IntroductionPage: CaseIterable {
case splash case splash
case noWifi case noWifi
case localNetworkAccess case localNetworkAccess
case bluetoothAccess
case enableShareExtension case enableShareExtension
case finished case finished
@@ -234,6 +290,8 @@ enum IntroductionPage: CaseIterable {
return "introduction_no_wifi" return "introduction_no_wifi"
case .localNetworkAccess: case .localNetworkAccess:
return "introduction_local_network_access" return "introduction_local_network_access"
case .bluetoothAccess:
return "BluetoothPermissionRequiredTitle"
case .enableShareExtension: case .enableShareExtension:
return "introduction_enable_share_extension" return "introduction_enable_share_extension"
case .finished: case .finished:
@@ -250,6 +308,8 @@ enum IntroductionPage: CaseIterable {
return "introduction_no_wifi_description" return "introduction_no_wifi_description"
case .localNetworkAccess: case .localNetworkAccess:
return "introduction_local_network_access_description".localized(with: "introduction_continue".localized()) return "introduction_local_network_access_description".localized(with: "introduction_continue".localized())
case .bluetoothAccess:
return "BluetoothPermissionRequiredSubtitle"
case .enableShareExtension: case .enableShareExtension:
if #available(macOS 13.0, *) { if #available(macOS 13.0, *) {
return "introduction_enable_share_extension_description".localized(with: "EnableQuickDropExtension".localized()) return "introduction_enable_share_extension_description".localized(with: "EnableQuickDropExtension".localized())
@@ -267,6 +327,8 @@ enum IntroductionPage: CaseIterable {
switch self { switch self {
case .noWifi: case .noWifi:
return .init(action: presentNextPage, warningTitle: "introduction_no_wifi_skip_title", warningMessage: "introduction_no_wifi_skip_message") return .init(action: presentNextPage, warningTitle: "introduction_no_wifi_skip_title", warningMessage: "introduction_no_wifi_skip_message")
case .bluetoothAccess:
return .init(action: { .enableShareExtension }, warningTitle: "BluetoothPermissionSkipAlertTitle", warningMessage: "BluetoothPermissionSkipAlertMessage")
default: default:
return nil return nil
} }
@@ -290,6 +352,8 @@ enum IntroductionPage: CaseIterable {
case .noWifi: case .noWifi:
return firstPermissionPane() return firstPermissionPane()
case .localNetworkAccess: case .localNetworkAccess:
return .bluetoothAccess
case .bluetoothAccess:
return .enableShareExtension return .enableShareExtension
case .enableShareExtension: case .enableShareExtension:
return .finished return .finished
@@ -304,7 +368,7 @@ enum IntroductionPage: CaseIterable {
// Local network access is requested first on macOS 15+ // Local network access is requested first on macOS 15+
return .localNetworkAccess return .localNetworkAccess
} }
return .enableShareExtension return .bluetoothAccess
} }
@@ -312,6 +376,8 @@ enum IntroductionPage: CaseIterable {
switch self { switch self {
case .noWifi: case .noWifi:
return NearbyConnectionManager.shared.isConnectedToLocalNetwork return NearbyConnectionManager.shared.isConnectedToLocalNetwork
case .bluetoothAccess:
return BluetoothPermissionManager.shared.isAllowed
default: default:
return true return true
} }
@@ -332,6 +398,8 @@ enum IntroductionPage: CaseIterable {
imageView(for: "wifi.slash", with: height) imageView(for: "wifi.slash", with: height)
case .localNetworkAccess: case .localNetworkAccess:
imageView(for: "network", with: height) imageView(for: "network", with: height)
case .bluetoothAccess:
imageView(for: "antenna.radiowaves.left.and.right", with: height)
case .enableShareExtension: case .enableShareExtension:
imageView(for: "square.and.arrow.up", with: height) imageView(for: "square.and.arrow.up", with: height)
case .finished: case .finished:
@@ -351,7 +419,6 @@ enum IntroductionPage: CaseIterable {
.padding(.top, height * 0.13) .padding(.top, height * 0.13)
} }
struct SkipAction: Identifiable { struct SkipAction: Identifiable {
let action: () -> IntroductionPage? let action: () -> IntroductionPage?
let warningTitle: String let warningTitle: String
@@ -0,0 +1,24 @@
//
// AndroidAirDropModeIssueView.swift
// QuickDrop
//
// Created by Leon Böttger on 10.06.26.
//
import Foundation
import SwiftUI
struct AndroidAirDropModeIssueView: View {
var body: some View {
IssueView(
image: .smartphone,
header: "AndroidAirDropModeHeader".localized(),
description: "AndroidAirDropModeDescription".localized()
)
}
}
#Preview {
AndroidAirDropModeIssueView()
.frame(width: issueViewWidth, height: issueViewHeight)
}
@@ -71,7 +71,7 @@ func openRouterSettingsPage() async {
alert.informativeText = "ApIsolationFindRouterFailedDescription".localized() alert.informativeText = "ApIsolationFindRouterFailedDescription".localized()
alert.addButton(withTitle: "CloseAlert".localized()) alert.addButton(withTitle: "CloseAlert".localized())
alert.runModal() ModelessAlertPresenter.present(alert)
} }
return return
} }
+1 -2
View File
@@ -102,7 +102,6 @@ struct IssueView: View {
#Preview { #Preview {
NetworkFilterIssueView() IssueView(image: .smartphone, header: "Header", description: "Description")
.frame(width: 600, height: 400) .frame(width: 600, height: 400)
} }
+56 -12
View File
@@ -11,10 +11,14 @@ import LUI
struct SettingsView: View { struct SettingsView: View {
let openPlus: () -> Void
@ObservedObject var settings = Settings.sharedInstance @ObservedObject var settings = Settings.sharedInstance
@ObservedObject var connectionManager = NearbyConnectionManager.shared @ObservedObject var connectionManager = NearbyConnectionManager.shared
@ObservedObject var iapManager = IAPManager.sharedInstance
@State private var isChangeDeviceNameAlertPresented = false @State private var isChangeDeviceNameAlertPresented = false
@State private var licenseWindow: NSWindow?
var body: some View { var body: some View {
@@ -68,17 +72,33 @@ struct SettingsView: View {
Divider() Divider()
.padding(.vertical, 10) .padding(.vertical, 10)
Button { HStack(spacing: 30) {
openTrustedDevicesWindow() SettingsFooterButton("ManageTrustedDevices") {
} label: { openTrustedDevicesWindow()
Text("ManageTrustedDevices") }
.underline()
.font(.subheadline) SettingsFooterButton("Acknowledgements") {
.opacity(0.5) licenseWindow = openLicenseWindow()
.frame(maxWidth: .infinity) }
.padding(.vertical, 15)
#if !GITHUB
if !iapManager.plusVersionState {
SettingsFooterButton("SupportQuickDrop") {
openPlus()
}
}
#endif
#if DEBUG
SettingsFooterButton("ResetAllSettings") {
IAPManager.sharedInstance.plusVersionState = false
Settings.sharedInstance.deleteAllUserDefaults()
}
#endif
} }
.buttonStyle(PlainButtonStyle()) .minimumScaleFactor(0.7)
.frame(maxWidth: .infinity)
.padding(.vertical, 15)
} }
} }
} }
@@ -131,7 +151,7 @@ struct SettingsView: View {
alert.informativeText = "FolderSelectionFailedMessage".localized() alert.informativeText = "FolderSelectionFailedMessage".localized()
alert.addButton(withTitle: "CloseAlert".localized()) alert.addButton(withTitle: "CloseAlert".localized())
alert.runModal() ModelessAlertPresenter.present(alert)
} }
} }
} }
@@ -194,7 +214,31 @@ struct SettingsView: View {
struct SettingsView_Previews: PreviewProvider { struct SettingsView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
SettingsView() SettingsView(openPlus: {})
.frame(width: 800, height: 500) .frame(width: 800, height: 500)
} }
} }
private struct SettingsFooterButton: View {
let title: String
let action: () -> Void
init(_ title: String, action: @escaping () -> Void) {
self.title = title
self.action = action
}
var body: some View {
Button {
action()
} label: {
Text(title.localized())
.underline()
.font(.subheadline)
.lineLimit(1)
.opacity(0.5)
}
.buttonStyle(.plain)
}
}
+22 -45
View File
@@ -12,12 +12,8 @@ import SwiftUI
struct TutorialView: View { struct TutorialView: View {
let tab: Tab let tab: Tab
let openPlus: () -> Void
let openAppAdvertisementView: () -> Void let openAppAdvertisementView: () -> Void
@State private var licenseWindow: NSWindow?
@ObservedObject var iapManager = IAPManager.sharedInstance
var body: some View { var body: some View {
LargeAppIconView(title: tab.title) { LargeAppIconView(title: tab.title) {
@@ -35,7 +31,7 @@ struct TutorialView: View {
.padding(.top) .padding(.top)
} }
else { else {
Text(tab.text.localized()) Text(tab.text)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.padding() .padding()
@@ -44,51 +40,22 @@ struct TutorialView: View {
.frame(width: 550) .frame(width: 550)
if tab == .receive { if tab == .receive {
HStack(spacing: 30) { Button {
showAndroidAirDropModeAlert()
Button { } label: {
licenseWindow = openLicenseWindow() Text("AndroidAirDropModeHelpLink".localized())
} label: { .underline()
Text("Acknowledgements") .font(.footnote)
.underline() .multilineTextAlignment(.center)
.font(.footnote) .opacity(0.7)
.opacity(0.5)
}
.buttonStyle(.plain)
#if !GITHUB
if !iapManager.plusVersionState {
Button {
openPlus()
} label: {
Text("SupportQuickDrop")
.underline()
.font(.footnote)
.opacity(0.5)
}
.buttonStyle(.plain)
}
#endif
#if DEBUG
Button {
IAPManager.sharedInstance.plusVersionState = false
Settings.sharedInstance.deleteAllUserDefaults()
} label: {
Text("ResetAllSettings")
.underline()
.font(.footnote)
.opacity(0.5)
}
.buttonStyle(.plain)
#endif
} }
.buttonStyle(.plain)
.padding() .padding()
} }
else if tab == .send { else if tab == .send {
EnableExtensionView() EnableExtensionView()
} }
else if tab == .notificationSync { else if tab == .notificationSync || tab == .clipboardSync {
Button { Button {
openAppAdvertisementView() openAppAdvertisementView()
} label: { } label: {
@@ -100,6 +67,16 @@ struct TutorialView: View {
} }
} }
} }
private func showAndroidAirDropModeAlert() {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = "AndroidAirDropModeAlertTitle".localized()
alert.informativeText = "AndroidAirDropModeAlertMessage".localized()
alert.addButton(withTitle: "CloseAlert".localized())
ModelessAlertPresenter.present(alert)
}
} }
@@ -198,5 +175,5 @@ private struct SharingPickerPresenter: NSViewRepresentable {
#Preview { #Preview {
WelcomeScreen(openPlusScreen: {}, openAppAdvertisementView: {}, openCableTransmissionView: {}, checkForNetworkIssues: {}) WelcomeScreen(openPlusScreen: {}, openAppAdvertisementView: {}, openCableTransmissionView: {}, checkForNetworkIssues: {}, navigationState: WelcomeScreenNavigationState())
} }
+60 -8
View File
@@ -9,6 +9,25 @@ import SwiftUI
import UniformTypeIdentifiers import UniformTypeIdentifiers
import LUI import LUI
struct WelcomeScreenNavigationRequest {
let tab: Tab
let id = UUID()
}
final class WelcomeScreenNavigationState: ObservableObject {
@Published private(set) var request: WelcomeScreenNavigationRequest
init(selection: Tab = .receive) {
self.request = WelcomeScreenNavigationRequest(tab: selection)
}
func select(_ tab: Tab) {
request = WelcomeScreenNavigationRequest(tab: tab)
}
}
struct WelcomeScreen: View { struct WelcomeScreen: View {
static let width: CGFloat = 1000 static let width: CGFloat = 1000
@@ -21,7 +40,23 @@ struct WelcomeScreen: View {
let openCableTransmissionView: () -> Void let openCableTransmissionView: () -> Void
let checkForNetworkIssues: () -> Void let checkForNetworkIssues: () -> Void
@State private var selection: Tab = Tab.receive @ObservedObject var navigationState: WelcomeScreenNavigationState
@State private var selection: Tab
init(
openPlusScreen: @escaping () -> Void,
openAppAdvertisementView: @escaping () -> Void,
openCableTransmissionView: @escaping () -> Void,
checkForNetworkIssues: @escaping () -> Void,
navigationState: WelcomeScreenNavigationState
) {
self.openPlusScreen = openPlusScreen
self.openAppAdvertisementView = openAppAdvertisementView
self.openCableTransmissionView = openCableTransmissionView
self.checkForNetworkIssues = checkForNetworkIssues
self.navigationState = navigationState
_selection = State(initialValue: navigationState.request.tab)
}
var body: some View { var body: some View {
@@ -35,7 +70,7 @@ struct WelcomeScreen: View {
) )
List(selection: listBinding) { List(selection: listBinding) {
ForEach(Tab.allCases.filter({$0 != .settings }), id: \.self) { tab in ForEach([Tab.receive, .send, .notificationSync, .clipboardSync], id: \.self) { tab in
Label(tab.sidebarTitle, systemImage: tab.systemImage) Label(tab.sidebarTitle, systemImage: tab.systemImage)
.tag(tab) .tag(tab)
.frame(height: 30) .frame(height: 30)
@@ -43,6 +78,12 @@ struct WelcomeScreen: View {
Divider() Divider()
Label(Tab.troubleshooting.sidebarTitle, systemImage: Tab.troubleshooting.systemImage)
.tag(Tab.troubleshooting)
.frame(height: 30)
Divider()
ExternalLinkLabel(label: "GetSupport", icon: "questionmark.circle") { ExternalLinkLabel(label: "GetSupport", icon: "questionmark.circle") {
LogExportPresenter.showUploadLogsAlert(openSupportMailWhenNotUploading: true) LogExportPresenter.showUploadLogsAlert(openSupportMailWhenNotUploading: true)
} }
@@ -76,12 +117,11 @@ struct WelcomeScreen: View {
Color.defaultBackground.edgesIgnoringSafeArea(.vertical) Color.defaultBackground.edgesIgnoringSafeArea(.vertical)
if selection == .settings { if selection == .settings {
SettingsView() SettingsView(openPlus: openPlusScreen)
} }
else { else {
TutorialView( TutorialView(
tab: selection, tab: selection,
openPlus: openPlusScreen,
openAppAdvertisementView: openAppAdvertisementView openAppAdvertisementView: openAppAdvertisementView
) )
.onAppear { .onAppear {
@@ -92,6 +132,11 @@ struct WelcomeScreen: View {
} }
} }
} }
.onReceive(navigationState.$request) { request in
if selection != request.tab {
selection = request.tab
}
}
.onDrop(of: [.fileURL], isTargeted: nil) { providers in .onDrop(of: [.fileURL], isTargeted: nil) { providers in
handleDrop(providers: providers) handleDrop(providers: providers)
return true return true
@@ -182,6 +227,7 @@ enum Tab: CaseIterable {
case receive case receive
case send case send
case notificationSync case notificationSync
case clipboardSync
case troubleshooting case troubleshooting
case settings case settings
@@ -193,6 +239,8 @@ enum Tab: CaseIterable {
return "SendFiles".localized() return "SendFiles".localized()
case .notificationSync: case .notificationSync:
return "NotificationSyncSidebar".localized() return "NotificationSyncSidebar".localized()
case .clipboardSync:
return "ClipboardSyncSidebar".localized()
case .troubleshooting: case .troubleshooting:
return "TroubleshootingAndFaq".localized() return "TroubleshootingAndFaq".localized()
case .settings: case .settings:
@@ -210,11 +258,13 @@ enum Tab: CaseIterable {
var text: String { var text: String {
switch self { switch self {
case .receive: case .receive:
"UserManualDescription" "UserManualDescription".localized()
case .send: case .send:
"SendFilesDescription" "SendFilesDescription".localized()
case .notificationSync: case .notificationSync:
"NotificationSyncManualDescription" "NotificationSyncManualDescription".localized()
case .clipboardSync:
"ClipboardSyncManualDescription".localized()
default: default:
"" ""
} }
@@ -228,6 +278,8 @@ enum Tab: CaseIterable {
return "tray.and.arrow.up" return "tray.and.arrow.up"
case .notificationSync: case .notificationSync:
return "bell" return "bell"
case .clipboardSync:
return "clipboard"
case .troubleshooting: case .troubleshooting:
return "exclamationmark.triangle" return "exclamationmark.triangle"
case .settings: case .settings:
@@ -238,5 +290,5 @@ enum Tab: CaseIterable {
#Preview { #Preview {
WelcomeScreen(openPlusScreen: {}, openAppAdvertisementView: {}, openCableTransmissionView: {}, checkForNetworkIssues: {}) WelcomeScreen(openPlusScreen: {}, openAppAdvertisementView: {}, openCableTransmissionView: {}, checkForNetworkIssues: {}, navigationState: WelcomeScreenNavigationState())
} }
+4 -4
View File
@@ -291,7 +291,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
controlPlusScreen(false) controlPlusScreen(false)
errorVibration() errorVibration()
ErrorAlertHandler.shared.showErrorAlert(for: name, error: error) ErrorAlertHandler.shared.showErrorAlert(for: name, error: error, isQuickDropPeer: device?.isQuickDropPeer ?? false)
} }
} else { } else {
@@ -359,7 +359,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
alert.informativeText = error.localizedDescription alert.informativeText = error.localizedDescription
alert.addButton(withTitle: "CloseAlert".localized()) alert.addButton(withTitle: "CloseAlert".localized())
alert.runModal() ModelessAlertPresenter.present(alert)
} }
} }
} }
@@ -408,7 +408,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
controlPlusScreen(false) controlPlusScreen(false)
errorVibration() errorVibration()
ErrorAlertHandler.shared.showErrorAlert(for: name, error: error) ErrorAlertHandler.shared.showErrorAlert(for: name, error: error, isQuickDropPeer: device?.isQuickDropPeer ?? false)
} }
return return
} }
@@ -453,7 +453,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
alert.informativeText = error.localizedDescription alert.informativeText = error.localizedDescription
alert.addButton(withTitle: "CloseAlert".localized()) alert.addButton(withTitle: "CloseAlert".localized())
alert.runModal() ModelessAlertPresenter.present(alert)
} }
} }
} }
+1 -1
View File
@@ -63,7 +63,7 @@ class SendModel: ObservableObject, OutboundAppDelegate {
if let name = selectedDevice?.name { if let name = selectedDevice?.name {
errorVibration() errorVibration()
ErrorAlertHandler.shared.showErrorAlert(for: name, error: error) ErrorAlertHandler.shared.showErrorAlert(for: name, error: error, isQuickDropPeer: selectedDevice?.isQuickDropPeer ?? false)
} }
progressValue = nil progressValue = nil
@@ -0,0 +1,145 @@
import Foundation
import CoreBluetooth
import Testing
@testable import QuickDrop
@Suite(.serialized)
struct AndroidAirDropModeDetectorTests {
@Test
func detectsWhenAirDropModeSignalsPassThreshold() {
var classifier = AndroidAirDropModeClassifier(minimumRSSI: -70, requiredPacketsPerSignal: 2)
let firstAirDropMode = classifier.record(.airDropMode(rssi: -58))
#expect(!firstAirDropMode.isDetected)
let detected = classifier.record(.airDropMode(rssi: -56))
#expect(detected.isDetected)
#expect(detected.airDropModePacketCount == 2)
#expect(detected.strongestAirDropModeRSSI == -56)
}
@Test
func matchesObservedMoseyAirDropSuffixVariants() {
#expect(
AndroidAirDropModeClassifier.isAirDropModeAdvertisement(
.airDropMode(manufacturerDataSuffix: "28b429412f624af300", rssi: -58)
)
)
#expect(
AndroidAirDropModeClassifier.isAirDropModeAdvertisement(
.airDropMode(manufacturerDataSuffix: "03b429412f624af300", rssi: -58)
)
)
}
@Test
func ignoresSignalsBelowRSSIThreshold() {
var classifier = AndroidAirDropModeClassifier(minimumRSSI: -70, requiredPacketsPerSignal: 2)
classifier.record(.airDropMode(rssi: -72))
let observation = classifier.record(.airDropMode(rssi: -85))
#expect(!observation.isDetected)
#expect(observation.airDropModePacketCount == 0)
}
#if os(iOS)
@Test
func iOSDetectsNonConnectableFCF1AdvertisementsWithoutServiceData() {
var classifier = AndroidAirDropModeClassifier(minimumRSSI: -70, requiredPacketsPerSignal: 2)
let firstAirDropMode = classifier.record(.iOSAirDropMode(rssi: -62))
#expect(!firstAirDropMode.isDetected)
let detected = classifier.record(.iOSAirDropMode(rssi: -61))
#expect(detected.isDetected)
#expect(detected.airDropModePacketCount == 2)
#expect(detected.strongestAirDropModeRSSI == -61)
}
@Test
func iOSIgnoresBackgroundFCF1AdvertisementsWithServiceData() {
var classifier = AndroidAirDropModeClassifier(minimumRSSI: -70, requiredPacketsPerSignal: 1)
classifier.record(.iOSBackgroundFCF1(rssi: -55))
let observation = classifier.record(.iOSBackgroundFCF1(rssi: -56))
#expect(!observation.isDetected)
#expect(observation.airDropModePacketCount == 0)
}
#else
@Test
func ignoresNearbyAppleAndBackgroundFCF1Advertisements() {
var classifier = AndroidAirDropModeClassifier(minimumRSSI: -70, requiredPacketsPerSignal: 1)
classifier.record(.advertisement(rssi: -50, manufacturerDataHex: "4c001006031dc4e5be78", serviceUUIDs: []))
classifier.record(.advertisement(rssi: -55, manufacturerDataHex: nil, serviceUUIDs: ["FCF1"]))
classifier.record(.advertisement(rssi: -56, manufacturerDataHex: "4c00051200000000000000000128b429412f624af300", serviceUUIDs: []))
let observation = classifier.record(
.advertisement(
rssi: -56,
manufacturerDataHex: "4c00051200000000000000000028b429412f624af300",
serviceUUIDs: ["FCF1"]
)
)
#expect(!observation.isDetected)
#expect(observation.airDropModePacketCount == 0)
}
#endif
@Test
func scansOnlyForAirDropModeServiceAndAllowsDuplicatePackets() {
#expect(AndroidAirDropModeDetector.scanServiceUUIDs.map(\.uuidString) == [AndroidAirDropModeClassifier.airDropModeServiceUUID])
#expect(AndroidAirDropModeDetector.scanOptions[CBCentralManagerScanOptionAllowDuplicatesKey] as? Bool == true)
}
}
private extension AndroidAirDropAdvertisement {
static func iOSAirDropMode(rssi: Int) -> Self {
advertisement(
rssi: rssi,
manufacturerDataHex: nil,
serviceUUIDs: ["FCF1"],
isConnectable: false
)
}
static func iOSBackgroundFCF1(rssi: Int) -> Self {
advertisement(
rssi: rssi,
manufacturerDataHex: nil,
serviceUUIDs: ["FCF1"],
serviceDataHexByUUID: ["FCF1": "0407329497cd97ce4739a79920b877a45bb8d6d1a7"],
isConnectable: true
)
}
static func airDropMode(rssi: Int) -> Self {
airDropMode(manufacturerDataSuffix: "28b429412f624af300", rssi: rssi)
}
static func airDropMode(manufacturerDataSuffix: String, rssi: Int) -> Self {
advertisement(
rssi: rssi,
manufacturerDataHex: "4c000512000000000000000001\(manufacturerDataSuffix)",
serviceUUIDs: ["FCF1"]
)
}
static func advertisement(
rssi: Int,
manufacturerDataHex: String?,
serviceUUIDs: Set<String>,
serviceDataHexByUUID: [String: String] = [:],
isConnectable: Bool? = nil
) -> Self {
AndroidAirDropAdvertisement(
rssi: rssi,
manufacturerDataHex: manufacturerDataHex,
serviceUUIDs: serviceUUIDs,
serviceDataHexByUUID: serviceDataHexByUUID,
isConnectable: isConnectable
)
}
}
+466
View File
@@ -25,6 +25,472 @@
} }
} }
}, },
"NSBluetoothAlwaysUsageDescription" : {
"comment" : "Privacy - Bluetooth Always Usage Description",
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
"value" : "يستخدم QuickDrop تقنية Bluetooth لتحسين اكتشاف الأجهزة القريبة."
}
},
"ca" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop utilitza Bluetooth per millorar la detecció de dispositius propers."
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop používá Bluetooth pro vylepšenou detekci zařízení v okolí."
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop bruger Bluetooth til forbedret registrering af enheder i nærheden."
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop verwendet Bluetooth für eine verbesserte Erkennung von Geräten in der Nähe."
}
},
"el" : {
"stringUnit" : {
"state" : "translated",
"value" : "Το QuickDrop χρησιμοποιεί Bluetooth για βελτιωμένο εντοπισμό κοντινών συσκευών."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
}
},
"en-AU" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
}
},
"en-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop usa Bluetooth para mejorar la detección de dispositivos cercanos."
}
},
"es-419" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop usa Bluetooth para mejorar la detección de dispositivos cercanos."
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop käyttää Bluetoothia läheisten laitteiden parempaan havaitsemiseen."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop utilise le Bluetooth pour améliorer la détection des appareils à proximité."
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop utilise le Bluetooth pour améliorer la détection des appareils à proximité."
}
},
"he" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop משתמש ב-Bluetooth לזיהוי משופר של מכשירים בקרבת מקום."
}
},
"hi" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop आस-पास के डिवाइस की बेहतर पहचान के लिए Bluetooth का उपयोग करता है।"
}
},
"hr" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop koristi Bluetooth za poboljšano otkrivanje uređaja u blizini."
}
},
"hu" : {
"stringUnit" : {
"state" : "translated",
"value" : "A QuickDrop Bluetoothot használ a közeli eszközök fejlettebb felismeréséhez."
}
},
"id" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop menggunakan Bluetooth untuk deteksi perangkat terdekat yang lebih baik."
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop usa il Bluetooth per un rilevamento migliorato dei dispositivi nelle vicinanze."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDropは、近くのデバイスをより正確に検出するためにBluetoothを使用します。"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop은 주변 기기를 더 잘 감지하기 위해 Bluetooth를 사용합니다."
}
},
"ms" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop menggunakan Bluetooth untuk pengesanan peranti berdekatan yang dipertingkatkan."
}
},
"nb" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop bruker Bluetooth for forbedret oppdagelse av enheter i nærheten."
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop gebruikt Bluetooth voor verbeterde detectie van apparaten in de buurt."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop używa Bluetooth do ulepszonego wykrywania pobliskich urządzeń."
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
"value" : "O QuickDrop usa Bluetooth para melhorar a detecção de dispositivos próximos."
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
"value" : "O QuickDrop usa Bluetooth para melhorar a deteção de dispositivos próximos."
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop folosește Bluetooth pentru detectarea îmbunătățită a dispozitivelor din apropiere."
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop использует Bluetooth для улучшенного обнаружения устройств поблизости."
}
},
"sv" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop använder Bluetooth för förbättrad upptäckt av enheter i närheten."
}
},
"th" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop ใช้ Bluetooth เพื่อการตรวจจับอุปกรณ์ใกล้เคียงที่ดียิ่งขึ้น"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop, yakındaki cihazları daha iyi algılamak için Bluetooth kullanır."
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop використовує Bluetooth для покращеного виявлення пристроїв поблизу."
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop sử dụng Bluetooth để phát hiện thiết bị lân cận tốt hơn."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop 使用蓝牙来增强附近设备检测。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop 使用藍牙來增強附近裝置偵測。"
}
}
}
},
"NSBluetoothPeripheralUsageDescription" : {
"comment" : "Privacy - Bluetooth Peripheral Usage Description",
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
"value" : "يستخدم QuickDrop تقنية Bluetooth لتحسين اكتشاف الأجهزة القريبة."
}
},
"ca" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop utilitza Bluetooth per millorar la detecció de dispositius propers."
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop používá Bluetooth pro vylepšenou detekci zařízení v okolí."
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop bruger Bluetooth til forbedret registrering af enheder i nærheden."
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop verwendet Bluetooth für eine verbesserte Erkennung von Geräten in der Nähe."
}
},
"el" : {
"stringUnit" : {
"state" : "translated",
"value" : "Το QuickDrop χρησιμοποιεί Bluetooth για βελτιωμένο εντοπισμό κοντινών συσκευών."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
}
},
"en-AU" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
}
},
"en-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop usa Bluetooth para mejorar la detección de dispositivos cercanos."
}
},
"es-419" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop usa Bluetooth para mejorar la detección de dispositivos cercanos."
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop käyttää Bluetoothia läheisten laitteiden parempaan havaitsemiseen."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop utilise le Bluetooth pour améliorer la détection des appareils à proximité."
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop utilise le Bluetooth pour améliorer la détection des appareils à proximité."
}
},
"he" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop משתמש ב-Bluetooth לזיהוי משופר של מכשירים בקרבת מקום."
}
},
"hi" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop आस-पास के डिवाइस की बेहतर पहचान के लिए Bluetooth का उपयोग करता है।"
}
},
"hr" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop koristi Bluetooth za poboljšano otkrivanje uređaja u blizini."
}
},
"hu" : {
"stringUnit" : {
"state" : "translated",
"value" : "A QuickDrop Bluetoothot használ a közeli eszközök fejlettebb felismeréséhez."
}
},
"id" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop menggunakan Bluetooth untuk deteksi perangkat terdekat yang lebih baik."
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop usa il Bluetooth per un rilevamento migliorato dei dispositivi nelle vicinanze."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDropは、近くのデバイスをより正確に検出するためにBluetoothを使用します。"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop은 주변 기기를 더 잘 감지하기 위해 Bluetooth를 사용합니다."
}
},
"ms" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop menggunakan Bluetooth untuk pengesanan peranti berdekatan yang dipertingkatkan."
}
},
"nb" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop bruker Bluetooth for forbedret oppdagelse av enheter i nærheten."
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop gebruikt Bluetooth voor verbeterde detectie van apparaten in de buurt."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop używa Bluetooth do ulepszonego wykrywania pobliskich urządzeń."
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
"value" : "O QuickDrop usa Bluetooth para melhorar a detecção de dispositivos próximos."
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
"value" : "O QuickDrop usa Bluetooth para melhorar a deteção de dispositivos próximos."
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop folosește Bluetooth pentru detectarea îmbunătățită a dispozitivelor din apropiere."
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop использует Bluetooth для улучшенного обнаружения устройств поблизости."
}
},
"sv" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop använder Bluetooth för förbättrad upptäckt av enheter i närheten."
}
},
"th" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop ใช้ Bluetooth เพื่อการตรวจจับอุปกรณ์ใกล้เคียงที่ดียิ่งขึ้น"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop, yakındaki cihazları daha iyi algılamak için Bluetooth kullanır."
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop використовує Bluetooth для покращеного виявлення пристроїв поблизу."
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop sử dụng Bluetooth để phát hiện thiết bị lân cận tốt hơn."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop 使用蓝牙来增强附近设备检测。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "QuickDrop 使用藍牙來增強附近裝置偵測。"
}
}
}
},
"NSHumanReadableCopyright" : { "NSHumanReadableCopyright" : {
"comment" : "Copyright (human-readable)", "comment" : "Copyright (human-readable)",
"extractionState" : "extracted_with_value", "extractionState" : "extracted_with_value",
+1 -1
View File
@@ -47,7 +47,7 @@ class SendModel: ObservableObject, OutboundAppDelegate {
func connectionFailed(with error: any Error) { func connectionFailed(with error: any Error) {
ErrorAlertHandler.shared.showErrorAlert(for: selectedDevice?.name ?? "?", error: error) ErrorAlertHandler.shared.showErrorAlert(for: selectedDevice?.name ?? "?", error: error, isQuickDropPeer: selectedDevice?.isQuickDropPeer ?? false)
progressValue = nil progressValue = nil
selectedDevice = nil selectedDevice = nil
+112 -3
View File
@@ -14,7 +14,51 @@ struct ContentView: View {
@ObservedObject var luiSettings = LUISettings.sharedInstance @ObservedObject var luiSettings = LUISettings.sharedInstance
@ObservedObject var iapManager = IAPManager.sharedInstance @ObservedObject var iapManager = IAPManager.sharedInstance
#if !EXTENSION
@ObservedObject private var bluetoothPermission = BluetoothPermissionManager.shared
@Environment(\.scenePhase) private var scenePhase
@State private var showAndroidAirDropModeIssue = false
@State private var didShowAndroidAirDropModeIssue = false
@State private var shouldRequestBluetoothPermissionAutomatically = LUISettings.sharedInstance.appLaunchedBefore
#endif
var body: some View { var body: some View {
#if !EXTENSION
rootContent
.sheet(isPresented: $showAndroidAirDropModeIssue) {
NavigationView {
AndroidAirDropModeIssueView()
.navigationTitle(String("QuickDrop"))
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: XButton(action: { showAndroidAirDropModeIssue = false }))
}
}
.onAppear {
requestBluetoothPermissionForExistingUserIfNeeded()
startAndroidAirDropModeDetectionIfAllowed()
}
.onChange(of: bluetoothPermission.authorization) { _ in
startAndroidAirDropModeDetectionIfAllowed()
}
.onChange(of: scenePhase) { newValue in
switch newValue {
case .active:
requestBluetoothPermissionForExistingUserIfNeeded()
startAndroidAirDropModeDetectionIfAllowed()
case .background:
AndroidAirDropModeDetector.shared.stop()
case .inactive:
break
@unknown default:
break
}
}
#else
rootContent
#endif
}
private var rootContent: some View {
AppRootView(phoneView: { AppRootView(phoneView: {
DeviceListView() DeviceListView()
.environment(\.sheetActive, isShareExtension()) .environment(\.sheetActive, isShareExtension())
@@ -26,16 +70,81 @@ struct ContentView: View {
NavigationLinkLabel(imageName: "folder.fill", text: "BrowseDownloadedFiles") NavigationLinkLabel(imageName: "folder.fill", text: "BrowseDownloadedFiles")
} }
LUILink(destination: NavigationSubView(header: "TrustedDevices") { LUILink(destination: TrustedDevicesView()) {
TrustedDevicesView()
}) {
NavigationLinkLabel(imageName: "checkmark.shield.fill", text: "ManageTrustedDevices", backgroundColor: .green) NavigationLinkLabel(imageName: "checkmark.shield.fill", text: "ManageTrustedDevices", backgroundColor: .green)
} }
} }
CustomSection {
LUILink(destination: QuickDropFAQSettingsView()) {
NavigationLinkLabel(imageName: "questionmark.circle.fill", text: "TroubleshootingAndFaq", backgroundColor: .orange)
}
}
}, firstIntroductionView: { }, firstIntroductionView: {
LocalNetworkPermissionView() LocalNetworkPermissionView()
}) })
} }
#if !EXTENSION
private func requestBluetoothPermissionForExistingUserIfNeeded() {
guard shouldRequestBluetoothPermissionAutomatically else { return }
requestBluetoothPermissionIfNeeded()
}
private func requestBluetoothPermissionIfNeeded() {
bluetoothPermission.refresh()
guard bluetoothPermission.canRequestPermission else { return }
log("[ContentView] Requesting Bluetooth permission for Android AirDrop mode detection.")
bluetoothPermission.requestPermission { allowed in
log("[ContentView] Bluetooth permission request completed. allowed=\(allowed)")
guard allowed else { return }
DispatchQueue.main.async {
startAndroidAirDropModeDetectionIfAllowed()
}
}
}
private func startAndroidAirDropModeDetectionIfAllowed() {
guard !didShowAndroidAirDropModeIssue else { return }
bluetoothPermission.refresh()
guard bluetoothPermission.isAllowed else { return }
AndroidAirDropModeDetector.shared.start { _ in
DispatchQueue.main.async {
guard !didShowAndroidAirDropModeIssue else { return }
didShowAndroidAirDropModeIssue = true
showAndroidAirDropModeIssue = true
}
}
}
#endif
}
private struct QuickDropFAQSettingsView: View {
private let faqItems: [FAQItem] = [
.init(question: "FaqNotVisibleOrConnectingQuestion", answer: "FaqNotVisibleOrConnectingAnswer"),
.init(question: "FaqPhotoDateNotPreservedQuestion", answer: "FaqPhotoDateNotPreservedAnswer"),
.init(question: "FaqAndroidDeviceNotVisibleQuestion", answer: "FaqAndroidDeviceNotVisibleAnswer"),
.init(question: "FaqTrustedDevicesQuestion", answer: "FaqTrustedDevicesAnswer"),
.init(question: "MultipleFilesSendingQuestion", answer: "MultipleFilesSendingAnswer"),
.init(question: "FaqBugQuestion", answer: "FaqBugAnswer"),
]
var body: some View {
NavigationSubView(header: "TroubleshootingAndFaq") {
FAQView(faqItems: faqItems)
}
}
} }
struct DeviceListView: View { struct DeviceListView: View {
+2
View File
@@ -70,6 +70,8 @@ struct SendPickerButton<Label: View>: View {
selectedPhotoItems = [] selectedPhotoItems = []
} catch { } catch {
log("Could not fetch photo URLs: \(error)") log("Could not fetch photo URLs: \(error)")
onResult(nil, nil) // let the caller dismiss the "preparing" it opened on `onPrepare`
selectedPhotoItems = []
} }
} }
} }
@@ -27,9 +27,13 @@ public struct LocalNetworkPermissionView: View {
let manager = NearbyConnectionManager.shared let manager = NearbyConnectionManager.shared
manager.startDeviceDiscovery() manager.startDeviceDiscovery()
} }
}, continueLabel: "introduction_continue", canSkip: false, nextView: { }, continueLabel: "introduction_continue", nextView: {
#if !EXTENSION
BluetoothPermissionView()
#else
IntroductionDoneView() IntroductionDoneView()
}, nextViewSkip: {}, topView: { #endif
}, topView: {
IntroductionIconView(icon: "wifi") IntroductionIconView(icon: "wifi")
}) })
.onChange(of: scenePhase) { newValue in .onChange(of: scenePhase) { newValue in
@@ -41,6 +45,49 @@ public struct LocalNetworkPermissionView: View {
} }
} }
#if !EXTENSION
private struct BluetoothPermissionView: View {
@ObservedObject private var bluetoothPermission = BluetoothPermissionManager.shared
@State private var requestedBluetoothAccess = false
var body: some View {
PermissionView(title: "BluetoothPermissionRequiredTitle",
watchSymbol: "",
subtitle: "BluetoothPermissionRequiredSubtitle",
permissionAction: requestBluetoothAccess,
continueLabel: "introduction_continue",
nextView: {
IntroductionDoneView()
},
topView: {
IntroductionIconView(icon: "antenna.radiowaves.left.and.right")
})
}
private func requestBluetoothAccess() {
requestedBluetoothAccess = true
bluetoothPermission.refresh()
if bluetoothPermission.isAllowed {
IntroductionViewController.sharedInstance.canProceed = true
return
}
guard bluetoothPermission.canRequestPermission else {
IntroductionViewController.sharedInstance.canProceed = true
return
}
bluetoothPermission.requestPermission { _ in
DispatchQueue.main.async {
IntroductionViewController.sharedInstance.canProceed = true
}
}
}
}
#endif
#Preview { #Preview {
NavigationView { NavigationView {
+26 -3
View File
@@ -92,6 +92,9 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
flowLayout.minimumLineSpacing = 10 flowLayout.minimumLineSpacing = 10
listView!.collectionViewLayout = flowLayout listView!.collectionViewLayout = flowLayout
listView!.dataSource = self listView!.dataSource = self
listView!.delegate = self
listView!.isSelectable = true
listView!.allowsMultipleSelection = false
progressDeviceIconWrap!.wantsLayer = true progressDeviceIconWrap!.wantsLayer = true
progressDeviceIconWrap!.layer!.masksToBounds = false progressDeviceIconWrap!.layer!.masksToBounds = false
@@ -225,8 +228,13 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
progressProgressBar?.doubleValue = 0 progressProgressBar?.doubleValue = 0
lastError = error lastError = error
ErrorAlertHandler.shared.showErrorAlert(for: chosenDevice?.name ?? "", error: error) ErrorAlertHandler.shared.showErrorAlert(
self.extensionContext!.cancelRequest(withError: error) for: chosenDevice?.name ?? "",
error: error,
isQuickDropPeer: chosenDevice?.isQuickDropPeer ?? false
) { [weak self] in
self?.extensionContext?.cancelRequest(withError: error)
}
} }
@@ -247,6 +255,7 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
func selectDevice(device: RemoteDeviceInfo) { func selectDevice(device: RemoteDeviceInfo) {
guard chosenDevice == nil else { return }
listViewWrapper?.animator().isHidden = true listViewWrapper?.animator().isHidden = true
dontSeeDeviceButton?.animator().isHidden = true dontSeeDeviceButton?.animator().isHidden = true
@@ -392,7 +401,7 @@ extension ShareViewController: NSCollectionViewDataSource {
guard let collectionViewItem = item as? DeviceListCell else { return item } guard let collectionViewItem = item as? DeviceListCell else { return item }
let device = foundDevices[indexPath[1]] let device = foundDevices[indexPath.item]
collectionViewItem.configure( collectionViewItem.configure(
deviceName: getDeviceName(device: device), deviceName: getDeviceName(device: device),
@@ -417,6 +426,20 @@ extension ShareViewController: NSCollectionViewDataSource {
} }
extension ShareViewController: NSCollectionViewDelegate {
func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set<IndexPath>) {
guard let indexPath = indexPaths.first else { return }
let itemIndex = indexPath.item
guard foundDevices.indices.contains(itemIndex) else { return }
collectionView.deselectItems(at: indexPaths)
selectDevice(device: foundDevices[itemIndex])
}
}
fileprivate func openPrivacyAndSecuritySettings() { fileprivate func openPrivacyAndSecuritySettings() {
guard let url = URL(string: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension") else { return } guard let url = URL(string: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension") else { return }
NSWorkspace.shared.open(url) NSWorkspace.shared.open(url)
+13
View File
@@ -19,6 +19,11 @@ class DeviceListCell: NSCollectionViewItem {
btn.action = #selector(onClick) btn.action = #selector(onClick)
btn.target = self btn.target = self
addClickRecognizer(to: view)
addClickRecognizer(to: textField)
addClickRecognizer(to: imageView)
addClickRecognizer(to: peerMarkerView)
peerMarkerView?.image = NSImage(named: "MenuBarIcon") peerMarkerView?.image = NSImage(named: "MenuBarIcon")
peerMarkerView?.image?.isTemplate = true peerMarkerView?.image?.isTemplate = true
peerMarkerView?.contentTintColor = .white peerMarkerView?.contentTintColor = .white
@@ -34,4 +39,12 @@ class DeviceListCell: NSCollectionViewItem {
imageView?.image = deviceImage imageView?.image = deviceImage
peerMarkerView?.isHidden = !showsQuickDropPeerMarker peerMarkerView?.isHidden = !showsQuickDropPeerMarker
} }
private func addClickRecognizer(to targetView: NSView?) {
guard let targetView else { return }
let recognizer = NSClickGestureRecognizer(target: self, action: #selector(onClick(_:)))
recognizer.buttonMask = 0x1
targetView.addGestureRecognizer(recognizer)
}
} }
+48
View File
@@ -0,0 +1,48 @@
#!/bin/sh
#
# Make Finder > Share > QuickDrop use the app Xcode just built (not a stale Release/archive copy).
#
# macOS LaunchServices registers EVERY QuickDrop.app on disk that shares the bundle id
# (com.leonboettger.neardrop) -- Debug build, Release build, archives, old DerivedData, ... -- and the
# share sheet then picks among those duplicates heuristically, so a stale copy can run instead of the
# one you just built. This script makes the freshly built app the ONLY registered copy of that bundle id.
#
# Usage:
# - Standalone: scripts/use-dev-share-extension.sh (re-points Finder at the newest Debug build)
# - Xcode: add as a Run post-action (Product > Scheme > Edit Scheme > Run > Post-actions >
# "+" > New Run Script Action; "Provide build settings from: QuickDrop"; paste:
# "${SRCROOT}/scripts/use-dev-share-extension.sh"
# Post-actions run outside the build sandbox, so lsregister/mdfind work there even
# though ENABLE_USER_SCRIPT_SANDBOXING is YES for build phases.
#
# `set -u` (not -e): a transient lsregister hiccup shouldn't fail the Xcode Run post-action (red dialog).
set -u
LSREGISTER="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
BUNDLE_ID="com.leonboettger.neardrop"
# Prefer the app Xcode just built (post-action provides these); else the newest Debug build on disk.
APP=""
if [ "${BUILT_PRODUCTS_DIR:-}" != "" ] && [ "${WRAPPER_NAME:-}" != "" ] && [ -d "${BUILT_PRODUCTS_DIR}/${WRAPPER_NAME}" ]; then
APP="${BUILT_PRODUCTS_DIR}/${WRAPPER_NAME}"
else
APP="$(mdfind "kMDItemCFBundleIdentifier == '${BUNDLE_ID}'" 2>/dev/null | grep '/Build/Products/Debug/QuickDrop.app$' | head -1)"
fi
if [ -z "${APP}" ] || [ ! -d "${APP}" ]; then
echo "use-dev-share-extension: no QuickDrop Debug build found -- build it in Xcode first." >&2
exit 0
fi
# Unregister every OTHER on-disk copy of the bundle id, then (re)register only this one.
mdfind "kMDItemCFBundleIdentifier == '${BUNDLE_ID}'" 2>/dev/null | while IFS= read -r other; do
[ "${other}" = "${APP}" ] && continue
"${LSREGISTER}" -u "${other}" 2>/dev/null || true
done
"${LSREGISTER}" -f "${APP}"
# Drop any stale running extension so the next Share rebuilds against the fresh registration.
pkill -f "${BUNDLE_ID}.ShareExtension" 2>/dev/null || true
echo "use-dev-share-extension: Finder Share -> QuickDrop now points at:"
echo " ${APP}"