Compare commits

..

2 Commits

Author SHA1 Message Date
dgtlmoon
8677b65cc8 oops 2022-11-27 15:56:22 +01:00
dgtlmoon
10813b130c Extra validation for URLs with jinja2 template 2022-11-27 15:52:32 +01:00
76 changed files with 1029 additions and 2760 deletions

View File

@@ -1,7 +1,7 @@
# pip dependencies install stage # pip dependencies install stage
FROM python:3.8-slim as builder FROM python:3.8-slim as builder
# See `cryptography` pin comment in requirements.txt # rustc compiler would be needed on ARM type devices but theres an issue with some deps not building..
ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1 ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -31,7 +31,8 @@ RUN pip install --target=/dependencies playwright~=1.27.1 \
# Final image stage # Final image stage
FROM python:3.8-slim FROM python:3.8-slim
# See `cryptography` pin comment in requirements.txt # Actual packages needed at runtime, usually due to the notification (apprise) backend
# rustc compiler would be needed on ARM type devices but theres an issue with some deps not building..
ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1 ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1
# Re #93, #73, excluding rustc (adds another 430Mb~) # Re #93, #73, excluding rustc (adds another 430Mb~)

View File

@@ -1,10 +1,9 @@
recursive-include changedetectionio/api * recursive-include changedetectionio/api *
recursive-include changedetectionio/blueprint *
recursive-include changedetectionio/model *
recursive-include changedetectionio/res *
recursive-include changedetectionio/static *
recursive-include changedetectionio/templates * recursive-include changedetectionio/templates *
recursive-include changedetectionio/static *
recursive-include changedetectionio/model *
recursive-include changedetectionio/tests * recursive-include changedetectionio/tests *
recursive-include changedetectionio/res *
prune changedetectionio/static/package-lock.json prune changedetectionio/static/package-lock.json
prune changedetectionio/static/styles/node_modules prune changedetectionio/static/styles/node_modules
prune changedetectionio/static/styles/package-lock.json prune changedetectionio/static/styles/package-lock.json

View File

@@ -13,33 +13,15 @@ _Live your data-life pro-actively, Detect website changes and perform meaningful
- Chrome browser included. - Chrome browser included.
- Super fast, no registration needed setup. - Super fast, no registration needed setup.
- Get started watching and receiving website change notifications straight away. - Start watching and receiving change notifications instantly.
### Target specific parts of the webpage using the Visual Selector tool. Easily see what changed, examine by word, line, or individual character.
Available when connected to a <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Playwright-content-fetcher">playwright content fetcher</a> (included as part of our subscription service) <img src="https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/docs/screenshot-diff.png" style="max-width:100%;" alt="Self-hosted web page change monitoring context difference " title="Self-hosted web page change monitoring context difference " />
[<img src="https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/docs/visualselector-anim.gif" style="max-width:100%;" alt="Self-hosted web page change monitoring context difference " title="Self-hosted web page change monitoring context difference " />](https://lemonade.changedetection.io/start?src=github)
### Easily see what changed, examine by word, line, or individual character.
[<img src="https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/docs/screenshot-diff.png" style="max-width:100%;" alt="Self-hosted web page change monitoring context difference " title="Self-hosted web page change monitoring context difference " />](https://lemonade.changedetection.io/start?src=github)
### Perform interactive browser steps #### Example use cases
Fill in text boxes, click buttons and more, setup your changedetection scenario.
Using the **Browser Steps** configuration, add basic steps before performing change detection, such as logging into websites, adding a product to a cart, accept cookie logins, entering dates and refining searches.
[<img src="docs/browsersteps-anim.gif" style="max-width:100%;" alt="Self-hosted web page change monitoring context difference " title="Website change detection with interactive browser steps, login, cookies etc" />](https://lemonade.changedetection.io/start?src=github)
After **Browser Steps** have been run, then visit the **Visual Selector** tab to refine the content you're interested in.
Requires Playwright to be enabled.
### Example use cases
- Products and services have a change in pricing - Products and services have a change in pricing
- _Out of stock notification_ and _Back In stock notification_ - _Out of stock notification_ and _Back In stock notification_
@@ -77,8 +59,27 @@ _Need an actual Chrome runner with Javascript support? We support fetching via W
We [recommend and use Bright Data](https://brightdata.grsm.io/n0r16zf7eivq) global proxy services, Bright Data will match any first deposit up to $100 using our signup link. We [recommend and use Bright Data](https://brightdata.grsm.io/n0r16zf7eivq) global proxy services, Bright Data will match any first deposit up to $100 using our signup link.
## Screenshots
Please :star: star :star: this project and help it grow! https://github.com/dgtlmoon/changedetection.io/ Please :star: star :star: this project and help it grow! https://github.com/dgtlmoon/changedetection.io/
### Target specific parts of the webpage using the Visual Selector tool.
Available when connected to a <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Playwright-content-fetcher">playwright content fetcher</a> (included as part of our subscription service)
<img src="https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/docs/visualselector-anim.gif" style="max-width:100%;" alt="Self-hosted web page change monitoring context difference " title="Self-hosted web page change monitoring context difference " />
### Perform interactive browser steps
Fill in text boxes, click buttons and more, setup your changedetection scenario.
Using the **Browser Steps** configuration, add basic steps before performing change detection, such as logging into websites, adding a product to a cart, accept cookie logins, entering dates and refining searches.
<img src="docs/browsersteps-anim.gif" style="max-width:100%;" alt="Self-hosted web page change monitoring context difference " title="Website change detection with interactive browser steps, login, cookies etc" />
After **Browser Steps** have been run, then visit the **Visual Selector** tab to refine the content you're interested in.
Requires Playwright to be enabled.
## Installation ## Installation
### Docker ### Docker
@@ -159,7 +160,7 @@ Just some examples
<img src="https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/docs/screenshot-notifications.png" style="max-width:100%;" alt="Self-hosted web page change monitoring notifications" title="Self-hosted web page change monitoring notifications" /> <img src="https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/docs/screenshot-notifications.png" style="max-width:100%;" alt="Self-hosted web page change monitoring notifications" title="Self-hosted web page change monitoring notifications" />
Now you can also customise your notification content and use <a target="_new" href="https://jinja.palletsprojects.com/en/3.0.x/templates/">Jinja2 templating</a> for their title and body! Now you can also customise your notification content!
## JSON API Monitoring ## JSON API Monitoring
@@ -187,29 +188,11 @@ When you enable a `json:` or `jq:` filter, you can even automatically extract an
<html> <html>
... ...
<script type="application/ld+json"> <script type="application/ld+json">
{"@context":"http://schema.org","@type":"Product","name":"Nan Optipro Stage 1 Baby Formula 800g","price": 23.50 }
{
"@context":"http://schema.org/",
"@type":"Product",
"offers":{
"@type":"Offer",
"availability":"http://schema.org/InStock",
"price":"3949.99",
"priceCurrency":"USD",
"url":"https://www.newegg.com/p/3D5-000D-001T1"
},
"description":"Cobratype King Cobra Hero Desktop Gaming PC",
"name":"Cobratype King Cobra Hero Desktop Gaming PC",
"sku":"3D5-000D-001T1",
"itemCondition":"NewCondition"
}
</script> </script>
``` ```
`json:$..price` or `jq:..price` would give `3949.99`, or you can extract the whole structure (use a JSONpath test website to validate with) `json:$.price` or `jq:.price` would give `23.50`, or you can extract the whole structure
The application also supports notifying you that it can follow this information automatically
## Proxy Configuration ## Proxy Configuration

View File

@@ -10,7 +10,6 @@ import threading
import time import time
import timeago import timeago
from changedetectionio import queuedWatchMetaData
from copy import deepcopy from copy import deepcopy
from distutils.util import strtobool from distutils.util import strtobool
from feedgen.feed import FeedGenerator from feedgen.feed import FeedGenerator
@@ -28,7 +27,6 @@ from flask import (
session, session,
url_for, url_for,
) )
from flask_compress import Compress as FlaskCompress
from flask_login import login_required from flask_login import login_required
from flask_restful import abort, Api from flask_restful import abort, Api
from flask_wtf import CSRFProtect from flask_wtf import CSRFProtect
@@ -36,7 +34,7 @@ from flask_wtf import CSRFProtect
from changedetectionio import html_tools from changedetectionio import html_tools
from changedetectionio.api import api_v1 from changedetectionio.api import api_v1
__version__ = '0.40.0.3' __version__ = '0.39.22.1'
datastore = None datastore = None
@@ -53,10 +51,6 @@ app = Flask(__name__,
static_url_path="", static_url_path="",
static_folder="static", static_folder="static",
template_folder="templates") template_folder="templates")
from flask_compress import Compress
# Super handy for compressing large BrowserSteps responses and others
FlaskCompress(app)
# Stop browser caching of assets # Stop browser caching of assets
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
@@ -96,12 +90,6 @@ def init_app_secret(datastore_path):
return secret return secret
@app.template_global()
def get_darkmode_state():
css_dark_mode = request.cookies.get('css_dark_mode', 'false')
return 'true' if css_dark_mode and strtobool(css_dark_mode) else 'false'
# We use the whole watch object from the store/JSON so we can see if there's some related status in terms of a thread # We use the whole watch object from the store/JSON so we can see if there's some related status in terms of a thread
# running or something similar. # running or something similar.
@app.template_filter('format_last_checked_time') @app.template_filter('format_last_checked_time')
@@ -209,6 +197,8 @@ def changedetection_app(config=None, datastore_o=None):
watch_api.add_resource(api_v1.SystemInfo, '/api/v1/systeminfo', watch_api.add_resource(api_v1.SystemInfo, '/api/v1/systeminfo',
resource_class_kwargs={'datastore': datastore, 'update_q': update_q}) resource_class_kwargs={'datastore': datastore, 'update_q': update_q})
# Setup cors headers to allow all domains # Setup cors headers to allow all domains
# https://flask-cors.readthedocs.io/en/latest/ # https://flask-cors.readthedocs.io/en/latest/
# CORS(app) # CORS(app)
@@ -405,6 +395,7 @@ def changedetection_app(config=None, datastore_o=None):
sorted_watches.append(watch) sorted_watches.append(watch)
existing_tags = datastore.get_all_tags() existing_tags = datastore.get_all_tags()
form = forms.quickWatchForm(request.form) form = forms.quickWatchForm(request.form)
output = render_template("watch-overview.html", output = render_template("watch-overview.html",
form=form, form=form,
@@ -416,7 +407,7 @@ def changedetection_app(config=None, datastore_o=None):
# Don't link to hosting when we're on the hosting environment # Don't link to hosting when we're on the hosting environment
hosted_sticky=os.getenv("SALTED_PASS", False) == False, hosted_sticky=os.getenv("SALTED_PASS", False) == False,
guid=datastore.data['app_guid'], guid=datastore.data['app_guid'],
queued_uuids=[q_uuid.item['uuid'] for q_uuid in update_q.queue]) queued_uuids=[uuid for p,uuid in update_q.queue])
if session.get('share-link'): if session.get('share-link'):
@@ -596,16 +587,25 @@ def changedetection_app(config=None, datastore_o=None):
using_default_check_time = False using_default_check_time = False
break break
# Use the default if it's the same as system-wide. # Use the default if its the same as system wide
if form.fetch_backend.data == datastore.data['settings']['application']['fetch_backend']: if form.fetch_backend.data == datastore.data['settings']['application']['fetch_backend']:
extra_update_obj['fetch_backend'] = None extra_update_obj['fetch_backend'] = None
# Ignore text # Ignore text
form_ignore_text = form.ignore_text.data form_ignore_text = form.ignore_text.data
datastore.data['watching'][uuid]['ignore_text'] = form_ignore_text datastore.data['watching'][uuid]['ignore_text'] = form_ignore_text
# Reset the previous_md5 so we process a new snapshot including stripping ignore text.
if form_ignore_text:
if len(datastore.data['watching'][uuid].history):
extra_update_obj['previous_md5'] = get_current_checksum_include_ignore_text(uuid=uuid)
# Reset the previous_md5 so we process a new snapshot including stripping ignore text.
if form.include_filters.data != datastore.data['watching'][uuid].get('include_filters', []):
if len(datastore.data['watching'][uuid].history):
extra_update_obj['previous_md5'] = get_current_checksum_include_ignore_text(uuid=uuid)
# Be sure proxy value is None # Be sure proxy value is None
if datastore.proxy_list is not None and form.data['proxy'] == '': if datastore.proxy_list is not None and form.data['proxy'] == '':
extra_update_obj['proxy'] = None extra_update_obj['proxy'] = None
@@ -623,7 +623,7 @@ def changedetection_app(config=None, datastore_o=None):
datastore.needs_write_urgent = True datastore.needs_write_urgent = True
# Queue the watch for immediate recheck, with a higher priority # Queue the watch for immediate recheck, with a higher priority
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid, 'skip_when_checksum_same': False})) update_q.put((1, uuid))
# Diff page [edit] link should go back to diff page # Diff page [edit] link should go back to diff page
if request.args.get("next") and request.args.get("next") == 'diff': if request.args.get("next") and request.args.get("next") == 'diff':
@@ -764,7 +764,7 @@ def changedetection_app(config=None, datastore_o=None):
importer = import_url_list() importer = import_url_list()
importer.run(data=request.values.get('urls'), flash=flash, datastore=datastore) importer.run(data=request.values.get('urls'), flash=flash, datastore=datastore)
for uuid in importer.new_uuids: for uuid in importer.new_uuids:
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid, 'skip_when_checksum_same': True})) update_q.put((1, uuid))
if len(importer.remaining_data) == 0: if len(importer.remaining_data) == 0:
return redirect(url_for('index')) return redirect(url_for('index'))
@@ -777,7 +777,7 @@ def changedetection_app(config=None, datastore_o=None):
d_importer = import_distill_io_json() d_importer = import_distill_io_json()
d_importer.run(data=request.values.get('distill-io'), flash=flash, datastore=datastore) d_importer.run(data=request.values.get('distill-io'), flash=flash, datastore=datastore)
for uuid in d_importer.new_uuids: for uuid in d_importer.new_uuids:
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid, 'skip_when_checksum_same': True})) update_q.put((1, uuid))
@@ -799,12 +799,10 @@ def changedetection_app(config=None, datastore_o=None):
return redirect(url_for('index')) return redirect(url_for('index'))
@app.route("/diff/<string:uuid>", methods=['GET', 'POST']) @app.route("/diff/<string:uuid>", methods=['GET'])
@login_required @login_required
def diff_history_page(uuid): def diff_history_page(uuid):
from changedetectionio import forms
# More for testing, possible to return the first/only # More for testing, possible to return the first/only
if uuid == 'first': if uuid == 'first':
uuid = list(datastore.data['watching'].keys()).pop() uuid = list(datastore.data['watching'].keys()).pop()
@@ -816,28 +814,6 @@ def changedetection_app(config=None, datastore_o=None):
flash("No history found for the specified link, bad link?", "error") flash("No history found for the specified link, bad link?", "error")
return redirect(url_for('index')) return redirect(url_for('index'))
# For submission of requesting an extract
extract_form = forms.extractDataForm(request.form)
if request.method == 'POST':
if not extract_form.validate():
flash("An error occurred, please see below.", "error")
else:
extract_regex = request.form.get('extract_regex').strip()
output = watch.extract_regex_from_all_history(extract_regex)
if output:
watch_dir = os.path.join(datastore_o.datastore_path, uuid)
response = make_response(send_from_directory(directory=watch_dir, path=output, as_attachment=True))
response.headers['Content-type'] = 'text/csv'
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = 0
return response
flash('Nothing matches that RegEx', 'error')
redirect(url_for('diff_history_page', uuid=uuid)+'#extract')
history = watch.history history = watch.history
dates = list(history.keys()) dates = list(history.keys())
@@ -880,23 +856,22 @@ def changedetection_app(config=None, datastore_o=None):
watch.get('fetch_backend', None) is None and system_uses_webdriver) else False watch.get('fetch_backend', None) is None and system_uses_webdriver) else False
output = render_template("diff.html", output = render_template("diff.html",
current_diff_url=watch['url'], watch_a=watch,
current_previous_version=str(previous_version), newest=newest_version_file_contents,
previous=previous_version_file_contents,
extra_stylesheets=extra_stylesheets, extra_stylesheets=extra_stylesheets,
versions=dates[:-1], # All except current/last
uuid=uuid,
newest_version_timestamp=dates[-1],
current_previous_version=str(previous_version),
current_diff_url=watch['url'],
extra_title=" - Diff - {}".format(watch['title'] if watch['title'] else watch['url']), extra_title=" - Diff - {}".format(watch['title'] if watch['title'] else watch['url']),
extract_form=extract_form, left_sticky=True,
screenshot=screenshot_url,
is_html_webdriver=is_html_webdriver, is_html_webdriver=is_html_webdriver,
last_error=watch['last_error'], last_error=watch['last_error'],
last_error_screenshot=watch.get_error_snapshot(),
last_error_text=watch.get_error_text(), last_error_text=watch.get_error_text(),
left_sticky=True, last_error_screenshot=watch.get_error_snapshot()
newest=newest_version_file_contents,
newest_version_timestamp=dates[-1],
previous=previous_version_file_contents,
screenshot=screenshot_url,
uuid=uuid,
versions=dates[:-1], # All except current/last
watch_a=watch
) )
return output return output
@@ -1000,6 +975,10 @@ def changedetection_app(config=None, datastore_o=None):
return output return output
@app.route("/favicon.ico", methods=['GET'])
def favicon():
return send_from_directory("static/images", path="favicon.ico")
# We're good but backups are even better! # We're good but backups are even better!
@app.route("/backup", methods=['GET']) @app.route("/backup", methods=['GET'])
@login_required @login_required
@@ -1142,7 +1121,7 @@ def changedetection_app(config=None, datastore_o=None):
if not add_paused and new_uuid: if not add_paused and new_uuid:
# Straight into the queue. # Straight into the queue.
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': new_uuid})) update_q.put((1, new_uuid))
flash("Watch added.") flash("Watch added.")
if add_paused: if add_paused:
@@ -1179,7 +1158,7 @@ def changedetection_app(config=None, datastore_o=None):
uuid = list(datastore.data['watching'].keys()).pop() uuid = list(datastore.data['watching'].keys()).pop()
new_uuid = datastore.clone(uuid) new_uuid = datastore.clone(uuid)
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=5, item={'uuid': new_uuid, 'skip_when_checksum_same': True})) update_q.put((5, new_uuid))
flash('Cloned.') flash('Cloned.')
return redirect(url_for('index')) return redirect(url_for('index'))
@@ -1187,7 +1166,7 @@ def changedetection_app(config=None, datastore_o=None):
@app.route("/api/checknow", methods=['GET']) @app.route("/api/checknow", methods=['GET'])
@login_required @login_required
def form_watch_checknow(): def form_watch_checknow():
# Forced recheck will skip the 'skip if content is the same' rule (, 'reprocess_existing_data': True})))
tag = request.args.get('tag') tag = request.args.get('tag')
uuid = request.args.get('uuid') uuid = request.args.get('uuid')
i = 0 i = 0
@@ -1196,9 +1175,11 @@ def changedetection_app(config=None, datastore_o=None):
for t in running_update_threads: for t in running_update_threads:
running_uuids.append(t.current_uuid) running_uuids.append(t.current_uuid)
# @todo check thread is running and skip
if uuid: if uuid:
if uuid not in running_uuids: if uuid not in running_uuids:
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid, 'skip_when_checksum_same': False})) update_q.put((1, uuid))
i = 1 i = 1
elif tag != None: elif tag != None:
@@ -1206,14 +1187,14 @@ def changedetection_app(config=None, datastore_o=None):
for watch_uuid, watch in datastore.data['watching'].items(): for watch_uuid, watch in datastore.data['watching'].items():
if (tag != None and tag in watch['tag']): if (tag != None and tag in watch['tag']):
if watch_uuid not in running_uuids and not datastore.data['watching'][watch_uuid]['paused']: if watch_uuid not in running_uuids and not datastore.data['watching'][watch_uuid]['paused']:
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': watch_uuid, 'skip_when_checksum_same': False})) update_q.put((1, watch_uuid))
i += 1 i += 1
else: else:
# No tag, no uuid, add everything. # No tag, no uuid, add everything.
for watch_uuid, watch in datastore.data['watching'].items(): for watch_uuid, watch in datastore.data['watching'].items():
if watch_uuid not in running_uuids and not datastore.data['watching'][watch_uuid]['paused']: if watch_uuid not in running_uuids and not datastore.data['watching'][watch_uuid]['paused']:
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': watch_uuid, 'skip_when_checksum_same': False})) update_q.put((1, watch_uuid))
i += 1 i += 1
flash("{} watches are queued for rechecking.".format(i)) flash("{} watches are queued for rechecking.".format(i))
return redirect(url_for('index', tag=tag)) return redirect(url_for('index', tag=tag))
@@ -1260,14 +1241,6 @@ def changedetection_app(config=None, datastore_o=None):
datastore.data['watching'][uuid.strip()]['notification_muted'] = False datastore.data['watching'][uuid.strip()]['notification_muted'] = False
flash("{} watches un-muted".format(len(uuids))) flash("{} watches un-muted".format(len(uuids)))
elif (op == 'recheck'):
for uuid in uuids:
uuid = uuid.strip()
if datastore.data['watching'].get(uuid):
# Recheck and require a full reprocessing
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid, 'skip_when_checksum_same': False}))
flash("{} watches un-muted".format(len(uuids)))
elif (op == 'notification-default'): elif (op == 'notification-default'):
from changedetectionio.notification import ( from changedetectionio.notification import (
default_notification_format_for_watch default_notification_format_for_watch
@@ -1340,10 +1313,6 @@ def changedetection_app(config=None, datastore_o=None):
import changedetectionio.blueprint.browser_steps as browser_steps import changedetectionio.blueprint.browser_steps as browser_steps
app.register_blueprint(browser_steps.construct_blueprint(datastore), url_prefix='/browser-steps') app.register_blueprint(browser_steps.construct_blueprint(datastore), url_prefix='/browser-steps')
import changedetectionio.blueprint.price_data_follower as price_data_follower
app.register_blueprint(price_data_follower.construct_blueprint(datastore, update_q), url_prefix='/price_data_follower')
# @todo handle ctrl break # @todo handle ctrl break
ticker_thread = threading.Thread(target=ticker_thread_check_time_launch_checks).start() ticker_thread = threading.Thread(target=ticker_thread_check_time_launch_checks).start()
threading.Thread(target=notification_runner).start() threading.Thread(target=notification_runner).start()
@@ -1449,11 +1418,7 @@ def ticker_thread_check_time_launch_checks():
watch_uuid_list = [] watch_uuid_list = []
while True: while True:
try: try:
# Get a list of watches sorted by last_checked, [1] because it gets passed a tuple watch_uuid_list = datastore.data['watching'].keys()
# This is so we examine the most over-due first
for k in sorted(datastore.data['watching'].items(), key=lambda item: item[1].get('last_checked',0)):
watch_uuid_list.append(k[0])
except RuntimeError as e: except RuntimeError as e:
# RuntimeError: dictionary changed size during iteration # RuntimeError: dictionary changed size during iteration
time.sleep(0.1) time.sleep(0.1)
@@ -1493,7 +1458,7 @@ def ticker_thread_check_time_launch_checks():
seconds_since_last_recheck = now - watch['last_checked'] seconds_since_last_recheck = now - watch['last_checked']
if seconds_since_last_recheck >= (threshold + watch.jitter_seconds) and seconds_since_last_recheck >= recheck_time_minimum_seconds: if seconds_since_last_recheck >= (threshold + watch.jitter_seconds) and seconds_since_last_recheck >= recheck_time_minimum_seconds:
if not uuid in running_uuids and uuid not in [q_uuid.item['uuid'] for q_uuid in update_q.queue]: if not uuid in running_uuids and uuid not in [q_uuid for p,q_uuid in update_q.queue]:
# Proxies can be set to have a limit on seconds between which they can be called # Proxies can be set to have a limit on seconds between which they can be called
watch_proxy = datastore.get_preferred_proxy_for_watch(uuid=uuid) watch_proxy = datastore.get_preferred_proxy_for_watch(uuid=uuid)
@@ -1524,9 +1489,8 @@ def ticker_thread_check_time_launch_checks():
priority, priority,
watch.jitter_seconds, watch.jitter_seconds,
now - watch['last_checked'])) now - watch['last_checked']))
# Into the queue with you # Into the queue with you
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=priority, item={'uuid': uuid, 'skip_when_checksum_same': True})) update_q.put((priority, uuid))
# Reset for next time # Reset for next time
watch.jitter_seconds = 0 watch.jitter_seconds = 0

View File

@@ -1,4 +1,3 @@
from changedetectionio import queuedWatchMetaData
from flask_restful import abort, Resource from flask_restful import abort, Resource
from flask import request, make_response from flask import request, make_response
import validators import validators
@@ -25,7 +24,7 @@ class Watch(Resource):
abort(404, message='No watch exists with the UUID of {}'.format(uuid)) abort(404, message='No watch exists with the UUID of {}'.format(uuid))
if request.args.get('recheck'): if request.args.get('recheck'):
self.update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid, 'skip_when_checksum_same': True})) self.update_q.put((1, uuid))
return "OK", 200 return "OK", 200
# Return without history, get that via another API call # Return without history, get that via another API call
@@ -101,7 +100,7 @@ class CreateWatch(Resource):
extras = {'title': json_data['title'].strip()} if json_data.get('title') else {} extras = {'title': json_data['title'].strip()} if json_data.get('title') else {}
new_uuid = self.datastore.add_watch(url=json_data['url'].strip(), tag=tag, extras=extras) new_uuid = self.datastore.add_watch(url=json_data['url'].strip(), tag=tag, extras=extras)
self.update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': new_uuid, 'skip_when_checksum_same': True})) self.update_q.put((1, new_uuid))
return {'uuid': new_uuid}, 201 return {'uuid': new_uuid}, 201
# Return concise list of available watches and some very basic info # Return concise list of available watches and some very basic info
@@ -119,7 +118,7 @@ class CreateWatch(Resource):
if request.args.get('recheck_all'): if request.args.get('recheck_all'):
for uuid in self.datastore.data['watching'].keys(): for uuid in self.datastore.data['watching'].keys():
self.update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid, 'skip_when_checksum_same': True})) self.update_q.put((1, uuid))
return {'status': "OK"}, 200 return {'status': "OK"}, 200
return list, 200 return list, 200

View File

