massive improvement

This commit is contained in:
2025-03-04 22:28:11 +00:00
parent d1483ce581
commit 1b97e3ba68
21 changed files with 7152 additions and 1611 deletions

665
public/css/styles.css Normal file
View File

@@ -0,0 +1,665 @@
/* Main Styles for Transmission RSS Manager */
:root {
--primary-color: #3498db;
--primary-dark: #2980b9;
--secondary-color: #2ecc71;
--secondary-dark: #27ae60;
--warning-color: #f39c12;
--danger-color: #e74c3c;
--background-color: #f8f9fa;
--dark-background: #1a1a1a;
--card-background: #ffffff;
--dark-card-background: #2a2a2a;
--text-color: #333333;
--dark-text-color: #f5f5f5;
--border-color: #dddddd;
--dark-border-color: #444444;
--success-background: #d4edda;
--success-text: #155724;
--error-background: #f8d7da;
--error-text: #721c24;
--input-background: #ffffff;
--dark-input-background: #333333;
}
/* Dark mode styles */
[data-theme="dark"] {
--background-color: var(--dark-background);
--card-background: var(--dark-card-background);
--text-color: var(--dark-text-color);
--border-color: var(--dark-border-color);
--input-background: var(--dark-input-background);
}
* {
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: var(--background-color);
margin: 0;
padding: 0;
transition: background-color 0.3s ease;
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
margin-top: 0;
}
a {
color: var(--primary-color);
text-decoration: none;
transition: color 0.3s ease;
}
a:hover {
color: var(--primary-dark);
text-decoration: underline;
}
/* Buttons */
.btn {
cursor: pointer;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-size: 14px;
background-color: var(--primary-color);
color: white;
transition: background-color 0.3s ease, transform 0.2s ease;
margin: 2px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.btn:active {
transform: translateY(0);
}
.btn i, .btn svg {
margin-right: 8px;
}
.btn.btn-sm {
padding: 4px 8px;
font-size: 12px;
}
.btn.btn-lg {
padding: 12px 20px;
font-size: 16px;
}
.btn.btn-primary {
background-color: var(--primary-color);
}
.btn.btn-success {
background-color: var(--secondary-color);
}
.btn.btn-warning {
background-color: var(--warning-color);
}
.btn.btn-danger {
background-color: var(--danger-color);
}
.btn.btn-outline {
background-color: transparent;
border: 1px solid var(--primary-color);
color: var(--primary-color);
}
.btn.btn-outline:hover {
background-color: var(--primary-color);
color: white;
}
/* Layout */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.row {
display: flex;
flex-wrap: wrap;
margin: -10px;
}
.col {
flex: 1;
padding: 10px;
}
.col-25 {
flex: 0 0 25%;
max-width: 25%;
padding: 10px;
}
.col-50 {
flex: 0 0 50%;
max-width: 50%;
padding: 10px;
}
.col-75 {
flex: 0 0 75%;
max-width: 75%;
padding: 10px;
}
/* Header and Navigation */
header {
background-color: var(--primary-color);
color: white;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: sticky;
top: 0;
z-index: 1000;
}
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
}
.navbar-brand {
font-size: 1.5rem;
font-weight: bold;
color: white;
text-decoration: none;
display: flex;
align-items: center;
}
.navbar-brand i, .navbar-brand svg {
margin-right: 8px;
}
.navbar-menu {
display: flex;
list-style: none;
margin: 0;
padding: 0;
}
.navbar-item {
margin: 0 10px;
}
.navbar-link {
color: rgba(255, 255, 255, 0.85);
text-decoration: none;
transition: color 0.3s ease;
display: flex;
align-items: center;
}
.navbar-link i, .navbar-link svg {
margin-right: 5px;
}
.navbar-link:hover,
.navbar-link.active {
color: white;
}
.navbar-right {
display: flex;
align-items: center;
}
.theme-toggle {
background: none;
border: none;
color: white;
cursor: pointer;
font-size: 1.2rem;
margin-left: 1rem;
}
/* Cards */
.card {
background-color: var(--card-background);
border-radius: 8px;
border: 1px solid var(--border-color);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
margin-bottom: 1.5rem;
transition: box-shadow 0.3s ease, transform 0.3s ease;
}
.card:hover {
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.card-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
}
.card-header h2, .card-header h3 {
margin: 0;
}
.card-body {
padding: 1.5rem;
}
.card-footer {
padding: 1rem 1.5rem;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
}
/* Forms */
.form-group {
margin-bottom: 1rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-control {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--input-background);
color: var(--text-color);
font-size: 1rem;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.form-control:focus {
border-color: var(--primary-color);
outline: none;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
}
.form-check {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
}
.form-check-input {
margin-right: 0.5rem;
}
/* Tables */
.table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1.5rem;
}
.table th,
.table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.table th {
background-color: rgba(0, 0, 0, 0.05);
font-weight: 600;
}
.table tr:hover {
background-color: rgba(0, 0, 0, 0.025);
}
.table-responsive {
overflow-x: auto;
}
/* Progress Bar */
.progress {
height: 0.75rem;
background-color: var(--border-color);
border-radius: 0.375rem;
overflow: hidden;
}
.progress-bar {
height: 100%;
background-color: var(--primary-color);
transition: width 0.3s ease;
}
.progress-bar-success {
background-color: var(--secondary-color);
}
.progress-bar-warning {
background-color: var(--warning-color);
}
.progress-bar-danger {
background-color: var(--danger-color);
}
/* Alerts */
.alert {
padding: 0.75rem 1.25rem;
margin-bottom: 1rem;
border-radius: 4px;
border: 1px solid transparent;
}
.alert-success {
background-color: var(--success-background);
border-color: #c3e6cb;
color: var(--success-text);
}
.alert-danger {
background-color: var(--error-background);
border-color: #f5c6cb;
color: var(--error-text);
}
.alert-warning {
background-color: #fff3cd;
border-color: #ffeeba;
color: #856404;
}
.alert-info {
background-color: #d1ecf1;
border-color: #bee5eb;
color: #0c5460;
}
/* Badges */
.badge {
display: inline-block;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
font-weight: 600;
border-radius: 0.25rem;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
}
.badge-primary {
background-color: var(--primary-color);
color: white;
}
.badge-success {
background-color: var(--secondary-color);
color: white;
}
.badge-warning {
background-color: var(--warning-color);
color: white;
}
.badge-danger {
background-color: var(--danger-color);
color: white;
}
/* Modals */
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1050;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.modal-backdrop.show {
opacity: 1;
visibility: visible;
}
.modal {
background-color: var(--card-background);
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
width: 100%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
opacity: 0;
transform: translateY(-20px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.modal-backdrop.show .modal {
opacity: 1;
transform: translateY(0);
}
.modal-header {
padding: 1rem;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
}
.modal-header h2 {
margin: 0;
}
.modal-close {
border: none;
background: none;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
color: var(--text-color);
}
.modal-body {
padding: 1rem;
}
.modal-footer {
padding: 1rem;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
}
/* Tabs */
.tabs {
display: flex;
border-bottom: 1px solid var(--border-color);
margin-bottom: 1.5rem;
}
.tab {
padding: 0.75rem 1.5rem;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: border-color 0.3s ease, color 0.3s ease;
font-weight: 500;
}
.tab:hover {
color: var(--primary-color);
}
.tab.active {
border-bottom-color: var(--primary-color);
color: var(--primary-color);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Dashboard Widgets */
.stats-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background-color: var(--card-background);
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
display: flex;
align-items: center;
}
.stat-icon {
width: 3rem;
height: 3rem;
border-radius: 50%;
background-color: rgba(52, 152, 219, 0.1);
display: flex;
align-items: center;
justify-content: center;
margin-right: 1rem;
font-size: 1.5rem;
color: var(--primary-color);
}
.stat-info h3 {
margin: 0;
font-size: 2rem;
font-weight: 700;
}
.stat-info p {
margin: 0;
color: #777;
font-size: 0.875rem;
}
/* Utilities */
.text-center { text-align: center; }
.text-right { text-align: right; }
.text-left { text-align: left; }
.font-weight-bold { font-weight: bold; }
.text-muted { color: #6c757d; }
.mt-1 { margin-top: 0.5rem; }
.mt-2 { margin-top: 1rem; }
.mt-3 { margin-top: 1.5rem; }
.mb-1 { margin-bottom: 0.5rem; }
.mb-2 { margin-bottom: 1rem; }
.mb-3 { margin-bottom: 1.5rem; }
.ml-1 { margin-left: 0.5rem; }
.ml-2 { margin-left: 1rem; }
.ml-3 { margin-left: 1.5rem; }
.mr-1 { margin-right: 0.5rem; }
.mr-2 { margin-right: 1rem; }
.mr-3 { margin-right: 1.5rem; }
.pt-1 { padding-top: 0.5rem; }
.pt-2 { padding-top: 1rem; }
.pt-3 { padding-top: 1.5rem; }
.pb-1 { padding-bottom: 0.5rem; }
.pb-2 { padding-bottom: 1rem; }
.pb-3 { padding-bottom: 1.5rem; }
.pl-1 { padding-left: 0.5rem; }
.pl-2 { padding-left: 1rem; }
.pl-3 { padding-left: 1.5rem; }
.pr-1 { padding-right: 0.5rem; }
.pr-2 { padding-right: 1rem; }
.pr-3 { padding-right: 1.5rem; }
.d-none { display: none; }
.d-block { display: block; }
.d-flex { display: flex; }
.flex-wrap { flex-wrap: wrap; }
.align-items-center { align-items: center; }
.justify-content-center { justify-content: center; }
.justify-content-between { justify-content: space-between; }
.justify-content-end { justify-content: flex-end; }
/* Media Queries */
@media (max-width: 768px) {
.navbar {
flex-direction: column;
align-items: stretch;
}
.navbar-menu {
flex-direction: column;
margin-top: 1rem;
}
.navbar-item {
margin: 0.25rem 0;
}
.navbar-right {
margin-top: 0.5rem;
justify-content: flex-end;
}
.stats-container {
grid-template-columns: 1fr;
}
.col-25, .col-50, .col-75 {
flex: 0 0 100%;
max-width: 100%;
}
}
@media (max-width: 576px) {
.card-header {
flex-direction: column;
align-items: flex-start;
}
.card-header > .btn {
margin-top: 0.5rem;
}
}

File diff suppressed because it is too large Load Diff

1650
public/js/app.js Normal file

File diff suppressed because it is too large Load Diff

637
public/js/utils.js Normal file
View File

@@ -0,0 +1,637 @@
/**
* Utility functions for Transmission RSS Manager
*/
/**
* Format a byte value to a human-readable string
* @param {number} bytes - Bytes to format
* @param {number} decimals - Number of decimal places to show
* @returns {string} - Formatted string (e.g., "1.5 MB")
*/
export function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
/**
* Create a debounced version of a function
* @param {Function} func - Function to debounce
* @param {number} wait - Milliseconds to wait
* @returns {Function} - Debounced function
*/
export function debounce(func, wait) {
let timeout;
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
/**
* Create a throttled version of a function
* @param {Function} func - Function to throttle
* @param {number} limit - Milliseconds to throttle
* @returns {Function} - Throttled function
*/
export function throttle(func, limit) {
let inThrottle;
return function(...args) {
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => { inThrottle = false; }, limit);
}
};
}
/**
* Safely parse JSON with error handling
* @param {string} json - JSON string to parse
* @param {*} fallback - Fallback value if parsing fails
* @returns {*} - Parsed object or fallback
*/
export function safeJsonParse(json, fallback = {}) {
try {
return JSON.parse(json);
} catch (e) {
console.error('Error parsing JSON:', e);
return fallback;
}
}
/**
* Escape HTML special characters
* @param {string} html - String potentially containing HTML
* @returns {string} - Escaped string
*/
export function escapeHtml(html) {
if (!html) return '';
const entities = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;'
};
return String(html).replace(/[&<>"'/]/g, match => entities[match]);
}
/**
* Get URL query parameters as an object
* @returns {Object} - Object containing query parameters
*/
export function getQueryParams() {
const params = {};
new URLSearchParams(window.location.search).forEach((value, key) => {
params[key] = value;
});
return params;
}
/**
* Add query parameters to a URL
* @param {string} url - Base URL
* @param {Object} params - Parameters to add
* @returns {string} - URL with parameters
*/
export function addQueryParams(url, params) {
const urlObj = new URL(url, window.location.origin);
Object.keys(params).forEach(key => {
if (params[key] !== null && params[key] !== undefined) {
urlObj.searchParams.append(key, params[key]);
}
});
return urlObj.toString();
}
/**
* Create a simple hash of a string
* @param {string} str - String to hash
* @returns {number} - Numeric hash
*/
export function simpleHash(str) {
let hash = 0;
if (str.length === 0) return hash;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return hash;
}
/**
* Generate a random string of specified length
* @param {number} length - Length of the string
* @returns {string} - Random string
*/
export function randomString(length = 8) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* Format a date to a readable string
* @param {string|Date} date - Date to format
* @param {boolean} includeTime - Whether to include time
* @returns {string} - Formatted date string
*/
export function formatDate(date, includeTime = false) {
try {
const d = new Date(date);
const options = {
year: 'numeric',
month: 'short',
day: 'numeric',
...(includeTime ? { hour: '2-digit', minute: '2-digit' } : {})
};
return d.toLocaleDateString(undefined, options);
} catch (e) {
console.error('Error formatting date:', e);
return '';
}
}
/**
* Check if a date is today
* @param {string|Date} date - Date to check
* @returns {boolean} - True if date is today
*/
export function isToday(date) {
const d = new Date(date);
const today = new Date();
return d.getDate() === today.getDate() &&
d.getMonth() === today.getMonth() &&
d.getFullYear() === today.getFullYear();
}
/**
* Get file extension from path
* @param {string} path - File path
* @returns {string} - File extension
*/
export function getFileExtension(path) {
if (!path) return '';
return path.split('.').pop().toLowerCase();
}
/**
* Check if file is an image based on extension
* @param {string} path - File path
* @returns {boolean} - True if file is an image
*/
export function isImageFile(path) {
const ext = getFileExtension(path);
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'].includes(ext);
}
/**
* Check if file is a video based on extension
* @param {string} path - File path
* @returns {boolean} - True if file is a video
*/
export function isVideoFile(path) {
const ext = getFileExtension(path);
return ['mp4', 'mkv', 'avi', 'mov', 'webm', 'wmv', 'flv', 'm4v'].includes(ext);
}
/**
* Check if file is an audio file based on extension
* @param {string} path - File path
* @returns {boolean} - True if file is audio
*/
export function isAudioFile(path) {
const ext = getFileExtension(path);
return ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac'].includes(ext);
}
/**
* Extract base filename without extension
* @param {string} path - File path
* @returns {string} - Base filename
*/
export function getBaseName(path) {
if (!path) return '';
const fileName = path.split('/').pop();
return fileName.substring(0, fileName.lastIndexOf('.')) || fileName;
}
/**
* Copy text to clipboard
* @param {string} text - Text to copy
* @returns {Promise<boolean>} - Success status
*/
export async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.error('Failed to copy text: ', err);
return false;
}
}
/**
* Download data as a file
* @param {string} content - Content to download
* @param {string} fileName - Name of the file
* @param {string} contentType - MIME type of the file
*/
export function downloadFile(content, fileName, contentType = 'text/plain') {
const a = document.createElement('a');
const file = new Blob([content], { type: contentType });
a.href = URL.createObjectURL(file);
a.download = fileName;
a.click();
URL.revokeObjectURL(a.href);
}
/**
* Sort array of objects by a property
* @param {Array} array - Array to sort
* @param {string} property - Property to sort by
* @param {boolean} ascending - Sort direction
* @returns {Array} - Sorted array
*/
export function sortArrayByProperty(array, property, ascending = true) {
const sortFactor = ascending ? 1 : -1;
return [...array].sort((a, b) => {
if (a[property] < b[property]) return -1 * sortFactor;
if (a[property] > b[property]) return 1 * sortFactor;
return 0;
});
}
/**
* Filter array by a search term across multiple properties
* @param {Array} array - Array to filter
* @param {string} search - Search term
* @param {Array<string>} properties - Properties to search in
* @returns {Array} - Filtered array
*/
export function filterArrayBySearch(array, search, properties) {
if (!search || !properties || properties.length === 0) return array;
const term = search.toLowerCase();
return array.filter(item => {
return properties.some(prop => {
const value = item[prop];
if (typeof value === 'string') {
return value.toLowerCase().includes(term);
}
return false;
});
});
}
/**
* Deep clone an object
* @param {Object} obj - Object to clone
* @returns {Object} - Cloned object
*/
export function deepClone(obj) {
if (!obj) return obj;
return JSON.parse(JSON.stringify(obj));
}
/**
* Get readable torrent status
* @param {number} status - Transmission status code
* @returns {string} - Human-readable status
*/
export function getTorrentStatus(status) {
const statusMap = {
0: 'Stopped',
1: 'Check Waiting',
2: 'Checking',
3: 'Download Waiting',
4: 'Downloading',
5: 'Seed Waiting',
6: 'Seeding'
};
return statusMap[status] || 'Unknown';
}
/**
* Get appropriate CSS class for a torrent status badge
* @param {number} status - Torrent status code
* @returns {string} - CSS class
*/
export function getBadgeClassForStatus(status) {
switch (status) {
case 0: return 'badge-danger'; // Stopped
case 1: case 2: case 3: return 'badge-warning'; // Checking/Waiting
case 4: return 'badge-primary'; // Downloading
case 5: case 6: return 'badge-success'; // Seeding
default: return 'badge-secondary';
}
}
/**
* Get appropriate CSS class for a torrent progress bar
* @param {number} status - Torrent status code
* @returns {string} - CSS class
*/
export function getProgressBarClassForStatus(status) {
switch (status) {
case 0: return 'bg-danger'; // Stopped
case 4: return 'bg-primary'; // Downloading
case 5: case 6: return 'bg-success'; // Seeding
default: return '';
}
}
/**
* Get cookie value by name
* @param {string} name - Cookie name
* @returns {string|null} - Cookie value or null
*/
export function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
}
/**
* Set a cookie
* @param {string} name - Cookie name
* @param {string} value - Cookie value
* @param {number} days - Days until expiry
*/
export function setCookie(name, value, days = 30) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
const expires = `expires=${date.toUTCString()}`;
document.cookie = `${name}=${value};${expires};path=/;SameSite=Strict`;
}
/**
* Delete a cookie
* @param {string} name - Cookie name
*/
export function deleteCookie(name) {
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;SameSite=Strict`;
}
/**
* Handle common API response with error checking
* @param {Response} response - Fetch API response
* @returns {Promise} - Resolves to response data
*/
export function handleApiResponse(response) {
if (!response.ok) {
// Try to get error message from response
return response.json()
.then(data => {
throw new Error(data.message || `HTTP error ${response.status}`);
})
.catch(e => {
// If JSON parsing fails, throw generic error
if (e instanceof SyntaxError) {
throw new Error(`HTTP error ${response.status}`);
}
throw e;
});
}
return response.json();
}
/**
* Encrypt a string using AES (for client-side only, not secure)
* @param {string} text - Text to encrypt
* @param {string} key - Encryption key
* @returns {string} - Encrypted text
*/
export function encrypt(text, key) {
// This is a simple XOR "encryption" - NOT SECURE!
// Only for basic obfuscation
let result = '';
for (let i = 0; i < text.length; i++) {
result += String.fromCharCode(text.charCodeAt(i) ^ key.charCodeAt(i % key.length));
}
return btoa(result); // Base64 encode
}
/**
* Decrypt a string encrypted with the encrypt function
* @param {string} encrypted - Encrypted text
* @param {string} key - Encryption key
* @returns {string} - Decrypted text
*/
export function decrypt(encrypted, key) {
try {
const text = atob(encrypted); // Base64 decode
let result = '';
for (let i = 0; i < text.length; i++) {
result += String.fromCharCode(text.charCodeAt(i) ^ key.charCodeAt(i % key.length));
}
return result;
} catch (e) {
console.error('Decryption error:', e);
return '';
}
}
/**
* Get the title display name for a media category
* @param {string} category - Category key
* @returns {string} - Formatted category title
*/
export function getCategoryTitle(category) {
switch(category) {
case 'movies': return 'Movies';
case 'tvShows': return 'TV Shows';
case 'music': return 'Music';
case 'books': return 'Books';
case 'magazines': return 'Magazines';
case 'software': return 'Software';
default: return category.charAt(0).toUpperCase() + category.slice(1);
}
}
/**
* Wait for an element to exist in the DOM
* @param {string} selector - CSS selector
* @param {number} timeout - Timeout in milliseconds
* @returns {Promise<Element>} - Element when found
*/
export function waitForElement(selector, timeout = 5000) {
return new Promise((resolve, reject) => {
const element = document.querySelector(selector);
if (element) return resolve(element);
const observer = new MutationObserver((mutations) => {
const element = document.querySelector(selector);
if (element) {
observer.disconnect();
resolve(element);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
setTimeout(() => {
observer.disconnect();
reject(new Error(`Element ${selector} not found within ${timeout}ms`));
}, timeout);
});
}
/**
* Create a notification message
* @param {string} message - Message to display
* @param {string} type - Type of notification (success, danger, warning, info)
* @param {number} duration - Display duration in milliseconds
*/
export function showNotification(message, type = 'info', duration = 5000) {
// Create notifications container if it doesn't exist
let container = document.getElementById('notifications-container');
if (!container) {
container = document.createElement('div');
container.id = 'notifications-container';
container.style.position = 'fixed';
container.style.top = '20px';
container.style.right = '20px';
container.style.zIndex = '1060';
document.body.appendChild(container);
}
// Create notification element
const notification = document.createElement('div');
notification.className = `alert alert-${type}`;
notification.innerHTML = message;
notification.style.opacity = '0';
notification.style.transform = 'translateY(-20px)';
notification.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
container.appendChild(notification);
// Fade in
setTimeout(() => {
notification.style.opacity = '1';
notification.style.transform = 'translateY(0)';
}, 10);
// Auto-remove after the specified duration
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateY(-20px)';
setTimeout(() => {
notification.remove();
}, 300);
}, duration);
}
/**
* Create authorization headers for API requests
* @param {string} token - Auth token
* @returns {Object} - Headers object
*/
export function createAuthHeaders(token) {
return token ? {
'Authorization': `Bearer ${token}`
} : {};
}
/**
* Validate common input types
*/
export const validator = {
/**
* Validate email
* @param {string} email - Email to validate
* @returns {boolean} - True if valid
*/
isEmail: (email) => {
const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
},
/**
* Validate URL
* @param {string} url - URL to validate
* @returns {boolean} - True if valid
*/
isUrl: (url) => {
try {
new URL(url);
return true;
} catch (e) {
return false;
}
},
/**
* Validate number
* @param {string|number} value - Value to validate
* @returns {boolean} - True if valid
*/
isNumeric: (value) => {
return !isNaN(parseFloat(value)) && isFinite(value);
},
/**
* Validate field is not empty
* @param {string} value - Value to validate
* @returns {boolean} - True if not empty
*/
isRequired: (value) => {
return value !== null && value !== undefined && value !== '';
},
/**
* Validate file path
* @param {string} path - Path to validate
* @returns {boolean} - True if valid
*/
isValidPath: (path) => {
// Simple path validation - should start with / for Unix-like systems
return /^(\/[\w.-]+)+\/?$/.test(path);
},
/**
* Validate password complexity
* @param {string} password - Password to validate
* @returns {boolean} - True if valid
*/
isStrongPassword: (password) => {
return password && password.length >= 8 &&
/[A-Z]/.test(password) &&
/[a-z]/.test(password) &&
/[0-9]/.test(password);
},
/**
* Validate a value is in range
* @param {number} value - Value to validate
* @param {number} min - Minimum value
* @param {number} max - Maximum value
* @returns {boolean} - True if in range
*/
isInRange: (value, min, max) => {
const num = parseFloat(value);
return !isNaN(num) && num >= min && num <= max;
}
};