Update: Popover for Localsend

This commit is contained in:
Hariharan Mudaliar 2026-03-28 15:53:39 +05:30
commit 794e05c7d6
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
7 changed files with 662 additions and 72 deletions

View file

@ -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

View file

@ -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()
}
}
}

View file

@ -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