Skip to content

Commit a5eda9d

Browse files
committed
Add book sorting and reader keyboard shortcuts with favorite indicator
1 parent 00a6a4b commit a5eda9d

2 files changed

Lines changed: 57 additions & 9 deletions

File tree

frontend/src/views/ReaderView.jsx

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import { useState, useEffect, useCallback, useRef } from 'react'
22
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
33
import {
44
LuArrowLeft, LuChevronLeft, LuChevronRight, LuDownload,
5-
LuFileText, LuColumns2, LuFile, LuSearch, LuList, LuBookmark, LuBookmarkPlus,
5+
LuFileText, LuColumns2, LuFile, LuSearch, LuList, LuBookmark, LuBookmarkPlus, LuHeart,
66
} from 'react-icons/lu'
77
import api, { mediaUrl } from '../api'
88
import Spinner from '../components/Spinner'
99
import { getBookPrefs, saveBookPrefs, saveRecentBook } from '../hooks/useBookPrefs'
1010
import { getUserPrefs } from '../hooks/useUserPrefs'
11+
import { useFavorites } from '../context/FavoritesContext'
1112
import useReaderGestures from '../hooks/useReaderGestures'
1213
import TocSidebar from '../components/reader/TocSidebar'
1314
import SearchSidebar from '../components/reader/SearchSidebar'
@@ -72,6 +73,8 @@ export default function ReaderView() {
7273
const [zoom, setZoom] = useState(1)
7374
const [pan, setPan] = useState({ x: 0, y: 0 })
7475

76+
const { isFavorite, toggleFavorite } = useFavorites()
77+
7578
const directionRef = useRef(1) // 1 = forward, -1 = backward
7679
const axisRef = useRef('x') // 'x' | 'y'
7780
const currentPageRef = useRef(1) // always tracks latest page without being a dep
@@ -225,19 +228,25 @@ export default function ReaderView() {
225228
if (mode === 'spread') goToPage(currentPage, 'spread')
226229
}, [mode]) // eslint-disable-line react-hooks/exhaustive-deps
227230

228-
// Keyboard navigation
231+
// Keyboard navigation + shortcuts
229232
const step = mode === 'spread' ? (currentPage === 1 ? 1 : 2) : 1
230233
useEffect(() => {
231234
const handleKeyDown = (e) => {
232-
if (mode === 'pdf') return
233-
if (e.key === 'ArrowLeft') { e.preventDefault(); goToPage(currentPage - step, undefined, 'x') }
234-
if (e.key === 'ArrowRight') { e.preventDefault(); goToPage(currentPage + step, undefined, 'x') }
235-
if (e.key === 'ArrowUp') { e.preventDefault(); goToPage(currentPage - step, undefined, 'y') }
236-
if (e.key === 'ArrowDown') { e.preventDefault(); goToPage(currentPage + step, undefined, 'y') }
235+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
236+
if (mode !== 'pdf') {
237+
if (e.key === 'ArrowLeft') { e.preventDefault(); goToPage(currentPage - step, undefined, 'x') }
238+
if (e.key === 'ArrowRight') { e.preventDefault(); goToPage(currentPage + step, undefined, 'x') }
239+
if (e.key === 'ArrowUp') { e.preventDefault(); goToPage(currentPage - step, undefined, 'y') }
240+
if (e.key === 'ArrowDown') { e.preventDefault(); goToPage(currentPage + step, undefined, 'y') }
241+
}
242+
if (e.key === 'f') toggleFavorite('book', bookId)
243+
if (e.key === 't') togglePanel('toc')
244+
if (e.key === 'b') togglePanel('bookmarks')
245+
if (e.key === 's') togglePanel('search')
237246
}
238247
window.addEventListener('keydown', handleKeyDown)
239248
return () => window.removeEventListener('keydown', handleKeyDown)
240-
}, [mode, currentPage, step, goToPage])
249+
}, [mode, currentPage, step, goToPage, bookId, toggleFavorite])
241250