@@ -21,6 +21,9 @@
# OR # OR
# - use multiprocessing to bump this over to its own process and add some transport layer (queue/pipes) # - use multiprocessing to bump this over to its own process and add some transport layer (queue/pipes)
from distutils.util import strtobool from distutils.util import strtobool
from flask import Blueprint, request, make_response from flask import Blueprint, request, make_response
from flask_login import login_required from flask_login import login_required
@@ -30,35 +33,38 @@ from changedetectionio.store import ChangeDetectionStore
browsersteps_live_ui_o = {} browsersteps_live_ui_o = {}
browsersteps_playwright_browser_interface = None browsersteps_playwright_browser_interface = None
browsersteps_playwright_browser_interface_browser = None
browsersteps_playwright_browser_interface_context = None
browsersteps_playwright_browser_interface_end_time = None
browsersteps_playwright_browser_interface_start_time = None browsersteps_playwright_browser_interface_start_time = None
browsersteps_playwright_browser_interface_browser = None
browsersteps_playwright_browser_interface_end_time = None
def cleanup_playwright_session(): def cleanup_playwright_session():
print("Cleaning up old playwright session because time was up")
global browsersteps_live_ui_o
global browsersteps_playwright_browser_interface global browsersteps_playwright_browser_interface
global browsersteps_live_ui_o
global browsersteps_playwright_browser_interface_browser global browsersteps_playwright_browser_interface_browser
global browsersteps_playwright_browser_interface_context global browsersteps_playwright_browser_interface
global browsersteps_playwright_browser_interface_end_time
global browsersteps_playwright_browser_interface_start_time global browsersteps_playwright_browser_interface_start_time
global browsersteps_playwright_browser_interface_end_time
import psutil
current_process = psutil.Process()
children = current_process.children(recursive=True)
for child in children:
print (child)
print('Child pid is {}'.format(child.pid))
# .stop() hangs sometimes if its called when there are no children to process
# but how do we know this is our child? dunno
if children:
browsersteps_playwright_browser_interface.stop()
browsersteps_live_ui_o = {} browsersteps_live_ui_o = {}
browsersteps_playwright_browser_interface = None browsersteps_playwright_browser_interface = None
browsersteps_playwright_browser_interface_start_time = None
browsersteps_playwright_browser_interface_browser = None browsersteps_playwright_browser_interface_browser = None
browsersteps_playwright_browser_interface_end_time = None browsersteps_playwright_browser_interface_end_time = None
browsersteps_playwright_browser_interface_start_time = None
print("Cleaning up old playwright session because time was up, calling .goodbye()")
try:
browsersteps_playwright_browser_interface_context.goodbye()
except Exception as e:
print ("Got exception in shutdown, probably OK")
print (str(e))
browsersteps_playwright_browser_interface_context = None
print ("Cleaning up old playwright session because time was up - done") print ("Cleaning up old playwright session because time was up - done")
def construct_blueprint(datastore: ChangeDetectionStore): def construct_blueprint(datastore: ChangeDetectionStore):
@@ -93,8 +99,12 @@ def construct_blueprint(datastore: ChangeDetectionStore):
if browsersteps_playwright_browser_interface_end_time: if browsersteps_playwright_browser_interface_end_time:
remaining = browsersteps_playwright_browser_interface_end_time-time.time() remaining = browsersteps_playwright_browser_interface_end_time-time.time()
if browsersteps_playwright_browser_interface_end_time and remaining <= 0: if browsersteps_playwright_browser_interface_end_time and remaining <= 0:
cleanup_playwright_session() cleanup_playwright_session()
return make_response('Browser session expired, please reload the Browser Steps interface', 401)
return make_response('Browser session expired, please reload the Browser Steps interface', 500)
# Actions - step/apply/etc, do the thing and return state # Actions - step/apply/etc, do the thing and return state
if request.method == 'POST': if request.method == 'POST':
@@ -121,11 +131,21 @@ def construct_blueprint(datastore: ChangeDetectionStore):
this_session.call_action(action_name=step_operation, this_session.call_action(action_name=step_operation,
selector=step_selector, selector=step_selector,
optional_value=step_optional_value) optional_value=step_optional_value)
except playwright._impl._api_types.TimeoutError as e:
print("Element wasnt found :-(", step_operation)
return make_response("Element was not found on page", 401)
except playwright._impl._api_types.Error as e:
# Browser/playwright level error
print("Browser error - got playwright._impl._api_types.Error, try reloading the session/browser")
print (str(e))
except Exception as e:
print("Exception when calling step operation", step_operation, str(e))
# Try to find something of value to give back to the user # Try to find something of value to give back to the user
return make_response(str(e).splitlines()[0], 401) for l in str(e).splitlines():
if 'DOMException' in l:
return make_response(l, 401)
return make_response('Browser session ran out of time :( Please reload this page.', 401)
# Get visual selector ready/update its data (also use the current filter info from the page?) # Get visual selector ready/update its data (also use the current filter info from the page?)
# When the last 'apply' button was pressed # When the last 'apply' button was pressed
@@ -142,11 +162,10 @@ def construct_blueprint(datastore: ChangeDetectionStore):
if not browsersteps_playwright_browser_interface: if not browsersteps_playwright_browser_interface:
print("Starting connection with playwright") print("Starting connection with playwright")
logging.debug("browser_steps.py connecting") logging.debug("browser_steps.py connecting")
from playwright.sync_api import sync_playwright
browsersteps_playwright_browser_interface = sync_playwright().start()
global browsersteps_playwright_browser_interface_context
from . import nonContext
browsersteps_playwright_browser_interface_context = nonContext.c_sync_playwright()
browsersteps_playwright_browser_interface = browsersteps_playwright_browser_interface_context.start()
time.sleep(1) time.sleep(1)
# At 20 minutes, some other variable is closing it # At 20 minutes, some other variable is closing it
@@ -186,45 +205,21 @@ def construct_blueprint(datastore: ChangeDetectionStore):
cleanup_playwright_session() cleanup_playwright_session()
return make_response('Browser session ran out of time :( Please reload this page.', 401) return make_response('Browser session ran out of time :( Please reload this page.', 401)
response = None try:
state = this_session.get_current_state()
except playwright._impl._api_types.Error as e:
return make_response("Browser session ran out of time :( Please reload this page."+str(e), 401)
if request.method == 'POST': p = {'screenshot': "data:image/png;base64,{}".format(
# Screenshots and other info only needed on requesting a step (POST) base64.b64encode(state[0]).decode('ascii')),
try: 'xpath_data': state[1],
state = this_session.get_current_state() 'session_age_start': this_session.age_start,
except playwright._impl._api_types.Error as e: 'browser_time_remaining': round(remaining)
return make_response("Browser session ran out of time :( Please reload this page."+str(e), 401) }
# Use send_file() which is way faster than read/write loop on bytes
import json
from tempfile import mkstemp
from flask import send_file
tmp_fd, tmp_file = mkstemp(text=True, suffix=".json", prefix="changedetectionio-")
output = json.dumps({'screenshot': "data:image/jpeg;base64,{}".format( # @todo BSON/binary JSON, faster xfer, OR pick it off the disk
base64.b64encode(state[0]).decode('ascii')), return p
'xpath_data': state[1],
'session_age_start': this_session.age_start,
'browser_time_remaining': round(remaining)
})
with os.fdopen(tmp_fd, 'w') as f:
f.write(output)
response = make_response(send_file(path_or_file=tmp_file,
mimetype='application/json; charset=UTF-8',
etag=True))
# No longer needed
os.unlink(tmp_file)
elif request.method == 'GET':
# Just enough to get the session rolling, it will call for goto-site via POST next
response = make_response({
'session_age_start': this_session.age_start,
'browser_time_remaining': round(remaining)
})
return response
return browser_steps_blueprint return browser_steps_blueprint

View File

@@ -22,7 +22,6 @@ browser_step_ui_config = {'Choose one': '0 0',
'Click element': '1 0', 'Click element': '1 0',
'Click element containing text': '0 1', 'Click element containing text': '0 1',
'Enter text in field': '1 1', 'Enter text in field': '1 1',
'Execute JS': '0 1',
# 'Extract text and use as filter': '1 0', # 'Extract text and use as filter': '1 0',
'Goto site': '0 0', 'Goto site': '0 0',
'Press Enter': '0 0', 'Press Enter': '0 0',
@@ -75,20 +74,22 @@ class steppable_browser_interface():
def action_goto_url(self, url, optional_value): def action_goto_url(self, url, optional_value):
# self.page.set_viewport_size({"width": 1280, "height": 5000}) # self.page.set_viewport_size({"width": 1280, "height": 5000})
now = time.time() now = time.time()
response = self.page.goto(url, timeout=0, wait_until='commit') response = self.page.goto(url, timeout=0, wait_until='domcontentloaded')
print("Time to goto URL", time.time() - now)
# Wait_until = commit # Wait_until = commit
# - `'commit'` - consider operation to be finished when network response is received and the document started loading. # - `'commit'` - consider operation to be finished when network response is received and the document started loading.
# Better to not use any smarts from Playwright and just wait an arbitrary number of seconds # Better to not use any smarts from Playwright and just wait an arbitrary number of seconds
# This seemed to solve nearly all 'TimeoutErrors' # This seemed to solve nearly all 'TimeoutErrors'
print("Time to goto URL ", time.time() - now) extra_wait = int(os.getenv("WEBDRIVER_DELAY_BEFORE_CONTENT_READY", 5))
self.page.wait_for_timeout(extra_wait * 1000)
def action_click_element_containing_text(self, selector=None, value=''): def action_click_element_containing_text(self, selector=None, value=''):
if not len(value.strip()): if not len(value.strip()):
return return
elem = self.page.get_by_text(value) elem = self.page.get_by_text(value)
if elem.count(): if elem.count():
elem.first.click(delay=randint(200, 500), timeout=3000) elem.first.click(delay=randint(200, 500))
def action_enter_text_in_field(self, selector, value): def action_enter_text_in_field(self, selector, value):
if not len(selector.strip()): if not len(selector.strip()):
@@ -96,9 +97,6 @@ class steppable_browser_interface():
self.page.fill(selector, value, timeout=10 * 1000) self.page.fill(selector, value, timeout=10 * 1000)
def action_execute_js(self, selector, value):
self.page.evaluate(value)
def action_click_element(self, selector, value): def action_click_element(self, selector, value):
print("Clicking element") print("Clicking element")
if not len(selector.strip()): if not len(selector.strip()):
@@ -144,10 +142,10 @@ class steppable_browser_interface():
self.page.keyboard.press("PageDown", delay=randint(200, 500)) self.page.keyboard.press("PageDown", delay=randint(200, 500))
def action_check_checkbox(self, selector, value): def action_check_checkbox(self, selector, value):
self.page.locator(selector).check(timeout=1000) self.page.locator(selector).check()
def action_uncheck_checkbox(self, selector, value): def action_uncheck_checkbox(self, selector, value):
self.page.locator(selector, timeout=1000).uncheck(timeout=1000) self.page.locator(selector).uncheck()
# Responsible for maintaining a live 'context' with browserless # Responsible for maintaining a live 'context' with browserless
@@ -209,7 +207,7 @@ class browsersteps_live_ui(steppable_browser_interface):
# Listen for all console events and handle errors # Listen for all console events and handle errors
self.page.on("console", lambda msg: print(f"Browser steps console - {msg.type}: {msg.text} {msg.args}")) self.page.on("console", lambda msg: print(f"Browser steps console - {msg.type}: {msg.text} {msg.args}"))
print("Time to browser setup", time.time() - now) print("time to browser setup", time.time() - now)
self.page.wait_for_timeout(1 * 1000) self.page.wait_for_timeout(1 * 1000)
def mark_as_closed(self): def mark_as_closed(self):
@@ -234,7 +232,7 @@ class browsersteps_live_ui(steppable_browser_interface):
self.page.evaluate("var include_filters=''") self.page.evaluate("var include_filters=''")
# Go find the interactive elements # Go find the interactive elements
# @todo in the future, something smarter that can scan for elements with .click/focus etc event handlers? # @todo in the future, something smarter that can scan for elements with .click/focus etc event handlers?
elements = 'a,button,input,select,textarea,i,th,td,p,li,h1,h2,h3,h4,div,span' elements = 'a,button,input,select,textarea,i,th,td,p,li,h1,h2,h3,h4'
xpath_element_js = xpath_element_js.replace('%ELEMENTS%', elements) xpath_element_js = xpath_element_js.replace('%ELEMENTS%', elements)
xpath_data = self.page.evaluate("async () => {" + xpath_element_js + "}") xpath_data = self.page.evaluate("async () => {" + xpath_element_js + "}")
# So the JS will find the smallest one first # So the JS will find the smallest one first

View File

@@ -1,18 +0,0 @@
from playwright.sync_api import PlaywrightContextManager
import asyncio
# So playwright wants to run as a context manager, but we do something horrible and hacky
# we are holding the session open for as long as possible, then shutting it down, and opening a new one
# So it means we don't get to use PlaywrightContextManager' __enter__ __exit__
# To work around this, make goodbye() act the same as the __exit__()
#
# But actually I think this is because the context is opened correctly with __enter__() but we timeout the connection
# then theres some lock condition where we cant destroy it without it hanging
class c_PlaywrightContextManager(PlaywrightContextManager):
def goodbye(self) -> None:
self.__exit__()
def c_sync_playwright() -> PlaywrightContextManager:
return c_PlaywrightContextManager()

View File

@@ -1,33 +0,0 @@
from distutils.util import strtobool
from flask import Blueprint, flash, redirect, url_for
from flask_login import login_required
from changedetectionio.store import ChangeDetectionStore
from changedetectionio import queuedWatchMetaData
from queue import PriorityQueue
PRICE_DATA_TRACK_ACCEPT = 'accepted'
PRICE_DATA_TRACK_REJECT = 'rejected'
def construct_blueprint(datastore: ChangeDetectionStore, update_q: PriorityQueue):
price_data_follower_blueprint = Blueprint('price_data_follower', __name__)
@login_required
@price_data_follower_blueprint.route("/<string:uuid>/accept", methods=['GET'])
def accept(uuid):
datastore.data['watching'][uuid]['track_ldjson_price_data'] = PRICE_DATA_TRACK_ACCEPT
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid, 'skip_when_checksum_same': False}))
return redirect(url_for("form_watch_checknow", uuid=uuid))
@login_required
@price_data_follower_blueprint.route("/<string:uuid>/reject", methods=['GET'])
def reject(uuid):
datastore.data['watching'][uuid]['track_ldjson_price_data'] = PRICE_DATA_TRACK_REJECT
return redirect(url_for("index"))
return price_data_follower_blueprint

View File

@@ -23,9 +23,6 @@ class Non200ErrorCodeReceived(Exception):
self.page_text = html_tools.html_to_text(page_html) self.page_text = html_tools.html_to_text(page_html)
return return
class checksumFromPreviousCheckWasTheSame(Exception):
def __init__(self):
return
class JSActionExceptions(Exception): class JSActionExceptions(Exception):
def __init__(self, status_code, url, screenshot, message=''): def __init__(self, status_code, url, screenshot, message=''):
@@ -42,7 +39,7 @@ class BrowserStepsStepTimout(Exception):
class PageUnloadable(Exception): class PageUnloadable(Exception):
def __init__(self, status_code, url, message, screenshot=False): def __init__(self, status_code, url, screenshot=False, message=False):
# Set this so we can use it in other parts of the app # Set this so we can use it in other parts of the app
self.status_code = status_code self.status_code = status_code
self.url = url self.url = url
@@ -289,8 +286,6 @@ class base_html_playwright(Fetcher):
proxy=self.proxy, proxy=self.proxy,
# This is needed to enable JavaScript execution on GitHub and others # This is needed to enable JavaScript execution on GitHub and others
bypass_csp=True, bypass_csp=True,
# Can't think why we need the service workers for our use case?
service_workers='block',
# Should never be needed # Should never be needed
accept_downloads=False accept_downloads=False
) )
@@ -299,34 +294,24 @@ class base_html_playwright(Fetcher):
if len(request_headers): if len(request_headers):
context.set_extra_http_headers(request_headers) context.set_extra_http_headers(request_headers)
try:
self.page.set_default_navigation_timeout(90000) self.page.set_default_navigation_timeout(90000)
self.page.set_default_timeout(90000) self.page.set_default_timeout(90000)
# Listen for all console events and handle errors # Listen for all console events and handle errors
self.page.on("console", lambda msg: print(f"Playwright console: Watch URL: {url} {msg.type}: {msg.text} {msg.args}")) self.page.on("console", lambda msg: print(f"Playwright console: Watch URL: {url} {msg.type}: {msg.text} {msg.args}"))
# Goto page # Bug - never set viewport size BEFORE page.goto
try:
# Waits for the next navigation. Using Python context manager
# prevents a race condition between clicking and waiting for a navigation.
with self.page.expect_navigation():
response = self.page.goto(url, wait_until='load')
# Wait_until = commit # Wait_until = commit
# - `'commit'` - consider operation to be finished when network response is received and the document started loading. # - `'commit'` - consider operation to be finished when network response is received and the document started loading.
# Better to not use any smarts from Playwright and just wait an arbitrary number of seconds # Better to not use any smarts from Playwright and just wait an arbitrary number of seconds
# This seemed to solve nearly all 'TimeoutErrors' # This seemed to solve nearly all 'TimeoutErrors'
response = self.page.goto(url, wait_until='commit')
except playwright._impl._api_types.Error as e:
# Retry once - https://github.com/browserless/chrome/issues/2485
# Sometimes errors related to invalid cert's and other can be random
print ("Content Fetcher > retrying request got error - ", str(e))
time.sleep(1)
response = self.page.goto(url, wait_until='commit')
except Exception as e:
print ("Content Fetcher > Other exception when page.goto", str(e))
context.close()
browser.close()
raise PageUnloadable(url=url, status_code=None, message=str(e))
# Execute any browser steps
try:
extra_wait = int(os.getenv("WEBDRIVER_DELAY_BEFORE_CONTENT_READY", 5)) + self.render_extract_delay extra_wait = int(os.getenv("WEBDRIVER_DELAY_BEFORE_CONTENT_READY", 5)) + self.render_extract_delay
self.page.wait_for_timeout(extra_wait * 1000) self.page.wait_for_timeout(extra_wait * 1000)
@@ -339,15 +324,17 @@ class base_html_playwright(Fetcher):
# This can be ok, we will try to grab what we could retrieve # This can be ok, we will try to grab what we could retrieve
pass pass
except Exception as e: except Exception as e:
print ("Content Fetcher > Other exception when executing custom JS code", str(e)) print ("other exception when page.goto")
print (str(e))
context.close() context.close()
browser.close() browser.close()
raise PageUnloadable(url=url, status_code=None, message=str(e)) raise PageUnloadable(url=url, status_code=None)
if response is None: if response is None:
context.close() context.close()
browser.close() browser.close()
print ("Content Fetcher > Response object was none") print ("response object was none")
raise EmptyReply(url=url, status_code=None) raise EmptyReply(url=url, status_code=None)
# Bug 2(?) Set the viewport size AFTER loading the page # Bug 2(?) Set the viewport size AFTER loading the page
@@ -366,7 +353,7 @@ class base_html_playwright(Fetcher):
if len(self.page.content().strip()) == 0: if len(self.page.content().strip()) == 0:
context.close() context.close()
browser.close() browser.close()
print ("Content Fetcher > Content was empty") print ("Content was empty")
raise EmptyReply(url=url, status_code=None) raise EmptyReply(url=url, status_code=None)
# Bug 2(?) Set the viewport size AFTER loading the page # Bug 2(?) Set the viewport size AFTER loading the page
@@ -511,7 +498,7 @@ class base_html_webdriver(Fetcher):
try: try:
self.driver.quit() self.driver.quit()
except Exception as e: except Exception as e:
print("Content Fetcher > Exception in chrome shutdown/quit" + str(e)) print("Exception in chrome shutdown/quit" + str(e))
# "html_requests" is listed as the default fetcher in store.py! # "html_requests" is listed as the default fetcher in store.py!

View File

@@ -1,13 +1,11 @@
import hashlib import hashlib
import json
import logging import logging
import os import os
import re import re
import time
import urllib3 import urllib3
from changedetectionio import content_fetcher, html_tools from changedetectionio import content_fetcher, html_tools
from changedetectionio.blueprint.price_data_follower import PRICE_DATA_TRACK_ACCEPT, PRICE_DATA_TRACK_REJECT
from copy import deepcopy
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
@@ -40,7 +38,8 @@ class perform_site_check():
return regex return regex
def run(self, uuid, skip_when_checksum_same=True): def run(self, uuid):
from copy import deepcopy
changed_detected = False changed_detected = False
screenshot = False # as bytes screenshot = False # as bytes
stripped_text_from_html = "" stripped_text_from_html = ""
@@ -123,14 +122,6 @@ class perform_site_check():
self.screenshot = fetcher.screenshot self.screenshot = fetcher.screenshot
self.xpath_data = fetcher.xpath_data self.xpath_data = fetcher.xpath_data
# Watches added automatically in the queue manager will skip if its the same checksum as the previous run
# Saves a lot of CPU
update_obj['previous_md5_before_filters'] = hashlib.md5(fetcher.content.encode('utf-8')).hexdigest()
if skip_when_checksum_same:
if update_obj['previous_md5_before_filters'] == watch.get('previous_md5_before_filters'):
raise content_fetcher.checksumFromPreviousCheckWasTheSame()
# Fetching complete, now filters # Fetching complete, now filters
# @todo move to class / maybe inside of fetcher abstract base? # @todo move to class / maybe inside of fetcher abstract base?
@@ -149,7 +140,7 @@ class perform_site_check():
is_html = False is_html = False
is_json = False is_json = False
include_filters_rule = deepcopy(watch.get('include_filters', [])) include_filters_rule = watch.get('include_filters', [])
# include_filters_rule = watch['include_filters'] # include_filters_rule = watch['include_filters']
subtractive_selectors = watch.get( subtractive_selectors = watch.get(
"subtractive_selectors", [] "subtractive_selectors", []
@@ -157,10 +148,6 @@ class perform_site_check():
"global_subtractive_selectors", [] "global_subtractive_selectors", []
) )
# Inject a virtual LD+JSON price tracker rule
if watch.get('track_ldjson_price_data', '') == PRICE_DATA_TRACK_ACCEPT:
include_filters_rule.append(html_tools.LD_JSON_PRODUCT_OFFER_SELECTOR)
has_filter_rule = include_filters_rule and len("".join(include_filters_rule).strip()) has_filter_rule = include_filters_rule and len("".join(include_filters_rule).strip())
has_subtractive_selectors = subtractive_selectors and len(subtractive_selectors[0].strip()) has_subtractive_selectors = subtractive_selectors and len(subtractive_selectors[0].strip())
@@ -168,14 +155,6 @@ class perform_site_check():
include_filters_rule.append("json:$") include_filters_rule.append("json:$")
has_filter_rule = True has_filter_rule = True
if is_json:
# Sort the JSON so we dont get false alerts when the content is just re-ordered
try:
fetcher.content = json.dumps(json.loads(fetcher.content), sort_keys=True)
except Exception as e:
# Might have just been a snippet, or otherwise bad JSON, continue
pass
if has_filter_rule: if has_filter_rule:
json_filter_prefixes = ['json:', 'jq:'] json_filter_prefixes = ['json:', 'jq:']
for filter in include_filters_rule: for filter in include_filters_rule:
@@ -183,8 +162,6 @@ class perform_site_check():
stripped_text_from_html += html_tools.extract_json_as_string(content=fetcher.content, json_filter=filter) stripped_text_from_html += html_tools.extract_json_as_string(content=fetcher.content, json_filter=filter)
is_html = False is_html = False
if is_html or is_source: if is_html or is_source:
# CSS Filter, extract the HTML that matches and feed that into the existing inscriptis::get_text # CSS Filter, extract the HTML that matches and feed that into the existing inscriptis::get_text
@@ -196,13 +173,9 @@ class perform_site_check():
# Don't run get_text or xpath/css filters on plaintext # Don't run get_text or xpath/css filters on plaintext
stripped_text_from_html = html_content stripped_text_from_html = html_content
else: else:
# Does it have some ld+json price data? used for easier monitoring
update_obj['has_ldjson_price_data'] = html_tools.has_ldjson_product_info(fetcher.content)
# Then we assume HTML # Then we assume HTML
if has_filter_rule: if has_filter_rule:
html_content = "" html_content = ""
for filter_rule in include_filters_rule: for filter_rule in include_filters_rule:
# For HTML/XML we offer xpath as an option, just start a regular xPath "/.." # For HTML/XML we offer xpath as an option, just start a regular xPath "/.."
if filter_rule[0] == '/' or filter_rule.startswith('xpath:'): if filter_rule[0] == '/' or filter_rule.startswith('xpath:'):

View File

@@ -193,7 +193,7 @@ class ValidateAppRiseServers(object):
message = field.gettext('\'%s\' is not a valid AppRise URL.' % (server_url)) message = field.gettext('\'%s\' is not a valid AppRise URL.' % (server_url))
raise ValidationError(message) raise ValidationError(message)
class ValidateJinja2Template(object): class ValidateTokensList(object):
""" """
Validates that a {token} is from a valid set Validates that a {token} is from a valid set
""" """
@@ -202,27 +202,14 @@ class ValidateJinja2Template(object):
def __call__(self, form, field): def __call__(self, form, field):
from changedetectionio import notification from changedetectionio import notification
regex = re.compile('{.*?}')
from jinja2 import Environment, BaseLoader, TemplateSyntaxError for p in re.findall(regex, field.data):
from jinja2.meta import find_undeclared_variables if not p.strip('{}') in notification.valid_tokens:
message = field.gettext('Token \'%s\' is not a valid token.')
raise ValidationError(message % (p))
try:
jinja2_env = Environment(loader=BaseLoader)
jinja2_env.globals.update(notification.valid_tokens)
rendered = jinja2_env.from_string(field.data).render()
except TemplateSyntaxError as e:
raise ValidationError(f"This is not a valid Jinja2 template: {e}") from e
ast = jinja2_env.parse(field.data)
undefined = ", ".join(find_undeclared_variables(ast))
if undefined:
raise ValidationError(
f"The following tokens used in the notification are not valid: {undefined}"
)
class validateURL(object): class validateURL(object):
""" """
Flask wtform validators wont work with basic auth Flask wtform validators wont work with basic auth
""" """
@@ -238,7 +225,6 @@ class validateURL(object):
message = field.gettext('\'%s\' is not a valid URL.' % (field.data.strip())) message = field.gettext('\'%s\' is not a valid URL.' % (field.data.strip()))
raise ValidationError(message) raise ValidationError(message)
class ValidateListRegex(object): class ValidateListRegex(object):
""" """
Validates that anything that looks like a regex passes as a regex Validates that anything that looks like a regex passes as a regex
@@ -347,11 +333,11 @@ class quickWatchForm(Form):
# Common to a single watch and the global settings # Common to a single watch and the global settings
class commonSettingsForm(Form): class commonSettingsForm(Form):
notification_urls = StringListField('Notification URL List', validators=[validators.Optional(), ValidateAppRiseServers()]) notification_urls = StringListField('Notification URL list', validators=[validators.Optional(), ValidateAppRiseServers()])
notification_title = StringField('Notification Title', default='ChangeDetection.io Notification - {{ watch_url }}', validators=[validators.Optional(), ValidateJinja2Template()]) notification_title = StringField('Notification title', validators=[validators.Optional(), ValidateTokensList()])
notification_body = TextAreaField('Notification Body', default='{{ watch_url }} had a change.', validators=[validators.Optional(), ValidateJinja2Template()]) notification_body = TextAreaField('Notification body', validators=[validators.Optional(), ValidateTokensList()])
notification_format = SelectField('Notification format', choices=valid_notification_formats.keys()) notification_format = SelectField('Notification format', choices=valid_notification_formats.keys())
fetch_backend = RadioField(u'Fetch Method', choices=content_fetcher.available_fetchers(), validators=[ValidateContentFetcherIsReady()]) fetch_backend = RadioField(u'Fetch method', choices=content_fetcher.available_fetchers(), validators=[ValidateContentFetcherIsReady()])
extract_title_as_title = BooleanField('Extract <title> from document and use as watch title', default=False) extract_title_as_title = BooleanField('Extract <title> from document and use as watch title', default=False)
webdriver_delay = IntegerField('Wait seconds before extracting text', validators=[validators.Optional(), validators.NumberRange(min=1, webdriver_delay = IntegerField('Wait seconds before extracting text', validators=[validators.Optional(), validators.NumberRange(min=1,
message="Should contain one or more seconds")]) message="Should contain one or more seconds")])
@@ -462,8 +448,3 @@ class globalSettingsForm(Form):
requests = FormField(globalSettingsRequestForm) requests = FormField(globalSettingsRequestForm)
application = FormField(globalSettingsApplicationForm) application = FormField(globalSettingsApplicationForm)
save_button = SubmitField('Save', render_kw={"class": "pure-button pure-button-primary"}) save_button = SubmitField('Save', render_kw={"class": "pure-button pure-button-primary"})
class extractDataForm(Form):
extract_regex = StringField('RegEx to extract', validators=[validators.Length(min=1, message="Needs a RegEx")])
extract_submit_button = SubmitField('Extract as CSV', render_kw={"class": "pure-button pure-button-primary"})

View File

