bsc

Comprehensive codebase and cou...
Log | Files | Refs | Activity | README | LICENSE

commit a96e61aaac2402b13a137ae15cb2c7b04695aefe
parent 2eb188a6a4faaeff12bf183b48ae5c68a170a10f
Author: Amit Dutta <mail@amit.is-a.dev>
Date:   Sat, 29 Aug 2026 12:58:41 +0530

Merge pull request #112 from notamitgamer/feature/snapshot-progress-ui

Snapshot capture: concurrent fetches + button turns into a progress bar
Diffstat:
Mdocs/.vitepress/theme/components/SnapshotManager.vue | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
1 file changed, 105 insertions(+), 19 deletions(-)

diff --git a/docs/.vitepress/theme/components/SnapshotManager.vue b/docs/.vitepress/theme/components/SnapshotManager.vue @@ -1,10 +1,15 @@ <script setup> -import { ref, onMounted } from 'vue' +import { ref, computed, onMounted } from 'vue' import { listSnapshots, putSnapshot, deleteSnapshotMeta } from '../lib/snapshotDb' const snapshots = ref([]) const isCapturing = ref(false) -const progress = ref({ done: 0, total: 0, label: '' }) +const progress = ref({ done: 0, total: 0, phaseLabel: '', pagesTotal: 0, assetsTotal: 0 }) + +const progressPct = computed(() => { + if (!progress.value.total) return 0 + return Math.min(100, Math.round((progress.value.done / progress.value.total) * 100)) +}) const error = ref('') onMounted(refresh) @@ -50,26 +55,44 @@ function extractAssetUrls(html) { return urls } +// Runs `worker` over `items` with up to `concurrency` in flight at once, +// instead of one-at-a-time — this is what makes capture fast, and it's what +// lets the progress bar move continuously instead of stalling. +async function runPool(items, concurrency, worker) { + let i = 0 + async function runNext() { + while (i < items.length) { + const idx = i++ + await worker(items[idx], idx) + } + } + const workers = Array.from({ length: Math.min(concurrency, items.length) }, runNext) + await Promise.all(workers) +} + +const CONCURRENCY = 12 + async function takeSnapshot() { error.value = '' isCapturing.value = true const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}` const cacheName = `snapshot-${id}` let sizeBytes = 0 - let assetUrls = new Set() + const assetUrls = new Set() try { const pagePaths = await getPagePaths() - progress.value = { done: 0, total: pagePaths.length, label: 'Fetching pages…' } + // "total" spans pages + assets so the bar reflects overall progress, not + // just the current phase resetting to 0. + progress.value = { done: 0, total: pagePaths.length, phaseLabel: 'Fetching pages…', pagesTotal: pagePaths.length, assetsTotal: 0 } const cache = await caches.open(cacheName) - for (const path of pagePaths) { + await runPool(pagePaths, CONCURRENCY, async (path) => { try { const res = await fetch(path, { cache: 'no-store' }) if (res && res.ok) { - const clone = res.clone() - const html = await clone.text() + const html = await res.clone().text() sizeBytes += html.length extractAssetUrls(html).forEach((u) => assetUrls.add(u)) await cache.put(path, res) @@ -78,23 +101,30 @@ async function takeSnapshot() { /* skip page that failed, continue capturing the rest */ } progress.value.done++ - } + }) const assetList = [...assetUrls] - progress.value = { done: 0, total: assetList.length, label: 'Fetching assets…' } - for (const url of assetList) { + progress.value = { + done: pagePaths.length, + total: pagePaths.length + assetList.length, + phaseLabel: 'Fetching assets…', + pagesTotal: pagePaths.length, + assetsTotal: assetList.length, + } + + await runPool(assetList, CONCURRENCY, async (url) => { try { const res = await fetch(url, { cache: 'no-store' }) if (res && res.ok) { - const buf = await res.clone().arrayBuffer() - sizeBytes += buf.byteLength + const len = res.headers.get('content-length') + sizeBytes += len ? Number(len) : 0 await cache.put(url, res) } } catch (_) { /* skip */ } progress.value.done++ - } + }) await putSnapshot({ id, @@ -112,7 +142,7 @@ async function takeSnapshot() { try { await caches.delete(cacheName) } catch (_) {} } finally { isCapturing.value = false - progress.value = { done: 0, total: 0, label: '' } + progress.value = { done: 0, total: 0, phaseLabel: '', pagesTotal: 0, assetsTotal: 0 } } } @@ -143,11 +173,27 @@ async function renameSnapshot(snap) { Take one before you go offline, and come back to it any time from this page. </p> - <button class="capture-btn" :disabled="isCapturing" @click="takeSnapshot"> - <span v-if="!isCapturing">📸 Take snapshot now</span> - <span v-else>Capturing… {{ progress.label }} ({{ progress.done }}/{{ progress.total }})</span> + <button + v-if="!isCapturing" + class="capture-btn" + @click="takeSnapshot" + > + 📸 Take snapshot now </button> + <div v-else class="capture-btn capture-progress" role="progressbar" + :aria-valuenow="progressPct" aria-valuemin="0" aria-valuemax="100"> + <div class="capture-progress-fill" :style="{ width: progressPct + '%' }"></div> + <!-- Two copies of the same label, each clipped to one side of the fill, + so the text stays readable no matter how far the bar has progressed. --> + <div class="capture-progress-label capture-progress-label-track"> + {{ progress.phaseLabel }} {{ progress.done }}/{{ progress.total }} ({{ progressPct }}%) + </div> + <div class="capture-progress-label capture-progress-label-fill" :style="{ clipPath: `inset(0 ${100 - progressPct}% 0 0)` }"> + {{ progress.phaseLabel }} {{ progress.done }}/{{ progress.total }} ({{ progressPct }}%) + </div> + </div> + <p v-if="error" class="error">{{ error }}</p> <div v-if="snapshots.length" class="snapshot-list"> @@ -187,10 +233,50 @@ async function renameSnapshot(snap) { font-weight: 600; cursor: pointer; margin-bottom: 20px; + min-width: 260px; + height: 40px; + display: inline-flex; + align-items: center; + justify-content: center; } -.capture-btn:disabled { - opacity: 0.7; +.capture-progress { + position: relative; + overflow: hidden; cursor: default; + justify-content: flex-start; + padding: 0; + background: var(--vp-c-bg-soft); + border: 1px solid var(--vp-c-brand-1); +} +.capture-progress-fill { + position: absolute; + inset: 0 auto 0 0; + background: var(--vp-c-brand-1); + transition: width 0.25s ease; +} +.capture-progress-label { + position: absolute; + inset: 0; + z-index: 1; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: 0 12px; + font-size: 12.5px; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + pointer-events: none; +} +/* Text over the unfilled track: use the normal (theme-adaptive) text color. */ +.capture-progress-label-track { + color: var(--vp-c-text-1); +} +/* Text over the colored fill: always white, revealed only where the fill covers it. */ +.capture-progress-label-fill { + color: #fff; } .error { color: var(--vp-c-danger-1, #d33);
© notamitgamer • Site Built: 2026-09-05 01:53:16 UTC • git-mirror commit: c170d72 [view raw info]
Originally created with stagit • modified by notamitgamer
Forked from github.com/notamitgamer/git-mirror