242251
const wheelNav = getUserPrefs().wheelNav !== false
243252

@@ -358,6 +367,14 @@ export default function ReaderView() {
358367

359368
<AddToCampaignButton resourceType="book" resourceId={bookId} />
360369

370+
<button
371+
onClick={() => toggleFavorite('book', bookId)}
372+
title={isFavorite('book', bookId) ? 'Remove from favorites' : 'Add to favorites'}
373+
style={{ ...btnStyle, color: isFavorite('book', bookId) ? 'var(--gold)' : 'var(--text-muted)' }}
374+
>
375+
<LuHeart size={14} fill={isFavorite('book', bookId) ? 'var(--gold)' : 'none'} />
376+
</button>
377+
361378
{/* Add bookmark action — separate from the panel toggle */}
362379
{mode !== 'pdf' && (
363380
<button

frontend/src/views/SystemDetailView.jsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export default function SystemDetailView() {
2828
const [collapsedCats, setCollapsedCats] = useState(new Set())
2929
const [selectedTags, setSelectedTags] = useState(new Set())
3030
const [showAllTags, setShowAllTags] = useState(false)
31+
const [bookSort, setBookSort] = useState('title')
3132
const [searchQuery, setSearchQuery] = useState('')
3233
const [searchResults, setSearchResults] = useState(null)
3334
const [searching, setSearching] = useState(false)
@@ -66,6 +67,15 @@ export default function SystemDetailView() {
6667
return next
6768
})
6869

70+
const sortBooks = (books) => {
71+
const sorted = [...books]
72+
if (bookSort === 'title') sorted.sort((a, b) => a.title.localeCompare(b.title))
73+
if (bookSort === 'year') sorted.sort((a, b) => (b.year || 0) - (a.year || 0))
74+
if (bookSort === 'size') sorted.sort((a, b) => (b.file_size || 0) - (a.file_size || 0))
75+
if (bookSort === 'pages') sorted.sort((a, b) => (b.page_count || 0) - (a.page_count || 0))
76+
return sorted
77+
}
78+
6979
const categories = {}
7080
;(system.books || [])
7181
.filter(book => selectedTags.size === 0 || [...selectedTags].every(t => (book.tags || []).includes(t)))
@@ -221,6 +231,27 @@ export default function SystemDetailView() {
221231
</div>
222232
)}
223233

234+
{/* Sort bar */}
235+
{!searchResults && (
236+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
237+
<span style={{ fontSize: 13, color: 'var(--text-muted)', flexShrink: 0 }}>Sort:</span>
238+
{[['title', 'A–Z'], ['year', 'Year'], ['pages', 'Pages'], ['size', 'Size']].map(([val, label]) => (
239+
<button
240+
key={val}
241+
onClick={() => setBookSort(val)}
242+
style={{
243+
fontSize: 12, padding: '3px 10px', borderRadius: 6, cursor: 'pointer',
244+
background: bookSort === val ? 'var(--bg-card-hover)' : 'var(--bg-card)',
245+
border: '1px solid var(--border)',
246+
color: bookSort === val ? 'var(--gold)' : 'var(--text-dim)',
247+
}}
248+
>
249+
{label}
250+
</button>
251+
))}
252+
</div>
253+
)}
254+
224255
{/* Search results */}
225256
{searchResults && (
226257
<div style={{ marginBottom: 32 }}>
@@ -252,7 +283,7 @@ export default function SystemDetailView() {
252283
{!searchResults && [...CATEGORY_ORDER, ...Object.keys(categories).filter(c => !CATEGORY_ORDER.includes(c))]
253284
.filter(cat => categories[cat])
254285
.map(cat => {
255-
const books = categories[cat]
286+
const books = sortBooks(categories[cat])
256287
const CatIcon = CATEGORY_ICONS[cat] || LuFileText
257288
const isCollapsed = collapsedCats.has(cat)
258289
const toggle = () => setCollapsedCats(prev => {

0 commit comments

Comments
 (0)