diff --git a/README.md b/README.md index 8b167d4..9354d5d 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,12 @@ ## Features -- Feed organization with groups, unread tracking, bookmarks, and search -- RSS/Atom parsing with feed discovery support -- Responsive web UI with keyboard shortcuts and PWA support +- Fast reading workflow: unread tracking, bookmarks, search, and Google Reader-style keyboard shortcuts +- Feed management: RSS/Atom parsing, feed auto-discovery, and group organization +- Responsive web UI with PWA support - Self-hosting friendly: single binary or Docker deployment -- No AI features by design: just a focused, distraction-free RSS experience -- Built-in i18n (English, Chinese, German, French, Spanish, Russian, Portuguese, Swedish) +- Built-in i18n: English, Chinese, German, French, Spanish, Russian, Portuguese, Swedish +- No AI features by design: focused, distraction-free RSS reading ## Quick Start (Docker) diff --git a/docs/frontend-design.md b/docs/frontend-design.md index 1113bdb..9e13a27 100644 --- a/docs/frontend-design.md +++ b/docs/frontend-design.md @@ -118,9 +118,25 @@ This keeps list context stable while opening/closing article detail. Implemented shortcuts: - `Cmd/Ctrl + K`: toggle search dialog +- `Cmd/Ctrl + ,`: open settings dialog - `Esc`: close search/settings/article drawer -- `j` / `ArrowDown`: next article -- `k` / `ArrowUp`: previous article +- `/`: open search dialog +- `?`: open keyboard shortcuts help +- `j` / `n` / `ArrowDown`: next article +- `k` / `p` / `ArrowUp`: previous article +- `m`: toggle read/unread for current article +- `s` / `f`: toggle star for current article +- `o` / `v`: open current article in browser +- `g u`: go to unread +- `g a`: go to all +- `g s`: go to starred +- `g f`: go to feed management + +Shortcut help entry points: + +- Sidebar search button hint shows `Cmd+K / ?` +- Search dialog Quick Actions includes a "Keyboard Shortcuts" item +- Settings > Appearance includes a "Keyboard Shortcuts" section ## 10. Authentication UX diff --git a/frontend/src/components/article/article-drawer.tsx b/frontend/src/components/article/article-drawer.tsx index 62d328b..d57f536 100644 --- a/frontend/src/components/article/article-drawer.tsx +++ b/frontend/src/components/article/article-drawer.tsx @@ -67,8 +67,6 @@ export function ArticleDrawer() { const deleteBookmark = useDeleteBookmark(); const articleIds = listArticles.map((a) => a.id); - const { goToNext, goToPrevious, hasNext, hasPrevious } = - useArticleNavigation(articleIds); const storeArticle = selectedArticleId ? (listArticles.find((i) => i.id === selectedArticleId) ?? null) @@ -140,6 +138,18 @@ export function ArticleDrawer() { } }; + const { goToNext, goToPrevious, hasNext, hasPrevious } = + useArticleNavigation(articleIds, { + enabled: selectedArticleId !== null, + onToggleRead: () => { + void handleToggleRead(); + }, + onToggleStar: () => { + void handleToggleStar(); + }, + onOpenOriginal: handleOpenOriginal, + }); + return ( a.id); - useArticleNavigation(articleIds); + useArticleNavigation(articleIds, { + enabled: selectedArticleId === null, + }); // Determine title let title = t("article.list.all"); diff --git a/frontend/src/components/layout/app-layout.tsx b/frontend/src/components/layout/app-layout.tsx index bac9525..da24237 100644 --- a/frontend/src/components/layout/app-layout.tsx +++ b/frontend/src/components/layout/app-layout.tsx @@ -9,6 +9,7 @@ import { AddGroupDialog } from "@/components/group/add-group-dialog"; import { AddFeedDialog } from "@/components/feed/add-feed-dialog"; import { EditFeedDialog } from "@/components/feed/edit-feed-dialog"; import { ImportOpmlDialog } from "@/components/feed/import-opml-dialog"; +import { ShortcutsDialog } from "@/components/layout/shortcuts-dialog"; import { useKeyboardShortcuts } from "@/hooks/use-keyboard"; import { useI18n } from "@/lib/i18n"; import { useIsMobile } from "@/hooks/use-mobile"; @@ -63,6 +64,7 @@ export function AppLayout({ children }: AppLayoutProps) { + ); } diff --git a/frontend/src/components/layout/shortcuts-dialog.tsx b/frontend/src/components/layout/shortcuts-dialog.tsx new file mode 100644 index 0000000..f8f2353 --- /dev/null +++ b/frontend/src/components/layout/shortcuts-dialog.tsx @@ -0,0 +1,87 @@ +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { useI18n } from "@/lib/i18n"; +import { useUIStore } from "@/store"; + +interface ShortcutItem { + keys: string; + action: string; +} + +interface ShortcutSection { + title: string; + items: ShortcutItem[]; +} + +export function ShortcutsDialog() { + const { t } = useI18n(); + const isShortcutsOpen = useUIStore((s) => s.isShortcutsOpen); + const setShortcutsOpen = useUIStore((s) => s.setShortcutsOpen); + + const sections: ShortcutSection[] = [ + { + title: t("shortcuts.section.global"), + items: [ + { keys: "Cmd+K / Ctrl+K", action: t("shortcuts.action.toggleSearch") }, + { keys: "/", action: t("shortcuts.action.openSearch") }, + { keys: "Cmd+, / Ctrl+,", action: t("shortcuts.action.openSettings") }, + { keys: "?", action: t("shortcuts.action.showHelp") }, + { keys: "Esc", action: t("shortcuts.action.closeDialog") }, + ], + }, + { + title: t("shortcuts.section.article"), + items: [ + { + keys: "j / ArrowDown / n", + action: t("shortcuts.action.nextArticle"), + }, + { + keys: "k / ArrowUp / p", + action: t("shortcuts.action.previousArticle"), + }, + { keys: "m", action: t("shortcuts.action.toggleRead") }, + { keys: "s / f", action: t("shortcuts.action.toggleStar") }, + { keys: "o / v", action: t("shortcuts.action.openOriginal") }, + ], + }, + { + title: t("shortcuts.section.navigation"), + items: [ + { keys: "g u", action: t("shortcuts.action.goUnread") }, + { keys: "g a", action: t("shortcuts.action.goAll") }, + { keys: "g s", action: t("shortcuts.action.goStarred") }, + { keys: "g f", action: t("shortcuts.action.goFeeds") }, + ], + }, + ]; + + return ( + + + + {t("shortcuts.title")} + +
+ {sections.map((section) => ( +
+

{section.title}

+
+ {section.items.map((item) => ( +
+ {item.action} + + {item.keys} + +
+ ))} +
+
+ ))} +
+
+
+ ); +} diff --git a/frontend/src/components/layout/sidebar.tsx b/frontend/src/components/layout/sidebar.tsx index 8b44c94..1388247 100644 --- a/frontend/src/components/layout/sidebar.tsx +++ b/frontend/src/components/layout/sidebar.tsx @@ -35,7 +35,7 @@ export function Sidebar() { {t("sidebar.search")} - ⌘K + Cmd+K / ? diff --git a/frontend/src/components/search/search-dialog.tsx b/frontend/src/components/search/search-dialog.tsx index 23268c9..9ca065a 100644 --- a/frontend/src/components/search/search-dialog.tsx +++ b/frontend/src/components/search/search-dialog.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { FileText, Loader2, Search, Settings } from "lucide-react"; +import { FileText, Keyboard, Loader2, Search, Settings } from "lucide-react"; import { getFaviconUrl } from "@/lib/api/favicon"; import { searchAPI } from "@/lib/api"; import type { SearchFeed, SearchItem } from "@/lib/api/types"; @@ -29,7 +29,8 @@ import { FeedFavicon } from "@/components/feed/feed-favicon"; export function SearchDialog() { const { t } = useI18n(); - const { isSearchOpen, setSearchOpen, setEditFeedOpen } = useUIStore(); + const { isSearchOpen, setSearchOpen, setEditFeedOpen, setShortcutsOpen } = + useUIStore(); const { getFeedById } = useFeedLookup(); const { setSelectedFeed, setSelectedArticle } = useUrlState(); const [query, setQuery] = useState(""); @@ -106,6 +107,11 @@ export function SearchDialog() { setSearchOpen(open); }; + const handleOpenShortcuts = () => { + setSearchOpen(false); + setShortcutsOpen(true); + }; + return ( @@ -196,6 +202,18 @@ export function SearchDialog() { {t("search.quickActionHint")} + +
+ + {t("shortcuts.title")} +
+ + ? + +
)} diff --git a/frontend/src/components/settings/settings-dialog.tsx b/frontend/src/components/settings/settings-dialog.tsx index d439002..ad40208 100644 --- a/frontend/src/components/settings/settings-dialog.tsx +++ b/frontend/src/components/settings/settings-dialog.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useTheme } from "next-themes"; import { toast } from "sonner"; -import { Bug, Download, Github, Info, Palette } from "lucide-react"; +import { Bug, Download, Github, Info, Keyboard, Palette } from "lucide-react"; import { Dialog, DialogContent } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { @@ -50,6 +50,8 @@ function NavItem({ icon, label, active, onClick }: NavItemProps) { function AppearanceContent() { const { t } = useI18n(); const { theme, setTheme } = useTheme(); + const setSettingsOpen = useUIStore((s) => s.setSettingsOpen); + const setShortcutsOpen = useUIStore((s) => s.setShortcutsOpen); const { locale, articlePageSize, setLocale, setArticlePageSize } = usePreferencesStore(); @@ -128,6 +130,27 @@ function AppearanceContent() { + + {/* Keyboard shortcuts */} +
+
+

{t("settings.shortcuts.label")}

+

+ {t("settings.shortcuts.description")} +

+
+ +
); } diff --git a/frontend/src/hooks/use-keyboard.ts b/frontend/src/hooks/use-keyboard.ts index c459d2b..a3d057c 100644 --- a/frontend/src/hooks/use-keyboard.ts +++ b/frontend/src/hooks/use-keyboard.ts @@ -1,52 +1,241 @@ -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; +import { useNavigate } from "@tanstack/react-router"; import { useUIStore } from "@/store"; import { useUrlState } from "./use-url-state"; +function isTypingTarget(target: EventTarget | null): boolean { + if (!(target instanceof HTMLElement)) { + return false; + } + + if (target.isContentEditable) { + return true; + } + + const tagName = target.tagName; + if (tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT") { + return true; + } + + return target.closest("[contenteditable='true'], [data-hotkey-ignore='true']") !== null; +} + +interface ArticleNavigationOptions { + enabled?: boolean; + onToggleRead?: () => void | Promise; + onToggleStar?: () => void | Promise; + onOpenOriginal?: () => void; +} + export function useKeyboardShortcuts() { - const { setSearchOpen, setSettingsOpen, isSearchOpen, isSettingsOpen } = - useUIStore(); - const { selectedArticleId, setSelectedArticle } = useUrlState(); + const setSearchOpen = useUIStore((s) => s.setSearchOpen); + const setSettingsOpen = useUIStore((s) => s.setSettingsOpen); + const setShortcutsOpen = useUIStore((s) => s.setShortcutsOpen); + const isSearchOpen = useUIStore((s) => s.isSearchOpen); + const isSettingsOpen = useUIStore((s) => s.isSettingsOpen); + const isShortcutsOpen = useUIStore((s) => s.isShortcutsOpen); + const { selectedArticleId, setSelectedArticle, selectTopLevelFilter } = + useUrlState(); + const navigate = useNavigate(); + const pendingPrefixRef = useRef<"g" | null>(null); + const pendingPrefixTimerRef = useRef(null); useEffect(() => { + function resetPrefix() { + pendingPrefixRef.current = null; + if (pendingPrefixTimerRef.current !== null) { + window.clearTimeout(pendingPrefixTimerRef.current); + pendingPrefixTimerRef.current = null; + } + } + + function startPrefix(prefix: "g") { + resetPrefix(); + pendingPrefixRef.current = prefix; + pendingPrefixTimerRef.current = window.setTimeout(() => { + resetPrefix(); + }, 1200); + } + function handleKeyDown(event: KeyboardEvent) { + if (event.defaultPrevented) { + return; + } + + const key = event.key.toLowerCase(); + // ⌘K or Ctrl+K: Open search - if ((event.metaKey || event.ctrlKey) && event.key === "k") { + if ((event.metaKey || event.ctrlKey) && key === "k") { event.preventDefault(); setSearchOpen(!isSearchOpen); + resetPrefix(); + return; + } + + // ⌘, or Ctrl+, : Open settings + if ((event.metaKey || event.ctrlKey) && event.key === ",") { + event.preventDefault(); + setSettingsOpen(true); + resetPrefix(); return; } // ESC: Close modals/drawer if (event.key === "Escape") { if (isSearchOpen) { + resetPrefix(); setSearchOpen(false); return; } if (isSettingsOpen) { + resetPrefix(); setSettingsOpen(false); return; } + if (isShortcutsOpen) { + resetPrefix(); + setShortcutsOpen(false); + return; + } if (selectedArticleId !== null) { + resetPrefix(); setSelectedArticle(null); return; } + + resetPrefix(); + return; + } + + if (event.metaKey || event.ctrlKey || event.altKey) { + return; + } + + if (isTypingTarget(event.target)) { + return; + } + + if (isSearchOpen || isSettingsOpen || isShortcutsOpen) { + return; + } + + if (pendingPrefixRef.current === "g") { + if (key === "u") { + event.preventDefault(); + selectTopLevelFilter("unread"); + resetPrefix(); + return; + } + if (key === "a") { + event.preventDefault(); + selectTopLevelFilter("all"); + resetPrefix(); + return; + } + if (key === "s") { + event.preventDefault(); + selectTopLevelFilter("starred"); + resetPrefix(); + return; + } + if (key === "f") { + event.preventDefault(); + navigate({ to: "/feeds" }); + resetPrefix(); + return; + } + + resetPrefix(); + } + + if (key === "g") { + event.preventDefault(); + startPrefix("g"); + return; + } + + // /: Open search + if (event.key === "/") { + event.preventDefault(); + setSearchOpen(true); + return; + } + + // ?: Open shortcuts help + if (event.key === "?") { + event.preventDefault(); + setShortcutsOpen(true); + return; } } - document.addEventListener("keydown", handleKeyDown); - return () => document.removeEventListener("keydown", handleKeyDown); - }, [isSearchOpen, isSettingsOpen, selectedArticleId, setSearchOpen, setSettingsOpen, setSelectedArticle]); + document.addEventListener("keydown", handleKeyDown, true); + return () => { + if (pendingPrefixTimerRef.current !== null) { + window.clearTimeout(pendingPrefixTimerRef.current); + } + document.removeEventListener("keydown", handleKeyDown, true); + }; + }, [ + isSearchOpen, + isSettingsOpen, + isShortcutsOpen, + navigate, + selectTopLevelFilter, + selectedArticleId, + setSearchOpen, + setSettingsOpen, + setShortcutsOpen, + setSelectedArticle, + ]); } -export function useArticleNavigation(articleIds: number[]) { +export function useArticleNavigation( + articleIds: number[], + options: ArticleNavigationOptions = {}, +) { const { selectedArticleId, setSelectedArticle } = useUrlState(); + const isSearchOpen = useUIStore((s) => s.isSearchOpen); + const isSettingsOpen = useUIStore((s) => s.isSettingsOpen); + const isAddGroupOpen = useUIStore((s) => s.isAddGroupOpen); + const isAddFeedOpen = useUIStore((s) => s.isAddFeedOpen); + const isEditFeedOpen = useUIStore((s) => s.isEditFeedOpen); + const isImportOpmlOpen = useUIStore((s) => s.isImportOpmlOpen); + const isShortcutsOpen = useUIStore((s) => s.isShortcutsOpen); + const { + enabled = true, + onToggleRead, + onToggleStar, + onOpenOriginal, + } = options; useEffect(() => { + if (!enabled) { + return; + } + function handleKeyDown(event: KeyboardEvent) { - // Don't handle if we're in an input or if no article is selected + if (event.defaultPrevented) { + return; + } + + // Don't handle navigation keys while typing in form fields + if (isTypingTarget(event.target)) { + return; + } + + if (event.metaKey || event.ctrlKey || event.altKey) { + return; + } + if ( - event.target instanceof HTMLInputElement || - event.target instanceof HTMLTextAreaElement + isSearchOpen || + isSettingsOpen || + isAddGroupOpen || + isAddFeedOpen || + isEditFeedOpen || + isImportOpmlOpen || + isShortcutsOpen ) { return; } @@ -55,8 +244,10 @@ export function useArticleNavigation(articleIds: number[]) { ? articleIds.indexOf(selectedArticleId) : -1; + const key = event.key.toLowerCase(); + // J or ArrowDown: Next article - if (event.key === "j" || event.key === "ArrowDown") { + if (key === "j" || key === "n" || event.key === "ArrowDown") { event.preventDefault(); if (currentIndex < articleIds.length - 1) { setSelectedArticle(articleIds[currentIndex + 1]); @@ -65,7 +256,7 @@ export function useArticleNavigation(articleIds: number[]) { } // K or ArrowUp: Previous article - if (event.key === "k" || event.key === "ArrowUp") { + if (key === "k" || key === "p" || event.key === "ArrowUp") { event.preventDefault(); if (currentIndex > 0) { setSelectedArticle(articleIds[currentIndex - 1]); @@ -73,17 +264,50 @@ export function useArticleNavigation(articleIds: number[]) { return; } - // Enter: Open selected article - if (event.key === "Enter" && currentIndex >= 0) { + if (selectedArticleId === null) { + return; + } + + // M: toggle read/unread + if (key === "m" && onToggleRead) { event.preventDefault(); - // Article is already selected, just keeping selection + void onToggleRead(); + return; + } + + // S/F: toggle star + if ((key === "s" || key === "f") && onToggleStar) { + event.preventDefault(); + void onToggleStar(); + return; + } + + // O/V: open original article + if ((key === "o" || key === "v") && onOpenOriginal) { + event.preventDefault(); + onOpenOriginal(); return; } } document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); - }, [articleIds, selectedArticleId, setSelectedArticle]); + }, [ + articleIds, + enabled, + isAddFeedOpen, + isAddGroupOpen, + isEditFeedOpen, + isImportOpmlOpen, + isSearchOpen, + isSettingsOpen, + isShortcutsOpen, + onOpenOriginal, + onToggleRead, + onToggleStar, + selectedArticleId, + setSelectedArticle, + ]); const goToNext = () => { const currentIndex = selectedArticleId diff --git a/frontend/src/lib/i18n/messages/de.ts b/frontend/src/lib/i18n/messages/de.ts index 4508425..07462ab 100644 --- a/frontend/src/lib/i18n/messages/de.ts +++ b/frontend/src/lib/i18n/messages/de.ts @@ -30,6 +30,24 @@ export const deMessages: PartialMessages = { "search.quickActionHint": "Tippen, um Feeds und Artikel zu durchsuchen", "search.title": "Suche", "search.unknownFeed": "Unbekannter Feed", + "shortcuts.action.closeDialog": "Offenen Dialog oder Drawer schliessen", + "shortcuts.action.goAll": "Zu allen Artikeln wechseln", + "shortcuts.action.goFeeds": "Zur Feed-Verwaltung wechseln", + "shortcuts.action.goStarred": "Zu favorisierten Artikeln wechseln", + "shortcuts.action.goUnread": "Zu ungelesenen Artikeln wechseln", + "shortcuts.action.nextArticle": "Nachsten Artikel auswahlen", + "shortcuts.action.openOriginal": "Originalartikel offnen", + "shortcuts.action.openSearch": "Suche offnen", + "shortcuts.action.openSettings": "Einstellungen offnen", + "shortcuts.action.previousArticle": "Vorherigen Artikel auswahlen", + "shortcuts.action.showHelp": "Hilfe fur Tastaturkurzel offnen", + "shortcuts.action.toggleRead": "Gelesen/Ungelesen umschalten", + "shortcuts.action.toggleSearch": "Suchdialog umschalten", + "shortcuts.action.toggleStar": "Favorit umschalten", + "shortcuts.section.article": "Artikel", + "shortcuts.section.global": "Global", + "shortcuts.section.navigation": "Navigation", + "shortcuts.title": "Tastaturkurzel", "settings.about.description": "Ein leichter RSS-Feed-Aggregator und Reader.", "settings.about.github": "GitHub", "settings.about.install": "App installieren", @@ -41,6 +59,9 @@ export const deMessages: PartialMessages = { "settings.installCancelled": "Installation abgebrochen", "settings.language.description": "Bevorzugte Sprache auswahlen", "settings.language.label": "Sprache", + "settings.shortcuts.description": "Alle verfugbaren Tastaturkurzel anzeigen", + "settings.shortcuts.label": "Tastaturkurzel", + "settings.shortcuts.open": "Anzeigen", "settings.tab.about": "Info", "settings.tab.appearance": "Darstellung", "settings.theme.dark": "Dunkel", diff --git a/frontend/src/lib/i18n/messages/en.ts b/frontend/src/lib/i18n/messages/en.ts index 873030a..cd05648 100644 --- a/frontend/src/lib/i18n/messages/en.ts +++ b/frontend/src/lib/i18n/messages/en.ts @@ -28,6 +28,24 @@ export const enMessages = { "search.quickActionHint": "Type to search feeds and articles", "search.title": "Search", "search.unknownFeed": "Unknown feed", + "shortcuts.action.closeDialog": "Close open dialog or drawer", + "shortcuts.action.goAll": "Go to all articles", + "shortcuts.action.goFeeds": "Go to manage feeds", + "shortcuts.action.goStarred": "Go to starred articles", + "shortcuts.action.goUnread": "Go to unread articles", + "shortcuts.action.nextArticle": "Select next article", + "shortcuts.action.openOriginal": "Open original article", + "shortcuts.action.openSearch": "Open search", + "shortcuts.action.openSettings": "Open settings", + "shortcuts.action.previousArticle": "Select previous article", + "shortcuts.action.showHelp": "Open keyboard shortcuts help", + "shortcuts.action.toggleRead": "Toggle read/unread", + "shortcuts.action.toggleSearch": "Toggle search dialog", + "shortcuts.action.toggleStar": "Toggle star", + "shortcuts.section.article": "Article", + "shortcuts.section.global": "Global", + "shortcuts.section.navigation": "Navigation", + "shortcuts.title": "Keyboard Shortcuts", "settings.about.description": "A lightweight RSS feed aggregator and reader.", "settings.about.github": "GitHub", "settings.about.install": "Install App", @@ -39,6 +57,9 @@ export const enMessages = { "settings.installCancelled": "Installation cancelled", "settings.language.description": "Select your preferred language", "settings.language.label": "Language", + "settings.shortcuts.description": "View all available keyboard shortcuts", + "settings.shortcuts.label": "Keyboard Shortcuts", + "settings.shortcuts.open": "View", "settings.tab.about": "About", "settings.tab.appearance": "Appearance", "settings.theme.dark": "Dark", diff --git a/frontend/src/lib/i18n/messages/es.ts b/frontend/src/lib/i18n/messages/es.ts index eaa7a77..57ae0ef 100644 --- a/frontend/src/lib/i18n/messages/es.ts +++ b/frontend/src/lib/i18n/messages/es.ts @@ -30,6 +30,24 @@ export const esMessages: PartialMessages = { "search.quickActionHint": "Escribe para buscar feeds y articulos", "search.title": "Buscar", "search.unknownFeed": "Feed desconocido", + "shortcuts.action.closeDialog": "Cerrar dialogo o panel abierto", + "shortcuts.action.goAll": "Ir a todos los articulos", + "shortcuts.action.goFeeds": "Ir a gestionar feeds", + "shortcuts.action.goStarred": "Ir a articulos destacados", + "shortcuts.action.goUnread": "Ir a articulos no leidos", + "shortcuts.action.nextArticle": "Seleccionar articulo siguiente", + "shortcuts.action.openOriginal": "Abrir articulo original", + "shortcuts.action.openSearch": "Abrir busqueda", + "shortcuts.action.openSettings": "Abrir ajustes", + "shortcuts.action.previousArticle": "Seleccionar articulo anterior", + "shortcuts.action.showHelp": "Abrir ayuda de atajos de teclado", + "shortcuts.action.toggleRead": "Alternar leido/no leido", + "shortcuts.action.toggleSearch": "Alternar dialogo de busqueda", + "shortcuts.action.toggleStar": "Alternar destacado", + "shortcuts.section.article": "Articulo", + "shortcuts.section.global": "Global", + "shortcuts.section.navigation": "Navegacion", + "shortcuts.title": "Atajos de teclado", "settings.about.description": "Un agregador y lector RSS ligero.", "settings.about.github": "GitHub", "settings.about.install": "Instalar aplicacion", @@ -41,6 +59,9 @@ export const esMessages: PartialMessages = { "settings.installCancelled": "Instalacion cancelada", "settings.language.description": "Selecciona tu idioma preferido", "settings.language.label": "Idioma", + "settings.shortcuts.description": "Ver todos los atajos de teclado disponibles", + "settings.shortcuts.label": "Atajos de teclado", + "settings.shortcuts.open": "Ver", "settings.tab.about": "Acerca de", "settings.tab.appearance": "Apariencia", "settings.theme.dark": "Oscuro", diff --git a/frontend/src/lib/i18n/messages/fr.ts b/frontend/src/lib/i18n/messages/fr.ts index b3f04fd..9117466 100644 --- a/frontend/src/lib/i18n/messages/fr.ts +++ b/frontend/src/lib/i18n/messages/fr.ts @@ -30,6 +30,24 @@ export const frMessages: PartialMessages = { "search.quickActionHint": "Tapez pour rechercher des flux et des articles", "search.title": "Recherche", "search.unknownFeed": "Flux inconnu", + "shortcuts.action.closeDialog": "Fermer la boite de dialogue ou le panneau ouvert", + "shortcuts.action.goAll": "Aller a tous les articles", + "shortcuts.action.goFeeds": "Aller a la gestion des flux", + "shortcuts.action.goStarred": "Aller aux articles favoris", + "shortcuts.action.goUnread": "Aller aux articles non lus", + "shortcuts.action.nextArticle": "Selectionner l'article suivant", + "shortcuts.action.openOriginal": "Ouvrir l'article original", + "shortcuts.action.openSearch": "Ouvrir la recherche", + "shortcuts.action.openSettings": "Ouvrir les parametres", + "shortcuts.action.previousArticle": "Selectionner l'article precedent", + "shortcuts.action.showHelp": "Ouvrir l'aide des raccourcis clavier", + "shortcuts.action.toggleRead": "Basculer lu/non lu", + "shortcuts.action.toggleSearch": "Basculer la fenetre de recherche", + "shortcuts.action.toggleStar": "Basculer favori", + "shortcuts.section.article": "Article", + "shortcuts.section.global": "Global", + "shortcuts.section.navigation": "Navigation", + "shortcuts.title": "Raccourcis clavier", "settings.about.description": "Un agregateur et lecteur RSS leger.", "settings.about.github": "GitHub", "settings.about.install": "Installer l'application", @@ -41,6 +59,9 @@ export const frMessages: PartialMessages = { "settings.installCancelled": "Installation annulee", "settings.language.description": "Selectionnez votre langue preferee", "settings.language.label": "Langue", + "settings.shortcuts.description": "Voir tous les raccourcis clavier disponibles", + "settings.shortcuts.label": "Raccourcis clavier", + "settings.shortcuts.open": "Voir", "settings.tab.about": "A propos", "settings.tab.appearance": "Apparence", "settings.theme.dark": "Sombre", diff --git a/frontend/src/lib/i18n/messages/pt.ts b/frontend/src/lib/i18n/messages/pt.ts index b1f854a..d32080f 100644 --- a/frontend/src/lib/i18n/messages/pt.ts +++ b/frontend/src/lib/i18n/messages/pt.ts @@ -30,6 +30,24 @@ export const ptMessages: PartialMessages = { "search.quickActionHint": "Digite para pesquisar feeds e artigos", "search.title": "Pesquisar", "search.unknownFeed": "Feed desconhecido", + "shortcuts.action.closeDialog": "Fechar dialogo ou painel aberto", + "shortcuts.action.goAll": "Ir para todos os artigos", + "shortcuts.action.goFeeds": "Ir para gerenciar feeds", + "shortcuts.action.goStarred": "Ir para artigos com estrela", + "shortcuts.action.goUnread": "Ir para artigos nao lidos", + "shortcuts.action.nextArticle": "Selecionar proximo artigo", + "shortcuts.action.openOriginal": "Abrir artigo original", + "shortcuts.action.openSearch": "Abrir pesquisa", + "shortcuts.action.openSettings": "Abrir configuracoes", + "shortcuts.action.previousArticle": "Selecionar artigo anterior", + "shortcuts.action.showHelp": "Abrir ajuda de atalhos de teclado", + "shortcuts.action.toggleRead": "Alternar lido/nao lido", + "shortcuts.action.toggleSearch": "Alternar dialogo de pesquisa", + "shortcuts.action.toggleStar": "Alternar estrela", + "shortcuts.section.article": "Artigo", + "shortcuts.section.global": "Global", + "shortcuts.section.navigation": "Navegacao", + "shortcuts.title": "Atalhos de teclado", "settings.about.description": "Um agregador e leitor RSS leve.", "settings.about.github": "GitHub", "settings.about.install": "Instalar app", @@ -41,6 +59,9 @@ export const ptMessages: PartialMessages = { "settings.installCancelled": "Instalacao cancelada", "settings.language.description": "Selecione seu idioma preferido", "settings.language.label": "Idioma", + "settings.shortcuts.description": "Ver todos os atalhos de teclado disponiveis", + "settings.shortcuts.label": "Atalhos de teclado", + "settings.shortcuts.open": "Ver", "settings.tab.about": "Sobre", "settings.tab.appearance": "Aparencia", "settings.theme.dark": "Escuro", diff --git a/frontend/src/lib/i18n/messages/ru.ts b/frontend/src/lib/i18n/messages/ru.ts index 1774979..5501f14 100644 --- a/frontend/src/lib/i18n/messages/ru.ts +++ b/frontend/src/lib/i18n/messages/ru.ts @@ -30,6 +30,24 @@ export const ruMessages: PartialMessages = { "search.quickActionHint": "Введите запрос для поиска лент и статей", "search.title": "Поиск", "search.unknownFeed": "Неизвестная лента", + "shortcuts.action.closeDialog": "Закрыть открытый диалог или панель", + "shortcuts.action.goAll": "Перейти ко всем статьям", + "shortcuts.action.goFeeds": "Перейти к управлению лентами", + "shortcuts.action.goStarred": "Перейти к избранным статьям", + "shortcuts.action.goUnread": "Перейти к непрочитанным статьям", + "shortcuts.action.nextArticle": "Выбрать следующую статью", + "shortcuts.action.openOriginal": "Открыть оригинальную статью", + "shortcuts.action.openSearch": "Открыть поиск", + "shortcuts.action.openSettings": "Открыть настройки", + "shortcuts.action.previousArticle": "Выбрать предыдущую статью", + "shortcuts.action.showHelp": "Открыть справку по горячим клавишам", + "shortcuts.action.toggleRead": "Переключить прочитано/непрочитано", + "shortcuts.action.toggleSearch": "Переключить окно поиска", + "shortcuts.action.toggleStar": "Переключить избранное", + "shortcuts.section.article": "Статья", + "shortcuts.section.global": "Глобальные", + "shortcuts.section.navigation": "Навигация", + "shortcuts.title": "Горячие клавиши", "settings.about.description": "Легкий RSS-агрегатор и ридер.", "settings.about.github": "GitHub", "settings.about.install": "Установить приложение", @@ -41,6 +59,9 @@ export const ruMessages: PartialMessages = { "settings.installCancelled": "Установка отменена", "settings.language.description": "Выберите предпочтительный язык", "settings.language.label": "Язык", + "settings.shortcuts.description": "Показать все доступные горячие клавиши", + "settings.shortcuts.label": "Горячие клавиши", + "settings.shortcuts.open": "Показать", "settings.tab.about": "О приложении", "settings.tab.appearance": "Внешний вид", "settings.theme.dark": "Темная", diff --git a/frontend/src/lib/i18n/messages/sv.ts b/frontend/src/lib/i18n/messages/sv.ts index 08b0f11..a95f2d2 100644 --- a/frontend/src/lib/i18n/messages/sv.ts +++ b/frontend/src/lib/i18n/messages/sv.ts @@ -30,6 +30,24 @@ export const svMessages: PartialMessages = { "search.quickActionHint": "Skriv for att soka i floden och artiklar", "search.title": "Sok", "search.unknownFeed": "Okant flode", + "shortcuts.action.closeDialog": "Stang oppen dialog eller panel", + "shortcuts.action.goAll": "Ga till alla artiklar", + "shortcuts.action.goFeeds": "Ga till hantera floden", + "shortcuts.action.goStarred": "Ga till stjarnmarkta artiklar", + "shortcuts.action.goUnread": "Ga till olasta artiklar", + "shortcuts.action.nextArticle": "Valj nasta artikel", + "shortcuts.action.openOriginal": "Oppna originalartikel", + "shortcuts.action.openSearch": "Oppna sokning", + "shortcuts.action.openSettings": "Oppna installningar", + "shortcuts.action.previousArticle": "Valj forra artikel", + "shortcuts.action.showHelp": "Oppna hjalp for kortkommandon", + "shortcuts.action.toggleRead": "Vaxla last/olast", + "shortcuts.action.toggleSearch": "Vaxla sokdialog", + "shortcuts.action.toggleStar": "Vaxla stjarnmarkering", + "shortcuts.section.article": "Artikel", + "shortcuts.section.global": "Global", + "shortcuts.section.navigation": "Navigering", + "shortcuts.title": "Kortkommandon", "settings.about.description": "En lattviktig RSS-aggregator och lasare.", "settings.about.github": "GitHub", "settings.about.install": "Installera app", @@ -41,6 +59,9 @@ export const svMessages: PartialMessages = { "settings.installCancelled": "Installationen avbrots", "settings.language.description": "Valj ditt foredragna sprak", "settings.language.label": "Sprak", + "settings.shortcuts.description": "Visa alla tillgangliga kortkommandon", + "settings.shortcuts.label": "Kortkommandon", + "settings.shortcuts.open": "Visa", "settings.tab.about": "Om", "settings.tab.appearance": "Utseende", "settings.theme.dark": "Mork", diff --git a/frontend/src/lib/i18n/messages/zh.ts b/frontend/src/lib/i18n/messages/zh.ts index 5848744..a42b8e5 100644 --- a/frontend/src/lib/i18n/messages/zh.ts +++ b/frontend/src/lib/i18n/messages/zh.ts @@ -30,6 +30,24 @@ export const zhMessages: PartialMessages = { "search.quickActionHint": "输入关键词以搜索订阅源和文章", "search.title": "搜索", "search.unknownFeed": "未知订阅源", + "shortcuts.action.closeDialog": "关闭当前打开的弹窗或抽屉", + "shortcuts.action.goAll": "跳转到全部文章", + "shortcuts.action.goFeeds": "跳转到订阅管理", + "shortcuts.action.goStarred": "跳转到收藏文章", + "shortcuts.action.goUnread": "跳转到未读文章", + "shortcuts.action.nextArticle": "选择下一篇文章", + "shortcuts.action.openOriginal": "打开原文", + "shortcuts.action.openSearch": "打开搜索", + "shortcuts.action.openSettings": "打开设置", + "shortcuts.action.previousArticle": "选择上一篇文章", + "shortcuts.action.showHelp": "打开快捷键帮助", + "shortcuts.action.toggleRead": "切换已读/未读", + "shortcuts.action.toggleSearch": "切换搜索弹窗", + "shortcuts.action.toggleStar": "切换收藏", + "shortcuts.section.article": "文章", + "shortcuts.section.global": "全局", + "shortcuts.section.navigation": "导航", + "shortcuts.title": "键盘快捷键", "settings.about.description": "一个轻量级 RSS 聚合与阅读器。", "settings.about.github": "GitHub", "settings.about.install": "安装应用", @@ -41,6 +59,9 @@ export const zhMessages: PartialMessages = { "settings.installCancelled": "已取消安装", "settings.language.description": "选择你偏好的语言", "settings.language.label": "语言", + "settings.shortcuts.description": "查看所有可用的键盘快捷键", + "settings.shortcuts.label": "键盘快捷键", + "settings.shortcuts.open": "查看", "settings.tab.about": "关于", "settings.tab.appearance": "外观", "settings.theme.dark": "深色", diff --git a/frontend/src/store/ui.ts b/frontend/src/store/ui.ts index 60061bd..37944d0 100644 --- a/frontend/src/store/ui.ts +++ b/frontend/src/store/ui.ts @@ -10,6 +10,7 @@ interface UIState { isEditFeedOpen: boolean; editingFeed: Feed | null; isImportOpmlOpen: boolean; + isShortcutsOpen: boolean; // Mobile sidebar isSidebarOpen: boolean; @@ -21,6 +22,7 @@ interface UIState { setAddFeedOpen: (open: boolean) => void; setEditFeedOpen: (open: boolean, feed?: Feed) => void; setImportOpmlOpen: (open: boolean) => void; + setShortcutsOpen: (open: boolean) => void; setSidebarOpen: (open: boolean) => void; } @@ -32,6 +34,7 @@ export const useUIStore = create((set) => ({ isEditFeedOpen: false, editingFeed: null, isImportOpmlOpen: false, + isShortcutsOpen: false, isSidebarOpen: false, setSearchOpen: (open) => set({ isSearchOpen: open }), @@ -47,5 +50,7 @@ export const useUIStore = create((set) => ({ setImportOpmlOpen: (open) => set({ isImportOpmlOpen: open }), + setShortcutsOpen: (open) => set({ isShortcutsOpen: open }), + setSidebarOpen: (open) => set({ isSidebarOpen: open }), }));