@@ -10,10 +10,6 @@ import re
# HTML added to be sure each result matching a filter (.example) gets converted to a new line by Inscriptis # HTML added to be sure each result matching a filter (.example) gets converted to a new line by Inscriptis
TEXT_FILTER_LIST_LINE_SUFFIX = "<br/>" TEXT_FILTER_LIST_LINE_SUFFIX = "<br/>"
# 'price' , 'lowPrice', 'highPrice' are usually under here
# all of those may or may not appear on different websites
LD_JSON_PRODUCT_OFFER_SELECTOR = "json:$..offers"
class JSONNotFound(ValueError): class JSONNotFound(ValueError):
def __init__(self, msg): def __init__(self, msg):
ValueError.__init__(self, msg) ValueError.__init__(self, msg)
@@ -131,10 +127,8 @@ def _get_stripped_text_from_json_match(match):
return stripped_text_from_html return stripped_text_from_html
# content - json def extract_json_as_string(content, json_filter):
# json_filter - ie json:$..price
# ensure_is_ldjson_info_type - str "product", optional, "@type == product" (I dont know how to do that as a json selector)
def extract_json_as_string(content, json_filter, ensure_is_ldjson_info_type=None):
stripped_text_from_html = False stripped_text_from_html = False
# Try to parse/filter out the JSON, if we get some parser error, then maybe it's embedded <script type=ldjson> # Try to parse/filter out the JSON, if we get some parser error, then maybe it's embedded <script type=ldjson>
@@ -145,12 +139,7 @@ def extract_json_as_string(content, json_filter, ensure_is_ldjson_info_type=None
# Foreach <script json></script> blob.. just return the first that matches json_filter # Foreach <script json></script> blob.. just return the first that matches json_filter
s = [] s = []
soup = BeautifulSoup(content, 'html.parser') soup = BeautifulSoup(content, 'html.parser')
bs_result = soup.findAll('script')
if ensure_is_ldjson_info_type:
bs_result = soup.findAll('script', {"type": "application/ld+json"})
else:
bs_result = soup.findAll('script')
if not bs_result: if not bs_result:
raise JSONNotFound("No parsable JSON found in this document") raise JSONNotFound("No parsable JSON found in this document")
@@ -167,14 +156,7 @@ def extract_json_as_string(content, json_filter, ensure_is_ldjson_info_type=None
continue continue
else: else:
stripped_text_from_html = _parse_json(json_data, json_filter) stripped_text_from_html = _parse_json(json_data, json_filter)
if ensure_is_ldjson_info_type: if stripped_text_from_html:
# Could sometimes be list, string or something else random
if isinstance(json_data, dict):
# If it has LD JSON 'key' @type, and @type is 'product', and something was found for the search
# (Some sites have multiple of the same ld+json @type='product', but some have the review part, some have the 'price' part)
if json_data.get('@type', False) and json_data.get('@type','').lower() == ensure_is_ldjson_info_type.lower() and stripped_text_from_html:
break
elif stripped_text_from_html:
break break
if not stripped_text_from_html: if not stripped_text_from_html:
@@ -261,18 +243,6 @@ def html_to_text(html_content: str, render_anchor_tag_content=False) -> str:
return text_content return text_content
# Does LD+JSON exist with a @type=='product' and a .price set anywhere?
def has_ldjson_product_info(content):
try:
pricing_data = extract_json_as_string(content=content, json_filter=LD_JSON_PRODUCT_OFFER_SELECTOR, ensure_is_ldjson_info_type="product")
except JSONNotFound as e:
# Totally fine
return False
x=bool(pricing_data)
return x
def workarounds_for_obfuscations(content): def workarounds_for_obfuscations(content):
""" """
Some sites are using sneaky tactics to make prices and other information un-renderable by Inscriptis Some sites are using sneaky tactics to make prices and other information un-renderable by Inscriptis

View File

@@ -27,7 +27,6 @@ class model(dict):
'base_url' : None, 'base_url' : None,
'extract_title_as_title': False, 'extract_title_as_title': False,
'empty_pages_are_a_change': False, 'empty_pages_are_a_change': False,
'css_dark_mode': False,
'fetch_backend': getenv("DEFAULT_FETCH_BACKEND", "html_requests"), 'fetch_backend': getenv("DEFAULT_FETCH_BACKEND", "html_requests"),
'filter_failure_notification_threshold_attempts': _FILTER_FAILURE_THRESHOLD_ATTEMPTS_DEFAULT, 'filter_failure_notification_threshold_attempts': _FILTER_FAILURE_THRESHOLD_ATTEMPTS_DEFAULT,
'global_ignore_text': [], # List of text to ignore when calculating the comparison checksum 'global_ignore_text': [], # List of text to ignore when calculating the comparison checksum

View File

@@ -14,52 +14,49 @@ from changedetectionio.notification import (
class model(dict): class model(dict):
__newest_history_key = None __newest_history_key = None
__history_n = 0 __history_n=0
__base_config = { __base_config = {
# 'history': {}, # Dict of timestamp and output stripped filename (removed) #'history': {}, # Dict of timestamp and output stripped filename (removed)
# 'newest_history_key': 0, (removed, taken from history.txt index) #'newest_history_key': 0, (removed, taken from history.txt index)
'body': None, 'body': None,
'check_unique_lines': False, # On change-detected, compare against all history if its something new 'check_unique_lines': False, # On change-detected, compare against all history if its something new
'check_count': 0, 'check_count': 0,
'consecutive_filter_failures': 0, # Every time the CSS/xPath filter cannot be located, reset when all is fine. 'consecutive_filter_failures': 0, # Every time the CSS/xPath filter cannot be located, reset when all is fine.
'extract_text': [], # Extract text by regex after filters 'extract_text': [], # Extract text by regex after filters
'extract_title_as_title': False, 'extract_title_as_title': False,
'fetch_backend': None, 'fetch_backend': None,
'filter_failure_notification_send': strtobool(os.getenv('FILTER_FAILURE_NOTIFICATION_SEND_DEFAULT', 'True')), 'filter_failure_notification_send': strtobool(os.getenv('FILTER_FAILURE_NOTIFICATION_SEND_DEFAULT', 'True')),
'has_ldjson_price_data': None, 'headers': {}, # Extra headers to send
'track_ldjson_price_data': None, 'ignore_text': [], # List of text to ignore when calculating the comparison checksum
'headers': {}, # Extra headers to send 'include_filters': [],
'ignore_text': [], # List of text to ignore when calculating the comparison checksum 'last_checked': 0,
'include_filters': [], 'last_error': False,
'last_checked': 0, 'last_viewed': 0, # history key value of the last viewed via the [diff] link
'last_error': False, 'method': 'GET',
'last_viewed': 0, # history key value of the last viewed via the [diff] link # Custom notification content
'method': 'GET', 'notification_body': None,
# Custom notification content 'notification_format': default_notification_format_for_watch,
'notification_body': None, 'notification_muted': False,
'notification_format': default_notification_format_for_watch, 'notification_title': None,
'notification_muted': False, 'notification_screenshot': False, # Include the latest screenshot if available and supported by the apprise URL
'notification_title': None, 'notification_urls': [], # List of URLs to add to the notification Queue (Usually AppRise)
'notification_screenshot': False, # Include the latest screenshot if available and supported by the apprise URL 'paused': False,
'notification_urls': [], # List of URLs to add to the notification Queue (Usually AppRise) 'previous_md5': False,
'paused': False, 'proxy': None, # Preferred proxy connection
'previous_md5': False, 'subtractive_selectors': [],
'previous_md5_before_filters': False, # Used for skipping changedetection entirely 'tag': None,
'proxy': None, # Preferred proxy connection 'text_should_not_be_present': [], # Text that should not present
'subtractive_selectors': [], # Re #110, so then if this is set to None, we know to use the default value instead
'tag': None, # Requires setting to None on submit if it's the same as the default
'text_should_not_be_present': [], # Text that should not present # Should be all None by default, so we use the system default in this case.
# Re #110, so then if this is set to None, we know to use the default value instead 'time_between_check': {'weeks': None, 'days': None, 'hours': None, 'minutes': None, 'seconds': None},
# Requires setting to None on submit if it's the same as the default 'title': None,
# Should be all None by default, so we use the system default in this case. 'trigger_text': [], # List of text or regex to wait for until a change is detected
'time_between_check': {'weeks': None, 'days': None, 'hours': None, 'minutes': None, 'seconds': None}, 'url': None,
'title': None, 'uuid': str(uuid.uuid4()),
'trigger_text': [], # List of text or regex to wait for until a change is detected 'webdriver_delay': None,
'url': None, 'webdriver_js_execute_code': None, # Run before change-detection
'uuid': str(uuid.uuid4()), }
'webdriver_delay': None,
'webdriver_js_execute_code': None, # Run before change-detection
}
jitter_seconds = 0 jitter_seconds = 0
def __init__(self, *arg, **kw): def __init__(self, *arg, **kw):
@@ -321,47 +318,3 @@ class model(dict):
if os.path.isfile(fname): if os.path.isfile(fname):
return fname return fname
return False return False
def extract_regex_from_all_history(self, regex):
import csv
import re
import datetime
csv_output_filename = False
csv_writer = False
f = None
# self.history will be keyed with the full path
for k, fname in self.history.items():
if os.path.isfile(fname):
with open(fname, "r") as f:
contents = f.read()
res = re.findall(regex, contents, re.MULTILINE)
if res:
if not csv_writer:
# A file on the disk can be transferred much faster via flask than a string reply
csv_output_filename = 'report.csv'
f = open(os.path.join(self.watch_data_dir, csv_output_filename), 'w')
# @todo some headers in the future
#fieldnames = ['Epoch seconds', 'Date']
csv_writer = csv.writer(f,
delimiter=',',
quotechar='"',
quoting=csv.QUOTE_MINIMAL,
#fieldnames=fieldnames
)
csv_writer.writerow(['Epoch seconds', 'Date'])
# csv_writer.writeheader()
date_str = datetime.datetime.fromtimestamp(int(k)).strftime('%Y-%m-%d %H:%M:%S')
for r in res:
row = [k, date_str]
if isinstance(r, str):
row.append(r)
else:
row+=r
csv_writer.writerow(row)
if f:
f.close()
return csv_output_filename

View File

@@ -1,7 +1,5 @@
import apprise import apprise
from jinja2 import Environment, BaseLoader
from apprise import NotifyFormat from apprise import NotifyFormat
import json
valid_tokens = { valid_tokens = {
'base_url': '', 'base_url': '',
@@ -18,8 +16,8 @@ valid_tokens = {
default_notification_format_for_watch = 'System default' default_notification_format_for_watch = 'System default'
default_notification_format = 'Text' default_notification_format = 'Text'
default_notification_body = '{{watch_url}} had a change.\n---\n{{diff}}\n---\n' default_notification_body = '{watch_url} had a change.\n---\n{diff}\n---\n'
default_notification_title = 'ChangeDetection.io Notification - {{watch_url}}' default_notification_title = 'ChangeDetection.io Notification - {watch_url}'
valid_notification_formats = { valid_notification_formats = {
'Text': NotifyFormat.TEXT, 'Text': NotifyFormat.TEXT,
@@ -29,67 +27,25 @@ valid_notification_formats = {
default_notification_format_for_watch: default_notification_format_for_watch default_notification_format_for_watch: default_notification_format_for_watch
} }
# include the decorator
from apprise.decorators import notify
@notify(on="delete")
@notify(on="deletes")
@notify(on="get")
@notify(on="gets")
@notify(on="post")
@notify(on="posts")
@notify(on="put")
@notify(on="puts")
def apprise_custom_api_call_wrapper(body, title, notify_type, *args, **kwargs):
import requests
url = kwargs['meta'].get('url')
if url.startswith('post'):
r = requests.post
elif url.startswith('get'):
r = requests.get
elif url.startswith('put'):
r = requests.put
elif url.startswith('delete'):
r = requests.delete
url = url.replace('post://', 'http://')
url = url.replace('posts://', 'https://')
url = url.replace('put://', 'http://')
url = url.replace('puts://', 'https://')
url = url.replace('get://', 'http://')
url = url.replace('gets://', 'https://')
url = url.replace('put://', 'http://')
url = url.replace('puts://', 'https://')
url = url.replace('delete://', 'http://')
url = url.replace('deletes://', 'https://')
# Try to auto-guess if it's JSON
headers = {}
try:
json.loads(body)
headers = {'Content-Type': 'application/json; charset=utf-8'}
except ValueError as e:
pass
r(url, headers=headers, data=body)
def process_notification(n_object, datastore): def process_notification(n_object, datastore):
# Insert variables into the notification content
notification_parameters = create_notification_parameters(n_object, datastore)
# Get the notification body from datastore # Get the notification body from datastore
jinja2_env = Environment(loader=BaseLoader) n_body = n_object.get('notification_body', default_notification_body)
n_body = jinja2_env.from_string(n_object.get('notification_body', default_notification_body)).render(**notification_parameters) n_title = n_object.get('notification_title', default_notification_title)
n_title = jinja2_env.from_string(n_object.get('notification_title', default_notification_title)).render(**notification_parameters)
n_format = valid_notification_formats.get( n_format = valid_notification_formats.get(
n_object['notification_format'], n_object['notification_format'],
valid_notification_formats[default_notification_format], valid_notification_formats[default_notification_format],
) )
# Insert variables into the notification content
notification_parameters = create_notification_parameters(n_object, datastore)
for n_k in notification_parameters:
token = '{' + n_k + '}'
val = notification_parameters[n_k]
n_title = n_title.replace(token, val)
n_body = n_body.replace(token, val)
# https://github.com/caronc/apprise/wiki/Development_LogCapture # https://github.com/caronc/apprise/wiki/Development_LogCapture
# Anything higher than or equal to WARNING (which covers things like Connection errors) # Anything higher than or equal to WARNING (which covers things like Connection errors)
# raise it as an exception # raise it as an exception
@@ -97,7 +53,6 @@ def process_notification(n_object, datastore):
sent_objs=[] sent_objs=[]
from .apprise_asset import asset from .apprise_asset import asset
for url in n_object['notification_urls']: for url in n_object['notification_urls']:
url = jinja2_env.from_string(url).render(**notification_parameters)
apobj = apprise.Apprise(debug=True, asset=asset) apobj = apprise.Apprise(debug=True, asset=asset)
url = url.strip() url = url.strip()
if len(url): if len(url):
@@ -111,12 +66,7 @@ def process_notification(n_object, datastore):
# So if no avatar_url is specified, add one so it can be correctly calculated into the total payload # So if no avatar_url is specified, add one so it can be correctly calculated into the total payload
k = '?' if not '?' in url else '&' k = '?' if not '?' in url else '&'
if not 'avatar_url' in url \ if not 'avatar_url' in url and not url.startswith('mail'):
and not url.startswith('mail') \
and not url.startswith('post') \
and not url.startswith('get') \
and not url.startswith('delete') \
and not url.startswith('put'):
url += k + 'avatar_url=https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/changedetectionio/static/images/avatar-256x256.png' url += k + 'avatar_url=https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/changedetectionio/static/images/avatar-256x256.png'
if url.startswith('tgram://'): if url.startswith('tgram://'):
@@ -194,7 +144,7 @@ def create_notification_parameters(n_object, datastore):
watch_url = n_object['watch_url'] watch_url = n_object['watch_url']
# Re #148 - Some people have just {{ base_url }} in the body or title, but this may break some notification services # Re #148 - Some people have just {base_url} in the body or title, but this may break some notification services
# like 'Join', so it's always best to atleast set something obvious so that they are not broken. # like 'Join', so it's always best to atleast set something obvious so that they are not broken.
if base_url == '': if base_url == '':
base_url = "<base-url-env-var-not-set>" base_url = "<base-url-env-var-not-set>"

View File

@@ -1,10 +0,0 @@
from dataclasses import dataclass, field
from typing import Any
# So that we can queue some metadata in `item`
# https://docs.python.org/3/library/queue.html#queue.PriorityQueue
#
@dataclass(order=True)
class PrioritizedItem:
priority: int
item: Any=field(compare=False)

View File

@@ -1,16 +1,3 @@
// Copyright (C) 2021 Leigh Morresi (dgtlmoon@gmail.com)
// All rights reserved.
// @file Scrape the page looking for elements of concern (%ELEMENTS%)
// http://matatk.agrip.org.uk/tests/position-and-width/
// https://stackoverflow.com/questions/26813480/when-is-element-getboundingclientrect-guaranteed-to-be-updated-accurate
//
// Some pages like https://www.londonstockexchange.com/stock/NCCL/ncondezi-energy-limited/analysis
// will automatically force a scroll somewhere, so include the position offset
// Lets hope the position doesnt change while we iterate the bbox's, but this is better than nothing
var scroll_y=+document.documentElement.scrollTop || document.body.scrollTop
// Include the getXpath script directly, easier than fetching // Include the getXpath script directly, easier than fetching
function getxpath(e) { function getxpath(e) {
var n = e; var n = e;
@@ -84,21 +71,8 @@ var bbox;
for (var i = 0; i < elements.length; i++) { for (var i = 0; i < elements.length; i++) {
bbox = elements[i].getBoundingClientRect(); bbox = elements[i].getBoundingClientRect();
// Exclude items that are not interactable or visible // forget really small ones
if(elements[i].style.opacity === "0") { if (bbox['width'] < 15 && bbox['height'] < 15) {
continue
}
if(elements[i].style.display === "none" || elements[i].style.pointerEvents === "none" ) {
continue
}
// Skip really small ones, and where width or height ==0
if (bbox['width'] * bbox['height'] < 100) {
continue;
}
// Don't include elements that are offset from canvas
if (bbox['top']+scroll_y < 0 || bbox['left'] < 0) {
continue; continue;
} }
@@ -135,20 +109,19 @@ for (var i = 0; i < elements.length; i++) {
continue; continue;
} }
// @todo Possible to ONLY list where it's clickable to save JSON xfer size
size_pos.push({ size_pos.push({
xpath: xpath_result, xpath: xpath_result,
width: Math.round(bbox['width']), width: Math.round(bbox['width']),
height: Math.round(bbox['height']), height: Math.round(bbox['height']),
left: Math.floor(bbox['left']), left: Math.floor(bbox['left']),
top: Math.floor(bbox['top'])+scroll_y, top: Math.floor(bbox['top']),
tagName: (elements[i].tagName) ? elements[i].tagName.toLowerCase() : '', tagName: (elements[i].tagName) ? elements[i].tagName.toLowerCase() : '',
tagtype: (elements[i].tagName == 'INPUT' && elements[i].type) ? elements[i].type.toLowerCase() : '', tagtype: (elements[i].tagName == 'INPUT' && elements[i].type) ? elements[i].type.toLowerCase() : ''
isClickable: (elements[i].onclick) || window.getComputedStyle(elements[i]).cursor == "pointer"
}); });
} }
// Inject the current one set in the include_filters, which may be a CSS rule // Inject the current one set in the include_filters, which may be a CSS rule
// used for displaying the current one in VisualSelector, where its not one we generated. // used for displaying the current one in VisualSelector, where its not one we generated.
if (include_filters.length) { if (include_filters.length) {
@@ -176,40 +149,22 @@ if (include_filters.length) {
} }
if (q) { if (q) {
// #1231 - IN the case XPath attribute filter is applied, we will have to traverse up and find the element. bbox = q.getBoundingClientRect();
if (q.hasOwnProperty('getBoundingClientRect')) { } else {
bbox = q.getBoundingClientRect(); console.log("xpath_element_scraper: filter element "+f+" was not found");
console.log("xpath_element_scraper: Got filter element, scroll from top was " + scroll_y)
} else {
try {
// Try and see we can find its ownerElement
bbox = q.ownerElement.getBoundingClientRect();
console.log("xpath_element_scraper: Got filter by ownerElement element, scroll from top was " + scroll_y)
} catch (e) {
console.log("xpath_element_scraper: error looking up ownerElement")
}
}
}
if(!q) {
console.log("xpath_element_scraper: filter element " + f + " was not found");
} }
if (bbox && bbox['width'] > 0 && bbox['height'] > 0) { if (bbox && bbox['width'] > 0 && bbox['height'] > 0) {
size_pos.push({ size_pos.push({
xpath: f, xpath: f,
width: parseInt(bbox['width']), width: Math.round(bbox['width']),
height: parseInt(bbox['height']), height: Math.round(bbox['height']),
left: parseInt(bbox['left']), left: Math.floor(bbox['left']),
top: parseInt(bbox['top'])+scroll_y top: Math.floor(bbox['top'])
}); });
} }
} }
} }
// Sort the elements so we find the smallest one first, in other words, we find the smallest one matching in that area
// so that we dont select the wrapping element by mistake and be unable to select what we want
size_pos.sort((a, b) => (a.width*a.height > b.width*b.height) ? 1 : -1)
// Window.width required for proper scaling in the frontend // Window.width required for proper scaling in the frontend
return {'size_pos': size_pos, 'browser_width': window.innerWidth}; return {'size_pos': size_pos, 'browser_width': window.innerWidth};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="favicons/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="256.000000pt" height="256.000000pt" viewBox="0 0 256.000000 256.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,256.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M0 1280 l0 -1280 1280 0 1280 0 0 1280 0 1280 -1280 0 -1280 0 0
-1280z m1555 936 c387 -112 675 -426 741 -810 24 -138 15 -352 -20 -470 -106
-353 -360 -606 -713 -712 -75 -22 -113 -27 -253 -31 -144 -5 -176 -2 -252 16
-316 75 -564 271 -707 557 -67 136 -92 237 -98 401 -7 164 5 253 47 378 106
315 349 556 665 659 114 37 180 45 350 41 125 -2 165 -7 240 -29z"/>
<path d="M1091 2165 c-364 -82 -629 -328 -738 -682 -24 -80 -27 -103 -27 -258
-1 -146 2 -182 21 -251 74 -271 259 -497 508 -621 477 -238 1061 -35 1294 450
61 126 83 220 88 379 7 194 -15 307 -93 461 -126 251 -340 428 -614 507 -99
29 -343 37 -439 15z m829 -473 c55 -54 100 -106 100 -116 0 -21 -184 -213
-212 -222 -24 -7 -48 12 -48 38 0 11 26 47 58 80 l57 60 -151 -3 c-145 -4
-152 -5 -190 -31 -22 -15 -78 -73 -124 -128 l-85 -99 -32 31 -32 31 30 38 c17
22 70 79 117 128 66 67 97 92 127 100 22 6 106 11 188 11 81 0 147 3 147 8 0
4 -25 31 -55 61 -55 55 -65 77 -43 99 25 25 50 10 148 -86z m-1002 -101 c46
-24 141 -121 312 -321 203 -236 290 -330 322 -346 22 -11 60 -14 169 -12 l141
3 -51 58 c-28 32 -51 64 -51 71 0 18 21 36 43 36 24 0 217 -193 217 -217 0
-19 -185 -210 -212 -219 -24 -7 -48 12 -48 38 0 10 23 43 50 72 l50 53 -52 7
c-29 3 -93 6 -142 6 -104 0 -152 12 -200 52 -19 15 -135 144 -258 286 -274
316 -305 347 -354 361 -22 6 -94 11 -161 11 -67 0 -128 3 -137 6 -22 9 -21 61
2 67 9 3 86 5 170 6 133 1 158 -2 190 -18z m227 -468 c23 -34 17 -43 -103
-172 -119 -128 -131 -133 -343 -129 l-154 3 0 35 c0 34 1 35 50 42 28 3 96 7
153 7 64 1 115 6 136 15 20 8 71 56 127 120 52 58 99 106 105 106 7 0 20 -12
29 -27z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,19 +0,0 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@@ -1,4 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="15" height="16.363636" viewBox="0 0 15 16.363636" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <svg
<path d="m 14.318182,11.762045 v 1.1925 H 5.4102273 L 11.849318,7.1140909 C 12.234545,9.1561364 12.54,11.181818 14.318182,11.762045 Z m -6.7984093,4.601591 c 1.0759091,0 2.0256823,-0.955909 2.0256823,-2.045454 H 5.4545455 c 0,1.089545 0.9879545,2.045454 2.0652272,2.045454 z M 15,2.8622727 0.9177273,15.636136 0,14.627045 l 1.8443182,-1.6725 h -1.1625 v -1.1925 C 4.0070455,10.677273 2.1784091,4.5388636 5.3611364,2.6897727 5.8009091,2.4347727 6.0709091,1.9609091 6.0702273,1.4488636 v -0.00205 C 6.0702273,0.64772727 6.7104545,0 7.5,0 8.2895455,0 8.9297727,0.64772727 8.9297727,1.4468182 v 0.00205 C 8.9290909,1.9602319 9.199773,2.4354591 9.638864,2.6897773 10.364318,3.111141 10.827273,3.7568228 11.1525,4.5129591 L 14.085682,1.8531818 Z M 6.8181818,1.3636364 C 6.8181818,1.74 7.1236364,2.0454545 7.5,2.0454545 7.8763636,2.0454545 8.1818182,1.74 8.1818182,1.3636364 8.1818182,0.98795455 7.8763636,0.68181818 7.5,0.68181818 c -0.3763636,0 -0.6818182,0.30613637 -0.6818182,0.68181822 z" id="path2" style="fill:#f8321b;stroke-width:0.681818;fill-opacity:1"/> width="15"
height="16.363636"
viewBox="0 0 15 16.363636"
version="1.1"
id="svg4"
sodipodi:docname="bell-off.svg"
inkscape:version="1.1.1 (1:1.1+202109281949+c3084ef5ed)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview5"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="28.416667"
inkscape:cx="-0.59824046"
inkscape:cy="12"
inkscape:window-width="1554"
inkscape:window-height="896"
inkscape:window-x="2095"
inkscape:window-y="107"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" />
<defs
id="defs8" />
<path
d="m 14.318182,11.762045 v 1.1925 H 5.4102273 L 11.849318,7.1140909 C 12.234545,9.1561364 12.54,11.181818 14.318182,11.762045 Z m -6.7984093,4.601591 c 1.0759091,0 2.0256823,-0.955909 2.0256823,-2.045454 H 5.4545455 c 0,1.089545 0.9879545,2.045454 2.0652272,2.045454 z M 15,2.8622727 0.9177273,15.636136 0,14.627045 l 1.8443182,-1.6725 h -1.1625 v -1.1925 C 4.0070455,10.677273 2.1784091,4.5388636 5.3611364,2.6897727 5.8009091,2.4347727 6.0709091,1.9609091 6.0702273,1.4488636 v -0.00205 C 6.0702273,0.64772727 6.7104545,0 7.5,0 8.2895455,0 8.9297727,0.64772727 8.9297727,1.4468182 v 0.00205 C 8.9290909,1.9602319 9.199773,2.4354591 9.638864,2.6897773 10.364318,3.111141 10.827273,3.7568228 11.1525,4.5129591 L 14.085682,1.8531818 Z M 6.8181818,1.3636364 C 6.8181818,1.74 7.1236364,2.0454545 7.5,2.0454545 7.8763636,2.0454545 8.1818182,1.74 8.1818182,1.3636364 8.1818182,0.98795455 7.8763636,0.68181818 7.5,0.68181818 c -0.3763636,0 -0.6818182,0.30613637 -0.6818182,0.68181822 z"
id="path2"
style="fill:#f8321b;stroke-width:0.681818;fill-opacity:1" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="83.39" height="89.648" enable-background="new 0 0 122.406 122.881" version="1.1" viewBox="0 0 83.39 89.648" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="translate(5e-4 -33.234)"><path d="m44.239 42.946-39.111 39.896 34.908 34.91 39.09-39.876-1.149-34.931zm-0.91791 42.273c0.979-0.979 1.507-1.99 1.577-3.027 0.077-1.043-0.248-2.424-0.967-4.135-0.725-1.717-1.348-3.346-1.87-4.885s-0.814-3.014-0.897-4.432c-0.07-1.42 0.134-2.768 0.624-4.045 0.477-1.279 1.348-2.545 2.607-3.804 2.099-2.099 4.535-3.123 7.314-3.065 2.773 0.063 5.457 1.158 8.04 3.294l2.881 3.034c1.946 2.607 2.799 5.33 2.557 8.166-0.235 2.83-1.532 5.426-3.893 7.785l-6.296-6.297c1.291-1.291 2.035-2.531 2.238-3.727 0.191-1.197-0.165-2.252-1.081-3.168-0.821-0.82-1.717-1.195-2.69-1.139-0.967 0.064-1.908 0.547-2.817 1.457-0.922 0.922-1.393 1.914-1.412 2.977s0.306 2.416 0.973 4.064c0.661 1.652 1.24 3.25 1.736 4.801 0.496 1.553 0.782 3.035 0.858 4.445 0.076 1.426-0.127 2.787-0.591 4.104-0.477 1.316-1.336 2.596-2.588 3.848-2.125 2.125-4.522 3.186-7.212 3.18s-5.311-1.063-7.855-3.16l-3.747 3.746-2.964-2.965 3.766-3.764c-2.423-2.996-3.568-5.998-3.447-9.02 0.127-3.014 1.476-5.813 4.045-8.383l6.278 6.277c-1.412 1.412-2.175 2.799-2.277 4.16-0.108 1.367 0.414 2.627 1.571 3.783 0.839 0.84 1.755 1.26 2.741 1.242 0.985-0.017 1.92-0.47 2.798-1.347zm21.127-46.435h17.457c-0.0269 2.2368 0.69936 16.025 0.69936 16.025l0.785 23.858c0.019 0.609-0.221 1.164-0.619 1.564l5e-3 4e-3 -41.236 42.022c-0.82213 0.8378-2.175 0.83-3.004 0l-37.913-37.91c-0.83-0.83-0.83-2.176 0-3.006l41.236-42.021c0.39287-0.42671 1.502-0.53568 1.502-0.53568zm18.011 11.59c-59.392-29.687-29.696-14.843 0 0z"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,5 +1,46 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="18" height="19.92" viewBox="0 0 18 19.92" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <svg
<path d="M -3,-2 H 21 V 22 H -3 Z" fill="none" id="path2"/> width="18"
<path d="m 15,14.08 c -0.76,0 -1.44,0.3 -1.96,0.77 L 5.91,10.7 C 5.96,10.47 6,10.24 6,10 6,9.76 5.96,9.53 5.91,9.3 L 12.96,5.19 C 13.5,5.69 14.21,6 15,6 16.66,6 18,4.66 18,3 18,1.34 16.66,0 15,0 c -1.66,0 -3,1.34 -3,3 0,0.24 0.04,0.47 0.09,0.7 L 5.04,7.81 C 4.5,7.31 3.79,7 3,7 1.34,7 0,8.34 0,10 c 0,1.66 1.34,3 3,3 0.79,0 1.5,-0.31 2.04,-0.81 l 7.12,4.16 c -0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92 0,-1.61 -1.31,-2.92 -2.92,-2.92 z" id="path4" style="fill:#0078e7;fill-opacity:1"/> height="19.92"
viewBox="0 0 18 19.92"
version="1.1"
id="svg6"
sodipodi:docname="spread.svg"
inkscape:version="1.1.1 (1:1.1+202109281949+c3084ef5ed)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs10" />
<sodipodi:namedview
id="namedview8"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="28.416667"
inkscape:cx="9.0087975"
inkscape:cy="9.9941348"
inkscape:window-width="1920"
inkscape:window-height="1056"
inkscape:window-x="1920"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg6" />
<path
d="M -3,-2 H 21 V 22 H -3 Z"
fill="none"
id="path2" />
<path
d="m 15,14.08 c -0.76,0 -1.44,0.3 -1.96,0.77 L 5.91,10.7 C 5.96,10.47 6,10.24 6,10 6,9.76 5.96,9.53 5.91,9.3 L 12.96,5.19 C 13.5,5.69 14.21,6 15,6 16.66,6 18,4.66 18,3 18,1.34 16.66,0 15,0 c -1.66,0 -3,1.34 -3,3 0,0.24 0.04,0.47 0.09,0.7 L 5.04,7.81 C 4.5,7.31 3.79,7 3,7 1.34,7 0,8.34 0,10 c 0,1.66 1.34,3 3,3 0.79,0 1.5,-0.31 2.04,-0.81 l 7.12,4.16 c -0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92 0,-1.61 -1.31,-2.92 -2.92,-2.92 z"
id="path4"
style="fill:#0078e7;fill-opacity:1" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 787 B

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -10,10 +10,10 @@ $(document).ready(function () {
} }
}) })
var browsersteps_session_id; var browsersteps_session_id;
var browserless_seconds_remaining = 0; var browserless_seconds_remaining=0;
var apply_buttons_disabled = false; var apply_buttons_disabled = false;
var include_text_elements = $("#include_text_elements"); var include_text_elements = $("#include_text_elements");
var xpath_data = false; var xpath_data;
var current_selected_i; var current_selected_i;
var state_clicked = false; var state_clicked = false;
var c; var c;
@@ -25,42 +25,11 @@ $(document).ready(function () {
$(window).resize(function () { $(window).resize(function () {
set_scale(); set_scale();
}); });
// Should always be disabled
$('#browser_steps >li:first-child select').val('Goto site').attr('disabled', 'disabled');
$('#browsersteps-click-start').click(function () {
$("#browsersteps-click-start").fadeOut();
$("#browsersteps-selector-wrapper .spinner").fadeIn();
start();
});
$('a#browsersteps-tab').click(function () { $('a#browsersteps-tab').click(function () {
reset(); start();
}); });
window.addEventListener('hashchange', function () {
if (window.location.hash == '#browser-steps') {
reset();
}
});
function reset() {
xpath_data = false;
$('#browsersteps-img').removeAttr('src');
$("#browsersteps-click-start").show();
$("#browsersteps-selector-wrapper .spinner").hide();
browserless_seconds_remaining = 0;
browsersteps_session_id = false;
apply_buttons_disabled = false;
ctx.clearRect(0, 0, c.width, c.height);
set_first_gotosite_disabled();
}
function set_first_gotosite_disabled() {
$('#browser_steps >li:first-child select').val('Goto site').attr('disabled', 'disabled');
$('#browser_steps >li:first-child').css('opacity', '0.5');
}
// Show seconds remaining until playwright/browserless needs to restart the session // Show seconds remaining until playwright/browserless needs to restart the session
// (See comment at the top of changedetectionio/blueprint/browser_steps/__init__.py ) // (See comment at the top of changedetectionio/blueprint/browser_steps/__init__.py )
setInterval(() => { setInterval(() => {
@@ -71,6 +40,21 @@ $(document).ready(function () {
}, "1000") }, "1000")
if (window.location.hash == '#browser-steps') {
start();
}
window.addEventListener('hashchange', function () {
if (window.location.hash == '#browser-steps') {
start();
}
// For when the page loads
if (!window.location.hash || window.location.hash != '#browser-steps') {
$("img#browsersteps-img").attr('src', '');
return;
}
});
function set_scale() { function set_scale() {
// some things to check if the scaling doesnt work // some things to check if the scaling doesnt work
@@ -103,6 +87,7 @@ $(document).ready(function () {
// @todo is click better? // @todo is click better?
$('#browsersteps-selector-canvas').off("mousemove mousedown click"); $('#browsersteps-selector-canvas').off("mousemove mousedown click");
// Undo disable_browsersteps_ui // Undo disable_browsersteps_ui
$("#browser_steps select,input").removeAttr('disabled').css('opacity', '1.0');
$("#browser-steps-ui").css('opacity', '1.0'); $("#browser-steps-ui").css('opacity', '1.0');
// init // init
@@ -118,7 +103,7 @@ $(document).ready(function () {
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
e.preventDefault() e.preventDefault()
console.log(e); console.log(e);
console.log("current xpath in index is " + current_selected_i); console.log("current xpath in index is "+current_selected_i);
last_click_xy = {'x': parseInt((1 / x_scale) * e.offsetX), 'y': parseInt((1 / y_scale) * e.offsetY)} last_click_xy = {'x': parseInt((1 / x_scale) * e.offsetX), 'y': parseInt((1 / y_scale) * e.offsetY)}
process_selected(current_selected_i); process_selected(current_selected_i);
current_selected_i = false; current_selected_i = false;
@@ -133,10 +118,6 @@ $(document).ready(function () {
}); });
$('#browsersteps-selector-canvas').bind('mousemove', function (e) { $('#browsersteps-selector-canvas').bind('mousemove', function (e) {
if (!xpath_data) {
return;
}
// checkbox if find elements is enabled // checkbox if find elements is enabled
ctx.clearRect(0, 0, c.width, c.height); ctx.clearRect(0, 0, c.width, c.height);
ctx.fillStyle = 'rgba(255,0,0, 0.1)'; ctx.fillStyle = 'rgba(255,0,0, 0.1)';
@@ -172,8 +153,8 @@ $(document).ready(function () {
// does it mean sort the xpath list by size (w*h) i think so! // does it mean sort the xpath list by size (w*h) i think so!
} else { } else {
if (include_text_elements[0].checked === true) { if ( include_text_elements[0].checked === true) {
// blue one with background instead? // blue one with background instead?
ctx.fillStyle = 'rgba(0,0,255, 0.1)'; ctx.fillStyle = 'rgba(0,0,255, 0.1)';
ctx.strokeStyle = 'rgba(0,0,200, 0.7)'; ctx.strokeStyle = 'rgba(0,0,200, 0.7)';
$('#browsersteps-selector-canvas').css('cursor', 'grab'); $('#browsersteps-selector-canvas').css('cursor', 'grab');
@@ -194,6 +175,7 @@ $(document).ready(function () {
// }); // });
// callback for clicking on an xpath on the canvas // callback for clicking on an xpath on the canvas
function process_selected(xpath_data_index) { function process_selected(xpath_data_index) {
found_something = false; found_something = false;
@@ -208,23 +190,25 @@ $(document).ready(function () {
console.log(x); console.log(x);
if (x && first_available.length) { if (x && first_available.length) {
// @todo will it let you click shit that has a layer ontop? probably not. // @todo will it let you click shit that has a layer ontop? probably not.
if (x['tagtype'] === 'text' || x['tagtype'] === 'email' || x['tagName'] === 'textarea' || x['tagtype'] === 'password' || x['tagtype'] === 'search') { if (x['tagtype'] === 'text' || x['tagtype'] === 'email' || x['tagName'] === 'textarea' || x['tagtype'] === 'password' || x['tagtype'] === 'search' ) {
$('select', first_available).val('Enter text in field').change(); $('select', first_available).val('Enter text in field').change();
$('input[type=text]', first_available).first().val(x['xpath']); $('input[type=text]', first_available).first().val(x['xpath']);
$('input[placeholder="Value"]', first_available).addClass('ok').click().focus(); $('input[placeholder="Value"]', first_available).addClass('ok').click().focus();
found_something = true; found_something = true;
} else { } else {
if (x['isClickable'] || x['tagName'].startsWith('h') || x['tagName'] === 'a' || x['tagName'] === 'button' || x['tagtype'] === 'submit' || x['tagtype'] === 'checkbox' || x['tagtype'] === 'radio' || x['tagtype'] === 'li') { // Assume it's just for clicking on
// what are we clicking on?
if (x['tagName'].startsWith('h')|| x['tagName'] === 'a' || x['tagName'] === 'button' || x['tagtype'] === 'submit'|| x['tagtype'] === 'checkbox'|| x['tagtype'] === 'radio'|| x['tagtype'] === 'li') {
$('select', first_available).val('Click element').change(); $('select', first_available).val('Click element').change();
$('input[type=text]', first_available).first().val(x['xpath']); $('input[type=text]', first_available).first().val(x['xpath']);
found_something = true; found_something = true;
} }
} }
first_available.xpath_data_index = xpath_data_index; first_available.xpath_data_index=xpath_data_index;
if (!found_something) { if (!found_something) {
if (include_text_elements[0].checked === true) { if ( include_text_elements[0].checked === true) {
// Suggest that we use as filter? // Suggest that we use as filter?
// @todo filters should always be in the last steps, nothing non-filter after it // @todo filters should always be in the last steps, nothing non-filter after it
found_something = true; found_something = true;
@@ -236,6 +220,8 @@ $(document).ready(function () {
} }
} }
} }
} else {
} }
} }
@@ -248,15 +234,15 @@ $(document).ready(function () {
function start() { function start() {
console.log("Starting browser-steps UI"); console.log("Starting browser-steps UI");
browsersteps_session_id = Date.now(); browsersteps_session_id=Date.now();
// @todo This setting of the first one should be done at the datalayer but wtforms doesnt wanna play nice // @todo This setting of the first one should be done at the datalayer but wtforms doesnt wanna play nice
$('#browser_steps >li:first-child').removeClass('empty'); $('#browser_steps >li:first-child').removeClass('empty');
set_first_gotosite_disabled(); $('#browser_steps >li:first-child select').val('Goto site').attr('disabled', 'disabled');
$('#browser-steps-ui .loader .spinner').show(); $('#browser-steps-ui .loader').show();
$('.clear,.remove', $('#browser_steps >li:first-child')).hide(); $('.clear,.remove', $('#browser_steps >li:first-child')).hide();
$.ajax({ $.ajax({
type: "GET", type: "GET",
url: browser_steps_sync_url + "&browsersteps_session_id=" + browsersteps_session_id, url: browser_steps_sync_url+"&browsersteps_session_id="+browsersteps_session_id,
statusCode: { statusCode: {
400: function () { 400: function () {
// More than likely the CSRF token was lost when the server restarted // More than likely the CSRF token was lost when the server restarted
@@ -265,12 +251,11 @@ $(document).ready(function () {
} }
}).done(function (data) { }).done(function (data) {
xpath_data = data.xpath_data; xpath_data = data.xpath_data;
$('#browsersteps-img').attr('src', data.screenshot);
$("#loading-status-text").fadeIn(); $("#loading-status-text").fadeIn();
// This should trigger 'Goto site' // This should trigger 'Goto site'
console.log("Got startup response, requesting Goto-Site (first) step fake click");
$('#browser_steps >li:first-child .apply').click(); $('#browser_steps >li:first-child .apply').click();
browserless_seconds_remaining = data.browser_time_remaining; browserless_seconds_remaining = data.browser_time_remaining;
set_first_gotosite_disabled();
}).fail(function (data) { }).fail(function (data) {
console.log(data); console.log(data);
alert('There was an error communicating with the server.'); alert('There was an error communicating with the server.');
@@ -279,7 +264,7 @@ $(document).ready(function () {
} }
function disable_browsersteps_ui() { function disable_browsersteps_ui() {
set_first_gotosite_disabled(); $("#browser_steps select,input").attr('disabled', 'disabled').css('opacity', '0.5');
$("#browser-steps-ui").css('opacity', '0.3'); $("#browser-steps-ui").css('opacity', '0.3');
$('#browsersteps-selector-canvas').off("mousemove mousedown click"); $('#browsersteps-selector-canvas').off("mousemove mousedown click");
} }
@@ -326,14 +311,11 @@ $(document).ready(function () {
// Add the extra buttons to the steps // Add the extra buttons to the steps
$('ul#browser_steps li').each(function (i) { $('ul#browser_steps li').each(function (i) {
var s = '<div class="control">' + '<a data-step-index=' + i + ' class="pure-button button-secondary button-green button-xsmall apply" >Apply</a>&nbsp;'; $(this).append('<div class="control">' +
if (i > 0) { '<a data-step-index=' + i + ' class="pure-button button-secondary button-green button-xsmall apply" >Apply</a>&nbsp;' +
// The first step never gets these (Goto-site) '<a data-step-index=' + i + ' class="pure-button button-secondary button-xsmall clear" >Clear</a>&nbsp;' +
s += '<a data-step-index=' + i + ' class="pure-button button-secondary button-xsmall clear" >Clear</a>&nbsp;' + '<a data-step-index=' + i + ' class="pure-button button-secondary button-red button-xsmall remove" >Remove</a>' +
'<a data-step-index=' + i + ' class="pure-button button-secondary button-red button-xsmall remove" >Remove</a>'; '</div>')
}
s += '</div>';
$(this).append(s)
} }
); );
@@ -370,15 +352,15 @@ $(document).ready(function () {
$('ul#browser_steps li .control .apply').click(function (event) { $('ul#browser_steps li .control .apply').click(function (event) {
// sequential requests @todo refactor // sequential requests @todo refactor
if (apply_buttons_disabled) { if(apply_buttons_disabled) {
return; return;
} }
var current_data = $(event.currentTarget).closest('li'); var current_data = $(event.currentTarget).closest('li');
$('#browser-steps-ui .loader .spinner').fadeIn(); $('#browser-steps-ui .loader').fadeIn();
apply_buttons_disabled = true; apply_buttons_disabled=true;
$('ul#browser_steps li .control .apply').css('opacity', 0.5); $('ul#browser_steps li .control .apply').css('opacity',0.5);
$("#browsersteps-img").css('opacity', 0.65); $("#browsersteps-img").css('opacity',0.65);
var is_last_step = 0; var is_last_step = 0;
var step_n = $(event.currentTarget).data('step-index'); var step_n = $(event.currentTarget).data('step-index');
@@ -390,17 +372,17 @@ $(document).ready(function () {
} }
}); });
if (is_last_step == (step_n + 1)) { if (is_last_step == (step_n+1)) {
is_last_step = true; is_last_step = true;
} else { } else {
is_last_step = false; is_last_step = false;
} }
console.log("Requesting step via POST " + $("select[id$='operation']", current_data).first().val());
// POST the currently clicked step form widget back and await response, redraw // POST the currently clicked step form widget back and await response, redraw
$.ajax({ $.ajax({
method: "POST", method: "POST",
url: browser_steps_sync_url + "&browsersteps_session_id=" + browsersteps_session_id, url: browser_steps_sync_url+"&browsersteps_session_id="+browsersteps_session_id,
data: { data: {
'operation': $("select[id$='operation']", current_data).first().val(), 'operation': $("select[id$='operation']", current_data).first().val(),
'selector': $("input[id$='selector']", current_data).first().val(), 'selector': $("input[id$='selector']", current_data).first().val(),
@@ -413,35 +395,28 @@ $(document).ready(function () {
// More than likely the CSRF token was lost when the server restarted // More than likely the CSRF token was lost when the server restarted
alert("There was a problem processing the request, please reload the page."); alert("There was a problem processing the request, please reload the page.");
$("#loading-status-text").hide(); $("#loading-status-text").hide();
$('#browser-steps-ui .loader .spinner').fadeOut();
},
401: function (data) {
// More than likely the CSRF token was lost when the server restarted
alert(data.responseText);
$("#loading-status-text").hide();
$('#browser-steps-ui .loader .spinner').fadeOut();
} }
} }
}).done(function (data) { }).done(function (data) {
// it should return the new state (selectors available and screenshot) // it should return the new state (selectors available and screenshot)
xpath_data = data.xpath_data; xpath_data = data.xpath_data;
$('#browsersteps-img').attr('src', data.screenshot); $('#browsersteps-img').attr('src', data.screenshot);
$('#browser-steps-ui .loader .spinner').fadeOut(); $('#browser-steps-ui .loader').fadeOut();
apply_buttons_disabled = false; apply_buttons_disabled=false;
$("#browsersteps-img").css('opacity', 1); $("#browsersteps-img").css('opacity',1);
$('ul#browser_steps li .control .apply').css('opacity', 1); $('ul#browser_steps li .control .apply').css('opacity',1);
browserless_seconds_remaining = data.browser_time_remaining; browserless_seconds_remaining = data.browser_time_remaining;
$("#loading-status-text").hide(); $("#loading-status-text").hide();
set_first_gotosite_disabled();
}).fail(function (data) { }).fail(function (data) {
console.log(data); console.log(data);
if (data.responseText.includes("Browser session expired")) { if (data.responseText.includes("Browser session expired")) {
disable_browsersteps_ui(); disable_browsersteps_ui();
} }
apply_buttons_disabled = false; apply_buttons_disabled=false;
$("#loading-status-text").hide(); $("#loading-status-text").hide();
$('ul#browser_steps li .control .apply').css('opacity', 1); $('ul#browser_steps li .control .apply').css('opacity',1);
$("#browsersteps-img").css('opacity', 1); $("#browsersteps-img").css('opacity',1);
//$('#browsersteps-selector-wrapper .loader').fadeOut(2500);
}); });
}); });

View File

@@ -13,8 +13,6 @@ $(document).ready(function () {
} else if (hash_name === '#error-screenshot') { } else if (hash_name === '#error-screenshot') {
$("img#error-screenshot-img").attr('src', error_screenshot_url); $("img#error-screenshot-img").attr('src', error_screenshot_url);
$("#settings").hide(); $("#settings").hide();
} else if (hash_name === '#extract') {
$("#settings").hide();
} }

View File

@@ -1,110 +1,112 @@
var a = document.getElementById("a"); var a = document.getElementById('a');
var b = document.getElementById("b"); var b = document.getElementById('b');
var result = document.getElementById("result"); var result = document.getElementById('result');
function changed() { function changed() {
// https://github.com/kpdecker/jsdiff/issues/389 // https://github.com/kpdecker/jsdiff/issues/389
// I would love to use `{ignoreWhitespace: true}` here but it breaks the formatting // I would love to use `{ignoreWhitespace: true}` here but it breaks the formatting
options = { options = {ignoreWhitespace: document.getElementById('ignoreWhitespace').checked};
ignoreWhitespace: document.getElementById("ignoreWhitespace").checked,
};
var diff = Diff[window.diffType](a.textContent, b.textContent, options); var diff = Diff[window.diffType](a.textContent, b.textContent, options);
var fragment = document.createDocumentFragment(); var fragment = document.createDocumentFragment();
for (var i = 0; i < diff.length; i++) { for (var i = 0; i < diff.length; i++) {
if (diff[i].added && diff[i + 1] && diff[i + 1].removed) {
var swap = diff[i]; if (diff[i].added && diff[i + 1] && diff[i + 1].removed) {
diff[i] = diff[i + 1]; var swap = diff[i];
diff[i + 1] = swap; diff[i] = diff[i + 1];
diff[i + 1] = swap;
}
var node;
if (diff[i].removed) {
node = document.createElement('del');
node.classList.add("change");
node.appendChild(document.createTextNode(diff[i].value));
} else if (diff[i].added) {
node = document.createElement('ins');
node.classList.add("change");
node.appendChild(document.createTextNode(diff[i].value));
} else {
node = document.createTextNode(diff[i].value);
}
fragment.appendChild(node);
} }
var node; result.textContent = '';
if (diff[i].removed) { result.appendChild(fragment);
node = document.createElement("del");
node.classList.add("change");
const wrapper = node.appendChild(document.createElement("span"));
wrapper.appendChild(document.createTextNode(diff[i].value));
} else if (diff[i].added) {
node = document.createElement("ins");
node.classList.add("change");
const wrapper = node.appendChild(document.createElement("span"));
wrapper.appendChild(document.createTextNode(diff[i].value));
} else {
node = document.createTextNode(diff[i].value);
}
fragment.appendChild(node);
}
result.textContent = ""; // Jump at start
result.appendChild(fragment); inputs.current = 0;
next_diff();
// Jump at start
inputs.current = 0;
next_diff();
} }
window.onload = function () { window.onload = function () {
/* Convert what is options from UTC time.time() to local browser time */
var diffList = document.getElementById("diff-version");
if (typeof diffList != "undefined" && diffList != null) {
for (var option of diffList.options) {
var dateObject = new Date(option.value * 1000);
option.label = dateObject.toLocaleString();
}
}
/* Set current version date as local time in the browser also */
var current_v = document.getElementById("current-v-date"); /* Convert what is options from UTC time.time() to local browser time */
var dateObject = new Date(newest_version_timestamp * 1000); var diffList = document.getElementById("diff-version");
current_v.innerHTML = dateObject.toLocaleString(); if (typeof (diffList) != 'undefined' && diffList != null) {
onDiffTypeChange( for (var option of diffList.options) {
document.querySelector('#settings [name="diff_type"]:checked'), var dateObject = new Date(option.value * 1000);
); option.label = dateObject.toLocaleString();
changed(); }
}
/* Set current version date as local time in the browser also */
var current_v = document.getElementById("current-v-date");
var dateObject = new Date(newest_version_timestamp*1000);
current_v.innerHTML = dateObject.toLocaleString();
onDiffTypeChange(document.querySelector('#settings [name="diff_type"]:checked'));
changed();
}; };
a.onpaste = a.onchange = b.onpaste = b.onchange = changed; a.onpaste = a.onchange =
b.onpaste = b.onchange = changed;
if ("oninput" in a) { if ('oninput' in a) {
a.oninput = b.oninput = changed; a.oninput = b.oninput = changed;
} else { } else {
a.onkeyup = b.onkeyup = changed; a.onkeyup = b.onkeyup = changed;
} }
function onDiffTypeChange(radio) { function onDiffTypeChange(radio) {
window.diffType = radio.value; window.diffType = radio.value;
// Not necessary // Not necessary
// document.title = "Diff " + radio.value.slice(4); // document.title = "Diff " + radio.value.slice(4);
} }
var radio = document.getElementsByName("diff_type"); var radio = document.getElementsByName('diff_type');
for (var i = 0; i < radio.length; i++) { for (var i = 0; i < radio.length; i++) {
radio[i].onchange = function (e) { radio[i].onchange = function (e) {
onDiffTypeChange(e.target); onDiffTypeChange(e.target);
changed(); changed();
}; }
} }
document.getElementById("ignoreWhitespace").onchange = function (e) { document.getElementById('ignoreWhitespace').onchange = function (e) {
changed(); changed();
}; }
var inputs = document.getElementsByClassName("change");
var inputs = document.getElementsByClassName('change');
inputs.current = 0; inputs.current = 0;
function next_diff() { function next_diff() {
var element = inputs[inputs.current];
var headerOffset = 80;
var elementPosition = element.getBoundingClientRect().top;
var offsetPosition = elementPosition - headerOffset + window.scrollY;
window.scrollTo({ var element = inputs[inputs.current];
top: offsetPosition, var headerOffset = 80;
behavior: "smooth", var elementPosition = element.getBoundingClientRect().top;
}); var offsetPosition = elementPosition - headerOffset + window.scrollY;
inputs.current++; window.scrollTo({
if (inputs.current >= inputs.length) { top: offsetPosition,
inputs.current = 0; behavior: "smooth"
} });
inputs.current++;
if (inputs.current >= inputs.length) {
inputs.current = 0;
}
} }

View File

@@ -1,24 +0,0 @@
/**
* @file
* Toggles theme between light and dark mode.
*/
$(document).ready(function () {
const button = document.getElementsByClassName("toggle-theme")[0];
button.onclick = () => {
const htmlElement = document.getElementsByTagName("html");
const isDarkMode = htmlElement[0].dataset.darkmode === "true";
htmlElement[0].dataset.darkmode = !isDarkMode;
if (isDarkMode) {
button.classList.remove("dark");
setCookieValue(false);
} else {
button.classList.add("dark");
setCookieValue(true);
}
};
const setCookieValue = (value) => {
document.cookie = `css_dark_mode=${value};max-age=31536000;path=/`
}
});

View File

@@ -1,5 +1,4 @@
// Copyright (C) 2021 Leigh Morresi (dgtlmoon@gmail.com) // Horrible proof of concept code :)
// All rights reserved.
// yes - this is really a hack, if you are a front-ender and want to help, please get in touch! // yes - this is really a hack, if you are a front-ender and want to help, please get in touch!
$(document).ready(function () { $(document).ready(function () {
@@ -178,10 +177,9 @@ $(document).ready(function () {
// Basically, find the most 'deepest' // Basically, find the most 'deepest'
var found = 0; var found = 0;
ctx.fillStyle = 'rgba(205,0,0,0.35)'; ctx.fillStyle = 'rgba(205,0,0,0.35)';
// Will be sorted by smallest width*height first for (var i = selector_data['size_pos'].length; i !== 0; i--) {
for (var i = 0; i <= selector_data['size_pos'].length; i++) {
// draw all of them? let them choose somehow? // draw all of them? let them choose somehow?
var sel = selector_data['size_pos'][i]; var sel = selector_data['size_pos'][i - 1];
// If we are in a bounding-box // If we are in a bounding-box
if (e.offsetY > sel.top * y_scale && e.offsetY < sel.top * y_scale + sel.height * y_scale if (e.offsetY > sel.top * y_scale && e.offsetY < sel.top * y_scale + sel.height * y_scale
&& &&
@@ -197,7 +195,7 @@ $(document).ready(function () {
// no need to keep digging // no need to keep digging
// @todo or, O to go out/up, I to go in // @todo or, O to go out/up, I to go in
// or double click to go up/out the selector? // or double click to go up/out the selector?
current_selected_i = i; current_selected_i = i - 1;
found += 1; found += 1;
break; break;
} }

View File

@@ -1,148 +1,10 @@
/**
* CSS custom properties (aka variables).
*/
:root {
--color-white: #fff;
--color-grey-50: #111;
--color-grey-100: #262626;
--color-grey-200: #333;
--color-grey-300: #444;
--color-grey-325: #555;
--color-grey-350: #565d64;
--color-grey-400: #666;
--color-grey-500: #777;
--color-grey-600: #999;
--color-grey-700: #cbcbcb;
--color-grey-750: #ddd;
--color-grey-800: #e0e0e0;
--color-grey-850: #eee;
--color-grey-900: #f2f2f2;
--color-black: #000;
--color-dark-red: #a00;
--color-light-red: #dd0000;
--color-background-page: var(--color-grey-100);
--color-background-gradient-first: #5ad8f7;
--color-background-gradient-second: #2f50af;
--color-background-gradient-third: #9150bf;
--color-background: var(--color-white);
--color-text: var(--color-grey-200);
--color-link: #1b98f8;
--color-menu-accent: #ed5900;
--color-background-code: var(--color-grey-850);
--color-error: var(--color-dark-red);
--color-error-input: #ffebeb;
--color-error-list: var(--color-light-red);
--color-table-background: var(--color-background);
--color-table-stripe: var(--color-grey-900);
--color-text-tab: var(--color-white);
--color-background-tab: rgba(255, 255, 255, 0.2);
--color-background-tab-hover: rgba(255, 255, 255, 0.5);
--color-text-tab-active: #222;
--color-api-key: #0078e7;
--color-background-button-primary: #0078e7;
--color-background-button-green: #42dd53;
--color-background-button-red: #dd4242;
--color-background-button-success: rgb(28, 184, 65);
--color-background-button-error: rgb(202, 60, 60);
--color-text-button-error: var(--color-white);
--color-background-button-warning: rgb(202, 60, 60);
--color-text-button-warning: var(--color-white);
--color-background-button-secondary: rgb(66, 184, 221);
--color-background-button-cancel: rgb(200, 200, 200);
--color-text-button: var(--color-white);
--color-background-button-tag: rgb(99, 99, 99);
--color-background-snapshot-age: #dfdfdf;
--color-error-text-snapshot-age: var(--color-white);
--color-error-background-snapshot-age: #ff0000;
--color-background-button-tag-active: #9c9c9c;
--color-text-messages: var(--color-white);
--color-background-messages-message: rgba(255, 255, 255, .2);
--color-background-messages-error: rgba(255, 1, 1, .5);
--color-background-messages-notice: rgba(255, 255, 255, .5);
--color-border-notification: #ccc;
--color-background-checkbox-operations: rgba(0, 0, 0, 0.05);
--color-warning: #ff3300;
--color-border-warning: var(--color-warning);
--color-text-legend: var(--color-white);
--color-link-new-version: #e07171;
--color-last-checked: #bbb;
--color-text-footer: #444;
--color-border-watch-table-cell: #eee;
--color-text-watch-tag-list: #e70069;
--color-background-new-watch-form: rgba(0, 0, 0, 0.05);
--color-background-new-watch-input: var(--color-white);
--color-text-new-watch-input: var(--color-text);
--color-border-input: var(--color-grey-500);
--color-shadow-input: var(--color-grey-400);
--color-background-input: var(--color-white);
--color-text-input: var(--color-text);
--color-text-input-description: var(--color-grey-500);
--color-text-input-placeholder: var(--color-grey-600);
--color-background-table-thead: var(--color-grey-800);
--color-border-table-cell: var(--color-grey-700);
--color-text-menu-heading: var(--color-grey-350);
--color-text-menu-link: var(--color-grey-500);
--color-background-menu-link-hover: var(--color-grey-850);
--color-text-menu-link-hover: var(--color-grey-300);
--color-shadow-jump: var(--color-grey-500);
--color-icon-github: var(--color-black);
--color-icon-github-hover: var(--color-grey-300);
--color-watch-table-error: var(--color-dark-red);
--color-watch-table-row-text: var(--color-grey-100); }
html[data-darkmode="true"] {
--color-link: #59bdfb;
--color-text: var(--color-white);
--color-background-gradient-first: #3f90a5;
--color-background-gradient-second: #1e316c;
--color-background-gradient-third: #4d2c64;
--color-background-new-watch-input: var(--color-grey-100);
--color-text-new-watch-input: var(--color-text);
--color-background-table-thead: var(--color-grey-200);
--color-table-background: var(--color-grey-300);
--color-table-stripe: var(--color-grey-325);
--color-background: var(--color-grey-300);
--color-text-menu-heading: var(--color-grey-850);
--color-text-menu-link: var(--color-grey-800);
--color-border-table-cell: var(--color-grey-400);
--color-text-tab-active: var(--color-text);
--color-border-input: var(--color-grey-400);
--color-shadow-input: var(--color-grey-50);
--color-background-input: var(--color-grey-350);
--color-text-input-description: var(--color-grey-600);
--color-text-input-placeholder: var(--color-grey-600);
--color-text-watch-tag-list: #fa3e92;
--color-background-code: var(--color-grey-200);
--color-background-tab: rgba(0, 0, 0, 0.2);
--color-background-tab-hover: rgba(0, 0, 0, 0.5);
--color-background-snapshot-age: var(--color-grey-200);
--color-shadow-jump: var(--color-grey-200);
--color-icon-github: var(--color-white);
--color-icon-github-hover: var(--color-grey-700);
--color-watch-table-error: var(--color-light-red);
--color-watch-table-row-text: var(--color-grey-800); }
html[data-darkmode="true"] .icon-spread {
filter: hue-rotate(-10deg) brightness(1.5); }
html[data-darkmode="true"] .watch-table .title-col a[target="_blank"]::after,
html[data-darkmode="true"] .watch-table .current-diff-url::after {
filter: invert(0.5) hue-rotate(10deg) brightness(2); }
html[data-darkmode="true"] .watch-table .watch-controls .state-off img {
opacity: 0.3; }
html[data-darkmode="true"] .watch-table .watch-controls .state-on img {
opacity: 1.0; }
html[data-darkmode="true"] .watch-table .unviewed {
color: #fff; }
html[data-darkmode="true"] .watch-table .unviewed.error {
color: var(--color-watch-table-error); }
#diff-ui { #diff-ui {
background: var(--color-background); background: #fff;
padding: 2em; padding: 2em;
margin-left: 1em; margin-left: 1em;
margin-right: 1em; margin-right: 1em;
border-radius: 5px; } border-radius: 5px;
#diff-ui #text { font-size: 11px; }
font-size: 11px; }
#diff-ui table { #diff-ui table {
table-layout: fixed; table-layout: fixed;
width: 100%; } width: 100%; }
@@ -183,10 +45,6 @@ ins {
margin-left: 1em; margin-left: 1em;
display: inline-block; display: inline-block;
font-weight: normal; } font-weight: normal; }
#settings del {
padding: 0.5em; }
#settings ins {
padding: 0.5em; }
.source { .source {
position: absolute; position: absolute;

View File

@@ -0,0 +1,96 @@
#diff-ui {
background: #fff;
padding: 2em;
margin-left: 1em;
margin-right: 1em;
border-radius: 5px;
font-size: 11px;
table {
table-layout: fixed;
width: 100%;
}
td {
padding: 3px 4px;
border: 1px solid transparent;
vertical-align: top;
font: 1em monospace;
text-align: left;
}
pre {
white-space: pre-wrap;
}
}
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;
}
#settings {
background: rgba(0,0,0,.05);
padding: 1em;
border-radius: 10px;
margin-bottom: 1em;
color: #fff;
font-size: 80%;
label {
margin-left: 1em;
display: inline-block;
font-weight: normal;
}
}
.source {
position: absolute;
right: 1%;
top: .2em;
}
@-moz-document url-prefix() {
body {
height: 99%; /* Hide scroll bar in Firefox */
}
}
td#diff-col div {
text-align: justify;
white-space: pre-wrap;
}
.ignored {
background-color: #ccc;
/* border: #0d91fa 1px solid; */
opacity: 0.7;
}
.triggered {
background-color: #1b98f8;
}
/* ignored and triggered? make it obvious error */
.ignored.triggered {
background-color: #ff0000;
}
.tab-pane-inner#screenshot {
text-align: center;
img {
max-width: 99%;
}
}

View File

@@ -4,8 +4,7 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"watch": "node-sass -w scss -o .", "build": "node-sass styles.scss -o .;node-sass diff.scss -o ."
"build": "node-sass scss -o ."
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",

View File

@@ -6,11 +6,6 @@
} }
li { li {
&:not(:first-child) {
&:hover {
opacity: 1.0;
}
}
list-style: decimal; list-style: decimal;
padding: 5px; padding: 5px;
.control { .control {
@@ -75,8 +70,6 @@
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
margin-left: -40px; margin-left: -40px;
z-index: 100; z-index: 100;
max-width: 350px;
text-align: center;
} }
/* nice tall skinny one */ /* nice tall skinny one */
@@ -85,11 +78,4 @@
height: 80px; height: 80px;
font-size: 3px; font-size: 3px;
} }
#browsersteps-click-start {
&:hover {
cursor: pointer;
}
color: var(--color-grey-400);
}
} }

View File

@@ -1,121 +0,0 @@
@import "parts/_variables.scss";
#diff-ui {
background: var(--color-background);
padding: 2em;
margin-left: 1em;
margin-right: 1em;
border-radius: 5px;
// The first tab 'text' diff
#text {
font-size: 11px;
}
table {
table-layout: fixed;
width: 100%;
}
td {
padding: 3px 4px;
border: 1px solid transparent;
vertical-align: top;
font: 1em monospace;
text-align: left;
}
pre {
white-space: pre-wrap;
}
}
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;
.change {
span {}
}
}
#settings {
background: rgba(0, 0, 0, .05);
padding: 1em;
border-radius: 10px;
margin-bottom: 1em;
color: #fff;
font-size: 80%;
label {
margin-left: 1em;
display: inline-block;
font-weight: normal;
}
del {
padding: 0.5em;
}
ins {
padding: 0.5em;
}
}
.source {
position: absolute;
right: 1%;
top: .2em;
}
@-moz-document url-prefix() {
body {
height: 99%;
/* Hide scroll bar in Firefox */
}
}
td#diff-col div {
text-align: justify;
white-space: pre-wrap;
}
.ignored {
background-color: #ccc;
/* border: #0d91fa 1px solid; */
opacity: 0.7;
}
.triggered {
background-color: #1b98f8;
}
/* ignored and triggered? make it obvious error */
.ignored.triggered {
background-color: #ff0000;
}
.tab-pane-inner#screenshot {
text-align: center;
img {
max-width: 99%;
}
}

View File

@@ -1,175 +0,0 @@
/**
* CSS custom properties (aka variables).
*/
:root {
--color-white: #fff;
--color-grey-50: #111;
--color-grey-100: #262626;
--color-grey-200: #333;
--color-grey-300: #444;
--color-grey-325: #555;
--color-grey-350: #565d64;
--color-grey-400: #666;
--color-grey-500: #777;
--color-grey-600: #999;
--color-grey-700: #cbcbcb;
--color-grey-750: #ddd;
--color-grey-800: #e0e0e0;
--color-grey-850: #eee;
--color-grey-900: #f2f2f2;
--color-black: #000;
--color-dark-red: #a00;
--color-light-red: #dd0000;
--color-background-page: var(--color-grey-100);
--color-background-gradient-first: #5ad8f7;
--color-background-gradient-second: #2f50af;
--color-background-gradient-third: #9150bf;
--color-background: var(--color-white);
--color-text: var(--color-grey-200);
--color-link: #1b98f8;
--color-menu-accent: #ed5900;
--color-background-code: var(--color-grey-850);
--color-error: var(--color-dark-red);
--color-error-input: #ffebeb;
--color-error-list: var(--color-light-red);
--color-table-background: var(--color-background);
--color-table-stripe: var(--color-grey-900);
--color-text-tab: var(--color-white);
--color-background-tab: rgba(255, 255, 255, 0.2);
--color-background-tab-hover: rgba(255, 255, 255, 0.5);
--color-text-tab-active: #222;
--color-api-key: #0078e7;
--color-background-button-primary: #0078e7;
--color-background-button-green: #42dd53;
--color-background-button-red: #dd4242;
--color-background-button-success: rgb(28, 184, 65);
--color-background-button-error: rgb(202, 60, 60);
--color-text-button-error: var(--color-white);
--color-background-button-warning: rgb(202, 60, 60);
--color-text-button-warning: var(--color-white);
--color-background-button-secondary: rgb(66, 184, 221);
--color-background-button-cancel: rgb(200, 200, 200);
--color-text-button: var(--color-white);
--color-background-button-tag: rgb(99, 99, 99);
--color-background-snapshot-age: #dfdfdf;
--color-error-text-snapshot-age: var(--color-white);
--color-error-background-snapshot-age: #ff0000;
--color-background-button-tag-active: #9c9c9c;
--color-text-messages: var(--color-white);
--color-background-messages-message: rgba(255, 255, 255, .2);
--color-background-messages-error: rgba(255, 1, 1, .5);
--color-background-messages-notice: rgba(255, 255, 255, .5);
--color-border-notification: #ccc;
--color-background-checkbox-operations: rgba(0, 0, 0, 0.05);
--color-warning: #ff3300;
--color-border-warning: var(--color-warning);
--color-text-legend: var(--color-white);
--color-link-new-version: #e07171;
--color-last-checked: #bbb;
--color-text-footer: #444;
--color-border-watch-table-cell: #eee;
--color-text-watch-tag-list: #e70069;
--color-background-new-watch-form: rgba(0, 0, 0, 0.05);
--color-background-new-watch-input: var(--color-white);
--color-text-new-watch-input: var(--color-text);
--color-border-input: var(--color-grey-500);
--color-shadow-input: var(--color-grey-400);
--color-background-input: var(--color-white);
--color-text-input: var(--color-text);
--color-text-input-description: var(--color-grey-500);
--color-text-input-placeholder: var(--color-grey-600);
--color-background-table-thead: var(--color-grey-800);
--color-border-table-cell: var(--color-grey-700);
--color-text-menu-heading: var(--color-grey-350);
--color-text-menu-link: var(--color-grey-500);
--color-background-menu-link-hover: var(--color-grey-850);
--color-text-menu-link-hover: var(--color-grey-300);
--color-shadow-jump: var(--color-grey-500);
--color-icon-github: var(--color-black);
--color-icon-github-hover: var(--color-grey-300);
--color-watch-table-error: var(--color-dark-red);
--color-watch-table-row-text: var(--color-grey-100);
}
html[data-darkmode="true"] {
--color-link: #59bdfb;
--color-text: var(--color-white);
--color-background-gradient-first: #3f90a5;
--color-background-gradient-second: #1e316c;
--color-background-gradient-third: #4d2c64;
--color-background-new-watch-input: var(--color-grey-100);
--color-text-new-watch-input: var(--color-text);
--color-background-table-thead: var(--color-grey-200);
--color-table-background: var(--color-grey-300);
--color-table-stripe: var(--color-grey-325);
--color-background: var(--color-grey-300);
--color-text-menu-heading: var(--color-grey-850);
--color-text-menu-link: var(--color-grey-800);
--color-border-table-cell: var(--color-grey-400);
--color-text-tab-active: var(--color-text);
--color-border-input: var(--color-grey-400);
--color-shadow-input: var(--color-grey-50);
--color-background-input: var(--color-grey-350);
--color-text-input-description: var(--color-grey-600);
--color-text-input-placeholder: var(--color-grey-600);
--color-text-watch-tag-list: #fa3e92;
--color-background-code: var(--color-grey-200);
--color-background-tab: rgba(0, 0, 0, 0.2);
--color-background-tab-hover: rgba(0, 0, 0, 0.5);
--color-background-snapshot-age: var(--color-grey-200);
--color-shadow-jump: var(--color-grey-200);
--color-icon-github: var(--color-white);
--color-icon-github-hover: var(--color-grey-700);
--color-watch-table-error: var(--color-light-red);
--color-watch-table-row-text: var(--color-grey-800);
.icon-spread {
filter: hue-rotate(-10deg) brightness(1.5);
}
.watch-table {
.title-col a[target="_blank"]::after,
.current-diff-url::after {
filter: invert(.5) hue-rotate(10deg) brightness(2);
}
.watch-controls {
.state-off {
img {
opacity: 0.3;
}
}
.state-on {
img {
opacity: 1.0;
}
}
}
.unviewed {
color: #fff;
&.error {
color: var(--color-watch-table-error);
}
}
}
}

View File

@@ -1,143 +1,9 @@
/* /*
* -- BASE STYLES -- * -- BASE STYLES --
* Most of these are inherited from Base, but I want to change a few.
nvm use v14.18.1 && npm install && npm run build
* or npm run watch
*/ */
/**
* CSS custom properties (aka variables).
*/
:root {
--color-white: #fff;
--color-grey-50: #111;
--color-grey-100: #262626;
--color-grey-200: #333;
--color-grey-300: #444;
--color-grey-325: #555;
--color-grey-350: #565d64;
--color-grey-400: #666;
--color-grey-500: #777;
--color-grey-600: #999;
--color-grey-700: #cbcbcb;
--color-grey-750: #ddd;
--color-grey-800: #e0e0e0;
--color-grey-850: #eee;
--color-grey-900: #f2f2f2;
--color-black: #000;
--color-dark-red: #a00;
--color-light-red: #dd0000;
--color-background-page: var(--color-grey-100);
--color-background-gradient-first: #5ad8f7;
--color-background-gradient-second: #2f50af;
--color-background-gradient-third: #9150bf;
--color-background: var(--color-white);
--color-text: var(--color-grey-200);
--color-link: #1b98f8;
--color-menu-accent: #ed5900;
--color-background-code: var(--color-grey-850);
--color-error: var(--color-dark-red);
--color-error-input: #ffebeb;
--color-error-list: var(--color-light-red);
--color-table-background: var(--color-background);
--color-table-stripe: var(--color-grey-900);
--color-text-tab: var(--color-white);
--color-background-tab: rgba(255, 255, 255, 0.2);
--color-background-tab-hover: rgba(255, 255, 255, 0.5);
--color-text-tab-active: #222;
--color-api-key: #0078e7;
--color-background-button-primary: #0078e7;
--color-background-button-green: #42dd53;
--color-background-button-red: #dd4242;
--color-background-button-success: rgb(28, 184, 65);
--color-background-button-error: rgb(202, 60, 60);
--color-text-button-error: var(--color-white);
--color-background-button-warning: rgb(202, 60, 60);
--color-text-button-warning: var(--color-white);
--color-background-button-secondary: rgb(66, 184, 221);
--color-background-button-cancel: rgb(200, 200, 200);
--color-text-button: var(--color-white);
--color-background-button-tag: rgb(99, 99, 99);
--color-background-snapshot-age: #dfdfdf;
--color-error-text-snapshot-age: var(--color-white);
--color-error-background-snapshot-age: #ff0000;
--color-background-button-tag-active: #9c9c9c;
--color-text-messages: var(--color-white);
--color-background-messages-message: rgba(255, 255, 255, .2);
--color-background-messages-error: rgba(255, 1, 1, .5);
--color-background-messages-notice: rgba(255, 255, 255, .5);
--color-border-notification: #ccc;
--color-background-checkbox-operations: rgba(0, 0, 0, 0.05);
--color-warning: #ff3300;
--color-border-warning: var(--color-warning);
--color-text-legend: var(--color-white);
--color-link-new-version: #e07171;
--color-last-checked: #bbb;
--color-text-footer: #444;
--color-border-watch-table-cell: #eee;
--color-text-watch-tag-list: #e70069;
--color-background-new-watch-form: rgba(0, 0, 0, 0.05);
--color-background-new-watch-input: var(--color-white);
--color-text-new-watch-input: var(--color-text);
--color-border-input: var(--color-grey-500);
--color-shadow-input: var(--color-grey-400);
--color-background-input: var(--color-white);
--color-text-input: var(--color-text);
--color-text-input-description: var(--color-grey-500);
--color-text-input-placeholder: var(--color-grey-600);
--color-background-table-thead: var(--color-grey-800);
--color-border-table-cell: var(--color-grey-700);
--color-text-menu-heading: var(--color-grey-350);
--color-text-menu-link: var(--color-grey-500);
--color-background-menu-link-hover: var(--color-grey-850);
--color-text-menu-link-hover: var(--color-grey-300);
--color-shadow-jump: var(--color-grey-500);
--color-icon-github: var(--color-black);
--color-icon-github-hover: var(--color-grey-300);
--color-watch-table-error: var(--color-dark-red);
--color-watch-table-row-text: var(--color-grey-100); }
html[data-darkmode="true"] {
--color-link: #59bdfb;
--color-text: var(--color-white);
--color-background-gradient-first: #3f90a5;
--color-background-gradient-second: #1e316c;
--color-background-gradient-third: #4d2c64;
--color-background-new-watch-input: var(--color-grey-100);
--color-text-new-watch-input: var(--color-text);
--color-background-table-thead: var(--color-grey-200);
--color-table-background: var(--color-grey-300);
--color-table-stripe: var(--color-grey-325);
--color-background: var(--color-grey-300);
--color-text-menu-heading: var(--color-grey-850);
--color-text-menu-link: var(--color-grey-800);
--color-border-table-cell: var(--color-grey-400);
--color-text-tab-active: var(--color-text);
--color-border-input: var(--color-grey-400);
--color-shadow-input: var(--color-grey-50);
--color-background-input: var(--color-grey-350);
--color-text-input-description: var(--color-grey-600);
--color-text-input-placeholder: var(--color-grey-600);
--color-text-watch-tag-list: #fa3e92;
--color-background-code: var(--color-grey-200);
--color-background-tab: rgba(0, 0, 0, 0.2);
--color-background-tab-hover: rgba(0, 0, 0, 0.5);
--color-background-snapshot-age: var(--color-grey-200);
--color-shadow-jump: var(--color-grey-200);
--color-icon-github: var(--color-white);
--color-icon-github-hover: var(--color-grey-700);
--color-watch-table-error: var(--color-light-red);
--color-watch-table-row-text: var(--color-grey-800); }
html[data-darkmode="true"] .icon-spread {
filter: hue-rotate(-10deg) brightness(1.5); }
html[data-darkmode="true"] .watch-table .title-col a[target="_blank"]::after,
html[data-darkmode="true"] .watch-table .current-diff-url::after {
filter: invert(0.5) hue-rotate(10deg) brightness(2); }
html[data-darkmode="true"] .watch-table .watch-controls .state-off img {
opacity: 0.3; }
html[data-darkmode="true"] .watch-table .watch-controls .state-on img {
opacity: 1.0; }
html[data-darkmode="true"] .watch-table .unviewed {
color: #fff; }
html[data-darkmode="true"] .watch-table .unviewed.error {
color: var(--color-watch-table-error); }
/* spinner */ /* spinner */
.spinner, .spinner,
.spinner:after { .spinner:after {
@@ -184,8 +50,6 @@ html[data-darkmode="true"] {
#browser_steps li { #browser_steps li {
list-style: decimal; list-style: decimal;
padding: 5px; } padding: 5px; }
#browser_steps li:not(:first-child):hover {
opacity: 1.0; }
#browser_steps li .control { #browser_steps li .control {
padding-left: 5px; padding-left: 5px;
padding-right: 5px; } padding-right: 5px; }
@@ -232,17 +96,11 @@ html[data-darkmode="true"] {
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
margin-left: -40px; margin-left: -40px;
z-index: 100; z-index: 100; }
max-width: 350px;
text-align: center; }
#browsersteps-selector-wrapper .spinner, #browsersteps-selector-wrapper .spinner:after { #browsersteps-selector-wrapper .spinner, #browsersteps-selector-wrapper .spinner:after {
width: 80px; width: 80px;
height: 80px; height: 80px;
font-size: 3px; } font-size: 3px; }
#browsersteps-selector-wrapper #browsersteps-click-start {
color: var(--color-grey-400); }
#browsersteps-selector-wrapper #browsersteps-click-start:hover {
cursor: pointer; }
.arrow { .arrow {
border: solid #1b98f8; border: solid #1b98f8;
@@ -263,70 +121,28 @@ html[data-darkmode="true"] {
-webkit-transform: rotate(45deg); } -webkit-transform: rotate(45deg); }
body { body {
color: var(--color-text); color: #333;
background: var(--color-background-page); } background: #262626; }
.visually-hidden {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px; }
.pure-table-even { .pure-table-even {
background: var(--color-background); } background: #fff; }
/* Some styles from https://css-tricks.com/ */ /* Some styles from https://css-tricks.com/ */
a { a {
text-decoration: none; text-decoration: none;
color: var(--color-link); } color: #1b98f8; }
a.github-link { a.github-link {
color: var(--color-icon-github); color: #fff; }
margin: 0 1rem 0 0.5rem; }
a.github-link svg {
fill: currentColor; }
a.github-link:hover {
color: var(--color-icon-github-hover); }
button.toggle-theme {
width: 4rem;
background: transparent;
border: none;
cursor: pointer;
color: var(--color-icon-github); }
button.toggle-theme:hover {
color: var(--color-icon-github-hover); }
button.toggle-theme svg {
fill: currentColor; }
button.toggle-theme .icon-light {
display: block; }
button.toggle-theme .icon-dark {
display: none; }
button.toggle-theme.dark .icon-light {
display: none; }
button.toggle-theme.dark .icon-dark {
display: block; }
.pure-menu-horizontal { .pure-menu-horizontal {
background: var(--color-background); background: #fff;
padding: 5px; padding: 5px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
border-bottom: 2px solid var(--color-menu-accent); border-bottom: 2px solid #ed5900;
align-items: center; } align-items: center; }
.pure-menu-heading {
color: var(--color-text-menu-heading); }
.pure-menu-link {
color: var(--color-text-menu-link); }
.pure-menu-link:hover {
background-color: var(--color-background-menu-link-hover);
color: var(--color-text-menu-link-hover); }
section.content { section.content {
padding-top: 5em; padding-top: 5em;
padding-bottom: 1em; padding-bottom: 1em;
@@ -336,24 +152,21 @@ section.content {
justify-content: center; } justify-content: center; }
code { code {
background: var(--color-background-code); background: #eee; }
color: var(--color-text); }
/* table related */ /* table related */
.watch-table { .watch-table {
width: 100%; width: 100%;
font-size: 80%; } font-size: 80%; }
.watch-table tr { .watch-table tr.unviewed {
color: var(--color-watch-table-row-text); } font-weight: bold; }
.watch-table tr.unviewed { .watch-table .error {
font-weight: bold; } color: #a00; }
.watch-table tr.error {
color: var(--color-watch-table-error); }
.watch-table td { .watch-table td {
white-space: nowrap; } white-space: nowrap; }
.watch-table td.title-col { .watch-table td.title-col {
word-break: break-all; word-break: break-all;
white-space: normal; } white-space: normal; }
.watch-table th { .watch-table th {
white-space: nowrap; } white-space: nowrap; }
.watch-table th a { .watch-table th a {
@@ -362,13 +175,12 @@ code {
font-weight: bolder; } font-weight: bolder; }
.watch-table th a.inactive .arrow { .watch-table th a.inactive .arrow {
display: none; } display: none; }
.watch-table .title-col a[target="_blank"]::after, .watch-table .title-col a[target="_blank"]::after, .watch-table .current-diff-url::after {
.watch-table .current-diff-url::after {
content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==); content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==);
margin: 0 3px 0 5px; } margin: 0 3px 0 5px; }
.watch-tag-list { .watch-tag-list {
color: var(--color-text-watch-tag-list); color: #e70069;
white-space: nowrap; } white-space: nowrap; }
.box { .box {
@@ -391,10 +203,9 @@ code {
body:after { body:after {
content: ""; content: "";
background: linear-gradient(130deg, var(--color-background-gradient-first), var(--color-background-gradient-second) 41.07%, var(--color-background-gradient-third) 84.05%); } background: linear-gradient(130deg, #5ad8f7, #2f50af 41.07%, #9150bf 84.05%); }
body:after, body:after, body:before {
body:before {
display: block; display: block;
height: 650px; height: 650px;
position: absolute; position: absolute;
@@ -410,8 +221,7 @@ body::before {
content: ""; content: "";
background-size: cover; } background-size: cover; }
body:after, body:after, body:before {
body:before {
-webkit-clip-path: polygon(100% 0, 0 0, 0 77.5%, 1% 77.4%, 2% 77.1%, 3% 76.6%, 4% 75.9%, 5% 75.05%, 6% 74.05%, 7% 72.95%, 8% 71.75%, 9% 70.55%, 10% 69.3%, 11% 68.05%, 12% 66.9%, 13% 65.8%, 14% 64.8%, 15% 64%, 16% 63.35%, 17% 62.85%, 18% 62.6%, 19% 62.5%, 20% 62.65%, 21% 63%, 22% 63.5%, 23% 64.2%, 24% 65.1%, 25% 66.1%, 26% 67.2%, 27% 68.4%, 28% 69.65%, 29% 70.9%, 30% 72.15%, 31% 73.3%, 32% 74.35%, 33% 75.3%, 34% 76.1%, 35% 76.75%, 36% 77.2%, 37% 77.45%, 38% 77.5%, 39% 77.3%, 40% 76.95%, 41% 76.4%, 42% 75.65%, 43% 74.75%, 44% 73.75%, 45% 72.6%, 46% 71.4%, 47% 70.15%, 48% 68.9%, 49% 67.7%, 50% 66.55%, 51% 65.5%, 52% 64.55%, 53% 63.75%, 54% 63.15%, 55% 62.75%, 56% 62.55%, 57% 62.5%, 58% 62.7%, 59% 63.1%, 60% 63.7%, 61% 64.45%, 62% 65.4%, 63% 66.45%, 64% 67.6%, 65% 68.8%, 66% 70.05%, 67% 71.3%, 68% 72.5%, 69% 73.6%, 70% 74.65%, 71% 75.55%, 72% 76.35%, 73% 76.9%, 74% 77.3%, 75% 77.5%, 76% 77.45%, 77% 77.25%, 78% 76.8%, 79% 76.2%, 80% 75.4%, 81% 74.45%, 82% 73.4%, 83% 72.25%, 84% 71.05%, 85% 69.8%, 86% 68.55%, 87% 67.35%, 88% 66.2%, 89% 65.2%, 90% 64.3%, 91% 63.55%, 92% 63%, 93% 62.65%, 94% 62.5%, 95% 62.55%, 96% 62.8%, 97% 63.3%, 98% 63.9%, 99% 64.75%, 100% 65.7%); -webkit-clip-path: polygon(100% 0, 0 0, 0 77.5%, 1% 77.4%, 2% 77.1%, 3% 76.6%, 4% 75.9%, 5% 75.05%, 6% 74.05%, 7% 72.95%, 8% 71.75%, 9% 70.55%, 10% 69.3%, 11% 68.05%, 12% 66.9%, 13% 65.8%, 14% 64.8%, 15% 64%, 16% 63.35%, 17% 62.85%, 18% 62.6%, 19% 62.5%, 20% 62.65%, 21% 63%, 22% 63.5%, 23% 64.2%, 24% 65.1%, 25% 66.1%, 26% 67.2%, 27% 68.4%, 28% 69.65%, 29% 70.9%, 30% 72.15%, 31% 73.3%, 32% 74.35%, 33% 75.3%, 34% 76.1%, 35% 76.75%, 36% 77.2%, 37% 77.45%, 38% 77.5%, 39% 77.3%, 40% 76.95%, 41% 76.4%, 42% 75.65%, 43% 74.75%, 44% 73.75%, 45% 72.6%, 46% 71.4%, 47% 70.15%, 48% 68.9%, 49% 67.7%, 50% 66.55%, 51% 65.5%, 52% 64.55%, 53% 63.75%, 54% 63.15%, 55% 62.75%, 56% 62.55%, 57% 62.5%, 58% 62.7%, 59% 63.1%, 60% 63.7%, 61% 64.45%, 62% 65.4%, 63% 66.45%, 64% 67.6%, 65% 68.8%, 66% 70.05%, 67% 71.3%, 68% 72.5%, 69% 73.6%, 70% 74.65%, 71% 75.55%, 72% 76.35%, 73% 76.9%, 74% 77.3%, 75% 77.5%, 76% 77.45%, 77% 77.25%, 78% 76.8%, 79% 76.2%, 80% 75.4%, 81% 74.45%, 82% 73.4%, 83% 72.25%, 84% 71.05%, 85% 69.8%, 86% 68.55%, 87% 67.35%, 88% 66.2%, 89% 65.2%, 90% 64.3%, 91% 63.55%, 92% 63%, 93% 62.65%, 94% 62.5%, 95% 62.55%, 96% 62.8%, 97% 63.3%, 98% 63.9%, 99% 64.75%, 100% 65.7%);
clip-path: polygon(100% 0, 0 0, 0 77.5%, 1% 77.4%, 2% 77.1%, 3% 76.6%, 4% 75.9%, 5% 75.05%, 6% 74.05%, 7% 72.95%, 8% 71.75%, 9% 70.55%, 10% 69.3%, 11% 68.05%, 12% 66.9%, 13% 65.8%, 14% 64.8%, 15% 64%, 16% 63.35%, 17% 62.85%, 18% 62.6%, 19% 62.5%, 20% 62.65%, 21% 63%, 22% 63.5%, 23% 64.2%, 24% 65.1%, 25% 66.1%, 26% 67.2%, 27% 68.4%, 28% 69.65%, 29% 70.9%, 30% 72.15%, 31% 73.3%, 32% 74.35%, 33% 75.3%, 34% 76.1%, 35% 76.75%, 36% 77.2%, 37% 77.45%, 38% 77.5%, 39% 77.3%, 40% 76.95%, 41% 76.4%, 42% 75.65%, 43% 74.75%, 44% 73.75%, 45% 72.6%, 46% 71.4%, 47% 70.15%, 48% 68.9%, 49% 67.7%, 50% 66.55%, 51% 65.5%, 52% 64.55%, 53% 63.75%, 54% 63.15%, 55% 62.75%, 56% 62.55%, 57% 62.5%, 58% 62.7%, 59% 63.1%, 60% 63.7%, 61% 64.45%, 62% 65.4%, 63% 66.45%, 64% 67.6%, 65% 68.8%, 66% 70.05%, 67% 71.3%, 68% 72.5%, 69% 73.6%, 70% 74.65%, 71% 75.55%, 72% 76.35%, 73% 76.9%, 74% 77.3%, 75% 77.5%, 76% 77.45%, 77% 77.25%, 78% 76.8%, 79% 76.2%, 80% 75.4%, 81% 74.45%, 82% 73.4%, 83% 72.25%, 84% 71.05%, 85% 69.8%, 86% 68.55%, 87% 67.35%, 88% 66.2%, 89% 65.2%, 90% 64.3%, 91% 63.55%, 92% 63%, 93% 62.65%, 94% 62.5%, 95% 62.55%, 96% 62.8%, 97% 63.3%, 98% 63.9%, 99% 64.75%, 100% 65.7%); } clip-path: polygon(100% 0, 0 0, 0 77.5%, 1% 77.4%, 2% 77.1%, 3% 76.6%, 4% 75.9%, 5% 75.05%, 6% 74.05%, 7% 72.95%, 8% 71.75%, 9% 70.55%, 10% 69.3%, 11% 68.05%, 12% 66.9%, 13% 65.8%, 14% 64.8%, 15% 64%, 16% 63.35%, 17% 62.85%, 18% 62.6%, 19% 62.5%, 20% 62.65%, 21% 63%, 22% 63.5%, 23% 64.2%, 24% 65.1%, 25% 66.1%, 26% 67.2%, 27% 68.4%, 28% 69.65%, 29% 70.9%, 30% 72.15%, 31% 73.3%, 32% 74.35%, 33% 75.3%, 34% 76.1%, 35% 76.75%, 36% 77.2%, 37% 77.45%, 38% 77.5%, 39% 77.3%, 40% 76.95%, 41% 76.4%, 42% 75.65%, 43% 74.75%, 44% 73.75%, 45% 72.6%, 46% 71.4%, 47% 70.15%, 48% 68.9%, 49% 67.7%, 50% 66.55%, 51% 65.5%, 52% 64.55%, 53% 63.75%, 54% 63.15%, 55% 62.75%, 56% 62.55%, 57% 62.5%, 58% 62.7%, 59% 63.1%, 60% 63.7%, 61% 64.45%, 62% 65.4%, 63% 66.45%, 64% 67.6%, 65% 68.8%, 66% 70.05%, 67% 71.3%, 68% 72.5%, 69% 73.6%, 70% 74.65%, 71% 75.55%, 72% 76.35%, 73% 76.9%, 74% 77.3%, 75% 77.5%, 76% 77.45%, 77% 77.25%, 78% 76.8%, 79% 76.2%, 80% 75.4%, 81% 74.45%, 82% 73.4%, 83% 72.25%, 84% 71.05%, 85% 69.8%, 86% 68.55%, 87% 67.35%, 88% 66.2%, 89% 65.2%, 90% 64.3%, 91% 63.55%, 92% 63%, 93% 62.65%, 94% 62.5%, 95% 62.55%, 96% 62.8%, 97% 63.3%, 98% 63.9%, 99% 64.75%, 100% 65.7%); }
@@ -424,99 +234,83 @@ body:before {
max-width: 400px; max-width: 400px;
display: block; } display: block; }
.pure-button-primary,
a.pure-button-primary,
.pure-button-selected,
a.pure-button-selected {
background-color: var(--color-background-button-primary); }
.button-secondary { .button-secondary {
color: var(--color-text-button); color: white;
border-radius: 4px; border-radius: 4px;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); } text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); }
.button-success { .button-success {
background: var(--color-background-button-success); } background: #1cb841;
/* this is a green */ }
.button-tag { .button-tag {
background: var(--color-background-button-tag); background: #636363;
color: var(--color-text-button); color: #fff;
font-size: 65%; font-size: 65%;
border-bottom-left-radius: initial; border-bottom-left-radius: initial;
border-bottom-right-radius: initial; } border-bottom-right-radius: initial; }
.button-tag.active { .button-tag.active {
background: var(--color-background-button-tag-active); background: #9c9c9c;
font-weight: bold; } font-weight: bold; }
.button-error { .button-error {
background: var(--color-background-button-error); background: #ca3c3c;
color: var(--color-text-button-error); } /* this is a maroon */ }
.button-warning { .button-warning {
background: var(--color-background-button-warning); background: #df7514;
color: var(--color-text-button-warning); } /* this is an orange */ }
.button-secondary { .button-secondary {
background: var(--color-background-button-secondary); } background: #42b8dd;
/* this is a light blue */ }
.button-cancel { .button-cancel {
background: var(--color-background-button-cancel); } background: #c8c8c8;
/* this is a green */ }
#save_button {
margin-right: 1rem; }
.messages li { .messages li {
list-style: none; list-style: none;
padding: 1em; padding: 1em;
border-radius: 10px; border-radius: 10px;
color: var(--color-text-messages); color: #fff;
font-weight: bold; } font-weight: bold; }
.messages li.message { .messages li.message {
background: var(--color-background-messages-message); } background: rgba(255, 255, 255, 0.2); }
.messages li.error { .messages li.error {
background: var(--color-background-messages-error); } background: rgba(255, 1, 1, 0.5); }
.messages li.notice { .messages li.notice {
background: var(--color-background-messages-notice); } background: rgba(255, 255, 255, 0.5); }
.messages.with-share-link > *:hover { .messages.with-share-link > *:hover {
cursor: pointer; } cursor: pointer; }
.notifications-wrapper {
padding: 0.5rem 0 1rem 0; }
label:hover {
cursor: pointer; }
#notification-customisation { #notification-customisation {
border: 1px solid var(--color-border-notification); border: 1px solid #ccc;
padding: 0.5rem; padding: 0.5rem;
border-radius: 5px; } border-radius: 5px; }
#notification-error-log { #notification-error-log {
border: 1px solid var(--color-border-notification); border: 1px solid #ccc;
padding: 1rem; padding: 1rem;
border-radius: 5px; border-radius: 5px;
overflow-wrap: break-word; } overflow-wrap: break-word; }
#token-table.pure-table td, #token-table.pure-table td, #token-table.pure-table th {
#token-table.pure-table th {
font-size: 80%; } font-size: 80%; }
#new-watch-form { #new-watch-form {
background: var(--color-background-new-watch-form); background: rgba(0, 0, 0, 0.05);
padding: 1em; padding: 1em;
border-radius: 10px; border-radius: 10px;
margin-bottom: 1em; } margin-bottom: 1em; }
#new-watch-form input { #new-watch-form input {
display: inline-block; display: inline-block;
margin-bottom: 5px; } margin-bottom: 5px; }
#new-watch-form input:not(.pure-button) {
background-color: var(--color-background-new-watch-input);
color: var(--color-text-new-watch-input); }
#new-watch-form .label { #new-watch-form .label {
display: none; } display: none; }
#new-watch-form legend { #new-watch-form legend {
color: var(--color-text-legend); color: #fff;
font-weight: bold; } font-weight: bold; }
#new-watch-form #watch-add-wrapper-zone > div { #new-watch-form #watch-add-wrapper-zone > div {
display: inline-block; } display: inline-block; }
@@ -531,14 +325,14 @@ label:hover {
position: fixed; position: fixed;
left: 0px; left: 0px;
top: 120px; top: 120px;
background: var(--color-background); background: #fff;
padding: 10px; padding: 10px;
border-top-right-radius: 5px; border-top-right-radius: 5px;
border-bottom-right-radius: 5px; border-bottom-right-radius: 5px;
box-shadow: 1px 1px 4px var(--color-shadow-jump); } box-shadow: 5px 0 5px -2px #888; }
#diff-jump a { #diff-jump a {
color: var(--color-link); color: #1b98f8;
cursor: pointer; cursor: grabbing;
-moz-user-select: none; -moz-user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
-ms-user-select: none; -ms-user-select: none;
@@ -547,8 +341,8 @@ label:hover {
footer { footer {
padding: 10px; padding: 10px;
background: var(--color-background); background: #fff;
color: var(--color-text-footer); color: #444;
text-align: center; } text-align: center; }
#feed-icon { #feed-icon {
@@ -567,7 +361,7 @@ footer {
position: absolute; position: absolute;
top: 60px; top: 60px;
font-size: 65%; font-size: 65%;
background: var(--color-background); background: #fff;
padding: 10px; } padding: 10px; }
.sticky-tab#left-sticky { .sticky-tab#left-sticky {
left: 0px; } left: 0px; }
@@ -579,10 +373,9 @@ footer {
font-weight: bold; } font-weight: bold; }
#new-version-text a { #new-version-text a {
color: var(--color-link-new-version); } color: #e07171; }
.watch-controls { .watch-controls {
color: #f8321b;
/* default */ } /* default */ }
.watch-controls .state-on img { .watch-controls .state-on img {
opacity: 0.8; } opacity: 0.8; }
@@ -597,7 +390,7 @@ footer {
font-family: monospace; font-family: monospace;
white-space: pre; white-space: pre;
overflow-wrap: normal; overflow-wrap: normal;
overflow-x: auto; } overflow-x: scroll; }
.pure-form { .pure-form {
/* The input fields with errors */ /* The input fields with errors */
@@ -607,39 +400,27 @@ footer {
.pure-form fieldset ul { .pure-form fieldset ul {
padding-bottom: 0px; padding-bottom: 0px;
margin-bottom: 0px; } margin-bottom: 0px; }
.pure-form .pure-control-group, .pure-form .pure-control-group, .pure-form .pure-group, .pure-form .pure-controls {
.pure-form .pure-group,
.pure-form .pure-controls {
padding-bottom: 1em; } padding-bottom: 1em; }
.pure-form .pure-control-group div, .pure-form .pure-control-group div, .pure-form .pure-group div, .pure-form .pure-controls div {
.pure-form .pure-group div,
.pure-form .pure-controls div {
margin: 0px; } margin: 0px; }
.pure-form .pure-control-group .checkbox > *, .pure-form .pure-control-group .checkbox > *, .pure-form .pure-group .checkbox > *, .pure-form .pure-controls .checkbox > * {
.pure-form .pure-group .checkbox > *,
.pure-form .pure-controls .checkbox > * {
display: inline; display: inline;
vertical-align: middle; } vertical-align: middle; }
.pure-form .pure-control-group .checkbox > label, .pure-form .pure-control-group .checkbox > label, .pure-form .pure-group .checkbox > label, .pure-form .pure-controls .checkbox > label {
.pure-form .pure-group .checkbox > label,
.pure-form .pure-controls .checkbox > label {
padding-left: 5px; } padding-left: 5px; }
.pure-form .pure-control-group legend,
.pure-form .pure-group legend,
.pure-form .pure-controls legend {
color: var(--color-text-legend); }
.pure-form .error input { .pure-form .error input {
background-color: var(--color-error-input); } background-color: #ffebeb; }
.pure-form ul.errors { .pure-form ul.errors {
padding: .5em .6em; padding: .5em .6em;
border: 1px solid var(--color-error-list); border: 1px solid #dd0000;
border-radius: 4px; border-radius: 4px;
vertical-align: middle; vertical-align: middle;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
box-sizing: border-box; } box-sizing: border-box; }
.pure-form ul.errors li { .pure-form ul.errors li {
margin-left: 1em; margin-left: 1em;
color: var(--color-error-list); } color: #dd0000; }
.pure-form label { .pure-form label {
font-weight: bold; } font-weight: bold; }
.pure-form textarea { .pure-form textarea {
@@ -681,19 +462,15 @@ footer {
/* Force table to not be like tables anymore */ /* Force table to not be like tables anymore */
/* Force table to not be like tables anymore */ /* Force table to not be like tables anymore */
/* Hide table headers (but not display: none;, for accessibility) */ } /* Hide table headers (but not display: none;, for accessibility) */ }
.watch-table thead, .watch-table thead, .watch-table tbody, .watch-table th, .watch-table td, .watch-table tr {
.watch-table tbody,
.watch-table th,
.watch-table td,
.watch-table tr {
display: block; } display: block; }
.watch-table .last-checked > span { .watch-table .last-checked > span {
vertical-align: middle; } vertical-align: middle; }
.watch-table .last-checked::before { .watch-table .last-checked::before {
color: var(--color-last-checked); color: #555;
content: "Last Checked "; } content: "Last Checked "; }
.watch-table .last-changed::before { .watch-table .last-changed::before {
color: var(--color-last-checked); color: #555;
content: "Last Changed "; } content: "Last Changed "; }
.watch-table td.inline { .watch-table td.inline {
display: inline-block; } display: inline-block; }
@@ -701,13 +478,12 @@ footer {
position: absolute; position: absolute;
top: -9999px; top: -9999px;
left: -9999px; } left: -9999px; }
.watch-table .pure-table td, .watch-table .pure-table td, .watch-table .pure-table th {
.watch-table .pure-table th {
border: none; } border: none; }
.watch-table td { .watch-table td {
/* Behave like a "row" */ /* Behave like a "row" */
border: none; border: none;
border-bottom: 1px solid var(--color-border-watch-table-cell); border-bottom: 1px solid #eee;
vertical-align: middle; } vertical-align: middle; }
.watch-table td:before { .watch-table td:before {
/* Top/left values mimic padding */ /* Top/left values mimic padding */
@@ -717,66 +493,12 @@ footer {
padding-right: 10px; padding-right: 10px;
white-space: nowrap; } white-space: nowrap; }
.watch-table.pure-table-striped tr { .watch-table.pure-table-striped tr {
background-color: var(--color-table-background); } background-color: #fff; }
.watch-table.pure-table-striped tr:nth-child(2n-1) { .watch-table.pure-table-striped tr:nth-child(2n-1) {
background-color: var(--color-table-stripe); } background-color: #eee; }
.watch-table.pure-table-striped tr:nth-child(2n-1) td { .watch-table.pure-table-striped tr:nth-child(2n-1) td {
background-color: inherit; } } background-color: inherit; } }
.pure-table {
border-color: var(--color-border-table-cell); }
.pure-table thead {
background-color: var(--color-background-table-thead);
color: var(--color-text); }
.pure-table td,
.pure-table th {
border-left-color: var(--color-border-table-cell); }
.pure-table-striped tr:nth-child(2n-1) td {
background-color: var(--color-table-stripe); }
.pure-form input[type=color],
.pure-form input[type=date],
.pure-form input[type=datetime-local],
.pure-form input[type=datetime],
.pure-form input[type=email],
.pure-form input[type=month],
.pure-form input[type=number],
.pure-form input[type=password],
.pure-form input[type=search],
.pure-form input[type=tel],
.pure-form input[type=text],
.pure-form input[type=time],
.pure-form input[type=url],
.pure-form input[type=week],
.pure-form select,
.pure-form textarea {
border: var(--color-border-input);
box-shadow: inset 0 1px 3px var(--color-shadow-input);
background-color: var(--color-background-input);
color: var(--color-text-input); }
.pure-form input[type=color]:active,
.pure-form input[type=date]:active,
.pure-form input[type=datetime-local]:active,
.pure-form input[type=datetime]:active,
.pure-form input[type=email]:active,
.pure-form input[type=month]:active,
.pure-form input[type=number]:active,
.pure-form input[type=password]:active,
.pure-form input[type=search]:active,
.pure-form input[type=tel]:active,
.pure-form input[type=text]:active,
.pure-form input[type=time]:active,
.pure-form input[type=url]:active,
.pure-form input[type=week]:active,
.pure-form select:active,
.pure-form textarea:active {
background-color: var(--color-background-input); }
input::placeholder,
textarea::placeholder {
color: var(--color-text-input-placeholder); }
/** Desktop vs mobile input field strategy /** Desktop vs mobile input field strategy
- We dont use 'size' with <input> because `size` is too unreliable to override, and will often push-out - We dont use 'size' with <input> because `size` is too unreliable to override, and will often push-out
- Rely always on width in CSS - Rely always on width in CSS
@@ -793,29 +515,25 @@ textarea::placeholder {
.tabs ul li { .tabs ul li {
margin-right: 3px; margin-right: 3px;
display: inline-block; display: inline-block;
color: var(--color-text-tab); color: #fff;
border-top-left-radius: 5px; border-top-left-radius: 5px;
border-top-right-radius: 5px; border-top-right-radius: 5px;
background-color: var(--color-background-tab); } background-color: rgba(255, 255, 255, 0.2); }
.tabs ul li:not(.active):hover { .tabs ul li.active, .tabs ul li :target {
background-color: var(--color-background-tab-hover); } background-color: #fff; }
.tabs ul li.active, .tabs ul li.active a, .tabs ul li :target a {
.tabs ul li :target { color: #222;
background-color: var(--color-background); }
.tabs ul li.active a,
.tabs ul li :target a {
color: var(--color-text-tab-active);
font-weight: bold; } font-weight: bold; }
.tabs ul li a { .tabs ul li a {
display: block; display: block;
padding: 0.8em; padding: 0.8em;
color: var(--color-text-tab); } color: #fff; }
.pure-form-stacked > div:first-child { .pure-form-stacked > div:first-child {
display: block; } display: block; }
.login-form .inner { .login-form .inner {
background: var(--color-background); background: #fff;
padding: 20px; padding: 20px;
border-radius: 5px; } border-radius: 5px; }
@@ -845,16 +563,13 @@ body.full-width .edit-form {
.edit-form .box-wrap { .edit-form .box-wrap {
position: relative; } position: relative; }
.edit-form .inner { .edit-form .inner {
background: var(--color-background); background: #fff;
padding: 20px; } padding: 20px; }
.edit-form #actions { .edit-form #actions {
display: block; display: block;
background: var(--color-background); } background: #fff; }
.edit-form .pure-form-message-inline { .edit-form .pure-form-message-inline {
padding-left: 0; padding-left: 0; }
color: var(--color-text-input-description); }
.edit-form .pure-form-message-inline code {
font-size: .875em; }
ul { ul {
padding-left: 1em; padding-left: 1em;
@@ -891,13 +606,13 @@ ul {
cursor: pointer; } cursor: pointer; }
#api-key-copy { #api-key-copy {
color: var(--color-api-key); } color: #0078e7; }
.button-green { .button-green {
background-color: var(--color-background-button-green); } background-color: #42dd53; }
.button-red { .button-red {
background-color: var(--color-background-button-red); } background-color: #dd4242; }
.noselect { .noselect {
-webkit-touch-callout: none; -webkit-touch-callout: none;
@@ -910,21 +625,20 @@ ul {
/* Internet Explorer/Edge */ /* Internet Explorer/Edge */
user-select: none; user-select: none;
/* Non-prefixed version, currently /* Non-prefixed version, currently
supported by Chrome, Edge, Opera and Firefox */ } supported by Chrome, Edge, Opera and Firefox */ }
.snapshot-age { .snapshot-age {
padding: 4px; padding: 4px;
margin: 0.5rem 0; background-color: #dfdfdf;
background-color: var(--color-background-snapshot-age);
border-radius: 3px; border-radius: 3px;
font-weight: bold; font-weight: bold;
margin-bottom: 4px; } margin-bottom: 4px; }
.snapshot-age.error { .snapshot-age.error {
background-color: var(--color-error-background-snapshot-age); background-color: #ff0000;
color: var(--color-error-text-snapshot-age); } color: #fff; }
#checkbox-operations { #checkbox-operations {
background: var(--color-background-checkbox-operations); background: rgba(0, 0, 0, 0.05);
padding: 1em; padding: 1em;
border-radius: 10px; border-radius: 10px;
margin-bottom: 1em; margin-bottom: 1em;
@@ -934,10 +648,10 @@ ul {
vertical-align: middle; } vertical-align: middle; }
.inline-warning { .inline-warning {
border: 1px solid var(--color-border-warning); border: 1px solid #ff3300;
padding: 0.5rem; padding: 0.5rem;
border-radius: 5px; border-radius: 5px;
color: var(--color-warning); } color: #ff3300; }
.inline-warning > span { .inline-warning > span {
display: inline-block; display: inline-block;
vertical-align: middle; } vertical-align: middle; }
@@ -945,24 +659,3 @@ ul {
display: inline; display: inline;
height: 26px; height: 26px;
vertical-align: middle; } vertical-align: middle; }
/* automatic price following helpers */
.tracking-ldjson-price-data {
background-color: var(--color-background-button-green);
color: #000;
padding: 3px;
border-radius: 3px;
white-space: nowrap; }
.ldjson-price-track-offer {
font-weight: bold;
font-style: italic; }
.ldjson-price-track-offer a.pure-button {
border-radius: 3px;
padding: 3px;
background-color: var(--color-background-button-green); }
.price-follow-tag-icon {
display: inline-block;
height: 0.8rem;
vertical-align: middle; }

View File

@@ -1,107 +1,42 @@
/* /*
* -- BASE STYLES -- * -- BASE STYLES --
* Most of these are inherited from Base, but I want to change a few.
nvm use v14.18.1 && npm install && npm run build
* or npm run watch
*/ */
@import "parts/_variables"; @import "parts/spinners";
@import "parts/_spinners"; @import "parts/browser-steps";
@import "parts/_browser-steps"; @import "parts/_arrows.scss";
@import "parts/_arrows";
body { body {
color: var(--color-text); color: #333;
background: var(--color-background-page); background: #262626;
}
.visually-hidden {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
} }
.pure-table-even { .pure-table-even {
background: var(--color-background); background: #fff;
} }
/* Some styles from https://css-tricks.com/ */ /* Some styles from https://css-tricks.com/ */
a { a {
text-decoration: none; text-decoration: none;
color: var(--color-link); color: #1b98f8;
} }
a.github-link { a.github-link {
color: var(--color-icon-github); color: #fff;
margin: 0 1rem 0 0.5rem;
svg {
fill: currentColor;
}
&:hover {
color: var(--color-icon-github-hover);
}
}
button.toggle-theme {
width: 4rem;
background: transparent;
border: none;
cursor: pointer;
color: var(--color-icon-github);
&:hover {
color: var(--color-icon-github-hover);
}
svg {
fill: currentColor;
}
.icon-light {
display: block;
}
.icon-dark {
display: none;
}
&.dark {
.icon-light {
display: none;
}
.icon-dark {
display: block;
}
}
} }
.pure-menu-horizontal { .pure-menu-horizontal {
background: var(--color-background); background: #fff;
padding: 5px; padding: 5px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
border-bottom: 2px solid var(--color-menu-accent); border-bottom: 2px solid #ed5900;
align-items: center; align-items: center;
} }
.pure-menu-heading {
color: var(--color-text-menu-heading);
}
.pure-menu-link {
color: var(--color-text-menu-link);
&:hover {
background-color: var(--color-background-menu-link-hover);
color: var(--color-text-menu-link-hover);
}
}
section.content { section.content {
padding-top: 5em; padding-top: 5em;
padding-bottom: 1em; padding-bottom: 1em;
@@ -112,8 +47,7 @@ section.content {
} }
code { code {
background: var(--color-background-code); background: #eee;
color: var(--color-text);
} }
/* table related */ /* table related */
@@ -121,36 +55,30 @@ code {
width: 100%; width: 100%;
font-size: 80%; font-size: 80%;
tr { tr.unviewed {
&.unviewed { font-weight: bold;
font-weight: bold;
}
&.error {
color: var(--color-watch-table-error);
}
color: var(--color-watch-table-row-text);
} }
.error {
color: #a00;
}
td { td {
white-space: nowrap; white-space: nowrap;
&.title-col {
word-break: break-all;
white-space: normal;
}
} }
td.title-col {
word-break: break-all;
white-space: normal;
}
th { th {
white-space: nowrap; white-space: nowrap;
a { a {
font-weight: normal; font-weight: normal;
&.active { &.active {
font-weight: bolder; font-weight: bolder;
} }
&.inactive { &.inactive {
.arrow { .arrow {
display: none; display: none;
@@ -159,15 +87,14 @@ code {
} }
} }
.title-col a[target="_blank"]::after, .title-col a[target="_blank"]::after, .current-diff-url::after {
.current-diff-url::after {
content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==); content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==);
margin: 0 3px 0 5px; margin: 0 3px 0 5px;
} }
} }
.watch-tag-list { .watch-tag-list {
color: var(--color-text-watch-tag-list); color: #e70069;
white-space: nowrap; white-space: nowrap;
} }
@@ -199,11 +126,10 @@ code {
body:after { body:after {
content: ""; content: "";
background: linear-gradient(130deg, var(--color-background-gradient-first), var(--color-background-gradient-second) 41.07%, var(--color-background-gradient-third) 84.05%); background: linear-gradient(130deg, #5ad8f7, #2f50af 41.07%, #9150bf 84.05%);
} }
body:after, body:after, body:before {
body:before {
display: block; display: block;
height: 650px; height: 650px;
position: absolute; position: absolute;
@@ -223,8 +149,7 @@ body::before {
background-size: cover background-size: cover
} }
body:after, body:after, body:before {
body:before {
-webkit-clip-path: polygon(100% 0, 0 0, 0 77.5%, 1% 77.4%, 2% 77.1%, 3% 76.6%, 4% 75.9%, 5% 75.05%, 6% 74.05%, 7% 72.95%, 8% 71.75%, 9% 70.55%, 10% 69.3%, 11% 68.05%, 12% 66.9%, 13% 65.8%, 14% 64.8%, 15% 64%, 16% 63.35%, 17% 62.85%, 18% 62.6%, 19% 62.5%, 20% 62.65%, 21% 63%, 22% 63.5%, 23% 64.2%, 24% 65.1%, 25% 66.1%, 26% 67.2%, 27% 68.4%, 28% 69.65%, 29% 70.9%, 30% 72.15%, 31% 73.3%, 32% 74.35%, 33% 75.3%, 34% 76.1%, 35% 76.75%, 36% 77.2%, 37% 77.45%, 38% 77.5%, 39% 77.3%, 40% 76.95%, 41% 76.4%, 42% 75.65%, 43% 74.75%, 44% 73.75%, 45% 72.6%, 46% 71.4%, 47% 70.15%, 48% 68.9%, 49% 67.7%, 50% 66.55%, 51% 65.5%, 52% 64.55%, 53% 63.75%, 54% 63.15%, 55% 62.75%, 56% 62.55%, 57% 62.5%, 58% 62.7%, 59% 63.1%, 60% 63.7%, 61% 64.45%, 62% 65.4%, 63% 66.45%, 64% 67.6%, 65% 68.8%, 66% 70.05%, 67% 71.3%, 68% 72.5%, 69% 73.6%, 70% 74.65%, 71% 75.55%, 72% 76.35%, 73% 76.9%, 74% 77.3%, 75% 77.5%, 76% 77.45%, 77% 77.25%, 78% 76.8%, 79% 76.2%, 80% 75.4%, 81% 74.45%, 82% 73.4%, 83% 72.25%, 84% 71.05%, 85% 69.8%, 86% 68.55%, 87% 67.35%, 88% 66.2%, 89% 65.2%, 90% 64.3%, 91% 63.55%, 92% 63%, 93% 62.65%, 94% 62.5%, 95% 62.55%, 96% 62.8%, 97% 63.3%, 98% 63.9%, 99% 64.75%, 100% 65.7%); -webkit-clip-path: polygon(100% 0, 0 0, 0 77.5%, 1% 77.4%, 2% 77.1%, 3% 76.6%, 4% 75.9%, 5% 75.05%, 6% 74.05%, 7% 72.95%, 8% 71.75%, 9% 70.55%, 10% 69.3%, 11% 68.05%, 12% 66.9%, 13% 65.8%, 14% 64.8%, 15% 64%, 16% 63.35%, 17% 62.85%, 18% 62.6%, 19% 62.5%, 20% 62.65%, 21% 63%, 22% 63.5%, 23% 64.2%, 24% 65.1%, 25% 66.1%, 26% 67.2%, 27% 68.4%, 28% 69.65%, 29% 70.9%, 30% 72.15%, 31% 73.3%, 32% 74.35%, 33% 75.3%, 34% 76.1%, 35% 76.75%, 36% 77.2%, 37% 77.45%, 38% 77.5%, 39% 77.3%, 40% 76.95%, 41% 76.4%, 42% 75.65%, 43% 74.75%, 44% 73.75%, 45% 72.6%, 46% 71.4%, 47% 70.15%, 48% 68.9%, 49% 67.7%, 50% 66.55%, 51% 65.5%, 52% 64.55%, 53% 63.75%, 54% 63.15%, 55% 62.75%, 56% 62.55%, 57% 62.5%, 58% 62.7%, 59% 63.1%, 60% 63.7%, 61% 64.45%, 62% 65.4%, 63% 66.45%, 64% 67.6%, 65% 68.8%, 66% 70.05%, 67% 71.3%, 68% 72.5%, 69% 73.6%, 70% 74.65%, 71% 75.55%, 72% 76.35%, 73% 76.9%, 74% 77.3%, 75% 77.5%, 76% 77.45%, 77% 77.25%, 78% 76.8%, 79% 76.2%, 80% 75.4%, 81% 74.45%, 82% 73.4%, 83% 72.25%, 84% 71.05%, 85% 69.8%, 86% 68.55%, 87% 67.35%, 88% 66.2%, 89% 65.2%, 90% 64.3%, 91% 63.55%, 92% 63%, 93% 62.65%, 94% 62.5%, 95% 62.55%, 96% 62.8%, 97% 63.3%, 98% 63.9%, 99% 64.75%, 100% 65.7%);
clip-path: polygon(100% 0, 0 0, 0 77.5%, 1% 77.4%, 2% 77.1%, 3% 76.6%, 4% 75.9%, 5% 75.05%, 6% 74.05%, 7% 72.95%, 8% 71.75%, 9% 70.55%, 10% 69.3%, 11% 68.05%, 12% 66.9%, 13% 65.8%, 14% 64.8%, 15% 64%, 16% 63.35%, 17% 62.85%, 18% 62.6%, 19% 62.5%, 20% 62.65%, 21% 63%, 22% 63.5%, 23% 64.2%, 24% 65.1%, 25% 66.1%, 26% 67.2%, 27% 68.4%, 28% 69.65%, 29% 70.9%, 30% 72.15%, 31% 73.3%, 32% 74.35%, 33% 75.3%, 34% 76.1%, 35% 76.75%, 36% 77.2%, 37% 77.45%, 38% 77.5%, 39% 77.3%, 40% 76.95%, 41% 76.4%, 42% 75.65%, 43% 74.75%, 44% 73.75%, 45% 72.6%, 46% 71.4%, 47% 70.15%, 48% 68.9%, 49% 67.7%, 50% 66.55%, 51% 65.5%, 52% 64.55%, 53% 63.75%, 54% 63.15%, 55% 62.75%, 56% 62.55%, 57% 62.5%, 58% 62.7%, 59% 63.1%, 60% 63.7%, 61% 64.45%, 62% 65.4%, 63% 66.45%, 64% 67.6%, 65% 68.8%, 66% 70.05%, 67% 71.3%, 68% 72.5%, 69% 73.6%, 70% 74.65%, 71% 75.55%, 72% 76.35%, 73% 76.9%, 74% 77.3%, 75% 77.5%, 76% 77.45%, 77% 77.25%, 78% 76.8%, 79% 76.2%, 80% 75.4%, 81% 74.45%, 82% 73.4%, 83% 72.25%, 84% 71.05%, 85% 69.8%, 86% 68.55%, 87% 67.35%, 88% 66.2%, 89% 65.2%, 90% 64.3%, 91% 63.55%, 92% 63%, 93% 62.65%, 94% 62.5%, 95% 62.55%, 96% 62.8%, 97% 63.3%, 98% 63.9%, 99% 64.75%, 100% 65.7%) clip-path: polygon(100% 0, 0 0, 0 77.5%, 1% 77.4%, 2% 77.1%, 3% 76.6%, 4% 75.9%, 5% 75.05%, 6% 74.05%, 7% 72.95%, 8% 71.75%, 9% 70.55%, 10% 69.3%, 11% 68.05%, 12% 66.9%, 13% 65.8%, 14% 64.8%, 15% 64%, 16% 63.35%, 17% 62.85%, 18% 62.6%, 19% 62.5%, 20% 62.65%, 21% 63%, 22% 63.5%, 23% 64.2%, 24% 65.1%, 25% 66.1%, 26% 67.2%, 27% 68.4%, 28% 69.65%, 29% 70.9%, 30% 72.15%, 31% 73.3%, 32% 74.35%, 33% 75.3%, 34% 76.1%, 35% 76.75%, 36% 77.2%, 37% 77.45%, 38% 77.5%, 39% 77.3%, 40% 76.95%, 41% 76.4%, 42% 75.65%, 43% 74.75%, 44% 73.75%, 45% 72.6%, 46% 71.4%, 47% 70.15%, 48% 68.9%, 49% 67.7%, 50% 66.55%, 51% 65.5%, 52% 64.55%, 53% 63.75%, 54% 63.15%, 55% 62.75%, 56% 62.55%, 57% 62.5%, 58% 62.7%, 59% 63.1%, 60% 63.7%, 61% 64.45%, 62% 65.4%, 63% 66.45%, 64% 67.6%, 65% 68.8%, 66% 70.05%, 67% 71.3%, 68% 72.5%, 69% 73.6%, 70% 74.65%, 71% 75.55%, 72% 76.35%, 73% 76.9%, 74% 77.3%, 75% 77.5%, 76% 77.45%, 77% 77.25%, 78% 76.8%, 79% 76.2%, 80% 75.4%, 81% 74.45%, 82% 73.4%, 83% 72.25%, 84% 71.05%, 85% 69.8%, 86% 68.55%, 87% 67.35%, 88% 66.2%, 89% 65.2%, 90% 64.3%, 91% 63.55%, 92% 63%, 93% 62.65%, 94% 62.5%, 95% 62.55%, 96% 62.8%, 97% 63.3%, 98% 63.9%, 99% 64.75%, 100% 65.7%)
} }
@@ -240,57 +165,51 @@ body:before {
display: block; display: block;
} }
.pure-button-primary,
a.pure-button-primary,
.pure-button-selected,
a.pure-button-selected {
background-color: var(--color-background-button-primary);
}
.button-secondary { .button-secondary {
color: var(--color-text-button); color: white;
border-radius: 4px; border-radius: 4px;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
} }
.button-success { .button-success {
background: var(--color-background-button-success); background: rgb(28, 184, 65);
/* this is a green */
} }
.button-tag { .button-tag {
background: var(--color-background-button-tag); background: rgb(99, 99, 99);
color: var(--color-text-button); color: #fff;
font-size: 65%; font-size: 65%;
border-bottom-left-radius: initial; border-bottom-left-radius: initial;
border-bottom-right-radius: initial; border-bottom-right-radius: initial;
&.active { &.active {
background: var(--color-background-button-tag-active); background: #9c9c9c;
font-weight: bold; font-weight: bold;
} }
} }
.button-error { .button-error {
background: var(--color-background-button-error); background: rgb(202, 60, 60);
color: var(--color-text-button-error); /* this is a maroon */
} }
.button-warning { .button-warning {
background: var(--color-background-button-warning); background: rgb(223, 117, 20);
color: var(--color-text-button-warning); /* this is an orange */
} }
.button-secondary { .button-secondary {
background: var(--color-background-button-secondary); background: rgb(66, 184, 221);
/* this is a light blue */
} }
.button-cancel { .button-cancel {
background: var(--color-background-button-cancel); background: rgb(200, 200, 200);
} /* this is a green */
#save_button {
margin-right: 1rem;
} }
.messages { .messages {
@@ -298,62 +217,50 @@ a.pure-button-selected {
list-style: none; list-style: none;
padding: 1em; padding: 1em;
border-radius: 10px; border-radius: 10px;
color: var(--color-text-messages); color: #fff;
font-weight: bold; font-weight: bold;
&.message { &.message {
background: var(--color-background-messages-message); background: rgba(255, 255, 255, .2);
} }
&.error { &.error {
background: var(--color-background-messages-error); background: rgba(255, 1, 1, .5);
} }
&.notice { &.notice {
background: var(--color-background-messages-notice); background: rgba(255, 255, 255, .5);
} }
} }
&.with-share-link { &.with-share-link {
>*:hover { > *:hover {
cursor: pointer; cursor: pointer;
} }
} }
} }
.notifications-wrapper {
padding: 0.5rem 0 1rem 0;
}
label {
&:hover {
cursor: pointer;
}
}
#notification-customisation { #notification-customisation {
border: 1px solid var(--color-border-notification); border: 1px solid #ccc;
padding: 0.5rem; padding: 0.5rem;
border-radius: 5px; border-radius: 5px;
} }
#notification-error-log { #notification-error-log {
border: 1px solid var(--color-border-notification); border: 1px solid #ccc;
padding: 1rem; padding: 1rem;
border-radius: 5px; border-radius: 5px;
overflow-wrap: break-word; overflow-wrap: break-word;
} }
#token-table { #token-table {
&.pure-table td, &.pure-table th {
&.pure-table td,
&.pure-table th {
font-size: 80%; font-size: 80%;
} }
} }
#new-watch-form { #new-watch-form {
background: var(--color-background-new-watch-form); background: rgba(0, 0, 0, .05);
padding: 1em; padding: 1em;
border-radius: 10px; border-radius: 10px;
margin-bottom: 1em; margin-bottom: 1em;
@@ -363,25 +270,19 @@ label {
margin-bottom: 5px; margin-bottom: 5px;
} }
input:not(.pure-button) {
background-color: var(--color-background-new-watch-input);
color: var(--color-text-new-watch-input);
}
.label { .label {
display: none; display: none;
} }
legend { legend {
color: var(--color-text-legend); color: #fff;
font-weight: bold; font-weight: bold;
} }
#watch-add-wrapper-zone { #watch-add-wrapper-zone {
>div { > div {
display: inline-block; display: inline-block;
} }
@media only screen and (max-width: 760px) { @media only screen and (max-width: 760px) {
#url { #url {
width: 100%; width: 100%;
@@ -399,15 +300,15 @@ label {
position: fixed; position: fixed;
left: 0px; left: 0px;
top: 120px; top: 120px;
background: var(--color-background); background: #fff;
padding: 10px; padding: 10px;
border-top-right-radius: 5px; border-top-right-radius: 5px;
border-bottom-right-radius: 5px; border-bottom-right-radius: 5px;
box-shadow: 1px 1px 4px var(--color-shadow-jump); box-shadow: 5px 0 5px -2px #888;
a { a {
color: var(--color-link); color: #1b98f8;
cursor: pointer; cursor: grabbing;
-moz-user-select: none; -moz-user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
-ms-user-select: none; -ms-user-select: none;
@@ -418,8 +319,8 @@ label {
footer { footer {
padding: 10px; padding: 10px;
background: var(--color-background); background: #fff;
color: var(--color-text-footer); color: #444;
text-align: center; text-align: center;
} }
@@ -442,7 +343,7 @@ footer {
position: absolute; position: absolute;
top: 60px; top: 60px;
font-size: 65%; font-size: 65%;
background: var(--color-background); background: #fff;
padding: 10px; padding: 10px;
&#left-sticky { &#left-sticky {
@@ -461,12 +362,10 @@ footer {
} }
#new-version-text a { #new-version-text a {
color: var(--color-link-new-version); color: #e07171;
} }
.watch-controls { .watch-controls {
color: #f8321b;
.state-on { .state-on {
img { img {
opacity: 0.8; opacity: 0.8;
@@ -484,6 +383,7 @@ footer {
opacity: 0.8; opacity: 0.8;
} }
} }
} }
.monospaced-textarea { .monospaced-textarea {
@@ -492,8 +392,7 @@ footer {
font-family: monospace; font-family: monospace;
white-space: pre; white-space: pre;
overflow-wrap: normal; overflow-wrap: normal;
// No scrollbars until needed. overflow-x: scroll;
overflow-x: auto;
} }
} }
@@ -508,9 +407,7 @@ footer {
} }
} }
.pure-control-group, .pure-control-group, .pure-group, .pure-controls {
.pure-group,
.pure-controls {
padding-bottom: 1em; padding-bottom: 1em;
div { div {
@@ -518,32 +415,28 @@ footer {
} }
.checkbox { .checkbox {
>* { > * {
display: inline; display: inline;
vertical-align: middle; vertical-align: middle;
} }
>label { > label {
padding-left: 5px; padding-left: 5px;
} }
} }
legend {
color: var(--color-text-legend);
}
} }
/* The input fields with errors */ /* The input fields with errors */
.error { .error {
input { input {
background-color: var(--color-error-input); background-color: #ffebeb;
} }
} }
/* The list of errors */ /* The list of errors */
ul.errors { ul.errors {
padding: .5em .6em; padding: .5em .6em;
border: 1px solid var(--color-error-list); border: 1px solid #dd0000;
border-radius: 4px; border-radius: 4px;
vertical-align: middle; vertical-align: middle;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
@@ -551,7 +444,7 @@ footer {
li { li {
margin-left: 1em; margin-left: 1em;
color: var(--color-error-list); color: #dd0000;
} }
} }
@@ -569,7 +462,7 @@ footer {
list-style: none; list-style: none;
li { li {
>* { > * {
display: inline-block; display: inline-block;
} }
} }
@@ -578,25 +471,21 @@ footer {
} }
@media only screen and (max-width: 760px), @media only screen and (max-width: 760px), (min-device-width: 768px) and (max-device-width: 1024px) {
(min-device-width: 768px) and (max-device-width: 1024px) {
.box { .box {
max-width: 95% max-width: 95%
} }
.edit-form { .edit-form {
padding: 0.5em; padding: 0.5em;
margin: 0; margin: 0;
} }
#nav-menu { #nav-menu {
overflow-x: scroll; overflow-x: scroll;
} }
} }
@media only screen and (max-width: 760px), @media only screen and (max-width: 760px), (min-device-width: 768px) and (max-device-width: 800px) {
(min-device-width: 768px) and (max-device-width: 800px) {
div.sticky-tab#hosted-sticky { div.sticky-tab#hosted-sticky {
top: 60px; top: 60px;
@@ -625,29 +514,24 @@ footer {
and also iPads specifically. and also iPads specifically.
*/ */
.watch-table { .watch-table {
/* Force table to not be like tables anymore */ /* Force table to not be like tables anymore */
thead, thead, tbody, th, td, tr {
tbody,
th,
td,
tr {
display: block; display: block;
} }
.last-checked { .last-checked {
>span { > span {
vertical-align: middle; vertical-align: middle;
} }
} }
.last-checked::before { .last-checked::before {
color: var(--color-last-checked); color: #555;
content: "Last Checked "; content: "Last Checked ";
} }
.last-changed::before { .last-changed::before {
color: var(--color-last-checked); color: #555;
content: "Last Changed "; content: "Last Changed ";
} }
@@ -663,17 +547,15 @@ footer {
left: -9999px; left: -9999px;
} }
.pure-table td, .pure-table td, .pure-table th {
.pure-table th {
border: none; border: none;
} }
td { td {
/* Behave like a "row" */ /* Behave like a "row" */
border: none; border: none;
border-bottom: 1px solid var(--color-border-watch-table-cell); border-bottom: 1px solid #eee;
vertical-align: middle; vertical-align: middle;
&:before { &:before {
/* Top/left values mimic padding */ /* Top/left values mimic padding */
top: 6px; top: 6px;
@@ -686,11 +568,11 @@ footer {
&.pure-table-striped { &.pure-table-striped {
tr { tr {
background-color: var(--color-table-background); background-color: #fff;
} }
tr:nth-child(2n-1) { tr:nth-child(2n-1) {
background-color: var(--color-table-stripe); background-color: #eee;
} }
tr:nth-child(2n-1) td { tr:nth-child(2n-1) td {
@@ -701,66 +583,12 @@ footer {
} }
} }
.pure-table {
border-color: var(--color-border-table-cell);
thead {
background-color: var(--color-background-table-thead);
color: var(--color-text);
}
td,
th {
border-left-color: var(--color-border-table-cell);
}
}
.pure-table-striped {
tr:nth-child(2n-1) {
td {
background-color: var(--color-table-stripe);
}
}
}
.pure-form input[type=color],
.pure-form input[type=date],
.pure-form input[type=datetime-local],
.pure-form input[type=datetime],
.pure-form input[type=email],
.pure-form input[type=month],
.pure-form input[type=number],
.pure-form input[type=password],
.pure-form input[type=search],
.pure-form input[type=tel],
.pure-form input[type=text],
.pure-form input[type=time],
.pure-form input[type=url],
.pure-form input[type=week],
.pure-form select,
.pure-form textarea {
border: var(--color-border-input);
box-shadow: inset 0 1px 3px var(--color-shadow-input);
background-color: var(--color-background-input);
color: var(--color-text-input);
&:active {
background-color: var(--color-background-input);
}
}
input::placeholder,
textarea::placeholder {
color: var(--color-text-input-placeholder);
}
/** Desktop vs mobile input field strategy /** Desktop vs mobile input field strategy
- We dont use 'size' with <input> because `size` is too unreliable to override, and will often push-out - We dont use 'size' with <input> because `size` is too unreliable to override, and will often push-out
- Rely always on width in CSS - Rely always on width in CSS
*/ */
@media only screen and (min-width: 761px) { @media only screen and (min-width: 761px) {
/* m-d is medium-desktop */ /* m-d is medium-desktop */
.m-d { .m-d {
min-width: 80%; min-width: 80%;
@@ -777,23 +605,16 @@ textarea::placeholder {
li { li {
margin-right: 3px; margin-right: 3px;
display: inline-block; display: inline-block;
color: var(--color-text-tab); color: #fff;
border-top-left-radius: 5px; border-top-left-radius: 5px;
border-top-right-radius: 5px; border-top-right-radius: 5px;
background-color: var(--color-background-tab); background-color: rgba(255, 255, 255, 0.2);
&:not(.active) { &.active, :target {
&:hover { background-color: #fff;
background-color: var(--color-background-tab-hover);
}
}
&.active,
:target {
background-color: var(--color-background);
a { a {
color: var(--color-text-tab-active); color: #222;
font-weight: bold; font-weight: bold;
} }
} }
@@ -801,24 +622,22 @@ textarea::placeholder {
a { a {
display: block; display: block;
padding: 0.8em; padding: 0.8em;
color: var(--color-text-tab); color: #fff;
} }
} }
} }
} }
$form-edge-padding: 20px; $form-edge-padding: 20px;
.pure-form-stacked { .pure-form-stacked {
>div:first-child { > div:first-child {
display: block; display: block;
} }
} }
.login-form { .login-form {
.inner { .inner {
background: var(--color-background); background: #fff;;
;
padding: $form-edge-padding; padding: $form-edge-padding;
border-radius: 5px; border-radius: 5px;
} }
@@ -848,13 +667,11 @@ $form-edge-padding: 20px;
#selector-header { #selector-header {
padding-bottom: 1em; padding-bottom: 1em;
} }
body.full-width { body.full-width {
.edit-form { .edit-form {
width: 95%; width: 95%;
} }
} }
.edit-form { .edit-form {
min-width: 70%; min-width: 70%;
/* so it cant overflow */ /* so it cant overflow */
@@ -865,21 +682,17 @@ body.full-width {
} }
.inner { .inner {
background: var(--color-background); background: #fff;;
padding: $form-edge-padding; padding: $form-edge-padding;
} }
#actions { #actions {
display: block; display: block;
background: var(--color-background); background: #fff;
} }
.pure-form-message-inline { .pure-form-message-inline {
padding-left: 0; padding-left: 0;
color: var(--color-text-input-description);
code {
font-size: .875em;
}
} }
} }
@@ -903,15 +716,14 @@ ul {
height: 100%; height: 100%;
overflow-y: scroll; overflow-y: scroll;
position: relative; position: relative;
//width: 100%; //width: 100%;
>img { > img {
position: absolute; position: absolute;
z-index: 4; z-index: 4;
max-width: 100%; max-width: 100%;
} }
>canvas { > canvas {
position: relative; position: relative;
z-index: 5; z-index: 5;
max-width: 100%; max-width: 100%;
@@ -939,61 +751,54 @@ ul {
} }
#api-key-copy { #api-key-copy {
color: var(--color-api-key); color: #0078e7;
} }
.button-green { .button-green {
background-color: var(--color-background-button-green); background-color: #42dd53;
} }
.button-red { .button-red {
background-color: var(--color-background-button-red); background-color: #dd4242;
} }
.noselect { .noselect {
-webkit-touch-callout: none; -webkit-touch-callout: none; /* iOS Safari */
/* iOS Safari */ -webkit-user-select: none; /* Safari */
-webkit-user-select: none; -moz-user-select: none; /* Old versions of Firefox */
/* Safari */ -ms-user-select: none; /* Internet Explorer/Edge */
-moz-user-select: none;
/* Old versions of Firefox */
-ms-user-select: none;
/* Internet Explorer/Edge */
user-select: none; user-select: none;
/* Non-prefixed version, currently /* Non-prefixed version, currently
supported by Chrome, Edge, Opera and Firefox */ supported by Chrome, Edge, Opera and Firefox */
} }
.snapshot-age { .snapshot-age {
padding: 4px; padding: 4px;
margin: 0.5rem 0; background-color: #dfdfdf;
background-color: var(--color-background-snapshot-age);
border-radius: 3px; border-radius: 3px;
font-weight: bold; font-weight: bold;
margin-bottom: 4px; margin-bottom: 4px;
&.error { &.error {
background-color: var(--color-error-background-snapshot-age); background-color: #ff0000;
color: var(--color-error-text-snapshot-age); color: #fff;
} }
} }
#checkbox-operations { #checkbox-operations {
background: var(--color-background-checkbox-operations); background: rgba(0, 0, 0, 0.05);
padding: 1em; padding: 1em;
border-radius: 10px; border-radius: 10px;
margin-bottom: 1em; margin-bottom: 1em;
display: none; display: none;
} }
.checkbox-uuid { .checkbox-uuid {
>* { > * {
vertical-align: middle; vertical-align: middle;
} }
} }
.inline-warning { .inline-warning {
>span { > span {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
} }
@@ -1004,35 +809,8 @@ ul {
vertical-align: middle; vertical-align: middle;
} }
border: 1px solid var(--color-border-warning); border: 1px solid #ff3300;
padding: 0.5rem; padding: 0.5rem;
border-radius: 5px; border-radius: 5px;
color: var(--color-warning); color: #ff3300;
} }
/* automatic price following helpers */
.tracking-ldjson-price-data {
background-color: var(--color-background-button-green);
color: #000;
padding: 3px;
border-radius: 3px;
white-space: nowrap;
}
.ldjson-price-track-offer {
a.pure-button {
border-radius: 3px;
padding: 3px;
background-color: var(--color-background-button-green);
}
font-weight: bold;
font-style: italic;
}
.price-follow-tag-icon {
display: inline-block;
height: 0.8rem;
vertical-align: middle;
}

View File

@@ -250,15 +250,12 @@ class ChangeDetectionStore:
def clear_watch_history(self, uuid): def clear_watch_history(self, uuid):
import pathlib import pathlib
self.__data['watching'][uuid].update({ self.__data['watching'][uuid].update(
'last_checked': 0, {'last_checked': 0,
'has_ldjson_price_data': None, 'last_viewed': 0,
'last_error': False, 'previous_md5': False,
'last_notification_error': False, 'last_notification_error': False,
'last_viewed': 0, 'last_error': False})
'previous_md5': False,
'track_ldjson_price_data': None,
})
# JSON Data, Screenshots, Textfiles (history index and snapshots), HTML in the future etc # JSON Data, Screenshots, Textfiles (history index and snapshots), HTML in the future etc
for item in pathlib.Path(os.path.join(self.datastore_path, uuid)).rglob("*.*"): for item in pathlib.Path(os.path.join(self.datastore_path, uuid)).rglob("*.*"):
@@ -292,7 +289,6 @@ class ChangeDetectionStore:
# List of permissible attributes we accept from the wild internet # List of permissible attributes we accept from the wild internet
for k in [ for k in [
'body', 'body',
'browser_steps',
'css_filter', 'css_filter',
'extract_text', 'extract_text',
'extract_title_as_title', 'extract_title_as_title',
@@ -624,44 +620,4 @@ class ChangeDetectionStore:
watch['include_filters'] = [existing_filter] watch['include_filters'] = [existing_filter]
except: except:
continue continue
return return
# Convert old static notification tokens to jinja2 tokens
def update_9(self):
# Each watch
import re
# only { } not {{ or }}
r = r'(?<!{){(?!{)(\w+)(?<!})}(?!})'
for uuid, watch in self.data['watching'].items():
try:
n_body = watch.get('notification_body', '')
if n_body:
watch['notification_body'] = re.sub(r, r'{{\1}}', n_body)
n_title = watch.get('notification_title')
if n_title:
watch['notification_title'] = re.sub(r, r'{{\1}}', n_title)
n_urls = watch.get('notification_urls')
if n_urls:
for i, url in enumerate(n_urls):
watch['notification_urls'][i] = re.sub(r, r'{{\1}}', url)
except:
continue
# System wide
n_body = self.data['settings']['application'].get('notification_body')
if n_body:
self.data['settings']['application']['notification_body'] = re.sub(r, r'{{\1}}', n_body)
n_title = self.data['settings']['application'].get('notification_title')
if n_body:
self.data['settings']['application']['notification_title'] = re.sub(r, r'{{\1}}', n_title)
n_urls = self.data['settings']['application'].get('notification_urls')
if n_urls:
for i, url in enumerate(n_urls):
self.data['settings']['application']['notification_urls'][i] = re.sub(r, r'{{\1}}', url)
return

View File

@@ -16,16 +16,14 @@
<li><code>discord://</code> only supports a maximum <strong>2,000 characters</strong> of notification text, including the title.</li> <li><code>discord://</code> only supports a maximum <strong>2,000 characters</strong> of notification text, including the title.</li>
<li><code>tgram://</code> bots cant send messages to other bots, so you should specify chat ID of non-bot user.</li> <li><code>tgram://</code> bots cant send messages to other bots, so you should specify chat ID of non-bot user.</li>
<li><code>tgram://</code> only supports very limited HTML and can fail when extra tags are sent, <a href="https://core.telegram.org/bots/api#html-style">read more here</a> (or use plaintext/markdown format)</li> <li><code>tgram://</code> only supports very limited HTML and can fail when extra tags are sent, <a href="https://core.telegram.org/bots/api#html-style">read more here</a> (or use plaintext/markdown format)</li>
<li><code>gets://</code>, <code>posts://</code>, <code>puts://</code>, <code>deletes://</code> for direct API calls (or omit the "<code>s</code>" for non-SSL ie <code>get://</code>)</li>
</ul> </ul>
</div> </div>
<div class="notifications-wrapper"> <br/>
<a id="send-test-notification" class="pure-button button-secondary button-xsmall" style="font-size: 70%">Send test notification</a> <a id="send-test-notification" class="pure-button button-secondary button-xsmall" style="font-size: 70%">Send test notification</a>
{% if emailprefix %} {% if emailprefix %}
<a id="add-email-helper" class="pure-button button-secondary button-xsmall" style="font-size: 70%">Add email</a> <a id="add-email-helper" class="pure-button button-secondary button-xsmall" style="font-size: 70%">Add email</a>
{% endif %} {% endif %}
<a href="{{url_for('notification_logs')}}" class="pure-button button-secondary button-xsmall" style="font-size: 70%">Notification debug logs</a> <a href="{{url_for('notification_logs')}}" class="pure-button button-secondary button-xsmall" style="font-size: 70%">Notification debug logs</a>
</div>
</div> </div>
<div id="notification-customisation" class="pure-control-group"> <div id="notification-customisation" class="pure-control-group">
<div class="pure-control-group"> <div class="pure-control-group">
@@ -42,9 +40,8 @@
<span class="pure-form-message-inline">Format for all notifications</span> <span class="pure-form-message-inline">Format for all notifications</span>
</div> </div>
<div class="pure-controls"> <div class="pure-controls">
<p class="pure-form-message-inline"> <span class="pure-form-message-inline">
You can use <a target="_new" href="https://jinja.palletsprojects.com/en/3.0.x/templates/">Jinja2</a> templating in the notification title, body and URL. These tokens can be used in the notification body and title to customise the notification text.
</p>
<table class="pure-table" id="token-table"> <table class="pure-table" id="token-table">
<thead> <thead>
@@ -55,49 +52,52 @@
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td><code>{{ '{{ base_url }}' }}</code></td> <td><code>{base_url}</code></td>
<td>The URL of the changedetection.io instance you are running.</td> <td>The URL of the changedetection.io instance you are running.</td>
</tr> </tr>
<tr> <tr>
<td><code>{{ '{{ watch_url }}' }}</code></td> <td><code>{watch_url}</code></td>
<td>The URL being watched.</td> <td>The URL being watched.</td>
</tr> </tr>
<tr> <tr>
<td><code>{{ '{{ watch_uuid }}' }}</code></td> <td><code>{watch_uuid}</code></td>
<td>The UUID of the watch.</td> <td>The UUID of the watch.</td>
</tr> </tr>
<tr> <tr>
<td><code>{{ '{{ watch_title }}' }}</code></td> <td><code>{watch_title}</code></td>
<td>The title of the watch.</td> <td>The title of the watch.</td>
</tr> </tr>
<tr> <tr>
<td><code>{{ '{{ watch_tag }}' }}</code></td> <td><code>{watch_tag}</code></td>
<td>The watch label / tag</td> <td>The tag of the watch.</td>
</tr> </tr>
<tr> <tr>
<td><code>{{ '{{ preview_url }}' }}</code></td> <td><code>{preview_url}</code></td>
<td>The URL of the preview page generated by changedetection.io.</td> <td>The URL of the preview page generated by changedetection.io.</td>
</tr> </tr>
<tr> <tr>
<td><code>{{ '{{ diff_url }}' }}</code></td> <td><code>{diff}</code></td>
<td>The diff output - differences only</td> <td>The diff output - differences only</td>
</tr> </tr>
<tr> <tr>
<td><code>{{ '{{ diff_full }}' }}</code></td> <td><code>{diff_full}</code></td>
<td>The diff output - full difference output</td> <td>The diff output - full difference output</td>
</tr> </tr>
<tr> <tr>
<td><code>{{ '{{ current_snapshot }}' }}</code></td> <td><code>{diff_url}</code></td>
<td>The URL of the diff page generated by changedetection.io.</td>
</tr>
<tr>
<td><code>{current_snapshot}</code></td>
<td>The current snapshot value, useful when combined with JSON or CSS filters <td>The current snapshot value, useful when combined with JSON or CSS filters
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="pure-form-message-inline"> <br/>
<br> URLs generated by changedetection.io (such as <code>{diff_url}</code>) require the <code>BASE_URL</code> environment variable set.<br/>
URLs generated by changedetection.io (such as <code>{{ '{{ diff_url }}' }}</code>) require the <code>BASE_URL</code> environment variable set.<br/> Your <code>BASE_URL</code> var is currently "{{settings_application['current_base_url']}}"
Your <code>BASE_URL</code> var is currently "{{settings_application['current_base_url']}}" </span>
</div>
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}

View File

@@ -1,152 +1,110 @@
<!DOCTYPE html> <!doctype html>
<html lang="en" data-darkmode="{{ get_darkmode_state() }}"> <html lang="en">
<head>
<head> <meta charset="utf-8">
<meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="description" content="Self hosted website change detection.">
<meta name="description" content="Self hosted website change detection."/>
<title>Change Detection{{extra_title}}</title> <title>Change Detection{{extra_title}}</title>
<link rel="alternate" type="application/rss+xml" title="Changedetection.io » Feed{% if active_tag %}- {{active_tag}}{% endif %}" href="{{ url_for('rss', tag=active_tag , token=app_rss_token)}}"/> <link rel="alternate" type="application/rss+xml" title="Changedetection.io » Feed{% if active_tag %}- {{active_tag}}{% endif %}" href="{{ url_for('rss', tag=active_tag , token=app_rss_token)}}" />
<link rel="stylesheet" href="{{url_for('static_content', group='styles', filename='pure-min.css')}}"/> <link rel="stylesheet" href="{{url_for('static_content', group='styles', filename='pure-min.css')}}">
<link rel="stylesheet" href="{{url_for('static_content', group='styles', filename='styles.css')}}"/> <link rel="stylesheet" href="{{url_for('static_content', group='styles', filename='styles.css')}}">
{% if extra_stylesheets %} {% if extra_stylesheets %}
{% for m in extra_stylesheets %} {% for m in extra_stylesheets %}
<link rel="stylesheet" href="{{ m }}?ver=1000"/> <link rel="stylesheet" href="{{ m }}?ver=1000">
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<link rel="apple-touch-icon" sizes="180x180" href="{{url_for('static_content', group='favicons', filename='apple-touch-icon.png')}}"/>
<link rel="icon" type="image/png" sizes="32x32" href="{{url_for('static_content', group='favicons', filename='favicon-32x32.png')}}"/>
<link rel="icon" type="image/png" sizes="16x16" href="{{url_for('static_content', group='favicons', filename='favicon-16x16.png')}}"/>
<link rel="manifest" href="{{url_for('static_content', group='favicons', filename='site.webmanifest')}}"/>
<link rel="mask-icon" href="{{url_for('static_content', group='favicons', filename='safari-pinned-tab.svg')}}" color="#5bbad5"/>
<link rel="shortcut icon" href="{{url_for('static_content', group='favicons', filename='favicon.ico')}}"/>
<meta name="msapplication-TileColor" content="#da532c"/>
<meta name="msapplication-config" content="favicons/browserconfig.xml"/>
<meta name="theme-color" content="#ffffff"/>
<style> <style>
body::before { body::before {
background-image: url({{url_for('static_content', group='images', filename='gradient-border.png') }}); background-image: url({{url_for('static_content', group='images', filename='gradient-border.png')}});
} }
</style> </style>
<script type="text/javascript" src="{{url_for('static_content', group='js', filename='jquery-3.6.0.min.js')}}"></script> <script type="text/javascript" src="{{url_for('static_content', group='js', filename='jquery-3.6.0.min.js')}}"></script>
</head>
<body> </head>
<div class="header">
<div class="home-menu pure-menu pure-menu-horizontal pure-menu-fixed" id="nav-menu"> <body>
<div class="header">
<div class="home-menu pure-menu pure-menu-horizontal pure-menu-fixed" id="nav-menu">
{% if has_password and not current_user.is_authenticated %} {% if has_password and not current_user.is_authenticated %}
<a class="pure-menu-heading" href="https://github.com/dgtlmoon/changedetection.io" rel="noopener"> <a class="pure-menu-heading" href="https://github.com/dgtlmoon/changedetection.io" rel="noopener"><strong>Change</strong>Detection.io</a>
<strong>Change</strong>Detection.io</a>
{% else %} {% else %}
<a class="pure-menu-heading" href="{{url_for('index')}}"> <a class="pure-menu-heading" href="{{url_for('index')}}"><strong>Change</strong>Detection.io</a>
<strong>Change</strong>Detection.io</a>
{% endif %} {% endif %}
{% if current_diff_url %} {% if current_diff_url %}
<a class="current-diff-url" href="{{ current_diff_url }}"> <a class=current-diff-url href="{{ current_diff_url }}"><span style="max-width: 30%; overflow: hidden;">{{ current_diff_url }}</span></a>
<span style="max-width: 30%; overflow: hidden">{{ current_diff_url }}</span></a>
{% else %} {% else %}
{% if new_version_available and not(has_password and not current_user.is_authenticated) %} {% if new_version_available and not (has_password and not current_user.is_authenticated) %}
<span id="new-version-text" class="pure-menu-heading"> <span id="new-version-text" class="pure-menu-heading"><a href="https://github.com/dgtlmoon/changedetection.io">A new version is available</a></span>
<a href="https://github.com/dgtlmoon/changedetection.io">A new version is available</a> {% endif %}
</span>
{% endif %}
{% endif %} {% endif %}
<ul class="pure-menu-list" id="top-right-menu"> <ul class="pure-menu-list" id="top-right-menu">
{% if current_user.is_authenticated or not has_password %} {% if current_user.is_authenticated or not has_password %}
{% if not {% if not current_diff_url %}
current_diff_url %} <li class="pure-menu-item">
<li class="pure-menu-item">
<a href="{{ url_for('settings_page')}}" class="pure-menu-link">SETTINGS</a> <a href="{{ url_for('settings_page')}}" class="pure-menu-link">SETTINGS</a>
</li> </li>
<li class="pure-menu-item"> <li class="pure-menu-item">
<a href="{{ url_for('import_page')}}" class="pure-menu-link">IMPORT</a> <a href="{{ url_for('import_page')}}" class="pure-menu-link">IMPORT</a>
</li> </li>
<li class="pure-menu-item"> <li class="pure-menu-item">
<a href="{{ url_for('get_backup')}}" class="pure-menu-link">BACKUP</a> <a href="{{ url_for('get_backup')}}" class="pure-menu-link">BACKUP</a>
</li> </li>
{% else %} {% else %}
<li class="pure-menu-item"> <li class="pure-menu-item">
<a href="{{ url_for('edit_page', uuid=uuid, next='diff') }}" class="pure-menu-link">EDIT</a> <a href="{{ url_for('edit_page', uuid=uuid, next='diff') }}" class="pure-menu-link">EDIT</a>
</li>
{% endif %}
{% else %}
<li class="pure-menu-item">
<a class="pure-menu-link" href="https://github.com/dgtlmoon/changedetection.io">Website Change Detection and Notification.</a>
</li> </li>
{% endif %}
{% if current_user.is_authenticated %}
<li class="pure-menu-item">
<a href="{{url_for('logout')}}" class="pure-menu-link">LOG OUT</a>
</li>
{% endif %}
<li class="pure-menu-item">
{% if dark_mode %}
{% set darkClass = 'dark' %}
{% endif %} {% endif %}
<button class="toggle-theme {{darkClass}}" type="button" title="Toggle Light/Dark Mode"> {% else %}
<span class="visually-hidden">Toggle light/dark mode</span> <li class="pure-menu-item">
<span class="icon-light"> <a class="pure-menu-link" href="https://github.com/dgtlmoon/changedetection.io">Website Change Detection and Notification.</a>
{% include "svgs/light-mode-toggle-icon.svg" %} </li>
</span> {% endif %}
<span class="icon-dark">
{% include "svgs/dark-mode-toggle-icon.svg" %} {% if current_user.is_authenticated %}
</span> <li class="pure-menu-item"><a href="{{url_for('logout')}}" class="pure-menu-link">LOG OUT</a></li>
</button> {% endif %}
</li> <li class="pure-menu-item"><a class="github-link" href="https://github.com/dgtlmoon/changedetection.io">
<li class="pure-menu-item"> <svg class="octicon octicon-mark-github v-align-middle" height="32" viewBox="0 0 16 16"
<a class="github-link" href="https://github.com/dgtlmoon/changedetection.io"> version="1.1"
{% include "svgs/github.svg" %} width="32" aria-hidden="true">
</a> <path fill-rule="evenodd"
</li> d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path>
</svg>
</a></li>
</ul> </ul>
</div>
</div> </div>
{% if hosted_sticky %} </div>
<div class="sticky-tab" id="hosted-sticky"> {% if hosted_sticky %}<div class="sticky-tab" id="hosted-sticky"><a href="https://lemonade.changedetection.io/start?ref={{guid}}">Let us host your instance!</a></div>{% endif %}
<a href="https://lemonade.changedetection.io/start?ref={{guid}}">Let us host your instance!</a> {% if left_sticky %}<div class="sticky-tab" id="left-sticky"><a href="{{url_for('preview_page', uuid=uuid)}}">Show current snapshot</a></div> {% endif %}
</div> {% if right_sticky %}<div class="sticky-tab" id="right-sticky">{{ right_sticky }}</div> {% endif %}
{% endif %} <section class="content">
{% if left_sticky %} <header>
<div class="sticky-tab" id="left-sticky">
<a href="{{url_for('preview_page', uuid=uuid)}}">Show current snapshot</a>
</div>
{% endif %}
{% if right_sticky %}
<div class="sticky-tab" id="right-sticky">{{ right_sticky }}</div>
{% endif %}
<section class="content">
<header>
{% block header %}{% endblock %} {% block header %}{% endblock %}
</header> </header>
{% with messages = get_flashed_messages(with_categories = true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if {% if messages %}
messages %} <ul class=messages>
<ul class="messages"> {% for category, message in messages %}
{% for category, message in messages %} <li class="{{ category }}">{{ message }}</li>
<li class="{{ category }}">{{ message }}</li> {% endfor %}
{% endfor %}
</ul> </ul>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% if session['share-link'] %}
{% if session['share-link'] %}
<ul class="messages with-share-link"> <ul class="messages with-share-link">
<li class="message"> <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>
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> </ul>
{% endif %} {% endif %}
{% block content %}{% endblock %}
</section>
<script
type="text/javascript"
src="{{url_for('static_content', group='js', filename='toggle-theme.js')}}"
defer></script>
</body>
{% block content %}
{% endblock %}
</section>
</body>
</html> </html>

View File

@@ -1,49 +1,32 @@
{% extends 'base.html' %} {% block content %} {% extends 'base.html' %}
{% block content %}
<div class="edit-form"> <div class="edit-form">
<div class="box-wrap inner"> <div class="box-wrap inner">
<form <form class="pure-form pure-form-stacked" action="{{url_for('clear_all_history')}}" method="POST">
class="pure-form pure-form-stacked" <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
action="{{url_for('clear_all_history')}}" <fieldset>
method="POST" <div class="pure-control-group">
> This will remove version history (snapshots) for ALL watches, but keep your list of URLs! <br/>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> You may like to use the <strong>BACKUP</strong> link first.<br/>
<fieldset> </div>
<div class="pure-control-group"> <br/>
This will remove version history (snapshots) for ALL watches, but keep <div class="pure-control-group">
your list of URLs! <br /> <label for="confirmtext">Confirmation text</label>
You may like to use the <strong>BACKUP</strong> link first.<br /> <input type="text" id="confirmtext" required="" name="confirmtext" value="" size="10"/>
</div> <span class="pure-form-message-inline">Type in the word <strong>clear</strong> to confirm that you understand.</span>
<br /> </div>
<div class="pure-control-group"> <br/>
<label for="confirmtext">Confirmation text</label> <div class="pure-control-group">
<input <button type="submit" class="pure-button pure-button-primary">Clear History!</button>
type="text" </div>
id="confirmtext" <br/>
required="" <div class="pure-control-group">
name="confirmtext" <a href="{{url_for('index')}}" class="pure-button button-small button-cancel">Cancel</a>
value="" </div>
size="10" </fieldset>
/>
<span class="pure-form-message-inline"
>Type in the word <strong>clear</strong> to confirm that you
understand.</span
>
</div>
<br />
<div class="pure-control-group">
<button type="submit" class="pure-button pure-button-primary">
Clear History!
</button>
</div>
<br />
<div class="pure-control-group">
<a href="{{url_for('index')}}" class="pure-button button-cancel"
>Cancel</a
>
</div>
</fieldset>
</form> </form>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,5 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %}
{% block content %} {% block content %}
<script> <script>
const screenshot_url="{{url_for('static_content', group='screenshot', filename=uuid)}}"; const screenshot_url="{{url_for('static_content', group='screenshot', filename=uuid)}}";
@@ -58,7 +58,6 @@
{% if last_error_screenshot %}<li class="tab" id="error-screenshot-tab"><a href="#error-screenshot">Error Screenshot</a></li> {% endif %} {% if last_error_screenshot %}<li class="tab" id="error-screenshot-tab"><a href="#error-screenshot">Error Screenshot</a></li> {% endif %}
<li class="tab" id=""><a href="#text">Text</a></li> <li class="tab" id=""><a href="#text">Text</a></li>
<li class="tab" id="screenshot-tab"><a href="#screenshot">Screenshot</a></li> <li class="tab" id="screenshot-tab"><a href="#screenshot">Screenshot</a></li>
<li class="tab" id="extract-tab"><a href="#extract">Extract Data</a></li>
</ul> </ul>
</div> </div>
@@ -109,37 +108,6 @@
<strong>Screenshot requires Playwright/WebDriver enabled</strong> <strong>Screenshot requires Playwright/WebDriver enabled</strong>
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane-inner" id="extract">
<form id="extract-data-form" class="pure-form pure-form-stacked edit-form"
action="{{ url_for('diff_history_page', uuid=uuid) }}#extract"
method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<p>This tool will extract text data from all of the watch history.</p>
<div class="pure-control-group">
{{ render_field(extract_form.extract_regex) }}
<span class="pure-form-message-inline">
A <strong>RegEx</strong> is a pattern that identifies exactly which part inside of the text that you want to extract.<br/>
<p>
For example, to extract only the numbers from text &dash;</br>
<strong>Raw text</strong>: <code>Temperature <span style="color: red">5.5</span>°C in Sydney</code></br>
<strong>RegEx to extract:</strong> <code>Temperature <span style="color: red">([0-9\.]+)</span></code><br/>
</p>
<p>
<a href="https://RegExr.com/">Be sure to test your RegEx here.</a>
</p>
<p>
Each RegEx group bracket <code>()</code> will be in its own column, the first column value is always the date.
</p>
</span>
</div>
<div class="pure-control-group">
{{ render_button(extract_form.extract_submit_button) }}
</div>
</form>
</div>
</div> </div>
<script> <script>

View File

@@ -166,12 +166,8 @@ User-Agent: wonderbra 1.0") }}
<div id="browser-steps-ui" class="noselect" style="width: 100%; background-color: #eee; border-radius: 5px;"> <div id="browser-steps-ui" class="noselect" style="width: 100%; background-color: #eee; border-radius: 5px;">
<div class="noselect" id="browsersteps-selector-wrapper" style="width: 100%"> <div class="noselect" id="browsersteps-selector-wrapper" style="width: 100%">
<span class="loader" > <span class="loader">
<span id="browsersteps-click-start"> <div class="spinner"></div>
<h2 >Click here to Start</h2>
Please allow 10-15 seconds for the browser to connect.
</span>
<div class="spinner" style="display: none;"></div>
</span> </span>
<img class="noselect" id="browsersteps-img" src="" style="max-width: 100%; width: 100%;" /> <img class="noselect" id="browsersteps-img" src="" style="max-width: 100%; width: 100%;" />
<canvas class="noselect" id="browsersteps-selector-canvas" style="max-width: 100%; width: 100%;"></canvas> <canvas class="noselect" id="browsersteps-selector-canvas" style="max-width: 100%; width: 100%;"></canvas>
@@ -244,7 +240,6 @@ xpath://body/div/span[contains(@class, 'example-class')]",
<span class="pure-form-message-inline"><strong>Note!: //text() function does not work where the &lt;element&gt; contains &lt;![CDATA[]]&gt;</strong></span><br/> <span class="pure-form-message-inline"><strong>Note!: //text() function does not work where the &lt;element&gt; contains &lt;![CDATA[]]&gt;</strong></span><br/>
{% endif %} {% endif %}
<span class="pure-form-message-inline">One rule per line, <i>any</i> rules that matches will be used.<br/> <span class="pure-form-message-inline">One rule per line, <i>any</i> rules that matches will be used.<br/>
<ul> <ul>
<li>CSS - Limit text to this CSS rule, only text matching this CSS rule is included.</li> <li>CSS - Limit text to this CSS rule, only text matching this CSS rule is included.</li>
<li>JSON - Limit text to this JSON rule, using either <a href="https://pypi.org/project/jsonpath-ng/" target="new">JSONPath</a> or <a href="https://stedolan.github.io/jq/" target="new">jq</a> (if installed). <li>JSON - Limit text to this JSON rule, using either <a href="https://pypi.org/project/jsonpath-ng/" target="new">JSONPath</a> or <a href="https://stedolan.github.io/jq/" target="new">jq</a> (if installed).

View File

@@ -60,7 +60,7 @@
{{ render_field(form.application.form.base_url, placeholder="http://yoursite.com:5000/", {{ render_field(form.application.form.base_url, placeholder="http://yoursite.com:5000/",
class="m-d") }} class="m-d") }}
<span class="pure-form-message-inline"> <span class="pure-form-message-inline">
Base URL used for the <code>{{ '{{ base_url }}' }}</code> token in notifications and RSS links.<br/>Default value is the ENV var 'BASE_URL' (Currently "{{settings_application['current_base_url']}}"), Base URL used for the <code>{base_url}</code> token in notifications and RSS links.<br/>Default value is the ENV var 'BASE_URL' (Currently "{{settings_application['current_base_url']}}"),
<a href="https://github.com/dgtlmoon/changedetection.io/wiki/Configurable-BASE_URL-setting">read more here</a>. <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Configurable-BASE_URL-setting">read more here</a>.
</span> </span>
</div> </div>

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 512 262.86"><path fill-rule="nonzero" d="M316.78 16.55h-205.9c-30.5 0-58.22 12.48-78.31 32.57C12.47 69.21 0 96.93 0 127.44c0 30.5 12.47 58.22 32.57 78.31 20.09 20.1 47.81 32.57 78.31 32.57h193.25c21.54 15.43 47.9 24.54 76.26 24.54h.18c36.14 0 69.02-14.79 92.83-38.6 23.8-23.81 38.6-56.67 38.6-92.83 0-36.15-14.78-69.03-38.63-92.8C449.53 14.8 416.67 0 380.57 0h-.18c-23.02 0-44.72 6.02-63.61 16.55zm70.62 97.17.43.09c.82-3.45 2.83-6.19 6.04-8.16 3.2-1.98 6.53-2.57 10.01-1.75l.1-.43c-3.47-.82-6.2-2.83-8.17-6.03-1.98-3.22-2.57-6.55-1.75-10.01l-.43-.1c-.82 3.47-2.83 6.2-6.03 8.18-3.21 1.98-6.55 2.56-10.02 1.74l-.1.43c3.47.82 6.2 2.84 8.18 6.04 1.99 3.19 2.56 6.52 1.74 10zm36.87 16.77.53.12c1.02-4.35 3.55-7.78 7.58-10.26 4.02-2.49 8.2-3.22 12.56-2.19l.13-.53c-4.35-1.03-7.78-3.55-10.26-7.59-2.49-4.03-3.22-8.22-2.2-12.56l-.53-.12c-1.02 4.35-3.55 7.77-7.58 10.26-4.02 2.49-8.21 3.22-12.56 2.19l-.13.53c4.36 1.03 7.78 3.55 10.26 7.58 2.49 4.02 3.22 8.22 2.2 12.57zm-38.79-61.01c-15.69 7.67-26.98 23.26-28.29 41.93-1.96 27.88 19.05 52.06 46.92 54.02 13.23.93 25.64-3.32 35.22-11.02 4.75-3.82 9.66-.45 7.59 4.36-11.33 26.42-38.45 44.04-68.74 41.91-38.29-2.69-67.14-35.91-64.45-74.19C316.3 89.8 347.05 61.67 383.44 62c6.71.06 8.13 4.5 2.04 7.48zm-5.09-53.95h.18c63.75 0 115.91 52.15 115.91 115.9 0 63.75-52.23 115.91-115.91 115.91h-.18c-63.68 0-115.91-52.16-115.91-115.91s52.16-115.9 115.91-115.9z"/></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,3 +0,0 @@
<svg class="octicon octicon-mark-github v-align-middle" height="32" viewbox="0 0 16 16" version="1.1" width="32" aria-hidden="true">
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 749 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 512 256.04"><path d="M128.02 0h.18c22.03 0 42.83 5.66 61 15.6h210.38c30.89 0 59 12.65 79.38 33.04C499.35 68.99 512 97.1 512 128.02c0 30.92-12.66 59.03-33.02 79.4l-.42.38c-20.34 20.15-48.29 32.64-78.98 32.64H189.24c-18.17 9.93-38.98 15.6-61.04 15.6h-.18c-35.2 0-67.22-14.41-90.42-37.6C14.41 195.25 0 163.24 0 128.02s14.4-67.24 37.59-90.43l.91-.83C61.65 14.05 93.29 0 128.02 0zm-5.95 54.42c0-1.95.8-3.73 2.08-5 2.74-2.77 7.27-2.76 10.02-.01l.14.16a7.042 7.042 0 0 1 1.94 4.85v12.95c0 1.95-.8 3.73-2.08 5.01-2.75 2.75-7.27 2.75-10.02 0a7.084 7.084 0 0 1-2.08-5.01V54.42zm6.05 31.17c11.72 0 22.32 4.75 30 12.43 7.67 7.68 12.43 18.29 12.43 30 0 11.72-4.75 22.32-12.43 30s-18.28 12.43-30 12.43c-11.72 0-22.32-4.75-30.01-12.43-7.67-7.68-12.43-18.28-12.43-30 0-11.72 4.76-22.32 12.43-30 7.69-7.67 18.3-12.43 30.01-12.43zm-56.33-5.34a7.114 7.114 0 0 1-2.07-5.01c0-3.9 3.18-7.09 7.09-7.09 1.81 0 3.62.69 5 2.07l9.16 9.16a7.065 7.065 0 0 1 2.08 5.01c0 1.8-.7 3.62-2.08 5.01a7.057 7.057 0 0 1-5.01 2.08c-1.8 0-3.61-.7-5-2.07l-9.17-9.16zm-17.28 53.81c-1.95 0-3.73-.8-5-2.08-2.77-2.74-2.76-7.27-.01-10.01l.15-.14a7.04 7.04 0 0 1 4.86-1.94h12.94a7.082 7.082 0 0 1 7.09 7.09c0 1.95-.8 3.73-2.07 5.01a7.099 7.099 0 0 1-5.02 2.07H54.51zm25.82 50.28a7.049 7.049 0 0 1-5 2.07c-3.91 0-7.09-3.16-7.09-7.08 0-1.81.68-3.62 2.07-5.01l9.31-9.29a7.02 7.02 0 0 1 4.86-1.94 7.09 7.09 0 0 1 7.09 7.09c0 1.79-.69 3.6-2.08 4.99l-9.16 9.17zm53.82 17.29c0 1.94-.8 3.73-2.08 5-2.74 2.76-7.27 2.75-10.02 0l-.13-.15a7.033 7.033 0 0 1-1.94-4.85v-12.95c0-1.96.8-3.73 2.07-5.01 2.76-2.75 7.27-2.75 10.03 0a7.1 7.1 0 0 1 2.07 5.01v12.95zm50.28-25.83a7.055 7.055 0 0 1 2.07 5.01c0 3.89-3.18 7.09-7.08 7.09-1.81 0-3.63-.69-5.01-2.07l-9.16-9.16a7.095 7.095 0 0 1-2.07-5.02c0-3.9 3.18-7.09 7.08-7.09 1.8 0 3.61.7 5 2.08l9.17 9.16zm17.29-53.82c1.93 0 3.73.81 5 2.08 2.76 2.75 2.75 7.27 0 10.02l-.15.14a7.098 7.098 0 0 1-4.85 1.94h-12.95c-1.96 0-3.74-.8-5.01-2.08-2.76-2.75-2.76-7.27 0-10.02a7.049 7.049 0 0 1 5.01-2.08h12.95zM175.89 71.7a7.074 7.074 0 0 1 5-2.07c3.9 0 7.1 3.19 7.1 7.09 0 1.81-.69 3.62-2.07 5l-9.32 9.31a7.12 7.12 0 0 1-4.86 1.93c-3.91 0-7.09-3.18-7.09-7.09 0-1.8.7-3.61 2.08-5l9.16-9.17zm34.17-41.87c2.96 2.47 5.81 5.07 8.53 7.8 23.22 23.15 37.63 55.17 37.63 90.39s-14.42 67.23-37.6 90.42a130.2 130.2 0 0 1-8.5 7.77h189.46c26.83 0 51.24-10.91 69.02-28.5l.32-.35c17.79-17.79 28.85-42.35 28.85-69.34 0-26.99-11.06-51.55-28.85-69.35-17.77-17.8-42.33-28.84-69.34-28.84H210.06zm-82.04-14.71h.18c62.09 0 112.89 50.81 112.89 112.9 0 62.1-50.86 112.9-112.89 112.9h-.18c-62.03 0-112.9-50.8-112.9-112.9 0-62.09 50.81-112.9 112.9-112.9z"/></svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -14,7 +14,7 @@
<div id="watch-add-wrapper-zone"> <div id="watch-add-wrapper-zone">
<div> <div>
{{ render_simple_field(form.url, placeholder="https://...", required=true) }} {{ render_simple_field(form.url, placeholder="https://...", required=true) }}
{{ render_simple_field(form.tag, value=active_tag if active_tag else '', placeholder="watch label / tag") }} {{ render_simple_field(form.tag, value=active_tag if active_tag else '', placeholder="watch group") }}
</div> </div>
<div> <div>
{{ render_simple_field(form.watch_submit_button, title="Watch this URL!" ) }} {{ render_simple_field(form.watch_submit_button, title="Watch this URL!" ) }}
@@ -32,7 +32,6 @@
<button class="pure-button button-secondary button-xsmall" style="font-size: 70%" name="op" value="unpause">UnPause</button> <button class="pure-button button-secondary button-xsmall" style="font-size: 70%" name="op" value="unpause">UnPause</button>
<button class="pure-button button-secondary button-xsmall" style="font-size: 70%" name="op" value="mute">Mute</button> <button class="pure-button button-secondary button-xsmall" style="font-size: 70%" name="op" value="mute">Mute</button>
<button class="pure-button button-secondary button-xsmall" style="font-size: 70%" name="op" value="unmute">UnMute</button> <button class="pure-button button-secondary button-xsmall" style="font-size: 70%" name="op" value="unmute">UnMute</button>
<button class="pure-button button-secondary button-xsmall" style="font-size: 70%" name="op" value="recheck">Recheck</button>
<button class="pure-button button-secondary button-xsmall" style="font-size: 70%" name="op" value="notification-default">Use default notification</button> <button class="pure-button button-secondary button-xsmall" style="font-size: 70%" name="op" value="notification-default">Use default notification</button>
<button class="pure-button button-secondary button-xsmall" style="background: #dd4242; font-size: 70%" name="op" value="delete">Delete</button> <button class="pure-button button-secondary button-xsmall" style="background: #dd4242; font-size: 70%" name="op" value="delete">Delete</button>
</div> </div>
@@ -81,17 +80,17 @@
<td class="inline checkbox-uuid" ><input name="uuids" type="checkbox" value="{{ watch.uuid}} "/> <span>{{ loop.index }}</span></td> <td class="inline checkbox-uuid" ><input name="uuids" type="checkbox" value="{{ watch.uuid}} "/> <span>{{ loop.index }}</span></td>
<td class="inline watch-controls"> <td class="inline watch-controls">
{% if not watch.paused %} {% if not watch.paused %}
<a class="state-off" href="{{url_for('index', op='pause', uuid=watch.uuid, tag=active_tag)}}"><img src="{{url_for('static_content', group='images', filename='pause.svg')}}" alt="Pause checks" title="Pause checks" class="icon icon-pause"/></a> <a class="state-off" href="{{url_for('index', op='pause', uuid=watch.uuid, tag=active_tag)}}"><img src="{{url_for('static_content', group='images', filename='pause.svg')}}" alt="Pause checks" title="Pause checks"/></a>
{% else %} {% else %}
<a class="state-on" href="{{url_for('index', op='pause', uuid=watch.uuid, tag=active_tag)}}"><img src="{{url_for('static_content', group='images', filename='play.svg')}}" alt="UnPause checks" title="UnPause checks" class="icon icon-unpause"/></a> <a class="state-on" href="{{url_for('index', op='pause', uuid=watch.uuid, tag=active_tag)}}"><img src="{{url_for('static_content', group='images', filename='play.svg')}}" alt="UnPause checks" title="UnPause checks"/></a>
{% endif %} {% endif %}
<a class="link-mute state-{{'on' if watch.notification_muted else 'off'}}" href="{{url_for('index', op='mute', uuid=watch.uuid, tag=active_tag)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="Mute notifications" title="Mute notifications" class="icon icon-mute"/></a> <a class="state-{{'on' if watch.notification_muted}}" href="{{url_for('index', op='mute', uuid=watch.uuid, tag=active_tag)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="Mute notifications" title="Mute notifications"/></a>
</td> </td>
<td class="title-col inline">{{watch.title if watch.title is not none and watch.title|length > 0 else watch.url}} <td class="title-col inline">{{watch.title if watch.title is not none and watch.title|length > 0 else watch.url}}
<a class="external" target="_blank" rel="noopener" href="{{ watch.link.replace('source:','') }}"></a> <a class="external" target="_blank" rel="noopener" href="{{ watch.link.replace('source:','') }}"></a>
<a class="link-spread" href="{{url_for('form_share_put_watch', uuid=watch.uuid)}}"><img style="height: 1em;display:inline-block;" src="{{url_for('static_content', group='images', filename='spread.svg')}}" class="icon icon-spread" title="Create a link to share watch config with others" /></a> <a href="{{url_for('form_share_put_watch', uuid=watch.uuid)}}"><img style="height: 1em;display:inline-block;" src="{{url_for('static_content', group='images', filename='spread.svg')}}" /></a>
{%if watch.fetch_backend == "html_webdriver" %}<img style="height: 1em; display:inline-block;" src="{{url_for('static_content', group='images', filename='Google-Chrome-icon.png')}}" title="Using a chrome browser" />{% endif %} {%if watch.fetch_backend == "html_webdriver" %}<img style="height: 1em; display:inline-block;" src="{{url_for('static_content', group='images', filename='Google-Chrome-icon.png')}}" />{% endif %}
{% if watch.last_error is defined and watch.last_error != False %} {% if watch.last_error is defined and watch.last_error != False %}
<div class="fetch-error">{{ watch.last_error }}</div> <div class="fetch-error">{{ watch.last_error }}</div>
@@ -99,12 +98,6 @@
{% if watch.last_notification_error is defined and watch.last_notification_error != False %} {% if watch.last_notification_error is defined and watch.last_notification_error != False %}
<div class="fetch-error notification-error"><a href="{{url_for('notification_logs')}}">{{ watch.last_notification_error }}</a></div> <div class="fetch-error notification-error"><a href="{{url_for('notification_logs')}}">{{ watch.last_notification_error }}</a></div>
{% endif %} {% endif %}
{% if watch['has_ldjson_price_data'] and not watch['track_ldjson_price_data'] %}
<div class="ldjson-price-track-offer">Embedded price data detected, follow only price data? <a href="{{url_for('price_data_follower.accept', uuid=watch.uuid)}}" class="pure-button button-xsmall">Yes</a> <a href="{{url_for('price_data_follower.reject', uuid=watch.uuid)}}" class="">No</a></div>
{% endif %}
{% if watch['track_ldjson_price_data'] == 'accepted' %}
<span class="tracking-ldjson-price-data" title="Automatically following embedded price information"><img src="{{url_for('static_content', group='images', filename='price-tag-icon.svg')}}" class="price-follow-tag-icon"/> Price</span>
{% endif %}
{% if not active_tag %} {% if not active_tag %}
<span class="watch-tag-list">{{ watch.tag}}</span> <span class="watch-tag-list">{{ watch.tag}}</span>
{% endif %} {% endif %}
@@ -118,13 +111,13 @@
</td> </td>
<td> <td>
<a {% if watch.uuid in queued_uuids %}disabled="true"{% endif %} href="{{ url_for('form_watch_checknow', uuid=watch.uuid, tag=request.args.get('tag')) }}" <a {% if watch.uuid in queued_uuids %}disabled="true"{% endif %} href="{{ url_for('form_watch_checknow', uuid=watch.uuid, tag=request.args.get('tag')) }}"
class="recheck pure-button pure-button-primary">{% if watch.uuid in queued_uuids %}Queued{% else %}Recheck{% endif %}</a> class="recheck pure-button button-small pure-button-primary">{% if watch.uuid in queued_uuids %}Queued{% else %}Recheck{% endif %}</a>
<a href="{{ url_for('edit_page', uuid=watch.uuid)}}" class="pure-button pure-button-primary">Edit</a> <a href="{{ url_for('edit_page', uuid=watch.uuid)}}" class="pure-button button-small pure-button-primary">Edit</a>
{% if watch.history_n >= 2 %} {% if watch.history_n >= 2 %}
<a href="{{ url_for('diff_history_page', uuid=watch.uuid) }}" target="{{watch.uuid}}" class="pure-button pure-button-primary diff-link">Diff</a> <a href="{{ url_for('diff_history_page', uuid=watch.uuid) }}" target="{{watch.uuid}}" class="pure-button button-small pure-button-primary diff-link">Diff</a>
{% else %} {% else %}
{% if watch.history_n == 1 or (watch.history_n ==0 and watch.error_text_ctime )%} {% if watch.history_n == 1 or (watch.history_n ==0 and watch.error_text_ctime )%}
<a href="{{ url_for('preview_page', uuid=watch.uuid)}}" target="{{watch.uuid}}" class="pure-button pure-button-primary">Preview</a> <a href="{{ url_for('preview_page', uuid=watch.uuid)}}" target="{{watch.uuid}}" class="pure-button button-small pure-button-primary">Preview</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
</td> </td>

View File

@@ -1,146 +0,0 @@
#!/usr/bin/python3
import time
from flask import url_for
from .util import live_server_setup, extract_UUID_from_client, extract_api_key_from_UI
def set_response_with_ldjson():
test_return_data = """<html>
<body>
Some initial text</br>
<p>Which is across multiple lines</p>
</br>
So let's see what happens. </br>
<div class="sametext">Some text thats the same</div>
<div class="changetext">Some text that will change</div>
<script type="application/ld+json">
{
"@context":"https://schema.org/",
"@type":"Product",
"@id":"https://www.some-virtual-phone-shop.com/celular-iphone-14/p",
"name":"Celular Iphone 14 Pro Max 256Gb E Sim A16 Bionic",
"brand":{
"@type":"Brand",
"name":"APPLE"
},
"image":"https://www.some-virtual-phone-shop.com/15509426/image.jpg",
"description":"You dont need it",
"mpn":"111111",
"sku":"22222",
"offers":{
"@type":"AggregateOffer",
"lowPrice":8097000,
"highPrice":8099900,
"priceCurrency":"COP",
"offers":[
{
"@type":"Offer",
"price":8097000,
"priceCurrency":"COP",
"availability":"http://schema.org/InStock",
"sku":"102375961",
"itemCondition":"http://schema.org/NewCondition",
"seller":{
"@type":"Organization",
"name":"ajax"
}
}
],
"offerCount":1
}
}
</script>
</body>
</html>
"""
with open("test-datastore/endpoint-content.txt", "w") as f:
f.write(test_return_data)
return None
def set_response_without_ldjson():
test_return_data = """<html>
<body>
Some initial text</br>
<p>Which is across multiple lines</p>
</br>
So let's see what happens. </br>
<div class="sametext">Some text thats the same</div>
<div class="changetext">Some text that will change</div>
</body>
</html>
"""
with open("test-datastore/endpoint-content.txt", "w") as f:
f.write(test_return_data)
return None
# actually only really used by the distll.io importer, but could be handy too
def test_check_ldjson_price_autodetect(client, live_server):
live_server_setup(live_server)
# Give the endpoint time to spin up
time.sleep(1)
set_response_with_ldjson()
# Add our URL to the import page
test_url = url_for('test_endpoint', _external=True)
res = client.post(
url_for("import_page"),
data={"urls": test_url},
follow_redirects=True
)
assert b"1 Imported" in res.data
time.sleep(3)
# Should get a notice that it's available
res = client.get(url_for("index"))
assert b'ldjson-price-track-offer' in res.data
# Accept it
uuid = extract_UUID_from_client(client)
client.get(url_for('price_data_follower.accept', uuid=uuid, follow_redirects=True))
time.sleep(2)
# Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True)
time.sleep(2)
# Offer should be gone
res = client.get(url_for("index"))
assert b'Embedded price data' not in res.data
assert b'tracking-ldjson-price-data' in res.data
# and last snapshop (via API) should be just the price
api_key = extract_api_key_from_UI(client)
res = client.get(
url_for("watchsinglehistory", uuid=uuid, timestamp='latest'),
headers={'x-api-key': api_key},
)
# Should see this (dont know where the whitespace came from)
assert b'"highPrice": 8099900' in res.data
# And not this cause its not the ld-json
assert b"So let's see what happens" not in res.data
client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
##########################################################################################
# And we shouldnt see the offer
set_response_without_ldjson()
# Add our URL to the import page
test_url = url_for('test_endpoint', _external=True)
res = client.post(
url_for("import_page"),
data={"urls": test_url},
follow_redirects=True
)
assert b"1 Imported" in res.data
time.sleep(3)
res = client.get(url_for("index"))
assert b'ldjson-price-track-offer' not in res.data
##########################################################################################
client.get(url_for("form_delete", uuid="all"), follow_redirects=True)

View File

@@ -121,7 +121,7 @@ def test_element_removal_full(client, live_server):
url_for("import_page"), data={"urls": test_url}, follow_redirects=True url_for("import_page"), data={"urls": test_url}, follow_redirects=True
) )
assert b"1 Imported" in res.data assert b"1 Imported" in res.data
time.sleep(1)
# Goto the edit page, add the filter data # Goto the edit page, add the filter data
# Not sure why \r needs to be added - absent of the #changetext this is not necessary # Not sure why \r needs to be added - absent of the #changetext this is not necessary
subtractive_selectors_data = "header\r\nfooter\r\nnav\r\n#changetext" subtractive_selectors_data = "header\r\nfooter\r\nnav\r\n#changetext"

View File

@@ -38,6 +38,9 @@ def test_check_encoding_detection(client, live_server):
follow_redirects=True follow_redirects=True
) )
# Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up # Give the thread time to pick it up
time.sleep(2) time.sleep(2)

View File

@@ -77,8 +77,7 @@ def test_DNS_errors(client, live_server):
time.sleep(3) time.sleep(3)
res = client.get(url_for("index")) res = client.get(url_for("index"))
found_name_resolution_error = b"Temporary failure in name resolution" in res.data or b"Name or service not known" in res.data assert b'Name or service not known' in res.data
assert found_name_resolution_error
# Should always record that we tried # Should always record that we tried
assert bytes("just now".encode('utf-8')) in res.data assert bytes("just now".encode('utf-8')) in res.data

View File

@@ -1,70 +0,0 @@
#!/usr/bin/python3
import time
from flask import url_for
from urllib.request import urlopen
from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks
sleep_time_for_fetch_thread = 3
def test_check_extract_text_from_diff(client, live_server):
import time
with open("test-datastore/endpoint-content.txt", "w") as f:
f.write("Now it's {} seconds since epoch, time flies!".format(str(time.time())))
live_server_setup(live_server)
# Add our URL to the import page
res = client.post(
url_for("import_page"),
data={"urls": url_for('test_endpoint', _external=True)},
follow_redirects=True
)
assert b"1 Imported" in res.data
time.sleep(1)
# Load in 5 different numbers/changes
last_date=""
for n in range(5):
# Give the thread time to pick it up
print("Bumping snapshot and checking.. ", n)
last_date = str(time.time())
with open("test-datastore/endpoint-content.txt", "w") as f:
f.write("Now it's {} seconds since epoch, time flies!".format(last_date))
client.get(url_for("form_watch_checknow"), follow_redirects=True)
wait_for_all_checks(client)
res = client.post(
url_for("diff_history_page", uuid="first"),
data={"extract_regex": "Now it's ([0-9\.]+)",
"extract_submit_button": "Extract as CSV"},
follow_redirects=False
)
assert b'Nothing matches that RegEx' not in res.data
assert res.content_type == 'text/csv'
# Read the csv reply as stringio
from io import StringIO
import csv
f = StringIO(res.data.decode('utf-8'))
reader = csv.reader(f, delimiter=',')
output=[]
for row in reader:
output.append(row)
assert output[0][0] == 'Epoch seconds'
# Header line + 1 origin/first + 5 changes
assert(len(output) == 7)
# We expect to find the last bumped date in the changes in the last field of the spreadsheet
assert(output[6][2] == last_date)
# And nothing else, only that group () of the decimal and .
assert "time flies" not in output[6][2]

View File

@@ -73,17 +73,17 @@ def test_filter_doesnt_exist_then_exists_should_get_notification(client, live_se
# Just a regular notification setting, this will be used by the special 'filter not found' notification # Just a regular notification setting, this will be used by the special 'filter not found' notification
notification_form_data = {"notification_urls": notification_url, notification_form_data = {"notification_urls": notification_url,
"notification_title": "New ChangeDetection.io Notification - {{watch_url}}", "notification_title": "New ChangeDetection.io Notification - {watch_url}",
"notification_body": "BASE URL: {{base_url}}\n" "notification_body": "BASE URL: {base_url}\n"
"Watch URL: {{watch_url}}\n" "Watch URL: {watch_url}\n"
"Watch UUID: {{watch_uuid}}\n" "Watch UUID: {watch_uuid}\n"
"Watch title: {{watch_title}}\n" "Watch title: {watch_title}\n"
"Watch tag: {{watch_tag}}\n" "Watch tag: {watch_tag}\n"
"Preview: {{preview_url}}\n" "Preview: {preview_url}\n"
"Diff URL: {{diff_url}}\n" "Diff URL: {diff_url}\n"
"Snapshot: {{current_snapshot}}\n" "Snapshot: {current_snapshot}\n"
"Diff: {{diff}}\n" "Diff: {diff}\n"
"Diff Full: {{diff_full}}\n" "Diff Full: {diff_full}\n"
":-)", ":-)",
"notification_format": "Text"} "notification_format": "Text"}

View File

@@ -56,17 +56,17 @@ def run_filter_test(client, content_filter):
# Just a regular notification setting, this will be used by the special 'filter not found' notification # Just a regular notification setting, this will be used by the special 'filter not found' notification
notification_form_data = {"notification_urls": notification_url, notification_form_data = {"notification_urls": notification_url,
"notification_title": "New ChangeDetection.io Notification - {{watch_url}}", "notification_title": "New ChangeDetection.io Notification - {watch_url}",
"notification_body": "BASE URL: {{base_url}}\n" "notification_body": "BASE URL: {base_url}\n"
"Watch URL: {{watch_url}}\n" "Watch URL: {watch_url}\n"
"Watch UUID: {{watch_uuid}}\n" "Watch UUID: {watch_uuid}\n"
"Watch title: {{watch_title}}\n" "Watch title: {watch_title}\n"
"Watch tag: {{watch_tag}}\n" "Watch tag: {watch_tag}\n"
"Preview: {{preview_url}}\n" "Preview: {preview_url}\n"
"Diff URL: {{diff_url}}\n" "Diff URL: {diff_url}\n"
"Snapshot: {{current_snapshot}}\n" "Snapshot: {current_snapshot}\n"
"Diff: {{diff}}\n" "Diff: {diff}\n"
"Diff Full: {{diff_full}}\n" "Diff Full: {diff_full}\n"
":-)", ":-)",
"notification_format": "Text"} "notification_format": "Text"}
@@ -84,7 +84,6 @@ def run_filter_test(client, content_filter):
data=notification_form_data, data=notification_form_data,
follow_redirects=True follow_redirects=True
) )
assert b"Updated watch." in res.data assert b"Updated watch." in res.data
time.sleep(3) time.sleep(3)

View File

@@ -101,6 +101,9 @@ def test_check_ignore_text_functionality(client, live_server):
) )
assert b"1 Imported" in res.data assert b"1 Imported" in res.data
# Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up # Give the thread time to pick it up
time.sleep(sleep_time_for_fetch_thread) time.sleep(sleep_time_for_fetch_thread)
@@ -196,6 +199,9 @@ def test_check_global_ignore_text_functionality(client, live_server):
) )
assert b"1 Imported" in res.data assert b"1 Imported" in res.data
# Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up # Give the thread time to pick it up
time.sleep(sleep_time_for_fetch_thread) time.sleep(sleep_time_for_fetch_thread)

View File

@@ -69,6 +69,8 @@ def test_normal_page_check_works_with_ignore_status_code(client, live_server):
assert b"1 Imported" in res.data assert b"1 Imported" in res.data
time.sleep(sleep_time_for_fetch_thread) time.sleep(sleep_time_for_fetch_thread)
# Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True)
set_some_changed_response() set_some_changed_response()
time.sleep(sleep_time_for_fetch_thread) time.sleep(sleep_time_for_fetch_thread)
@@ -102,6 +104,9 @@ def test_403_page_check_works_with_ignore_status_code(client, live_server):
) )
assert b"1 Imported" in res.data assert b"1 Imported" in res.data
# Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up # Give the thread time to pick it up
time.sleep(sleep_time_for_fetch_thread) time.sleep(sleep_time_for_fetch_thread)
@@ -114,9 +119,11 @@ def test_403_page_check_works_with_ignore_status_code(client, live_server):
) )
assert b"Updated watch." in res.data assert b"Updated watch." in res.data
# Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up # Give the thread time to pick it up
time.sleep(sleep_time_for_fetch_thread) time.sleep(sleep_time_for_fetch_thread)
# Make a change # Make a change
set_some_changed_response() set_some_changed_response()

