1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-21 18:55:36 +02:00

Compare commits

..

No commits in common. "b0d02034ea7cffffad0027bf3f5417414cc2ac36" and "72cf7ff642a68bb6197e1b9689c4f619219872b1" have entirely different histories.

9 changed files with 110 additions and 190 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -4,8 +4,8 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Archivr</title> <title>Archivr</title>
<script type="module" crossorigin src="/assets/index-u6DliWTb.js"></script> <script type="module" crossorigin src="/assets/index-Gm5awz10.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-p2mzAHzf.css"> <link rel="stylesheet" crossorigin href="/assets/index-C89vW3ol.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

6
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1783776592, "lastModified": 1782723713,
"narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=", "narHash": "sha256-oPXCU/SSUokcGaJREHibG1CBX3+s/W7orDWQOZDsEeQ=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3", "rev": "b5aa0fbd538984f6e3d201be0005b4463d8b09f8",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -27,17 +27,13 @@ const PREVIEW_ROUTE = (() => {
})() })()
const VIEWS = ['archive','tags','collections','runs','admin','settings'] const VIEWS = ['archive','tags','collections','runs','admin','settings']
const SETTINGS_TABS = ['profile','tokens','instance','cookies','extensions','storage'] const SETTINGS_TABS = ['profile','tokens','instance','storage']
function parseLocation() { function parseLocation() {
const parts = window.location.pathname.split('/').filter(Boolean) const parts = window.location.pathname.split('/').filter(Boolean)
const view = VIEWS.includes(parts[0]) ? parts[0] : 'archive' const view = VIEWS.includes(parts[0]) ? parts[0] : 'archive'
const settingsTab = (view === 'settings' && SETTINGS_TABS.includes(parts[1])) ? parts[1] : 'profile' const settingsTab = (view === 'settings' && SETTINGS_TABS.includes(parts[1])) ? parts[1] : 'profile'
const params = new URLSearchParams(window.location.search) return { view, settingsTab }
const q = params.get('q') ?? ''
const tag = view === 'archive' ? (params.get('tag') ?? null) : null
const entry = view === 'archive' ? (params.get('entry') ?? null) : null
return { view, settingsTab, q, tag, entry }
} }
function locationPath(view, settingsTab) { function locationPath(view, settingsTab) {
@ -70,13 +66,9 @@ export default function App() {
// Sync URL state on back/forward // Sync URL state on back/forward
useEffect(() => { useEffect(() => {
const handler = () => { const handler = () => {
const { view, settingsTab, q, tag, entry } = parseLocation() const { view, settingsTab } = parseLocation()
setView(view) setView(view)
setSettingsTab(settingsTab) setSettingsTab(settingsTab)
setSearchQuery(q)
setTagFilter(tag)
setSelectedEntryUid(entry)
setSelectedEntry(null)
} }
window.addEventListener('popstate', handler) window.addEventListener('popstate', handler)
return () => window.removeEventListener('popstate', handler) return () => window.removeEventListener('popstate', handler)
@ -85,12 +77,12 @@ export default function App() {
const [archives, setArchives] = useState([]) const [archives, setArchives] = useState([])
const [archiveId, setArchiveId] = useState(null) const [archiveId, setArchiveId] = useState(null)
const [entries, setEntries] = useState([]) const [entries, setEntries] = useState([])
const [selectedEntryUid, setSelectedEntryUid] = useState(() => parseLocation().entry) const [selectedEntryUid, setSelectedEntryUid] = useState(null)
const [selectedEntry, setSelectedEntry] = useState(null) const [selectedEntry, setSelectedEntry] = useState(null)
const [tagFilter, setTagFilter] = useState(() => parseLocation().tag) const [tagFilter, setTagFilter] = useState(null)
const [view, setView] = useState(() => parseLocation().view) const [view, setView] = useState(() => parseLocation().view)
const [settingsTab, setSettingsTab] = useState(() => parseLocation().settingsTab) const [settingsTab, setSettingsTab] = useState(() => parseLocation().settingsTab)
const [searchQuery, setSearchQuery] = useState(() => parseLocation().q) const [searchQuery, setSearchQuery] = useState('')
const [resultCount, setResultCount] = useState('') const [resultCount, setResultCount] = useState('')
const [searchBusy, setSearchBusy] = useState(false) const [searchBusy, setSearchBusy] = useState(false)
const [runs, setRuns] = useState([]) const [runs, setRuns] = useState([])
@ -110,9 +102,6 @@ export default function App() {
// Entry detail (shared between PreviewPanel and ContextRail) // Entry detail (shared between PreviewPanel and ContextRail)
const [entryDetail, setEntryDetail] = useState(null) const [entryDetail, setEntryDetail] = useState(null)
const detailSeqRef = useRef(0) const detailSeqRef = useRef(0)
const searchInputRef = useRef(null)
const pendingSearchFocus = useRef(false)
const firstArchiveLoad = useRef(true)
const humanizeTags = currentUser?.humanize_slugs ?? false; const humanizeTags = currentUser?.humanize_slugs ?? false;
@ -168,16 +157,6 @@ export default function App() {
// Archive change: parallel load entries + runs + tags // Archive change: parallel load entries + runs + tags
useEffect(() => { useEffect(() => {
if (!archiveId) return if (!archiveId) return
if (firstArchiveLoad.current) {
// First load: URL-initialized filters are already in state; the debounced
// search and tagFilter effects will call loadEntries with the right values.
firstArchiveLoad.current = false
Promise.all([
fetchRuns(archiveId).then(setRuns),
fetchTags(archiveId).then(setTagNodes),
])
return
}
setTagFilter(null) setTagFilter(null)
setSelectedEntry(null) setSelectedEntry(null)
setSelectedEntryUid(null) setSelectedEntryUid(null)
@ -217,13 +196,13 @@ export default function App() {
} }
}, [archiveId]) }, [archiveId])
// Sync view + settingsTab URL
// Sync view + settingsTab URL (skip when serving a standalone preview page) // Sync view + settingsTab URL (skip when serving a standalone preview page)
// Preserve existing search params so ?q/tag/entry survive view navigation.
useEffect(() => { useEffect(() => {
if (PREVIEW_ROUTE) return if (PREVIEW_ROUTE) return
const path = locationPath(view, settingsTab) const path = locationPath(view, settingsTab)
if (window.location.pathname !== path) { if (window.location.pathname !== path) {
history.pushState(null, '', path + window.location.search) history.pushState(null, '', path)
} }
}, [view, settingsTab]) }, [view, settingsTab])
@ -287,56 +266,6 @@ export default function App() {
setSelectedEntryUid(prev => prev === entryUid ? null : prev) setSelectedEntryUid(prev => prev === entryUid ? null : prev)
}, []) }, [])
// Restore selectedEntry object from selectedEntryUid when entries load.
// Handles page refresh and back/forward navigation where only the UID is known.
useEffect(() => {
if (!selectedEntryUid || selectedEntry) return
const found = entries.find(e => e.entry_uid === selectedEntryUid)
if (found) setSelectedEntry(found)
}, [entries, selectedEntryUid, selectedEntry])
// Sync search params URL via replaceState (no new history entry).
useEffect(() => {
if (PREVIEW_ROUTE) return
const params = new URLSearchParams()
if (searchQuery) params.set('q', searchQuery)
if (view === 'archive' && tagFilter) params.set('tag', tagFilter)
if (view === 'archive' && selectedEntryUid) params.set('entry', selectedEntryUid)
const qs = params.toString()
const url = window.location.pathname + (qs ? '?' + qs : '')
const current = window.location.pathname + window.location.search
if (current !== url) history.replaceState(null, '', url)
}, [searchQuery, tagFilter, selectedEntryUid, view])
// K / Ctrl+K: focus the search input, switching to archive view first if needed.
useEffect(() => {
const handler = (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault()
if (view === 'archive') {
searchInputRef.current?.focus()
searchInputRef.current?.select()
} else {
pendingSearchFocus.current = true
setView('archive')
}
}
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [view])
// After switching to archive view via K, focus the search input once rendered.
useEffect(() => {
if (view === 'archive' && pendingSearchFocus.current) {
pendingSearchFocus.current = false
requestAnimationFrame(() => {
searchInputRef.current?.focus()
searchInputRef.current?.select()
})
}
}, [view])
const handleCaptureClick = useCallback(() => { const handleCaptureClick = useCallback(() => {
setCaptureDialogOpen(true) setCaptureDialogOpen(true)
}, []) }, [])
@ -420,7 +349,6 @@ export default function App() {
</svg> </svg>
</span> </span>
<input <input
ref={searchInputRef}
className="search-input" className="search-input"
type="search" type="search"
aria-label="Search archive" aria-label="Search archive"

View file

@ -691,7 +691,7 @@ function ExtensionsTab() {
const cookieExtEnabled = settings?.cookie_ext_enabled ?? true const cookieExtEnabled = settings?.cookie_ext_enabled ?? true
return ( return (
<div> <div style={{ maxWidth: 560 }}>
<div className="form-section"> <div className="form-section">
<h2>Extensions</h2> <h2>Extensions</h2>
<p className="form-hint" style={{ marginBottom: 20 }}> <p className="form-hint" style={{ marginBottom: 20 }}>
@ -699,62 +699,60 @@ function ExtensionsTab() {
accept cookie banners, and more. Changes take effect on the next capture. accept cookie banners, and more. Changes take effect on the next capture.
</p> </p>
<div className="ext-grid"> <div className="ext-card">
<div className="ext-card"> <div className="ext-card-header">
<div className="ext-card-header"> <div className="ext-card-info">
<div className="ext-card-info"> <span className="ext-card-name">uBlock Origin Lite</span>
<span className="ext-card-name">uBlock Origin Lite</span> <span className="ext-card-desc">
<span className="ext-card-desc"> Blocks ads, trackers, and other page clutter during archiving
Blocks ads, trackers, and other page clutter during archiving via Chrome&rsquo;s declarativeNetRequest API (Manifest V3).
via Chrome&rsquo;s declarativeNetRequest API (Manifest V3). </span>
{!extAvailable && (
<span className="ext-card-hint">
Not configured &mdash; set <code>ARCHIVR_UBLOCK_EXT</code> to the
unpacked extension directory to enable.
</span> </span>
{!extAvailable && ( )}
<span className="ext-card-hint">
Not configured &mdash; set <code>ARCHIVR_UBLOCK_EXT</code> to the
unpacked extension directory to enable.
</span>
)}
</div>
<button
type="button"
role="switch"
aria-checked={extEnabled}
className={`ext-toggle${extEnabled ? ' ext-toggle--on' : ''}`}
onClick={() => toggleUblock(!extEnabled)}
disabled={saving}
aria-label="Toggle uBlock Origin Lite"
>
<span className="ext-toggle-knob" />
</button>
</div> </div>
<button
type="button"
role="switch"
aria-checked={extEnabled}
className={`ext-toggle${extEnabled ? ' ext-toggle--on' : ''}`}
onClick={() => toggleUblock(!extEnabled)}
disabled={saving}
aria-label="Toggle uBlock Origin Lite"
>
<span className="ext-toggle-knob" />
</button>
</div> </div>
</div>
<div className="ext-card"> <div className="ext-card">
<div className="ext-card-header"> <div className="ext-card-header">
<div className="ext-card-info"> <div className="ext-card-info">
<span className="ext-card-name">I Still Don&rsquo;t Care About Cookies</span> <span className="ext-card-name">I Still Don&rsquo;t Care About Cookies</span>
<span className="ext-card-desc"> <span className="ext-card-desc">
Dismiss cookie consent banners during archiving. Dismiss cookie consent banners during archiving.
</span>
{!cookieExtAvailable && (
<span className="ext-card-hint">
Not configured &mdash; set <code>ARCHIVR_COOKIE_EXT</code> to the
unpacked extension directory to enable.
</span> </span>
{!cookieExtAvailable && ( )}
<span className="ext-card-hint">
Not configured &mdash; set <code>ARCHIVR_COOKIE_EXT</code> to the
unpacked extension directory to enable.
</span>
)}
</div>
<button
type="button"
role="switch"
aria-checked={cookieExtEnabled}
className={`ext-toggle${cookieExtEnabled ? ' ext-toggle--on' : ''}`}
onClick={() => toggleCookieExt(!cookieExtEnabled)}
disabled={saving}
aria-label="Toggle I Still Don't Care About Cookies"
>
<span className="ext-toggle-knob" />
</button>
</div> </div>
<button
type="button"
role="switch"
aria-checked={cookieExtEnabled}
className={`ext-toggle${cookieExtEnabled ? ' ext-toggle--on' : ''}`}
onClick={() => toggleCookieExt(!cookieExtEnabled)}
disabled={saving}
aria-label="Toggle I Still Don't Care About Cookies"
>
<span className="ext-toggle-knob" />
</button>
</div> </div>
</div> </div>

View file

@ -1014,12 +1014,6 @@ select {
.ext-toggle--sm.ext-toggle--on .ext-toggle-knob { transform: translateX(16px); } .ext-toggle--sm.ext-toggle--on .ext-toggle-knob { transform: translateX(16px); }
/* ── Extension card (Settings / Extensions tab) ──────────────────────────── */ /* ── Extension card (Settings / Extensions tab) ──────────────────────────── */
.ext-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 12px;
}
.ext-card { .ext-card {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: var(--r2); border-radius: var(--r2);