Add AirDrop detection for iOS

This commit is contained in:
Leon Böttger committed 2026-06-14 21:01:59 +02:00
1 parent d98be9c23c
commit 7d82520c2f
8 files changed
+5161 -1708

No files matched your search

+113 -3
View File
@@ -13,8 +13,52 @@ struct ContentView: View {
@ObservedObject var luiSettings = LUISettings.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 {
#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: {
DeviceListView()
.environment(\.sheetActive, isShareExtension())
@@ -26,16 +70,82 @@ struct ContentView: View {
NavigationLinkLabel(imageName: "folder.fill", text: "BrowseDownloadedFiles")
}
LUILink(destination: NavigationSubView(header: "TrustedDevices") {
TrustedDevicesView()
}) {
LUILink(destination: TrustedDevicesView()) {
NavigationLinkLabel(imageName: "checkmark.shield.fill", text: "ManageTrustedDevices", backgroundColor: .green)
}
}
CustomSection {
LUILink(destination: QuickDropFAQSettingsView()) {
NavigationLinkLabel(imageName: "questionmark.circle.fill", text: "TroubleshootingAndFaq", backgroundColor: .orange)
}
}
}, firstIntroductionView: {
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 {
@@ -28,7 +28,11 @@ public struct LocalNetworkPermissionView: View {
manager.startDeviceDiscovery()
}
}, continueLabel: "introduction_continue", canSkip: false, nextView: {
#if !EXTENSION
BluetoothPermissionView()
#else
IntroductionDoneView()
#endif
}, nextViewSkip: {}, topView: {
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 {
NavigationView {