Compare commits
	
		
			36 Commits
		
	
	
		
			darkmode
			...
			jinja2-not
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 5787f581e9 | ||
|   | a048e4a02d | ||
|   | 99524513c0 | ||
|   | 42e1098e7b | ||
|   | a1afb77f36 | ||
|   | 18a90825a1 | ||
|   | 8103634e9d | ||
|   | c9a2dd6920 | ||
|   | 7e76276703 | ||
|   | 69662ff91c | ||
|   | 5e0dae5703 | ||
|   | fc94c57d7f | ||
|   | 7b94ba6f23 | ||
|   | 2345b6b558 | ||
|   | b8d5a12ad0 | ||
|   | 9e67a572c5 | ||
|   | 378d7b7362 | ||
|   | 8773f5ed90 | ||
|   | a5e6676431 | ||
|   | c51b243e7c | ||
|   | 5dd275555e | ||
|   | d1d4045c49 | ||
|   | 77409eeb3a | ||
|   | dbfcc3a5a3 | ||
|   | e97c2b3224 | ||
|   | 3f49d9591c | ||
|   | 8e254c4314 | ||
|   | f4163cfa6f | ||
|   | 86fbf505fd | ||
|   | 8bceb0abd4 | ||
|   | bba10afd97 | ||
|   | 658a88f895 | ||
|   | 4f602ff69a | ||
|   | 7c7946ec0b | ||
|   | 1d0ba2e633 | ||
|   | 72b9f9151b | 
| @@ -159,7 +159,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"  /> | ||||
|  | ||||
| Now you can also customise your notification content! | ||||
| 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! | ||||
|  | ||||
| ## JSON API Monitoring | ||||
|  | ||||
|   | ||||
| @@ -202,7 +202,9 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|     watch_api.add_resource(api_v1.SystemInfo, '/api/v1/systeminfo', | ||||
|                            resource_class_kwargs={'datastore': datastore, 'update_q': update_q}) | ||||
|  | ||||
|  | ||||
|     def getDarkModeSetting(): | ||||
|       css_dark_mode = request.cookies.get('css_dark_mode') | ||||
|       return True if (css_dark_mode == 'true' or css_dark_mode == True) else False | ||||
|  | ||||
|     # Setup cors headers to allow all domains | ||||
|     # https://flask-cors.readthedocs.io/en/latest/ | ||||
| @@ -403,6 +405,7 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|  | ||||
|         form = forms.quickWatchForm(request.form) | ||||
|         output = render_template("watch-overview.html", | ||||
|                                  dark_mode=getDarkModeSetting(), | ||||
|                                  form=form, | ||||
|                                  watches=sorted_watches, | ||||
|                                  tags=existing_tags, | ||||
| @@ -661,6 +664,7 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|                                      browser_steps_config=browser_step_ui_config, | ||||
|                                      current_base_url=datastore.data['settings']['application']['base_url'], | ||||
|                                      emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False), | ||||
|                                      dark_mode=getDarkModeSetting(), | ||||
|                                      form=form, | ||||
|                                      has_default_notification_urls=True if len(datastore.data['settings']['application']['notification_urls']) else False, | ||||
|                                      has_empty_checktime=using_default_check_time, | ||||
| @@ -748,6 +752,7 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|  | ||||
|         output = render_template("settings.html", | ||||
|                                  form=form, | ||||
|                                  dark_mode=getDarkModeSetting(), | ||||
|                                  current_base_url = datastore.data['settings']['application']['base_url'], | ||||
|                                  hide_remove_pass=os.getenv("SALTED_PASS", False), | ||||
|                                  api_key=datastore.data['settings']['application'].get('api_access_token'), | ||||
| @@ -788,6 +793,7 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|  | ||||
|         # Could be some remaining, or we could be on GET | ||||
|         output = render_template("import.html", | ||||
|                                  dark_mode=getDarkModeSetting(), | ||||
|                                  import_url_list_remaining="\n".join(remaining_urls), | ||||
|                                  original_distill_json='' | ||||
|                                  ) | ||||
| @@ -804,10 +810,12 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|  | ||||
|         return redirect(url_for('index')) | ||||
|  | ||||
|     @app.route("/diff/<string:uuid>", methods=['GET']) | ||||
|     @app.route("/diff/<string:uuid>", methods=['GET', 'POST']) | ||||
|     @login_required | ||||
|     def diff_history_page(uuid): | ||||
|  | ||||
|         from changedetectionio import forms | ||||
|  | ||||
|         # More for testing, possible to return the first/only | ||||
|         if uuid == 'first': | ||||
|             uuid = list(datastore.data['watching'].keys()).pop() | ||||
| @@ -819,6 +827,28 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|             flash("No history found for the specified link, bad link?", "error") | ||||
|             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 | ||||
|         dates = list(history.keys()) | ||||
|  | ||||
| @@ -861,22 +891,24 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|                     watch.get('fetch_backend', None) is None and system_uses_webdriver) else False | ||||
|  | ||||
|         output = render_template("diff.html", | ||||
|                                  watch_a=watch, | ||||
|                                  newest=newest_version_file_contents, | ||||
|                                  previous=previous_version_file_contents, | ||||
|                                  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'], | ||||
|                                  current_previous_version=str(previous_version), | ||||
|                                  dark_mode=getDarkModeSetting(), | ||||
|                                  extra_stylesheets=extra_stylesheets, | ||||
|                                  extra_title=" - Diff - {}".format(watch['title'] if watch['title'] else watch['url']), | ||||
|                                  left_sticky=True, | ||||
|                                  screenshot=screenshot_url, | ||||
|                                  extract_form=extract_form, | ||||
|                                  is_html_webdriver=is_html_webdriver, | ||||
|                                  last_error=watch['last_error'], | ||||
|                                  last_error_screenshot=watch.get_error_snapshot(), | ||||
|                                  last_error_text=watch.get_error_text(), | ||||
|                                  last_error_screenshot=watch.get_error_snapshot() | ||||
|                                  left_sticky=True, | ||||
|                                  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 | ||||
| @@ -912,6 +944,7 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|                                      content=content, | ||||
|                                      history_n=watch.history_n, | ||||
|                                      extra_stylesheets=extra_stylesheets, | ||||
|                                      dark_mode=getDarkModeSetting(), | ||||
| #                                     current_diff_url=watch['url'], | ||||
|                                      watch=watch, | ||||
|                                      uuid=uuid, | ||||
| @@ -958,6 +991,7 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|                                  content=content, | ||||
|                                  history_n=watch.history_n, | ||||
|                                  extra_stylesheets=extra_stylesheets, | ||||
|                                  dark_mode=getDarkModeSetting(), | ||||
|                                  ignored_line_numbers=ignored_line_numbers, | ||||
|                                  triggered_line_numbers=trigger_line_numbers, | ||||
|                                  current_diff_url=watch['url'], | ||||
| @@ -976,6 +1010,7 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|     def notification_logs(): | ||||
|         global notification_debug_log | ||||
|         output = render_template("notification-log.html", | ||||
|                                  dark_mode=getDarkModeSetting(), | ||||
|                                  logs=notification_debug_log if len(notification_debug_log) else ["Notification logs are empty - no notifications sent yet."]) | ||||
|  | ||||
|         return output | ||||
|   | ||||
| @@ -193,7 +193,7 @@ class ValidateAppRiseServers(object): | ||||
|                 message = field.gettext('\'%s\' is not a valid AppRise URL.' % (server_url)) | ||||
|                 raise ValidationError(message) | ||||
|  | ||||
| class ValidateTokensList(object): | ||||
| class ValidateJinja2Template(object): | ||||
|     """ | ||||
|     Validates that a {token} is from a valid set | ||||
|     """ | ||||
| @@ -202,14 +202,27 @@ class ValidateTokensList(object): | ||||
|  | ||||
|     def __call__(self, form, field): | ||||
|         from changedetectionio import notification | ||||
|         regex = re.compile('{.*?}') | ||||
|         for p in re.findall(regex, field.data): | ||||
|             if not p.strip('{}') in notification.valid_tokens: | ||||
|                 message = field.gettext('Token \'%s\' is not a valid token.') | ||||
|                 raise ValidationError(message % (p)) | ||||
|              | ||||
|  | ||||
|         from jinja2 import Environment, BaseLoader, TemplateSyntaxError | ||||
|         from jinja2.meta import find_undeclared_variables | ||||
|  | ||||
|  | ||||
|         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): | ||||
|      | ||||
|  | ||||
|     """ | ||||
|        Flask wtform validators wont work with basic auth | ||||
|     """ | ||||
| @@ -225,6 +238,7 @@ class validateURL(object): | ||||
|             message = field.gettext('\'%s\' is not a valid URL.' % (field.data.strip())) | ||||
|             raise ValidationError(message) | ||||
|  | ||||
|  | ||||
| class ValidateListRegex(object): | ||||
|     """ | ||||
|     Validates that anything that looks like a regex passes as a regex | ||||
| @@ -333,11 +347,11 @@ class quickWatchForm(Form): | ||||
|  | ||||
| # Common to a single watch and the global settings | ||||
| class commonSettingsForm(Form): | ||||
|     notification_urls = StringListField('Notification URL list', validators=[validators.Optional(), ValidateAppRiseServers()]) | ||||
|     notification_title = StringField('Notification title', validators=[validators.Optional(), ValidateTokensList()]) | ||||
|     notification_body = TextAreaField('Notification body', validators=[validators.Optional(), ValidateTokensList()]) | ||||
|     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_body = TextAreaField('Notification Body', default='{{ watch_url }} had a change.', validators=[validators.Optional(), ValidateJinja2Template()]) | ||||
|     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) | ||||
|     webdriver_delay = IntegerField('Wait seconds before extracting text', validators=[validators.Optional(), validators.NumberRange(min=1, | ||||
|                                                                                                                                     message="Should contain one or more seconds")]) | ||||
| @@ -448,3 +462,8 @@ class globalSettingsForm(Form): | ||||
|     requests = FormField(globalSettingsRequestForm) | ||||
|     application = FormField(globalSettingsApplicationForm) | ||||
|     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"}) | ||||
|   | ||||
| @@ -27,6 +27,7 @@ class model(dict): | ||||
|                     'base_url' : None, | ||||
|                     'extract_title_as_title': False, | ||||
|                     'empty_pages_are_a_change': False, | ||||
|                     'css_dark_mode': False, | ||||
|                     'fetch_backend': getenv("DEFAULT_FETCH_BACKEND", "html_requests"), | ||||
|                     'filter_failure_notification_threshold_attempts': _FILTER_FAILURE_THRESHOLD_ATTEMPTS_DEFAULT, | ||||
|                     'global_ignore_text': [], # List of text to ignore when calculating the comparison checksum | ||||
|   | ||||
| @@ -318,3 +318,47 @@ class model(dict): | ||||
|         if os.path.isfile(fname): | ||||
|             return fname | ||||
|         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 | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| import apprise | ||||
| from jinja2 import Environment, BaseLoader | ||||
| from apprise import NotifyFormat | ||||
| import json | ||||
|  | ||||
| valid_tokens = { | ||||
|     'base_url': '', | ||||
| @@ -16,8 +18,8 @@ valid_tokens = { | ||||
|  | ||||
| default_notification_format_for_watch = 'System default' | ||||
| default_notification_format = 'Text' | ||||
| default_notification_body = '{watch_url} had a change.\n---\n{diff}\n---\n' | ||||
| default_notification_title = 'ChangeDetection.io Notification - {watch_url}' | ||||
| default_notification_body = '{{watch_url}} had a change.\n---\n{{diff}}\n---\n' | ||||
| default_notification_title = 'ChangeDetection.io Notification - {{watch_url}}' | ||||
|  | ||||
| valid_notification_formats = { | ||||
|     'Text': NotifyFormat.TEXT, | ||||
| @@ -27,25 +29,67 @@ valid_notification_formats = { | ||||
|     default_notification_format_for_watch: default_notification_format_for_watch | ||||
| } | ||||
|  | ||||
| def process_notification(n_object, datastore): | ||||
| # include the decorator | ||||
| from apprise.decorators import notify | ||||
|  | ||||
|     # Get the notification body from datastore | ||||
|     n_body = n_object.get('notification_body', default_notification_body) | ||||
|     n_title = n_object.get('notification_title', default_notification_title) | ||||
|     n_format = valid_notification_formats.get( | ||||
|         n_object['notification_format'], | ||||
|         valid_notification_formats[default_notification_format], | ||||
|     ) | ||||
| @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): | ||||
|  | ||||
|     # 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) | ||||
|  | ||||
|     # Get the notification body from datastore | ||||
|     jinja2_env = Environment(loader=BaseLoader) | ||||
|     n_body = jinja2_env.from_string(n_object.get('notification_body', default_notification_body)).render(**notification_parameters) | ||||
|     n_title = jinja2_env.from_string(n_object.get('notification_title', default_notification_title)).render(**notification_parameters) | ||||
|     n_format = valid_notification_formats.get( | ||||
|         n_object['notification_format'], | ||||
|         valid_notification_formats[default_notification_format], | ||||
|     ) | ||||
|      | ||||
|     # https://github.com/caronc/apprise/wiki/Development_LogCapture | ||||
|     # Anything higher than or equal to WARNING (which covers things like Connection errors) | ||||
|     # raise it as an exception | ||||
| @@ -53,6 +97,7 @@ def process_notification(n_object, datastore): | ||||
|     sent_objs=[] | ||||
|     from .apprise_asset import asset | ||||
|     for url in n_object['notification_urls']: | ||||
|         url = jinja2_env.from_string(url).render(**notification_parameters) | ||||
|         apobj = apprise.Apprise(debug=True, asset=asset) | ||||
|         url = url.strip() | ||||
|         if len(url): | ||||
| @@ -66,7 +111,12 @@ 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 | ||||
|                 k = '?' if not '?' in url else '&' | ||||
|                 if not 'avatar_url' in url and not url.startswith('mail'): | ||||
|                 if not 'avatar_url' in url \ | ||||
|                         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' | ||||
|  | ||||
|                 if url.startswith('tgram://'): | ||||
| @@ -144,7 +194,7 @@ def create_notification_parameters(n_object, datastore): | ||||
|  | ||||
|     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. | ||||
|     if base_url == '': | ||||
|         base_url = "<base-url-env-var-not-set>" | ||||
|   | ||||
| @@ -1,42 +1,4 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <svg | ||||
|    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 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"> | ||||
|   <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> | ||||
|   | ||||
| Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.2 KiB | 
| @@ -1,46 +1,5 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <svg | ||||
|    width="18" | ||||
|    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 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"> | ||||
|   <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> | ||||
|   | ||||
| Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 787 B | 
| @@ -13,6 +13,8 @@ $(document).ready(function () { | ||||
|         } else if (hash_name === '#error-screenshot') { | ||||
|             $("img#error-screenshot-img").attr('src', error_screenshot_url); | ||||
|             $("#settings").hide(); | ||||
|         } else if (hash_name === '#extract') { | ||||
|             $("#settings").hide(); | ||||
|         } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,112 +1,110 @@ | ||||
| var a = document.getElementById('a'); | ||||
| var b = document.getElementById('b'); | ||||
| var result = document.getElementById('result'); | ||||
| var a = document.getElementById("a"); | ||||
| var b = document.getElementById("b"); | ||||
| var result = document.getElementById("result"); | ||||
|  | ||||
| function changed() { | ||||
|     // https://github.com/kpdecker/jsdiff/issues/389 | ||||
|     // I would love to use `{ignoreWhitespace: true}` here but it breaks the formatting | ||||
|     options = {ignoreWhitespace: document.getElementById('ignoreWhitespace').checked}; | ||||
|   // https://github.com/kpdecker/jsdiff/issues/389 | ||||
|   // I would love to use `{ignoreWhitespace: true}` here but it breaks the formatting | ||||
|   options = { | ||||
|     ignoreWhitespace: document.getElementById("ignoreWhitespace").checked, | ||||
|   }; | ||||
|  | ||||
|     var diff = Diff[window.diffType](a.textContent, b.textContent, options); | ||||
|     var fragment = document.createDocumentFragment(); | ||||
|     for (var i = 0; i < diff.length; i++) { | ||||
|  | ||||
|         if (diff[i].added && diff[i + 1] && diff[i + 1].removed) { | ||||
|             var swap = diff[i]; | ||||
|             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 diff = Diff[window.diffType](a.textContent, b.textContent, options); | ||||
|   var fragment = document.createDocumentFragment(); | ||||
|   for (var i = 0; i < diff.length; i++) { | ||||
|     if (diff[i].added && diff[i + 1] && diff[i + 1].removed) { | ||||
|       var swap = diff[i]; | ||||
|       diff[i] = diff[i + 1]; | ||||
|       diff[i + 1] = swap; | ||||
|     } | ||||
|  | ||||
|     result.textContent = ''; | ||||
|     result.appendChild(fragment); | ||||
|     var node; | ||||
|     if (diff[i].removed) { | ||||
|       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); | ||||
|   } | ||||
|  | ||||
|     // Jump at start | ||||
|     inputs.current = 0; | ||||
|     next_diff(); | ||||
|   result.textContent = ""; | ||||
|   result.appendChild(fragment); | ||||
|  | ||||
|   // Jump at start | ||||
|   inputs.current = 0; | ||||
|   next_diff(); | ||||
| } | ||||
|  | ||||
| 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(); | ||||
|         } | ||||
|   /* 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"); | ||||
|     var dateObject = new Date(newest_version_timestamp*1000); | ||||
|     current_v.innerHTML = dateObject.toLocaleString(); | ||||
|     onDiffTypeChange(document.querySelector('#settings [name="diff_type"]:checked')); | ||||
|     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) { | ||||
|     a.oninput = b.oninput = changed; | ||||
| if ("oninput" in a) { | ||||
|   a.oninput = b.oninput = changed; | ||||
| } else { | ||||
|     a.onkeyup = b.onkeyup = changed; | ||||
|   a.onkeyup = b.onkeyup = changed; | ||||
| } | ||||
|  | ||||
| function onDiffTypeChange(radio) { | ||||
|     window.diffType = radio.value; | ||||
| // Not necessary | ||||
| //	document.title = "Diff " + radio.value.slice(4); | ||||
|   window.diffType = radio.value; | ||||
|   // Not necessary | ||||
|   //	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++) { | ||||
|     radio[i].onchange = function (e) { | ||||
|         onDiffTypeChange(e.target); | ||||
|         changed(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| document.getElementById('ignoreWhitespace').onchange = function (e) { | ||||
|   radio[i].onchange = function (e) { | ||||
|     onDiffTypeChange(e.target); | ||||
|     changed(); | ||||
|   }; | ||||
| } | ||||
|  | ||||
| document.getElementById("ignoreWhitespace").onchange = function (e) { | ||||
|   changed(); | ||||
| }; | ||||
|  | ||||
| var inputs = document.getElementsByClassName('change'); | ||||
| var inputs = document.getElementsByClassName("change"); | ||||
| inputs.current = 0; | ||||
|  | ||||
|  | ||||
| function next_diff() { | ||||
|   var element = inputs[inputs.current]; | ||||
|   var headerOffset = 80; | ||||
|   var elementPosition = element.getBoundingClientRect().top; | ||||
|   var offsetPosition = elementPosition - headerOffset + window.scrollY; | ||||
|  | ||||
|     var element = inputs[inputs.current]; | ||||
|     var headerOffset = 80; | ||||
|     var elementPosition = element.getBoundingClientRect().top; | ||||
|     var offsetPosition = elementPosition - headerOffset + window.scrollY; | ||||
|   window.scrollTo({ | ||||
|     top: offsetPosition, | ||||
|     behavior: "smooth", | ||||
|   }); | ||||
|  | ||||
|     window.scrollTo({ | ||||
|         top: offsetPosition, | ||||
|         behavior: "smooth" | ||||
|     }); | ||||
|  | ||||
|     inputs.current++; | ||||
|     if (inputs.current >= inputs.length) { | ||||
|         inputs.current = 0; | ||||
|     } | ||||
|   inputs.current++; | ||||
|   if (inputs.current >= inputs.length) { | ||||
|     inputs.current = 0; | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										24
									
								
								changedetectionio/static/js/toggle-theme.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,24 @@ | ||||
| /** | ||||
|  * @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=/` | ||||
|   } | ||||
| }); | ||||
| @@ -1,10 +1,146 @@ | ||||
| /** | ||||
|  * 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"] .watch-controls img { | ||||
|     opacity: 0.4; } | ||||
|   html[data-darkmode="true"] .watch-table .unviewed { | ||||
|     color: #fff; } | ||||
|     html[data-darkmode="true"] .watch-table .unviewed.error { | ||||
|       color: var(--color-watch-table-error); } | ||||
|   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); } | ||||
|  | ||||
| #diff-ui { | ||||
|   background: #fff; | ||||
|   background: var(--color-background); | ||||
|   padding: 2em; | ||||
|   margin-left: 1em; | ||||
|   margin-right: 1em; | ||||
|   border-radius: 5px; | ||||
|   font-size: 11px; } | ||||
|   border-radius: 5px; } | ||||
|   #diff-ui #text { | ||||
|     font-size: 11px; } | ||||
|   #diff-ui table { | ||||
|     table-layout: fixed; | ||||
|     width: 100%; } | ||||
| @@ -45,6 +181,10 @@ ins { | ||||
|     margin-left: 1em; | ||||
|     display: inline-block; | ||||
|     font-weight: normal; } | ||||
|   #settings del { | ||||
|     padding: 0.5em; } | ||||
|   #settings ins { | ||||
|     padding: 0.5em; } | ||||
|  | ||||
| .source { | ||||
|   position: absolute; | ||||
|   | ||||
| @@ -1,96 +0,0 @@ | ||||
| #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%; | ||||
|   } | ||||
| } | ||||
| @@ -4,7 +4,8 @@ | ||||
|   "description": "", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|     "build": "node-sass styles.scss -o .;node-sass diff.scss -o ." | ||||
|     "watch": "node-sass -w scss -o .", | ||||
|     "build": "node-sass scss -o ." | ||||
|   }, | ||||
|   "author": "", | ||||
|   "license": "ISC", | ||||
|   | ||||
							
								
								
									
										121
									
								
								changedetectionio/static/styles/scss/diff.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,121 @@ | ||||
| @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%; | ||||
|   } | ||||
| } | ||||
| @@ -90,5 +90,6 @@ | ||||
|     &:hover { | ||||
|       cursor: pointer; | ||||
|     } | ||||
|     color: var(--color-grey-400); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										167
									
								
								changedetectionio/static/styles/scss/parts/_variables.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,167 @@ | ||||
| /** | ||||
|  * 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); | ||||
|  | ||||
|   // Anything that can't be manipulated through variables follows. | ||||
|   .watch-controls { | ||||
|     img { | ||||
|       opacity: 0.4; | ||||
|     } | ||||
|   } | ||||
|   .watch-table .unviewed { | ||||
|     color: #fff; | ||||
|     &.error { | ||||
|       color: var(--color-watch-table-error); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .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); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,42 +1,107 @@ | ||||
| /* | ||||
|  * -- 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/spinners"; | ||||
| @import "parts/browser-steps"; | ||||
| @import "parts/_arrows.scss"; | ||||
| @import "parts/_variables"; | ||||
| @import "parts/_spinners"; | ||||
| @import "parts/_browser-steps"; | ||||
| @import "parts/_arrows"; | ||||
| 
 | ||||
| body { | ||||
|   color: #333; | ||||
|   background: #262626; | ||||
|   color: var(--color-text); | ||||
|   background: var(--color-background-page); | ||||
| } | ||||
| 
 | ||||
| .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 { | ||||
|   background: #fff; | ||||
|   background: var(--color-background); | ||||
| } | ||||
| 
 | ||||
| /* Some styles from https://css-tricks.com/ */ | ||||
| a { | ||||
|   text-decoration: none; | ||||
|   color: #1b98f8; | ||||
|   color: var(--color-link); | ||||
| } | ||||
| 
 | ||||
| a.github-link { | ||||
|   color: #fff; | ||||
|   color: var(--color-icon-github); | ||||
|   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 { | ||||
|   background: #fff; | ||||
|   background: var(--color-background); | ||||
|   padding: 5px; | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   border-bottom: 2px solid #ed5900; | ||||
|   border-bottom: 2px solid var(--color-menu-accent); | ||||
|   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 { | ||||
|   padding-top: 5em; | ||||
|   padding-bottom: 1em; | ||||
| @@ -47,7 +112,8 @@ section.content { | ||||
| } | ||||
| 
 | ||||
| code { | ||||
|   background: #eee; | ||||
|   background: var(--color-background-code); | ||||
|   color: var(--color-text); | ||||
| } | ||||
| 
 | ||||
| /* table related */ | ||||
| @@ -55,30 +121,36 @@ code { | ||||
|   width: 100%; | ||||
|   font-size: 80%; | ||||
| 
 | ||||
|   tr.unviewed { | ||||
|     font-weight: bold; | ||||
|   tr { | ||||
|     &.unviewed { | ||||
|       font-weight: bold; | ||||
|     } | ||||
|     &.error { | ||||
|       color: var(--color-watch-table-error); | ||||
|     } | ||||
|     color: var(--color-watch-table-row-text); | ||||
|   } | ||||
| 
 | ||||
|   .error { | ||||
|     color: #a00; | ||||
|   } | ||||
| 
 | ||||
|   td { | ||||
|     white-space: nowrap; | ||||
|     &.title-col { | ||||
|       word-break: break-all; | ||||
|       white-space: normal; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   td.title-col { | ||||
|     word-break: break-all; | ||||
|     white-space: normal; | ||||
|   } | ||||
| 
 | ||||
|   th { | ||||
|     white-space: nowrap; | ||||
| 
 | ||||
|     a { | ||||
|       font-weight: normal; | ||||
| 
 | ||||
|       &.active { | ||||
|         font-weight: bolder; | ||||
|       } | ||||
| 
 | ||||
|       &.inactive { | ||||
|         .arrow { | ||||
|           display: none; | ||||
| @@ -87,14 +159,15 @@ code { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .title-col a[target="_blank"]::after, .current-diff-url::after { | ||||
|   .title-col a[target="_blank"]::after, | ||||
|   .current-diff-url::after { | ||||
|     content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==); | ||||
|     margin: 0 3px 0 5px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .watch-tag-list { | ||||
|   color: #e70069; | ||||
|   color: var(--color-text-watch-tag-list); | ||||
|   white-space: nowrap; | ||||
| } | ||||
| 
 | ||||
| @@ -126,10 +199,11 @@ code { | ||||
| 
 | ||||
| body:after { | ||||
|   content: ""; | ||||
|   background: linear-gradient(130deg, #5ad8f7, #2f50af 41.07%, #9150bf 84.05%); | ||||
|   background: linear-gradient(130deg, var(--color-background-gradient-first), var(--color-background-gradient-second) 41.07%, var(--color-background-gradient-third) 84.05%); | ||||
| } | ||||
| 
 | ||||
| body:after, body:before { | ||||
| body:after, | ||||
| body:before { | ||||
|   display: block; | ||||
|   height: 650px; | ||||
|   position: absolute; | ||||
| @@ -149,7 +223,8 @@ body::before { | ||||
|   background-size: cover | ||||
| } | ||||
| 
 | ||||
| body:after, body:before { | ||||
| body:after, | ||||
| 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%); | ||||
|   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%) | ||||
| } | ||||
| @@ -165,51 +240,57 @@ body:after, body:before { | ||||
|   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 { | ||||
|   color: white; | ||||
|   color: var(--color-text-button); | ||||
|   border-radius: 4px; | ||||
|   text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); | ||||
| } | ||||
| 
 | ||||
| .button-success { | ||||
|   background: rgb(28, 184, 65); | ||||
|   /* this is a green */ | ||||
|   background: var(--color-background-button-success); | ||||
| } | ||||
| 
 | ||||
| .button-tag { | ||||
|   background: rgb(99, 99, 99); | ||||
|   color: #fff; | ||||
|   background: var(--color-background-button-tag); | ||||
|   color: var(--color-text-button); | ||||
|   font-size: 65%; | ||||
|   border-bottom-left-radius: initial; | ||||
|   border-bottom-right-radius: initial; | ||||
| 
 | ||||
|   &.active { | ||||
|     background: #9c9c9c; | ||||
|     background: var(--color-background-button-tag-active); | ||||
|     font-weight: bold; | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| .button-error { | ||||
|   background: rgb(202, 60, 60); | ||||
|   /* this is a maroon */ | ||||
|   background: var(--color-background-button-error); | ||||
|   color: var(--color-text-button-error); | ||||
| } | ||||
| 
 | ||||
| .button-warning { | ||||
|   background: rgb(223, 117, 20); | ||||
|   /* this is an orange */ | ||||
|   background: var(--color-background-button-warning); | ||||
|   color: var(--color-text-button-warning); | ||||
| } | ||||
| 
 | ||||
| .button-secondary { | ||||
|   background: rgb(66, 184, 221); | ||||
|   /* this is a light blue */ | ||||
|   background: var(--color-background-button-secondary); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| .button-cancel { | ||||
|   background: rgb(200, 200, 200); | ||||
|   /* this is a green */ | ||||
|   background: var(--color-background-button-cancel); | ||||
| } | ||||
| 
 | ||||
| #save_button { | ||||
|   margin-right: 1rem; | ||||
| } | ||||
| 
 | ||||
| .messages { | ||||
| @@ -217,50 +298,62 @@ body:after, body:before { | ||||
|     list-style: none; | ||||
|     padding: 1em; | ||||
|     border-radius: 10px; | ||||
|     color: #fff; | ||||
|     color: var(--color-text-messages); | ||||
|     font-weight: bold; | ||||
| 
 | ||||
|     &.message { | ||||
|       background: rgba(255, 255, 255, .2); | ||||
|       background: var(--color-background-messages-message); | ||||
|     } | ||||
| 
 | ||||
|     &.error { | ||||
|       background: rgba(255, 1, 1, .5); | ||||
|       background: var(--color-background-messages-error); | ||||
|     } | ||||
| 
 | ||||
|     &.notice { | ||||
|       background: rgba(255, 255, 255, .5); | ||||
|       background: var(--color-background-messages-notice); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   &.with-share-link { | ||||
|     > *:hover { | ||||
|     >*:hover { | ||||
|       cursor: pointer; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .notifications-wrapper { | ||||
|   padding: 0.5rem 0 1rem 0; | ||||
| } | ||||
| 
 | ||||
| label { | ||||
|  &:hover { | ||||
|    cursor: pointer; | ||||
|  }   | ||||
| } | ||||
| 
 | ||||
| #notification-customisation { | ||||
|   border: 1px solid #ccc; | ||||
|   border: 1px solid var(--color-border-notification); | ||||
|   padding: 0.5rem; | ||||
|   border-radius: 5px; | ||||
| } | ||||
| 
 | ||||
| #notification-error-log { | ||||
|   border: 1px solid #ccc; | ||||
|   border: 1px solid var(--color-border-notification); | ||||
|   padding: 1rem; | ||||
|   border-radius: 5px; | ||||
|   overflow-wrap: break-word; | ||||
| } | ||||
| 
 | ||||
| #token-table { | ||||
|   &.pure-table td, &.pure-table th { | ||||
| 
 | ||||
|   &.pure-table td, | ||||
|   &.pure-table th { | ||||
|     font-size: 80%; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| #new-watch-form { | ||||
|   background: rgba(0, 0, 0, .05); | ||||
|   background: var(--color-background-new-watch-form); | ||||
|   padding: 1em; | ||||
|   border-radius: 10px; | ||||
|   margin-bottom: 1em; | ||||
| @@ -270,19 +363,25 @@ body:after, body:before { | ||||
|     margin-bottom: 5px; | ||||
|   } | ||||
| 
 | ||||
|   input:not(.pure-button) { | ||||
|     background-color: var(--color-background-new-watch-input); | ||||
|     color: var(--color-text-new-watch-input); | ||||
|   } | ||||
| 
 | ||||
|   .label { | ||||
|     display: none; | ||||
|   } | ||||
| 
 | ||||
|   legend { | ||||
|     color: #fff; | ||||
|     color: var(--color-text-legend); | ||||
|     font-weight: bold; | ||||
|   } | ||||
| 
 | ||||
|   #watch-add-wrapper-zone { | ||||
|     > div { | ||||
|     >div { | ||||
|       display: inline-block; | ||||
|     } | ||||
| 
 | ||||
|     @media only screen and (max-width: 760px) { | ||||
|       #url { | ||||
|         width: 100%; | ||||
| @@ -300,15 +399,15 @@ body:after, body:before { | ||||
|   position: fixed; | ||||
|   left: 0px; | ||||
|   top: 120px; | ||||
|   background: #fff; | ||||
|   background: var(--color-background); | ||||
|   padding: 10px; | ||||
|   border-top-right-radius: 5px; | ||||
|   border-bottom-right-radius: 5px; | ||||
|   box-shadow: 5px 0 5px -2px #888; | ||||
|   box-shadow: 1px 1px 4px var(--color-shadow-jump); | ||||
| 
 | ||||
|   a { | ||||
|     color: #1b98f8; | ||||
|     cursor: grabbing; | ||||
|     color: var(--color-link); | ||||
|     cursor: pointer; | ||||
|     -moz-user-select: none; | ||||
|     -webkit-user-select: none; | ||||
|     -ms-user-select: none; | ||||
| @@ -319,8 +418,8 @@ body:after, body:before { | ||||
| 
 | ||||
| footer { | ||||
|   padding: 10px; | ||||
|   background: #fff; | ||||
|   color: #444; | ||||
|   background: var(--color-background); | ||||
|   color: var(--color-text-footer); | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| @@ -343,7 +442,7 @@ footer { | ||||
|   position: absolute; | ||||
|   top: 60px; | ||||
|   font-size: 65%; | ||||
|   background: #fff; | ||||
|   background: var(--color-background); | ||||
|   padding: 10px; | ||||
| 
 | ||||
|   &#left-sticky { | ||||
| @@ -362,10 +461,12 @@ footer { | ||||
| } | ||||
| 
 | ||||
| #new-version-text a { | ||||
|   color: #e07171; | ||||
|   color: var(--color-link-new-version); | ||||
| } | ||||
| 
 | ||||
| .watch-controls { | ||||
|   color: #f8321b; | ||||
| 
 | ||||
|   .state-on { | ||||
|     img { | ||||
|       opacity: 0.8; | ||||
| @@ -383,7 +484,6 @@ footer { | ||||
|       opacity: 0.8; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| .monospaced-textarea { | ||||
| @@ -392,7 +492,8 @@ footer { | ||||
|     font-family: monospace; | ||||
|     white-space: pre; | ||||
|     overflow-wrap: normal; | ||||
|     overflow-x: scroll; | ||||
|     // No scrollbars until needed. | ||||
|     overflow-x: auto; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @@ -407,7 +508,9 @@ footer { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .pure-control-group, .pure-group, .pure-controls { | ||||
|   .pure-control-group, | ||||
|   .pure-group, | ||||
|   .pure-controls { | ||||
|     padding-bottom: 1em; | ||||
| 
 | ||||
|     div { | ||||
| @@ -415,28 +518,32 @@ footer { | ||||
|     } | ||||
| 
 | ||||
|     .checkbox { | ||||
|       > * { | ||||
|       >* { | ||||
|         display: inline; | ||||
|         vertical-align: middle; | ||||
|       } | ||||
| 
 | ||||
|       > label { | ||||
|       >label { | ||||
|         padding-left: 5px; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     legend { | ||||
|       color: var(--color-text-legend); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /* The input fields with errors */ | ||||
|   .error { | ||||
|     input { | ||||
|       background-color: #ffebeb; | ||||
|       background-color: var(--color-error-input); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /* The list of errors */ | ||||
|   ul.errors { | ||||
|     padding: .5em .6em; | ||||
|     border: 1px solid #dd0000; | ||||
|     border: 1px solid var(--color-error-list); | ||||
|     border-radius: 4px; | ||||
|     vertical-align: middle; | ||||
|     -webkit-box-sizing: border-box; | ||||
| @@ -444,7 +551,7 @@ footer { | ||||
| 
 | ||||
|     li { | ||||
|       margin-left: 1em; | ||||
|       color: #dd0000; | ||||
|       color: var(--color-error-list); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @@ -462,7 +569,7 @@ footer { | ||||
|       list-style: none; | ||||
| 
 | ||||
|       li { | ||||
|         > * { | ||||
|         >* { | ||||
|           display: inline-block; | ||||
|         } | ||||
|       } | ||||
| @@ -471,21 +578,25 @@ footer { | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @media only screen and (max-width: 760px), (min-device-width: 768px) and (max-device-width: 1024px) { | ||||
| @media only screen and (max-width: 760px), | ||||
| (min-device-width: 768px) and (max-device-width: 1024px) { | ||||
|   .box { | ||||
|     max-width: 95% | ||||
|   } | ||||
| 
 | ||||
|   .edit-form { | ||||
|     padding: 0.5em; | ||||
|     margin: 0; | ||||
|   } | ||||
| 
 | ||||
|   #nav-menu { | ||||
|     overflow-x: scroll; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @media only screen and (max-width: 760px), (min-device-width: 768px) and (max-device-width: 800px) { | ||||
| @media only screen and (max-width: 760px), | ||||
| (min-device-width: 768px) and (max-device-width: 800px) { | ||||
| 
 | ||||
|   div.sticky-tab#hosted-sticky { | ||||
|     top: 60px; | ||||
| @@ -514,24 +625,29 @@ footer { | ||||
|   and also iPads specifically. | ||||
|   */ | ||||
|   .watch-table { | ||||
| 
 | ||||
|     /* Force table to not be like tables anymore */ | ||||
|     thead, tbody, th, td, tr { | ||||
|     thead, | ||||
|     tbody, | ||||
|     th, | ||||
|     td, | ||||
|     tr { | ||||
|       display: block; | ||||
|     } | ||||
| 
 | ||||
|     .last-checked { | ||||
|       > span { | ||||
|       >span { | ||||
|         vertical-align: middle; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     .last-checked::before { | ||||
|       color: #555; | ||||
|       color: var(--color-last-checked); | ||||
|       content: "Last Checked "; | ||||
|     } | ||||
| 
 | ||||
|     .last-changed::before { | ||||
|       color: #555; | ||||
|       color: var(--color-last-checked); | ||||
|       content: "Last Changed "; | ||||
|     } | ||||
| 
 | ||||
| @@ -547,15 +663,17 @@ footer { | ||||
|       left: -9999px; | ||||
|     } | ||||
| 
 | ||||
|     .pure-table td, .pure-table th { | ||||
|     .pure-table td, | ||||
|     .pure-table th { | ||||
|       border: none; | ||||
|     } | ||||
| 
 | ||||
|     td { | ||||
|       /* Behave  like a "row" */ | ||||
|       border: none; | ||||
|       border-bottom: 1px solid #eee; | ||||
|       border-bottom: 1px solid var(--color-border-watch-table-cell); | ||||
|       vertical-align: middle; | ||||
| 
 | ||||
|       &:before { | ||||
|         /* Top/left values mimic padding */ | ||||
|         top: 6px; | ||||
| @@ -568,11 +686,11 @@ footer { | ||||
| 
 | ||||
|     &.pure-table-striped { | ||||
|       tr { | ||||
|         background-color: #fff; | ||||
|         background-color: var(--color-table-background); | ||||
|       } | ||||
| 
 | ||||
|       tr:nth-child(2n-1) { | ||||
|         background-color: #eee; | ||||
|         background-color: var(--color-table-stripe); | ||||
|       } | ||||
| 
 | ||||
|       tr:nth-child(2n-1) td { | ||||
| @@ -583,12 +701,66 @@ 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 | ||||
| - We dont use 'size' with <input> because `size` is too unreliable to override, and will often push-out | ||||
| - Rely always on width in CSS | ||||
| */ | ||||
| @media only screen and (min-width: 761px) { | ||||
| 
 | ||||
|   /* m-d is medium-desktop */ | ||||
|   .m-d { | ||||
|     min-width: 80%; | ||||
| @@ -605,20 +777,23 @@ footer { | ||||
|     li { | ||||
|       margin-right: 3px; | ||||
|       display: inline-block; | ||||
|       color: #fff; | ||||
|       color: var(--color-text-tab); | ||||
|       border-top-left-radius: 5px; | ||||
|       border-top-right-radius: 5px; | ||||
|       background-color: rgba(255, 255, 255, 0.2); | ||||
|       background-color: var(--color-background-tab); | ||||
| 
 | ||||
|       &:not(.active) { | ||||
|         &:hover { | ||||
|           background-color: rgba(255, 255, 255, 0.5); | ||||
|           background-color: var(--color-background-tab-hover); | ||||
|         } | ||||
|       } | ||||
|       &.active, :target { | ||||
|         background-color: #fff; | ||||
| 
 | ||||
|       &.active, | ||||
|       :target { | ||||
|         background-color: var(--color-background); | ||||
| 
 | ||||
|         a { | ||||
|           color: #222; | ||||
|           color: var(--color-text-tab-active); | ||||
|           font-weight: bold; | ||||
|         } | ||||
|       } | ||||
| @@ -626,22 +801,24 @@ footer { | ||||
|       a { | ||||
|         display: block; | ||||
|         padding: 0.8em; | ||||
|         color: #fff; | ||||
|         color: var(--color-text-tab); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| $form-edge-padding: 20px; | ||||
| 
 | ||||
| .pure-form-stacked { | ||||
|   > div:first-child { | ||||
|   >div:first-child { | ||||
|     display: block; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .login-form { | ||||
|   .inner { | ||||
|     background: #fff;; | ||||
|     background: var(--color-background); | ||||
|     ; | ||||
|     padding: $form-edge-padding; | ||||
|     border-radius: 5px; | ||||
|   } | ||||
| @@ -671,11 +848,13 @@ $form-edge-padding: 20px; | ||||
| #selector-header { | ||||
|   padding-bottom: 1em; | ||||
| } | ||||
| 
 | ||||
| body.full-width { | ||||
|   .edit-form { | ||||
|     width: 95%; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .edit-form { | ||||
|   min-width: 70%; | ||||
|   /* so it cant overflow */ | ||||
| @@ -686,17 +865,21 @@ body.full-width { | ||||
|   } | ||||
| 
 | ||||
|   .inner { | ||||
|     background: #fff;; | ||||
|     background: var(--color-background); | ||||
|     padding: $form-edge-padding; | ||||
|   } | ||||
| 
 | ||||
|   #actions { | ||||
|     display: block; | ||||
|     background: #fff; | ||||
|     background: var(--color-background); | ||||
|   } | ||||
| 
 | ||||
|   .pure-form-message-inline { | ||||
|     padding-left: 0; | ||||
|     color: var(--color-text-input-description); | ||||
|     code { | ||||
|       font-size: .875em; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @@ -720,14 +903,15 @@ ul { | ||||
|   height: 100%; | ||||
|   overflow-y: scroll; | ||||
|   position: relative; | ||||
| 
 | ||||
|   //width: 100%; | ||||
|   > img { | ||||
|   >img { | ||||
|     position: absolute; | ||||
|     z-index: 4; | ||||
|     max-width: 100%; | ||||
|   } | ||||
| 
 | ||||
|   > canvas { | ||||
|   >canvas { | ||||
|     position: relative; | ||||
|     z-index: 5; | ||||
|     max-width: 100%; | ||||
| @@ -755,54 +939,61 @@ ul { | ||||
| } | ||||
| 
 | ||||
| #api-key-copy { | ||||
|   color: #0078e7; | ||||
|   color: var(--color-api-key); | ||||
| } | ||||
| 
 | ||||
| .button-green { | ||||
|   background-color: #42dd53; | ||||
|   background-color: var(--color-background-button-green); | ||||
| } | ||||
| 
 | ||||
| .button-red { | ||||
|   background-color: #dd4242; | ||||
|   background-color: var(--color-background-button-red); | ||||
| } | ||||
| 
 | ||||
| .noselect { | ||||
|   -webkit-touch-callout: none; /* iOS Safari */ | ||||
|   -webkit-user-select: none; /* Safari */ | ||||
|   -moz-user-select: none; /* Old versions of Firefox */ | ||||
|   -ms-user-select: none; /* Internet Explorer/Edge */ | ||||
|   -webkit-touch-callout: none; | ||||
|   /* iOS Safari */ | ||||
|   -webkit-user-select: none; | ||||
|   /* Safari */ | ||||
|   -moz-user-select: none; | ||||
|   /* Old versions of Firefox */ | ||||
|   -ms-user-select: none; | ||||
|   /* Internet Explorer/Edge */ | ||||
|   user-select: none; | ||||
|   /* Non-prefixed version, currently | ||||
|                                    supported by Chrome, Edge, Opera and Firefox */ | ||||
|     supported by Chrome, Edge, Opera and Firefox */ | ||||
| } | ||||
| 
 | ||||
| .snapshot-age { | ||||
|   padding: 4px; | ||||
|   background-color: #dfdfdf; | ||||
|   margin: 0.5rem 0; | ||||
|   background-color: var(--color-background-snapshot-age); | ||||
|   border-radius: 3px; | ||||
|   font-weight: bold; | ||||
|   margin-bottom: 4px; | ||||
| 
 | ||||
|   &.error { | ||||
|     background-color: #ff0000; | ||||
|     color: #fff; | ||||
|     background-color: var(--color-error-background-snapshot-age); | ||||
|     color: var(--color-error-text-snapshot-age); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| #checkbox-operations { | ||||
|   background: rgba(0, 0, 0, 0.05); | ||||
|   background: var(--color-background-checkbox-operations); | ||||
|   padding: 1em; | ||||
|   border-radius: 10px; | ||||
|   margin-bottom: 1em; | ||||
|   display: none; | ||||
| } | ||||
| 
 | ||||
| .checkbox-uuid { | ||||
|   > * { | ||||
|   >* { | ||||
|     vertical-align: middle; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .inline-warning { | ||||
|   > span { | ||||
|   >span { | ||||
|     display: inline-block; | ||||
|     vertical-align: middle; | ||||
|   } | ||||
| @@ -813,8 +1004,8 @@ ul { | ||||
|     vertical-align: middle; | ||||
|   } | ||||
| 
 | ||||
|   border: 1px solid #ff3300; | ||||
|   border: 1px solid var(--color-border-warning); | ||||
|   padding: 0.5rem; | ||||
|   border-radius: 5px; | ||||
|   color: #ff3300; | ||||
|   color: var(--color-warning); | ||||
| } | ||||
| @@ -1,9 +1,141 @@ | ||||
| /* | ||||
|  * -- 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"] .watch-controls img { | ||||
|     opacity: 0.4; } | ||||
|   html[data-darkmode="true"] .watch-table .unviewed { | ||||
|     color: #fff; } | ||||
|     html[data-darkmode="true"] .watch-table .unviewed.error { | ||||
|       color: var(--color-watch-table-error); } | ||||
|   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); } | ||||
|  | ||||
| /* spinner */ | ||||
| .spinner, | ||||
| .spinner:after { | ||||
| @@ -105,8 +237,10 @@ nvm use v14.18.1 && npm install && npm run build | ||||
|     width: 80px; | ||||
|     height: 80px; | ||||
|     font-size: 3px; } | ||||
|   #browsersteps-selector-wrapper #browsersteps-click-start:hover { | ||||
|     cursor: pointer; } | ||||
|   #browsersteps-selector-wrapper #browsersteps-click-start { | ||||
|     color: var(--color-grey-400); } | ||||
|     #browsersteps-selector-wrapper #browsersteps-click-start:hover { | ||||
|       cursor: pointer; } | ||||
|  | ||||
| .arrow { | ||||
|   border: solid #1b98f8; | ||||
| @@ -127,28 +261,70 @@ nvm use v14.18.1 && npm install && npm run build | ||||
|     -webkit-transform: rotate(45deg); } | ||||
|  | ||||
| body { | ||||
|   color: #333; | ||||
|   background: #262626; } | ||||
|   color: var(--color-text); | ||||
|   background: var(--color-background-page); } | ||||
|  | ||||
| .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 { | ||||
|   background: #fff; } | ||||
|   background: var(--color-background); } | ||||
|  | ||||
| /* Some styles from https://css-tricks.com/ */ | ||||
| a { | ||||
|   text-decoration: none; | ||||
|   color: #1b98f8; } | ||||
|   color: var(--color-link); } | ||||
|  | ||||
| a.github-link { | ||||
|   color: #fff; } | ||||
|   color: var(--color-icon-github); | ||||
|   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 { | ||||
|   background: #fff; | ||||
|   background: var(--color-background); | ||||
|   padding: 5px; | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   border-bottom: 2px solid #ed5900; | ||||
|   border-bottom: 2px solid var(--color-menu-accent); | ||||
|   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 { | ||||
|   padding-top: 5em; | ||||
|   padding-bottom: 1em; | ||||
| @@ -158,21 +334,24 @@ section.content { | ||||
|   justify-content: center; } | ||||
|  | ||||
| code { | ||||
|   background: #eee; } | ||||
|   background: var(--color-background-code); | ||||
|   color: var(--color-text); } | ||||
|  | ||||
| /* table related */ | ||||
| .watch-table { | ||||
|   width: 100%; | ||||
|   font-size: 80%; } | ||||
|   .watch-table tr.unviewed { | ||||
|     font-weight: bold; } | ||||
|   .watch-table .error { | ||||
|     color: #a00; } | ||||
|   .watch-table tr { | ||||
|     color: var(--color-watch-table-row-text); } | ||||
|     .watch-table tr.unviewed { | ||||
|       font-weight: bold; } | ||||
|     .watch-table tr.error { | ||||
|       color: var(--color-watch-table-error); } | ||||
|   .watch-table td { | ||||
|     white-space: nowrap; } | ||||
|   .watch-table td.title-col { | ||||
|     word-break: break-all; | ||||
|     white-space: normal; } | ||||
|     .watch-table td.title-col { | ||||
|       word-break: break-all; | ||||
|       white-space: normal; } | ||||
|   .watch-table th { | ||||
|     white-space: nowrap; } | ||||
|     .watch-table th a { | ||||
| @@ -181,12 +360,13 @@ code { | ||||
|         font-weight: bolder; } | ||||
|       .watch-table th a.inactive .arrow { | ||||
|         display: none; } | ||||
|   .watch-table .title-col a[target="_blank"]::after, .watch-table .current-diff-url::after { | ||||
|   .watch-table .title-col a[target="_blank"]::after, | ||||
|   .watch-table .current-diff-url::after { | ||||
|     content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==); | ||||
|     margin: 0 3px 0 5px; } | ||||
|  | ||||
| .watch-tag-list { | ||||
|   color: #e70069; | ||||
|   color: var(--color-text-watch-tag-list); | ||||
|   white-space: nowrap; } | ||||
|  | ||||
| .box { | ||||
| @@ -209,9 +389,10 @@ code { | ||||
|  | ||||
| body:after { | ||||
|   content: ""; | ||||
|   background: linear-gradient(130deg, #5ad8f7, #2f50af 41.07%, #9150bf 84.05%); } | ||||
|   background: linear-gradient(130deg, var(--color-background-gradient-first), var(--color-background-gradient-second) 41.07%, var(--color-background-gradient-third) 84.05%); } | ||||
|  | ||||
| body:after, body:before { | ||||
| body:after, | ||||
| body:before { | ||||
|   display: block; | ||||
|   height: 650px; | ||||
|   position: absolute; | ||||
| @@ -227,7 +408,8 @@ body::before { | ||||
|   content: ""; | ||||
|   background-size: cover; } | ||||
|  | ||||
| body:after, body:before { | ||||
| body:after, | ||||
| 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%); | ||||
|   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,83 +422,99 @@ body:after, body:before { | ||||
|   max-width: 400px; | ||||
|   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 { | ||||
|   color: white; | ||||
|   color: var(--color-text-button); | ||||
|   border-radius: 4px; | ||||
|   text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); } | ||||
|  | ||||
| .button-success { | ||||
|   background: #1cb841; | ||||
|   /* this is a green */ } | ||||
|   background: var(--color-background-button-success); } | ||||
|  | ||||
| .button-tag { | ||||
|   background: #636363; | ||||
|   color: #fff; | ||||
|   background: var(--color-background-button-tag); | ||||
|   color: var(--color-text-button); | ||||
|   font-size: 65%; | ||||
|   border-bottom-left-radius: initial; | ||||
|   border-bottom-right-radius: initial; } | ||||
|   .button-tag.active { | ||||
|     background: #9c9c9c; | ||||
|     background: var(--color-background-button-tag-active); | ||||
|     font-weight: bold; } | ||||
|  | ||||
| .button-error { | ||||
|   background: #ca3c3c; | ||||
|   /* this is a maroon */ } | ||||
|   background: var(--color-background-button-error); | ||||
|   color: var(--color-text-button-error); } | ||||
|  | ||||
| .button-warning { | ||||
|   background: #df7514; | ||||
|   /* this is an orange */ } | ||||
|   background: var(--color-background-button-warning); | ||||
|   color: var(--color-text-button-warning); } | ||||
|  | ||||
| .button-secondary { | ||||
|   background: #42b8dd; | ||||
|   /* this is a light blue */ } | ||||
|   background: var(--color-background-button-secondary); } | ||||
|  | ||||
| .button-cancel { | ||||
|   background: #c8c8c8; | ||||
|   /* this is a green */ } | ||||
|   background: var(--color-background-button-cancel); } | ||||
|  | ||||
| #save_button { | ||||
|   margin-right: 1rem; } | ||||
|  | ||||
| .messages li { | ||||
|   list-style: none; | ||||
|   padding: 1em; | ||||
|   border-radius: 10px; | ||||
|   color: #fff; | ||||
|   color: var(--color-text-messages); | ||||
|   font-weight: bold; } | ||||
|   .messages li.message { | ||||
|     background: rgba(255, 255, 255, 0.2); } | ||||
|     background: var(--color-background-messages-message); } | ||||
|   .messages li.error { | ||||
|     background: rgba(255, 1, 1, 0.5); } | ||||
|     background: var(--color-background-messages-error); } | ||||
|   .messages li.notice { | ||||
|     background: rgba(255, 255, 255, 0.5); } | ||||
|     background: var(--color-background-messages-notice); } | ||||
|  | ||||
| .messages.with-share-link > *:hover { | ||||
|   cursor: pointer; } | ||||
|  | ||||
| .notifications-wrapper { | ||||
|   padding: 0.5rem 0 1rem 0; } | ||||
|  | ||||
| label:hover { | ||||
|   cursor: pointer; } | ||||
|  | ||||
| #notification-customisation { | ||||
|   border: 1px solid #ccc; | ||||
|   border: 1px solid var(--color-border-notification); | ||||
|   padding: 0.5rem; | ||||
|   border-radius: 5px; } | ||||
|  | ||||
| #notification-error-log { | ||||
|   border: 1px solid #ccc; | ||||
|   border: 1px solid var(--color-border-notification); | ||||
|   padding: 1rem; | ||||
|   border-radius: 5px; | ||||
|   overflow-wrap: break-word; } | ||||
|  | ||||
| #token-table.pure-table td, #token-table.pure-table th { | ||||
| #token-table.pure-table td, | ||||
| #token-table.pure-table th { | ||||
|   font-size: 80%; } | ||||
|  | ||||
| #new-watch-form { | ||||
|   background: rgba(0, 0, 0, 0.05); | ||||
|   background: var(--color-background-new-watch-form); | ||||
|   padding: 1em; | ||||
|   border-radius: 10px; | ||||
|   margin-bottom: 1em; } | ||||
|   #new-watch-form input { | ||||
|     display: inline-block; | ||||
|     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 { | ||||
|     display: none; } | ||||
|   #new-watch-form legend { | ||||
|     color: #fff; | ||||
|     color: var(--color-text-legend); | ||||
|     font-weight: bold; } | ||||
|   #new-watch-form #watch-add-wrapper-zone > div { | ||||
|     display: inline-block; } | ||||
| @@ -331,14 +529,14 @@ body:after, body:before { | ||||
|   position: fixed; | ||||
|   left: 0px; | ||||
|   top: 120px; | ||||
|   background: #fff; | ||||
|   background: var(--color-background); | ||||
|   padding: 10px; | ||||
|   border-top-right-radius: 5px; | ||||
|   border-bottom-right-radius: 5px; | ||||
|   box-shadow: 5px 0 5px -2px #888; } | ||||
|   box-shadow: 1px 1px 4px var(--color-shadow-jump); } | ||||
|   #diff-jump a { | ||||
|     color: #1b98f8; | ||||
|     cursor: grabbing; | ||||
|     color: var(--color-link); | ||||
|     cursor: pointer; | ||||
|     -moz-user-select: none; | ||||
|     -webkit-user-select: none; | ||||
|     -ms-user-select: none; | ||||
| @@ -347,8 +545,8 @@ body:after, body:before { | ||||
|  | ||||
| footer { | ||||
|   padding: 10px; | ||||
|   background: #fff; | ||||
|   color: #444; | ||||
|   background: var(--color-background); | ||||
|   color: var(--color-text-footer); | ||||
|   text-align: center; } | ||||
|  | ||||
| #feed-icon { | ||||
| @@ -367,7 +565,7 @@ footer { | ||||
|   position: absolute; | ||||
|   top: 60px; | ||||
|   font-size: 65%; | ||||
|   background: #fff; | ||||
|   background: var(--color-background); | ||||
|   padding: 10px; } | ||||
|   .sticky-tab#left-sticky { | ||||
|     left: 0px; } | ||||
| @@ -379,9 +577,10 @@ footer { | ||||
|     font-weight: bold; } | ||||
|  | ||||
| #new-version-text a { | ||||
|   color: #e07171; } | ||||
|   color: var(--color-link-new-version); } | ||||
|  | ||||
| .watch-controls { | ||||
|   color: #f8321b; | ||||
|   /* default */ } | ||||
|   .watch-controls .state-on img { | ||||
|     opacity: 0.8; } | ||||
| @@ -396,7 +595,7 @@ footer { | ||||
|   font-family: monospace; | ||||
|   white-space: pre; | ||||
|   overflow-wrap: normal; | ||||
|   overflow-x: scroll; } | ||||
|   overflow-x: auto; } | ||||
|  | ||||
| .pure-form { | ||||
|   /* The input fields with errors */ | ||||
| @@ -406,27 +605,39 @@ footer { | ||||
|     .pure-form fieldset ul { | ||||
|       padding-bottom: 0px; | ||||
|       margin-bottom: 0px; } | ||||
|   .pure-form .pure-control-group, .pure-form .pure-group, .pure-form .pure-controls { | ||||
|   .pure-form .pure-control-group, | ||||
|   .pure-form .pure-group, | ||||
|   .pure-form .pure-controls { | ||||
|     padding-bottom: 1em; } | ||||
|     .pure-form .pure-control-group div, .pure-form .pure-group div, .pure-form .pure-controls div { | ||||
|     .pure-form .pure-control-group div, | ||||
|     .pure-form .pure-group div, | ||||
|     .pure-form .pure-controls div { | ||||
|       margin: 0px; } | ||||
|     .pure-form .pure-control-group .checkbox > *, .pure-form .pure-group .checkbox > *, .pure-form .pure-controls .checkbox > * { | ||||
|     .pure-form .pure-control-group .checkbox > *, | ||||
|     .pure-form .pure-group .checkbox > *, | ||||
|     .pure-form .pure-controls .checkbox > * { | ||||
|       display: inline; | ||||
|       vertical-align: middle; } | ||||
|     .pure-form .pure-control-group .checkbox > label, .pure-form .pure-group .checkbox > label, .pure-form .pure-controls .checkbox > label { | ||||
|     .pure-form .pure-control-group .checkbox > label, | ||||
|     .pure-form .pure-group .checkbox > label, | ||||
|     .pure-form .pure-controls .checkbox > label { | ||||
|       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 { | ||||
|     background-color: #ffebeb; } | ||||
|     background-color: var(--color-error-input); } | ||||
|   .pure-form ul.errors { | ||||
|     padding: .5em .6em; | ||||
|     border: 1px solid #dd0000; | ||||
|     border: 1px solid var(--color-error-list); | ||||
|     border-radius: 4px; | ||||
|     vertical-align: middle; | ||||
|     -webkit-box-sizing: border-box; | ||||
|     box-sizing: border-box; } | ||||
|     .pure-form ul.errors li { | ||||
|       margin-left: 1em; | ||||
|       color: #dd0000; } | ||||
|       color: var(--color-error-list); } | ||||
|   .pure-form label { | ||||
|     font-weight: bold; } | ||||
|   .pure-form textarea { | ||||
| @@ -468,15 +679,19 @@ footer { | ||||
|     /* 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) */ } | ||||
|     .watch-table thead, .watch-table tbody, .watch-table th, .watch-table td, .watch-table tr { | ||||
|     .watch-table thead, | ||||
|     .watch-table tbody, | ||||
|     .watch-table th, | ||||
|     .watch-table td, | ||||
|     .watch-table tr { | ||||
|       display: block; } | ||||
|     .watch-table .last-checked > span { | ||||
|       vertical-align: middle; } | ||||
|     .watch-table .last-checked::before { | ||||
|       color: #555; | ||||
|       color: var(--color-last-checked); | ||||
|       content: "Last Checked "; } | ||||
|     .watch-table .last-changed::before { | ||||
|       color: #555; | ||||
|       color: var(--color-last-checked); | ||||
|       content: "Last Changed "; } | ||||
|     .watch-table td.inline { | ||||
|       display: inline-block; } | ||||
| @@ -484,12 +699,13 @@ footer { | ||||
|       position: absolute; | ||||
|       top: -9999px; | ||||
|       left: -9999px; } | ||||
|     .watch-table .pure-table td, .watch-table .pure-table th { | ||||
|     .watch-table .pure-table td, | ||||
|     .watch-table .pure-table th { | ||||
|       border: none; } | ||||
|     .watch-table td { | ||||
|       /* Behave  like a "row" */ | ||||
|       border: none; | ||||
|       border-bottom: 1px solid #eee; | ||||
|       border-bottom: 1px solid var(--color-border-watch-table-cell); | ||||
|       vertical-align: middle; } | ||||
|       .watch-table td:before { | ||||
|         /* Top/left values mimic padding */ | ||||
| @@ -499,12 +715,66 @@ footer { | ||||
|         padding-right: 10px; | ||||
|         white-space: nowrap; } | ||||
|     .watch-table.pure-table-striped tr { | ||||
|       background-color: #fff; } | ||||
|       background-color: var(--color-table-background); } | ||||
|     .watch-table.pure-table-striped tr:nth-child(2n-1) { | ||||
|       background-color: #eee; } | ||||
|       background-color: var(--color-table-stripe); } | ||||
|     .watch-table.pure-table-striped tr:nth-child(2n-1) td { | ||||
|       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 | ||||
| - We dont use 'size' with <input> because `size` is too unreliable to override, and will often push-out | ||||
| - Rely always on width in CSS | ||||
| @@ -521,27 +791,29 @@ footer { | ||||
|   .tabs ul li { | ||||
|     margin-right: 3px; | ||||
|     display: inline-block; | ||||
|     color: #fff; | ||||
|     color: var(--color-text-tab); | ||||
|     border-top-left-radius: 5px; | ||||
|     border-top-right-radius: 5px; | ||||
|     background-color: rgba(255, 255, 255, 0.2); } | ||||
|     background-color: var(--color-background-tab); } | ||||
|     .tabs ul li:not(.active):hover { | ||||
|       background-color: rgba(255, 255, 255, 0.5); } | ||||
|     .tabs ul li.active, .tabs ul li :target { | ||||
|       background-color: #fff; } | ||||
|       .tabs ul li.active a, .tabs ul li :target a { | ||||
|         color: #222; | ||||
|       background-color: var(--color-background-tab-hover); } | ||||
|     .tabs ul li.active, | ||||
|     .tabs ul li :target { | ||||
|       background-color: var(--color-background); } | ||||
|       .tabs ul li.active a, | ||||
|       .tabs ul li :target a { | ||||
|         color: var(--color-text-tab-active); | ||||
|         font-weight: bold; } | ||||
|     .tabs ul li a { | ||||
|       display: block; | ||||
|       padding: 0.8em; | ||||
|       color: #fff; } | ||||
|       color: var(--color-text-tab); } | ||||
|  | ||||
| .pure-form-stacked > div:first-child { | ||||
|   display: block; } | ||||
|  | ||||
| .login-form .inner { | ||||
|   background: #fff; | ||||
|   background: var(--color-background); | ||||
|   padding: 20px; | ||||
|   border-radius: 5px; } | ||||
|  | ||||
| @@ -571,13 +843,16 @@ body.full-width .edit-form { | ||||
|   .edit-form .box-wrap { | ||||
|     position: relative; } | ||||
|   .edit-form .inner { | ||||
|     background: #fff; | ||||
|     background: var(--color-background); | ||||
|     padding: 20px; } | ||||
|   .edit-form #actions { | ||||
|     display: block; | ||||
|     background: #fff; } | ||||
|     background: var(--color-background); } | ||||
|   .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 { | ||||
|   padding-left: 1em; | ||||
| @@ -614,13 +889,13 @@ ul { | ||||
|   cursor: pointer; } | ||||
|  | ||||
| #api-key-copy { | ||||
|   color: #0078e7; } | ||||
|   color: var(--color-api-key); } | ||||
|  | ||||
| .button-green { | ||||
|   background-color: #42dd53; } | ||||
|   background-color: var(--color-background-button-green); } | ||||
|  | ||||
| .button-red { | ||||
|   background-color: #dd4242; } | ||||
|   background-color: var(--color-background-button-red); } | ||||
|  | ||||
| .noselect { | ||||
|   -webkit-touch-callout: none; | ||||
| @@ -633,20 +908,21 @@ ul { | ||||
|   /* Internet Explorer/Edge */ | ||||
|   user-select: none; | ||||
|   /* Non-prefixed version, currently | ||||
|                                    supported by Chrome, Edge, Opera and Firefox */ } | ||||
|     supported by Chrome, Edge, Opera and Firefox */ } | ||||
|  | ||||
| .snapshot-age { | ||||
|   padding: 4px; | ||||
|   background-color: #dfdfdf; | ||||
|   margin: 0.5rem 0; | ||||
|   background-color: var(--color-background-snapshot-age); | ||||
|   border-radius: 3px; | ||||
|   font-weight: bold; | ||||
|   margin-bottom: 4px; } | ||||
|   .snapshot-age.error { | ||||
|     background-color: #ff0000; | ||||
|     color: #fff; } | ||||
|     background-color: var(--color-error-background-snapshot-age); | ||||
|     color: var(--color-error-text-snapshot-age); } | ||||
|  | ||||
| #checkbox-operations { | ||||
|   background: rgba(0, 0, 0, 0.05); | ||||
|   background: var(--color-background-checkbox-operations); | ||||
|   padding: 1em; | ||||
|   border-radius: 10px; | ||||
|   margin-bottom: 1em; | ||||
| @@ -656,10 +932,10 @@ ul { | ||||
|   vertical-align: middle; } | ||||
|  | ||||
| .inline-warning { | ||||
|   border: 1px solid #ff3300; | ||||
|   border: 1px solid var(--color-border-warning); | ||||
|   padding: 0.5rem; | ||||
|   border-radius: 5px; | ||||
|   color: #ff3300; } | ||||
|   color: var(--color-warning); } | ||||
|   .inline-warning > span { | ||||
|     display: inline-block; | ||||
|     vertical-align: middle; } | ||||
|   | ||||
| @@ -621,4 +621,44 @@ class ChangeDetectionStore: | ||||
|                     watch['include_filters'] = [existing_filter] | ||||
|             except: | ||||
|                 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: | ||||
|                     self.data['settings']['application']['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 | ||||
|   | ||||
| @@ -16,14 +16,16 @@ | ||||
|                                 <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> 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> | ||||
|                             </div> | ||||
|                             <br/> | ||||
|                             <a id="send-test-notification" class="pure-button button-secondary button-xsmall" style="font-size: 70%">Send test notification</a> | ||||
| {% if emailprefix %} | ||||
|                             <a id="add-email-helper" class="pure-button button-secondary button-xsmall" style="font-size: 70%">Add email</a> | ||||
| {% endif %} | ||||
|                             <a href="{{url_for('notification_logs')}}" class="pure-button button-secondary button-xsmall" style="font-size: 70%">Notification debug logs</a> | ||||
|                             <div class="notifications-wrapper"> | ||||
|                               <a id="send-test-notification" class="pure-button button-secondary button-xsmall" style="font-size: 70%">Send test notification</a> | ||||
|                             {% if emailprefix %} | ||||
|                               <a id="add-email-helper" class="pure-button button-secondary button-xsmall" style="font-size: 70%">Add email</a> | ||||
|                             {% endif %} | ||||
|                               <a href="{{url_for('notification_logs')}}" class="pure-button button-secondary button-xsmall" style="font-size: 70%">Notification debug logs</a> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div id="notification-customisation" class="pure-control-group"> | ||||
|                             <div class="pure-control-group"> | ||||
| @@ -40,8 +42,9 @@ | ||||
|                                 <span class="pure-form-message-inline">Format for all notifications</span> | ||||
|                             </div> | ||||
|                             <div class="pure-controls"> | ||||
|                             <span class="pure-form-message-inline"> | ||||
|                                 These tokens can be used in the notification body and title to customise the notification text. | ||||
|                             <p 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. | ||||
|                             </p> | ||||
|  | ||||
|                                 <table class="pure-table" id="token-table"> | ||||
|                                     <thead> | ||||
| @@ -52,52 +55,49 @@ | ||||
|                                     </thead> | ||||
|                                     <tbody> | ||||
|                                     <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> | ||||
|                                     </tr> | ||||
|                                     <tr> | ||||
|                                         <td><code>{watch_url}</code></td> | ||||
|                                         <td><code>{{ '{{ watch_url }}' }}</code></td> | ||||
|                                         <td>The URL being watched.</td> | ||||
|                                     </tr> | ||||
|                                     <tr> | ||||
|                                         <td><code>{watch_uuid}</code></td> | ||||
|                                         <td><code>{{ '{{ watch_uuid }}' }}</code></td> | ||||
|                                         <td>The UUID of the watch.</td> | ||||
|                                     </tr> | ||||
|                                     <tr> | ||||
|                                         <td><code>{watch_title}</code></td> | ||||
|                                         <td><code>{{ '{{ watch_title }}' }}</code></td> | ||||
|                                         <td>The title of the watch.</td> | ||||
|                                     </tr> | ||||
|                                     <tr> | ||||
|                                         <td><code>{watch_tag}</code></td> | ||||
|                                         <td><code>{{ '{{ watch_tag }}' }}</code></td> | ||||
|                                         <td>The tag of the watch.</td> | ||||
|                                     </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> | ||||
|                                     </tr> | ||||
|                                     <tr> | ||||
|                                         <td><code>{diff}</code></td> | ||||
|                                         <td><code>{{ '{{ diff_url }}' }}</code></td> | ||||
|                                         <td>The diff output - differences only</td> | ||||
|                                     </tr> | ||||
|                                     <tr> | ||||
|                                         <td><code>{diff_full}</code></td> | ||||
|                                         <td><code>{{ '{{ diff_full }}' }}</code></td> | ||||
|                                         <td>The diff output - full difference output</td> | ||||
|                                     </tr> | ||||
|                                     <tr> | ||||
|                                         <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><code>{{ '{{ current_snapshot }}' }}</code></td> | ||||
|                                         <td>The current snapshot value, useful when combined with JSON or CSS filters | ||||
|                                         </td> | ||||
|                                     </tr> | ||||
|                                     </tbody> | ||||
|                                 </table> | ||||
|                                 <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']}}" | ||||
|                             </span> | ||||
|                                 <div class="pure-form-message-inline"> | ||||
|                                     <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']}}" | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
| {% endmacro %} | ||||
|   | ||||
| @@ -1,121 +1,152 @@ | ||||
| <!doctype html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|     <meta name="description" content="Self hosted website change detection."> | ||||
| <!DOCTYPE html> | ||||
| <html lang="en" data-darkmode="{{ dark_mode|lower }}"> | ||||
|  | ||||
|   <head> | ||||
|     <meta charset="utf-8"/> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | ||||
|     <meta name="description" content="Self hosted website change detection."/> | ||||
|     <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="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="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='styles.css')}}"/> | ||||
|     {% if extra_stylesheets %} | ||||
|         {% for m in extra_stylesheets %} | ||||
|         <link rel="stylesheet" href="{{ m }}?ver=1000"> | ||||
|         {% endfor %} | ||||
|       {% for m in extra_stylesheets %} | ||||
|         <link rel="stylesheet" href="{{ m }}?ver=1000"/> | ||||
|       {% endfor %} | ||||
|     {% 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"> | ||||
|     <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> | ||||
|     body::before { | ||||
|         background-image: url({{url_for('static_content', group='images', filename='gradient-border.png')}}); | ||||
|     } | ||||
|       body::before { | ||||
|         background-image: url({{url_for('static_content', group='images', filename='gradient-border.png') }}); | ||||
|       } | ||||
|     </style> | ||||
|     <script type="text/javascript" src="{{url_for('static_content', group='js', filename='jquery-3.6.0.min.js')}}"></script> | ||||
|   </head> | ||||
|  | ||||
| </head> | ||||
|  | ||||
| <body> | ||||
|  | ||||
| <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 %} | ||||
|             <a class="pure-menu-heading" href="https://github.com/dgtlmoon/changedetection.io" rel="noopener"><strong>Change</strong>Detection.io</a> | ||||
|           <a class="pure-menu-heading" href="https://github.com/dgtlmoon/changedetection.io" rel="noopener"> | ||||
|             <strong>Change</strong>Detection.io</a> | ||||
|         {% else %} | ||||
|             <a class="pure-menu-heading" href="{{url_for('index')}}"><strong>Change</strong>Detection.io</a> | ||||
|           <a class="pure-menu-heading" href="{{url_for('index')}}"> | ||||
|             <strong>Change</strong>Detection.io</a> | ||||
|         {% endif %} | ||||
|         {% if current_diff_url %} | ||||
|         <a class=current-diff-url href="{{ current_diff_url }}"><span style="max-width: 30%; overflow: hidden;">{{ current_diff_url }}</span></a> | ||||
|           <a class="current-diff-url" href="{{ current_diff_url }}"> | ||||
|             <span style="max-width: 30%; overflow: hidden">{{ current_diff_url }}</span></a> | ||||
|         {% else %} | ||||
|         {% if new_version_available and not (has_password and not current_user.is_authenticated) %} | ||||
|         <span id="new-version-text" class="pure-menu-heading"><a href="https://github.com/dgtlmoon/changedetection.io">A new version is available</a></span> | ||||
|         {% endif %} | ||||
|           {% if new_version_available and not(has_password and not current_user.is_authenticated) %} | ||||
|             <span id="new-version-text" class="pure-menu-heading"> | ||||
|               <a href="https://github.com/dgtlmoon/changedetection.io">A new version is available</a> | ||||
|             </span> | ||||
|           {% endif %} | ||||
|         {% endif %} | ||||
|  | ||||
|         <ul class="pure-menu-list"  id="top-right-menu"> | ||||
|         {% if current_user.is_authenticated or not has_password %} | ||||
|             {% if not current_diff_url %} | ||||
|             <li class="pure-menu-item"> | ||||
|         <ul class="pure-menu-list" id="top-right-menu"> | ||||
|           {% if current_user.is_authenticated or not has_password %} | ||||
|             {% if not | ||||
|             current_diff_url %} | ||||
|               <li class="pure-menu-item"> | ||||
|                 <a href="{{ url_for('settings_page')}}" class="pure-menu-link">SETTINGS</a> | ||||
|             </li> | ||||
|             <li class="pure-menu-item"> | ||||
|               </li> | ||||
|               <li class="pure-menu-item"> | ||||
|                 <a href="{{ url_for('import_page')}}" class="pure-menu-link">IMPORT</a> | ||||
|             </li> | ||||
|             <li class="pure-menu-item"> | ||||
|               </li> | ||||
|               <li class="pure-menu-item"> | ||||
|                 <a href="{{ url_for('get_backup')}}" class="pure-menu-link">BACKUP</a> | ||||
|             </li> | ||||
|               </li> | ||||
|             {% 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> | ||||
|             </li> | ||||
|               </li> | ||||
|             {% endif %} | ||||
|         {% else %} | ||||
|           {% else %} | ||||
|             <li class="pure-menu-item"> | ||||
|                 <a class="pure-menu-link" href="https://github.com/dgtlmoon/changedetection.io">Website Change Detection and Notification.</a> | ||||
|               <a class="pure-menu-link" href="https://github.com/dgtlmoon/changedetection.io">Website Change Detection and Notification.</a> | ||||
|             </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"><a class="github-link" href="https://github.com/dgtlmoon/changedetection.io"> | ||||
|                 <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> | ||||
|             </a></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 %} | ||||
|             <button class="toggle-theme {{darkClass}}" type="button" title="Toggle Light/Dark Mode"> | ||||
|               <span class="visually-hidden">Toggle light/dark mode</span> | ||||
|               <span class="icon-light"> | ||||
|                 {% include "svgs/light-mode-toggle-icon.svg" %} | ||||
|               </span> | ||||
|               <span class="icon-dark"> | ||||
|                 {% include "svgs/dark-mode-toggle-icon.svg" %} | ||||
|               </span> | ||||
|             </button> | ||||
|           </li> | ||||
|           <li class="pure-menu-item"> | ||||
|             <a class="github-link" href="https://github.com/dgtlmoon/changedetection.io"> | ||||
|               {% include "svgs/github.svg" %} | ||||
|             </a> | ||||
|           </li> | ||||
|         </ul> | ||||
|       </div> | ||||
|     </div> | ||||
| </div> | ||||
| {% 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 %} | ||||
| {% if left_sticky %}<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> | ||||
|     {% 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 %} | ||||
|     {% if left_sticky %} | ||||
|       <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 %} | ||||
|     </header> | ||||
|       </header> | ||||
|  | ||||
|     {% with messages = get_flashed_messages(with_categories=true) %} | ||||
|       {% if messages %} | ||||
|         <ul class=messages> | ||||
|         {% for category, message in messages %} | ||||
|           <li class="{{ category }}">{{ message }}</li> | ||||
|         {% endfor %} | ||||
|       {% with messages = get_flashed_messages(with_categories = true) %} | ||||
|       {% if | ||||
|       messages %} | ||||
|         <ul class="messages"> | ||||
|           {% for category, message in messages %} | ||||
|             <li class="{{ category }}">{{ message }}</li> | ||||
|           {% endfor %} | ||||
|         </ul> | ||||
|       {% endif %} | ||||
|     {% endwith %} | ||||
|  | ||||
|     {% if session['share-link'] %} | ||||
|       {% endwith %} | ||||
|       {% if session['share-link'] %} | ||||
|         <ul class="messages with-share-link"> | ||||
|           <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> | ||||
|           <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> | ||||
|         </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> | ||||
|   | ||||
| @@ -1,32 +1,49 @@ | ||||
| {% extends 'base.html' %} | ||||
|  | ||||
| {% block content %} | ||||
| {% extends 'base.html' %} {% block content %} | ||||
| <div class="edit-form"> | ||||
|     <div class="box-wrap inner"> | ||||
|     <form class="pure-form pure-form-stacked" action="{{url_for('clear_all_history')}}" method="POST"> | ||||
|         <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> | ||||
|         <fieldset> | ||||
|             <div class="pure-control-group"> | ||||
|                 This will remove version history (snapshots) for ALL watches, but keep your list of URLs! <br/> | ||||
|                 You may like to use the <strong>BACKUP</strong> link first.<br/> | ||||
|             </div> | ||||
|             <br/> | ||||
|             <div class="pure-control-group"> | ||||
|                 <label for="confirmtext">Confirmation text</label> | ||||
|                 <input type="text" id="confirmtext" required="" name="confirmtext" value="" size="10"/> | ||||
|                 <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-small button-cancel">Cancel</a> | ||||
|             </div> | ||||
|         </fieldset> | ||||
|   <div class="box-wrap inner"> | ||||
|     <form | ||||
|       class="pure-form pure-form-stacked" | ||||
|       action="{{url_for('clear_all_history')}}" | ||||
|       method="POST" | ||||
|     > | ||||
|       <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> | ||||
|       <fieldset> | ||||
|         <div class="pure-control-group"> | ||||
|           This will remove version history (snapshots) for ALL watches, but keep | ||||
|           your list of URLs! <br /> | ||||
|           You may like to use the <strong>BACKUP</strong> link first.<br /> | ||||
|         </div> | ||||
|         <br /> | ||||
|         <div class="pure-control-group"> | ||||
|           <label for="confirmtext">Confirmation text</label> | ||||
|           <input | ||||
|             type="text" | ||||
|             id="confirmtext" | ||||
|             required="" | ||||
|             name="confirmtext" | ||||
|             value="" | ||||
|             size="10" | ||||
|           /> | ||||
|           <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> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| {% extends 'base.html' %} | ||||
|  | ||||
| {% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %} | ||||
| {% block content %} | ||||
| <script> | ||||
|     const screenshot_url="{{url_for('static_content', group='screenshot', filename=uuid)}}"; | ||||
| @@ -58,6 +58,7 @@ | ||||
|         {% 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="screenshot-tab"><a href="#screenshot">Screenshot</a></li> | ||||
|         <li class="tab" id="extract-tab"><a href="#extract">Extract Data</a></li> | ||||
|     </ul> | ||||
| </div> | ||||
|  | ||||
| @@ -108,6 +109,37 @@ | ||||
|            <strong>Screenshot requires Playwright/WebDriver enabled</strong> | ||||
|          {% endif %} | ||||
|      </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 ‐</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 ([0-9\.]+)</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> | ||||
|  | ||||
| <script> | ||||
|   | ||||
| @@ -244,6 +244,7 @@ xpath://body/div/span[contains(@class, 'example-class')]", | ||||
|                           <span class="pure-form-message-inline"><strong>Note!: //text() function does not work where the <element> contains <![CDATA[]]></strong></span><br/> | ||||
|                         {% endif %} | ||||
|                         <span class="pure-form-message-inline">One rule per line, <i>any</i> rules that matches will be used.<br/> | ||||
|  | ||||
|                     <ul> | ||||
|                         <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). | ||||
|   | ||||
| @@ -60,7 +60,7 @@ | ||||
|                         {{ render_field(form.application.form.base_url, placeholder="http://yoursite.com:5000/", | ||||
|                         class="m-d") }} | ||||
|                         <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>. | ||||
|                         </span> | ||||
|                     </div> | ||||
|   | ||||
| @@ -0,0 +1 @@ | ||||
| <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> | ||||
| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										3
									
								
								changedetectionio/templates/svgs/github.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| <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> | ||||
| After Width: | Height: | Size: 749 B | 
| @@ -0,0 +1 @@ | ||||
| <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> | ||||
| After Width: | Height: | Size: 2.7 KiB | 
| @@ -80,15 +80,15 @@ | ||||
|                 <td class="inline checkbox-uuid" ><input name="uuids"  type="checkbox" value="{{ watch.uuid}} "/> <span>{{ loop.index }}</span></td> | ||||
|                 <td class="inline watch-controls"> | ||||
|                     {% 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"/></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" class="icon icon-pause"/></a> | ||||
|                     {% 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"/></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" class="icon icon-unpause"/></a> | ||||
|                     {% endif %} | ||||
|                     <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> | ||||
|                     <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> | ||||
|                 </td> | ||||
|                 <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 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> | ||||
|                     <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" /></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')}}" />{% endif %} | ||||
|  | ||||
| @@ -111,13 +111,13 @@ | ||||
|                 </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')) }}" | ||||
|                        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 button-small pure-button-primary">Edit</a> | ||||
|                        class="recheck pure-button 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> | ||||
|                     {% if watch.history_n >= 2 %} | ||||
|                     <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> | ||||
|                     <a href="{{ url_for('diff_history_page', uuid=watch.uuid) }}" target="{{watch.uuid}}" class="pure-button pure-button-primary diff-link">Diff</a> | ||||
|                     {% else %} | ||||
|                         {% 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 button-small pure-button-primary">Preview</a> | ||||
|                             <a href="{{ url_for('preview_page', uuid=watch.uuid)}}" target="{{watch.uuid}}" class="pure-button pure-button-primary">Preview</a> | ||||
|                         {% endif %} | ||||
|                     {% endif %} | ||||
|                 </td> | ||||
|   | ||||
							
								
								
									
										70
									
								
								changedetectionio/tests/test_extract_csv.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,70 @@ | ||||
| #!/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] | ||||
| @@ -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 | ||||
|     notification_form_data = {"notification_urls": notification_url, | ||||
|                               "notification_title": "New ChangeDetection.io Notification - {watch_url}", | ||||
|                               "notification_body": "BASE URL: {base_url}\n" | ||||
|                                                    "Watch URL: {watch_url}\n" | ||||
|                                                    "Watch UUID: {watch_uuid}\n" | ||||
|                                                    "Watch title: {watch_title}\n" | ||||
|                                                    "Watch tag: {watch_tag}\n" | ||||
|                                                    "Preview: {preview_url}\n" | ||||
|                                                    "Diff URL: {diff_url}\n" | ||||
|                                                    "Snapshot: {current_snapshot}\n" | ||||
|                                                    "Diff: {diff}\n" | ||||
|                                                    "Diff Full: {diff_full}\n" | ||||
|                               "notification_title": "New ChangeDetection.io Notification - {{watch_url}}", | ||||
|                               "notification_body": "BASE URL: {{base_url}}\n" | ||||
|                                                    "Watch URL: {{watch_url}}\n" | ||||
|                                                    "Watch UUID: {{watch_uuid}}\n" | ||||
|                                                    "Watch title: {{watch_title}}\n" | ||||
|                                                    "Watch tag: {{watch_tag}}\n" | ||||
|                                                    "Preview: {{preview_url}}\n" | ||||
|                                                    "Diff URL: {{diff_url}}\n" | ||||
|                                                    "Snapshot: {{current_snapshot}}\n" | ||||
|                                                    "Diff: {{diff}}\n" | ||||
|                                                    "Diff Full: {{diff_full}}\n" | ||||
|                                                    ":-)", | ||||
|                               "notification_format": "Text"} | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|     notification_form_data = {"notification_urls": notification_url, | ||||
|                               "notification_title": "New ChangeDetection.io Notification - {watch_url}", | ||||
|                               "notification_body": "BASE URL: {base_url}\n" | ||||
|                                                    "Watch URL: {watch_url}\n" | ||||
|                                                    "Watch UUID: {watch_uuid}\n" | ||||
|                                                    "Watch title: {watch_title}\n" | ||||
|                                                    "Watch tag: {watch_tag}\n" | ||||
|                                                    "Preview: {preview_url}\n" | ||||
|                                                    "Diff URL: {diff_url}\n" | ||||
|                                                    "Snapshot: {current_snapshot}\n" | ||||
|                                                    "Diff: {diff}\n" | ||||
|                                                    "Diff Full: {diff_full}\n" | ||||
|                               "notification_title": "New ChangeDetection.io Notification - {{watch_url}}", | ||||
|                               "notification_body": "BASE URL: {{base_url}}\n" | ||||
|                                                    "Watch URL: {{watch_url}}\n" | ||||
|                                                    "Watch UUID: {{watch_uuid}}\n" | ||||
|                                                    "Watch title: {{watch_title}}\n" | ||||
|                                                    "Watch tag: {{watch_tag}}\n" | ||||
|                                                    "Preview: {{preview_url}}\n" | ||||
|                                                    "Diff URL: {{diff_url}}\n" | ||||
|                                                    "Snapshot: {{current_snapshot}}\n" | ||||
|                                                    "Diff: {{diff}}\n" | ||||
|                                                    "Diff Full: {{diff_full}}\n" | ||||
|                                                    ":-)", | ||||
|                               "notification_format": "Text"} | ||||
|  | ||||
| @@ -84,6 +84,7 @@ def run_filter_test(client, content_filter): | ||||
|         data=notification_form_data, | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|  | ||||
|     assert b"Updated watch." in res.data | ||||
|     time.sleep(3) | ||||
|  | ||||
|   | ||||
| @@ -90,17 +90,17 @@ def test_check_notification(client, live_server): | ||||
|     print (">>>> Notification URL: "+notification_url) | ||||
|  | ||||
|     notification_form_data = {"notification_urls": notification_url, | ||||
|                               "notification_title": "New ChangeDetection.io Notification - {watch_url}", | ||||
|                               "notification_body": "BASE URL: {base_url}\n" | ||||
|                                                    "Watch URL: {watch_url}\n" | ||||
|                                                    "Watch UUID: {watch_uuid}\n" | ||||
|                                                    "Watch title: {watch_title}\n" | ||||
|                                                    "Watch tag: {watch_tag}\n" | ||||
|                                                    "Preview: {preview_url}\n" | ||||
|                                                    "Diff URL: {diff_url}\n" | ||||
|                                                    "Snapshot: {current_snapshot}\n" | ||||
|                                                    "Diff: {diff}\n" | ||||
|                                                    "Diff Full: {diff_full}\n" | ||||
|                               "notification_title": "New ChangeDetection.io Notification - {{watch_url}}", | ||||
|                               "notification_body": "BASE URL: {{base_url}}\n" | ||||
|                                                    "Watch URL: {{watch_url}}\n" | ||||
|                                                    "Watch UUID: {{watch_uuid}}\n" | ||||
|                                                    "Watch title: {{watch_title}}\n" | ||||
|                                                    "Watch tag: {{watch_tag}}\n" | ||||
|                                                    "Preview: {{preview_url}}\n" | ||||
|                                                    "Diff URL: {{diff_url}}\n" | ||||
|                                                    "Snapshot: {{current_snapshot}}\n" | ||||
|                                                    "Diff: {{diff}}\n" | ||||
|                                                    "Diff Full: {{diff_full}}\n" | ||||
|                                                    ":-)", | ||||
|                               "notification_screenshot": True, | ||||
|                               "notification_format": "Text"} | ||||
| @@ -179,7 +179,6 @@ def test_check_notification(client, live_server): | ||||
|         logging.debug(">>> Skipping BASE_URL check") | ||||
|  | ||||
|  | ||||
|  | ||||
|     # This should insert the {current_snapshot} | ||||
|     set_more_modified_response() | ||||
|     client.get(url_for("form_watch_checknow"), follow_redirects=True) | ||||
| @@ -237,10 +236,9 @@ def test_check_notification(client, live_server): | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def test_notification_validation(client, live_server): | ||||
|     #live_server_setup(live_server) | ||||
|     time.sleep(3) | ||||
|     time.sleep(1) | ||||
|  | ||||
|     # re #242 - when you edited an existing new entry, it would not correctly show the notification settings | ||||
|     # Add our URL to the import page | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
| @@ -269,19 +267,33 @@ def test_notification_validation(client, live_server): | ||||
| #    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 | ||||
| # Disabled for now | ||||
| #    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("Token 'rubbish' is not a valid token or is unknown".encode('utf-8')) in res.data | ||||
|  | ||||
|     # And trying to define an invalid Jinja2 template should also throw 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" | ||||
|         data={"application-notification_title": "New ChangeDetection.io Notification - {{ watch_url }}", | ||||
|               "application-notification_body": "Rubbish: {{ rubbish }\n", | ||||
|               "application-notification_urls": "json://foobar.com", | ||||
|               "application-minutes_between_check": 180, | ||||
|               "application-fetch_backend": "html_requests" | ||||
|               }, | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|     assert bytes("This is not a valid Jinja2 template".encode('utf-8')) in res.data | ||||
|  | ||||
|     assert bytes("is not a valid token".encode('utf-8')) in res.data | ||||
|  | ||||
|     # cleanup for the next | ||||
|     client.get( | ||||
| @@ -289,4 +301,55 @@ def test_notification_validation(client, live_server): | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|  | ||||
| def test_notification_jinja2(client, live_server): | ||||
|     #live_server_setup(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") | ||||
| @@ -11,23 +11,23 @@ def test_check_notification_error_handling(client, live_server): | ||||
|     set_original_response() | ||||
|  | ||||
|     # Give the endpoint time to spin up | ||||
|     time.sleep(3) | ||||
|     time.sleep(2) | ||||
|  | ||||
|     # use a different URL so that it doesnt interfere with the actual check until we are ready | ||||
|     # Set a URL and fetch it, then set a notification URL which is going to give errors | ||||
|     test_url = url_for('test_endpoint', _external=True) | ||||
|     res = client.post( | ||||
|         url_for("form_quick_watch_add"), | ||||
|         data={"url": "https://changedetection.io/CHANGELOG.txt", "tag": ''}, | ||||
|         data={"url": test_url, "tag": ''}, | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|     assert b"Watch added" in res.data | ||||
|  | ||||
|     time.sleep(10) | ||||
|     time.sleep(2) | ||||
|     set_modified_response() | ||||
|  | ||||
|     # Check we capture the failure, we can just use trigger_check = y here | ||||
|     res = client.post( | ||||
|         url_for("edit_page", uuid="first"), | ||||
|         data={"notification_urls": "jsons://broken-url.changedetection.io/test", | ||||
|         data={"notification_urls": "jsons://broken-url-xxxxxxxx123/test", | ||||
|               "notification_title": "xxx", | ||||
|               "notification_body": "xxxxx", | ||||
|               "notification_format": "Text", | ||||
| @@ -36,15 +36,14 @@ def test_check_notification_error_handling(client, live_server): | ||||
|               "title": "", | ||||
|               "headers": "", | ||||
|               "time_between_check-minutes": "180", | ||||
|               "fetch_backend": "html_requests", | ||||
|               "trigger_check": "y"}, | ||||
|               "fetch_backend": "html_requests"}, | ||||
|         follow_redirects=True | ||||
|     ) | ||||
|     assert b"Updated watch." in res.data | ||||
|  | ||||
|     found=False | ||||
|     for i in range(1, 10): | ||||
|         time.sleep(1) | ||||
|  | ||||
|         logging.debug("Fetching watch overview....") | ||||
|         res = client.get( | ||||
|             url_for("index")) | ||||
| @@ -53,6 +52,7 @@ def test_check_notification_error_handling(client, live_server): | ||||
|             found=True | ||||
|             break | ||||
|  | ||||
|         time.sleep(1) | ||||
|  | ||||
|     assert found | ||||
|  | ||||
| @@ -60,7 +60,7 @@ def test_check_notification_error_handling(client, live_server): | ||||
|     # The error should show in the notification logs | ||||
|     res = client.get( | ||||
|         url_for("notification_logs")) | ||||
|     assert bytes("Name or service not known".encode('utf-8')) in res.data | ||||
|     found_name_resolution_error = b"Temporary failure in name resolution" in res.data or b"Name or service not known" in res.data | ||||
|     assert found_name_resolution_error | ||||
|  | ||||
|  | ||||
|     # And it should be listed on the watch overview | ||||
|     client.get(url_for("form_delete", uuid="all"), follow_redirects=True) | ||||
|   | ||||
| @@ -149,6 +149,9 @@ def live_server_setup(live_server): | ||||
|             if data != None: | ||||
|                 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) | ||||
|         return "Text was set" | ||||
|  | ||||
|   | ||||