cleanup: dedupe formatBytes — renderer-stats + renderer-archive copies hoist to renderer-shared

renderer-stats.ts and renderer-archive.ts each had their own byte-size formatter (formatBytesForStats / formatBytesForArchive). The two were textually identical: both handle the B -> KB -> MB -> GB -> TB ladder with the same toFixed precision and return '0 B' for non-finite / zero / negative input.

Hoisted to renderer-shared.ts as plain formatBytes. Removed both per-file copies and renamed all 14 call sites across the two modules. The two narrower variants in renderer-settings.ts (formatBytesForMetrics — caps at GB) and renderer.ts (formatBytesRenderer — caps at GB, less protection) stay file-scoped because they have different scale/protection semantics for their specific contexts (runtime metrics + download progress, which never reach TB).

Continues the renderer-shared consolidation from 4.6.127 (applyHtml/escapeHtml).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xRangerDE 2026-05-11 09:45:10 +02:00
parent a62080cb44
commit 2b09b7868a
3 changed files with 26 additions and 29 deletions

View File

@ -2,15 +2,6 @@ let archiveStreamerSelectPopulated = false;
let archiveSearchInFlight = false; let archiveSearchInFlight = false;
let archiveSearchDebounceTimer: number | null = null; let archiveSearchDebounceTimer: number | null = null;
function formatBytesForArchive(bytes: number): string {
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
if (bytes < 1024 * 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
return `${(bytes / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TB`;
}
function populateArchiveStreamerSelect(): void { function populateArchiveStreamerSelect(): void {
if (archiveStreamerSelectPopulated) return; if (archiveStreamerSelectPopulated) return;
const select = document.getElementById('archiveSearchStreamer') as HTMLSelectElement | null; const select = document.getElementById('archiveSearchStreamer') as HTMLSelectElement | null;
@ -118,7 +109,7 @@ function renderArchiveSearchResults(result: ArchiveSearchResult): void {
<span class="archive-result-date">${escapeHtml(date)}</span> <span class="archive-result-date">${escapeHtml(date)}</span>
</div> </div>
<div class="archive-result-filename" title="${escapeHtml(hit.fullPath)}">${escapeHtml(hit.fileName)}</div> <div class="archive-result-filename" title="${escapeHtml(hit.fullPath)}">${escapeHtml(hit.fileName)}</div>
<div class="archive-result-size">${escapeHtml(formatBytesForArchive(hit.size))}</div> <div class="archive-result-size">${escapeHtml(formatBytes(hit.size))}</div>
</div> </div>
<div class="archive-result-actions"> <div class="archive-result-actions">
<button type="button" class="queue-detail-btn" onclick="openFilePath('${safeFullAttr}')">${escapeHtml(UI_TEXT.static.archiveOpen || 'Oeffnen')}</button> <button type="button" class="queue-detail-btn" onclick="openFilePath('${safeFullAttr}')">${escapeHtml(UI_TEXT.static.archiveOpen || 'Oeffnen')}</button>

View File

@ -29,6 +29,20 @@ function applyHtml(el: HTMLElement, html: string): void {
(el as unknown as Record<string, string>)[key] = html; (el as unknown as Record<string, string>)[key] = html;
} }
/* Generic file-size formatter for the renderer. Scales B -> KB -> MB
-> GB -> TB; returns '0 B' for zero / negative / non-finite input.
Used by the archive search results and the stats card. Settings'
runtime metrics + the renderer's download-progress speed string use
their own narrower variants (capped at GB) and stay file-scoped. */
function formatBytes(bytes: number): string {
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
if (bytes < 1024 * 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
return `${(bytes / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TB`;
}
/* localStorage helpers every renderer module that persists state was /* localStorage helpers every renderer module that persists state was
wrapping its get/set calls in the same try/catch idiom to handle wrapping its get/set calls in the same try/catch idiom to handle
environments where localStorage isn't writable (private-browsing environments where localStorage isn't writable (private-browsing

View File

@ -38,12 +38,12 @@ function renderStatsSummary(stats: ArchiveStats): void {
} }
const cards: Array<{ label: string; value: string; sub?: string }> = [ const cards: Array<{ label: string; value: string; sub?: string }> = [
{ label: UI_TEXT.static.statsTotalRecordings, value: String(stats.liveCount + stats.vodCount), sub: formatBytesForStats(stats.liveBytes + stats.vodBytes) }, { label: UI_TEXT.static.statsTotalRecordings, value: String(stats.liveCount + stats.vodCount), sub: formatBytes(stats.liveBytes + stats.vodBytes) },
{ label: UI_TEXT.static.statsLiveRecordings, value: String(stats.liveCount), sub: formatBytesForStats(stats.liveBytes) }, { label: UI_TEXT.static.statsLiveRecordings, value: String(stats.liveCount), sub: formatBytes(stats.liveBytes) },
{ label: UI_TEXT.static.statsVodRecordings, value: String(stats.vodCount), sub: formatBytesForStats(stats.vodBytes) }, { label: UI_TEXT.static.statsVodRecordings, value: String(stats.vodCount), sub: formatBytes(stats.vodBytes) },
{ label: UI_TEXT.static.statsStreamers, value: String(stats.streamerCount) }, { label: UI_TEXT.static.statsStreamers, value: String(stats.streamerCount) },
{ label: UI_TEXT.static.statsAvgSize, value: stats.avgRecordingSizeBytes > 0 ? formatBytesForStats(stats.avgRecordingSizeBytes) : '-' }, { label: UI_TEXT.static.statsAvgSize, value: stats.avgRecordingSizeBytes > 0 ? formatBytes(stats.avgRecordingSizeBytes) : '-' },
{ label: UI_TEXT.static.statsChatFiles, value: String(stats.chatCount), sub: formatBytesForStats(stats.chatBytes) } { label: UI_TEXT.static.statsChatFiles, value: String(stats.chatCount), sub: formatBytes(stats.chatBytes) }
]; ];
applyHtml(grid, cards.map((c) => ` applyHtml(grid, cards.map((c) => `
@ -72,13 +72,13 @@ function renderStatsTopStreamers(top: ArchiveStatsTopStreamer[], totalBytes: num
<div class="stats-top-row"> <div class="stats-top-row">
<div class="stats-top-meta"> <div class="stats-top-meta">
<span><strong>${escapeHtml(s.streamer)}</strong> <span class="stats-top-meta-sub"><span aria-hidden="true">&middot;</span> ${s.fileCount} ${escapeHtml(UI_TEXT.static.statsFiles)}</span></span> <span><strong>${escapeHtml(s.streamer)}</strong> <span class="stats-top-meta-sub"><span aria-hidden="true">&middot;</span> ${s.fileCount} ${escapeHtml(UI_TEXT.static.statsFiles)}</span></span>
<span class="stats-top-meta-sub">${formatBytesForStats(s.bytes)} <span class="stats-top-share">(${sharePct}%)</span></span> <span class="stats-top-meta-sub">${formatBytes(s.bytes)} <span class="stats-top-share">(${sharePct}%)</span></span>
</div> </div>
<div class="stats-top-bar-track"> <div class="stats-top-bar-track">
<div class="stats-top-bar-fill" style="width: ${pct}%;"></div> <div class="stats-top-bar-fill" style="width: ${pct}%;"></div>
${(s.liveBytes > 0 || s.vodBytes > 0) ? `<div class="stats-top-bar-labels"> ${(s.liveBytes > 0 || s.vodBytes > 0) ? `<div class="stats-top-bar-labels">
${s.liveBytes > 0 ? `LIVE ${formatBytesForStats(s.liveBytes)}` : ''} ${s.liveBytes > 0 ? `LIVE ${formatBytes(s.liveBytes)}` : ''}
${s.vodBytes > 0 ? `VOD ${formatBytesForStats(s.vodBytes)}` : ''} ${s.vodBytes > 0 ? `VOD ${formatBytes(s.vodBytes)}` : ''}
</div>` : ''} </div>` : ''}
</div> </div>
</div> </div>
@ -103,7 +103,7 @@ function renderStatsActivity(days: ArchiveStatsDay[]): void {
const bars = days.map((d, idx) => { const bars = days.map((d, idx) => {
const heightPct = Math.max(4, Math.round((d.count / maxCount) * 100)); const heightPct = Math.max(4, Math.round((d.count / maxCount) * 100));
const tooltip = `${d.date}: ${d.count} ${UI_TEXT.static.statsFiles} - ${formatBytesForStats(d.bytes)}`; const tooltip = `${d.date}: ${d.count} ${UI_TEXT.static.statsFiles} - ${formatBytes(d.bytes)}`;
const showLabel = idx === 0 || idx === days.length - 1 || idx % 7 === 0; const showLabel = idx === 0 || idx === days.length - 1 || idx % 7 === 0;
const dayLabel = showLabel ? d.date.slice(5) : ''; const dayLabel = showLabel ? d.date.slice(5) : '';
return ` return `
@ -122,7 +122,7 @@ function renderStatsActivity(days: ArchiveStatsDay[]): void {
<div class="stats-activity-row">${bars}</div> <div class="stats-activity-row">${bars}</div>
<div class="stats-activity-summary">${escapeHtml(UI_TEXT.static.statsActivitySummary <div class="stats-activity-summary">${escapeHtml(UI_TEXT.static.statsActivitySummary
.replace('{count}', String(totalCount)) .replace('{count}', String(totalCount))
.replace('{size}', formatBytesForStats(totalBytes)))}</div> .replace('{size}', formatBytes(totalBytes)))}</div>
`); `);
} }
@ -142,7 +142,7 @@ function renderStatsSizeBuckets(buckets: ArchiveStatsBucket[]): void {
<div class="stats-bucket-row"> <div class="stats-bucket-row">
<div class="stats-bucket-meta"> <div class="stats-bucket-meta">
<span>${escapeHtml(b.label)}</span> <span>${escapeHtml(b.label)}</span>
<span class="stats-bucket-meta-sub">${b.count} <span aria-hidden="true">&middot;</span> ${formatBytesForStats(b.bytes)}</span> <span class="stats-bucket-meta-sub">${b.count} <span aria-hidden="true">&middot;</span> ${formatBytes(b.bytes)}</span>
</div> </div>
<div class="stats-bucket-bar-track"> <div class="stats-bucket-bar-track">
<div class="stats-bucket-bar-fill" style="width: ${pct}%;"></div> <div class="stats-bucket-bar-fill" style="width: ${pct}%;"></div>
@ -152,14 +152,6 @@ function renderStatsSizeBuckets(buckets: ArchiveStatsBucket[]): void {
}).join('')); }).join(''));
} }
function formatBytesForStats(bytes: number): string {
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
if (bytes < 1024 * 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
return `${(bytes / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TB`;
}
(window as unknown as { refreshArchiveStats: typeof refreshArchiveStats }).refreshArchiveStats = refreshArchiveStats; (window as unknown as { refreshArchiveStats: typeof refreshArchiveStats }).refreshArchiveStats = refreshArchiveStats;