diff --git a/DynamicIsland.xcodeproj/project.pbxproj b/DynamicIsland.xcodeproj/project.pbxproj
index 4c209ab..021b45f 100644
--- a/DynamicIsland.xcodeproj/project.pbxproj
+++ b/DynamicIsland.xcodeproj/project.pbxproj
@@ -58,13 +58,6 @@
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
- 7EA69B212F5B100000EB2D16 /* Exceptions for "DynamicIsland" folder in "DynamicIsland" target */ = {
- isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
- membershipExceptions = (
- Info.plist,
- );
- target = F54406E62CD4987A00C7AFD9 /* DynamicIsland */;
- };
7EA69B1E2F5AE61200EB2D16 /* Exceptions for "Contents" folder in "DynamicIsland" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
@@ -72,6 +65,13 @@
);
target = F54406E62CD4987A00C7AFD9 /* DynamicIsland */;
};
+ 7EA69B212F5B100000EB2D16 /* Exceptions for "DynamicIsland" folder in "DynamicIsland" target */ = {
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+ membershipExceptions = (
+ Info.plist,
+ );
+ target = F54406E62CD4987A00C7AFD9 /* DynamicIsland */;
+ };
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */
@@ -454,7 +454,7 @@
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_ASSET_PATHS = "\"DynamicIsland/Preview Content\"";
- DEVELOPMENT_TEAM = 5W5BDQQ32W;
+ DEVELOPMENT_TEAM = 9Y64TRM77N;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
@@ -526,7 +526,7 @@
CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_ASSET_PATHS = "\"DynamicIsland/Preview Content\"";
DEVELOPMENT_TEAM = "";
- "DEVELOPMENT_TEAM[sdk=macosx*]" = 5W5BDQQ32W;
+ "DEVELOPMENT_TEAM[sdk=macosx*]" = 9Y64TRM77N;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings
index 64ead76..7fb3de0 100644
--- a/DynamicIsland/Localizable.xcstrings
+++ b/DynamicIsland/Localizable.xcstrings
@@ -10032,6 +10032,10 @@
}
}
},
+ "Customize the appearance of the LocalSend device selection popup that appears when you drop files." : {
+ "comment" : "A description of the LocalSend device picker.",
+ "isCommentAutoGenerated" : true
+ },
"Dash outline equals the exact notch bounds." : {
"comment" : "A description under the live preview section of the animation editor.",
"isCommentAutoGenerated" : true,
@@ -10493,6 +10497,10 @@
}
}
},
+ "Device Picker Style" : {
+ "comment" : "A dropdown menu that lets you choose how the LocalSend device picker is displayed.",
+ "isCommentAutoGenerated" : true
+ },
"Devices" : {
"comment" : "The title of the \"Devices\" section in the settings menu.",
"isCommentAutoGenerated" : true,
@@ -16737,6 +16745,10 @@
}
}
},
+ "Liquid Glass Variant" : {
+ "comment" : "A label for selecting the variant of the Liquid Glass effect.",
+ "isCommentAutoGenerated" : true
+ },
"Listens for Focus session changes via distributed notifications" : {
"localizations" : {
"ko" : {
@@ -16927,6 +16939,10 @@
}
}
},
+ "LocalSend Device Picker" : {
+ "comment" : "A section header for the LocalSend device picker settings.",
+ "isCommentAutoGenerated" : true
+ },
"Lock Screen" : {
"localizations" : {
"fr" : {
@@ -17522,6 +17538,10 @@
}
}
},
+ "Make sure LocalSend is running on other devices" : {
+ "comment" : "A description of the error.",
+ "isCommentAutoGenerated" : true
+ },
"Manage indicator preferences from Settings → Privacy. No manual microphone toggle is required." : {
"comment" : "A description of how to manage the microphone privacy indicator preferences.",
"isCommentAutoGenerated" : true,
@@ -24854,6 +24874,14 @@
}
}
},
+ "Scan Again" : {
+ "comment" : "A button to refresh the list of nearby devices.",
+ "isCommentAutoGenerated" : true
+ },
+ "Scanning for devices..." : {
+ "comment" : "A description of the scanning process for devices.",
+ "isCommentAutoGenerated" : true
+ },
"Screen Assistant" : {
"localizations" : {
"ko" : {
@@ -25275,6 +25303,10 @@
}
}
},
+ "Select Device" : {
+ "comment" : "A label displayed above the LocalSend device picker view.",
+ "isCommentAutoGenerated" : true
+ },
"Select one or more profiles to customize your experience" : {
"comment" : "A description below the header that instructs users to select one or more profiles to customize their experience.",
"isCommentAutoGenerated" : true,
@@ -30867,6 +30899,10 @@
},
"shouldTranslate" : false
},
+ "Variant %lld" : {
+ "comment" : "A label for a specific variant of the Liquid Glass.",
+ "isCommentAutoGenerated" : true
+ },
"Version" : {
"localizations" : {
"ar" : {
diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift
index 624d355..a4b40d7 100644
--- a/DynamicIsland/components/Settings/SettingsView.swift
+++ b/DynamicIsland/components/Settings/SettingsView.swift
@@ -793,7 +793,8 @@ struct SettingsView: View {
SettingsSearchEntry(tab: .shelf, title: "Expanded drag detection area", keywords: ["shelf", "drag"], highlightID: SettingsTab.shelf.highlightID(for: "Expanded drag detection area")),
SettingsSearchEntry(tab: .shelf, title: "Copy items on drag", keywords: ["shelf", "drag", "copy"], highlightID: SettingsTab.shelf.highlightID(for: "Copy items on drag")),
SettingsSearchEntry(tab: .shelf, title: "Remove from shelf after dragging", keywords: ["shelf", "drag", "remove"], highlightID: SettingsTab.shelf.highlightID(for: "Remove from shelf after dragging")),
- SettingsSearchEntry(tab: .shelf, title: "Quick Share Service", keywords: ["shelf", "share", "airdrop"], highlightID: SettingsTab.shelf.highlightID(for: "Quick Share Service")),
+ SettingsSearchEntry(tab: .shelf, title: "Quick Share Service", keywords: ["shelf", "share", "airdrop", "localsend"], highlightID: SettingsTab.shelf.highlightID(for: "Quick Share Service")),
+ SettingsSearchEntry(tab: .shelf, title: "LocalSend Device Picker Style", keywords: ["localsend", "glass", "picker", "material"], highlightID: SettingsTab.shelf.highlightID(for: "Device Picker Style")),
// Appearance
SettingsSearchEntry(tab: .appearance, title: "Main screen style", keywords: ["dynamic island", "pill", "non-notch", "display style", "notch style"], highlightID: SettingsTab.appearance.highlightID(for: "Main screen style")),
@@ -3486,6 +3487,10 @@ struct Shelf: View {
.font(.caption)
.foregroundStyle(.secondary)
}
+
+ if quickShareProvider == "LocalSend" {
+ LocalSendSettingsSection(highlightID: highlightID)
+ }
}
.accentColor(.effectiveAccent)
.navigationTitle("Shelf")
@@ -3496,6 +3501,41 @@ struct Shelf: View {
}
}
+// MARK: - LocalSend Settings Section
+
+private struct LocalSendSettingsSection: View {
+ let highlightID: (String) -> String
+
+ @Default(.localSendDevicePickerGlassMode) private var glassMode
+ @Default(.localSendDevicePickerLiquidGlassVariant) private var liquidGlassVariant
+
+ var body: some View {
+ Section {
+ Picker("Device Picker Style", selection: $glassMode) {
+ ForEach(LockScreenGlassCustomizationMode.allCases) { mode in
+ Text(mode.localizedName).tag(mode)
+ }
+ }
+ .pickerStyle(.menu)
+
+ if glassMode == .customLiquid {
+ Picker("Liquid Glass Variant", selection: $liquidGlassVariant) {
+ ForEach(LiquidGlassVariant.allCases) { variant in
+ Text("Variant \(variant.rawValue)").tag(variant)
+ }
+ }
+ .pickerStyle(.menu)
+ }
+ } header: {
+ Text("LocalSend Device Picker")
+ } footer: {
+ Text("Customize the appearance of the LocalSend device selection popup that appears when you drop files.")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ }
+}
+
struct LiveActivitiesSettings: View {
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
@ObservedObject var recordingManager = ScreenRecordingManager.shared
diff --git a/DynamicIsland/components/Shelf/AirDropView.swift b/DynamicIsland/components/Shelf/AirDropView.swift
index 1199646..efa83b0 100644
--- a/DynamicIsland/components/Shelf/AirDropView.swift
+++ b/DynamicIsland/components/Shelf/AirDropView.swift
@@ -29,7 +29,7 @@ struct AirDropView: View {
@Default(.quickShareProvider) var quickShareProvider
@StateObject private var quickShareService = QuickShareService.shared
@State private var showShareSettings = false
- @State private var isGearHover = false
+ @State private var isSwitchHover = false
var body: some View {
dropArea
@@ -50,27 +50,26 @@ struct AirDropView: View {
ZStack {
dropLabel
- // Gear in the bottom-right for quick-share settings
+ // Switch button in the top-right for quick-share settings
VStack {
- Spacer()
HStack {
Spacer()
Button {
quickShareService.ensureDiscovered()
showShareSettings.toggle()
} label: {
- Image(systemName: "gearshape")
+ Image(systemName: "switch.2")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 14, height: 14)
.padding(8)
.background(
RoundedRectangle(cornerRadius: 8)
- .fill(isGearHover ? Color(.windowBackgroundColor).opacity(0.12) : Color.clear)
+ .fill(isSwitchHover ? Color(.windowBackgroundColor).opacity(0.12) : Color.clear)
)
- .foregroundColor(isGearHover ? .accentColor : .gray)
+ .foregroundColor(isSwitchHover ? .accentColor : .gray)
.contentShape(Rectangle())
- .onHover { over in isGearHover = over }
+ .onHover { over in isSwitchHover = over }
.help("Quick Share settings")
}
.buttonStyle(.plain)
@@ -132,8 +131,10 @@ struct AirDropView: View {
}
.padding()
}
- .padding(8)
+ .padding(.trailing, 8)
+ .padding(.top, 8)
}
+ Spacer()
}
}
}
diff --git a/DynamicIsland/components/Shelf/Views/FileShareView.swift b/DynamicIsland/components/Shelf/Views/FileShareView.swift
index 5795f16..cfcc90b 100644
--- a/DynamicIsland/components/Shelf/Views/FileShareView.swift
+++ b/DynamicIsland/components/Shelf/Views/FileShareView.swift
@@ -31,12 +31,14 @@ struct FileShareView: View {
@StateObject private var localSend = LocalSendService.shared
@Default(.quickShareProvider) var quickShareProvider: String
@State private var showQuickSharePopover = false
- @State private var isGearHover = false
+ @State private var isSwitchHover = false
@State private var autoCloseToken = UUID()
@State private var hostView: NSView?
@State private var interactionNonce: UUID = .init()
@State private var isProcessing = false
+ @State private var pendingDropProviders: [NSItemProvider]?
+ @State private var showLocalSendPicker = false
private var selectedProvider: QuickShareProvider {
quickShare.availableProviders.first(where: { $0.id == quickShareProvider }) ?? QuickShareProvider(id: "System Share Menu", imageData: nil, supportsRawText: true)
@@ -56,7 +58,12 @@ struct FileShareView: View {
.onDrop(of: [.fileURL, .url, .utf8PlainText, .plainText, .data, .image], isTargeted: $vm.dropZoneTargeting) { providers in
interactionNonce = .init()
vm.dropEvent = true
- Task { await handleDrop(providers) }
+ if selectedProvider.id == "LocalSend" {
+ pendingDropProviders = providers
+ showLocalSendPicker = true
+ } else {
+ Task { await handleDrop(providers) }
+ }
return true
}
.onTapGesture {
@@ -125,76 +132,30 @@ struct FileShareView: View {
.truncationMode(.tail)
.frame(maxWidth: .infinity)
- // Share mode selector moved to the notch tab gear icon
-
- if selectedProvider.id == "LocalSend" {
- HStack(spacing: 8) {
- Menu {
- if localSend.devices.isEmpty {
- Text("No devices found")
- } else {
- ForEach(localSend.devices) { device in
- Button(device.displayName) {
- localSend.selectedDeviceID = device.id
- }
- }
- }
- } label: {
- HStack(spacing: 6) {
- Image(systemName: "iphone.and.arrow.forward")
- .font(.system(size: 11, weight: .semibold))
- Text(localSend.devices.first(where: { $0.id == localSend.selectedDeviceID })?.displayName ?? "Select device")
- .font(.system(size: 11, weight: .medium))
- .lineLimit(1)
- }
- .foregroundStyle(.white.opacity(0.9))
- }
-
- Button {
- localSend.refreshDeviceScan()
- } label: {
- Image(systemName: "arrow.clockwise")
- .font(.system(size: 11, weight: .semibold))
- .foregroundStyle(.white.opacity(0.9))
- .rotationEffect(.degrees(localSend.isRefreshing ? 360 : 0))
- .animation(
- localSend.isRefreshing
- ? .linear(duration: 0.85).repeatForever(autoreverses: false)
- : .easeOut(duration: 0.2),
- value: localSend.isRefreshing
- )
- }
- .buttonStyle(.plain)
- .disabled(localSend.isRefreshing)
- .help("Refresh devices")
- }
-
- }
-
}
.padding(18)
.frame(maxWidth: .infinity)
- // Gear pinned to top-left corner of quick share box
+ // Switch button pinned to top-right corner
VStack {
HStack {
+ Spacer()
Button {
- // Ensure notch stays open immediately when opening the selector
vm.setAutoCloseSuppression(true, token: autoCloseToken)
quickShare.ensureDiscovered()
showQuickSharePopover.toggle()
} label: {
- Image(systemName: "gear")
+ Image(systemName: "switch.2")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 14, height: 14)
.padding(8)
- .background(RoundedRectangle(cornerRadius: 8).fill(isGearHover ? Color(.windowBackgroundColor).opacity(0.12) : Color.clear))
- .foregroundColor(isGearHover ? .accentColor : .gray)
+ .background(RoundedRectangle(cornerRadius: 8).fill(isSwitchHover ? Color(.windowBackgroundColor).opacity(0.12) : Color.clear))
+ .foregroundColor(isSwitchHover ? .accentColor : .gray)
}
.buttonStyle(PlainButtonStyle())
.onHover { hovering in
- isGearHover = hovering
+ isSwitchHover = hovering
vm.setAutoCloseSuppression(hovering, token: autoCloseToken)
}
.popover(isPresented: $showQuickSharePopover, arrowEdge: .bottom) {
@@ -267,10 +228,8 @@ struct FileShareView: View {
}
.onHover { hovering in vm.setAutoCloseSuppression(hovering, token: autoCloseToken) }
}
- .padding(.leading, 8)
+ .padding(.trailing, 8)
.padding(.top, 8)
-
- Spacer()
}
Spacer()
}
@@ -319,6 +278,28 @@ struct FileShareView: View {
}
}
.contentShape(RoundedRectangle(cornerRadius: 12))
+ .onChange(of: showLocalSendPicker) { _, show in
+ if show {
+ LocalSendDevicePickerWindowManager.shared.show(
+ onDeviceSelected: { device in
+ localSend.selectedDeviceID = device.id
+ showLocalSendPicker = false
+ if let providers = pendingDropProviders {
+ Task {
+ await handleDrop(providers)
+ pendingDropProviders = nil
+ }
+ }
+ },
+ onDismiss: {
+ showLocalSendPicker = false
+ pendingDropProviders = nil
+ }
+ )
+ } else {
+ LocalSendDevicePickerWindowManager.shared.hide()
+ }
+ }
}
// MARK: - Actions
diff --git a/DynamicIsland/managers/LocalSendDevicePickerWindowManager.swift b/DynamicIsland/managers/LocalSendDevicePickerWindowManager.swift
new file mode 100644
index 0000000..8300f8b
--- /dev/null
+++ b/DynamicIsland/managers/LocalSendDevicePickerWindowManager.swift
@@ -0,0 +1,530 @@
+/*
+ * Atoll (DynamicIsland)
+ * Copyright (C) 2024-2026 Atoll Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+import AppKit
+import SwiftUI
+import Defaults
+
+@MainActor
+final class LocalSendDevicePickerWindowManager {
+ static let shared = LocalSendDevicePickerWindowManager()
+
+ private var window: NSWindow?
+ private var onDeviceSelected: ((LocalSendDeviceInfo) -> Void)?
+ private var onDismiss: (() -> Void)?
+
+ private init() {}
+
+ func show(
+ onDeviceSelected: @escaping (LocalSendDeviceInfo) -> Void,
+ onDismiss: @escaping () -> Void
+ ) {
+ self.onDeviceSelected = onDeviceSelected
+ self.onDismiss = onDismiss
+
+ let pickerView = LocalSendDevicePickerView(
+ onDeviceSelected: { [weak self] device in
+ self?.onDeviceSelected?(device)
+ self?.hide()
+ },
+ onDismiss: { [weak self] in
+ self?.onDismiss?()
+ self?.hide()
+ }
+ )
+
+ if let existingWindow = window {
+ existingWindow.contentView = NSHostingView(rootView: pickerView)
+ existingWindow.makeKeyAndOrderFront(nil)
+ return
+ }
+
+ guard let screen = NSScreen.main else { return }
+
+ let windowSize = CGSize(width: 480, height: 400)
+ let windowOrigin = CGPoint(
+ x: screen.frame.midX - windowSize.width / 2,
+ y: screen.frame.midY - windowSize.height / 2
+ )
+
+ let newWindow = NSWindow(
+ contentRect: NSRect(origin: windowOrigin, size: windowSize),
+ styleMask: [.borderless],
+ backing: .buffered,
+ defer: false
+ )
+
+ newWindow.isOpaque = false
+ newWindow.backgroundColor = .clear
+ newWindow.hasShadow = true
+ newWindow.level = .floating
+ newWindow.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
+ newWindow.contentView = NSHostingView(rootView: pickerView)
+ newWindow.isMovableByWindowBackground = true
+
+ window = newWindow
+ newWindow.makeKeyAndOrderFront(nil)
+
+ // Add click-outside-to-dismiss
+ NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) { [weak self] event in
+ guard let self, let window = self.window else { return event }
+ let locationInWindow = event.locationInWindow
+ let windowFrame = window.frame
+ let clickLocation = NSPoint(
+ x: windowFrame.origin.x + locationInWindow.x,
+ y: windowFrame.origin.y + locationInWindow.y
+ )
+
+ if !windowFrame.contains(clickLocation) && event.window != window {
+ self.onDismiss?()
+ self.hide()
+ }
+ return event
+ }
+ }
+
+ func hide() {
+ window?.orderOut(nil)
+ window = nil
+ onDeviceSelected = nil
+ onDismiss = nil
+ }
+}
+
+// MARK: - LocalSend Device Picker View
+
+struct LocalSendDevicePickerView: View {
+ @StateObject private var localSend = LocalSendService.shared
+ @Default(.localSendDevicePickerGlassMode) private var glassMode
+ @Default(.localSendDevicePickerLiquidGlassVariant) private var liquidGlassVariant
+
+ let onDeviceSelected: (LocalSendDeviceInfo) -> Void
+ let onDismiss: () -> Void
+
+ @State private var hoveredDeviceID: String?
+
+ private let cornerRadius: CGFloat = 24
+
+ private var usesCustomLiquidGlass: Bool {
+ glassMode == .customLiquid
+ }
+
+ private var usesStandardLiquidGlass: Bool {
+ guard glassMode == .standard else { return false }
+ if #available(macOS 26.0, *) {
+ return true
+ }
+ return false
+ }
+
+ var body: some View {
+ ZStack {
+ // Background
+ panelBackground
+
+ // Content
+ VStack(spacing: 16) {
+ // Header
+ HStack {
+ Image("LocalSend")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 28, height: 28)
+
+ Text("Select Device")
+ .font(.system(size: 18, weight: .semibold, design: .rounded))
+ .foregroundColor(.white)
+
+ Spacer()
+
+ // Refresh button
+ Button {
+ localSend.refreshDeviceScan()
+ } label: {
+ Image(systemName: "arrow.clockwise")
+ .font(.system(size: 14, weight: .semibold))
+ .foregroundStyle(.white.opacity(0.8))
+ .rotationEffect(.degrees(localSend.isRefreshing ? 360 : 0))
+ .animation(
+ localSend.isRefreshing
+ ? .linear(duration: 0.85).repeatForever(autoreverses: false)
+ : .easeOut(duration: 0.2),
+ value: localSend.isRefreshing
+ )
+ .frame(width: 32, height: 32)
+ .background(Circle().fill(Color.white.opacity(0.1)))
+ }
+ .buttonStyle(.plain)
+ .disabled(localSend.isRefreshing)
+ .help("Refresh devices")
+
+ // Close button
+ Button {
+ onDismiss()
+ } label: {
+ Image(systemName: "xmark")
+ .font(.system(size: 12, weight: .bold))
+ .foregroundStyle(.white.opacity(0.6))
+ .frame(width: 28, height: 28)
+ .background(Circle().fill(Color.white.opacity(0.1)))
+ }
+ .buttonStyle(.plain)
+ }
+ .padding(.horizontal, 20)
+ .padding(.top, 20)
+
+ // Devices grid
+ if localSend.devices.isEmpty {
+ emptyStateView
+ } else {
+ devicesGridView
+ }
+ }
+ }
+ .frame(width: 480, height: 400)
+ .clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
+ .overlay {
+ RoundedRectangle(cornerRadius: cornerRadius)
+ .stroke(Color.white.opacity(0.2), lineWidth: 1)
+ }
+ .shadow(color: Color.black.opacity(0.4), radius: 30, x: 0, y: 15)
+ .onAppear {
+ localSend.startDiscovery()
+ localSend.refreshDeviceScan()
+ }
+ }
+
+ @ViewBuilder
+ private var panelBackground: some View {
+ if usesCustomLiquidGlass {
+ LiquidGlassBackground(variant: liquidGlassVariant, cornerRadius: cornerRadius) {
+ Color.black.opacity(0.3)
+ }
+ } else if usesStandardLiquidGlass {
+ if #available(macOS 26.0, *) {
+ LocalSendGlassBackdrop(cornerRadius: cornerRadius)
+ }
+ } else {
+ RoundedRectangle(cornerRadius: cornerRadius)
+ .fill(.ultraThinMaterial)
+ .background(
+ RoundedRectangle(cornerRadius: cornerRadius)
+ .fill(Color.black.opacity(0.4))
+ )
+ }
+ }
+
+ private var emptyStateView: some View {
+ VStack(spacing: 12) {
+ Spacer()
+
+ if localSend.isRefreshing {
+ ProgressView()
+ .progressViewStyle(CircularProgressViewStyle(tint: .white))
+ .scaleEffect(1.2)
+
+ Text("Scanning for devices...")
+ .font(.system(size: 14, weight: .medium))
+ .foregroundColor(.white.opacity(0.7))
+ } else {
+ Image(systemName: "antenna.radiowaves.left.and.right.slash")
+ .font(.system(size: 48, weight: .light))
+ .foregroundColor(.white.opacity(0.4))
+
+ Text("No devices found")
+ .font(.system(size: 16, weight: .medium))
+ .foregroundColor(.white.opacity(0.7))
+
+ Text("Make sure LocalSend is running on other devices")
+ .font(.system(size: 12))
+ .foregroundColor(.white.opacity(0.5))
+ .multilineTextAlignment(.center)
+
+ Button {
+ localSend.refreshDeviceScan()
+ } label: {
+ Text("Scan Again")
+ .font(.system(size: 13, weight: .medium))
+ .foregroundColor(.white)
+ .padding(.horizontal, 16)
+ .padding(.vertical, 8)
+ .background(
+ Capsule()
+ .fill(Color.accentColor.opacity(0.8))
+ )
+ }
+ .buttonStyle(.plain)
+ .padding(.top, 8)
+ }
+
+ Spacer()
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+
+ private var devicesGridView: some View {
+ ScrollView {
+ LazyVGrid(
+ columns: [
+ GridItem(.flexible(), spacing: 16),
+ GridItem(.flexible(), spacing: 16),
+ GridItem(.flexible(), spacing: 16)
+ ],
+ spacing: 16
+ ) {
+ ForEach(localSend.devices) { device in
+ DeviceGridItem(
+ device: device,
+ isHovered: hoveredDeviceID == device.id,
+ onSelect: {
+ onDeviceSelected(device)
+ }
+ )
+ .onHover { hovering in
+ hoveredDeviceID = hovering ? device.id : nil
+ }
+ }
+ }
+ .padding(.horizontal, 20)
+ .padding(.bottom, 20)
+ }
+ }
+}
+
+// MARK: - Device Grid Item
+
+private struct DeviceGridItem: View {
+ let device: LocalSendDeviceInfo
+ let isHovered: Bool
+ let onSelect: () -> Void
+
+ var body: some View {
+ Button(action: onSelect) {
+ VStack(spacing: 10) {
+ // Device icon
+ ZStack {
+ Circle()
+ .fill(Color.white.opacity(isHovered ? 0.15 : 0.08))
+ .frame(width: 56, height: 56)
+
+ Image(systemName: deviceIcon)
+ .font(.system(size: 24, weight: .medium))
+ .foregroundColor(isHovered ? .accentColor : .white.opacity(0.8))
+ }
+
+ // Device name
+ VStack(spacing: 2) {
+ Text(device.alias)
+ .font(.system(size: 12, weight: .medium))
+ .foregroundColor(.white)
+ .lineLimit(1)
+ .truncationMode(.tail)
+
+ if let model = device.model, !model.isEmpty {
+ Text(model)
+ .font(.system(size: 10))
+ .foregroundColor(.white.opacity(0.5))
+ .lineLimit(1)
+ .truncationMode(.tail)
+ }
+ }
+ }
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 16)
+ .background(
+ RoundedRectangle(cornerRadius: 16)
+ .fill(Color.white.opacity(isHovered ? 0.12 : 0.06))
+ )
+ .overlay(
+ RoundedRectangle(cornerRadius: 16)
+ .stroke(isHovered ? Color.accentColor.opacity(0.6) : Color.white.opacity(0.1), lineWidth: 1)
+ )
+ }
+ .buttonStyle(.plain)
+ .scaleEffect(isHovered ? 1.02 : 1.0)
+ .animation(.spring(response: 0.3, dampingFraction: 0.7), value: isHovered)
+ }
+
+ private var deviceIcon: String {
+ let aliasLower = device.alias.lowercased()
+ let modelLower = (device.model ?? "").lowercased()
+ let combined = aliasLower + " " + modelLower
+
+ // iPhone / iOS devices
+ if combined.contains("iphone") {
+ return "iphone"
+ }
+ if combined.contains("ipad") {
+ return "ipad"
+ }
+ if combined.contains("ipod") {
+ return "ipodtouch"
+ }
+
+ // Android phones by brand
+ if combined.contains("pixel") || combined.contains("nexus") {
+ return "smartphone"
+ }
+ if combined.contains("samsung") || combined.contains("galaxy") {
+ return "smartphone"
+ }
+ if combined.contains("oneplus") || combined.contains("one plus") {
+ return "smartphone"
+ }
+ if combined.contains("xiaomi") || combined.contains("redmi") || combined.contains("poco") || combined.contains("mi ") {
+ return "smartphone"
+ }
+ if combined.contains("huawei") || combined.contains("honor") {
+ return "smartphone"
+ }
+ if combined.contains("oppo") || combined.contains("realme") || combined.contains("vivo") {
+ return "smartphone"
+ }
+ if combined.contains("motorola") || combined.contains("moto ") {
+ return "smartphone"
+ }
+ if combined.contains("nokia") {
+ return "smartphone"
+ }
+ if combined.contains("sony") || combined.contains("xperia") {
+ return "smartphone"
+ }
+ if combined.contains("lg") {
+ return "smartphone"
+ }
+ if combined.contains("asus") && (combined.contains("phone") || combined.contains("rog")) {
+ return "smartphone"
+ }
+ if combined.contains("nothing") && combined.contains("phone") {
+ return "smartphone"
+ }
+
+ // Android tablets
+ if combined.contains("tab") || combined.contains("tablet") {
+ return "tablet.portrait"
+ }
+
+ // Mac devices
+ if combined.contains("macbook") {
+ return "laptopcomputer"
+ }
+ if combined.contains("imac") {
+ return "desktopcomputer"
+ }
+ if combined.contains("mac mini") || combined.contains("mini") && combined.contains("mac") {
+ return "macmini"
+ }
+ if combined.contains("mac pro") {
+ return "macpro.gen3"
+ }
+ if combined.contains("mac studio") {
+ return "macstudio"
+ }
+ if combined.contains("mac") || combined.contains("macos") {
+ return "desktopcomputer"
+ }
+
+ // Windows/Linux laptops and desktops
+ if combined.contains("laptop") || combined.contains("notebook") {
+ return "laptopcomputer"
+ }
+ if combined.contains("thinkpad") || combined.contains("lenovo") {
+ return "laptopcomputer"
+ }
+ if combined.contains("dell") || combined.contains("xps") || combined.contains("inspiron") || combined.contains("latitude") {
+ return "laptopcomputer"
+ }
+ if combined.contains("hp") || combined.contains("pavilion") || combined.contains("envy") || combined.contains("spectre") || combined.contains("elitebook") {
+ return "laptopcomputer"
+ }
+ if combined.contains("asus") || combined.contains("zenbook") || combined.contains("vivobook") {
+ return "laptopcomputer"
+ }
+ if combined.contains("acer") || combined.contains("aspire") || combined.contains("swift") {
+ return "laptopcomputer"
+ }
+ if combined.contains("msi") {
+ return "laptopcomputer"
+ }
+ if combined.contains("surface") {
+ return "laptopcomputer"
+ }
+ if combined.contains("razer") {
+ return "laptopcomputer"
+ }
+ if combined.contains("chromebook") {
+ return "laptopcomputer"
+ }
+
+ // Desktop keywords
+ if combined.contains("desktop") || combined.contains("pc") || combined.contains("workstation") {
+ return "desktopcomputer"
+ }
+ if combined.contains("windows") || combined.contains("linux") || combined.contains("ubuntu") || combined.contains("fedora") {
+ return "desktopcomputer"
+ }
+
+ // Android generic
+ if combined.contains("android") {
+ return "smartphone"
+ }
+
+ // Phone/Mobile generic
+ if combined.contains("phone") || combined.contains("mobile") {
+ return "smartphone"
+ }
+
+ // TV devices
+ if combined.contains("tv") || combined.contains("shield") || combined.contains("fire") || combined.contains("roku") || combined.contains("chromecast") {
+ return "appletv"
+ }
+
+ // Default: try to guess based on common patterns
+ // If it looks like a computer name with a person's name, assume laptop
+ if combined.contains("'s ") || combined.contains("-pc") || combined.contains("-laptop") {
+ return "laptopcomputer"
+ }
+
+ // Default fallback
+ return "desktopcomputer"
+ }
+}
+
+// MARK: - Glass Backdrop for LocalSend Picker
+
+@available(macOS 26.0, *)
+private struct LocalSendGlassBackdrop: View {
+ let cornerRadius: CGFloat
+
+ var body: some View {
+ GeometryReader { proxy in
+ let dynamicFontSize = max(min(proxy.size.width, proxy.size.height) / 8, 42)
+
+ Text("LocalSend Device Picker")
+ .font(.system(size: dynamicFontSize, weight: .semibold, design: .rounded))
+ .foregroundStyle(Color.clear)
+ .frame(width: proxy.size.width, height: proxy.size.height)
+ .glassEffect(
+ .clear.interactive(),
+ in: .rect(cornerRadius: cornerRadius)
+ )
+ }
+ .allowsHitTesting(false)
+ .accessibilityHidden(true)
+ }
+}
diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift
index 392c081..c03afc5 100644
--- a/DynamicIsland/models/Constants.swift
+++ b/DynamicIsland/models/Constants.swift
@@ -889,6 +889,8 @@ extension Defaults.Keys {
static let openShelfByDefault = Key("openShelfByDefault", default: true)
static let quickShareProvider = Key("quickShareProvider", default: "AirDrop")
static let localSendSelectedDeviceID = Key("localSendSelectedDeviceID", default: "")
+ static let localSendDevicePickerGlassMode = Key("localSendDevicePickerGlassMode", default: .standard)
+ static let localSendDevicePickerLiquidGlassVariant = Key("localSendDevicePickerLiquidGlassVariant", default: .v11)
static let copyOnDrag = Key("copyOnDrag", default: false)
static let autoRemoveShelfItems = Key("autoRemoveShelfItems", default: false)
static let expandedDragDetection = Key("expandedDragDetection", default: true)