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.
b0d02034ea
...
72cf7ff642
9 changed files with 110 additions and 190 deletions
1
crates/archivr-server/static/assets/index-C89vW3ol.css
Normal file
1
crates/archivr-server/static/assets/index-C89vW3ol.css
Normal file
File diff suppressed because one or more lines are too long
47
crates/archivr-server/static/assets/index-Gm5awz10.js
Normal file
47
crates/archivr-server/static/assets/index-Gm5awz10.js
Normal file
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
|
|
@ -4,8 +4,8 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Archivr</title>
|
||||
<script type="module" crossorigin src="/assets/index-u6DliWTb.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-p2mzAHzf.css">
|
||||
<script type="module" crossorigin src="/assets/index-Gm5awz10.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-C89vW3ol.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
6
flake.lock
generated
6
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1783776592,
|
||||
"narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=",
|
||||
"lastModified": 1782723713,
|
||||
"narHash": "sha256-oPXCU/SSUokcGaJREHibG1CBX3+s/W7orDWQOZDsEeQ=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3",
|
||||
"rev": "b5aa0fbd538984f6e3d201be0005b4463d8b09f8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
|
|
@ -27,17 +27,13 @@ const PREVIEW_ROUTE = (() => {
|
|||
})()
|
||||
|
||||
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() {
|
||||
const parts = window.location.pathname.split('/').filter(Boolean)
|
||||
const view = VIEWS.includes(parts[0]) ? parts[0] : 'archive'
|
||||
const settingsTab = (view === 'settings' && SETTINGS_TABS.includes(parts[1])) ? parts[1] : 'profile'
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
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 }
|
||||
return { view, settingsTab }
|
||||
}
|
||||
|
||||
function locationPath(view, settingsTab) {
|
||||
|
|
@ -70,13 +66,9 @@ export default function App() {
|
|||
// Sync URL → state on back/forward
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
const { view, settingsTab, q, tag, entry } = parseLocation()
|
||||
const { view, settingsTab } = parseLocation()
|
||||
setView(view)
|
||||
setSettingsTab(settingsTab)
|
||||
setSearchQuery(q)
|
||||
setTagFilter(tag)
|
||||
setSelectedEntryUid(entry)
|
||||
setSelectedEntry(null)
|
||||
}
|
||||
window.addEventListener('popstate', handler)
|
||||
return () => window.removeEventListener('popstate', handler)
|
||||
|
|
@ -85,12 +77,12 @@ export default function App() {
|
|||
const [archives, setArchives] = useState([])
|
||||
const [archiveId, setArchiveId] = useState(null)
|
||||
const [entries, setEntries] = useState([])
|
||||
const [selectedEntryUid, setSelectedEntryUid] = useState(() => parseLocation().entry)
|
||||
const [selectedEntryUid, setSelectedEntryUid] = 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 [settingsTab, setSettingsTab] = useState(() => parseLocation().settingsTab)
|
||||
const [searchQuery, setSearchQuery] = useState(() => parseLocation().q)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [resultCount, setResultCount] = useState('')
|
||||
const [searchBusy, setSearchBusy] = useState(false)
|
||||
const [runs, setRuns] = useState([])
|
||||
|
|
@ -110,9 +102,6 @@ export default function App() {
|
|||
// ── Entry detail (shared between PreviewPanel and ContextRail) ──────────
|
||||
const [entryDetail, setEntryDetail] = useState(null)
|
||||
const detailSeqRef = useRef(0)
|
||||
const searchInputRef = useRef(null)
|
||||
const pendingSearchFocus = useRef(false)
|
||||
const firstArchiveLoad = useRef(true)
|
||||
|
||||
const humanizeTags = currentUser?.humanize_slugs ?? false;
|
||||
|
||||
|
|
@ -168,16 +157,6 @@ export default function App() {
|
|||
// Archive change: parallel load entries + runs + tags
|
||||
useEffect(() => {
|
||||
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)
|
||||
setSelectedEntry(null)
|
||||
setSelectedEntryUid(null)
|
||||
|
|
@ -217,13 +196,13 @@ export default function App() {
|
|||
}
|
||||
}, [archiveId])
|
||||
|
||||
// Sync view + settingsTab → URL
|
||||
// Sync view + settingsTab → URL (skip when serving a standalone preview page)
|
||||
// Preserve existing search params so ?q/tag/entry survive view navigation.
|
||||
useEffect(() => {
|
||||
if (PREVIEW_ROUTE) return
|
||||
const path = locationPath(view, settingsTab)
|
||||
if (window.location.pathname !== path) {
|
||||
history.pushState(null, '', path + window.location.search)
|
||||
history.pushState(null, '', path)
|
||||
}
|
||||
}, [view, settingsTab])
|
||||
|
||||
|
|
@ -287,56 +266,6 @@ export default function App() {
|
|||
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(() => {
|
||||
setCaptureDialogOpen(true)
|
||||
}, [])
|
||||
|
|
@ -420,7 +349,6 @@ export default function App() {
|
|||
</svg>
|
||||
</span>
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
className="search-input"
|
||||
type="search"
|
||||
aria-label="Search archive"
|
||||
|
|
|
|||
|
|
@ -691,7 +691,7 @@ function ExtensionsTab() {
|
|||
const cookieExtEnabled = settings?.cookie_ext_enabled ?? true
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ maxWidth: 560 }}>
|
||||
<div className="form-section">
|
||||
<h2>Extensions</h2>
|
||||
<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.
|
||||
</p>
|
||||
|
||||
<div className="ext-grid">
|
||||
<div className="ext-card">
|
||||
<div className="ext-card-header">
|
||||
<div className="ext-card-info">
|
||||
<span className="ext-card-name">uBlock Origin Lite</span>
|
||||
<span className="ext-card-desc">
|
||||
Blocks ads, trackers, and other page clutter during archiving
|
||||
via Chrome’s declarativeNetRequest API (Manifest V3).
|
||||
<div className="ext-card">
|
||||
<div className="ext-card-header">
|
||||
<div className="ext-card-info">
|
||||
<span className="ext-card-name">uBlock Origin Lite</span>
|
||||
<span className="ext-card-desc">
|
||||
Blocks ads, trackers, and other page clutter during archiving
|
||||
via Chrome’s declarativeNetRequest API (Manifest V3).
|
||||
</span>
|
||||
{!extAvailable && (
|
||||
<span className="ext-card-hint">
|
||||
Not configured — set <code>ARCHIVR_UBLOCK_EXT</code> to the
|
||||
unpacked extension directory to enable.
|
||||
</span>
|
||||
{!extAvailable && (
|
||||
<span className="ext-card-hint">
|
||||
Not configured — 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>
|
||||
<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 className="ext-card">
|
||||
<div className="ext-card-header">
|
||||
<div className="ext-card-info">
|
||||
<span className="ext-card-name">I Still Don’t Care About Cookies</span>
|
||||
<span className="ext-card-desc">
|
||||
Dismiss cookie consent banners during archiving.
|
||||
<div className="ext-card">
|
||||
<div className="ext-card-header">
|
||||
<div className="ext-card-info">
|
||||
<span className="ext-card-name">I Still Don’t Care About Cookies</span>
|
||||
<span className="ext-card-desc">
|
||||
Dismiss cookie consent banners during archiving.
|
||||
</span>
|
||||
{!cookieExtAvailable && (
|
||||
<span className="ext-card-hint">
|
||||
Not configured — set <code>ARCHIVR_COOKIE_EXT</code> to the
|
||||
unpacked extension directory to enable.
|
||||
</span>
|
||||
{!cookieExtAvailable && (
|
||||
<span className="ext-card-hint">
|
||||
Not configured — 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>
|
||||
<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>
|
||||
|
||||
|
|
|
|||
|
|
@ -1014,12 +1014,6 @@ select {
|
|||
.ext-toggle--sm.ext-toggle--on .ext-toggle-knob { transform: translateX(16px); }
|
||||
|
||||
/* ── Extension card (Settings / Extensions tab) ──────────────────────────── */
|
||||
.ext-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ext-card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--r2);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue