/** * 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 = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/' }; 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} - 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} 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 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; } };