diff --git a/changedetectionio/__init__.py b/changedetectionio/__init__.py index 4baa923e..3d5b2846 100644 --- a/changedetectionio/__init__.py +++ b/changedetectionio/__init__.py @@ -966,10 +966,9 @@ def changedetection_app(config=None, datastore_o=None): @app.route("/static//", 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/", 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() diff --git a/changedetectionio/static/js/visual-selector.js b/changedetectionio/static/js/visual-selector.js new file mode 100644 index 00000000..e371effb --- /dev/null +++ b/changedetectionio/static/js/visual-selector.js @@ -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(); +} diff --git a/changedetectionio/static/js/watch-overview.js b/changedetectionio/static/js/watch-overview.js index fbb812c7..fb2b917d 100644 --- a/changedetectionio/static/js/watch-overview.js +++ b/changedetectionio/static/js/watch-overview.js @@ -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 ); }); diff --git a/changedetectionio/static/styles/styles.css b/changedetectionio/static/styles/styles.css index 174c9aea..6d4f8d5e 100644 --- a/changedetectionio/static/styles/styles.css +++ b/changedetectionio/static/styles/styles.css @@ -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; } diff --git a/changedetectionio/static/styles/styles.scss b/changedetectionio/static/styles/styles.scss index 3b305b45..cc973773 100644 --- a/changedetectionio/static/styles/styles.scss +++ b/changedetectionio/static/styles/styles.scss @@ -633,4 +633,18 @@ ul { width: 4em; } } -} \ No newline at end of file +} + +#selector-wrapper { + position: relative; + + > img { + position: absolute; + z-index: 4; + } + >canvas { + position: absolute; + z-index: 5; + } + +} diff --git a/changedetectionio/templates/edit.html b/changedetectionio/templates/edit.html index 07af9c4d..abec4ee5 100644 --- a/changedetectionio/templates/edit.html +++ b/changedetectionio/templates/edit.html @@ -5,11 +5,14 @@ + + @@ -176,6 +180,20 @@ nav +
+
+
+ One moment, fetching screenshot and element information..
+
+ + + + +
+
+
+
+
diff --git a/changedetectionio/templates/watch-overview.html b/changedetectionio/templates/watch-overview.html index e16a7cc4..c21c7ffa 100644 --- a/changedetectionio/templates/watch-overview.html +++ b/changedetectionio/templates/watch-overview.html @@ -2,8 +2,6 @@ {% block content %} {% from '_helpers.jinja' import render_simple_field %} - -