550-visual-selector
dgtlmoon 3 years ago
parent 4b7774db29
commit eef98c6adc

@ -966,10 +966,9 @@ def changedetection_app(config=None, datastore_o=None):
@app.route("/static/<string:group>/<string:filename>", methods=['GET'])
def static_content(group, filename):
if group == 'screenshot':
from flask import make_response
from flask import make_response
if group == 'screenshot':
# Could be sensitive, follow password requirements
if datastore.data['settings']['application']['password'] and not flask_login.current_user.is_authenticated:
abort(403)
@ -988,6 +987,26 @@ def changedetection_app(config=None, datastore_o=None):
except FileNotFoundError:
abort(404)
if group == 'visual_selector_data':
# Could be sensitive, follow password requirements
if datastore.data['settings']['application']['password'] and not flask_login.current_user.is_authenticated:
abort(403)
# These files should be in our subdirectory
try:
# set nocache, set content-type
watch_dir = datastore_o.datastore_path + "/" + filename
response = make_response(send_from_directory(filename="elements.json", directory=watch_dir, path=watch_dir + "/elements.json"))
response.headers['Content-type'] = 'application/json'
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = 0
return response
except FileNotFoundError:
abort(404)
# These files should be in our subdirectory
try:
return send_from_directory("static/{}".format(group), path=filename)
@ -1082,6 +1101,84 @@ def changedetection_app(config=None, datastore_o=None):
flash("{} watches are queued for rechecking.".format(i))
return redirect(url_for('index', tag=tag))
@app.route("/api/request-visual-selector-data/<string:uuid>", methods=['GET'])
@login_required
def visualselector_request_current_screenshot_and_metadata(uuid):
import json
watch = datastore.data['watching'][uuid]
path_to_datafile = os.path.join(datastore_o.datastore_path, uuid, "elements.json")
try:
os.unlink(path_to_datafile)
except FileNotFoundError:
pass
# docker run -p 3000:3000 browserless/chrome
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp("ws://127.0.0.1:3000")
page = browser.new_page()
page.set_viewport_size({"width": 1220, "height": 800})
page.goto(watch['url'])
# time.sleep(3)
# https://github.com/microsoft/playwright/issues/620
screenshot = page.screenshot(type='jpeg', full_page=True)
# Could be made a lot faster
# https://toruskit.com/blog/how-to-get-element-bounds-without-reflow/
info = page.evaluate("""async () => {
// Include the getXpath script directly, easier than fetching
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e=e||self).getXPath=n()}(this,function(){return function(e){var n=e;if(n&&n.id)return'//*[@id="'+n.id+'"]';for(var o=[];n&&Node.ELEMENT_NODE===n.nodeType;){for(var i=0,r=!1,d=n.previousSibling;d;)d.nodeType!==Node.DOCUMENT_TYPE_NODE&&d.nodeName===n.nodeName&&i++,d=d.previousSibling;for(d=n.nextSibling;d;){if(d.nodeName===n.nodeName){r=!0;break}d=d.nextSibling}o.push((n.prefix?n.prefix+":":"")+n.localName+(i||r?"["+(i+1)+"]":"")),n=n.parentNode}return o.length?"/"+o.reverse().join("/"):""}});
//# sourceMappingURL=index.umd.js.map
var elements = document.getElementsByTagName("*");
var size_pos=[];
// after page fetch, inject this JS
// build a map of all elements and their positions (maybe that only include text?)
var bbox;
for (var i = 0; i < elements.length; i++) {
bbox = elements[i].getBoundingClientRect();
if (! bbox['width'] || !bbox['height'] ) {
continue;
}
if (bbox['width'] >500 && bbox['height'] >500 ) {
continue;
}
if(! 'textContent' in elements[i] || elements[i].textContent.length < 2 ) {
continue;
}
size_pos.push({
xpath: getXPath(elements[i]),
width: bbox['width'],
height: bbox['height'],
left: bbox['left'],
top: bbox['top'],
childCount: elements[i].childElementCount,
text: elements[i].textContent
});
}
return size_pos;
}""")
browser.close()
with open(path_to_datafile ,'w') as f:
f.write(json.dumps(info, indent=4))
response = make_response(screenshot)
response.headers['Content-type'] = 'image/jpeg'
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = 0
return response
# @todo handle ctrl break
ticker_thread = threading.Thread(target=ticker_thread_check_time_launch_checks).start()