View File

@@ -394,48 +394,6 @@ def check_json_ext_filter(json_filter, client, live_server):
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data
def test_ignore_json_order(client, live_server):
# A change in order shouldn't trigger a notification
with open("test-datastore/endpoint-content.txt", "w") as f:
f.write('{"hello" : 123, "world": 123}')
# Add our URL to the import page
test_url = url_for('test_endpoint', content_type="application/json", _external=True)
res = client.post(
url_for("import_page"),
data={"urls": test_url},
follow_redirects=True
)
assert b"1 Imported" in res.data
time.sleep(2)
with open("test-datastore/endpoint-content.txt", "w") as f:
f.write('{"world" : 123, "hello": 123}')
# Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True)
time.sleep(2)
res = client.get(url_for("index"))
assert b'unviewed' not in res.data
# Just to be sure it still works
with open("test-datastore/endpoint-content.txt", "w") as f:
f.write('{"world" : 123, "hello": 124}')
# Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True)
time.sleep(2)
res = client.get(url_for("index"))
assert b'unviewed' in res.data
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data
def test_check_jsonpath_ext_filter(client, live_server): def test_check_jsonpath_ext_filter(client, live_server):
check_json_ext_filter('json:$[?(@.status==Sold)]', client, live_server) check_json_ext_filter('json:$[?(@.status==Sold)]', client, live_server)

