mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-04-30 14:50:39 +00:00
3d14df6a11
Multi-language / Translations Support (#3696) - Complete internationalization system implemented - Support for 7 languages: Czech (cs), German (de), French (fr), Italian (it), Korean (ko), Chinese Simplified (zh), Chinese Traditional (zh_TW) - Language selector with localized flags and theming - Flash message translations - Multiple translation fixes and improvements across all languages - Language setting preserved across redirects Pluggable Content Fetchers (#3653) - New architecture for extensible content fetcher system - Allows custom fetcher implementations Image / Screenshot Comparison Processor (#3680) - New processor for visual change detection (disabled for this release) - Supporting CSS/JS infrastructure added UI Improvements Design & Layout - Auto-generated tag color schemes - Simplified login form styling - Removed hard-coded CSS, moved to SCSS variables - Tag UI cleanup and improvements - Automatic tab wrapper functionality - Menu refactoring for better organization - Cleanup of offset settings - Hide sticky tabs on narrow viewports - Improved responsive layout (#3702) User Experience - Modal alerts/confirmations on delete/clear operations (#3693, #3598, #3382) - Auto-add https:// to URLs in quickwatch form if not present - Better redirect handling on login (#3699) - 'Recheck all' now returns to correct group/tag (#3673) - Language set redirect keeps hash fragment - More friendly human-readable text throughout UI Performance & Reliability Scheduler & Processing - Soft delays instead of blocking time.sleep() calls (#3710) - More resilient handling of same UUID being processed (#3700) - Better Puppeteer timeout handling - Improved Puppeteer shutdown/cleanup (#3692) - Requests cleanup now properly async History & Rendering - Faster server-side "difference" rendering on History page (#3442) - Show ignored/triggered rows in history - API: Retry watch data if watch dict changed (more reliable) API Improvements - Watch get endpoint: retry mechanism for changed watch data - WatchHistoryDiff API endpoint includes extra format args (#3703) Testing Improvements - Replace time.sleep with wait_for_notification_endpoint_output (#3716) - Test for mode switching (#3701) - Test for #3720 added (#3725) - Extract-text difference test fixes - Improved dev workflow Bug Fixes - Notification error text output (#3672, #3669, #3280) - HTML validation fixes (#3704) - Template discovery path fixes - Notification debug log now uses system locale for dates/times - Puppeteer spelling mistake in log output - Recalculation on anchor change - Queue bubble update disabled temporarily Dependency Updates - beautifulsoup4 updated (#3724) - psutil 7.1.0 → 7.2.1 (#3723) - python-engineio ~=4.12.3 → ~=4.13.0 (#3707) - python-socketio ~=5.14.3 → ~=5.16.0 (#3706) - flask-socketio ~=5.5.1 → ~=5.6.0 (#3691) - brotli ~=1.1 → ~=1.2 (#3687) - lxml updated (#3590) - pytest ~=7.2 → ~=9.0 (#3676) - jsonschema ~=4.0 → ~=4.25 (#3618) - pluggy ~=1.5 → ~=1.6 (#3616) - cryptography 44.0.1 → 46.0.3 (security) (#3589) Documentation - README updated with viewport size setup information Development Infrastructure - Dev container only built on dev branch - Improved dev workflow tooling
276 lines
7.1 KiB
JavaScript
276 lines
7.1 KiB
JavaScript
/**
|
|
* Toast - Modern toast notification system
|
|
* Inspired by Toastify, Notyf, and React Hot Toast
|
|
*
|
|
* Usage:
|
|
* Toast.success('Operation completed!');
|
|
* Toast.error('Something went wrong');
|
|
* Toast.info('Here is some information');
|
|
* Toast.warning('Warning message');
|
|
* Toast.show('Custom message', { type: 'success', duration: 3000 });
|
|
*
|
|
* License: MIT
|
|
*/
|
|
|
|
(function(window) {
|
|
'use strict';
|
|
|
|
// Toast configuration
|
|
const defaultConfig = {
|
|
duration: 5000, // Auto-dismiss after 5 seconds (0 = no auto-dismiss)
|
|
position: 'top-center', // top-right, top-center, top-left, bottom-right, bottom-center, bottom-left
|
|
closeButton: true, // Show close button
|
|
progressBar: true, // Show progress bar
|
|
pauseOnHover: true, // Pause auto-dismiss on hover
|
|
maxToasts: 5, // Maximum toasts to show at once
|
|
offset: '20px', // Offset from edge
|
|
zIndex: 10000, // Z-index for toast container
|
|
};
|
|
|
|
let config = { ...defaultConfig };
|
|
let toastCount = 0;
|
|
let container = null;
|
|
|
|
/**
|
|
* Initialize toast system with custom config
|
|
*/
|
|
function init(userConfig = {}) {
|
|
config = { ...defaultConfig, ...userConfig };
|
|
createContainer();
|
|
}
|
|
|
|
/**
|
|
* Create toast container if it doesn't exist
|
|
*/
|
|
function createContainer() {
|
|
if (container) return;
|
|
|
|
container = document.createElement('div');
|
|
container.className = `toast-container toast-${config.position}`;
|
|
container.style.zIndex = config.zIndex;
|
|
document.body.appendChild(container);
|
|
}
|
|
|
|
/**
|
|
* Show a toast notification
|
|
*/
|
|
function show(message, options = {}) {
|
|
createContainer();
|
|
|
|
const toast = createToastElement(message, options);
|
|
|
|
// Limit number of toasts
|
|
const existingToasts = container.querySelectorAll('.toast');
|
|
if (existingToasts.length >= config.maxToasts) {
|
|
removeToast(existingToasts[0]);
|
|
}
|
|
|
|
// Add to container
|
|
container.appendChild(toast);
|
|
|
|
// Trigger animation
|
|
requestAnimationFrame(() => {
|
|
toast.classList.add('toast-show');
|
|
});
|
|
|
|
// Auto-dismiss
|
|
if (options.duration !== 0 && (options.duration || config.duration) > 0) {
|
|
setupAutoDismiss(toast, options.duration || config.duration);
|
|
}
|
|
|
|
return {
|
|
dismiss: () => removeToast(toast)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create toast DOM element
|
|
*/
|
|
function createToastElement(message, options) {
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast toast-${options.type || 'default'}`;
|
|
toast.setAttribute('role', 'alert');
|
|
toast.setAttribute('aria-live', 'polite');
|
|
|
|
// Icon
|
|
const icon = createIcon(options.type || 'default');
|
|
if (icon) {
|
|
toast.appendChild(icon);
|
|
}
|
|
|
|
// Message
|
|
const messageEl = document.createElement('div');
|
|
messageEl.className = 'toast-message';
|
|
messageEl.textContent = message;
|
|
toast.appendChild(messageEl);
|
|
|
|
// Close button
|
|
if (options.closeButton !== false && config.closeButton) {
|
|
const closeBtn = document.createElement('button');
|
|
closeBtn.className = 'toast-close';
|
|
closeBtn.innerHTML = '×';
|
|
closeBtn.setAttribute('aria-label', 'Close');
|
|
closeBtn.onclick = () => removeToast(toast);
|
|
toast.appendChild(closeBtn);
|
|
}
|
|
|
|
// Progress bar
|
|
if (options.progressBar !== false && config.progressBar && (options.duration || config.duration) > 0) {
|
|
const progressBar = document.createElement('div');
|
|
progressBar.className = 'toast-progress';
|
|
toast.appendChild(progressBar);
|
|
toast._progressBar = progressBar;
|
|
}
|
|
|
|
return toast;
|
|
}
|
|
|
|
/**
|
|
* Create icon based on toast type
|
|
*/
|
|
function createIcon(type) {
|
|
const iconEl = document.createElement('div');
|
|
iconEl.className = 'toast-icon';
|
|
|
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
svg.setAttribute('viewBox', '0 0 24 24');
|
|
svg.setAttribute('fill', 'none');
|
|
svg.setAttribute('stroke', 'currentColor');
|
|
svg.setAttribute('stroke-width', '2');
|
|
|
|
let path = '';
|
|
switch (type) {
|
|
case 'success':
|
|
path = 'M20 6L9 17l-5-5';
|
|
break;
|
|
case 'error':
|
|
path = 'M18 6L6 18M6 6l12 12';
|
|
break;
|
|
case 'warning':
|
|
path = 'M12 9v4m0 4h.01M12 2a10 10 0 100 20 10 10 0 000-20z';
|
|
svg.setAttribute('stroke-width', '1.5');
|
|
break;
|
|
case 'info':
|
|
path = 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z';
|
|
svg.setAttribute('stroke-width', '1.5');
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
const pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
pathEl.setAttribute('d', path);
|
|
pathEl.setAttribute('stroke-linecap', 'round');
|
|
pathEl.setAttribute('stroke-linejoin', 'round');
|
|
svg.appendChild(pathEl);
|
|
iconEl.appendChild(svg);
|
|
|
|
return iconEl;
|
|
}
|
|
|
|
/**
|
|
* Setup auto-dismiss with progress bar
|
|
*/
|
|
function setupAutoDismiss(toast, duration) {
|
|
let startTime = Date.now();
|
|
let remainingTime = duration;
|
|
let isPaused = false;
|
|
let animationFrame;
|
|
|
|
function updateProgress() {
|
|
if (isPaused) return;
|
|
|
|
const elapsed = Date.now() - startTime;
|
|
const progress = Math.min(elapsed / duration, 1);
|
|
|
|
if (toast._progressBar) {
|
|
toast._progressBar.style.transform = `scaleX(${1 - progress})`;
|
|
}
|
|
|
|
if (progress >= 1) {
|
|
removeToast(toast);
|
|
} else {
|
|
animationFrame = requestAnimationFrame(updateProgress);
|
|
}
|
|
}
|
|
|
|
// Pause on hover
|
|
if (config.pauseOnHover) {
|
|
toast.addEventListener('mouseenter', () => {
|
|
isPaused = true;
|
|
remainingTime = duration - (Date.now() - startTime);
|
|
cancelAnimationFrame(animationFrame);
|
|
});
|
|
|
|
toast.addEventListener('mouseleave', () => {
|
|
isPaused = false;
|
|
startTime = Date.now();
|
|
duration = remainingTime;
|
|
animationFrame = requestAnimationFrame(updateProgress);
|
|
});
|
|
}
|
|
|
|
animationFrame = requestAnimationFrame(updateProgress);
|
|
}
|
|
|
|
/**
|
|
* Remove toast with animation
|
|
*/
|
|
function removeToast(toast) {
|
|
if (!toast || !toast.parentElement) return;
|
|
|
|
toast.classList.add('toast-hide');
|
|
|
|
// Remove after animation
|
|
setTimeout(() => {
|
|
if (toast.parentElement) {
|
|
toast.parentElement.removeChild(toast);
|
|
}
|
|
}, 300);
|
|
}
|
|
|
|
// Convenience methods
|
|
function success(message, options = {}) {
|
|
return show(message, { ...options, type: 'success' });
|
|
}
|
|
|
|
function error(message, options = {}) {
|
|
return show(message, { ...options, type: 'error' });
|
|
}
|
|
|
|
function warning(message, options = {}) {
|
|
return show(message, { ...options, type: 'warning' });
|
|
}
|
|
|
|
function info(message, options = {}) {
|
|
return show(message, { ...options, type: 'info' });
|
|
}
|
|
|
|
/**
|
|
* Clear all toasts
|
|
*/
|
|
function clear() {
|
|
if (!container) return;
|
|
const toasts = container.querySelectorAll('.toast');
|
|
toasts.forEach(removeToast);
|
|
}
|
|
|
|
// Public API
|
|
window.Toast = {
|
|
init,
|
|
show,
|
|
success,
|
|
error,
|
|
warning,
|
|
info,
|
|
clear,
|
|
version: '1.0.0'
|
|
};
|
|
|
|
// Auto-initialize
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
init();
|
|
});
|
|
|
|
})(window);
|