@ -0,0 +1,70 @@
$("img#selector-background").bind('load', function () {
// Image is ready
$('.fetching-update-notice').html("Fetching element data..");
$.ajax({
url: watch_visual_selector_data_url,
context: document.body
}).done(function (data) {
$('.fetching-update-notice').html("Rendering..");
reflow_selector(data);
});
});
function reflow_selector(selector_data) {
$('#selector-canvas').attr('width', $("img#selector-background").width());
$('#selector-canvas').attr('height', $("img#selector-background").height());
//.attr('height', $("img#selector-background").height());
// could trim it according to the lowest/furtheret item in the dataset
stage = new createjs.Stage("selector-canvas");
// to get onMouseOver & onMouseOut events, we need to enable them on the
// stage:
stage.enableMouseOver();
output = new createjs.Text("Test press, click, doubleclick, mouseover, and mouseout", "14px Arial");
output.x = output.y = 10;
stage.addChild(output);
var squares = [];
for (var i = 0; i < selector_data.length; i++) {
squares[i] = new createjs.Shape();
squares[i].graphics.beginFill("rgba(215,0,0,0.2)").drawRect(
selector_data[i]['left'],
selector_data[i]['top'],
selector_data[i]['width'],
selector_data[i]['height']);
squares[i].name = selector_data[i]['xpath'];
stage.addChild(squares[i]);
squares[i].on("click", handleMouseEvent);
squares[i].on("dblclick", handleMouseEvent);
squares[i].on("mouseover", handleMouseEvent);
squares[i].on("mouseout", handleMouseEvent);
squares.push(squares[i]);
}
stage.update();
$('.fetching-update-notice').hide();
}
function handleMouseEvent(evt) {
output.text = "evt.target: " + evt.target + ", evt.type: " + evt.type;
if(evt.type == 'mouseover') {
evt.target.graphics.beginFill("rgba(225,220,220,0.9)");
}
if(evt.type == 'mouseout') {
evt.target.graphics.beginFill("rgba(1,1,1,0.4)");
}
// to save CPU, we're only updating when we need to, instead of on a tick:1
stage.update();
}

@ -4,19 +4,4 @@ $(function () {
$(this).closest('.unviewed').removeClass('unviewed');
});
// after page fetch, inject this JS
// build a map of all elements and their positions (maybe that only include text?)
var p = $( "*" );
for (var i = 0; i < p.length; i++) {
console.log($(p[i]).offset());
console.log($(p[i]).width());
console.log($(p[i]).height());
console.log(getXPath( p[i]));
}
// overlay it on a rendered image of the page
//p.html( "left: " + offset.left + ", top: " + offset.top );
});

@ -445,3 +445,12 @@ ul {
display: inline; }
.time-check-widget tr input[type="number"] {
width: 4em; }
#selector-wrapper {
position: relative; }
#selector-wrapper > img {
position: absolute;
z-index: 4; }
#selector-wrapper > canvas {
position: absolute;
z-index: 5; }

@ -634,3 +634,17 @@ ul {
}
}
}
#selector-wrapper {
position: relative;
> img {
position: absolute;
z-index: 4;
}
>canvas {
position: absolute;
z-index: 5;
}
}

@ -5,11 +5,14 @@
<script type="text/javascript" src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
<script>
const notification_base_url="{{url_for('ajax_callback_send_notification_test')}}";
const watch_visual_selector_data_url="{{url_for('static_content', group='visual_selector_data', filename=uuid)}}";
{% if emailprefix %}
const email_notification_prefix=JSON.parse('{{ emailprefix|tojson }}');
{% endif %}
</script>
<script type="text/javascript" src="{{url_for('static_content', group='js', filename='notifications.js')}}" defer></script>
<script type="text/javascript" src="{{url_for('static_content', group='js', filename='visual-selector.js')}}" defer></script>
<script src="https://code.createjs.com/easeljs-0.8.2.min.js"></script>
<div class="edit-form monospaced-textarea">
@ -19,6 +22,7 @@
<li class="tab"><a href="#request">Request</a></li>
<li class="tab"><a href="#filters-and-triggers">Filters &amp; Triggers</a></li>
<li class="tab"><a href="#notifications">Notifications</a></li>
<li class="tab"><a href="#visualselector">Visual Selector</a></li>
</ul>
</div>
@ -176,6 +180,20 @@ nav
</fieldset>
</div>
<div class="tab-pane-inner visual-selector-ui" id="visualselector">
<fieldset>
<div class="pure-control-group">
<i class="fetching-update-notice">One moment, fetching screenshot and element information..</i><br/>
<div id="selector-wrapper">
<!-- request the screenshot and get the element offset info ready -->
<!-- use img src ready load to know everything is ready to map out -->
<img id="selector-background" src="{{ url_for('visualselector_request_current_screenshot_and_metadata', uuid=uuid) }}"/>
<canvas id="selector-canvas"></canvas>
</div>
</div>
</fieldset>
</div>
<div id="actions">
<div class="pure-control-group">

@ -2,8 +2,6 @@
{% block content %}
{% from '_helpers.jinja' import render_simple_field %}
<script type="text/javascript" src="{{url_for('static_content', group='js', filename='jquery-3.6.0.min.js')}}"></script>
<script crossorigin src="https://unpkg.com/get-xpath"></script>
<script type="text/javascript" src="{{url_for('static_content', group='js', filename='watch-overview.js')}}" defer></script>
<div class="box">

Loading…
Cancel
Save