Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 47 additions & 13 deletions frontend/src/components/maps/InlineTagEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@ import { useState, useRef, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { LuX } from 'react-icons/lu'

export default function InlineTagEditor({ tags, onSave, onCancel }) {
export default function InlineTagEditor({ tags, onSave, onCancel, suggestions = [] }) {
const { t } = useTranslation()
const [draft, setDraft] = useState([...tags])
const [input, setInput] = useState('')
const [activeIdx, setActiveIdx] = useState(-1)
const inputRef = useRef(null)

useEffect(() => { inputRef.current?.focus() }, [])

const commit = () => {
const tag = input.trim().toLowerCase().replace(/,+$/, '')
const filtered = input.trim().length > 0
? suggestions.filter(s => s.toLowerCase().includes(input.trim().toLowerCase()) && !draft.includes(s))
: []

const commit = (value) => {
const tag = (value ?? input).trim().toLowerCase().replace(/,+$/, '')
setInput('')
setActiveIdx(-1)
if (tag && !draft.includes(tag)) {
const next = [...draft, tag]
setDraft(next)
Expand All @@ -27,8 +33,13 @@ export default function InlineTagEditor({ tags, onSave, onCancel }) {
}

const handleKey = (e) => {
if (filtered.length > 0) {
if (e.key === 'ArrowDown') { e.preventDefault(); setActiveIdx(i => Math.min(i + 1, filtered.length - 1)); return }
if (e.key === 'ArrowUp') { e.preventDefault(); setActiveIdx(i => Math.max(i - 1, -1)); return }
if (e.key === 'Enter' && activeIdx >= 0) { e.preventDefault(); commit(filtered[activeIdx]); return }
}
if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); commit() }
else if (e.key === 'Escape') onCancel()
else if (e.key === 'Escape') { if (filtered.length > 0) { setInput(''); setActiveIdx(-1) } else onCancel() }
else if (e.key === 'Backspace' && !input && draft.length > 0) {
const next = draft.slice(0, -1)
setDraft(next)
Expand All @@ -37,7 +48,7 @@ export default function InlineTagEditor({ tags, onSave, onCancel }) {
}

return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, alignItems: 'center' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, alignItems: 'center', position: 'relative' }}>
{draft.map(tag => (
<span key={tag} style={editTagStyle}>
{tag}
Expand All @@ -50,14 +61,37 @@ export default function InlineTagEditor({ tags, onSave, onCancel }) {
</button>
</span>
))}
<input
ref={inputRef}
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={handleKey}
placeholder={t('inlineTagEditor.placeholder')}
style={{ fontSize: 13, padding: '2px 8px', borderRadius: 10, width: 100, background: 'var(--bg-input)', border: '1px solid var(--border)', color: 'var(--text)' }}
/>
<div style={{ position: 'relative' }}>
<input
ref={inputRef}
value={input}
onChange={e => { setInput(e.target.value); setActiveIdx(-1) }}
onKeyDown={handleKey}
placeholder={t('inlineTagEditor.placeholder')}
style={{ fontSize: 13, padding: '2px 8px', borderRadius: 10, width: 120, background: 'var(--bg-input)', border: '1px solid var(--border)', color: 'var(--text)' }}
/>
{filtered.length > 0 && (
<div style={{
position: 'absolute', top: '100%', left: 0, marginTop: 2, zIndex: 100,
background: 'var(--bg-panel)', border: '1px solid var(--border)', borderRadius: 6,
boxShadow: '0 4px 12px rgba(0,0,0,0.3)', minWidth: 140, maxHeight: 160, overflowY: 'auto',
}}>
{filtered.map((s, i) => (
<div
key={s}
onMouseDown={e => { e.preventDefault(); commit(s) }}
style={{
padding: '6px 12px', fontSize: 13, cursor: 'pointer',
background: i === activeIdx ? 'var(--bg-card-hover)' : 'transparent',
color: 'var(--text)',
}}
>
{s}
</div>
))}
</div>
)}
</div>
<button onClick={() => { commit(); onCancel() }} style={cancelBtnStyle}>{t('common.done')}</button>
</div>
)
Expand Down
17 changes: 15 additions & 2 deletions frontend/src/components/system/BookEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { useTranslation } from 'react-i18next'
import { LuX } from 'react-icons/lu'
import api from '../../api'
import InlineTagEditor from '../maps/InlineTagEditor'
import { saveBookPrefs, getBookPrefs } from '../../hooks/useBookPrefs'

export default function BookEditor({ book, onSave, onClose }) {
export default function BookEditor({ book, onSave, onClose, allTags = [] }) {
const { t } = useTranslation()
const [form, setForm] = useState({
title: book.title || '',
Expand All @@ -18,6 +19,8 @@ export default function BookEditor({ book, onSave, onClose }) {
const [tags, setTags] = useState(book.tags || [])
const [editingTags, setEditingTags] = useState(false)
const [saving, setSaving] = useState(false)
const [progressReset, setProgressReset] = useState(false)
const hasProgress = !!getBookPrefs(book.id).page

const field = (label, key, opts = {}) => (
<div style={{ marginBottom: 10 }}>
Expand Down Expand Up @@ -83,6 +86,7 @@ export default function BookEditor({ book, onSave, onClose }) {
tags={tags}
onSave={setTags}
onCancel={() => setEditingTags(false)}
suggestions={allTags.filter(t => !tags.includes(t))}
/>
) : (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, alignItems: 'center' }}>
Expand All @@ -98,13 +102,22 @@ export default function BookEditor({ book, onSave, onClose }) {
)}
</div>

<div style={{ display: 'flex', gap: 8 }}>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
<button onClick={onClose} style={{ padding: '6px 14px', borderRadius: 5, background: 'var(--bg-card)', border: '1px solid var(--border)', color: 'var(--text-dim)', fontSize: 13, cursor: 'pointer' }}>
{t('bookEditor.cancel')}
</button>
<button onClick={handleSave} disabled={saving} style={{ padding: '6px 14px', borderRadius: 5, background: 'var(--gold-dim)', border: 'none', color: 'var(--bg-deep)', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>
{saving ? t('bookEditor.saving') : t('bookEditor.save')}
</button>
{(hasProgress || progressReset) && (
<button
onClick={() => { saveBookPrefs(book.id, { page: null }); setProgressReset(true) }}
disabled={progressReset}
style={{ marginLeft: 'auto', padding: '6px 14px', borderRadius: 5, background: 'none', border: '1px solid var(--border)', color: progressReset ? 'var(--green)' : 'var(--text-muted)', fontSize: 12, cursor: progressReset ? 'default' : 'pointer' }}
>
{progressReset ? `✓ ${t('bookEditor.progressReset')}` : t('bookEditor.resetProgress')}
</button>
)}
</div>
</div>
)
Expand Down
15 changes: 13 additions & 2 deletions frontend/src/locales/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,16 @@
"addToFavorites": "Zu Favoriten hinzufügen",
"removeFromFavorites": "Aus Favoriten entfernen",
"bookmarkPage": "Seite als Lesezeichen speichern",
"downloadFile": "Herunterladen"
"downloadFile": "Herunterladen",
"keyboardShortcuts": "Tastaturkürzel",
"shortcutPrevNext": "Vorherige / nächste Seite",
"shortcutPrevNextVertical": "Vorherige / nächste Seite (vertikal)",
"shortcutFavorite": "Favorit umschalten",
"shortcutToc": "Inhaltsverzeichnis",
"shortcutBookmarks": "Lesezeichen",
"shortcutSearch": "Suche",
"shortcutHelp": "Diese Hilfe",
"shortcutClose": "Panels schließen"
},
"search": {
"title": "Bibliothek durchsuchen",
Expand Down Expand Up @@ -771,7 +780,9 @@
"save": "Speichern",
"saving": "Speichern…",
"cancel": "Abbrechen",
"failed": "Speichern fehlgeschlagen."
"failed": "Speichern fehlgeschlagen.",
"resetProgress": "Lesefortschritt zurücksetzen",
"progressReset": "Fortschritt zurückgesetzt"
},
"bookRow": {
"missingFile": "Fehlend",
Expand Down
15 changes: 13 additions & 2 deletions frontend/src/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,16 @@
"addToFavorites": "Add to favorites",
"removeFromFavorites": "Remove from favorites",
"bookmarkPage": "Bookmark this page",
"downloadFile": "Download"
"downloadFile": "Download",
"keyboardShortcuts": "Keyboard Shortcuts",
"shortcutPrevNext": "Previous / next page",
"shortcutPrevNextVertical": "Previous / next page (vertical)",
"shortcutFavorite": "Toggle favorite",
"shortcutToc": "Table of contents",
"shortcutBookmarks": "Bookmarks",
"shortcutSearch": "Search",
"shortcutHelp": "This help",
"shortcutClose": "Close panels"
},
"search": {
"title": "Search Your Library",
Expand Down Expand Up @@ -771,7 +780,9 @@
"save": "Save",
"saving": "Saving…",
"cancel": "Cancel",
"failed": "Failed to save."
"failed": "Failed to save.",
"resetProgress": "Reset reading progress",
"progressReset": "Progress reset"
},
"bookRow": {
"missingFile": "Missing",
Expand Down
39 changes: 38 additions & 1 deletion frontend/src/views/ReaderView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import {
LuArrowLeft, LuChevronLeft, LuChevronRight, LuDownload,
LuFileText, LuColumns2, LuFile, LuSearch, LuList, LuBookmark, LuBookmarkPlus, LuHeart,
LuFileText, LuColumns2, LuFile, LuSearch, LuList, LuBookmark, LuBookmarkPlus, LuHeart, LuKeyboard,
} from 'react-icons/lu'
import api, { mediaUrl } from '../api'
import Spinner from '../components/Spinner'
Expand Down Expand Up @@ -72,6 +72,7 @@ export default function ReaderView() {
const [pendingBookmark, setPendingBookmark] = useState(null)
const [pendingLabel, setPendingLabel] = useState('')
const [pendingNotes, setPendingNotes] = useState('')
const [showShortcuts, setShowShortcuts] = useState(false)
const [zoom, setZoom] = useState(1)
const [pan, setPan] = useState({ x: 0, y: 0 })

Expand Down Expand Up @@ -239,6 +240,8 @@ export default function ReaderView() {
if (e.key === 't') togglePanel('toc')
if (e.key === 'b') togglePanel('bookmarks')
if (e.key === 's') togglePanel('search')
if (e.key === '?') setShowShortcuts(v => !v)
if (e.key === 'Escape') setShowShortcuts(false)
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
Expand Down Expand Up @@ -386,8 +389,42 @@ export default function ReaderView() {
}}>
<LuDownload size={13} />
</a>

<button
onClick={() => setShowShortcuts(v => !v)}
title="Keyboard shortcuts (?)"
style={{ ...btnStyle, color: showShortcuts ? 'var(--gold)' : 'var(--text-muted)' }}
>
<LuKeyboard size={14} />
</button>
</div>

{showShortcuts && (
<div
onClick={() => setShowShortcuts(false)}
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', zIndex: 200, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
<div onClick={e => e.stopPropagation()} style={{ background: 'var(--bg-panel)', border: '1px solid var(--border)', borderRadius: 10, padding: 24, minWidth: 280, boxShadow: '0 8px 32px rgba(0,0,0,0.4)' }}>
<div style={{ fontSize: 15, fontWeight: 600, marginBottom: 16, color: 'var(--text)' }}>{t('reader.keyboardShortcuts')}</div>
{[
['← / →', t('reader.shortcutPrevNext')],
['↑ / ↓', t('reader.shortcutPrevNextVertical')],
['f', t('reader.shortcutFavorite')],
['t', t('reader.shortcutToc')],
['b', t('reader.shortcutBookmarks')],
['s', t('reader.shortcutSearch')],
['?', t('reader.shortcutHelp')],
['Esc', t('reader.shortcutClose')],
].map(([key, desc]) => (
<div key={key} style={{ display: 'flex', justifyContent: 'space-between', gap: 24, padding: '5px 0', borderBottom: '1px solid var(--border)', fontSize: 13 }}>
<kbd style={{ fontFamily: 'monospace', background: 'var(--bg-card)', border: '1px solid var(--border)', borderRadius: 4, padding: '1px 7px', color: 'var(--gold)', whiteSpace: 'nowrap' }}>{key}</kbd>
<span style={{ color: 'var(--text-dim)' }}>{desc}</span>
</div>
))}
</div>
</div>
)}

{/* Content + optional sidebar */}
<div style={{ flex: 1, minHeight: 0, overflow: 'hidden', display: 'flex' }}>
<div
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/views/SystemDetailView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ export default function SystemDetailView() {
{editingBookId === book.id && (
<BookEditor
book={book}
allTags={allTags}
onSave={(updated) => { saveBook(book.id, updated); setEditingBookId(null) }}
onClose={() => setEditingBookId(null)}
/>
Expand Down Expand Up @@ -410,6 +411,7 @@ export default function SystemDetailView() {
{editingBookId === book.id && (
<BookEditor
book={book}
allTags={allTags}
onSave={(updated) => { saveBook(book.id, updated); setEditingBookId(null) }}
onClose={() => setEditingBookId(null)}
/>
Expand Down
Loading