2025-03-04 22:28:11 +00:00

637 lines
17 KiB
JavaScript

/**
* 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;
}
};