diff --git a/Views/SearchResults.swift b/Views/SearchResults.swift index 6d5c2c83..2f102a99 100644 --- a/Views/SearchResults.swift +++ b/Views/SearchResults.swift @@ -24,7 +24,7 @@ struct SearchResults: View { @Environment(\.managedObjectContext) private var managedObjectContext @EnvironmentObject private var viewModel: SearchViewModel @EnvironmentObject private var navigation: NavigationViewModel - @FocusState private var focusedSearchItem: URL? + @FocusState private var focusedSearchItem: URL? // macOS only @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \ZimFile.size, ascending: false)], predicate: ZimFile.Predicate.isDownloaded, @@ -92,23 +92,26 @@ struct SearchResults: View { ArticleCell(result: result, zimFile: viewModel.zimFiles[result.zimFileID]) } .buttonStyle(.plain) - .id(result.url) - .focusable() - .focused($focusedSearchItem, equals: result.url) - .modifier(KeyPressHandler(key: .return, action: { - NotificationCenter.openURL(result.url) - })) - .modifier(KeyPressHandler(key: .escape, action: { - $focusedSearchItem.wrappedValue = nil - dismissSearch() - })) + .modifier( + Focusable( // macOS only + $focusedSearchItem, + equals: result.url, + onReturn: { + NotificationCenter.openURL(result.url) + }, + onDismiss: { + $focusedSearchItem.wrappedValue = nil + dismissSearch() + }) + ) } }.padding() } .onReceive(self.focusedSearchItem.publisher) { focusedURL in scrollReader.scrollTo(focusedURL, anchor: .center) } - .onMoveCommand { direction in + .modifier(MoveCommand(perform: { direction in + // macOS only if let focusedSearchItem, let index = viewModel.results.firstIndex(where: { $0.url == focusedSearchItem }) { let nextIndex: Int @@ -121,7 +124,7 @@ struct SearchResults: View { $focusedSearchItem.wrappedValue = viewModel.results[nextIndex].url } } - } + })) } } } @@ -160,7 +163,7 @@ struct SearchResults: View { } header: { searchFilterHeader } } } - .focusable(false) + .modifier(NotFocusable()) // macOS only } private var recentSearchHeader: some View { diff --git a/Views/ViewModifiers/Focusable.swift b/Views/ViewModifiers/Focusable.swift new file mode 100644 index 00000000..8c96e963 --- /dev/null +++ b/Views/ViewModifiers/Focusable.swift @@ -0,0 +1,65 @@ +// This file is part of Kiwix for iOS & macOS. +// +// Kiwix 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 +// any later version. +// +// Kiwix 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 Kiwix; If not, see https://www.gnu.org/licenses/. + +import SwiftUI + +struct NotFocusable: ViewModifier { + + func body(content: Content) -> some View { + #if os(macOS) + content.focusable(false) + #else + content + #endif + } +} + +struct Focusable: ViewModifier { + + private let value: Value + private let focusState: FocusState.Binding + private let onReturn: () -> Void + private let onDissmiss: () -> Void + + init( + _ binding: FocusState.Binding, + equals value: Value, + onReturn: @escaping () -> Void, + onDismiss: @escaping () -> Void + ) { + self.focusState = binding + self.value = value + self.onReturn = onReturn + self.onDissmiss = onDismiss + } + + func body(content: Content) -> some View { + #if os(macOS) + content + .id(value) + .focusable() + .focused(focusState, equals: value) + .modifier(KeyPressHandler(key: .return, action: { + onReturn() + })) + .modifier(KeyPressHandler(key: .escape, action: { + onDissmiss() + })) + #else + content + #endif + + } +} diff --git a/Views/ViewModifiers/KeyPressHandler.swift b/Views/ViewModifiers/KeyPressHandler.swift index db514505..27eb1fb6 100644 --- a/Views/ViewModifiers/KeyPressHandler.swift +++ b/Views/ViewModifiers/KeyPressHandler.swift @@ -21,15 +21,19 @@ struct KeyPressHandler: ViewModifier { let action: () -> Void func body(content: Content) -> some View { + #if os(macOS) if #available(macOS 14.0, *) { - mac14(content: content) + newApi(content: content) } else { content } + #else + content + #endif } - @available(macOS 14.0, *) - private func mac14(content: Content) -> some View { + @available(macOS 14.0, iOS 17.0, *) + private func newApi(content: Content) -> some View { content.onKeyPress(key, action: { Task { await MainActor.run { action() diff --git a/Views/ViewModifiers/MoveCommand.swift b/Views/ViewModifiers/MoveCommand.swift new file mode 100644 index 00000000..c1170e96 --- /dev/null +++ b/Views/ViewModifiers/MoveCommand.swift @@ -0,0 +1,57 @@ +// This file is part of Kiwix for iOS & macOS. +// +// Kiwix 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 +// any later version. +// +// Kiwix 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 Kiwix; If not, see https://www.gnu.org/licenses/. + +import SwiftUI + +enum MoveDirection: Sendable { + + case up + case down + case left + case right + + #if os(macOS) + init?(from direction: MoveCommandDirection) { + switch direction { + case .up: self = .up + case .down: self = .down + case .left: self = .left + case .right: self = .right + @unknown default: return nil + } + } + #endif +} + +struct MoveCommand: ViewModifier { + + private let action: ((MoveDirection) -> Void)? + + init(perform action: ((MoveDirection) -> Void)?) { + self.action = action + } + + func body(content: Content) -> some View { + #if os(macOS) + content.onMoveCommand { (direction: MoveCommandDirection) in + if let mappedDirection = MoveDirection(from: direction) { + action?(mappedDirection) + } + } + #else + content + #endif + } +}