diff --git a/ApplicationLibrary/Views/Connections/ConnectionListView.swift b/ApplicationLibrary/Views/Connections/ConnectionListView.swift index f585288..584e555 100644 --- a/ApplicationLibrary/Views/Connections/ConnectionListView.swift +++ b/ApplicationLibrary/Views/Connections/ConnectionListView.swift @@ -1,5 +1,8 @@ import Library import SwiftUI +#if canImport(UIKit) && !os(tvOS) + import UIKit +#endif @MainActor public struct ConnectionListView: View { @@ -29,34 +32,33 @@ public struct ConnectionListView: View { #if os(iOS) .toolbar { ToolbarItemGroup(placement: .topBarTrailing) { - Menu { - Picker("State", selection: $viewModel.connectionStateFilter) { - ForEach(ConnectionStateFilter.allCases) { state in - Text(state.name) - } - } - - Picker("Sort By", selection: $viewModel.connectionSort) { - ForEach(ConnectionSort.allCases, id: \.self) { sortBy in - Text(sortBy.name) - } - } - - Button("Close All Connections", role: .destructive) { - viewModel.closeAllConnections() - } - } label: { - Label("Filter", systemImage: "line.3.horizontal.circle") - } + ConnectionMenuButton( + connectionStateFilter: $viewModel.connectionStateFilter, + connectionSort: $viewModel.connectionSort, + closeAllConnections: viewModel.closeAllConnections + ) if #available(iOS 26.0, *), !Variant.debugNoIOS26 { } else { StartStopButton() } } } - #endif - #if os(macOS) - .searchable(text: $viewModel.searchText) + #elseif os(macOS) + .applySearchable(text: $viewModel.searchText, isSearching: $viewModel.isSearching, shouldShow: viewModel.isSearching) + .toolbar { + ToolbarItemGroup { + if #available(macOS 14.0, *) { + Button(action: viewModel.toggleSearch) { + Label("Search", systemImage: "magnifyingglass") + } + } + ConnectionMenuView( + connectionStateFilter: $viewModel.connectionStateFilter, + connectionSort: $viewModel.connectionSort, + closeAllConnections: viewModel.closeAllConnections + ) + } + } #endif .alert($viewModel.alert) .onAppear { @@ -91,3 +93,144 @@ public struct ConnectionListView: View { #endif } } + +#if os(iOS) + private struct ConnectionMenuButton: UIViewRepresentable { + @Binding var connectionStateFilter: ConnectionStateFilter + @Binding var connectionSort: ConnectionSort + let closeAllConnections: () -> Void + @Environment(\.colorScheme) private var colorScheme + + func makeCoordinator() -> Coordinator { + Coordinator() + } + + class Coordinator { + var lastStateFilter: ConnectionStateFilter? + var lastSort: ConnectionSort? + var lastColorScheme: ColorScheme? + } + + func makeUIView(context: Context) -> UIButton { + let button = UIButton(type: .system) + let config = UIImage.SymbolConfiguration(scale: .large) + button.setImage(UIImage(systemName: "line.3.horizontal.circle", withConfiguration: config), for: .normal) + if #available(iOS 17.0, *) { + button.tintColor = colorScheme == .dark ? .white : .black + } + button.showsMenuAsPrimaryAction = true + button.menu = createMenu() + button.setContentHuggingPriority(.required, for: .horizontal) + button.setContentCompressionResistancePriority(.required, for: .horizontal) + + let coordinator = context.coordinator + coordinator.lastStateFilter = connectionStateFilter + coordinator.lastSort = connectionSort + coordinator.lastColorScheme = colorScheme + return button + } + + func updateUIView(_ uiView: UIButton, context: Context) { + let coordinator = context.coordinator + let needsMenuUpdate = coordinator.lastStateFilter != connectionStateFilter || + coordinator.lastSort != connectionSort + + if needsMenuUpdate { + coordinator.lastStateFilter = connectionStateFilter + coordinator.lastSort = connectionSort + uiView.menu = createMenu() + } + + if coordinator.lastColorScheme != colorScheme { + coordinator.lastColorScheme = colorScheme + if #available(iOS 17.0, *) { + uiView.tintColor = colorScheme == .dark ? .white : .black + } + } + } + + private func createMenu() -> UIMenu { + let stateActions = ConnectionStateFilter.allCases.map { state in + UIAction( + title: state.name, + state: connectionStateFilter == state ? .on : .off + ) { _ in + connectionStateFilter = state + } + } + + let stateMenu = UIMenu( + title: NSLocalizedString("State", comment: ""), + options: .singleSelection, + children: stateActions + ) + + let sortActions = ConnectionSort.allCases.map { sort in + UIAction( + title: sort.name, + state: connectionSort == sort ? .on : .off + ) { _ in + connectionSort = sort + } + } + + let sortMenu = UIMenu( + title: NSLocalizedString("Sort By", comment: ""), + options: .singleSelection, + children: sortActions + ) + + let closeAction = UIAction( + title: NSLocalizedString("Close All Connections", comment: ""), + image: UIImage(systemName: "xmark.circle"), + attributes: .destructive + ) { _ in + closeAllConnections() + } + + return UIMenu(children: [stateMenu, sortMenu, closeAction]) + } + } +#elseif os(macOS) + private struct ConnectionMenuView: View { + @Binding var connectionStateFilter: ConnectionStateFilter + @Binding var connectionSort: ConnectionSort + let closeAllConnections: () -> Void + + var body: some View { + Menu { + Picker("State", selection: $connectionStateFilter) { + ForEach(ConnectionStateFilter.allCases) { state in + Text(state.name) + } + } + + Picker("Sort By", selection: $connectionSort) { + ForEach(ConnectionSort.allCases, id: \.self) { sortBy in + Text(sortBy.name) + } + } + + Button("Close All Connections", role: .destructive) { + closeAllConnections() + } + } label: { + Image(systemName: "ellipsis.circle") + } + } + } + + private extension View { + func applySearchable(text: Binding, isSearching: Binding, shouldShow: Bool) -> some View { + if #available(macOS 14.0, *) { + if shouldShow { + return AnyView(searchable(text: text, isPresented: isSearching)) + } else { + return AnyView(self) + } + } else { + return AnyView(searchable(text: text)) + } + } + } +#endif