View File

@@ -90,17 +90,17 @@ def test_check_notification(client, live_server):
print (">>>> Notification URL: "+notification_url) print (">>>> Notification URL: "+notification_url)
notification_form_data = {"notification_urls": notification_url, notification_form_data = {"notification_urls": notification_url,
"notification_title": "New ChangeDetection.io Notification - {{watch_url}}", "notification_title": "New ChangeDetection.io Notification - {watch_url}",
"notification_body": "BASE URL: {{base_url}}\n" "notification_body": "BASE URL: {base_url}\n"
"Watch URL: {{watch_url}}\n" "Watch URL: {watch_url}\n"
"Watch UUID: {{watch_uuid}}\n" "Watch UUID: {watch_uuid}\n"
"Watch title: {{watch_title}}\n" "Watch title: {watch_title}\n"
"Watch tag: {{watch_tag}}\n" "Watch tag: {watch_tag}\n"
"Preview: {{preview_url}}\n" "Preview: {preview_url}\n"
"Diff URL: {{diff_url}}\n" "Diff URL: {diff_url}\n"
"Snapshot: {{current_snapshot}}\n" "Snapshot: {current_snapshot}\n"
"Diff: {{diff}}\n" "Diff: {diff}\n"
"Diff Full: {{diff_full}}\n" "Diff Full: {diff_full}\n"
":-)", ":-)",
"notification_screenshot": True, "notification_screenshot": True,
"notification_format": "Text"} "notification_format": "Text"}
@@ -179,6 +179,7 @@ def test_check_notification(client, live_server):
logging.debug(">>> Skipping BASE_URL check") logging.debug(">>> Skipping BASE_URL check")
# This should insert the {current_snapshot} # This should insert the {current_snapshot}
set_more_modified_response() set_more_modified_response()
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("form_watch_checknow"), follow_redirects=True)
@@ -236,10 +237,10 @@ def test_check_notification(client, live_server):
follow_redirects=True follow_redirects=True
) )
def test_notification_validation(client, live_server): def test_notification_validation(client, live_server):
#live_server_setup(live_server)
time.sleep(1) time.sleep(3)
# re #242 - when you edited an existing new entry, it would not correctly show the notification settings # re #242 - when you edited an existing new entry, it would not correctly show the notification settings
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
@@ -267,6 +268,21 @@ def test_notification_validation(client, live_server):
# ) # )
# assert b"Notification Body and Title is required when a Notification URL is used" in res.data # assert b"Notification Body and Title is required when a Notification URL is used" in res.data
# Now adding a wrong token should give us an error
res = client.post(
url_for("settings_page"),
data={"application-notification_title": "New ChangeDetection.io Notification - {watch_url}",
"application-notification_body": "Rubbish: {rubbish}\n",
"application-notification_format": "Text",
"application-notification_urls": "json://localhost/foobar",
"requests-time_between_check-minutes": 180,
"fetch_backend": "html_requests"
},
follow_redirects=True
)
assert bytes("is not a valid token".encode('utf-8')) in res.data
# cleanup for the next # cleanup for the next
client.get( client.get(
url_for("form_delete", uuid="all"), url_for("form_delete", uuid="all"),
@@ -274,58 +290,3 @@ def test_notification_validation(client, live_server):
) )
def test_notification_custom_endpoint_and_jinja2(client, live_server):
time.sleep(1)
# test_endpoint - that sends the contents of a file
# test_notification_endpoint - that takes a POST and writes it to file (test-datastore/notification.txt)
# CUSTOM JSON BODY CHECK for POST://
set_original_response()
test_notification_url = url_for('test_notification_endpoint', _external=True).replace('http://', 'post://')+"?xxx={{ watch_url }}"
res = client.post(
url_for("settings_page"),
data={"application-notification_title": "New ChangeDetection.io Notification - {{ watch_url }}",
"application-notification_body": '{ "url" : "{{ watch_url }}", "secret": 444 }',
# https://github.com/caronc/apprise/wiki/Notify_Custom_JSON#get-parameter-manipulation
"application-notification_urls": test_notification_url,
"application-minutes_between_check": 180,
"application-fetch_backend": "html_requests"
},
follow_redirects=True
)
assert b'Settings updated' in res.data
# Add a watch and trigger a HTTP POST
test_url = url_for('test_endpoint', _external=True)
res = client.post(
url_for("form_quick_watch_add"),
data={"url": test_url, "tag": 'nice one'},
follow_redirects=True
)
assert b"Watch added" in res.data
time.sleep(2)
set_modified_response()
client.get(url_for("form_watch_checknow"), follow_redirects=True)
time.sleep(2)
with open("test-datastore/notification.txt", 'r') as f:
x=f.read()
j = json.loads(x)
assert j['url'].startswith('http://localhost')
assert j['secret'] == 444
# URL check, this will always be converted to lowercase
assert os.path.isfile("test-datastore/notification-url.txt")
with open("test-datastore/notification-url.txt", 'r') as f:
notification_url = f.read()
assert 'xxx=http' in notification_url
os.unlink("test-datastore/notification-url.txt")

