1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-22 03:05:32 +02:00

feat: Reader mode via Mozilla Readability.js

Adds an opt-in 'Reader mode' advanced option to the capture dialog.
When enabled, Readability.js is injected as a browser script during
SingleFile capture; it fires on single-file-on-before-capture-start,
replaces the page body with the distilled article content, injects a
clean typographic stylesheet, and adds a header with title/byline/site.
Falls back silently if Readability fails (e.g. non-article pages).

- vendor/readability/Readability.js  Apache 2.0, Mozilla, v0.6.0
- singlefile.rs: embed READABILITY_JS + READER_MODE_WRAPPER_JS via
  include_str!; write both to temp dir when reader_mode is true;
  base_single_file_cmd now accepts &[&Path] for multiple --browser-script
- capture.rs: CaptureConfig.reader_mode: bool
- routes.rs: CaptureBody.reader_mode: Option<bool> (defaults false)
- api.js: submitCapture passes reader_mode in payload
- CaptureDialog.jsx: Reader mode toggle in Advanced options (off by default)
This commit is contained in:
TheGeneralist 2026-07-07 20:04:03 +02:00
parent 28c62ddc5a
commit 596a6d1c89
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4
9 changed files with 2967 additions and 74 deletions

View file

@ -98,9 +98,10 @@ export async function fetchTags(archiveId) {
export async function submitCapture(archiveId, locator, quality = null, extensions = null) {
const payload = { locator }
if (quality && quality !== 'best') payload.quality = quality
// extensions: { ublock_enabled?: bool } — per-capture overrides
if (extensions && typeof extensions.ublock_enabled === 'boolean') {
payload.ublock_enabled = extensions.ublock_enabled
// extensions: { ublock_enabled?: bool, reader_mode?: bool } — per-capture overrides
if (extensions) {
if (typeof extensions.ublock_enabled === 'boolean') payload.ublock_enabled = extensions.ublock_enabled
if (typeof extensions.reader_mode === 'boolean') payload.reader_mode = extensions.reader_mode
}
const res = await fetch(`/api/archives/${archiveId}/captures`, {
method: "POST",

View file

@ -128,6 +128,8 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on
// Effective uBlock for this session
const ublockEnabled = ublockOverride !== null ? ublockOverride : (globalUblock ?? true)
// Reader mode: off by default, per-session only
const [readerMode, setReaderMode] = useState(false)
// On mount: clean up old single-locator sessionStorage keys; reconnect running jobs
useEffect(() => {
@ -240,7 +242,7 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on
const qual = item.quality || 'best'
setItems(prev => prev.map(it => it.id === item.id ? { ...it, status: 'submitting', error: null } : it))
try {
const extensions = { ublock_enabled: ublockEnabled }
const extensions = { ublock_enabled: ublockEnabled, reader_mode: readerMode }
const job = await submitCapture(aid, loc, qual, extensions)
setItems(prev => prev.map(it =>
it.id === item.id ? { ...it, status: 'running', jobUid: job.job_uid, archiveId: aid } : it
@ -395,6 +397,22 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on
<span className="ext-toggle-knob" />
</button>
</label>
<label className="capture-ext-row" style={{ marginTop: 8 }}>
<span className="capture-ext-label">
<span className="capture-ext-name">Reader mode</span>
<span className="capture-ext-desc">Distil to article text via Readability (off by default)</span>
</span>
<button
type="button"
role="switch"
aria-checked={readerMode}
className={`ext-toggle ext-toggle--sm${readerMode ? ' ext-toggle--on' : ''}`}
onClick={() => setReaderMode(v => !v)}
aria-label="Toggle reader mode for this capture"
>
<span className="ext-toggle-knob" />
</button>
</label>
</div>
)}
</div>