diff --git a/changedetectionio/apprise_plugin/__init__.py b/changedetectionio/apprise_plugin/__init__.py
new file mode 100644
index 000000000..93c382fa4
--- /dev/null
+++ b/changedetectionio/apprise_plugin/__init__.py
@@ -0,0 +1,78 @@
+# include the decorator
+from apprise.decorators import notify
+
+@notify(on="delete")
+@notify(on="deletes")
+@notify(on="get")
+@notify(on="gets")
+@notify(on="post")
+@notify(on="posts")
+@notify(on="put")
+@notify(on="puts")
+def apprise_custom_api_call_wrapper(body, title, notify_type, *args, **kwargs):
+ import requests
+ import json
+ from apprise.utils import parse_url as apprise_parse_url
+ from apprise import URLBase
+
+ 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://')
+
+ headers = {}
+ params = {}
+ auth = None
+
+ # Convert /foobar?+some-header=hello to proper header dictionary
+ results = apprise_parse_url(url)
+ if results:
+ # Add our headers that the user can potentially over-ride if they wish
+ # to to our returned result set and tidy entries by unquoting them
+ headers = {URLBase.unquote(x): URLBase.unquote(y)
+ for x, y in results['qsd+'].items()}
+
+ # https://github.com/caronc/apprise/wiki/Notify_Custom_JSON#get-parameter-manipulation
+ # In Apprise, it relies on prefixing each request arg with "-", because it uses say &method=update as a flag for apprise
+ # but here we are making straight requests, so we need todo convert this against apprise's logic
+ for k, v in results['qsd'].items():
+ if not k.strip('+-') in results['qsd+'].keys():
+ params[URLBase.unquote(k)] = URLBase.unquote(v)
+
+ # Determine Authentication
+ auth = ''
+ if results.get('user') and results.get('password'):
+ auth = (URLBase.unquote(results.get('user')), URLBase.unquote(results.get('user')))
+ elif results.get('user'):
+ auth = (URLBase.unquote(results.get('user')))
+
+ # Try to auto-guess if it's JSON
+ try:
+ json.loads(body)
+ headers['Content-Type'] = 'application/json; charset=utf-8'
+ except ValueError as e:
+ pass
+
+ r(results.get('url'),
+ auth=auth,
+ data=body.encode('utf-8') if type(body) is str else body,
+ headers=headers,
+ params=params
+ )
\ No newline at end of file
diff --git a/changedetectionio/blueprint/browser_steps/browser_steps.py b/changedetectionio/blueprint/browser_steps/browser_steps.py
index 6f38be2e0..b9765bacd 100644
--- a/changedetectionio/blueprint/browser_steps/browser_steps.py
+++ b/changedetectionio/blueprint/browser_steps/browser_steps.py
@@ -25,6 +25,7 @@ browser_step_ui_config = {'Choose one': '0 0',
'Click element if exists': '1 0',
'Click element': '1 0',
'Click element containing text': '0 1',
+ 'Click element containing text if exists': '0 1',
'Enter text in field': '1 1',
'Execute JS': '0 1',
# 'Extract text and use as filter': '1 0',
@@ -96,12 +97,24 @@ class steppable_browser_interface():
return self.action_goto_url(value=self.start_url)
def action_click_element_containing_text(self, selector=None, value=''):
+ logger.debug("Clicking element containing text")
if not len(value.strip()):
return
elem = self.page.get_by_text(value)
if elem.count():
elem.first.click(delay=randint(200, 500), timeout=3000)
+ def action_click_element_containing_text_if_exists(self, selector=None, value=''):
+ logger.debug("Clicking element containing text if exists")
+ if not len(value.strip()):
+ return
+ elem = self.page.get_by_text(value)
+ logger.debug(f"Clicking element containing text - {elem.count()} elements found")
+ if elem.count():
+ elem.first.click(delay=randint(200, 500), timeout=3000)
+ else:
+ return
+
def action_enter_text_in_field(self, selector, value):
if not len(selector.strip()):
return
diff --git a/changedetectionio/blueprint/tags/templates/edit-tag.html b/changedetectionio/blueprint/tags/templates/edit-tag.html
index 2ccc68a05..a713cf6a9 100644
--- a/changedetectionio/blueprint/tags/templates/edit-tag.html
+++ b/changedetectionio/blueprint/tags/templates/edit-tag.html
@@ -58,9 +58,9 @@ xpath://body/div/span[contains(@class, 'example-class')]",
{% if '/text()' in field %}
{% endif %}
-
*/
+ overflow-wrap: break-word; /* Allows long words to break and wrap to the next line */
+ }
+}
+
+#activate-text-preview {
+ right: 0;
+ position: absolute;
+ z-index: 0;
+ box-shadow: 1px 1px 4px var(--color-shadow-jump);
+}
diff --git a/changedetectionio/static/styles/scss/styles.scss b/changedetectionio/static/styles/scss/styles.scss
index b720b6d4f..0c3fd6cd5 100644
--- a/changedetectionio/static/styles/scss/styles.scss
+++ b/changedetectionio/static/styles/scss/styles.scss
@@ -12,6 +12,7 @@
@import "parts/_darkmode";
@import "parts/_menu";
@import "parts/_love";
+@import "parts/preview_text_filter";
body {
color: var(--color-text);
diff --git a/changedetectionio/static/styles/styles.css b/changedetectionio/static/styles/styles.css
index 4f3fec102..7bc040822 100644
--- a/changedetectionio/static/styles/styles.css
+++ b/changedetectionio/static/styles/styles.css
@@ -46,14 +46,31 @@
#browser_steps li > label {
display: none; }
-#browser-steps-fieldlist {
- height: 100%;
- overflow-y: scroll; }
-
#browser-steps .flex-wrapper {
display: flex;
flex-flow: row;
- height: 70vh; }
+ height: 70vh;
+ font-size: 80%; }
+ #browser-steps .flex-wrapper #browser-steps-ui {
+ flex-grow: 1;
+ /* Allow it to grow and fill the available space */
+ flex-shrink: 1;
+ /* Allow it to shrink if needed */
+ flex-basis: 0;
+ /* Start with 0 base width so it stretches as much as possible */
+ background-color: #eee;
+ border-radius: 5px; }
+ #browser-steps .flex-wrapper #browser-steps-fieldlist {
+ flex-grow: 0;
+ /* Don't allow it to grow */
+ flex-shrink: 0;
+ /* Don't allow it to shrink */
+ flex-basis: auto;
+ /* Base width is determined by the content */
+ max-width: 400px;
+ /* Set a max width to prevent overflow */
+ padding-left: 1rem;
+ overflow-y: scroll; }
/* this is duplicate :( */
#browsersteps-selector-wrapper {
@@ -411,6 +428,47 @@ html[data-darkmode="true"] #toggle-light-mode .icon-dark {
fill: #ff0000 !important;
transition: all ease 0.3s !important; }
+body.preview-text-enabled {
+ /* layout of the page */
+ /* actual preview area */ }
+ body.preview-text-enabled #filters-and-triggers > div {
+ display: flex;
+ /* Establishes Flexbox layout */
+ gap: 20px;
+ /* Adds space between the columns */
+ position: relative;
+ /* Ensures the sticky positioning is relative to this parent */ }
+ body.preview-text-enabled #edit-text-filter, body.preview-text-enabled #text-preview {
+ flex: 1;
+ /* Each column takes an equal amount of available space */
+ align-self: flex-start;
+ /* Aligns the right column to the start, allowing it to maintain its content height */ }
+ body.preview-text-enabled #edit-text-filter #pro-tips {
+ display: none; }
+ body.preview-text-enabled #text-preview {
+ position: sticky;
+ top: 25px;
+ display: block !important; }
+ body.preview-text-enabled #text-preview-inner {
+ background: var(--color-grey-900);
+ border: 1px solid var(--color-grey-600);
+ padding: 1rem;
+ color: #333;
+ font-family: "Courier New", Courier, monospace;
+ /* Sets the font to a monospace type */
+ font-size: 12px;
+ overflow-x: scroll;
+ white-space: pre-wrap;
+ /* Preserves whitespace and line breaks like */
+ overflow-wrap: break-word;
+ /* Allows long words to break and wrap to the next line */ }
+
+#activate-text-preview {
+ right: 0;
+ position: absolute;
+ z-index: 0;
+ box-shadow: 1px 1px 4px var(--color-shadow-jump); }
+
body {
color: var(--color-text);
background: var(--color-background-page);
@@ -1194,11 +1252,9 @@ ul {
color: #fff;
opacity: 0.7; }
-
.restock-label svg {
vertical-align: middle; }
-
#chrome-extension-link {
padding: 9px;
border: 1px solid var(--color-grey-800);
diff --git a/changedetectionio/store.py b/changedetectionio/store.py
index e5999b9ad..42691a9b8 100644
--- a/changedetectionio/store.py
+++ b/changedetectionio/store.py
@@ -13,7 +13,6 @@ from threading import Lock
import json
import os
import re
-import requests
import secrets
import threading
import time
@@ -281,6 +280,7 @@ class ChangeDetectionStore:
self.needs_write_urgent = True
def add_watch(self, url, tag='', extras=None, tag_uuids=None, write_to_disk_now=True):
+ import requests
if extras is None:
extras = {}
diff --git a/changedetectionio/templates/_common_fields.html b/changedetectionio/templates/_common_fields.html
index 14fa91479..9447f9035 100644
--- a/changedetectionio/templates/_common_fields.html
+++ b/changedetectionio/templates/_common_fields.html
@@ -11,8 +11,11 @@
class="notification-urls" )
}}
- Show token/placeholders
+