import UIKit /// The page's keyboard, natively. /// /// Feedly's set, which is what ipx uses: a letter to move through items or feeds, g and a letter /// to go somewhere, ? to list them. None fire with Ctrl, Alt or Command held, so the system's own /// shortcuts still work. /// /// UIKeyCommand rather than SwiftUI's onKeyPress, which is iOS 17 and this still runs on 16. enum Keys { /// What a key asks for. The controller decides how to do it. enum Action: Equatable { case nextItem, previousItem case nextFeed, previousFeed case play, toggleRead, togglePin, openOriginal case readAll, refresh, toggleSidebar, shortcuts case playPause, back15, forward30 case go(Go) case startPair } enum Go: Equatable { case all, directory, popular, listening, settings } /// Single keys, and the ones that only mean something after g. static let single: [String: Action] = [ "j": .nextItem, "n": .nextItem, "k": .previousItem, "p": .previousItem, "J": .nextFeed, "K": .previousFeed, "o": .play, "m": .toggleRead, "s": .togglePin, "v": .openOriginal, "A": .readAll, "r": .refresh, "[": .toggleSidebar, "?": .shortcuts, "g": .startPair, ] static let paired: [String: Go] = [ "a": .all, "d": .directory, "p": .popular, "l": .listening, "s": .settings, ] /// Every key the app answers to, for UIResponder.keyCommands. Space and the arrows are /// listed separately because their input is not a letter. static func commands(_ selector: Selector) -> [UIKeyCommand] { var all = single.keys.map { UIKeyCommand(input: $0, modifierFlags: [], action: selector) } all.append(UIKeyCommand(input: " ", modifierFlags: [], action: selector)) all.append(UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: [], action: selector)) all.append(UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: selector)) // The second key of a g pair: only these, and only just after a g. for key in paired.keys where single[key] == nil { all.append(UIKeyCommand(input: key, modifierFlags: [], action: selector)) } for command in all { command.wantsPriorityOverSystemBehavior = true } return all } /// How long after g the second key still counts, matching the page. static let pairWindow: TimeInterval = 1.5 /// What a key press means, given whether a g is still warm. static func action(for input: String, afterG: Bool) -> Action? { if afterG, let go = paired[input] { return .go(go) } switch input { case " ": return .playPause case UIKeyCommand.inputLeftArrow: return .back15 case UIKeyCommand.inputRightArrow: return .forward30 default: return single[input] } } /// The list ? shows, grouped as the page groups it. static let listing: [(String, [(String, String)])] = [ ("Go to", [("g a", "All Subscriptions"), ("g d", "Directory"), ("g p", "Popular"), ("g l", "Currently Listening"), ("g s", "Settings"), ("⇧ J", "Next feed"), ("⇧ K", "Previous feed"), ("r", "Refresh"), ("[", "Show or hide the feed list")]), ("Items", [("j or n", "Next item"), ("k or p", "Previous item"), ("⇧ A", "Mark all read")]), ("The selected item", [("o", "Play it"), ("m", "Mark it read or unread"), ("s", "Pin it, or unpin it"), ("v", "Open the original")]), ("The player", [("Space", "Play or pause"), ("← →", "Back 15 seconds, forward 30")]), ("Anywhere", [("?", "This list")]), ] }