8dc105ff3e
- Delete React files (App.jsx, main.jsx, App.css) - Create modular vanilla JS structure in client/src/: - state.js (global state object) - api.js (all fetch calls) - sse.js (SSE connection management) - ui/auth.js (authentication UI) - ui/downloads.js (downloads rendering) - ui/history.js (history section) - ui/statusPanel.js (status panel) - ui/webhooks.js (webhook management) - ui/filters.js (download client filter) - ui/theme.js (theme switching) - ui/tabs.js (tab navigation) - utils/format.js (formatting utilities) - utils/storage.js (localStorage helpers) - main.js (DOMContentLoaded bootstrap) - Update vite.config.js for vanilla build outputting to ../public/app.js - Build succeeds: 14 modules, 43.88 kB output
75 lines
2.4 KiB
JavaScript
75 lines
2.4 KiB
JavaScript
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
|
|
|
export function formatSize(size) {
|
|
if (!size) return 'N/A';
|
|
// If already a formatted string (e.g., "21.5 GB"), return as-is
|
|
if (typeof size === 'string') {
|
|
return size;
|
|
}
|
|
// If it's a number (bytes), format it
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.floor(Math.log(size) / Math.log(1024));
|
|
return Math.round(size / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
|
}
|
|
|
|
export function formatSpeed(bytesPerSecond) {
|
|
if (!bytesPerSecond || bytesPerSecond === 0) return '0 B/s';
|
|
|
|
const units = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
|
|
let value = bytesPerSecond;
|
|
let unitIndex = 0;
|
|
|
|
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
value /= 1024;
|
|
unitIndex++;
|
|
}
|
|
|
|
return `${value.toFixed(2)} ${units[unitIndex]}`;
|
|
}
|
|
|
|
export function formatDate(dateString) {
|
|
if (!dateString) return 'N/A';
|
|
return new Date(dateString).toLocaleString();
|
|
}
|
|
|
|
export function formatTimeAgo(timestamp) {
|
|
if (!timestamp) return 'Never';
|
|
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
if (seconds < 60) return seconds + 's ago';
|
|
const minutes = Math.floor(seconds / 60);
|
|
if (minutes < 60) return minutes + 'm ago';
|
|
const hours = Math.floor(minutes / 60);
|
|
if (hours < 24) return hours + 'h ago';
|
|
return Math.floor(hours / 24) + 'd ago';
|
|
}
|
|
|
|
export function escapeHtml(str) {
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Build an episode-info element for series downloads/history.
|
|
// Single episode: "S01E05 — Episode Title"
|
|
// Multiple episodes: "Multiple episodes" with tooltip listing them all.
|
|
// Returns null if no episode data.
|
|
export function formatEpisodeInfo(episodes) {
|
|
if (!episodes || episodes.length === 0) return null;
|
|
const el = document.createElement('p');
|
|
el.className = 'episode-info';
|
|
if (episodes.length === 1) {
|
|
const ep = episodes[0];
|
|
const code = 'S' + String(ep.season).padStart(2, '0') + 'E' + String(ep.episode).padStart(2, '0');
|
|
el.textContent = ep.title ? code + ' \u2014 ' + ep.title : code;
|
|
} else {
|
|
el.textContent = 'Multiple episodes';
|
|
el.classList.add('multi-episode');
|
|
const lines = episodes.map(ep => {
|
|
const code = 'S' + String(ep.season).padStart(2, '0') + 'E' + String(ep.episode).padStart(2, '0');
|
|
return ep.title ? code + ' \u2014 ' + ep.title : code;
|
|
});
|
|
el.setAttribute('data-tooltip', lines.join('\n'));
|
|
}
|
|
return el;
|
|
}
|