mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-01-15 03:30:23 +00:00
Compare commits
1 Commits
layout-css
...
resilient-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe45bfc27a |
@@ -204,7 +204,7 @@ class fetcher(Fetcher):
|
||||
import re
|
||||
self.delete_browser_steps_screenshots()
|
||||
|
||||
n = int(os.getenv("WEBDRIVER_DELAY_BEFORE_CONTENT_READY", 12)) + self.render_extract_delay
|
||||
n = int(os.getenv("WEBDRIVER_DELAY_BEFORE_CONTENT_READY", 5)) + self.render_extract_delay
|
||||
extra_wait = min(n, 15)
|
||||
|
||||
logger.debug(f"Extra wait set to {extra_wait}s, requested was {n}s.")
|
||||
@@ -288,27 +288,28 @@ class fetcher(Fetcher):
|
||||
# Enable Network domain to detect when first bytes arrive
|
||||
await self.page._client.send('Network.enable')
|
||||
|
||||
# Now set up the frame navigation handlers
|
||||
async def handle_frame_navigation(event=None):
|
||||
# Wait n seconds after the frameStartedLoading, not from any frameStartedLoading/frameStartedNavigating
|
||||
logger.debug(f"Frame navigated: {event}")
|
||||
w = extra_wait - 2 if extra_wait > 4 else 2
|
||||
logger.debug(f"Waiting {w} seconds before calling Page.stopLoading...")
|
||||
await asyncio.sleep(w)
|
||||
logger.debug("Issuing stopLoading command...")
|
||||
await self.page._client.send('Page.stopLoading')
|
||||
logger.debug("stopLoading command sent!")
|
||||
|
||||
async def setup_frame_handlers_on_first_response(event):
|
||||
# Only trigger for the main document response
|
||||
if event.get('type') == 'Document':
|
||||
logger.debug("First response received, setting up frame handlers for forced page stop load.")
|
||||
|
||||
# De-register this listener - we only need it once
|
||||
self.page._client.remove_listener('Network.responseReceived', setup_frame_handlers_on_first_response)
|
||||
|
||||
# Now set up the frame navigation handlers
|
||||
async def handle_frame_navigation(event):
|
||||
# Wait n seconds after the frameStartedLoading, not from any frameStartedLoading/frameStartedNavigating
|
||||
logger.debug(f"Frame navigated: {event}")
|
||||
w = extra_wait - 2 if extra_wait > 4 else 2
|
||||
logger.debug(f"Waiting {w} seconds before calling Page.stopLoading...")
|
||||
await asyncio.sleep(w)
|
||||
logger.debug("Issuing stopLoading command...")
|
||||
await self.page._client.send('Page.stopLoading')
|
||||
logger.debug("stopLoading command sent!")
|
||||
|
||||
self.page._client.on('Page.frameStartedNavigating', lambda e: asyncio.create_task(handle_frame_navigation(e)))
|
||||
self.page._client.on('Page.frameStartedLoading', lambda e: asyncio.create_task(handle_frame_navigation(e)))
|
||||
self.page._client.on('Page.frameStoppedLoading', lambda e: logger.debug(f"Frame stopped loading: {e}"))
|
||||
logger.debug("First response received, setting up frame handlers for forced page stop load DONE SETUP")
|
||||
# De-register this listener - we only need it once
|
||||
self.page._client.remove_listener('Network.responseReceived', setup_frame_handlers_on_first_response)
|
||||
|
||||
# Listen for first response to trigger frame handler setup
|
||||
self.page._client.on('Network.responseReceived', setup_frame_handlers_on_first_response)
|
||||
@@ -317,11 +318,8 @@ class fetcher(Fetcher):
|
||||
attempt=0
|
||||
while not response:
|
||||
logger.debug(f"Attempting page fetch {url} attempt {attempt}")
|
||||
asyncio.create_task(handle_frame_navigation())
|
||||
response = await self.page.goto(url, timeout=0)
|
||||
await asyncio.sleep(1 + extra_wait)
|
||||
await self.page._client.send('Page.stopLoading')
|
||||
|
||||
if response:
|
||||
break
|
||||
if not response:
|
||||
|
||||
@@ -26,7 +26,6 @@ from flask import (
|
||||
session,
|
||||
url_for,
|
||||
)
|
||||
from urllib.parse import urlparse
|
||||
from flask_compress import Compress as FlaskCompress
|
||||
from flask_login import current_user
|
||||
from flask_restful import abort, Api
|
||||
@@ -351,13 +350,6 @@ def changedetection_app(config=None, datastore_o=None):
|
||||
global datastore, socketio_server
|
||||
datastore = datastore_o
|
||||
|
||||
# Import and create a wrapper for is_safe_url that has access to app
|
||||
from changedetectionio.is_safe_url import is_safe_url as _is_safe_url
|
||||
|
||||
def is_safe_url(target):
|
||||
"""Wrapper for is_safe_url that passes the app instance"""
|
||||
return _is_safe_url(target, app)
|
||||
|
||||
# so far just for read-only via tests, but this will be moved eventually to be the main source
|
||||
# (instead of the global var)
|
||||
app.config['DATASTORE'] = datastore_o
|
||||
@@ -479,21 +471,11 @@ def changedetection_app(config=None, datastore_o=None):
|
||||
|
||||
@login_manager.unauthorized_handler
|
||||
def unauthorized_handler():
|
||||
# Pass the current request path so users are redirected back after login
|
||||
return redirect(url_for('login', redirect=request.path))
|
||||
return redirect(url_for('login', next=url_for('watchlist.index')))
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
flask_login.logout_user()
|
||||
|
||||
# Check if there's a redirect parameter to return to after re-login
|
||||
redirect_url = request.args.get('redirect')
|
||||
|
||||
# If redirect is provided and safe, pass it to login page
|
||||
if redirect_url and is_safe_url(redirect_url):
|
||||
return redirect(url_for('login', redirect=redirect_url))
|
||||
|
||||
# Otherwise just go to watchlist
|
||||
return redirect(url_for('watchlist.index'))
|
||||
|
||||
@app.route('/set-language/<locale>')
|
||||
@@ -505,36 +487,20 @@ def changedetection_app(config=None, datastore_o=None):
|
||||
else:
|
||||
logger.error(f"Invalid locale {locale}, available: {language_codes}")
|
||||
|
||||
# Check if there's a redirect parameter to return to the same page
|
||||
redirect_url = request.args.get('redirect')
|
||||
|
||||
# If redirect is provided and safe, use it
|
||||
if redirect_url and is_safe_url(redirect_url):
|
||||
return redirect(redirect_url)
|
||||
|
||||
# Otherwise redirect to watchlist
|
||||
# Redirect back to the page they came from, or home
|
||||
return redirect(url_for('watchlist.index'))
|
||||
|
||||
# https://github.com/pallets/flask/blob/93dd1709d05a1cf0e886df6223377bdab3b077fb/examples/tutorial/flaskr/__init__.py#L39
|
||||
# You can divide up the stuff like this
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
# Extract and validate the redirect parameter
|
||||
redirect_url = request.args.get('redirect') or request.form.get('redirect')
|
||||
|
||||
# Validate the redirect URL - default to watchlist if invalid
|
||||
if redirect_url and is_safe_url(redirect_url):
|
||||
validated_redirect = redirect_url
|
||||
else:
|
||||
validated_redirect = url_for('watchlist.index')
|
||||
|
||||
if request.method == 'GET':
|
||||
if flask_login.current_user.is_authenticated:
|
||||
# Already logged in - redirect immediately to the target
|
||||
flash(gettext("Already logged in"))
|
||||
return redirect(validated_redirect)
|
||||
return redirect(url_for("watchlist.index"))
|
||||
flash(gettext("You must be logged in, please log in."), 'error')
|
||||
output = render_template("login.html", redirect_url=validated_redirect)
|
||||
output = render_template("login.html")
|
||||
return output
|
||||
|
||||
user = User()
|
||||
@@ -544,13 +510,23 @@ def changedetection_app(config=None, datastore_o=None):
|
||||
|
||||
if (user.check_password(password)):
|
||||
flask_login.login_user(user, remember=True)
|
||||
# Redirect to the validated URL after successful login
|
||||
return redirect(validated_redirect)
|
||||
|
||||
# For now there's nothing else interesting here other than the index/list page
|
||||
# It's more reliable and safe to ignore the 'next' redirect
|
||||
# When we used...
|
||||
# next = request.args.get('next')
|
||||
# return redirect(next or url_for('watchlist.index'))
|
||||
# We would sometimes get login loop errors on sites hosted in sub-paths
|
||||
|
||||
# note for the future:
|
||||
# if not is_safe_valid_url(next):
|
||||
# return flask.abort(400)
|
||||
return redirect(url_for('watchlist.index'))
|
||||
|
||||
else:
|
||||
flash(gettext('Incorrect password'), 'error')
|
||||
|
||||
return redirect(url_for('login', redirect=redirect_url if redirect_url else None))
|
||||
return redirect(url_for('login'))
|
||||
|
||||
@app.before_request
|
||||
def before_request_handle_cookie_x_settings():
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
"""
|
||||
URL redirect validation module for preventing open redirect vulnerabilities.
|
||||
|
||||
This module provides functionality to safely validate redirect URLs, ensuring they:
|
||||
1. Point to internal routes only (no external redirects)
|
||||
2. Are properly normalized (preventing browser parsing differences)
|
||||
3. Match registered Flask routes (no fake/non-existent pages)
|
||||
4. Are fully logged for security monitoring
|
||||
|
||||
References:
|
||||
- https://flask-login.readthedocs.io/ (safe redirect patterns)
|
||||
- https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-v-user-logins
|
||||
- https://www.pythonkitchen.com/how-prevent-open-redirect-vulnerab-flask/
|
||||
"""
|
||||
|
||||
from urllib.parse import urlparse, urljoin
|
||||
from flask import request
|
||||
from loguru import logger
|
||||
|
||||
|
||||
def is_safe_url(target, app):
|
||||
"""
|
||||
Validate that a redirect URL is safe to prevent open redirect vulnerabilities.
|
||||
|
||||
This follows Flask/Werkzeug best practices by ensuring the redirect URL:
|
||||
1. Is a relative path starting with exactly one '/'
|
||||
2. Does not start with '//' (double-slash attack)
|
||||
3. Has no external protocol handlers
|
||||
4. Points to a valid registered route in the application
|
||||
5. Is properly normalized to prevent browser parsing differences
|
||||
|
||||
Args:
|
||||
target: The URL to validate (e.g., '/settings', '/login#top')
|
||||
app: The Flask application instance (needed for route validation)
|
||||
|
||||
Returns:
|
||||
bool: True if the URL is safe for redirection, False otherwise
|
||||
|
||||
Examples:
|
||||
>>> is_safe_url('/settings', app)
|
||||
True
|
||||
>>> is_safe_url('//evil.com', app)
|
||||
False
|
||||
>>> is_safe_url('/settings#general', app)
|
||||
True
|
||||
>>> is_safe_url('/fake-page', app)
|
||||
False
|
||||
"""
|
||||
if not target:
|
||||
return False
|
||||
|
||||
# Normalize the URL to prevent browser parsing differences
|
||||
# Strip whitespace and replace backslashes (which some browsers interpret as forward slashes)
|
||||
target = target.strip()
|
||||
target = target.replace('\\', '/')
|
||||
|
||||
# First, check if it starts with // or more (double-slash attack)
|
||||
if target.startswith('//'):
|
||||
logger.warning(f"Blocked redirect attempt with double-slash: {target}")
|
||||
return False
|
||||
|
||||
# Parse the URL to check for scheme and netloc
|
||||
parsed = urlparse(target)
|
||||
|
||||
# Block any URL with a scheme (http://, https://, javascript:, etc.)
|
||||
if parsed.scheme:
|
||||
logger.warning(f"Blocked redirect attempt with scheme: {target}")
|
||||
return False
|
||||
|
||||
# Block any URL with a network location (netloc)
|
||||
# This catches patterns like //evil.com, user@host, etc.
|
||||
if parsed.netloc:
|
||||
logger.warning(f"Blocked redirect attempt with netloc: {target}")
|
||||
return False
|
||||
|
||||
# At this point, we have a relative URL with no scheme or netloc
|
||||
# Use urljoin to resolve it and verify it points to the same host
|
||||
ref_url = urlparse(request.host_url)
|
||||
test_url = urlparse(urljoin(request.host_url, target))
|
||||
|
||||
# Check: ensure the resolved URL has the same netloc as current host
|
||||
if not (test_url.scheme in ('http', 'https') and ref_url.netloc == test_url.netloc):
|
||||
logger.warning(f"Blocked redirect attempt with mismatched netloc: {target}")
|
||||
return False
|
||||
|
||||
# Additional validation: Check if the URL matches a registered route
|
||||
# This prevents redirects to non-existent pages or unintended endpoints
|
||||
try:
|
||||
# Get the path without query string and fragment
|
||||
# Fragments (like #general) are automatically stripped by urlparse
|
||||
path = parsed.path
|
||||
|
||||
# Create a URL adapter bound to the server name
|
||||
adapter = app.url_map.bind(ref_url.netloc)
|
||||
|
||||
# Try to match the path to a registered route
|
||||
# This will raise NotFound if the route doesn't exist
|
||||
endpoint, values = adapter.match(path, return_rule=False)
|
||||
|
||||
# Block redirects to static file endpoints - these are catch-all routes
|
||||
# that would match arbitrary paths, potentially allowing unintended redirects
|
||||
if endpoint in ('static_content', 'static', 'static_flags'):
|
||||
logger.warning(f"Blocked redirect to static endpoint: {target}")
|
||||
return False
|
||||
|
||||
# Successfully matched a valid route
|
||||
logger.debug(f"Validated safe redirect to endpoint '{endpoint}': {target}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
# Route doesn't exist or can't be matched
|
||||
logger.warning(f"Blocked redirect to non-existent route: {target} (error: {e})")
|
||||
return False
|
||||
@@ -1,99 +0,0 @@
|
||||
/**
|
||||
* Flask Toast Bridge
|
||||
* Automatically converts Flask flash messages to toast notifications
|
||||
*
|
||||
* Maps Flask message categories to toast types:
|
||||
* - 'message' or 'info' -> info toast
|
||||
* - 'success' -> success toast
|
||||
* - 'error' or 'danger' -> error toast
|
||||
* - 'warning' -> warning toast
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Find the Flask messages container
|
||||
const messagesContainer = document.querySelector('ul.messages');
|
||||
|
||||
if (!messagesContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all flash messages
|
||||
const messages = messagesContainer.querySelectorAll('li');
|
||||
|
||||
if (messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let toastIndex = 0;
|
||||
|
||||
// Convert each message to a toast (except errors)
|
||||
messages.forEach(function(messageEl) {
|
||||
const text = messageEl.textContent.trim();
|
||||
const category = getMessageCategory(messageEl);
|
||||
|
||||
// Skip error messages - they should stay in the page
|
||||
if (category === 'error') {
|
||||
return;
|
||||
}
|
||||
|
||||
const toastType = mapCategoryToToastType(category);
|
||||
|
||||
// Stagger toast appearance for multiple messages
|
||||
setTimeout(function() {
|
||||
Toast[toastType](text, {
|
||||
duration: 6000 // 6 seconds for Flask messages
|
||||
});
|
||||
}, toastIndex * 200); // 200ms delay between each toast
|
||||
|
||||
toastIndex++;
|
||||
|
||||
// Hide this specific message element (not errors)
|
||||
messageEl.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Extract message category from class names
|
||||
*/
|
||||
function getMessageCategory(messageEl) {
|
||||
const classes = messageEl.className.split(' ');
|
||||
|
||||
// Common Flask flash message categories
|
||||
const categoryMap = {
|
||||
'success': 'success',
|
||||
'error': 'error',
|
||||
'danger': 'error',
|
||||
'warning': 'warning',
|
||||
'info': 'info',
|
||||
'message': 'info',
|
||||
'notice': 'info'
|
||||
};
|
||||
|
||||
for (let className of classes) {
|
||||
if (categoryMap[className]) {
|
||||
return categoryMap[className];
|
||||
}
|
||||
}
|
||||
|
||||
// Default to info if no category found
|
||||
return 'info';
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Flask category to Toast type
|
||||
*/
|
||||
function mapCategoryToToastType(category) {
|
||||
const typeMap = {
|
||||
'success': 'success',
|
||||
'error': 'error',
|
||||
'warning': 'warning',
|
||||
'info': 'info'
|
||||
};
|
||||
|
||||
return typeMap[category] || 'info';
|
||||
}
|
||||
|
||||
})();
|
||||
@@ -1,69 +0,0 @@
|
||||
// Hamburger menu toggle functionality
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const hamburgerToggle = document.getElementById('hamburger-toggle');
|
||||
const mobileMenuDrawer = document.getElementById('mobile-menu-drawer');
|
||||
const mobileMenuOverlay = document.getElementById('mobile-menu-overlay');
|
||||
|
||||
if (!hamburgerToggle || !mobileMenuDrawer || !mobileMenuOverlay) {
|
||||
return;
|
||||
}
|
||||
|
||||
function openMenu() {
|
||||
hamburgerToggle.classList.add('active');
|
||||
mobileMenuDrawer.classList.add('active');
|
||||
mobileMenuOverlay.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
hamburgerToggle.classList.remove('active');
|
||||
mobileMenuDrawer.classList.remove('active');
|
||||
mobileMenuOverlay.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
if (mobileMenuDrawer.classList.contains('active')) {
|
||||
closeMenu();
|
||||
} else {
|
||||
openMenu();
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle menu on hamburger click
|
||||
hamburgerToggle.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
toggleMenu();
|
||||
});
|
||||
|
||||
// Close menu when clicking overlay
|
||||
mobileMenuOverlay.addEventListener('click', closeMenu);
|
||||
|
||||
// Close menu when clicking a menu item
|
||||
const menuItems = mobileMenuDrawer.querySelectorAll('.mobile-menu-items a');
|
||||
menuItems.forEach(function(item) {
|
||||
item.addEventListener('click', closeMenu);
|
||||
});
|
||||
|
||||
// Close menu on escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && mobileMenuDrawer.classList.contains('active')) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Close menu when window is resized above mobile breakpoint
|
||||
let resizeTimer;
|
||||
window.addEventListener('resize', function() {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(function() {
|
||||
if (window.innerWidth > 768 && mobileMenuDrawer.classList.contains('active')) {
|
||||
closeMenu();
|
||||
}
|
||||
}, 250);
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -15,22 +15,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Open modal when language button is clicked
|
||||
languageButton.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Update all language links to include current hash in the redirect parameter
|
||||
const currentPath = window.location.pathname;
|
||||
const currentHash = window.location.hash;
|
||||
|
||||
if (currentHash) {
|
||||
const languageOptions = languageModal.querySelectorAll('.language-option');
|
||||
languageOptions.forEach(function(option) {
|
||||
const url = new URL(option.href, window.location.origin);
|
||||
// Update the redirect parameter to include the hash
|
||||
const redirectPath = currentPath + currentHash;
|
||||
url.searchParams.set('redirect', redirectPath);
|
||||
option.setAttribute('href', url.pathname + url.search + url.hash);
|
||||
});
|
||||
}
|
||||
|
||||
languageModal.showModal();
|
||||
});
|
||||
|
||||
|
||||
@@ -74,9 +74,6 @@ $(document).ready(function () {
|
||||
}
|
||||
|
||||
|
||||
// Cache DOM elements for performance
|
||||
const queueBubble = document.getElementById('queue-bubble');
|
||||
|
||||
// Only try to connect if authentication isn't required or user is authenticated
|
||||
// The 'is_authenticated' variable will be set in the template
|
||||
if (typeof is_authenticated !== 'undefined' ? is_authenticated : true) {
|
||||
@@ -118,39 +115,7 @@ $(document).ready(function () {
|
||||
|
||||
socket.on('queue_size', function (data) {
|
||||
console.log(`${data.event_timestamp} - Queue size update: ${data.q_length}`);
|
||||
|
||||
// Update queue bubble in action sidebar
|
||||
if (queueBubble) {
|
||||
const count = parseInt(data.q_length) || 0;
|
||||
const oldCount = parseInt(queueBubble.getAttribute('data-count')) || 0;
|
||||
|
||||
if (count > 0) {
|
||||
// Format number according to browser locale
|
||||
const formatter = new Intl.NumberFormat(navigator.language);
|
||||
queueBubble.textContent = formatter.format(count);
|
||||
queueBubble.setAttribute('data-count', count);
|
||||
queueBubble.classList.add('visible');
|
||||
|
||||
// Add large-number class for numbers > 999
|
||||
if (count > 999) {
|
||||
queueBubble.classList.add('large-number');
|
||||
} else {
|
||||
queueBubble.classList.remove('large-number');
|
||||
}
|
||||
|
||||
// Pulse animation if count changed
|
||||
if (count !== oldCount) {
|
||||
queueBubble.classList.remove('pulse');
|
||||
// Force reflow to restart animation
|
||||
void queueBubble.offsetWidth;
|
||||
queueBubble.classList.add('pulse');
|
||||
}
|
||||
} else {
|
||||
// Hide bubble when queue is empty
|
||||
queueBubble.classList.remove('visible', 'pulse', 'large-number');
|
||||
queueBubble.setAttribute('data-count', '0');
|
||||
}
|
||||
}
|
||||
// Update queue size display if implemented in the UI
|
||||
})
|
||||
|
||||
// Listen for operation results
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
// Search modal functionality
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const searchModal = document.getElementById('search-modal');
|
||||
const openSearchButton = document.getElementById('open-search-modal');
|
||||
const closeSearchButton = document.getElementById('close-search-modal');
|
||||
const searchForm = document.getElementById('search-form');
|
||||
const searchInput = document.getElementById('search-modal-input');
|
||||
|
||||
if (!searchModal || !openSearchButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Open modal
|
||||
function openSearchModal() {
|
||||
searchModal.showModal();
|
||||
// Focus the input after a small delay to ensure modal is rendered
|
||||
setTimeout(function() {
|
||||
if (searchInput) {
|
||||
searchInput.focus();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Close modal
|
||||
function closeSearchModal() {
|
||||
searchModal.close();
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Open search modal on button click
|
||||
openSearchButton.addEventListener('click', openSearchModal);
|
||||
|
||||
// Close modal on cancel button
|
||||
if (closeSearchButton) {
|
||||
closeSearchButton.addEventListener('click', closeSearchModal);
|
||||
}
|
||||
|
||||
// Close modal on escape key (native behavior for dialog)
|
||||
searchModal.addEventListener('cancel', function(e) {
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal when clicking the backdrop
|
||||
searchModal.addEventListener('click', function(e) {
|
||||
const rect = searchModal.getBoundingClientRect();
|
||||
const isInDialog = (
|
||||
rect.top <= e.clientY &&
|
||||
e.clientY <= rect.top + rect.height &&
|
||||
rect.left <= e.clientX &&
|
||||
e.clientX <= rect.left + rect.width
|
||||
);
|
||||
if (!isInDialog) {
|
||||
closeSearchModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle Alt+S keyboard shortcut
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.altKey && e.key.toLowerCase() === 's') {
|
||||
e.preventDefault();
|
||||
openSearchModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle form submission
|
||||
if (searchForm) {
|
||||
searchForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Get form data
|
||||
const formData = new FormData(searchForm);
|
||||
const searchQuery = formData.get('q');
|
||||
const tags = formData.get('tags');
|
||||
|
||||
// Build URL
|
||||
const params = new URLSearchParams();
|
||||
if (searchQuery) {
|
||||
params.append('q', searchQuery);
|
||||
}
|
||||
if (tags) {
|
||||
params.append('tags', tags);
|
||||
}
|
||||
|
||||
// Navigate to search results
|
||||
window.location.href = '?' + params.toString();
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -1,12 +1,11 @@
|
||||
// Rewrite this is a plugin.. is all this JS really 'worth it?'
|
||||
|
||||
window.addEventListener('hashchange', function () {
|
||||
// Only remove active from tab elements, not menu items
|
||||
var tabs = document.querySelectorAll('.tabs li.active');
|
||||
tabs.forEach(function(tab) {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
document.body.classList.remove('full-width');
|
||||
var tabs = document.getElementsByClassName('active');
|
||||
while (tabs[0]) {
|
||||
tabs[0].classList.remove('active');
|
||||
document.body.classList.remove('full-width');
|
||||
}
|
||||
set_active_tab();
|
||||
}, false);
|
||||
|
||||
@@ -23,9 +22,9 @@ if (!has_errors.length) {
|
||||
|
||||
function set_active_tab() {
|
||||
document.body.classList.remove('full-width');
|
||||
var tab = document.querySelectorAll(".tabs a[href='" + location.hash + "']");
|
||||
var tab = document.querySelectorAll("a[href='" + location.hash + "']");
|
||||
if (tab.length) {
|
||||
tab[0].parentElement.classList.add("active");
|
||||
tab[0].parentElement.className = "active";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,275 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
15
changedetectionio/static/js/toastify.min.css
vendored
15
changedetectionio/static/js/toastify.min.css
vendored
@@ -1,15 +0,0 @@
|
||||
/**
|
||||
* Minified by jsDelivr using clean-css v5.3.3.
|
||||
* Original file: /npm/toastify-js@1.12.0/src/toastify.css
|
||||
*
|
||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||
*/
|
||||
/*!
|
||||
* Toastify js 1.12.0
|
||||
* https://github.com/apvarun/toastify-js
|
||||
* @license MIT licensed
|
||||
*
|
||||
* Copyright (C) 2018 Varun A P
|
||||
*/
|
||||
.toastify{padding:12px 20px;color:#fff;display:inline-block;box-shadow:0 3px 6px -1px rgba(0,0,0,.12),0 10px 36px -4px rgba(77,96,232,.3);background:-webkit-linear-gradient(315deg,#73a5ff,#5477f5);background:linear-gradient(135deg,#73a5ff,#5477f5);position:fixed;opacity:0;transition:all .4s cubic-bezier(.215, .61, .355, 1);border-radius:2px;cursor:pointer;text-decoration:none;max-width:calc(50% - 20px);z-index:2147483647}.toastify.on{opacity:1}.toast-close{background:0 0;border:0;color:#fff;cursor:pointer;font-family:inherit;font-size:1em;opacity:.4;padding:0 5px}.toastify-right{right:15px}.toastify-left{left:15px}.toastify-top{top:-150px}.toastify-bottom{bottom:-150px}.toastify-rounded{border-radius:25px}.toastify-avatar{width:1.5em;height:1.5em;margin:-7px 5px;border-radius:2px}.toastify-center{margin-left:auto;margin-right:auto;left:0;right:0;max-width:fit-content;max-width:-moz-fit-content}@media only screen and (max-width:360px){.toastify-left,.toastify-right{margin-left:auto;margin-right:auto;left:0;right:0;max-width:fit-content}}
|
||||
/*# sourceMappingURL=/sm/cb4335d1b03e933ed85cb59fffa60cf51f07567ed09831438c60f59afd166464.map */
|
||||
15
changedetectionio/static/js/toastify.min.js
vendored
15
changedetectionio/static/js/toastify.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,115 +0,0 @@
|
||||
// Action Sidebar - Minimal navigation icons with light grey aesthetic
|
||||
|
||||
.content-wrapper {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.action-sidebar {
|
||||
position: sticky;
|
||||
top: 100px;
|
||||
flex-shrink: 0;
|
||||
width: 80px;
|
||||
height: fit-content;
|
||||
background: transparent;
|
||||
padding: 1.5rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
position: relative;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
padding: 1rem 0.5rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.action-sidebar-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.75rem 0.5rem;
|
||||
min-width: 64px;
|
||||
text-decoration: none;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
|
||||
.action-icon {
|
||||
stroke: #fff;
|
||||
stroke-width: 2.5;
|
||||
}
|
||||
|
||||
.action-label {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
stroke: #fff;
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
transition: stroke 0.2s ease;
|
||||
}
|
||||
|
||||
.action-label {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
color: #fff;
|
||||
transition: color 0.2s ease;
|
||||
max-width: 60px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.content-main {
|
||||
flex: 0 1 auto;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// Dark mode adjustments
|
||||
html[data-darkmode=true] {
|
||||
.action-icon {
|
||||
/* stroke: #666;*/
|
||||
}
|
||||
|
||||
.action-label {
|
||||
/* color: #666;*/
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
// Hamburger Menu for Mobile Navigation
|
||||
|
||||
.hamburger-menu {
|
||||
display: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
z-index: 10001;
|
||||
position: relative;
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.hamburger-icon {
|
||||
width: 24px;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
height: 3px;
|
||||
width: 100%;
|
||||
background: var(--color-text);
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
transform-origin: center;
|
||||
}
|
||||
}
|
||||
|
||||
.hamburger-menu.active {
|
||||
.hamburger-icon span:nth-child(1) {
|
||||
transform: translateY(8.5px) rotate(45deg);
|
||||
}
|
||||
|
||||
.hamburger-icon span:nth-child(2) {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
|
||||
.hamburger-icon span:nth-child(3) {
|
||||
transform: translateY(-8.5px) rotate(-45deg);
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile menu overlay
|
||||
.mobile-menu-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 9999;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
&.active {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile menu drawer
|
||||
.mobile-menu-drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -280px;
|
||||
width: 280px;
|
||||
height: 100%;
|
||||
background: var(--color-background);
|
||||
opacity: 1;
|
||||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
|
||||
z-index: 10000;
|
||||
transition: right 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
overflow-y: auto;
|
||||
padding-top: 60px;
|
||||
|
||||
&.active {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.mobile-menu-items {
|
||||
list-style: none;
|
||||
padding: 1rem 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid var(--color-border-table-cell);
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: 1rem 1.5rem;
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-background-menu-link-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Logo styling
|
||||
.logo-cdio {
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
|
||||
.logo-cd {
|
||||
color: var(--color-grey-500);
|
||||
}
|
||||
|
||||
.logo-io {
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
// Always visible items container
|
||||
.menu-always-visible {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
// Hide regular menu items on mobile
|
||||
@media only screen and (max-width: 768px) {
|
||||
.menu-collapsible {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.pure-menu-horizontal {
|
||||
overflow-x: visible !important;
|
||||
}
|
||||
|
||||
#nav-menu {
|
||||
overflow-x: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Desktop - hide mobile menu elements
|
||||
@media only screen and (min-width: 769px) {
|
||||
.hamburger-menu,
|
||||
.mobile-menu-drawer,
|
||||
.mobile-menu-overlay {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-darkmode=true] {
|
||||
.mobile-menu-drawer {
|
||||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
// Modern Login Form - Friendly and Welcoming Design
|
||||
|
||||
.login-form {
|
||||
min-height: 52vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 1rem;
|
||||
|
||||
.inner {
|
||||
background: var(--color-background);
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 10px 40px rgba(0, 0, 0, 0.08),
|
||||
0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
padding: 3rem 2.5rem;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
|
||||
// Subtle accent line at top
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--color-link) 0%,
|
||||
var(--color-menu-accent) 100%
|
||||
);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 15px 50px rgba(0, 0, 0, 0.12),
|
||||
0 5px 15px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pure-control-group {
|
||||
margin-bottom: 1.75rem;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 0.875rem 1rem;
|
||||
border: 2px solid var(--color-grey-800);
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
background: var(--color-background-input);
|
||||
color: var(--color-text-input);
|
||||
transition: all 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-link);
|
||||
box-shadow: 0 0 0 3px rgba(27, 152, 248, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-text-input-placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
width: 100%;
|
||||
padding: 0.875rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: var(--color-background-button-primary);
|
||||
color: var(--color-text-button);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(27, 152, 248, 0.2);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(27, 152, 248, 0.3);
|
||||
background: #0066cc;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 4px rgba(27, 152, 248, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Messages styling for login page
|
||||
.content-main > ul.messages {
|
||||
position: fixed;
|
||||
top: 120px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
z-index: 1000;
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
|
||||
li {
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
animation: slideDown 0.3s ease-out;
|
||||
border: 2px solid transparent;
|
||||
|
||||
&.error {
|
||||
background: #fee;
|
||||
border: 2px solid #ef4444;
|
||||
color: #991b1b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.success {
|
||||
background: #f0fdf4;
|
||||
border: 2px solid #10b981;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
&.info,
|
||||
&.message {
|
||||
background: #eff6ff;
|
||||
border: 2px solid #3b82f6;
|
||||
color: #1e40af;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode adjustments
|
||||
html[data-darkmode="true"] {
|
||||
.login-form {
|
||||
.inner {
|
||||
box-shadow:
|
||||
0 10px 40px rgba(0, 0, 0, 0.4),
|
||||
0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
|
||||
&:hover {
|
||||
box-shadow:
|
||||
0 15px 50px rgba(0, 0, 0, 0.5),
|
||||
0 5px 15px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
input[type="password"] {
|
||||
border-color: var(--color-grey-400);
|
||||
|
||||
&:focus {
|
||||
border-color: var(--color-link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-main > ul.messages {
|
||||
li {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
|
||||
&.error {
|
||||
background: #4a1d1d;
|
||||
border-color: #ef4444;
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
&.success {
|
||||
background: #1a3a2a;
|
||||
border-color: #10b981;
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
&.info,
|
||||
&.message {
|
||||
background: #1e3a5f;
|
||||
border-color: #3b82f6;
|
||||
color: #93c5fd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile adjustments
|
||||
@media only screen and (max-width: 768px) {
|
||||
.login-form {
|
||||
min-height: auto;
|
||||
padding: 1rem 0.5rem;
|
||||
padding-top: 5rem; // Space for error message
|
||||
|
||||
.inner {
|
||||
padding: 2rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-main > ul.messages {
|
||||
top: 70px; // Higher up on mobile to avoid overlap
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
transform: none;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
@@ -22,12 +22,4 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
// Active menu item styling
|
||||
&.active {
|
||||
.pure-menu-link {
|
||||
background-color: var(--color-background-menu-link-hover);
|
||||
color: var(--color-text-menu-link-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
// Reusable notification bubble for action sidebar icons
|
||||
|
||||
.action-sidebar-item {
|
||||
position: relative;
|
||||
|
||||
.notification-bubble {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
background: #ff4444;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
border-radius: 9px;
|
||||
padding: 0 2px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
pointer-events: none;
|
||||
transition: all 0.2s ease;
|
||||
display: none;
|
||||
|
||||
// Red bubble for errors/urgent
|
||||
&.red-bubble {
|
||||
background: #ff4444;
|
||||
}
|
||||
|
||||
// Blue bubble for informational
|
||||
&.blue-bubble {
|
||||
background: #4a9eff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// Pulse animation when value changes
|
||||
&.pulse {
|
||||
animation: bubblePulse 0.4s ease-out;
|
||||
}
|
||||
|
||||
// Large numbers get smaller font
|
||||
&.large-number {
|
||||
font-size: 8px;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bubblePulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode adjustments
|
||||
html[data-darkmode=true] {
|
||||
.notification-bubble {
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
// Search Modal Styles
|
||||
|
||||
#search-modal {
|
||||
.modal-body {
|
||||
padding: 2rem 1.5rem;
|
||||
|
||||
.pure-control-group {
|
||||
padding-bottom: 0;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
#search-modal-input {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0.6rem 0.8rem;
|
||||
font-size: 1rem;
|
||||
border: 1px solid var(--color-border-input);
|
||||
border-radius: 4px;
|
||||
background-color: var(--color-background-input);
|
||||
color: var(--color-text-input);
|
||||
box-shadow: inset 0 1px 3px var(--color-shadow-input);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-link);
|
||||
box-shadow: 0 0 0 3px rgba(27, 152, 248, 0.1);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-text-input-placeholder);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode adjustments
|
||||
html[data-darkmode=true] {
|
||||
#search-modal {
|
||||
#search-modal-input {
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 3px rgba(89, 189, 251, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
// Toast Notification System
|
||||
// Modern, animated toast notifications
|
||||
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
pointer-events: none;
|
||||
z-index: 10000;
|
||||
|
||||
// Positioning
|
||||
&.toast-top-right {
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
&.toast-top-center {
|
||||
top: 100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
&.toast-top-left {
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
&.toast-bottom-right {
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
&.toast-bottom-center {
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
&.toast-bottom-left {
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
padding: 1rem 1.25rem;
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
pointer-events: auto;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transform: translateY(-50px);
|
||||
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
font-family: inherit;
|
||||
|
||||
&.toast-show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&.toast-hide {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px) scale(0.95);
|
||||
}
|
||||
|
||||
// Toast types
|
||||
&.toast-success {
|
||||
border-left: 4px solid #10b981;
|
||||
|
||||
.toast-icon {
|
||||
color: #10b981;
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-error {
|
||||
border-left: 4px solid #ef4444;
|
||||
|
||||
.toast-icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-warning {
|
||||
border-left: 4px solid #f59e0b;
|
||||
|
||||
.toast-icon {
|
||||
color: #f59e0b;
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-info {
|
||||
border-left: 4px solid #3b82f6;
|
||||
|
||||
.toast-icon {
|
||||
color: #3b82f6;
|
||||
}
|
||||
}
|
||||
|
||||
&.toast-default {
|
||||
border-left: 4px solid var(--color-grey-500);
|
||||
}
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text);
|
||||
word-break: break-word;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.toast-close {
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: var(--color-grey-500);
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0;
|
||||
margin-left: 0.25rem;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-grey-800);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.toast-progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: currentColor;
|
||||
opacity: 0.3;
|
||||
transform-origin: left;
|
||||
transition: transform linear;
|
||||
}
|
||||
|
||||
// Dark mode adjustments
|
||||
html[data-darkmode=true] {
|
||||
.toast {
|
||||
background: var(--color-grey-300);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
background: var(--color-grey-400);
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile adjustments
|
||||
@media only screen and (max-width: 768px) {
|
||||
.toast-container {
|
||||
left: 10px !important;
|
||||
right: 10px !important;
|
||||
top: 10px !important;
|
||||
transform: none !important;
|
||||
align-items: stretch;
|
||||
|
||||
&.toast-bottom-right,
|
||||
&.toast-bottom-center,
|
||||
&.toast-bottom-left {
|
||||
top: auto !important;
|
||||
bottom: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.toast {
|
||||
min-width: auto;
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
transform: translateY(-100px);
|
||||
|
||||
&.toast-show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&.toast-hide {
|
||||
transform: translateY(-100px) scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Accessibility
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.toast {
|
||||
transition: opacity 0.2s ease;
|
||||
transform: none !important;
|
||||
|
||||
&.toast-show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.toast-hide {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,12 +23,6 @@
|
||||
@use "parts/widgets";
|
||||
@use "parts/diff_image";
|
||||
@use "parts/modal";
|
||||
@use "parts/action_sidebar";
|
||||
@use "parts/hamburger_menu";
|
||||
@use "parts/search_modal";
|
||||
@use "parts/notification_bubble";
|
||||
@use "parts/toast";
|
||||
@use "parts/login_form";
|
||||
|
||||
|
||||
body {
|
||||
@@ -77,6 +71,20 @@ a.github-link {
|
||||
}
|
||||
}
|
||||
|
||||
#search-q {
|
||||
opacity: 0;
|
||||
-webkit-transition: all .9s ease;
|
||||
-moz-transition: all .9s ease;
|
||||
transition: all .9s ease;
|
||||
width: 0;
|
||||
display: none;
|
||||
&.expanded {
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
#search-result-info {
|
||||
color: #fff;
|
||||
}
|
||||
@@ -816,7 +824,14 @@ $form-edge-padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
// Login form styles moved to parts/_login_form.scss
|
||||
.login-form {
|
||||
.inner {
|
||||
background: var(--color-background);
|
||||
;
|
||||
padding: $form-edge-padding;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-pane-inner {
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -22,7 +22,7 @@
|
||||
{% for idx, entry_errors in field.errors|enumerate %}
|
||||
{% if entry_errors is mapping and entry_errors %}
|
||||
{# Only show entries that have actual errors #}
|
||||
<li><strong>{{ _('Entry') }} {{ idx + 1 }}:</strong>
|
||||
<li><strong>Entry {{ idx + 1 }}:</strong>
|
||||
<ul>
|
||||
{% for field_name, messages in entry_errors.items() %}
|
||||
{% for message in messages %}
|
||||
@@ -150,7 +150,7 @@
|
||||
{% for subfield in fieldlist[0] %}
|
||||
<div class="fieldlist-header-cell">{{ subfield.label }}</div>
|
||||
{% endfor %}
|
||||
<div class="fieldlist-header-cell">{{ _('Actions') }}</div>
|
||||
<div class="fieldlist-header-cell">Actions</div>
|
||||
</div>
|
||||
<div class="fieldlist-body">
|
||||
{% for form_row in fieldlist %}
|
||||
@@ -169,9 +169,9 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="fieldlist-cell fieldlist-actions">
|
||||
<button type="button" class="addRuleRow" title="{{ _('Add a row/rule after') }}">+</button>
|
||||
<button type="button" class="removeRuleRow" title="{{ _('Remove this row/rule') }}">-</button>
|
||||
<button type="button" class="verifyRuleRow" title="{{ _('Verify this rule against current snapshot') }}">✓</button>
|
||||
<button type="button" class="addRuleRow" title="Add a row/rule after">+</button>
|
||||
<button type="button" class="removeRuleRow" title="Remove this row/rule">-</button>
|
||||
<button type="button" class="verifyRuleRow" title="Verify this rule against current snapshot">✓</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -181,8 +181,8 @@
|
||||
|
||||
|
||||
{% macro playwright_warning() %}
|
||||
<p><strong>{{ _('Error - This watch needs Chrome (with playwright/sockpuppetbrowser), but Chrome based fetching is not enabled.') }}</strong> {{ _('Alternatively try our') }} <a href="https://changedetection.io">{{ _('very affordable subscription based service which has all this setup for you') }}</a>.</p>
|
||||
<p>{{ _('You may need to') }} <a href="https://github.com/dgtlmoon/changedetection.io/blob/09ebc6ec6338545bdd694dc6eee57f2e9d2b8075/docker-compose.yml#L31">{{ _('Enable playwright environment variable') }}</a> {{ _('and uncomment the') }} <strong>sockpuppetbrowser</strong> {{ _('in the') }} <a href="https://github.com/dgtlmoon/changedetection.io/blob/master/docker-compose.yml">docker-compose.yml</a> {{ _('file') }}.</p>
|
||||
<p><strong>Error - This watch needs Chrome (with playwright/sockpuppetbrowser), but Chrome based fetching is not enabled.</strong> Alternatively try our <a href="https://changedetection.io">very affordable subscription based service which has all this setup for you</a>.</p>
|
||||
<p>You may need to <a href="https://github.com/dgtlmoon/changedetection.io/blob/09ebc6ec6338545bdd694dc6eee57f2e9d2b8075/docker-compose.yml#L31">Enable playwright environment variable</a> and uncomment the <strong>sockpuppetbrowser</strong> in the <a href="https://github.com/dgtlmoon/changedetection.io/blob/master/docker-compose.yml">docker-compose.yml</a> file.</p>
|
||||
<br>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -237,17 +237,18 @@
|
||||
<span id="scheduler-icon-label" style="">
|
||||
{{ render_checkbox_field(form.time_schedule_limit.enabled) }}
|
||||
<div class="pure-form-message-inline">
|
||||
{{ _('Set a hourly/week day schedule') }}
|
||||
Set a hourly/week day schedule
|
||||
</div>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
<br>
|
||||
<div id="schedule-day-limits-wrapper">
|
||||
<label>{{ _('Schedule time limits') }}</label><a data-template="business-hours"
|
||||
class="set-schedule pure-button button-secondary button-xsmall">{{ _('Business hours') }}</a>
|
||||
<a data-template="weekend" class="set-schedule pure-button button-secondary button-xsmall">{{ _('Weekends') }}</a>
|
||||
<a data-template="reset" class="set-schedule pure-button button-xsmall">{{ _('Reset') }}</a><br>
|
||||
<label>Schedule time limits</label><a data-template="business-hours"
|
||||
class="set-schedule pure-button button-secondary button-xsmall">Business
|
||||
hours</a>
|
||||
<a data-template="weekend" class="set-schedule pure-button button-secondary button-xsmall">Weekends</a>
|
||||
<a data-template="reset" class="set-schedule pure-button button-xsmall">Reset</a><br>
|
||||
<br>
|
||||
|
||||
<ul id="day-wrapper">
|
||||
@@ -256,8 +257,8 @@
|
||||
{{ render_nolabel_field(form.time_schedule_limit[day]) }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li id="timespan-warning">{{ _("Warning, one or more of your 'days' has a duration that would extend into the next day.") }}<br>
|
||||
{{ _('This could have unintended consequences.') }}</li>
|
||||
<li id="timespan-warning">Warning, one or more of your 'days' has a duration that would extend into the next day.<br>
|
||||
This could have unintended consequences.</li>
|
||||
<li id="timezone-info">
|
||||
{{ render_field(form.time_schedule_limit.timezone, placeholder=timezone_default_config) }} <span id="local-time-in-tz"></span>
|
||||
<datalist id="timezones" style="display: none;">
|
||||
@@ -267,12 +268,12 @@
|
||||
</ul>
|
||||
<br>
|
||||
<span class="pure-form-message-inline">
|
||||
<a href="https://changedetection.io/tutorials">{{ _('More help and examples about using the scheduler') }}</a>
|
||||
<a href="https://changedetection.io/tutorials">More help and examples about using the scheduler</a>
|
||||
</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="pure-form-message-inline">
|
||||
{{ _('Want to use a time schedule?') }} <a href="{{url_for('settings.settings_page')}}#timedate">{{ _('First confirm/save your Time Zone Settings') }}</a>
|
||||
Want to use a time schedule? <a href="{{url_for('settings.settings_page')}}#timedate">First confirm/save your Time Zone Settings</a>
|
||||
</span>
|
||||
<br>
|
||||
{% endif %}
|
||||
@@ -281,8 +282,8 @@
|
||||
|
||||
{% macro highlight_trigger_ignored_explainer() %}
|
||||
<p>
|
||||
<span title="{{ _('Triggers a change if this text appears, AND something changed in the document.') }}" style="background-color: var(--highlight-trigger-text-bg-color); color: #fff; padding: 4px; border-radius: 2px; margin-right: 4px;">{{ _('Triggered text') }}</span>
|
||||
<span title="{{ _('Ignored for calculating changes, but still shown.') }}" style="background-color: var(--highlight-ignored-text-bg-color); color: #fff; padding: 4px; border-radius: 2px; margin-right: 4px;">{{ _('Ignored text') }}</span>
|
||||
<span title="{{ _('No change-detection will occur because this text exists.') }}" style="background-color: var(--highlight-blocked-text-bg-color); color: #fff; padding: 4px; border-radius: 2px; margin-right: 4px;">{{ _('Blocked text') }}</span>
|
||||
<span title="Triggers a change if this text appears, AND something changed in the document." style="background-color: var(--highlight-trigger-text-bg-color); color: #fff; padding: 4px; border-radius: 2px; margin-right: 4px;">Triggered text</span>
|
||||
<span title="Ignored for calculating changes, but still shown." style="background-color: var(--highlight-ignored-text-bg-color); color: #fff; padding: 4px; border-radius: 2px; margin-right: 4px;">Ignored text</span>
|
||||
<span title="No change-detection will occur because this text exists." style="background-color: var(--highlight-blocked-text-bg-color); color: #fff; padding: 4px; border-radius: 2px; margin-right: 4px;">Blocked text</span>
|
||||
</p>
|
||||
{% endmacro %}
|
||||
@@ -71,43 +71,45 @@
|
||||
{% endif %}
|
||||
|
||||
<ul class="pure-menu-list" id="top-right-menu">
|
||||
<!-- Collapsible menu items (hidden on mobile, shown in drawer) -->
|
||||
{% if current_user.is_authenticated or not has_password %}
|
||||
{% if not current_diff_url %}
|
||||
<li class="pure-menu-item menu-collapsible {% if request.endpoint.startswith('tags.') %}active{% endif %}">
|
||||
<li class="pure-menu-item">
|
||||
<a href="{{ url_for('tags.tags_overview_page')}}" class="pure-menu-link">{{ _('GROUPS') }}</a>
|
||||
</li>
|
||||
<li class="pure-menu-item menu-collapsible {% if request.endpoint.startswith('settings.') %}active{% endif %}">
|
||||
<li class="pure-menu-item">
|
||||
<a href="{{ url_for('settings.settings_page')}}" class="pure-menu-link">{{ _('SETTINGS') }}</a>
|
||||
</li>
|
||||
<li class="pure-menu-item menu-collapsible {% if request.endpoint.startswith('imports.') %}active{% endif %}">
|
||||
<li class="pure-menu-item">
|
||||
<a href="{{ url_for('imports.import_page')}}" class="pure-menu-link">{{ _('IMPORT') }}</a>
|
||||
</li>
|
||||
<li class="pure-menu-item menu-collapsible {% if request.endpoint.startswith('backups.') %}active{% endif %}">
|
||||
<li class="pure-menu-item">
|
||||
<a href="{{ url_for('backups.index')}}" class="pure-menu-link">{{ _('BACKUPS') }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="pure-menu-item menu-collapsible">
|
||||
<li class="pure-menu-item">
|
||||
<a href="{{ url_for('ui.ui_edit.edit_page', uuid=uuid, next='diff') }}" class="pure-menu-link">{{ _('EDIT') }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li class="pure-menu-item menu-collapsible">
|
||||
<li class="pure-menu-item">
|
||||
<a class="pure-menu-link" href="https://changedetection.io">Website Change Detection and Notification.</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if current_user.is_authenticated %}
|
||||
<li class="pure-menu-item menu-collapsible">
|
||||
<a href="{{url_for('logout', redirect=request.path)}}" class="pure-menu-link">{{ _('LOG OUT') }}</a>
|
||||
<li class="pure-menu-item">
|
||||
<a href="{{url_for('logout')}}" class="pure-menu-link">{{ _('LOG OUT') }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<!-- Always visible items -->
|
||||
{% if current_user.is_authenticated or not has_password %}
|
||||
<li class="pure-menu-item">
|
||||
<button class="toggle-button" id="open-search-modal" type="button" title="{{ _('Search, or Use Alt+S Key') }}">
|
||||
{% include "svgs/search-icon.svg" %}
|
||||
</button>
|
||||
<li class="pure-menu-item pure-form" id="search-menu-item">
|
||||
<!-- We use GET here so it offers people a chance to set bookmarks etc -->
|
||||
<form name="searchForm" action="" method="GET">
|
||||
<input id="search-q" class="" name="q" placeholder="URL or Title {% if active_tag_uuid %}in '{{ active_tag.title }}'{% endif %}" required="" type="text" value="">
|
||||
<input name="tags" type="hidden" value="{% if active_tag_uuid %}{{active_tag_uuid}}{% endif %}">
|
||||
<button class="toggle-button " id="toggle-search" type="button" title="{{ _('Search, or Use Alt+S Key') }}" >
|
||||
{% include "svgs/search-icon.svg" %}
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="pure-menu-item">
|
||||
@@ -139,43 +141,13 @@
|
||||
<path id="heartpath" d="M 5.338316,0.50302766 C 0.71136983,0.50647126 -3.9576371,7.2707777 8.5004254,15.503028 23.833425,5.3700277 13.220206,-2.5384409 8.6762066,1.6475589 c -0.060791,0.054322 -0.11943,0.1110064 -0.1757812,0.1699219 -0.057,-0.059 -0.1157813,-0.116875 -0.1757812,-0.171875 C 7.4724566,0.86129334 6.4060729,0.50223298 5.338316,0.50302766 Z"
|
||||
style="fill:var(--color-background);fill-opacity:1;stroke:#ff0000;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
||||
</li>
|
||||
<li class="pure-menu-item">
|
||||
<a class="github-link" href="https://github.com/dgtlmoon/changedetection.io">
|
||||
{% include "svgs/github.svg" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Hamburger menu button (mobile only) -->
|
||||
<li class="pure-menu-item">
|
||||
<button class="hamburger-menu" id="hamburger-toggle" aria-label="Toggle menu">
|
||||
<div class="hamburger-icon">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu drawer -->
|
||||
<div class="mobile-menu-overlay" id="mobile-menu-overlay"></div>
|
||||
<div class="mobile-menu-drawer" id="mobile-menu-drawer">
|
||||
<ul class="mobile-menu-items">
|
||||
{% if current_user.is_authenticated or not has_password %}
|
||||
{% if not current_diff_url %}
|
||||
<li><a href="{{ url_for('tags.tags_overview_page')}}">{{ _('GROUPS') }}</a></li>
|
||||
<li><a href="{{ url_for('settings.settings_page')}}">{{ _('SETTINGS') }}</a></li>
|
||||
<li><a href="{{ url_for('imports.import_page')}}">{{ _('IMPORT') }}</a></li>
|
||||
<li><a href="{{ url_for('backups.index')}}">{{ _('BACKUPS') }}</a></li>
|
||||
{% else %}
|
||||
<li><a href="{{ url_for('ui.ui_edit.edit_page', uuid=uuid, next='diff') }}">{{ _('EDIT') }}</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if current_user.is_authenticated %}
|
||||
<li><a href="{{url_for('logout', redirect=request.path)}}">{{ _('LOG OUT') }}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
<div id="pure-menu-horizontal-spinner"></div>
|
||||
@@ -246,109 +218,32 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<header>
|
||||
{% block header %}{% endblock %}
|
||||
</header>
|
||||
|
||||
<div class="content-wrapper">
|
||||
{% if current_user.is_authenticated or not has_password %}
|
||||
<aside class="action-sidebar">
|
||||
<a href="{{ url_for('watchlist.index') }}" class="action-sidebar-item {% if request.endpoint.startswith('watchlist.') or request.endpoint.startswith('ui.') %}active{% endif %}" title="{{ _('Watch List') }}">
|
||||
<svg class="action-icon" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M12 6v6l4 2"/>
|
||||
</svg>
|
||||
<span class="action-label">{{ _('Watches') }}</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('queue_status') }}" class="action-sidebar-item {% if request.endpoint == 'queue_status' %}active{% endif %}" id="queue-action-item" title="{{ _('Queue Status') }}">
|
||||
<svg class="action-icon" viewBox="0 0 24 24">
|
||||
<line x1="8" y1="6" x2="21" y2="6"/>
|
||||
<line x1="8" y1="12" x2="21" y2="12"/>
|
||||
<line x1="8" y1="18" x2="21" y2="18"/>
|
||||
<line x1="3" y1="6" x2="3.01" y2="6"/>
|
||||
<line x1="3" y1="12" x2="3.01" y2="12"/>
|
||||
<line x1="3" y1="18" x2="3.01" y2="18"/>
|
||||
</svg>
|
||||
<span class="action-label">{{ _('Queue') }}</span>
|
||||
<span class="notification-bubble blue-bubble" id="queue-bubble" data-count="0"></span>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('settings.settings_page') }}" class="action-sidebar-item {% if request.endpoint.startswith('settings.') %}active{% endif %}" title="{{ _('Settings') }}">
|
||||
<svg class="action-icon" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M12 1v6m0 6v10M3.5 3.5l4.2 4.2m5.6 5.6l4.2 4.2M1 12h6m6 0h10M3.5 20.5l4.2-4.2m5.6-5.6l4.2-4.2"/>
|
||||
</svg>
|
||||
<span class="action-label">{{ _('Settings') }}</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('backups.index') }}" class="action-sidebar-item {% if request.endpoint.startswith('backups.') %}active{% endif %}" title="{{ _('Backups') }}">
|
||||
<svg class="action-icon" viewBox="0 0 24 24">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||
<polyline points="10 9 9 9 8 9"/>
|
||||
</svg>
|
||||
<span class="action-label">{{ _('Backups') }}</span>
|
||||
</a>
|
||||
|
||||
<a href="#" class="action-sidebar-item" title="{{ _('Sitemap Crawler') }}">
|
||||
<svg class="action-icon" viewBox="0 0 24 24">
|
||||
<!-- Spider web with map nodes -->
|
||||
<circle cx="12" cy="12" r="2"/>
|
||||
<!-- Radial web lines -->
|
||||
<line x1="12" y1="12" x2="12" y2="4"/>
|
||||
<line x1="12" y1="12" x2="19" y2="7"/>
|
||||
<line x1="12" y1="12" x2="20" y2="12"/>
|
||||
<line x1="12" y1="12" x2="19" y2="17"/>
|
||||
<line x1="12" y1="12" x2="12" y2="20"/>
|
||||
<line x1="12" y1="12" x2="5" y2="17"/>
|
||||
<line x1="12" y1="12" x2="4" y2="12"/>
|
||||
<line x1="12" y1="12" x2="5" y2="7"/>
|
||||
<!-- Outer web ring -->
|
||||
<circle cx="12" cy="12" r="8" fill="none"/>
|
||||
<!-- Map nodes on web -->
|
||||
<circle cx="12" cy="4" r="1.5"/>
|
||||
<circle cx="19" cy="7" r="1.5"/>
|
||||
<circle cx="20" cy="12" r="1.5"/>
|
||||
<circle cx="19" cy="17" r="1.5"/>
|
||||
<circle cx="12" cy="20" r="1.5"/>
|
||||
<circle cx="5" cy="17" r="1.5"/>
|
||||
<circle cx="4" cy="12" r="1.5"/>
|
||||
<circle cx="5" cy="7" r="1.5"/>
|
||||
</svg>
|
||||
<span class="action-label">{{ _('Sitemap') }}</span>
|
||||
</a>
|
||||
</aside>
|
||||
{% endif %}
|
||||
|
||||
<div class="content-main">
|
||||
<header>
|
||||
{% block header %}{% endblock %}
|
||||
</header>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories = true) %}
|
||||
{% if messages %}
|
||||
<ul class="messages">
|
||||
{% for category, message in messages %}
|
||||
<li class="{{ category }}">{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if session['share-link'] %}
|
||||
<ul class="messages with-share-link">
|
||||
<li class="message">
|
||||
Share this link:
|
||||
<span id="share-link">{{ session['share-link'] }}</span>
|
||||
<img style="height: 1em; display: inline-block" src="{{url_for('static_content', group='images', filename='copy.svg')}}" >
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% with messages = get_flashed_messages(with_categories = true) %}
|
||||
{% if
|
||||
messages %}
|
||||
<ul class="messages">
|
||||
{% for category, message in messages %}
|
||||
<li class="{{ category }}">{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if session['share-link'] %}
|
||||
<ul class="messages with-share-link">
|
||||
<li class="message">
|
||||
Share this link:
|
||||
<span id="share-link">{{ session['share-link'] }}</span>
|
||||
<img style="height: 1em; display: inline-block" src="{{url_for('static_content', group='images', filename='copy.svg')}}" >
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</section>
|
||||
<script src="{{url_for('static_content', group='js', filename='toggle-theme.js')}}" defer></script>
|
||||
<script src="{{url_for('static_content', group='js', filename='hamburger-menu.js')}}" defer></script>
|
||||
|
||||
<div id="checking-now-fixed-tab" style="display: none;"><span class="spinner"></span><span class="status-text"> {{ _('Checking now') }}</span></div>
|
||||
<div id="realtime-conn-error" style="display:none">{{ _('Real-time updates offline') }}</div>
|
||||
@@ -366,7 +261,7 @@
|
||||
<div class="modal-body">
|
||||
<div class="language-list">
|
||||
{% for locale, lang_data in available_languages.items()|sort %}
|
||||
<a href="{{ url_for('set_language', locale=locale, redirect=request.path) }}" class="language-option" data-locale="{{ locale }}">
|
||||
<a href="{{ url_for('set_language', locale=locale) }}" class="language-option" data-locale="{{ locale }}">
|
||||
<span class="{{ lang_data.flag }}" style="display: inline-block; width: 1.5em; height: 1.5em; vertical-align: middle; margin-right: 0.5em; border-radius: 50%; overflow: hidden;"></span> <span class="language-name">{{ lang_data.name }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
@@ -380,32 +275,7 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- Search Modal -->
|
||||
{% if current_user.is_authenticated or not has_password %}
|
||||
<dialog id="search-modal" class="modal-dialog" aria-labelledby="search-modal-title">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="search-modal-title">{{ _('Search') }}</h2>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="search-form" method="GET">
|
||||
<div class="pure-control-group">
|
||||
<label for="search-modal-input">{{ _('URL or Title') }}{% if active_tag_uuid %} {{ _('in') }} '{{ active_tag.title }}'{% endif %}</label>
|
||||
<input id="search-modal-input" class="m-d" name="q" placeholder="{{ _('Enter search term...') }}" required type="text" value="" autofocus>
|
||||
<input name="tags" type="hidden" value="{% if active_tag_uuid %}{{active_tag_uuid}}{% endif %}">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="pure-button button-cancel" id="close-search-modal">{{ _('Cancel') }}</button>
|
||||
<button type="submit" form="search-form" class="pure-button pure-button-primary">{{ _('Search') }}</button>
|
||||
</div>
|
||||
</dialog>
|
||||
{% endif %}
|
||||
|
||||
<script src="{{url_for('static_content', group='js', filename='language-selector.js')}}" defer></script>
|
||||
<script src="{{url_for('static_content', group='js', filename='search-modal.js')}}" defer></script>
|
||||
<script src="{{url_for('static_content', group='js', filename='toast.js')}}"></script>
|
||||
<script src="{{url_for('static_content', group='js', filename='flask-toast-bridge.js')}}" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<div class="inner">
|
||||
<form class="pure-form pure-form-stacked" action="{{url_for('login')}}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" id="redirect" name="redirect" value="{{ redirect_url }}">
|
||||
<fieldset>
|
||||
<div class="pure-control-group">
|
||||
<label for="password">{{ _('Password') }}</label>
|
||||
|
||||
@@ -110,42 +110,3 @@ def test_language_persistence_in_session(client, live_server, measure_memory_usa
|
||||
|
||||
assert res.status_code == 200
|
||||
assert b"Annulla" in res.data, "Italian text should persist across requests"
|
||||
|
||||
|
||||
def test_set_language_with_redirect(client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
Test that changing language keeps the user on the same page.
|
||||
Example: User is on /settings, changes language, stays on /settings.
|
||||
"""
|
||||
from flask import url_for
|
||||
|
||||
# Set language with a redirect parameter (simulating language change from /settings)
|
||||
res = client.get(
|
||||
url_for("set_language", locale="de", redirect="/settings"),
|
||||
follow_redirects=False
|
||||
)
|
||||
|
||||
# Should redirect back to settings
|
||||
assert res.status_code in [302, 303]
|
||||
assert '/settings' in res.location
|
||||
|
||||
# Verify language was set in session
|
||||
with client.session_transaction() as sess:
|
||||
assert sess.get('locale') == 'de'
|
||||
|
||||
# Test with invalid locale (should still redirect safely)
|
||||
res = client.get(
|
||||
url_for("set_language", locale="invalid_locale", redirect="/settings"),
|
||||
follow_redirects=False
|
||||
)
|
||||
assert res.status_code in [302, 303]
|
||||
assert '/settings' in res.location
|
||||
|
||||
# Test with malicious redirect (should default to watchlist)
|
||||
res = client.get(
|
||||
url_for("set_language", locale="en", redirect="https://evil.com"),
|
||||
follow_redirects=False
|
||||
)
|
||||
assert res.status_code in [302, 303]
|
||||
# Should not redirect to evil.com
|
||||
assert 'evil.com' not in res.location
|
||||
|
||||
@@ -240,6 +240,7 @@ def test_restock_itemprop_with_tag(client, live_server, measure_memory_usage, da
|
||||
|
||||
|
||||
def test_itemprop_percent_threshold(client, live_server, measure_memory_usage, datastore_path):
|
||||
|
||||
|
||||
delete_all_watches(client)
|
||||
|
||||
@@ -298,26 +299,7 @@ def test_itemprop_percent_threshold(client, live_server, measure_memory_usage, d
|
||||
assert b'has-unread-changes' not in res.data
|
||||
|
||||
|
||||
# Re #2600 - Switch the mode to normal type and back, and see if the values stick..
|
||||
###################################################################################
|
||||
uuid = next(iter(live_server.app.config['DATASTORE'].data['watching']))
|
||||
|
||||
res = client.post(
|
||||
url_for("ui.ui_edit.edit_page", uuid=uuid),
|
||||
data={"restock_settings-follow_price_changes": "y",
|
||||
"restock_settings-price_change_threshold_percent": 5.05,
|
||||
"processor": "text_json_diff",
|
||||
"url": test_url,
|
||||
'fetch_backend': "html_requests",
|
||||
"time_between_check_use_default": "y"
|
||||
},
|
||||
follow_redirects=True
|
||||
)
|
||||
assert b"Updated watch." in res.data
|
||||
# And back again
|
||||
live_server.app.config['DATASTORE'].data['watching'][uuid]['processor'] = 'restock_diff'
|
||||
res = client.get(url_for("ui.ui_edit.edit_page", uuid=uuid))
|
||||
assert b'type="text" value="5.05"' in res.data
|
||||
|
||||
delete_all_watches(client)
|
||||
|
||||
@@ -461,4 +443,3 @@ def test_special_prop_examples(client, live_server, measure_memory_usage, datast
|
||||
res = client.get(url_for("watchlist.index"))
|
||||
assert b'ception' not in res.data
|
||||
assert b'155.55' in res.data
|
||||
|
||||
|
||||
@@ -192,289 +192,3 @@ def test_xss_watch_last_error(client, live_server, measure_memory_usage, datasto
|
||||
assert b'<a href="https://foobar"></a><script>alert(123);</script>' in res.data
|
||||
assert b"https://foobar" in res.data # this text should be there
|
||||
|
||||
|
||||
def test_login_redirect_safe_urls(client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
Test that safe redirect URLs work correctly in login flow.
|
||||
This verifies the fix for open redirect vulnerabilities while maintaining
|
||||
legitimate redirect functionality for both authenticated and unauthenticated users.
|
||||
"""
|
||||
|
||||
# Test 1: Accessing /login?redirect=/settings when not logged in
|
||||
# Should show the login form with redirect parameter preserved
|
||||
res = client.get(
|
||||
url_for("login", redirect="/settings"),
|
||||
follow_redirects=False
|
||||
)
|
||||
# Should show login form
|
||||
assert res.status_code == 200
|
||||
# Check that the redirect is preserved in the hidden form field
|
||||
assert b'name="redirect"' in res.data
|
||||
|
||||
# Test 2: Valid internal redirect with query parameters
|
||||
res = client.get(
|
||||
url_for("login", redirect="/settings?tab=notifications"),
|
||||
follow_redirects=False
|
||||
)
|
||||
assert res.status_code == 200
|
||||
# Check that the redirect is preserved
|
||||
assert b'value="/settings?tab=notifications"' in res.data
|
||||
|
||||
# Test 3: Malicious external URL should be blocked and default to watchlist
|
||||
res = client.get(
|
||||
url_for("login", redirect="https://evil.com/phishing"),
|
||||
follow_redirects=False
|
||||
)
|
||||
# Should show login form
|
||||
assert res.status_code == 200
|
||||
# The redirect parameter in the form should NOT contain the evil URL
|
||||
# Check the actual input value, not just anywhere in the page
|
||||
assert b'value="https://evil.com' not in res.data
|
||||
assert b'value="/evil.com' not in res.data
|
||||
assert b'name="redirect"' in res.data
|
||||
|
||||
# Test 4: Double-slash attack should be blocked
|
||||
res = client.get(
|
||||
url_for("login", redirect="//evil.com"),
|
||||
follow_redirects=False
|
||||
)
|
||||
assert res.status_code == 200
|
||||
# Should not have the malicious URL in the redirect input value
|
||||
assert b'value="//evil.com"' not in res.data
|
||||
|
||||
# Test 5: Protocol handler exploit should be blocked
|
||||
res = client.get(
|
||||
url_for("login", redirect="javascript:alert(document.domain)"),
|
||||
follow_redirects=False
|
||||
)
|
||||
assert res.status_code == 200
|
||||
# Should not have javascript: in the redirect input value
|
||||
assert b'value="javascript:' not in res.data
|
||||
|
||||
# Test 6: At-symbol obfuscation attack should be blocked
|
||||
res = client.get(
|
||||
url_for("login", redirect="//@evil.com"),
|
||||
follow_redirects=False
|
||||
)
|
||||
assert res.status_code == 200
|
||||
# Should not have the malicious URL in the redirect input value
|
||||
assert b'value="//@evil.com"' not in res.data
|
||||
|
||||
# Test 7: Multiple slashes attack should be blocked
|
||||
res = client.get(
|
||||
url_for("login", redirect="////evil.com"),
|
||||
follow_redirects=False
|
||||
)
|
||||
assert res.status_code == 200
|
||||
# Should not have the malicious URL in the redirect input value
|
||||
assert b'value="////evil.com"' not in res.data
|
||||
|
||||
|
||||
def test_login_redirect_with_password(client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
Test that redirect functionality works correctly when a password is set.
|
||||
This ensures that notifications can always link to /login and users will
|
||||
be redirected to the correct page after authentication.
|
||||
"""
|
||||
|
||||
# Set a password
|
||||
from changedetectionio import store
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
# Generate a test password
|
||||
password = "test123"
|
||||
salt = os.urandom(32)
|
||||
key = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)
|
||||
salted_pass = base64.b64encode(salt + key).decode('ascii')
|
||||
|
||||
# Set the password in the datastore
|
||||
client.application.config['DATASTORE'].data['settings']['application']['password'] = salted_pass
|
||||
|
||||
# Test 1: Try to access /login?redirect=/settings without being logged in
|
||||
# Should show login form and preserve redirect parameter
|
||||
res = client.get(
|
||||
url_for("login", redirect="/settings"),
|
||||
follow_redirects=False
|
||||
)
|
||||
assert res.status_code == 200
|
||||
assert b"Password" in res.data
|
||||
# Check that redirect parameter is preserved in the form
|
||||
assert b'name="redirect"' in res.data
|
||||
assert b'value="/settings"' in res.data
|
||||
|
||||
# Test 2: Submit correct password with redirect parameter
|
||||
# Should redirect to /settings after successful login
|
||||
res = client.post(
|
||||
url_for("login"),
|
||||
data={"password": password, "redirect": "/settings"},
|
||||
follow_redirects=True
|
||||
)
|
||||
assert res.status_code == 200
|
||||
# Should be on settings page
|
||||
assert b"Settings" in res.data or b"settings" in res.data
|
||||
|
||||
# Test 3: Now that we're logged in, accessing /login?redirect=/settings
|
||||
# should redirect immediately without showing login form
|
||||
res = client.get(
|
||||
url_for("login", redirect="/"),
|
||||
follow_redirects=True
|
||||
)
|
||||
assert res.status_code == 200
|
||||
assert b"Already logged in" in res.data
|
||||
|
||||
# Test 4: Malicious redirect should be blocked even with correct password
|
||||
res = client.post(
|
||||
url_for("login"),
|
||||
data={"password": password, "redirect": "https://evil.com"},
|
||||
follow_redirects=True
|
||||
)
|
||||
# Should redirect to watchlist index instead of evil.com
|
||||
assert b"evil.com" not in res.data
|
||||
|
||||
# Logout for cleanup
|
||||
client.get(url_for("logout"))
|
||||
|
||||
# Test 5: Incorrect password with redirect should stay on login page
|
||||
res = client.post(
|
||||
url_for("login"),
|
||||
data={"password": "wrongpassword", "redirect": "/settings"},
|
||||
follow_redirects=True
|
||||
)
|
||||
assert res.status_code == 200
|
||||
assert b"Incorrect password" in res.data or b"password" in res.data
|
||||
|
||||
# Clear the password
|
||||
del client.application.config['DATASTORE'].data['settings']['application']['password']
|
||||
|
||||
|
||||
def test_login_redirect_from_protected_page(client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
Test the complete redirect flow: accessing a protected page while logged out
|
||||
should redirect to login with the page URL, then redirect back after login.
|
||||
This is the real-world scenario where users try to access /edit/uuid or /settings
|
||||
and need to login first.
|
||||
"""
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
# Add a watch first
|
||||
set_original_response(datastore_path=datastore_path)
|
||||
res = client.post(
|
||||
url_for("imports.import_page"),
|
||||
data={"urls": url_for('test_endpoint', _external=True)},
|
||||
follow_redirects=True
|
||||
)
|
||||
assert b"1 Imported" in res.data
|
||||
wait_for_all_checks(client)
|
||||
|
||||
# Set a password
|
||||
password = "test123"
|
||||
salt = os.urandom(32)
|
||||
key = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)
|
||||
salted_pass = base64.b64encode(salt + key).decode('ascii')
|
||||
client.application.config['DATASTORE'].data['settings']['application']['password'] = salted_pass
|
||||
|
||||
# Logout to ensure we're not authenticated
|
||||
client.get(url_for("logout"))
|
||||
|
||||
# Try to access a protected page (edit page for first watch)
|
||||
res = client.get(
|
||||
url_for("ui.ui_edit.edit_page", uuid="first"),
|
||||
follow_redirects=False
|
||||
)
|
||||
|
||||
# Should redirect to login with the edit page as redirect parameter
|
||||
assert res.status_code in [302, 303]
|
||||
assert '/login' in res.location
|
||||
assert 'redirect=' in res.location or 'redirect=%2F' in res.location
|
||||
|
||||
# Follow the redirect to login page
|
||||
res = client.get(res.location, follow_redirects=False)
|
||||
assert res.status_code == 200
|
||||
assert b'Password' in res.data
|
||||
|
||||
# The redirect parameter should be preserved in the login form
|
||||
# It should contain the edit page URL
|
||||
assert b'name="redirect"' in res.data
|
||||
assert b'value="/edit/first"' in res.data or b'value="%2Fedit%2Ffirst"' in res.data
|
||||
|
||||
# Now login with correct password and the redirect parameter
|
||||
res = client.post(
|
||||
url_for("login"),
|
||||
data={"password": password, "redirect": "/edit/first"},
|
||||
follow_redirects=False
|
||||
)
|
||||
|
||||
# Should redirect to the edit page
|
||||
assert res.status_code in [302, 303]
|
||||
assert '/edit/first' in res.location
|
||||
|
||||
# Follow the redirect to verify we're on the edit page
|
||||
res = client.get(res.location, follow_redirects=True)
|
||||
assert res.status_code == 200
|
||||
# Should see edit page content
|
||||
assert b'Edit' in res.data or b'Watching' in res.data
|
||||
|
||||
# Cleanup
|
||||
client.get(url_for("logout"))
|
||||
del client.application.config['DATASTORE'].data['settings']['application']['password']
|
||||
|
||||
|
||||
def test_logout_with_redirect(client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
Test that logout preserves the current page URL, so after re-login
|
||||
the user returns to where they were before logging out.
|
||||
Example: User is on /edit/uuid, clicks logout, then logs back in and
|
||||
returns to /edit/uuid.
|
||||
"""
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
# Set a password and login
|
||||
password = "test123"
|
||||
salt = os.urandom(32)
|
||||
key = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)
|
||||
salted_pass = base64.b64encode(salt + key).decode('ascii')
|
||||
client.application.config['DATASTORE'].data['settings']['application']['password'] = salted_pass
|
||||
|
||||
# Login
|
||||
res = client.post(
|
||||
url_for("login"),
|
||||
data={"password": password},
|
||||
follow_redirects=True
|
||||
)
|
||||
assert res.status_code == 200
|
||||
|
||||
# Now logout with a redirect parameter (simulating logout from /settings)
|
||||
res = client.get(
|
||||
url_for("logout", redirect="/settings"),
|
||||
follow_redirects=False
|
||||
)
|
||||
|
||||
# Should redirect to login with the redirect parameter
|
||||
assert res.status_code in [302, 303]
|
||||
assert '/login' in res.location
|
||||
assert 'redirect=' in res.location or 'redirect=%2F' in res.location
|
||||
|
||||
# Follow the redirect to login page
|
||||
res = client.get(res.location, follow_redirects=False)
|
||||
assert res.status_code == 200
|
||||
assert b'Password' in res.data
|
||||
# The redirect parameter should be preserved
|
||||
assert b'value="/settings"' in res.data or b'value="%2Fsettings"' in res.data
|
||||
|
||||
# Login again with the redirect
|
||||
res = client.post(
|
||||
url_for("login"),
|
||||
data={"password": password, "redirect": "/settings"},
|
||||
follow_redirects=False
|
||||
)
|
||||
|
||||
# Should redirect back to settings
|
||||
assert res.status_code in [302, 303]
|
||||
assert '/settings' in res.location or 'settings' in res.location
|
||||
|
||||
# Cleanup
|
||||
del client.application.config['DATASTORE'].data['settings']['application']['password']
|
||||
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-01-03 14:31+0100\n"
|
||||
"POT-Creation-Date: 2026-01-02 16:07+0100\n"
|
||||
"PO-Revision-Date: 2026-01-02 11:40+0100\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: cs\n"
|
||||
@@ -19,23 +19,24 @@ msgstr ""
|
||||
"Generated-By: Babel 2.17.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html:241
|
||||
#: changedetectionio/flask_app.py:214 changedetectionio/flask_app.py:226
|
||||
#: changedetectionio/flask_app.py:247
|
||||
#: changedetectionio/flask_app.py:213 changedetectionio/flask_app.py:225
|
||||
#: changedetectionio/flask_app.py:246
|
||||
#: changedetectionio/realtime/socket_server.py:171
|
||||
msgid "Not yet"
|
||||
msgstr "Ještě ne"
|
||||
|
||||
#: changedetectionio/flask_app.py:534
|
||||
msgid "Already logged in"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/flask_app.py:536
|
||||
#: changedetectionio/flask_app.py:468
|
||||
msgid "You must be logged in, please log in."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/flask_app.py:551
|
||||
#: changedetectionio/flask_app.py:495
|
||||
msgid "Already logged in"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/flask_app.py:522
|
||||
#, fuzzy
|
||||
msgid "Incorrect password"
|
||||
msgstr "Nesprávné heslo"
|
||||
msgstr "Heslo"
|
||||
|
||||
#: changedetectionio/forms.py:63 changedetectionio/forms.py:243
|
||||
msgid ""
|
||||
@@ -58,16 +59,19 @@ msgid "Not a valid timezone name"
|
||||
msgstr "Neplatný název časového pásma"
|
||||
|
||||
#: changedetectionio/forms.py:183
|
||||
#, fuzzy
|
||||
msgid "not set"
|
||||
msgstr "nenastaveno"
|
||||
msgstr "Ještě ne"
|
||||
|
||||
#: changedetectionio/forms.py:184
|
||||
#, fuzzy
|
||||
msgid "Start At"
|
||||
msgstr "Začíná v"
|
||||
msgstr "NASTAVENÍ"
|
||||
|
||||
#: changedetectionio/forms.py:185
|
||||
#, fuzzy
|
||||
msgid "Run duration"
|
||||
msgstr "Doba běhu"
|
||||
msgstr "Žádné informace"
|
||||
|
||||
#: changedetectionio/forms.py:188
|
||||
msgid "Use time scheduler"
|
||||
@@ -120,14 +124,17 @@ msgid "Days"
|
||||
msgstr "Dny"
|
||||
|
||||
#: changedetectionio/forms.py:253
|
||||
#, fuzzy
|
||||
msgid "Hours"
|
||||
msgstr "Hodiny"
|
||||
msgstr "Heslo"
|
||||
|
||||
#: changedetectionio/forms.py:254
|
||||
#, fuzzy
|
||||
msgid "Minutes"
|
||||
msgstr "Minuty"
|
||||
msgstr "Ztlumit"
|
||||
|
||||
#: changedetectionio/forms.py:255
|
||||
#, fuzzy
|
||||
msgid "Seconds"
|
||||
msgstr "sekundy"
|
||||
|
||||
@@ -169,36 +176,43 @@ msgid "Invalid value."
|
||||
msgstr "Neplatná hodnota."
|
||||
|
||||
#: changedetectionio/forms.py:732
|
||||
#, fuzzy
|
||||
msgid "Watch"
|
||||
msgstr "# monitory"
|
||||
msgstr "# Hodinky"
|
||||
|
||||
#: changedetectionio/forms.py:733 changedetectionio/forms.py:766
|
||||
msgid "Processor"
|
||||
msgstr "Procesor"
|
||||
|
||||
#: changedetectionio/forms.py:734
|
||||
#, fuzzy
|
||||
msgid "Edit > Watch"
|
||||
msgstr "Nejprve upravte a poté sledujte"
|
||||
|
||||
#: changedetectionio/forms.py:747 changedetectionio/forms.py:994
|
||||
#, fuzzy
|
||||
msgid "Fetch Method"
|
||||
msgstr "Nastavte metodu načítání"
|
||||
|
||||
#: changedetectionio/forms.py:748
|
||||
#, fuzzy
|
||||
msgid "Notification Body"
|
||||
msgstr "Text oznámení"
|
||||
msgstr "Žádné informace"
|
||||
|
||||
#: changedetectionio/forms.py:749
|
||||
#, fuzzy
|
||||
msgid "Notification format"
|
||||
msgstr "Formát oznámení"
|
||||
msgstr "Žádné informace"
|
||||
|
||||
#: changedetectionio/forms.py:750
|
||||
#, fuzzy
|
||||
msgid "Notification Title"
|
||||
msgstr "Název oznámení"
|
||||
msgstr "Žádné informace"
|
||||
|
||||
#: changedetectionio/forms.py:751
|
||||
#, fuzzy
|
||||
msgid "Notification URL List"
|
||||
msgstr "Seznam URL oznámení"
|
||||
msgstr "Žádné informace"
|
||||
|
||||
#: changedetectionio/forms.py:752
|
||||
msgid "Processor - What do you want to achieve?"
|
||||
@@ -206,9 +220,10 @@ msgstr "Procesor – Čeho chcete dosáhnout?"
|
||||
|
||||
#: changedetectionio/forms.py:753
|
||||
msgid "Default timezone for watch check scheduler"
|
||||
msgstr "Výchozí časové pásmo pro plánovač kontroly monitorů"
|
||||
msgstr "Výchozí časové pásmo pro plánovač kontroly hodinek"
|
||||
|
||||
#: changedetectionio/forms.py:754
|
||||
#, fuzzy
|
||||
msgid "Wait seconds before extracting text"
|
||||
msgstr "sekund před extrahováním textu."
|
||||
|
||||
@@ -217,6 +232,7 @@ msgid "Should contain one or more seconds"
|
||||
msgstr "Měl by obsahovat jednu nebo více sekund"
|
||||
|
||||
#: changedetectionio/forms.py:767
|
||||
#, fuzzy
|
||||
msgid "URLs"
|
||||
msgstr "URL"
|
||||
|
||||
@@ -229,18 +245,22 @@ msgid "Must be .xlsx file!"
|
||||
msgstr "Musí to být soubor .xlsx!"
|
||||
|
||||
#: changedetectionio/forms.py:769
|
||||
#, fuzzy
|
||||
msgid "File mapping"
|
||||
msgstr "Typ mapování souboru."
|
||||
|
||||
#: changedetectionio/forms.py:773
|
||||
#, fuzzy
|
||||
msgid "Operation"
|
||||
msgstr "Možnosti uživatelského rozhraní"
|
||||
|
||||
#: changedetectionio/forms.py:776
|
||||
#, fuzzy
|
||||
msgid "Selector"
|
||||
msgstr "Režim výběru:"
|
||||
|
||||
#: changedetectionio/forms.py:777
|
||||
#, fuzzy
|
||||
msgid "value"
|
||||
msgstr "Pauza"
|
||||
|
||||
@@ -249,14 +269,17 @@ msgid "Use global settings for time between check and scheduler."
|
||||
msgstr "Použijte globální nastavení pro čas mezi kontrolou a plánovačem."
|
||||
|
||||
#: changedetectionio/forms.py:798
|
||||
#, fuzzy
|
||||
msgid "CSS/JSONPath/JQ/XPath Filters"
|
||||
msgstr "CSS/xPath filtr"
|
||||
|
||||
#: changedetectionio/forms.py:800 changedetectionio/forms.py:996
|
||||
#, fuzzy
|
||||
msgid "Remove elements"
|
||||
msgstr "Vyberte podle prvku"
|
||||
|
||||
#: changedetectionio/forms.py:802
|
||||
#, fuzzy
|
||||
msgid "Extract text"
|
||||
msgstr "Extrahujte data"
|
||||
|
||||
@@ -266,22 +289,28 @@ msgid "Title"
|
||||
msgstr "Titul"
|
||||
|
||||
#: changedetectionio/forms.py:806
|
||||
#, fuzzy
|
||||
msgid "Ignore lines containing"
|
||||
msgstr "Ignorujte všechny odpovídající řádky"
|
||||
|
||||
#: changedetectionio/forms.py:808
|
||||
#, fuzzy
|
||||
msgid "Request body"
|
||||
msgstr "Žádost"
|
||||
|
||||
#: changedetectionio/forms.py:809
|
||||
#, fuzzy
|
||||
msgid "Request method"
|
||||
msgstr "Žádost"
|
||||
|
||||
#: changedetectionio/forms.py:810
|
||||
msgid "Ignore status codes (process non-2xx status codes as normal)"
|
||||
msgstr "Ignorovat stavové kódy (zpracovat stavové kódy jiné než 2xx jako normálně)"
|
||||
msgstr ""
|
||||
"Ignorovat stavové kódy (zpracovat stavové "
|
||||
"kódy jiné než 2xx jako normálně)"
|
||||
|
||||
#: changedetectionio/forms.py:811
|
||||
#, fuzzy
|
||||
msgid "Only trigger when unique lines appear in all history"
|
||||
msgstr "Spustit pouze tehdy, když se objeví jedinečné čáry"
|
||||
|
||||
@@ -299,18 +328,21 @@ msgid "Strip ignored lines"
|
||||
msgstr "Odstraňte ignorované řádky"
|
||||
|
||||
#: changedetectionio/forms.py:815
|
||||
#, fuzzy
|
||||
msgid "Trim whitespace before and after text"
|
||||
msgstr "Odstraňte všechny mezery před a za každým řádkem textu"
|
||||
|
||||
#: changedetectionio/forms.py:817
|
||||
#, fuzzy
|
||||
msgid "Added lines"
|
||||
msgstr "Přidané řádky"
|
||||
msgstr "Přihlášení"
|
||||
|
||||
#: changedetectionio/forms.py:818
|
||||
msgid "Replaced/changed lines"
|
||||
msgstr "Vyměněny/změněny řádky"
|
||||
|
||||
#: changedetectionio/forms.py:819
|
||||
#, fuzzy
|
||||
msgid "Removed lines"
|
||||
msgstr "Odebráno"
|
||||
|
||||
@@ -347,10 +379,12 @@ msgid "Notifications"
|
||||
msgstr "Žádné informace"
|
||||
|
||||
#: changedetectionio/forms.py:832
|
||||
#, fuzzy
|
||||
msgid "Muted"
|
||||
msgstr "Ztlumit"
|
||||
|
||||
#: changedetectionio/forms.py:832
|
||||
#, fuzzy
|
||||
msgid "On"
|
||||
msgstr "žádný"
|
||||
|
||||
@@ -360,7 +394,7 @@ msgstr "Připojte snímek obrazovky k oznámení (pokud je to možné)"
|
||||
|
||||
#: changedetectionio/forms.py:835
|
||||
msgid "Match"
|
||||
msgstr "# monitory"
|
||||
msgstr "# Hodinky"
|
||||
|
||||
#: changedetectionio/forms.py:835
|
||||
msgid "Match all of the following"
|
||||
@@ -395,6 +429,7 @@ msgid "Invalid template syntax in \"%(header)s\" header: %(error)s"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py:920 changedetectionio/forms.py:932
|
||||
#, fuzzy
|
||||
msgid "Name"
|
||||
msgstr "Zrušit ztlumení"
|
||||
|
||||
@@ -419,6 +454,7 @@ msgid "Plaintext requests"
|
||||
msgstr "Požadavky na prostý text"
|
||||
|
||||
#: changedetectionio/forms.py:946
|
||||
#, fuzzy
|
||||
msgid "Chrome requests"
|
||||
msgstr "Žádost"
|
||||
|
||||
@@ -459,10 +495,12 @@ msgid "Open 'History' page in a new tab"
|
||||
msgstr "Otevřete stránku „Historie“ na nové kartě"
|
||||
|
||||
#: changedetectionio/forms.py:981
|
||||
#, fuzzy
|
||||
msgid "Realtime UI Updates Enabled"
|
||||
msgstr "Aktualizace v reálném čase offline"
|
||||
|
||||
#: changedetectionio/forms.py:982
|
||||
#, fuzzy
|
||||
msgid "Favicons Enabled"
|
||||
msgstr "zvážit povolení"
|
||||
|
||||
@@ -475,6 +513,7 @@ msgid "API access token security check enabled"
|
||||
msgstr "Kontrola zabezpečení přístupového tokenu API povolena"
|
||||
|
||||
#: changedetectionio/forms.py:989
|
||||
#, fuzzy
|
||||
msgid "Notification base URL override"
|
||||
msgstr "Počet upozornění na upozornění"
|
||||
|
||||
@@ -483,10 +522,12 @@ msgid "Treat empty pages as a change?"
|
||||
msgstr "Považovat prázdné stránky za změnu?"
|
||||
|
||||
#: changedetectionio/forms.py:995
|
||||
#, fuzzy
|
||||
msgid "Ignore Text"
|
||||
msgstr "Text chyby"
|
||||
|
||||
#: changedetectionio/forms.py:997
|
||||
#, fuzzy
|
||||
msgid "Ignore whitespace"
|
||||
msgstr "Ignorujte mezery"
|
||||
|
||||
@@ -516,8 +557,9 @@ msgid "RSS \"System default\" template override"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py:1020
|
||||
#, fuzzy
|
||||
msgid "Remove password"
|
||||
msgstr "Odebrat heslo"
|
||||
msgstr "Heslo"
|
||||
|
||||
#: changedetectionio/forms.py:1021
|
||||
msgid "Render anchor tag content"
|
||||
@@ -526,12 +568,15 @@ msgstr "Vykreslit obsah kotvící značky"
|
||||
#: changedetectionio/forms.py:1022
|
||||
msgid "Allow anonymous access to watch history page when password is enabled"
|
||||
msgstr ""
|
||||
"Povolit anonymní přístup na stránku historie sledování, když je povoleno "
|
||||
"Povolit anonymní přístup na stránku "
|
||||
"historie sledování, když je povoleno "
|
||||
"hesloPovolit anonymní přístup na stránku "
|
||||
"historie sledování, když je povoleno "
|
||||
"heslo"
|
||||
|
||||
#: changedetectionio/forms.py:1024
|
||||
msgid "Hide muted watches from RSS feed"
|
||||
msgstr "Skrýt ztlumené monitory ze zdroje RSS"
|
||||
msgstr "Skrýt ztlumené hodinky ze zdroje RSS"
|
||||
|
||||
#: changedetectionio/forms.py:1027
|
||||
msgid "Enable RSS reader mode "
|
||||
@@ -554,6 +599,7 @@ msgid "RegEx to extract"
|
||||
msgstr "RegEx k extrahování"
|
||||
|
||||
#: changedetectionio/forms.py:1056
|
||||
#, fuzzy
|
||||
msgid "Extract as CSV"
|
||||
msgstr "Extrahujte data"
|
||||
|
||||
@@ -583,14 +629,12 @@ msgid "Backups were deleted."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/backups/templates/overview.html:6
|
||||
#: changedetectionio/templates/base.html:282
|
||||
#: changedetectionio/templates/base.html:290
|
||||
msgid "Backups"
|
||||
msgstr "Backups"
|
||||
msgstr "ZÁLOHY"
|
||||
|
||||
#: changedetectionio/blueprint/backups/templates/overview.html:9
|
||||
msgid "A backup is running!"
|
||||
msgstr "A backup is running!"
|
||||
msgstr "Probíhá zálohování!"
|
||||
|
||||
#: changedetectionio/blueprint/backups/templates/overview.html:13
|
||||
msgid ""
|
||||
@@ -604,15 +648,15 @@ msgstr "Mb"
|
||||
|
||||
#: changedetectionio/blueprint/backups/templates/overview.html:24
|
||||
msgid "No backups found."
|
||||
msgstr "No backups found."
|
||||
msgstr "Nebyly nalezeny žádné zálohy."
|
||||
|
||||
#: changedetectionio/blueprint/backups/templates/overview.html:28
|
||||
msgid "Create backup"
|
||||
msgstr "Create backup"
|
||||
msgstr "ZÁLOHY"
|
||||
|
||||
#: changedetectionio/blueprint/backups/templates/overview.html:30
|
||||
msgid "Remove backups"
|
||||
msgstr "Remove backups"
|
||||
msgstr "ZÁLOHY"
|
||||
|
||||
#: changedetectionio/blueprint/imports/importer.py:45
|
||||
msgid ""
|
||||
@@ -798,6 +842,7 @@ msgid "Password protection enabled."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/__init__.py:126
|
||||
#, fuzzy
|
||||
msgid "Settings updated."
|
||||
msgstr "NASTAVENÍ"
|
||||
|
||||
@@ -856,8 +901,10 @@ msgstr "Více informací"
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html:46
|
||||
msgid "Default recheck time for all watches, current system minimum is"
|
||||
msgstr ""
|
||||
"Výchozí čas opětovné kontroly pro všechny monitory, aktuální systémové "
|
||||
"minimum je"
|
||||
"Výchozí čas opětovné kontroly pro všechny hodinky, aktuální systémové "
|
||||
"minimum jeVýchozí čas opětovné kontroly pro všechny hodinky, aktuální "
|
||||
"systémové minimum jeVýchozí čas opětovné kontroly pro všechny hodinky, "
|
||||
"aktuální systémové minimum je"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html:46
|
||||
msgid "seconds"
|
||||
@@ -881,7 +928,7 @@ msgstr "Nejsou aktivní žádné pluginy"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html:405
|
||||
msgid "Back"
|
||||
msgstr "Zpět"
|
||||
msgstr "ZÁLOHY"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html:406
|
||||
msgid "Clear Snapshot History"
|
||||
@@ -893,6 +940,7 @@ msgid "The tag \"{}\" already exists"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/__init__.py:50
|
||||
#, fuzzy
|
||||
msgid "Tag added"
|
||||
msgstr "Přidáno"
|
||||
|
||||
@@ -915,6 +963,7 @@ msgid "Tag not found"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/__init__.py:184
|
||||
#, fuzzy
|
||||
msgid "Updated"
|
||||
msgstr "Ztlumit"
|
||||
|
||||
@@ -934,7 +983,7 @@ msgstr "přidáno"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html:50
|
||||
msgid "to any existing watch configurations."
|
||||
msgstr "do všech existujících konfigurací monitorů."
|
||||
msgstr "do všech existujících konfigurací hodinek."
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html:53
|
||||
#: changedetectionio/blueprint/ui/templates/edit.html:321
|
||||
@@ -951,7 +1000,9 @@ msgstr "Používejte opatrně!"
|
||||
msgid "This will easily fill up your email storage quota or flood other storages."
|
||||
msgstr ""
|
||||
"Snadno tak zaplníte kvótu e-mailového úložiště nebo zahltíte další "
|
||||
"úložiště."
|
||||
"úložiště.Snadno tak zaplníte kvótu e-mailového úložiště nebo zahltíte "
|
||||
"další úložiště.Snadno tak zaplníte kvótu e-mailového úložiště nebo "
|
||||
"zahltíte další úložiště."
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html:80
|
||||
#: changedetectionio/blueprint/ui/templates/edit.html:276
|
||||
@@ -976,7 +1027,7 @@ msgstr "povoleny adresy URL pro upozornění v celém systému"
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html:81
|
||||
#: changedetectionio/blueprint/ui/templates/edit.html:277
|
||||
msgid "this form will override notification settings for this watch only"
|
||||
msgstr "tento formulář přepíše nastavení oznámení pouze pro tyto monitory"
|
||||
msgstr "tento formulář přepíše nastavení oznámení pouze pro tyto hodinky"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/edit-tag.html:81
|
||||
#: changedetectionio/blueprint/ui/templates/edit.html:277
|
||||
@@ -1004,7 +1055,7 @@ msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html:31
|
||||
msgid "# Watches"
|
||||
msgstr "# monitory"
|
||||
msgstr "# Hodinky"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html:32
|
||||
msgid "Tag / Label name"
|
||||
@@ -1060,12 +1111,12 @@ msgstr "Odpojit"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html:69
|
||||
msgid "Keep the tag but unlink any watches"
|
||||
msgstr "Ponechte štítek, ale odpojte všechny monitory"
|
||||
msgstr "Ponechte štítek, ale odpojte všechny hodinky"
|
||||
|
||||
#: changedetectionio/blueprint/tags/templates/groups-overview.html:70
|
||||
#: changedetectionio/blueprint/ui/templates/edit.html:500
|
||||
msgid "RSS Feed for this watch"
|
||||
msgstr "RSS kanál pro tyto monitory"
|
||||
msgstr "RSS kanál pro tyto hodinky"
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py:20
|
||||
#, python-brace-format
|
||||
@@ -1075,7 +1126,7 @@ msgstr ""
|
||||
#: changedetectionio/blueprint/ui/__init__.py:27
|
||||
#, fuzzy, python-brace-format
|
||||
msgid "{} watches paused"
|
||||
msgstr "# monitory"
|
||||
msgstr "# Hodinky"
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py:34
|
||||
#, python-brace-format
|
||||
@@ -1090,7 +1141,7 @@ msgstr ""
|
||||
#: changedetectionio/blueprint/ui/__init__.py:48
|
||||
#, fuzzy, python-brace-format
|
||||
msgid "{} watches muted"
|
||||
msgstr "# monitory"
|
||||
msgstr "# Hodinky"
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py:55
|
||||
#, python-brace-format
|
||||
@@ -1123,6 +1174,7 @@ msgid "{} watches were tagged"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py:142
|
||||
#, fuzzy
|
||||
msgid "Watch not found"
|
||||
msgstr "Sledujte tuto adresu URL!"
|
||||
|
||||
@@ -1132,10 +1184,12 @@ msgid "Cleared snapshot history for watch {}"
|
||||
msgstr "Vymazat/resetovat historii"
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py:156
|
||||
#, fuzzy
|
||||
msgid "Cleared snapshot history for all watches"
|
||||
msgstr "Vymazat/resetovat historii"
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py:158
|
||||
#, fuzzy
|
||||
msgid "Incorrect confirmation text."
|
||||
msgstr "Žádné informace"
|
||||
|
||||
@@ -1145,6 +1199,7 @@ msgid "The watch by UUID {} does not exist."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py:199
|
||||
#, fuzzy
|
||||
msgid "Deleted."
|
||||
msgstr "Vymazat"
|
||||
|
||||
@@ -1211,8 +1266,9 @@ msgid "Updated watch - unpaused!"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/edit.py:239
|
||||
#, fuzzy
|
||||
msgid "Updated watch."
|
||||
msgstr "Smazat monitory?"
|
||||
msgstr "Smazat hodinky?"
|
||||
|
||||
#: changedetectionio/blueprint/ui/preview.py:78
|
||||
msgid "Preview unavailable - No fetch/check completed or triggers not reached"
|
||||
@@ -1243,7 +1299,7 @@ msgstr "Možná budete chtít použít"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html:13
|
||||
msgid "BACKUP"
|
||||
msgstr "BACKUP"
|
||||
msgstr "ZÁLOHY"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html:13
|
||||
msgid "link first."
|
||||
@@ -1270,8 +1326,7 @@ msgid "Clear History!"
|
||||
msgstr "Jasné historie"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html:39
|
||||
#: changedetectionio/templates/base.html:379
|
||||
#: changedetectionio/templates/base.html:399
|
||||
#: changedetectionio/templates/base.html:274
|
||||
msgid "Cancel"
|
||||
msgstr "Zrušit"
|
||||
|
||||
@@ -1301,11 +1356,11 @@ msgstr "Na"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:53
|
||||
msgid "Words"
|
||||
msgstr "Slova"
|
||||
msgstr "Heslo"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:57
|
||||
msgid "Lines"
|
||||
msgstr "Řádky"
|
||||
msgstr "Přihlášení"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:61
|
||||
msgid "Ignore Whitespace"
|
||||
@@ -1313,7 +1368,7 @@ msgstr "Ignorujte mezery"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:65
|
||||
msgid "Same/non-changed"
|
||||
msgstr "Stejné/beze změny"
|
||||
msgstr "Změněno"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:69
|
||||
msgid "Removed"
|
||||
@@ -1432,7 +1487,9 @@ msgstr "Aktuální snímek obrazovky z poslední žádosti"
|
||||
msgid "No screenshot available just yet! Try rechecking the page."
|
||||
msgstr ""
|
||||
"Zatím není k dispozici žádný snímek obrazovky! Zkuste stránku znovu "
|
||||
"zkontrolovat."
|
||||
"zkontrolovat.Zatím není k dispozici žádný snímek obrazovky! Zkuste "
|
||||
"stránku znovu zkontrolovat.Zatím není k dispozici žádný snímek obrazovky!"
|
||||
" Zkuste stránku znovu zkontrolovat."
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:154
|
||||
msgid "Screenshot requires Playwright/WebDriver enabled"
|
||||
@@ -1537,7 +1594,7 @@ msgstr "Znovu zkontrolujte vše"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/edit.html:133
|
||||
msgid "Choose a proxy for this watch"
|
||||
msgstr "RSS kanál pro tyto monitory"
|
||||
msgstr "RSS kanál pro tyto hodinky"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/edit.html:143
|
||||
msgid ""
|
||||
@@ -1584,7 +1641,9 @@ msgstr "Proměnné jsou podporovány v hodnotách hlavičky požadavku"
|
||||
msgid "Alert! Extra headers file found and will be added to this watch!"
|
||||
msgstr ""
|
||||
"Upozornění! Byl nalezen další soubor záhlaví a bude přidán do těchto "
|
||||
"monitorů!"
|
||||
"hodinek!Upozornění! Byl nalezen další soubor záhlaví a bude přidán do "
|
||||
"těchto hodinek!Upozornění! Byl nalezen další soubor záhlaví a bude přidán"
|
||||
" do těchto hodinek!"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/edit.html:194
|
||||
msgid "Headers can be also read from a file in your data-directory"
|
||||
@@ -1622,8 +1681,10 @@ msgstr ""
|
||||
#: changedetectionio/blueprint/ui/templates/edit.html:419
|
||||
msgid "Visual Selector data is not ready, watch needs to be checked atleast once."
|
||||
msgstr ""
|
||||
"Data Visual Selector nejsou připravena, monitory je třeba alespoň jednou "
|
||||
"zkontrolovat."
|
||||
"Data Visual Selector nejsou připravena, hodinky je třeba alespoň jednou "
|
||||
"zkontrolovat.Data Visual Selector nejsou připravena, hodinky je třeba "
|
||||
"alespoň jednou zkontrolovat.Data Visual Selector nejsou připravena, "
|
||||
"hodinky je třeba alespoň jednou zkontrolovat."
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/edit.html:253
|
||||
msgid ""
|
||||
@@ -1839,11 +1900,11 @@ msgstr "Stáhněte si nejnovější HTML snímek"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/edit.html:488
|
||||
msgid "Delete Watch?"
|
||||
msgstr "Smazat monitory?"
|
||||
msgstr "Smazat hodinky?"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/edit.html:489
|
||||
msgid "Are you sure you want to delete the watch for:"
|
||||
msgstr "Opravdu chcete smazat monitory pro:"
|
||||
msgstr "Opravdu chcete smazat hodinky pro:"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/edit.html:489
|
||||
msgid "This action cannot be undone."
|
||||
@@ -1891,7 +1952,7 @@ msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html:31
|
||||
msgid "Add a new web page change detection watch"
|
||||
msgstr "Přidejte nové monitory zjišťování změn webové stránky"
|
||||
msgstr "Přidejte nové hodinky zjišťování změn webové stránky"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html:34
|
||||
msgid "Watch this URL!"
|
||||
@@ -1907,7 +1968,7 @@ msgstr "Vytvořte odkaz ke sdílení"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html:45
|
||||
msgid "Tip: You can also add 'shared' watches."
|
||||
msgstr "Tip: Můžete také přidat „sdílené“ monitory."
|
||||
msgstr "Tip: Můžete také přidat „sdílené“ hodinky."
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html:45
|
||||
msgid "More info"
|
||||
@@ -1970,7 +2031,7 @@ msgstr "Vymazat/resetovat historii"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html:71
|
||||
msgid "Delete Watches?"
|
||||
msgstr "Smazat monitory?"
|
||||
msgstr "Smazat hodinky?"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html:72
|
||||
msgid ""
|
||||
@@ -2013,6 +2074,9 @@ msgstr ""
|
||||
"Nejsou nakonfigurována žádná sledování webových stránek, do výše "
|
||||
"uvedeného pole přidejte adresu URL neboNejsou nakonfigurována žádná "
|
||||
"sledování webových stránek, do výše uvedeného pole přidejte adresu URL "
|
||||
"neboNejsou nakonfigurována žádná sledování webových stránek, do výše "
|
||||
"uvedeného pole přidejte adresu URL neboNejsou nakonfigurována žádná "
|
||||
"sledování webových stránek, do výše uvedeného pole přidejte adresu URL "
|
||||
"nebo"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html:130
|
||||
@@ -2040,7 +2104,7 @@ msgid "No information"
|
||||
msgstr "Žádné informace"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html:234
|
||||
#: changedetectionio/templates/base.html:353
|
||||
#: changedetectionio/templates/base.html:248
|
||||
msgid "Checking now"
|
||||
msgstr "Probíhá kontrola"
|
||||
|
||||
@@ -2108,8 +2172,10 @@ msgstr "Hodnota ohraničujícího rámečku je příliš dlouhá"
|
||||
#: changedetectionio/processors/image_ssim_diff/forms.py:23
|
||||
msgid "Bounding box must be in format: x,y,width,height (integers only)"
|
||||
msgstr ""
|
||||
"Ohraničovací rámeček musí být ve formátu: x,y,šířka,výška (pouze celá "
|
||||
"čísla)"
|
||||
"Ohraničovací rámeček musí být ve "
|
||||
"formátu: x,y,šířka,výška (pouze celá "
|
||||
"čísla)Ohraničovací rámeček musí být ve "
|
||||
"formátu: x,y,šířka,výška (pouze celá čísla)"
|
||||
|
||||
#: changedetectionio/processors/image_ssim_diff/forms.py:29
|
||||
msgid "Bounding box values must be non-negative"
|
||||
@@ -2132,6 +2198,7 @@ msgid "Pixel Difference Sensitivity"
|
||||
msgstr "Rozdílová citlivost pixelů"
|
||||
|
||||
#: changedetectionio/processors/image_ssim_diff/forms.py:58
|
||||
#, fuzzy
|
||||
msgid "Use global default"
|
||||
msgstr "Použít výchozí nastavení systému"
|
||||
|
||||
@@ -2140,6 +2207,7 @@ msgid "Bounding Box"
|
||||
msgstr "Bounding Box"
|
||||
|
||||
#: changedetectionio/processors/image_ssim_diff/forms.py:76
|
||||
#, fuzzy
|
||||
msgid "Selection Mode"
|
||||
msgstr "Režim výběru:"
|
||||
|
||||
@@ -2206,6 +2274,7 @@ msgid "Follow price changes"
|
||||
msgstr "Sledujte změny cen"
|
||||
|
||||
#: changedetectionio/processors/restock_diff/forms.py:37
|
||||
#, fuzzy
|
||||
msgid "Restock & Price Detection"
|
||||
msgstr "Doplnění zásob a cena"
|
||||
|
||||
@@ -2225,245 +2294,69 @@ msgstr "Změny textu webové stránky/HTML, JSON a PDF"
|
||||
msgid "Detects all text changes where possible"
|
||||
msgstr "Detekuje všechny změny textu, kde je to možné"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:25
|
||||
msgid "Entry"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:153
|
||||
msgid "Actions"
|
||||
msgstr "Podmínky"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:172
|
||||
msgid "Add a row/rule after"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:173
|
||||
msgid "Remove this row/rule"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:174
|
||||
msgid "Verify this rule against current snapshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:184
|
||||
msgid ""
|
||||
"Error - This watch needs Chrome (with playwright/sockpuppetbrowser), but "
|
||||
"Chrome based fetching is not enabled."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:184
|
||||
msgid "Alternatively try our"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:184
|
||||
msgid ""
|
||||
"very affordable subscription based service which has all this setup for "
|
||||
"you"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
msgid "You may need to"
|
||||
msgstr "musíte"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
msgid "Enable playwright environment variable"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
msgid "and uncomment the"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
msgid "in the"
|
||||
msgstr "The"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
msgid "file"
|
||||
msgstr "Titul"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:240
|
||||
msgid "Set a hourly/week day schedule"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:247
|
||||
msgid "Schedule time limits"
|
||||
msgstr "Znovu zkontrolovat čas (minuty)"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:248
|
||||
msgid "Business hours"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:249
|
||||
msgid "Weekends"
|
||||
msgstr "týdny"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:250
|
||||
msgid "Reset"
|
||||
msgstr "Žádost"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:259
|
||||
msgid ""
|
||||
"Warning, one or more of your 'days' has a duration that would extend into"
|
||||
" the next day."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:260
|
||||
msgid "This could have unintended consequences."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:270
|
||||
msgid "More help and examples about using the scheduler"
|
||||
msgstr "Další nápověda a příklady zde"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:275
|
||||
msgid "Want to use a time schedule?"
|
||||
msgstr "Použijte časový plánovač"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:275
|
||||
msgid "First confirm/save your Time Zone Settings"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:284
|
||||
msgid ""
|
||||
"Triggers a change if this text appears, AND something changed in the "
|
||||
"document."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:284
|
||||
msgid "Triggered text"
|
||||
msgstr "Text chyby"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:285
|
||||
msgid "Ignored for calculating changes, but still shown."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:285
|
||||
msgid "Ignored text"
|
||||
msgstr "Text chyby"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:286
|
||||
msgid "No change-detection will occur because this text exists."
|
||||
msgstr "Blokovat detekci změn, když se text shoduje"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:286
|
||||
msgid "Blocked text"
|
||||
msgstr "Text chyby"
|
||||
|
||||
#: changedetectionio/templates/base.html:78
|
||||
#: changedetectionio/templates/base.html:168
|
||||
#: changedetectionio/templates/base.html:77
|
||||
msgid "GROUPS"
|
||||
msgstr "SKUPINY"
|
||||
|
||||
#: changedetectionio/templates/base.html:81
|
||||
#: changedetectionio/templates/base.html:169
|
||||
#: changedetectionio/templates/base.html:80
|
||||
msgid "SETTINGS"
|
||||
msgstr "NASTAVENÍ"
|
||||
|
||||
#: changedetectionio/templates/base.html:84
|
||||
#: changedetectionio/templates/base.html:170
|
||||
#: changedetectionio/templates/base.html:83
|
||||
msgid "IMPORT"
|
||||
msgstr "IMPORTOVAT"
|
||||
|
||||
#: changedetectionio/templates/base.html:87
|
||||
#: changedetectionio/templates/base.html:171
|
||||
#: changedetectionio/templates/base.html:86
|
||||
msgid "BACKUPS"
|
||||
msgstr "BACKUPS"
|
||||
msgstr "ZÁLOHY"
|
||||
|
||||
#: changedetectionio/templates/base.html:91
|
||||
#: changedetectionio/templates/base.html:173
|
||||
#: changedetectionio/templates/base.html:90
|
||||
msgid "EDIT"
|
||||
msgstr "UPRAVIT"
|
||||
|
||||
#: changedetectionio/templates/base.html:101
|
||||
#: changedetectionio/templates/base.html:177
|
||||
#: changedetectionio/templates/base.html:100
|
||||
msgid "LOG OUT"
|
||||
msgstr "ODHLÁSIT SE"
|
||||
|
||||
#: changedetectionio/templates/base.html:108
|
||||
#: changedetectionio/templates/base.html:109
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr "Vyhledejte nebo použijte klávesu Alt+S"
|
||||
|
||||
#: changedetectionio/templates/base.html:114
|
||||
#: changedetectionio/templates/base.html:116
|
||||
msgid "Toggle Light/Dark Mode"
|
||||
msgstr "Přepnout režim Světlý/Tmavý"
|
||||
|
||||
#: changedetectionio/templates/base.html:115
|
||||
#: changedetectionio/templates/base.html:117
|
||||
msgid "Toggle light/dark mode"
|
||||
msgstr "Přepínání mezi světlým/tmavým režimem"
|
||||
|
||||
#: changedetectionio/templates/base.html:125
|
||||
#: changedetectionio/templates/base.html:127
|
||||
msgid "Change Language"
|
||||
msgstr "Změnit jazyk"
|
||||
|
||||
#: changedetectionio/templates/base.html:126
|
||||
#: changedetectionio/templates/base.html:128
|
||||
msgid "Change language"
|
||||
msgstr "Změnit jazyk"
|
||||
|
||||
#: changedetectionio/templates/base.html:253
|
||||
msgid "Watch List"
|
||||
msgstr "# monitory"
|
||||
|
||||
#: changedetectionio/templates/base.html:258
|
||||
msgid "Watches"
|
||||
msgstr "# monitory"
|
||||
|
||||
#: changedetectionio/templates/base.html:261
|
||||
msgid "Queue Status"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:270
|
||||
msgid "Queue"
|
||||
msgstr "Ve frontě"
|
||||
|
||||
#: changedetectionio/templates/base.html:274
|
||||
#: changedetectionio/templates/base.html:279
|
||||
msgid "Settings"
|
||||
msgstr "NASTAVENÍ"
|
||||
|
||||
#: changedetectionio/templates/base.html:293
|
||||
msgid "Sitemap Crawler"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:318
|
||||
msgid "Sitemap"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:354
|
||||
#: changedetectionio/templates/base.html:249
|
||||
msgid "Real-time updates offline"
|
||||
msgstr "Aktualizace v reálném čase offline"
|
||||
|
||||
#: changedetectionio/templates/base.html:364
|
||||
#: changedetectionio/templates/base.html:259
|
||||
msgid "Select Language"
|
||||
msgstr "Vyberte Jazyk"
|
||||
|
||||
#: changedetectionio/templates/base.html:375
|
||||
#: changedetectionio/templates/base.html:270
|
||||
msgid ""
|
||||
"Language support is in beta, please help us improve by opening a PR on "
|
||||
"GitHub with any updates."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:387
|
||||
#: changedetectionio/templates/base.html:400
|
||||
msgid "Search"
|
||||
msgstr "Hledání"
|
||||
|
||||
#: changedetectionio/templates/base.html:392
|
||||
msgid "URL or Title"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:392
|
||||
msgid "in"
|
||||
msgstr "Více informací"
|
||||
|
||||
#: changedetectionio/templates/base.html:393
|
||||
msgid "Enter search term..."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/login.html:11
|
||||
#: changedetectionio/templates/login.html:10
|
||||
msgid "Password"
|
||||
msgstr "Heslo"
|
||||
|
||||
#: changedetectionio/templates/login.html:17
|
||||
#: changedetectionio/templates/login.html:16
|
||||
msgid "Login"
|
||||
msgstr "Přihlášení"
|
||||
|
||||
@@ -2534,7 +2427,7 @@ msgstr "Přihlášení"
|
||||
#~ msgstr "Neplatná hodnota."
|
||||
|
||||
#~ msgid "Watch"
|
||||
#~ msgstr "# monitory"
|
||||
#~ msgstr "# Hodinky"
|
||||
|
||||
#~ msgid "Processor"
|
||||
#~ msgstr "Procesor"
|
||||
@@ -2561,7 +2454,7 @@ msgstr "Přihlášení"
|
||||
#~ msgstr "Procesor – Čeho chcete dosáhnout?"
|
||||
|
||||
#~ msgid "Default timezone for watch check scheduler"
|
||||
#~ msgstr "Výchozí časové pásmo pro plánovač kontroly monitorů"
|
||||
#~ msgstr "Výchozí časové pásmo pro plánovač kontroly hodinek"
|
||||
|
||||
#~ msgid "Wait seconds before extracting text"
|
||||
#~ msgstr "sekund před extrahováním textu."
|
||||
@@ -2659,7 +2552,7 @@ msgstr "Přihlášení"
|
||||
#~ msgstr "Připojte snímek obrazovky k oznámení (pokud je to možné)"
|
||||
|
||||
#~ msgid "Match"
|
||||
#~ msgstr "# monitory"
|
||||
#~ msgstr "# Hodinky"
|
||||
|
||||
#~ msgid "Match all of the following"
|
||||
#~ msgstr "Spojte všechny následující položky"
|
||||
@@ -2766,7 +2659,7 @@ msgstr "Přihlášení"
|
||||
#~ "heslo"
|
||||
|
||||
#~ msgid "Hide muted watches from RSS feed"
|
||||
#~ msgstr "Skrýt ztlumené monitory ze zdroje RSS"
|
||||
#~ msgstr "Skrýt ztlumené hodinky ze zdroje RSS"
|
||||
|
||||
#~ msgid "Enable RSS reader mode "
|
||||
#~ msgstr "Povolit režim čtečky RSS"
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-01-03 14:31+0100\n"
|
||||
"POT-Creation-Date: 2026-01-02 16:07+0100\n"
|
||||
"PO-Revision-Date: 2026-01-02 15:32+0100\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: it\n"
|
||||
@@ -19,21 +19,21 @@ msgstr ""
|
||||
"Generated-By: Babel 2.17.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html:241
|
||||
#: changedetectionio/flask_app.py:214 changedetectionio/flask_app.py:226
|
||||
#: changedetectionio/flask_app.py:247
|
||||
#: changedetectionio/flask_app.py:213 changedetectionio/flask_app.py:225
|
||||
#: changedetectionio/flask_app.py:246
|
||||
#: changedetectionio/realtime/socket_server.py:171
|
||||
msgid "Not yet"
|
||||
msgstr "Non ancora"
|
||||
|
||||
#: changedetectionio/flask_app.py:534
|
||||
msgid "Already logged in"
|
||||
msgstr "Già autenticato"
|
||||
|
||||
#: changedetectionio/flask_app.py:536
|
||||
#: changedetectionio/flask_app.py:468
|
||||
msgid "You must be logged in, please log in."
|
||||
msgstr "Devi essere autenticato, effettua l'accesso."
|
||||
|
||||
#: changedetectionio/flask_app.py:551
|
||||
#: changedetectionio/flask_app.py:495
|
||||
msgid "Already logged in"
|
||||
msgstr "Già autenticato"
|
||||
|
||||
#: changedetectionio/flask_app.py:522
|
||||
msgid "Incorrect password"
|
||||
msgstr "Password errata"
|
||||
|
||||
@@ -605,8 +605,6 @@ msgid "Backups were deleted."
|
||||
msgstr "I backup sono stati eliminati."
|
||||
|
||||
#: changedetectionio/blueprint/backups/templates/overview.html:6
|
||||
#: changedetectionio/templates/base.html:282
|
||||
#: changedetectionio/templates/base.html:290
|
||||
msgid "Backups"
|
||||
msgstr "Backup"
|
||||
|
||||
@@ -1292,8 +1290,7 @@ msgid "Clear History!"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html:39
|
||||
#: changedetectionio/templates/base.html:379
|
||||
#: changedetectionio/templates/base.html:399
|
||||
#: changedetectionio/templates/base.html:274
|
||||
msgid "Cancel"
|
||||
msgstr "Annulla"
|
||||
|
||||
@@ -1323,11 +1320,11 @@ msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:53
|
||||
msgid "Words"
|
||||
msgstr "Parole"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:57
|
||||
msgid "Lines"
|
||||
msgstr "Righe"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:61
|
||||
msgid "Ignore Whitespace"
|
||||
@@ -1335,7 +1332,7 @@ msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:65
|
||||
msgid "Same/non-changed"
|
||||
msgstr "Uguale/non modificato"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:69
|
||||
msgid "Removed"
|
||||
@@ -1377,12 +1374,12 @@ msgstr ""
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:97
|
||||
#: changedetectionio/blueprint/ui/templates/preview.html:45
|
||||
msgid "Error Text"
|
||||
msgstr "Testo dell'errore"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:98
|
||||
#: changedetectionio/blueprint/ui/templates/preview.html:47
|
||||
msgid "Error Screenshot"
|
||||
msgstr "Screenshot dell'errore"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:99
|
||||
#: changedetectionio/blueprint/ui/templates/preview.html:50
|
||||
@@ -1392,7 +1389,7 @@ msgstr "Testo"
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:100
|
||||
#: changedetectionio/blueprint/ui/templates/preview.html:51
|
||||
msgid "Current screenshot"
|
||||
msgstr "Screenshot corrente"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:101
|
||||
msgid "Extract Data"
|
||||
@@ -1447,7 +1444,7 @@ msgstr ""
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:149
|
||||
#: changedetectionio/blueprint/ui/templates/preview.html:86
|
||||
msgid "Current screenshot from most recent request"
|
||||
msgstr "Screenshot corrente dalla richiesta più recente"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:151
|
||||
#: changedetectionio/blueprint/ui/templates/preview.html:88
|
||||
@@ -2052,7 +2049,7 @@ msgid "No information"
|
||||
msgstr "Nessuna informazione"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html:234
|
||||
#: changedetectionio/templates/base.html:353
|
||||
#: changedetectionio/templates/base.html:248
|
||||
msgid "Checking now"
|
||||
msgstr "Controllo in corso"
|
||||
|
||||
@@ -2119,9 +2116,7 @@ msgstr "Valore riquadro di selezione troppo lungo"
|
||||
|
||||
#: changedetectionio/processors/image_ssim_diff/forms.py:23
|
||||
msgid "Bounding box must be in format: x,y,width,height (integers only)"
|
||||
msgstr ""
|
||||
"Il riquadro deve essere nel formato: x,y,larghezza,altezza (solo numeri "
|
||||
"interi)"
|
||||
msgstr "Il riquadro deve essere nel formato: x,y,larghezza,altezza (solo numeri interi)"
|
||||
|
||||
#: changedetectionio/processors/image_ssim_diff/forms.py:29
|
||||
msgid "Bounding box values must be non-negative"
|
||||
@@ -2174,9 +2169,7 @@ msgstr "Rilevamento modifiche screenshot visivi"
|
||||
|
||||
#: changedetectionio/processors/image_ssim_diff/processor.py:22
|
||||
msgid "Compares screenshots using fast OpenCV algorithm, 10-100x faster than SSIM"
|
||||
msgstr ""
|
||||
"Confronta screenshot con algoritmo OpenCV veloce, 10-100x più veloce di "
|
||||
"SSIM"
|
||||
msgstr "Confronta screenshot con algoritmo OpenCV veloce, 10-100x più veloce di SSIM"
|
||||
|
||||
#: changedetectionio/processors/restock_diff/forms.py:15
|
||||
msgid "Re-stock detection"
|
||||
@@ -2240,233 +2233,59 @@ msgstr "Modifiche testo/HTML, JSON e PDF"
|
||||
msgid "Detects all text changes where possible"
|
||||
msgstr "Rileva tutte le modifiche di testo possibili"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:25
|
||||
msgid "Entry"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:153
|
||||
#, fuzzy
|
||||
msgid "Actions"
|
||||
msgstr "Condizioni"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:172
|
||||
msgid "Add a row/rule after"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:173
|
||||
msgid "Remove this row/rule"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:174
|
||||
msgid "Verify this rule against current snapshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:184
|
||||
msgid ""
|
||||
"Error - This watch needs Chrome (with playwright/sockpuppetbrowser), but "
|
||||
"Chrome based fetching is not enabled."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:184
|
||||
msgid "Alternatively try our"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:184
|
||||
msgid ""
|
||||
"very affordable subscription based service which has all this setup for "
|
||||
"you"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
msgid "You may need to"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
msgid "Enable playwright environment variable"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
msgid "and uncomment the"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
#, fuzzy
|
||||
msgid "in the"
|
||||
msgstr "Silenzia"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
#, fuzzy
|
||||
msgid "file"
|
||||
msgstr "Titolo"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:240
|
||||
msgid "Set a hourly/week day schedule"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:247
|
||||
#, fuzzy
|
||||
msgid "Schedule time limits"
|
||||
msgstr "Tempo di ricontrollo (minuti)"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:248
|
||||
msgid "Business hours"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:249
|
||||
#, fuzzy
|
||||
msgid "Weekends"
|
||||
msgstr "Settimane"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:250
|
||||
#, fuzzy
|
||||
msgid "Reset"
|
||||
msgstr "Richiesta"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:259
|
||||
msgid ""
|
||||
"Warning, one or more of your 'days' has a duration that would extend into"
|
||||
" the next day."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:260
|
||||
msgid "This could have unintended consequences."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:270
|
||||
msgid "More help and examples about using the scheduler"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:275
|
||||
#, fuzzy
|
||||
msgid "Want to use a time schedule?"
|
||||
msgstr "Usa pianificazione oraria"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:275
|
||||
msgid "First confirm/save your Time Zone Settings"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:284
|
||||
msgid ""
|
||||
"Triggers a change if this text appears, AND something changed in the "
|
||||
"document."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:284
|
||||
#, fuzzy
|
||||
msgid "Triggered text"
|
||||
msgstr "Ignora testo"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:285
|
||||
msgid "Ignored for calculating changes, but still shown."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:285
|
||||
#, fuzzy
|
||||
msgid "Ignored text"
|
||||
msgstr "Ignora testo"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:286
|
||||
#, fuzzy
|
||||
msgid "No change-detection will occur because this text exists."
|
||||
msgstr "Blocca rilevamento modifiche quando il testo corrisponde"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:286
|
||||
#, fuzzy
|
||||
msgid "Blocked text"
|
||||
msgstr "Ignora testo"
|
||||
|
||||
#: changedetectionio/templates/base.html:78
|
||||
#: changedetectionio/templates/base.html:168
|
||||
#: changedetectionio/templates/base.html:77
|
||||
msgid "GROUPS"
|
||||
msgstr "GRUPPI"
|
||||
|
||||
#: changedetectionio/templates/base.html:81
|
||||
#: changedetectionio/templates/base.html:169
|
||||
#: changedetectionio/templates/base.html:80
|
||||
msgid "SETTINGS"
|
||||
msgstr "IMPOSTAZIONI"
|
||||
|
||||
#: changedetectionio/templates/base.html:84
|
||||
#: changedetectionio/templates/base.html:170
|
||||
#: changedetectionio/templates/base.html:83
|
||||
msgid "IMPORT"
|
||||
msgstr "IMPORTA"
|
||||
|
||||
#: changedetectionio/templates/base.html:87
|
||||
#: changedetectionio/templates/base.html:171
|
||||
#: changedetectionio/templates/base.html:86
|
||||
msgid "BACKUPS"
|
||||
msgstr "BACKUP"
|
||||
|
||||
#: changedetectionio/templates/base.html:91
|
||||
#: changedetectionio/templates/base.html:173
|
||||
#: changedetectionio/templates/base.html:90
|
||||
msgid "EDIT"
|
||||
msgstr "MODIFICA"
|
||||
|
||||
#: changedetectionio/templates/base.html:101
|
||||
#: changedetectionio/templates/base.html:177
|
||||
#: changedetectionio/templates/base.html:100
|
||||
msgid "LOG OUT"
|
||||
msgstr "ESCI"
|
||||
|
||||
#: changedetectionio/templates/base.html:108
|
||||
#: changedetectionio/templates/base.html:109
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr "Cerca, o usa il tasto Alt+S"
|
||||
|
||||
#: changedetectionio/templates/base.html:114
|
||||
#: changedetectionio/templates/base.html:116
|
||||
msgid "Toggle Light/Dark Mode"
|
||||
msgstr "Cambia Modalità Chiaro/Scuro"
|
||||
|
||||
#: changedetectionio/templates/base.html:115
|
||||
#: changedetectionio/templates/base.html:117
|
||||
msgid "Toggle light/dark mode"
|
||||
msgstr "Cambia modalità chiaro/scuro"
|
||||
|
||||
#: changedetectionio/templates/base.html:125
|
||||
#: changedetectionio/templates/base.html:127
|
||||
msgid "Change Language"
|
||||
msgstr "Cambia Lingua"
|
||||
|
||||
#: changedetectionio/templates/base.html:126
|
||||
#: changedetectionio/templates/base.html:128
|
||||
msgid "Change language"
|
||||
msgstr "Cambia lingua"
|
||||
|
||||
#: changedetectionio/templates/base.html:253
|
||||
#, fuzzy
|
||||
msgid "Watch List"
|
||||
msgstr "Osserva"
|
||||
|
||||
#: changedetectionio/templates/base.html:258
|
||||
#, fuzzy
|
||||
msgid "Watches"
|
||||
msgstr "Osserva"
|
||||
|
||||
#: changedetectionio/templates/base.html:261
|
||||
msgid "Queue Status"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:270
|
||||
#, fuzzy
|
||||
msgid "Queue"
|
||||
msgstr "In coda"
|
||||
|
||||
#: changedetectionio/templates/base.html:274
|
||||
#: changedetectionio/templates/base.html:279
|
||||
#, fuzzy
|
||||
msgid "Settings"
|
||||
msgstr "IMPOSTAZIONI"
|
||||
|
||||
#: changedetectionio/templates/base.html:293
|
||||
msgid "Sitemap Crawler"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:318
|
||||
msgid "Sitemap"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:354
|
||||
#: changedetectionio/templates/base.html:249
|
||||
msgid "Real-time updates offline"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:364
|
||||
#: changedetectionio/templates/base.html:259
|
||||
msgid "Select Language"
|
||||
msgstr "Seleziona Lingua"
|
||||
|
||||
#: changedetectionio/templates/base.html:375
|
||||
#: changedetectionio/templates/base.html:270
|
||||
msgid ""
|
||||
"Language support is in beta, please help us improve by opening a PR on "
|
||||
"GitHub with any updates."
|
||||
@@ -2474,30 +2293,11 @@ msgstr ""
|
||||
"Il supporto linguistico è in versione beta, aiutaci a migliorare aprendo "
|
||||
"una PR su GitHub con eventuali aggiornamenti."
|
||||
|
||||
#: changedetectionio/templates/base.html:387
|
||||
#: changedetectionio/templates/base.html:400
|
||||
#, fuzzy
|
||||
msgid "Search"
|
||||
msgstr "Ricerca in corso"
|
||||
|
||||
#: changedetectionio/templates/base.html:392
|
||||
msgid "URL or Title"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:392
|
||||
#, fuzzy
|
||||
msgid "in"
|
||||
msgstr "Info"
|
||||
|
||||
#: changedetectionio/templates/base.html:393
|
||||
msgid "Enter search term..."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/login.html:11
|
||||
#: changedetectionio/templates/login.html:10
|
||||
msgid "Password"
|
||||
msgstr "Password"
|
||||
|
||||
#: changedetectionio/templates/login.html:17
|
||||
#: changedetectionio/templates/login.html:16
|
||||
msgid "Login"
|
||||
msgstr "Accedi"
|
||||
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-01-03 14:31+0100\n"
|
||||
"POT-Creation-Date: 2026-01-02 16:07+0100\n"
|
||||
"PO-Revision-Date: 2026-01-02 11:40+0100\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: ko\n"
|
||||
@@ -19,21 +19,21 @@ msgstr ""
|
||||
"Generated-By: Babel 2.17.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html:241
|
||||
#: changedetectionio/flask_app.py:214 changedetectionio/flask_app.py:226
|
||||
#: changedetectionio/flask_app.py:247
|
||||
#: changedetectionio/flask_app.py:213 changedetectionio/flask_app.py:225
|
||||
#: changedetectionio/flask_app.py:246
|
||||
#: changedetectionio/realtime/socket_server.py:171
|
||||
msgid "Not yet"
|
||||
msgstr "아직 아님"
|
||||
|
||||
#: changedetectionio/flask_app.py:534
|
||||
msgid "Already logged in"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/flask_app.py:536
|
||||
#: changedetectionio/flask_app.py:468
|
||||
msgid "You must be logged in, please log in."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/flask_app.py:551
|
||||
#: changedetectionio/flask_app.py:495
|
||||
msgid "Already logged in"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/flask_app.py:522
|
||||
#, fuzzy
|
||||
msgid "Incorrect password"
|
||||
msgstr "비밀번호"
|
||||
@@ -622,8 +622,6 @@ msgid "Backups were deleted."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/backups/templates/overview.html:6
|
||||
#: changedetectionio/templates/base.html:282
|
||||
#: changedetectionio/templates/base.html:290
|
||||
msgid "Backups"
|
||||
msgstr "백업"
|
||||
|
||||
@@ -1309,8 +1307,7 @@ msgid "Clear History!"
|
||||
msgstr "기록 지우기"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html:39
|
||||
#: changedetectionio/templates/base.html:379
|
||||
#: changedetectionio/templates/base.html:399
|
||||
#: changedetectionio/templates/base.html:274
|
||||
msgid "Cancel"
|
||||
msgstr "취소"
|
||||
|
||||
@@ -1340,11 +1337,11 @@ msgstr "에게"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:53
|
||||
msgid "Words"
|
||||
msgstr "단어"
|
||||
msgstr "비밀번호"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:57
|
||||
msgid "Lines"
|
||||
msgstr "줄"
|
||||
msgstr "로그인"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:61
|
||||
msgid "Ignore Whitespace"
|
||||
@@ -1352,7 +1349,7 @@ msgstr "공백 무시"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:65
|
||||
msgid "Same/non-changed"
|
||||
msgstr "동일/변경되지 않음"
|
||||
msgstr "변경됨"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/diff.html:69
|
||||
msgid "Removed"
|
||||
@@ -2069,7 +2066,7 @@ msgid "No information"
|
||||
msgstr "정보 없음"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html:234
|
||||
#: changedetectionio/templates/base.html:353
|
||||
#: changedetectionio/templates/base.html:248
|
||||
msgid "Checking now"
|
||||
msgstr "지금 확인 중"
|
||||
|
||||
@@ -2255,264 +2252,69 @@ msgstr "웹페이지 텍스트/HTML, JSON 및 PDF 변경"
|
||||
msgid "Detects all text changes where possible"
|
||||
msgstr "가능한 경우 모든 텍스트 변경 사항을 감지합니다."
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:25
|
||||
msgid "Entry"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:153
|
||||
#, fuzzy
|
||||
msgid "Actions"
|
||||
msgstr "정황"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:172
|
||||
msgid "Add a row/rule after"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:173
|
||||
msgid "Remove this row/rule"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:174
|
||||
msgid "Verify this rule against current snapshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:184
|
||||
msgid ""
|
||||
"Error - This watch needs Chrome (with playwright/sockpuppetbrowser), but "
|
||||
"Chrome based fetching is not enabled."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:184
|
||||
msgid "Alternatively try our"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:184
|
||||
msgid ""
|
||||
"very affordable subscription based service which has all this setup for "
|
||||
"you"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
#, fuzzy
|
||||
msgid "You may need to"
|
||||
msgstr "당신은"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
msgid "Enable playwright environment variable"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
msgid "and uncomment the"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
#, fuzzy
|
||||
msgid "in the"
|
||||
msgstr "그만큼"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
#, fuzzy
|
||||
msgid "file"
|
||||
msgstr "제목"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:240
|
||||
msgid "Set a hourly/week day schedule"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:247
|
||||
#, fuzzy
|
||||
msgid "Schedule time limits"
|
||||
msgstr "재확인 시간(분)"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:248
|
||||
msgid "Business hours"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:249
|
||||
#, fuzzy
|
||||
msgid "Weekends"
|
||||
msgstr "주"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:250
|
||||
#, fuzzy
|
||||
msgid "Reset"
|
||||
msgstr "요구"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:259
|
||||
msgid ""
|
||||
"Warning, one or more of your 'days' has a duration that would extend into"
|
||||
" the next day."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:260
|
||||
msgid "This could have unintended consequences."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:270
|
||||
#, fuzzy
|
||||
msgid "More help and examples about using the scheduler"
|
||||
msgstr "여기에 더 많은 도움말과 예시가 있습니다."
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:275
|
||||
#, fuzzy
|
||||
msgid "Want to use a time schedule?"
|
||||
msgstr "시간 스케줄러 사용"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:275
|
||||
msgid "First confirm/save your Time Zone Settings"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:284
|
||||
msgid ""
|
||||
"Triggers a change if this text appears, AND something changed in the "
|
||||
"document."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:284
|
||||
#, fuzzy
|
||||
msgid "Triggered text"
|
||||
msgstr "오류 텍스트"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:285
|
||||
msgid "Ignored for calculating changes, but still shown."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:285
|
||||
#, fuzzy
|
||||
msgid "Ignored text"
|
||||
msgstr "오류 텍스트"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:286
|
||||
#, fuzzy
|
||||
msgid "No change-detection will occur because this text exists."
|
||||
msgstr "텍스트가 일치하는 동안 변경 감지 차단"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:286
|
||||
#, fuzzy
|
||||
msgid "Blocked text"
|
||||
msgstr "오류 텍스트"
|
||||
|
||||
#: changedetectionio/templates/base.html:78
|
||||
#: changedetectionio/templates/base.html:168
|
||||
#: changedetectionio/templates/base.html:77
|
||||
msgid "GROUPS"
|
||||
msgstr "여러 떼"
|
||||
|
||||
#: changedetectionio/templates/base.html:81
|
||||
#: changedetectionio/templates/base.html:169
|
||||
#: changedetectionio/templates/base.html:80
|
||||
msgid "SETTINGS"
|
||||
msgstr "설정"
|
||||
|
||||
#: changedetectionio/templates/base.html:84
|
||||
#: changedetectionio/templates/base.html:170
|
||||
#: changedetectionio/templates/base.html:83
|
||||
msgid "IMPORT"
|
||||
msgstr "수입"
|
||||
|
||||
#: changedetectionio/templates/base.html:87
|
||||
#: changedetectionio/templates/base.html:171
|
||||
#: changedetectionio/templates/base.html:86
|
||||
msgid "BACKUPS"
|
||||
msgstr "백업"
|
||||
|
||||
#: changedetectionio/templates/base.html:91
|
||||
#: changedetectionio/templates/base.html:173
|
||||
#: changedetectionio/templates/base.html:90
|
||||
msgid "EDIT"
|
||||
msgstr "편집하다"
|
||||
|
||||
#: changedetectionio/templates/base.html:101
|
||||
#: changedetectionio/templates/base.html:177
|
||||
#: changedetectionio/templates/base.html:100
|
||||
msgid "LOG OUT"
|
||||
msgstr "로그아웃"
|
||||
|
||||
#: changedetectionio/templates/base.html:108
|
||||
#: changedetectionio/templates/base.html:109
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr "검색 또는 Alt+S 키 사용"
|
||||
|
||||
#: changedetectionio/templates/base.html:114
|
||||
#: changedetectionio/templates/base.html:116
|
||||
msgid "Toggle Light/Dark Mode"
|
||||
msgstr "밝은/어두운 모드 전환"
|
||||
|
||||
#: changedetectionio/templates/base.html:115
|
||||
#: changedetectionio/templates/base.html:117
|
||||
msgid "Toggle light/dark mode"
|
||||
msgstr "밝은/어두운 모드 전환"
|
||||
|
||||
#: changedetectionio/templates/base.html:125
|
||||
#: changedetectionio/templates/base.html:127
|
||||
msgid "Change Language"
|
||||
msgstr "언어 변경"
|
||||
|
||||
#: changedetectionio/templates/base.html:126
|
||||
#: changedetectionio/templates/base.html:128
|
||||
msgid "Change language"
|
||||
msgstr "언어 변경"
|
||||
|
||||
#: changedetectionio/templates/base.html:253
|
||||
#, fuzzy
|
||||
msgid "Watch List"
|
||||
msgstr "# 시계"
|
||||
|
||||
#: changedetectionio/templates/base.html:258
|
||||
#, fuzzy
|
||||
msgid "Watches"
|
||||
msgstr "# 시계"
|
||||
|
||||
#: changedetectionio/templates/base.html:261
|
||||
msgid "Queue Status"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:270
|
||||
#, fuzzy
|
||||
msgid "Queue"
|
||||
msgstr "대기 중"
|
||||
|
||||
#: changedetectionio/templates/base.html:274
|
||||
#: changedetectionio/templates/base.html:279
|
||||
#, fuzzy
|
||||
msgid "Settings"
|
||||
msgstr "설정"
|
||||
|
||||
#: changedetectionio/templates/base.html:293
|
||||
msgid "Sitemap Crawler"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:318
|
||||
msgid "Sitemap"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:354
|
||||
#: changedetectionio/templates/base.html:249
|
||||
msgid "Real-time updates offline"
|
||||
msgstr "실시간 업데이트 오프라인"
|
||||
|
||||
#: changedetectionio/templates/base.html:364
|
||||
#: changedetectionio/templates/base.html:259
|
||||
msgid "Select Language"
|
||||
msgstr "언어 선택"
|
||||
|
||||
#: changedetectionio/templates/base.html:375
|
||||
#: changedetectionio/templates/base.html:270
|
||||
msgid ""
|
||||
"Language support is in beta, please help us improve by opening a PR on "
|
||||
"GitHub with any updates."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:387
|
||||
#: changedetectionio/templates/base.html:400
|
||||
#, fuzzy
|
||||
msgid "Search"
|
||||
msgstr "수색"
|
||||
|
||||
#: changedetectionio/templates/base.html:392
|
||||
msgid "URL or Title"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:392
|
||||
#, fuzzy
|
||||
msgid "in"
|
||||
msgstr "추가 정보"
|
||||
|
||||
#: changedetectionio/templates/base.html:393
|
||||
msgid "Enter search term..."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/login.html:11
|
||||
#: changedetectionio/templates/login.html:10
|
||||
msgid "Password"
|
||||
msgstr "비밀번호"
|
||||
|
||||
#: changedetectionio/templates/login.html:17
|
||||
#: changedetectionio/templates/login.html:16
|
||||
msgid "Login"
|
||||
msgstr "로그인"
|
||||
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-01-03 14:31+0100\n"
|
||||
"POT-Creation-Date: 2026-01-02 16:07+0100\n"
|
||||
"PO-Revision-Date: 2026-01-02 11:54+0100\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: zh\n"
|
||||
@@ -19,21 +19,21 @@ msgstr ""
|
||||
"Generated-By: Babel 2.17.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html:241
|
||||
#: changedetectionio/flask_app.py:214 changedetectionio/flask_app.py:226
|
||||
#: changedetectionio/flask_app.py:247
|
||||
#: changedetectionio/flask_app.py:213 changedetectionio/flask_app.py:225
|
||||
#: changedetectionio/flask_app.py:246
|
||||
#: changedetectionio/realtime/socket_server.py:171
|
||||
msgid "Not yet"
|
||||
msgstr "还没有"
|
||||
|
||||
#: changedetectionio/flask_app.py:534
|
||||
msgid "Already logged in"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/flask_app.py:536
|
||||
#: changedetectionio/flask_app.py:468
|
||||
msgid "You must be logged in, please log in."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/flask_app.py:551
|
||||
#: changedetectionio/flask_app.py:495
|
||||
msgid "Already logged in"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/flask_app.py:522
|
||||
#, fuzzy
|
||||
msgid "Incorrect password"
|
||||
msgstr "密码"
|
||||
@@ -622,8 +622,6 @@ msgid "Backups were deleted."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/backups/templates/overview.html:6
|
||||
#: changedetectionio/templates/base.html:282
|
||||
#: changedetectionio/templates/base.html:290
|
||||
msgid "Backups"
|
||||
msgstr "备份"
|
||||
|
||||
@@ -1309,8 +1307,7 @@ msgid "Clear History!"
|
||||
msgstr "清晰的历史记录"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html:39
|
||||
#: changedetectionio/templates/base.html:379
|
||||
#: changedetectionio/templates/base.html:399
|
||||
#: changedetectionio/templates/base.html:274
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
@@ -2069,7 +2066,7 @@ msgid "No information"
|
||||
msgstr "暂无信息"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html:234
|
||||
#: changedetectionio/templates/base.html:353
|
||||
#: changedetectionio/templates/base.html:248
|
||||
msgid "Checking now"
|
||||
msgstr "立即检查"
|
||||
|
||||
@@ -2255,264 +2252,69 @@ msgstr "网页文本/HTML、JSON 和 PDF 更改"
|
||||
msgid "Detects all text changes where possible"
|
||||
msgstr "尽可能检测所有文本更改"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:25
|
||||
msgid "Entry"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:153
|
||||
#, fuzzy
|
||||
msgid "Actions"
|
||||
msgstr "状况"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:172
|
||||
msgid "Add a row/rule after"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:173
|
||||
msgid "Remove this row/rule"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:174
|
||||
msgid "Verify this rule against current snapshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:184
|
||||
msgid ""
|
||||
"Error - This watch needs Chrome (with playwright/sockpuppetbrowser), but "
|
||||
"Chrome based fetching is not enabled."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:184
|
||||
msgid "Alternatively try our"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:184
|
||||
msgid ""
|
||||
"very affordable subscription based service which has all this setup for "
|
||||
"you"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
#, fuzzy
|
||||
msgid "You may need to"
|
||||
msgstr "你需要"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
msgid "Enable playwright environment variable"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
msgid "and uncomment the"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
#, fuzzy
|
||||
msgid "in the"
|
||||
msgstr "这"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
#, fuzzy
|
||||
msgid "file"
|
||||
msgstr "标题"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:240
|
||||
msgid "Set a hourly/week day schedule"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:247
|
||||
#, fuzzy
|
||||
msgid "Schedule time limits"
|
||||
msgstr "复检时间(分钟)"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:248
|
||||
msgid "Business hours"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:249
|
||||
#, fuzzy
|
||||
msgid "Weekends"
|
||||
msgstr "周数"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:250
|
||||
#, fuzzy
|
||||
msgid "Reset"
|
||||
msgstr "要求"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:259
|
||||
msgid ""
|
||||
"Warning, one or more of your 'days' has a duration that would extend into"
|
||||
" the next day."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:260
|
||||
msgid "This could have unintended consequences."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:270
|
||||
#, fuzzy
|
||||
msgid "More help and examples about using the scheduler"
|
||||
msgstr "更多帮助和示例请参见此处"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:275
|
||||
#, fuzzy
|
||||
msgid "Want to use a time schedule?"
|
||||
msgstr "使用时间调度器"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:275
|
||||
msgid "First confirm/save your Time Zone Settings"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:284
|
||||
msgid ""
|
||||
"Triggers a change if this text appears, AND something changed in the "
|
||||
"document."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:284
|
||||
#, fuzzy
|
||||
msgid "Triggered text"
|
||||
msgstr "错误文本"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:285
|
||||
msgid "Ignored for calculating changes, but still shown."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:285
|
||||
#, fuzzy
|
||||
msgid "Ignored text"
|
||||
msgstr "错误文本"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:286
|
||||
#, fuzzy
|
||||
msgid "No change-detection will occur because this text exists."
|
||||
msgstr "文本匹配时阻止更改检测"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:286
|
||||
#, fuzzy
|
||||
msgid "Blocked text"
|
||||
msgstr "错误文本"
|
||||
|
||||
#: changedetectionio/templates/base.html:78
|
||||
#: changedetectionio/templates/base.html:168
|
||||
#: changedetectionio/templates/base.html:77
|
||||
msgid "GROUPS"
|
||||
msgstr "团体"
|
||||
|
||||
#: changedetectionio/templates/base.html:81
|
||||
#: changedetectionio/templates/base.html:169
|
||||
#: changedetectionio/templates/base.html:80
|
||||
msgid "SETTINGS"
|
||||
msgstr "设置"
|
||||
|
||||
#: changedetectionio/templates/base.html:84
|
||||
#: changedetectionio/templates/base.html:170
|
||||
#: changedetectionio/templates/base.html:83
|
||||
msgid "IMPORT"
|
||||
msgstr "进口"
|
||||
|
||||
#: changedetectionio/templates/base.html:87
|
||||
#: changedetectionio/templates/base.html:171
|
||||
#: changedetectionio/templates/base.html:86
|
||||
msgid "BACKUPS"
|
||||
msgstr "备份"
|
||||
|
||||
#: changedetectionio/templates/base.html:91
|
||||
#: changedetectionio/templates/base.html:173
|
||||
#: changedetectionio/templates/base.html:90
|
||||
msgid "EDIT"
|
||||
msgstr "编辑"
|
||||
|
||||
#: changedetectionio/templates/base.html:101
|
||||
#: changedetectionio/templates/base.html:177
|
||||
#: changedetectionio/templates/base.html:100
|
||||
msgid "LOG OUT"
|
||||
msgstr "退出"
|
||||
|
||||
#: changedetectionio/templates/base.html:108
|
||||
#: changedetectionio/templates/base.html:109
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr "搜索或使用 Alt+S 键"
|
||||
|
||||
#: changedetectionio/templates/base.html:114
|
||||
#: changedetectionio/templates/base.html:116
|
||||
msgid "Toggle Light/Dark Mode"
|
||||
msgstr "切换亮/暗模式"
|
||||
|
||||
#: changedetectionio/templates/base.html:115
|
||||
#: changedetectionio/templates/base.html:117
|
||||
msgid "Toggle light/dark mode"
|
||||
msgstr "切换亮/暗模式"
|
||||
|
||||
#: changedetectionio/templates/base.html:125
|
||||
#: changedetectionio/templates/base.html:127
|
||||
msgid "Change Language"
|
||||
msgstr "更改语言"
|
||||
|
||||
#: changedetectionio/templates/base.html:126
|
||||
#: changedetectionio/templates/base.html:128
|
||||
msgid "Change language"
|
||||
msgstr "更改语言"
|
||||
|
||||
#: changedetectionio/templates/base.html:253
|
||||
#, fuzzy
|
||||
msgid "Watch List"
|
||||
msgstr "# 手表"
|
||||
|
||||
#: changedetectionio/templates/base.html:258
|
||||
#, fuzzy
|
||||
msgid "Watches"
|
||||
msgstr "# 手表"
|
||||
|
||||
#: changedetectionio/templates/base.html:261
|
||||
msgid "Queue Status"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:270
|
||||
#, fuzzy
|
||||
msgid "Queue"
|
||||
msgstr "排队"
|
||||
|
||||
#: changedetectionio/templates/base.html:274
|
||||
#: changedetectionio/templates/base.html:279
|
||||
#, fuzzy
|
||||
msgid "Settings"
|
||||
msgstr "设置"
|
||||
|
||||
#: changedetectionio/templates/base.html:293
|
||||
msgid "Sitemap Crawler"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:318
|
||||
msgid "Sitemap"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:354
|
||||
#: changedetectionio/templates/base.html:249
|
||||
msgid "Real-time updates offline"
|
||||
msgstr "离线实时更新"
|
||||
|
||||
#: changedetectionio/templates/base.html:364
|
||||
#: changedetectionio/templates/base.html:259
|
||||
msgid "Select Language"
|
||||
msgstr "选择语言"
|
||||
|
||||
#: changedetectionio/templates/base.html:375
|
||||
#: changedetectionio/templates/base.html:270
|
||||
msgid ""
|
||||
"Language support is in beta, please help us improve by opening a PR on "
|
||||
"GitHub with any updates."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:387
|
||||
#: changedetectionio/templates/base.html:400
|
||||
#, fuzzy
|
||||
msgid "Search"
|
||||
msgstr "搜寻中"
|
||||
|
||||
#: changedetectionio/templates/base.html:392
|
||||
msgid "URL or Title"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:392
|
||||
#, fuzzy
|
||||
msgid "in"
|
||||
msgstr "更多信息"
|
||||
|
||||
#: changedetectionio/templates/base.html:393
|
||||
msgid "Enter search term..."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/login.html:11
|
||||
#: changedetectionio/templates/login.html:10
|
||||
msgid "Password"
|
||||
msgstr "密码"
|
||||
|
||||
#: changedetectionio/templates/login.html:17
|
||||
#: changedetectionio/templates/login.html:16
|
||||
msgid "Login"
|
||||
msgstr "登录"
|
||||
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-01-03 14:31+0100\n"
|
||||
"POT-Creation-Date: 2026-01-02 16:07+0100\n"
|
||||
"PO-Revision-Date: 2026-01-02 12:37+0100\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: zh_Hant_TW\n"
|
||||
@@ -19,21 +19,21 @@ msgstr ""
|
||||
"Generated-By: Babel 2.17.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html:241
|
||||
#: changedetectionio/flask_app.py:214 changedetectionio/flask_app.py:226
|
||||
#: changedetectionio/flask_app.py:247
|
||||
#: changedetectionio/flask_app.py:213 changedetectionio/flask_app.py:225
|
||||
#: changedetectionio/flask_app.py:246
|
||||
#: changedetectionio/realtime/socket_server.py:171
|
||||
msgid "Not yet"
|
||||
msgstr "還沒有"
|
||||
|
||||
#: changedetectionio/flask_app.py:534
|
||||
msgid "Already logged in"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/flask_app.py:536
|
||||
#: changedetectionio/flask_app.py:468
|
||||
msgid "You must be logged in, please log in."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/flask_app.py:551
|
||||
#: changedetectionio/flask_app.py:495
|
||||
msgid "Already logged in"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/flask_app.py:522
|
||||
#, fuzzy
|
||||
msgid "Incorrect password"
|
||||
msgstr "密碼"
|
||||
@@ -622,8 +622,6 @@ msgid "Backups were deleted."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/backups/templates/overview.html:6
|
||||
#: changedetectionio/templates/base.html:282
|
||||
#: changedetectionio/templates/base.html:290
|
||||
msgid "Backups"
|
||||
msgstr "備份"
|
||||
|
||||
@@ -1309,8 +1307,7 @@ msgid "Clear History!"
|
||||
msgstr "清除歷史!"
|
||||
|
||||
#: changedetectionio/blueprint/ui/templates/clear_all_history.html:39
|
||||
#: changedetectionio/templates/base.html:379
|
||||
#: changedetectionio/templates/base.html:399
|
||||
#: changedetectionio/templates/base.html:274
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
@@ -2069,7 +2066,7 @@ msgid "No information"
|
||||
msgstr "暫無信息"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html:234
|
||||
#: changedetectionio/templates/base.html:353
|
||||
#: changedetectionio/templates/base.html:248
|
||||
msgid "Checking now"
|
||||
msgstr "立即檢查"
|
||||
|
||||
@@ -2255,264 +2252,69 @@ msgstr "網頁文本/HTML、JSON 和 PDF 更改"
|
||||
msgid "Detects all text changes where possible"
|
||||
msgstr "盡可能檢測所有文本更改"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:25
|
||||
msgid "Entry"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:153
|
||||
#, fuzzy
|
||||
msgid "Actions"
|
||||
msgstr "狀況"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:172
|
||||
msgid "Add a row/rule after"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:173
|
||||
msgid "Remove this row/rule"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:174
|
||||
msgid "Verify this rule against current snapshot"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:184
|
||||
msgid ""
|
||||
"Error - This watch needs Chrome (with playwright/sockpuppetbrowser), but "
|
||||
"Chrome based fetching is not enabled."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:184
|
||||
msgid "Alternatively try our"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:184
|
||||
msgid ""
|
||||
"very affordable subscription based service which has all this setup for "
|
||||
"you"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
#, fuzzy
|
||||
msgid "You may need to"
|
||||
msgstr "你需要"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
msgid "Enable playwright environment variable"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
msgid "and uncomment the"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
#, fuzzy
|
||||
msgid "in the"
|
||||
msgstr "這"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:185
|
||||
#, fuzzy
|
||||
msgid "file"
|
||||
msgstr "標題"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:240
|
||||
msgid "Set a hourly/week day schedule"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:247
|
||||
#, fuzzy
|
||||
msgid "Schedule time limits"
|
||||
msgstr "複檢時間(分鐘)"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:248
|
||||
msgid "Business hours"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:249
|
||||
#, fuzzy
|
||||
msgid "Weekends"
|
||||
msgstr "週數"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:250
|
||||
#, fuzzy
|
||||
msgid "Reset"
|
||||
msgstr "要求"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:259
|
||||
msgid ""
|
||||
"Warning, one or more of your 'days' has a duration that would extend into"
|
||||
" the next day."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:260
|
||||
msgid "This could have unintended consequences."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:270
|
||||
#, fuzzy
|
||||
msgid "More help and examples about using the scheduler"
|
||||
msgstr "更多幫助和示例請參見此處"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:275
|
||||
#, fuzzy
|
||||
msgid "Want to use a time schedule?"
|
||||
msgstr "使用時間調度器"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:275
|
||||
msgid "First confirm/save your Time Zone Settings"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:284
|
||||
msgid ""
|
||||
"Triggers a change if this text appears, AND something changed in the "
|
||||
"document."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:284
|
||||
#, fuzzy
|
||||
msgid "Triggered text"
|
||||
msgstr "錯誤文本"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:285
|
||||
msgid "Ignored for calculating changes, but still shown."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:285
|
||||
#, fuzzy
|
||||
msgid "Ignored text"
|
||||
msgstr "錯誤文本"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:286
|
||||
#, fuzzy
|
||||
msgid "No change-detection will occur because this text exists."
|
||||
msgstr "文本匹配時阻止更改檢測"
|
||||
|
||||
#: changedetectionio/templates/_helpers.html:286
|
||||
#, fuzzy
|
||||
msgid "Blocked text"
|
||||
msgstr "錯誤文本"
|
||||
|
||||
#: changedetectionio/templates/base.html:78
|
||||
#: changedetectionio/templates/base.html:168
|
||||
#: changedetectionio/templates/base.html:77
|
||||
msgid "GROUPS"
|
||||
msgstr "團體"
|
||||
|
||||
#: changedetectionio/templates/base.html:81
|
||||
#: changedetectionio/templates/base.html:169
|
||||
#: changedetectionio/templates/base.html:80
|
||||
msgid "SETTINGS"
|
||||
msgstr "設定"
|
||||
|
||||
#: changedetectionio/templates/base.html:84
|
||||
#: changedetectionio/templates/base.html:170
|
||||
#: changedetectionio/templates/base.html:83
|
||||
msgid "IMPORT"
|
||||
msgstr "進口"
|
||||
|
||||
#: changedetectionio/templates/base.html:87
|
||||
#: changedetectionio/templates/base.html:171
|
||||
#: changedetectionio/templates/base.html:86
|
||||
msgid "BACKUPS"
|
||||
msgstr "備份"
|
||||
|
||||
#: changedetectionio/templates/base.html:91
|
||||
#: changedetectionio/templates/base.html:173
|
||||
#: changedetectionio/templates/base.html:90
|
||||
msgid "EDIT"
|
||||
msgstr "編輯"
|
||||
|
||||
#: changedetectionio/templates/base.html:101
|
||||
#: changedetectionio/templates/base.html:177
|
||||
#: changedetectionio/templates/base.html:100
|
||||
msgid "LOG OUT"
|
||||
msgstr "退出"
|
||||
|
||||
#: changedetectionio/templates/base.html:108
|
||||
#: changedetectionio/templates/base.html:109
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
msgstr "搜索或使用 Alt+S 鍵"
|
||||
|
||||
#: changedetectionio/templates/base.html:114
|
||||
#: changedetectionio/templates/base.html:116
|
||||
msgid "Toggle Light/Dark Mode"
|
||||
msgstr "切換亮/暗模式"
|
||||
|
||||
#: changedetectionio/templates/base.html:115
|
||||
#: changedetectionio/templates/base.html:117
|
||||
msgid "Toggle light/dark mode"
|
||||
msgstr "切換亮/暗模式"
|
||||
|
||||
#: changedetectionio/templates/base.html:125
|
||||
#: changedetectionio/templates/base.html:127
|
||||
msgid "Change Language"
|
||||
msgstr "更改語言"
|
||||
|
||||
#: changedetectionio/templates/base.html:126
|
||||
#: changedetectionio/templates/base.html:128
|
||||
msgid "Change language"
|
||||
msgstr "更改語言"
|
||||
|
||||
#: changedetectionio/templates/base.html:253
|
||||
#, fuzzy
|
||||
msgid "Watch List"
|
||||
msgstr "# 手錶"
|
||||
|
||||
#: changedetectionio/templates/base.html:258
|
||||
#, fuzzy
|
||||
msgid "Watches"
|
||||
msgstr "# 手錶"
|
||||
|
||||
#: changedetectionio/templates/base.html:261
|
||||
msgid "Queue Status"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:270
|
||||
#, fuzzy
|
||||
msgid "Queue"
|
||||
msgstr "排隊"
|
||||
|
||||
#: changedetectionio/templates/base.html:274
|
||||
#: changedetectionio/templates/base.html:279
|
||||
#, fuzzy
|
||||
msgid "Settings"
|
||||
msgstr "設定"
|
||||
|
||||
#: changedetectionio/templates/base.html:293
|
||||
msgid "Sitemap Crawler"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:318
|
||||
msgid "Sitemap"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:354
|
||||
#: changedetectionio/templates/base.html:249
|
||||
msgid "Real-time updates offline"
|
||||
msgstr "離線實時更新"
|
||||
|
||||
#: changedetectionio/templates/base.html:364
|
||||
#: changedetectionio/templates/base.html:259
|
||||
msgid "Select Language"
|
||||
msgstr "選擇語言"
|
||||
|
||||
#: changedetectionio/templates/base.html:375
|
||||
#: changedetectionio/templates/base.html:270
|
||||
msgid ""
|
||||
"Language support is in beta, please help us improve by opening a PR on "
|
||||
"GitHub with any updates."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:387
|
||||
#: changedetectionio/templates/base.html:400
|
||||
#, fuzzy
|
||||
msgid "Search"
|
||||
msgstr "搜尋中"
|
||||
|
||||
#: changedetectionio/templates/base.html:392
|
||||
msgid "URL or Title"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html:392
|
||||
#, fuzzy
|
||||
msgid "in"
|
||||
msgstr "資訊"
|
||||
|
||||
#: changedetectionio/templates/base.html:393
|
||||
msgid "Enter search term..."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/templates/login.html:11
|
||||
#: changedetectionio/templates/login.html:10
|
||||
msgid "Password"
|
||||
msgstr "密碼"
|
||||
|
||||
#: changedetectionio/templates/login.html:17
|
||||
#: changedetectionio/templates/login.html:16
|
||||
msgid "Login"
|
||||
msgstr "登入"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user