misc improvements

This commit is contained in:
dgtlmoon
2025-10-30 10:59:45 +01:00
parent 3897653673
commit c7924537d3
5 changed files with 189 additions and 69 deletions
@@ -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>
+84 -1
View File
@@ -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(),
+94 -66
View File
@@ -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
View File
@@ -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;