mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-09-24 22:36:33 +00:00
misc improvements
This commit is contained in:
@@ -9,6 +9,9 @@
|
||||
|
||||
const highlight_submit_ignore_url="{{url_for('ui.ui_edit.highlight_submit_ignore_url', uuid=uuid)}}";
|
||||
const watch_url= {{watch_a.link|tojson}};
|
||||
|
||||
// Initial scroll position: if set, scroll to this line number in #difference on page load
|
||||
const initialScrollToLineNumber = {{ initial_scroll_line_number|default('null') }};
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
|
||||
<script src="{{url_for('static_content', group='js', filename='plugins.js')}}"></script>
|
||||
@@ -100,7 +103,11 @@
|
||||
<div class="tip">Pro-tip: You can enable <strong>"share access when password is enabled"</strong> from settings.
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="cell-diff-jump-visualiser"></div>
|
||||
<div id="cell-diff-jump-visualiser">
|
||||
{%- for cell in diff_cell_grid -%}
|
||||
<div{% if cell.class %} class="{{ cell.class }}"{% endif %}></div>
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
<div class="snapshot-age">{{ watch_a.snapshot_text_ctime|format_timestamp_timeago }}</div>
|
||||
<pre id="difference" style="border-left: 2px solid #ddd;">{{ content| diff_unescape_difference_spans }}</pre>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,13 @@ import re
|
||||
from loguru import logger
|
||||
from markupsafe import Markup
|
||||
|
||||
from changedetectionio.diff import REMOVED_STYLE, ADDED_STYLE, REMOVED_INNER_STYLE, ADDED_INNER_STYLE
|
||||
from changedetectionio.diff import (
|
||||
REMOVED_STYLE, ADDED_STYLE, REMOVED_INNER_STYLE, ADDED_INNER_STYLE,
|
||||
REMOVED_PLACEMARKER_OPEN, REMOVED_PLACEMARKER_CLOSED,
|
||||
ADDED_PLACEMARKER_OPEN, ADDED_PLACEMARKER_CLOSED,
|
||||
CHANGED_PLACEMARKER_OPEN, CHANGED_PLACEMARKER_CLOSED,
|
||||
CHANGED_INTO_PLACEMARKER_OPEN, CHANGED_INTO_PLACEMARKER_CLOSED
|
||||
)
|
||||
from changedetectionio.notification.handler import apply_html_color_to_body
|
||||
from changedetectionio.notification_service import CUSTOM_LINEBREAK_PLACEHOLDER
|
||||
from changedetectionio.store import ChangeDetectionStore
|
||||
@@ -14,6 +20,77 @@ from changedetectionio import html_tools, diff
|
||||
from changedetectionio import worker_handler
|
||||
from changedetectionio.blueprint.cookie_preferences import PreferenceManager
|
||||
|
||||
|
||||
def build_diff_cell_visualizer(content, resolution=100):
|
||||
"""
|
||||
Build a visual cell grid for the diff visualizer.
|
||||
|
||||
Analyzes the content for placemarkers indicating changes and creates a
|
||||
grid of cells representing the document, with each cell marked as:
|
||||
- 'deletion' for removed content
|
||||
- 'insertion' for added content
|
||||
- 'mixed' for cells containing both deletions and insertions
|
||||
- empty string for cells with no changes
|
||||
|
||||
Args:
|
||||
content: The diff content with placemarkers
|
||||
resolution: Number of cells to create (default 100)
|
||||
|
||||
Returns:
|
||||
List of dicts with 'class' key for each cell's CSS class
|
||||
"""
|
||||
if not content:
|
||||
return [{'class': ''} for _ in range(resolution)]
|
||||
now = time.time()
|
||||
# Work with character positions for better accuracy
|
||||
content_length = len(content)
|
||||
|
||||
if content_length == 0:
|
||||
return [{'class': ''} for _ in range(resolution)]
|
||||
|
||||
chars_per_cell = max(1, content_length / resolution)
|
||||
|
||||
# Track change type for each cell
|
||||
cell_data = {}
|
||||
|
||||
# Placemarkers to detect
|
||||
change_markers = {
|
||||
REMOVED_PLACEMARKER_OPEN: 'deletion',
|
||||
ADDED_PLACEMARKER_OPEN: 'insertion',
|
||||
CHANGED_PLACEMARKER_OPEN: 'deletion',
|
||||
CHANGED_INTO_PLACEMARKER_OPEN: 'insertion',
|
||||
}
|
||||
|
||||
# Find all occurrences of each marker
|
||||
for marker, change_type in change_markers.items():
|
||||
pos = 0
|
||||
while True:
|
||||
pos = content.find(marker, pos)
|
||||
if pos == -1:
|
||||
break
|
||||
|
||||
# Calculate which cell this marker falls into
|
||||
cell_index = min(int(pos / chars_per_cell), resolution - 1)
|
||||
|
||||
if cell_index not in cell_data:
|
||||
cell_data[cell_index] = change_type
|
||||
elif cell_data[cell_index] != change_type:
|
||||
# Mixed changes in this cell
|
||||
cell_data[cell_index] = 'mixed'
|
||||
|
||||
pos += len(marker)
|
||||
|
||||
# Build the cell list
|
||||
cells = []
|
||||
for i in range(resolution):
|
||||
change_type = cell_data.get(i, '')
|
||||
cells.append({'class': change_type})
|
||||
|
||||
logger.debug(f"Built diff cell visualizer: {len([c for c in cells if c['class']])} cells with changes out of {resolution} in {time.time() - now:.2f}s")
|
||||
|
||||
return cells
|
||||
|
||||
|
||||
def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMetaData, watch_check_update):
|
||||
views_blueprint = Blueprint('ui_views', __name__, template_folder="../ui/templates")
|
||||
|
||||
@@ -266,6 +343,10 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
|
||||
include_equal=not diff_prefs.get('diff_changesOnly'),
|
||||
word_diff=diff_prefs.get('diff_type') == 'diffWords',
|
||||
)
|
||||
|
||||
# Build cell grid visualizer before applying HTML color (so we can detect placemarkers)
|
||||
diff_cell_grid = build_diff_cell_visualizer(content)
|
||||
|
||||
content = apply_html_color_to_body(n_body=content)
|
||||
content = content.replace(CUSTOM_LINEBREAK_PLACEHOLDER, "\n")
|
||||
offscreen_content = render_template("diff-offscreen-options.html")
|
||||
@@ -274,11 +355,13 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
|
||||
bottom_horizontal_offscreen_contents=offscreen_content,
|
||||
content=content,
|
||||
current_diff_url=watch['url'],
|
||||
diff_cell_grid=diff_cell_grid,
|
||||
diff_prefs=diff_prefs,
|
||||
extra_stylesheets=extra_stylesheets,
|
||||
extra_title=f" - {watch.label} - History",
|
||||
extract_form=extract_form,
|
||||
from_version=str(from_version),
|
||||
#initial_scroll_line_number=100,
|
||||
is_html_webdriver=is_html_webdriver,
|
||||
last_error=watch['last_error'],
|
||||
last_error_screenshot=watch.get_error_snapshot(),
|
||||
|
||||
@@ -4,91 +4,67 @@ $(document).ready(function () {
|
||||
var inputs = $('#difference span').toArray();
|
||||
inputs.current = 0;
|
||||
|
||||
// Build visual minimap of difference locations
|
||||
// Setup visual minimap of difference locations (cells are pre-built in Python)
|
||||
var $visualizer = $('#cell-diff-jump-visualiser');
|
||||
var $difference = $('#difference');
|
||||
var visualizerResolutionCells = 100; // Fixed resolution to prevent high CPU usage with many changes
|
||||
var $cells = $visualizer.find('> div');
|
||||
var visualizerResolutionCells = $cells.length;
|
||||
var cellHeight;
|
||||
var cellData = {}; // Map cell index to change type
|
||||
|
||||
if ($difference.length && inputs.length) {
|
||||
if ($difference.length && visualizerResolutionCells > 0) {
|
||||
var docHeight = $difference[0].scrollHeight;
|
||||
cellHeight = docHeight / visualizerResolutionCells;
|
||||
|
||||
// Map each span to a cell with its type based on color
|
||||
var differenceTop = $difference.offset().top;
|
||||
$(inputs).each(function() {
|
||||
var spanTop = $(this).offset().top - differenceTop;
|
||||
var cellIndex = Math.min(Math.floor(spanTop / cellHeight), visualizerResolutionCells - 1);
|
||||
var bgColor = $(this).css('background-color');
|
||||
var changeType;
|
||||
|
||||
// Determine type by background color
|
||||
if (bgColor === 'rgb(250, 218, 215)' || bgColor === '#fadad7') {
|
||||
changeType = 'deletion'; // Red background
|
||||
} else if (bgColor === 'rgb(234, 242, 194)' || bgColor === '#eaf2c2') {
|
||||
changeType = 'insertion'; // Green background
|
||||
} else {
|
||||
changeType = $(this).attr('role'); // Fallback to role attribute
|
||||
}
|
||||
|
||||
// Track the type of change in each cell (prioritize mixed changes)
|
||||
if (!cellData[cellIndex]) {
|
||||
cellData[cellIndex] = changeType;
|
||||
} else if (cellData[cellIndex] !== changeType) {
|
||||
cellData[cellIndex] = 'mixed'; // Both deletion and insertion in same cell
|
||||
}
|
||||
});
|
||||
|
||||
// Create cell strip
|
||||
for (var i = 0; i < visualizerResolutionCells; i++) {
|
||||
var $cell = $('<div>');
|
||||
var changeType = cellData[i];
|
||||
|
||||
if (changeType === 'deletion') {
|
||||
$cell.addClass('deletion');
|
||||
} else if (changeType === 'insertion') {
|
||||
$cell.addClass('insertion');
|
||||
} else if (changeType === 'note') {
|
||||
$cell.addClass('note');
|
||||
} else if (changeType === 'mixed') {
|
||||
$cell.addClass('mixed');
|
||||
}
|
||||
|
||||
// Add click handler to scroll to that position in the document
|
||||
$cell.data('cellIndex', i);
|
||||
$cell.on('click', function() {
|
||||
// Add click handlers to pre-built cells
|
||||
$cells.each(function(i) {
|
||||
$(this).data('cellIndex', i);
|
||||
$(this).on('click', function() {
|
||||
var cellIndex = $(this).data('cellIndex');
|
||||
var targetPositionInDifference = cellIndex * cellHeight;
|
||||
var viewportOffset = 150; // Keep change visible with 150px offset
|
||||
var viewportHeight = $(window).height();
|
||||
|
||||
// Scroll so target is at viewport center (where eyes expect it)
|
||||
window.scrollTo({
|
||||
top: $difference.offset().top + targetPositionInDifference - viewportOffset,
|
||||
top: $difference.offset().top + targetPositionInDifference - (viewportHeight / 2),
|
||||
behavior: "smooth"
|
||||
});
|
||||
});
|
||||
|
||||
$visualizer.append($cell);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$('#jump-next-diff').click(function () {
|
||||
if (!inputs || inputs.length === 0) return;
|
||||
|
||||
var element = inputs[inputs.current];
|
||||
var headerOffset = 80;
|
||||
var elementPosition = element.getBoundingClientRect().top;
|
||||
var offsetPosition = elementPosition - headerOffset + window.scrollY;
|
||||
// Find the next change after current scroll position
|
||||
var currentScrollPos = $(window).scrollTop();
|
||||
var viewportHeight = $(window).height();
|
||||
var currentCenter = currentScrollPos + (viewportHeight / 2);
|
||||
|
||||
// Add small buffer (50px) to jump past changes already near center
|
||||
var searchFromPosition = currentCenter + 50;
|
||||
|
||||
var nextElement = null;
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
var elementTop = $(inputs[i]).offset().top;
|
||||
if (elementTop > searchFromPosition) {
|
||||
nextElement = inputs[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no element found ahead, wrap to first element
|
||||
if (!nextElement) {
|
||||
nextElement = inputs[0];
|
||||
}
|
||||
|
||||
// Scroll to position the element at viewport center
|
||||
var elementTop = $(nextElement).offset().top;
|
||||
var targetScrollPos = elementTop - (viewportHeight / 2);
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
top: targetScrollPos,
|
||||
behavior: "smooth",
|
||||
});
|
||||
|
||||
inputs.current++;
|
||||
if (inputs.current >= inputs.length) {
|
||||
inputs.current = 0;
|
||||
}
|
||||
});
|
||||
|
||||
// Track current scroll position in visualizer
|
||||
@@ -96,20 +72,72 @@ $(document).ready(function () {
|
||||
if (!$difference.length || visualizerResolutionCells === 0) return;
|
||||
|
||||
var scrollTop = $(window).scrollTop();
|
||||
var viewportHeight = $(window).height();
|
||||
var viewportCenter = scrollTop + (viewportHeight / 2);
|
||||
var differenceTop = $difference.offset().top;
|
||||
var positionInDifference = scrollTop - differenceTop;
|
||||
var differenceHeight = $difference[0].scrollHeight;
|
||||
var positionInDifference = viewportCenter - differenceTop;
|
||||
|
||||
// Handle edge case: if we're at max scroll, show last cell
|
||||
// This prevents shorter documents from never reaching 100%
|
||||
var maxScrollTop = $(document).height() - viewportHeight;
|
||||
var isAtBottom = scrollTop >= maxScrollTop - 10; // 10px tolerance
|
||||
|
||||
// Calculate which cell we're currently viewing
|
||||
var currentCell = Math.floor(positionInDifference / cellHeight);
|
||||
currentCell = Math.max(0, Math.min(currentCell, visualizerResolutionCells - 1));
|
||||
var currentCell;
|
||||
if (isAtBottom) {
|
||||
currentCell = visualizerResolutionCells - 1;
|
||||
} else {
|
||||
currentCell = Math.floor(positionInDifference / cellHeight);
|
||||
currentCell = Math.max(0, Math.min(currentCell, visualizerResolutionCells - 1));
|
||||
}
|
||||
|
||||
// Remove previous active marker and add to current cell
|
||||
$visualizer.find('> div').removeClass('current-position');
|
||||
$visualizer.find('> div').eq(currentCell).addClass('current-position');
|
||||
}
|
||||
|
||||
// Debounce scroll event to reduce CPU usage
|
||||
// Recalculate cellHeight on window resize
|
||||
function handleResize() {
|
||||
if ($difference.length) {
|
||||
var docHeight = $difference[0].scrollHeight;
|
||||
cellHeight = docHeight / visualizerResolutionCells;
|
||||
updateVisualizerPosition();
|
||||
}
|
||||
}
|
||||
|
||||
// Debounce scroll and resize events to reduce CPU usage
|
||||
$(window).on('scroll', updateVisualizerPosition.debounce(5));
|
||||
$(window).on('resize', handleResize.debounce(100));
|
||||
|
||||
// Initial scroll to specific line if requested
|
||||
if (typeof initialScrollToLineNumber !== 'undefined' && initialScrollToLineNumber !== null && $difference.length) {
|
||||
// Convert line number to text position and scroll to it
|
||||
var diffText = $difference.text();
|
||||
var lines = diffText.split('\n');
|
||||
|
||||
if (initialScrollToLineNumber > 0 && initialScrollToLineNumber <= lines.length) {
|
||||
// Calculate character position of the target line
|
||||
var charPosition = 0;
|
||||
for (var i = 0; i < initialScrollToLineNumber - 1; i++) {
|
||||
charPosition += lines[i].length + 1; // +1 for newline
|
||||
}
|
||||
|
||||
// Estimate vertical position based on average line height
|
||||
var totalChars = diffText.length;
|
||||
var totalHeight = $difference[0].scrollHeight;
|
||||
var estimatedTop = (charPosition / totalChars) * totalHeight;
|
||||
|
||||
// Scroll to position with line at viewport center
|
||||
var viewportHeight = $(window).height();
|
||||
setTimeout(function() {
|
||||
window.scrollTo({
|
||||
top: $difference.offset().top + estimatedTop - (viewportHeight / 2),
|
||||
behavior: "smooth"
|
||||
});
|
||||
}, 100); // Small delay to ensure page is fully loaded
|
||||
}
|
||||
}
|
||||
|
||||
// Initial position update
|
||||
if ($difference.length && cellHeight) {
|
||||
|
||||
@@ -1 +1 @@
|
||||
#diff-ui{background:var(--color-background);padding:2em;margin-left:1em;margin-right:1em;border-radius:5px}#diff-ui #text{font-size:11px}#diff-ui table{table-layout:fixed;width:100%}#diff-ui td{padding:3px 4px;border:1px solid rgba(0,0,0,0);vertical-align:top;font:1em monospace;text-align:left;overflow:clip}#diff-ui pre{white-space:break-spaces}h1{display:inline;font-size:100%}del{text-decoration:none;color:#b30000;background:#fadad7}ins{background:#eaf2c2;color:#406619;text-decoration:none}#result{white-space:pre-wrap;word-break:break-word;overflow-wrap:break-word}#settings{background:rgba(0,0,0,.05);padding:1em;border-radius:10px;margin-bottom:1em;color:#fff;font-size:.9rem}#settings label{margin-left:1em;display:inline-block;font-weight:normal}#settings del{padding:.5em}#settings ins{padding:.5em}#settings option:checked{font-weight:bold}#settings [type=radio],#settings [type=checkbox]{vertical-align:middle}.source{position:absolute;right:1%;top:.2em}@-moz-document url-prefix(){body{height:99%}}td#diff-col div{text-align:justify;white-space:pre-wrap}.ignored{background-color:#ccc;opacity:.7}.triggered{background-color:#1b98f8}.ignored.triggered{background-color:red}.tab-pane-inner#screenshot{text-align:center}.tab-pane-inner#screenshot img{max-width:99%}.pure-form button.reset-margin{margin:0px}.diff-fieldset{display:flex;align-items:center;gap:4px;flex-wrap:wrap}ul#highlightSnippetActions{list-style-type:none;display:flex;align-items:center;justify-content:center;gap:1.5rem;flex-wrap:wrap;padding:0;margin:0}ul#highlightSnippetActions li{display:flex;flex-direction:column;align-items:center;text-align:center;padding:.5rem;gap:.3rem}ul#highlightSnippetActions li button,ul#highlightSnippetActions li a{white-space:nowrap}ul#highlightSnippetActions span{font-size:.8rem;color:var(--color-text-input-description)}#cell-diff-jump-visualiser{display:flex;flex-direction:row;gap:1px;background:var(--color-background);border-radius:3px;overflow-x:auto;position:sticky;top:0;z-index:10;padding-top:1rem;padding-bottom:1rem}#cell-diff-jump-visualiser>div{flex:1;min-width:1px;height:10px;background:var(--color-background-button-cancel);opacity:.3;border-radius:1px;transition:opacity .2s;position:relative}#cell-diff-jump-visualiser>div.deletion{background:#b30000;opacity:1}#cell-diff-jump-visualiser>div.insertion{background:#406619;opacity:1}#cell-diff-jump-visualiser>div.note{background:#406619;opacity:1}#cell-diff-jump-visualiser>div.mixed{background:linear-gradient(to right, #b30000 50%, #406619 50%);opacity:1}#cell-diff-jump-visualiser>div.current-position::after{content:"";position:absolute;bottom:-6px;left:50%;transform:translateX(-50%);width:0;height:0;border-left:4px solid rgba(0,0,0,0);border-right:4px solid rgba(0,0,0,0);border-bottom:4px solid var(--color-text)}#cell-diff-jump-visualiser>div:hover{opacity:.8;cursor:pointer}
|
||||
#diff-ui{background:var(--color-background);padding:2em;margin-left:1em;margin-right:1em;border-radius:5px}#diff-ui #text{font-size:11px}#diff-ui table{table-layout:fixed;width:100%}#diff-ui td{padding:3px 4px;border:1px solid rgba(0,0,0,0);vertical-align:top;font:1em monospace;text-align:left;overflow:clip}#diff-ui pre{white-space:break-spaces}h1{display:inline;font-size:100%}del{text-decoration:none;color:#b30000;background:#fadad7}ins{background:#eaf2c2;color:#406619;text-decoration:none}#result{white-space:pre-wrap;word-break:break-word;overflow-wrap:break-word}#settings{background:rgba(0,0,0,.05);padding:1em;border-radius:10px;margin-bottom:1em;color:#fff;font-size:.9rem}#settings label{margin-left:1em;display:inline-block;font-weight:normal}#settings del{padding:.5em}#settings ins{padding:.5em}#settings option:checked{font-weight:bold}#settings [type=radio],#settings [type=checkbox]{vertical-align:middle}.source{position:absolute;right:1%;top:.2em}@-moz-document url-prefix(){body{height:99%}}td#diff-col div{text-align:justify;white-space:pre-wrap}.ignored{background-color:#ccc;opacity:.7}.triggered{background-color:#1b98f8}.ignored.triggered{background-color:red}.tab-pane-inner#screenshot{text-align:center}.tab-pane-inner#screenshot img{max-width:99%}.pure-form button.reset-margin{margin:0px}.diff-fieldset{display:flex;align-items:center;gap:4px;flex-wrap:wrap}ul#highlightSnippetActions{list-style-type:none;display:flex;align-items:center;justify-content:center;gap:1.5rem;flex-wrap:wrap;padding:0;margin:0}ul#highlightSnippetActions li{display:flex;flex-direction:column;align-items:center;text-align:center;padding:.5rem;gap:.3rem}ul#highlightSnippetActions li button,ul#highlightSnippetActions li a{white-space:nowrap}ul#highlightSnippetActions span{font-size:.8rem;color:var(--color-text-input-description)}#cell-diff-jump-visualiser{display:flex;flex-direction:row;gap:1px;background:var(--color-background);border-radius:3px;overflow-x:auto;position:sticky;top:0;z-index:10;padding-top:1rem;padding-bottom:1rem;justify-content:center}#cell-diff-jump-visualiser>div{flex:1;min-width:1px;max-width:10px;height:10px;background:var(--color-background-button-cancel);opacity:.3;border-radius:1px;transition:opacity .2s;position:relative}#cell-diff-jump-visualiser>div.deletion{background:#b30000;opacity:1}#cell-diff-jump-visualiser>div.insertion{background:#406619;opacity:1}#cell-diff-jump-visualiser>div.note{background:#406619;opacity:1}#cell-diff-jump-visualiser>div.mixed{background:linear-gradient(to right, #b30000 50%, #406619 50%);opacity:1}#cell-diff-jump-visualiser>div.current-position::after{content:"";position:absolute;bottom:-6px;left:50%;transform:translateX(-50%);width:0;height:0;border-left:4px solid rgba(0,0,0,0);border-right:4px solid rgba(0,0,0,0);border-bottom:4px solid var(--color-text)}#cell-diff-jump-visualiser>div:hover{opacity:.8;cursor:pointer}
|
||||
|
||||
@@ -184,9 +184,11 @@ ul#highlightSnippetActions {
|
||||
z-index: 10;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
justify-content: center;
|
||||
> div {
|
||||
flex: 1;
|
||||
min-width: 1px;
|
||||
max-width: 10px;
|
||||
height: 10px;
|
||||
background: var(--color-background-button-cancel);
|
||||
opacity: 0.3;
|
||||
|
||||
Reference in New Issue
Block a user