View File

@@ -11,23 +11,23 @@ def test_check_notification_error_handling(client, live_server):
set_original_response() set_original_response()
# Give the endpoint time to spin up # Give the endpoint time to spin up
time.sleep(2) time.sleep(3)
# Set a URL and fetch it, then set a notification URL which is going to give errors # use a different URL so that it doesnt interfere with the actual check until we are ready
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
res = client.post( res = client.post(
url_for("form_quick_watch_add"), url_for("form_quick_watch_add"),
data={"url": test_url, "tag": ''}, data={"url": "https://changedetection.io/CHANGELOG.txt", "tag": ''},
follow_redirects=True follow_redirects=True
) )
assert b"Watch added" in res.data assert b"Watch added" in res.data
time.sleep(2) time.sleep(10)
set_modified_response()
# Check we capture the failure, we can just use trigger_check = y here
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("edit_page", uuid="first"),
data={"notification_urls": "jsons://broken-url-xxxxxxxx123/test", data={"notification_urls": "jsons://broken-url.changedetection.io/test",
"notification_title": "xxx", "notification_title": "xxx",
"notification_body": "xxxxx", "notification_body": "xxxxx",
"notification_format": "Text", "notification_format": "Text",
@@ -36,14 +36,15 @@ def test_check_notification_error_handling(client, live_server):
"title": "", "title": "",
"headers": "", "headers": "",
"time_between_check-minutes": "180", "time_between_check-minutes": "180",
"fetch_backend": "html_requests"}, "fetch_backend": "html_requests",
"trigger_check": "y"},
follow_redirects=True follow_redirects=True
) )
assert b"Updated watch." in res.data assert b"Updated watch." in res.data
found=False found=False
for i in range(1, 10): for i in range(1, 10):
time.sleep(1)
logging.debug("Fetching watch overview....") logging.debug("Fetching watch overview....")
res = client.get( res = client.get(
url_for("index")) url_for("index"))
@@ -52,7 +53,6 @@ def test_check_notification_error_handling(client, live_server):
found=True found=True
break break
time.sleep(1)
assert found assert found
@@ -60,7 +60,7 @@ def test_check_notification_error_handling(client, live_server):
# The error should show in the notification logs # The error should show in the notification logs
res = client.get( res = client.get(
url_for("notification_logs")) url_for("notification_logs"))
found_name_resolution_error = b"Temporary failure in name resolution" in res.data or b"Name or service not known" in res.data assert bytes("Name or service not known".encode('utf-8')) in res.data
assert found_name_resolution_error
client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
# And it should be listed on the watch overview

