mirror of
https://github.com/leonboe1/QuickDrop.git
synced 2026-11-11 02:51:11 +00:00
Add AirDrop detection for iOS
This commit is contained in:
1 parent
d98be9c23c
commit
7d82520c2f
8 files changed
+5161
-1708
No files matched your search
@@ -37,6 +37,10 @@
|
|||||||
A7D000000000000000000102 /* 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 */; };
|
A7D000000000000000000103 /* AndroidAirDropModeIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D000000000000000000001 /* AndroidAirDropModeIssueView.swift */; };
|
||||||
A7D000000000000000000104 /* 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 */; };
|
||||||
@@ -1624,6 +1628,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 */,
|
||||||
@@ -1640,6 +1646,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 */,
|
||||||
@@ -2407,6 +2415,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.";
|
||||||
@@ -2452,6 +2462,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.";
|
||||||
|
|||||||
+4433
-1697
File diff suppressed because it is too large.
Load diff
@@ -13,11 +13,23 @@ struct AndroidAirDropAdvertisement: Equatable {
|
|||||||
let rssi: Int
|
let rssi: Int
|
||||||
let manufacturerDataHex: String?
|
let manufacturerDataHex: String?
|
||||||
let serviceUUIDs: Set<String>
|
let serviceUUIDs: Set<String>
|
||||||
|
let serviceDataHexByUUID: [String: String]
|
||||||
|
let isConnectable: Bool?
|
||||||
|
|
||||||
init(rssi: Int, manufacturerDataHex: String?, serviceUUIDs: Set<String>) {
|
init(
|
||||||
|
rssi: Int,
|
||||||
|
manufacturerDataHex: String?,
|
||||||
|
serviceUUIDs: Set<String>,
|
||||||
|
serviceDataHexByUUID: [String: String] = [:],
|
||||||
|
isConnectable: Bool? = nil
|
||||||
|
) {
|
||||||
self.rssi = rssi
|
self.rssi = rssi
|
||||||
self.manufacturerDataHex = manufacturerDataHex?.lowercased()
|
self.manufacturerDataHex = manufacturerDataHex?.lowercased()
|
||||||
self.serviceUUIDs = Set(serviceUUIDs.map { $0.uppercased() })
|
self.serviceUUIDs = Set(serviceUUIDs.map { $0.uppercased() })
|
||||||
|
self.serviceDataHexByUUID = Dictionary(
|
||||||
|
uniqueKeysWithValues: serviceDataHexByUUID.map { ($0.key.uppercased(), $0.value.lowercased()) }
|
||||||
|
)
|
||||||
|
self.isConnectable = isConnectable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,8 +94,19 @@ struct AndroidAirDropModeClassifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func isAirDropModeAdvertisement(_ advertisement: AndroidAirDropAdvertisement) -> Bool {
|
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.manufacturerDataHex?.hasPrefix(airDropModeManufacturerPrefix) == true &&
|
||||||
advertisement.serviceUUIDs.contains(airDropModeServiceUUID)
|
advertisement.serviceUUIDs.contains(airDropModeServiceUUID)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func strongerRSSI(_ current: Int?, _ candidate: Int) -> Int {
|
private static func strongerRSSI(_ current: Int?, _ candidate: Int) -> Int {
|
||||||
@@ -184,7 +207,8 @@ final class AndroidAirDropModeDetector: NSObject, CBCentralManagerDelegate {
|
|||||||
private func beginScanning() {
|
private func beginScanning() {
|
||||||
guard !isScanning, let centralManager, centralManager.state == .poweredOn else { return }
|
guard !isScanning, let centralManager, centralManager.state == .poweredOn else { return }
|
||||||
|
|
||||||
log("[AndroidAirDropModeDetector] BLE scan started for service \(AndroidAirDropModeClassifier.airDropModeServiceUUID).")
|
let scanTargetDescription = Self.scanServiceUUIDs.map(\.uuidString).joined(separator: ", ")
|
||||||
|
log("[AndroidAirDropModeDetector] BLE scan started for \(scanTargetDescription).")
|
||||||
|
|
||||||
// Duplicate delivery is intentional: the classifier waits for repeated
|
// Duplicate delivery is intentional: the classifier waits for repeated
|
||||||
// matching packets so one transient advertisement does not trigger the alert.
|
// matching packets so one transient advertisement does not trigger the alert.
|
||||||
@@ -200,6 +224,7 @@ final class AndroidAirDropModeDetector: NSObject, CBCentralManagerDelegate {
|
|||||||
rssi: Int
|
rssi: Int
|
||||||
) -> AndroidAirDropAdvertisement {
|
) -> AndroidAirDropAdvertisement {
|
||||||
var serviceUUIDs = Set<String>()
|
var serviceUUIDs = Set<String>()
|
||||||
|
var serviceDataHexByUUID: [String: String] = [:]
|
||||||
|
|
||||||
[
|
[
|
||||||
CBAdvertisementDataServiceUUIDsKey,
|
CBAdvertisementDataServiceUUIDsKey,
|
||||||
@@ -211,13 +236,18 @@ final class AndroidAirDropModeDetector: NSObject, CBCentralManagerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let serviceData = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] {
|
if let serviceData = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] {
|
||||||
serviceData.keys.forEach { serviceUUIDs.insert($0.uuidString) }
|
serviceData.forEach { uuid, data in
|
||||||
|
serviceUUIDs.insert(uuid.uuidString)
|
||||||
|
serviceDataHexByUUID[uuid.uuidString] = data.androidAirDropModeHexString
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return AndroidAirDropAdvertisement(
|
return AndroidAirDropAdvertisement(
|
||||||
rssi: rssi,
|
rssi: rssi,
|
||||||
manufacturerDataHex: (advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data)?.androidAirDropModeHexString,
|
manufacturerDataHex: (advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data)?.androidAirDropModeHexString,
|
||||||
serviceUUIDs: serviceUUIDs
|
serviceUUIDs: serviceUUIDs,
|
||||||
|
serviceDataHexByUUID: serviceDataHexByUUID,
|
||||||
|
isConnectable: advertisementData[CBAdvertisementDataIsConnectable] as? Bool
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +43,31 @@ struct AndroidAirDropModeDetectorTests {
|
|||||||
#expect(observation.airDropModePacketCount == 0)
|
#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
|
@Test
|
||||||
func ignoresNearbyAppleAndBackgroundFCF1Advertisements() {
|
func ignoresNearbyAppleAndBackgroundFCF1Advertisements() {
|
||||||
var classifier = AndroidAirDropModeClassifier(minimumRSSI: -70, requiredPacketsPerSignal: 1)
|
var classifier = AndroidAirDropModeClassifier(minimumRSSI: -70, requiredPacketsPerSignal: 1)
|
||||||
@@ -61,6 +86,7 @@ struct AndroidAirDropModeDetectorTests {
|
|||||||
#expect(!observation.isDetected)
|
#expect(!observation.isDetected)
|
||||||
#expect(observation.airDropModePacketCount == 0)
|
#expect(observation.airDropModePacketCount == 0)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func scansOnlyForAirDropModeServiceAndAllowsDuplicatePackets() {
|
func scansOnlyForAirDropModeServiceAndAllowsDuplicatePackets() {
|
||||||
@@ -70,6 +96,25 @@ struct AndroidAirDropModeDetectorTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private extension AndroidAirDropAdvertisement {
|
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 {
|
static func airDropMode(rssi: Int) -> Self {
|
||||||
airDropMode(manufacturerDataSuffix: "28b429412f624af300", rssi: rssi)
|
airDropMode(manufacturerDataSuffix: "28b429412f624af300", rssi: rssi)
|
||||||
}
|
}
|
||||||
@@ -85,12 +130,16 @@ private extension AndroidAirDropAdvertisement {
|
|||||||
static func advertisement(
|
static func advertisement(
|
||||||
rssi: Int,
|
rssi: Int,
|
||||||
manufacturerDataHex: String?,
|
manufacturerDataHex: String?,
|
||||||
serviceUUIDs: Set<String>
|
serviceUUIDs: Set<String>,
|
||||||
|
serviceDataHexByUUID: [String: String] = [:],
|
||||||
|
isConnectable: Bool? = nil
|
||||||
) -> Self {
|
) -> Self {
|
||||||
AndroidAirDropAdvertisement(
|
AndroidAirDropAdvertisement(
|
||||||
rssi: rssi,
|
rssi: rssi,
|
||||||
manufacturerDataHex: manufacturerDataHex,
|
manufacturerDataHex: manufacturerDataHex,
|
||||||
serviceUUIDs: serviceUUIDs
|
serviceUUIDs: serviceUUIDs,
|
||||||
|
serviceDataHexByUUID: serviceDataHexByUUID,
|
||||||
|
isConnectable: isConnectable
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -13,8 +13,52 @@ 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,82 @@ 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)
|
||||||
|
.padding(.top)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DeviceListView: View {
|
struct DeviceListView: View {
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ public struct LocalNetworkPermissionView: View {
|
|||||||
manager.startDeviceDiscovery()
|
manager.startDeviceDiscovery()
|
||||||
}
|
}
|
||||||
}, continueLabel: "introduction_continue", canSkip: false, nextView: {
|
}, continueLabel: "introduction_continue", canSkip: false, nextView: {
|
||||||
|
#if !EXTENSION
|
||||||
|
BluetoothPermissionView()
|
||||||
|
#else
|
||||||
IntroductionDoneView()
|
IntroductionDoneView()
|
||||||
|
#endif
|
||||||
}, nextViewSkip: {}, topView: {
|
}, nextViewSkip: {}, topView: {
|
||||||
IntroductionIconView(icon: "wifi")
|
IntroductionIconView(icon: "wifi")
|
||||||
})
|
})
|
||||||
@@ -41,6 +45,53 @@ 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",
|
||||||
|
canSkip: false,
|
||||||
|
nextView: {
|
||||||
|
IntroductionDoneView()
|
||||||
|
},
|
||||||
|
nextViewSkip: {
|
||||||
|
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 {
|
||||||
|
|||||||
Reference in new issue
Block a user