View File

@@ -20,8 +20,6 @@ def test_headers_in_request(client, live_server):
) )
assert b"1 Imported" in res.data assert b"1 Imported" in res.data
time.sleep(1)
res = client.post( res = client.post(
url_for("import_page"), url_for("import_page"),
data={"urls": test_url}, data={"urls": test_url},
@@ -176,7 +174,6 @@ def test_method_in_request(client, live_server):
) )
assert b"1 Imported" in res.data assert b"1 Imported" in res.data
time.sleep(2)
res = client.post( res = client.post(
url_for("import_page"), url_for("import_page"),
data={"urls": test_url}, data={"urls": test_url},
@@ -184,8 +181,6 @@ def test_method_in_request(client, live_server):
) )
assert b"1 Imported" in res.data assert b"1 Imported" in res.data
time.sleep(2)
# Attempt to add a method which is not valid # Attempt to add a method which is not valid
res = client.post( res = client.post(
url_for("edit_page", uuid="first"), url_for("edit_page", uuid="first"),
@@ -211,7 +206,7 @@ def test_method_in_request(client, live_server):
assert b"Updated watch." in res.data assert b"Updated watch." in res.data
# Give the thread time to pick up the first version # Give the thread time to pick up the first version
time.sleep(2) time.sleep(5)
# The service should echo back the request verb # The service should echo back the request verb
res = client.get( res = client.get(
@@ -222,7 +217,7 @@ def test_method_in_request(client, live_server):
# The test call service will return the verb as the body # The test call service will return the verb as the body
assert b"PATCH" in res.data assert b"PATCH" in res.data
time.sleep(2) time.sleep(5)
watches_with_method = 0 watches_with_method = 0
with open('test-datastore/url-watches.json') as f: with open('test-datastore/url-watches.json') as f:

View File

@@ -149,9 +149,6 @@ def live_server_setup(live_server):
if data != None: if data != None:
f.write(data) f.write(data)
with open("test-datastore/notification-url.txt", "w") as f:
f.write(request.url)
print("\n>> Test notification endpoint was hit.\n", data) print("\n>> Test notification endpoint was hit.\n", data)
return "Text was set" return "Text was set"

View File

@@ -4,7 +4,6 @@ import queue
import time import time
from changedetectionio import content_fetcher from changedetectionio import content_fetcher
from changedetectionio import queuedWatchMetaData
from changedetectionio.fetch_site_status import FilterNotFoundInResponse from changedetectionio.fetch_site_status import FilterNotFoundInResponse
# A single update worker # A single update worker
@@ -158,12 +157,11 @@ class update_worker(threading.Thread):
while not self.app.config.exit.is_set(): while not self.app.config.exit.is_set():
try: try:
queued_item_data = self.q.get(block=False) priority, uuid = self.q.get(block=False)
except queue.Empty: except queue.Empty:
pass pass
else: else:
uuid = queued_item_data.item.get('uuid')
self.current_uuid = uuid self.current_uuid = uuid
if uuid in list(self.datastore.data['watching'].keys()): if uuid in list(self.datastore.data['watching'].keys()):
@@ -173,11 +171,11 @@ class update_worker(threading.Thread):
update_obj= {} update_obj= {}
xpath_data = False xpath_data = False
process_changedetection_results = True process_changedetection_results = True
print("> Processing UUID {} Priority {} URL {}".format(uuid, queued_item_data.priority, self.datastore.data['watching'][uuid]['url'])) print("> Processing UUID {} Priority {} URL {}".format(uuid, priority, self.datastore.data['watching'][uuid]['url']))
now = time.time() now = time.time()
try: try:
changed_detected, update_obj, contents = update_handler.run(uuid, skip_when_checksum_same=queued_item_data.item.get('skip_when_checksum_same')) changed_detected, update_obj, contents = update_handler.run(uuid)
# Re #342 # Re #342
# In Python 3, all strings are sequences of Unicode characters. There is a bytes type that holds raw bytes. # In Python 3, all strings are sequences of Unicode characters. There is a bytes type that holds raw bytes.
# We then convert/.decode('utf-8') for the notification etc # We then convert/.decode('utf-8') for the notification etc
@@ -243,10 +241,6 @@ class update_worker(threading.Thread):
process_changedetection_results = True process_changedetection_results = True
except content_fetcher.checksumFromPreviousCheckWasTheSame as e:
# Yes fine, so nothing todo
pass
except content_fetcher.BrowserStepsStepTimout as e: except content_fetcher.BrowserStepsStepTimout as e:
if not self.datastore.data['watching'].get(uuid): if not self.datastore.data['watching'].get(uuid):

View File

@@ -57,12 +57,6 @@ services:
# Used for fetching pages via WebDriver+Chrome where you need Javascript support. # Used for fetching pages via WebDriver+Chrome where you need Javascript support.
# Now working on arm64 (needs testing on rPi - tested on Oracle ARM instance) # Now working on arm64 (needs testing on rPi - tested on Oracle ARM instance)
# replace image with seleniarm/standalone-chromium:4.0.0-20211213 # replace image with seleniarm/standalone-chromium:4.0.0-20211213
# If WEBDRIVER or PLAYWRIGHT are enabled, changedetection container depends on that
# and must wait before starting (substitute "browser-chrome" with "playwright-chrome" if last one is used)
# depends_on:
# browser-chrome:
# condition: service_started
# browser-chrome: # browser-chrome:
# hostname: browser-chrome # hostname: browser-chrome

View File

@@ -1,6 +1,5 @@
flask~=2.0 flask~=2.0
flask_wtf flask_wtf
flask-compress
eventlet>=0.31.0 eventlet>=0.31.0
validators validators
timeago~=1.0 timeago~=1.0
@@ -29,9 +28,8 @@ apprise~=1.2.0
# apprise mqtt https://github.com/dgtlmoon/changedetection.io/issues/315 # apprise mqtt https://github.com/dgtlmoon/changedetection.io/issues/315
paho-mqtt paho-mqtt
# This mainly affects some ARM builds, which unlike the other builds ignores "ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1" # Pinned version of cryptography otherwise
# so without this pinning, the newer versions on ARM will forcefully try to build rust, which results in "rust compiler not found" # ERROR: Could not build wheels for cryptography which use PEP 517 and cannot be installed directly
# (introduced once apprise became a dep)
cryptography~=3.4 cryptography~=3.4
# Used for CSS filtering # Used for CSS filtering
@@ -59,3 +57,6 @@ jq~=1.3 ;python_version >= "3.8" and sys_platform == "linux"
# Any current modern version, required so far for screenshot PNG->JPEG conversion but will be used more in the future # Any current modern version, required so far for screenshot PNG->JPEG conversion but will be used more in the future
pillow pillow
# playwright is installed at Dockerfile build time because it's not available on all platforms # playwright is installed at Dockerfile build time because it's not available on all platforms
# For shutting down playwright BrowserSteps nicely
psutil

View File

@@ -1 +1 @@
python-3.9.15 python-3.8.12