From 9e71cf9409d2d5af9415ea0e9c2f6070ac5c2786 Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Fri, 10 Apr 2026 11:26:14 +0200 Subject: [PATCH] Groups - Set custom colour for tag/group/label background --- changedetectionio/blueprint/tags/__init__.py | 10 +++++ changedetectionio/blueprint/tags/form.py | 7 ++-- .../blueprint/tags/templates/edit-tag.html | 40 +++++++++++++++++++ .../tags/templates/groups-overview.html | 16 +++++++- .../blueprint/watchlist/__init__.py | 1 + .../watchlist/templates/watch-overview.html | 8 ++++ changedetectionio/model/Tag.py | 14 +++++++ changedetectionio/processors/__init__.py | 12 ++++++ 8 files changed, 103 insertions(+), 5 deletions(-) diff --git a/changedetectionio/blueprint/tags/__init__.py b/changedetectionio/blueprint/tags/__init__.py index e11910ff7..86d9154c1 100644 --- a/changedetectionio/blueprint/tags/__init__.py +++ b/changedetectionio/blueprint/tags/__init__.py @@ -22,11 +22,14 @@ def construct_blueprint(datastore: ChangeDetectionStore): tag_count = Counter(tag for watch in datastore.data['watching'].values() if watch.get('tags') for tag in watch['tags']) + from changedetectionio import processors output = render_template("groups-overview.html", app_rss_token=datastore.data['settings']['application'].get('rss_access_token'), available_tags=sorted_tags, form=add_form, + generate_tag_colors=processors.generate_processor_badge_colors, tag_count=tag_count, + wcag_text_color=processors.wcag_text_color, ) return output @@ -208,9 +211,16 @@ def construct_blueprint(datastore: ChangeDetectionStore): template = env.from_string(template_str) included_content = template.render(**template_args) + matching_watches = { + w_uuid: watch + for w_uuid, watch in datastore.data['watching'].items() + if default.matches_url(watch.get('url', '')) + } + output = render_template("edit-tag.html", extra_form_content=included_content, extra_tab_content=form.extra_tab_content() if form.extra_tab_content() else None, + matching_watches=matching_watches, settings_application=datastore.data['settings']['application'], **template_args ) diff --git a/changedetectionio/blueprint/tags/form.py b/changedetectionio/blueprint/tags/form.py index 6ff3a503c..61b8bdb82 100644 --- a/changedetectionio/blueprint/tags/form.py +++ b/changedetectionio/blueprint/tags/form.py @@ -10,12 +10,11 @@ from changedetectionio.processors.restock_diff.forms import processor_settings_f class group_restock_settings_form(restock_settings_form): overrides_watch = BooleanField('Activate for individual watches in this tag/group?', default=False) + url_match_pattern = StringField('Auto-apply to watches with URLs matching', + render_kw={"placeholder": "e.g. *://example.com/* or github.com/myorg"}) + tag_colour = StringField('Tag colour', default='') class SingleTag(Form): name = StringField('Tag name', [validators.InputRequired()], render_kw={"placeholder": "Name"}) save_button = SubmitField('Save', render_kw={"class": "pure-button pure-button-primary"}) - - - - diff --git a/changedetectionio/blueprint/tags/templates/edit-tag.html b/changedetectionio/blueprint/tags/templates/edit-tag.html index 8a0260903..ecdc0bbbe 100644 --- a/changedetectionio/blueprint/tags/templates/edit-tag.html +++ b/changedetectionio/blueprint/tags/templates/edit-tag.html @@ -43,6 +43,46 @@
{{ render_field(form.title, placeholder="https://...", required=true, class="m-d") }}
+
+ {{ render_field(form.url_match_pattern, class="m-d") }} + {{ _('Automatically applies this tag to any watch whose URL matches. Supports wildcards: *example.com* or plain substring: github.com/myorg')|safe }} +
+ {% if matching_watches %} +
+ + +
+ {% endif %} +
+ +
+ + + + +
+ {{ _('Leave unchecked to use the auto-generated colour based on the tag name.') }} +
+ diff --git a/changedetectionio/blueprint/tags/templates/groups-overview.html b/changedetectionio/blueprint/tags/templates/groups-overview.html index cd06b15ee..84b4305b0 100644 --- a/changedetectionio/blueprint/tags/templates/groups-overview.html +++ b/changedetectionio/blueprint/tags/templates/groups-overview.html @@ -3,6 +3,20 @@ {% from '_helpers.html' import render_simple_field, render_field %} +
@@ -48,7 +62,7 @@ Mute notifications {{ "{:,}".format(tag_count[uuid]) if uuid in tag_count else 0 }} - {{ tag.title }} + {{ tag.title }} {{ _('Edit') }} {{ _('Recheck') }} diff --git a/changedetectionio/blueprint/watchlist/__init__.py b/changedetectionio/blueprint/watchlist/__init__.py index ebdfa627b..ea879ac6a 100644 --- a/changedetectionio/blueprint/watchlist/__init__.py +++ b/changedetectionio/blueprint/watchlist/__init__.py @@ -92,6 +92,7 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe extra_classes='has-queue' if not update_q.empty() else '', form=form, generate_tag_colors=processors.generate_processor_badge_colors, + wcag_text_color=processors.wcag_text_color, guid=datastore.data['app_guid'], has_proxies=proxy_list, hosted_sticky=os.getenv("SALTED_PASS", False) == False, diff --git a/changedetectionio/blueprint/watchlist/templates/watch-overview.html b/changedetectionio/blueprint/watchlist/templates/watch-overview.html index 55fbb9e0c..70012e09e 100644 --- a/changedetectionio/blueprint/watchlist/templates/watch-overview.html +++ b/changedetectionio/blueprint/watchlist/templates/watch-overview.html @@ -71,6 +71,13 @@ document.addEventListener('DOMContentLoaded', function() { {%- for uuid, tag in tags -%} {%- if tag and tag.title -%} {%- set class_name = tag.title|sanitize_tag_class -%} +{%- if tag.get('tag_colour') -%} +.button-tag.tag-{{ class_name }}, +.watch-tag-list.tag-{{ class_name }} { + background-color: {{ tag.tag_colour }}; + color: {{ wcag_text_color(tag.tag_colour) }}; +} +{%- else -%} {%- set colors = generate_tag_colors(tag.title) -%} .button-tag.tag-{{ class_name }} { background-color: {{ colors['light']['bg'] }}; @@ -92,6 +99,7 @@ html[data-darkmode="true"] .watch-tag-list.tag-{{ class_name }} { color: {{ colors['dark']['color'] }}; } {%- endif -%} +{%- endif -%} {%- endfor -%}
diff --git a/changedetectionio/model/Tag.py b/changedetectionio/model/Tag.py index 7bfd4940a..cbbeed3e7 100644 --- a/changedetectionio/model/Tag.py +++ b/changedetectionio/model/Tag.py @@ -51,6 +51,20 @@ class model(EntityPersistenceMixin, watch_base): self.update(kw['default']) del kw['default'] + def matches_url(self, url: str) -> bool: + """Return True if url matches this tag's url_match_pattern. + + Supports fnmatch wildcards (* and ?) when the pattern contains them; + otherwise falls back to case-insensitive substring matching. + """ + import fnmatch + pattern = self.get('url_match_pattern', '').strip() + if not pattern or not url: + return False + if any(c in pattern for c in ('*', '?', '[')): + return fnmatch.fnmatch(url.lower(), pattern.lower()) + return pattern.lower() in url.lower() + # _save_to_disk() method provided by EntityPersistenceMixin # commit() and _get_commit_data() methods inherited from watch_base # Tag uses default _get_commit_data() (includes all keys) diff --git a/changedetectionio/processors/__init__.py b/changedetectionio/processors/__init__.py index fbb448aa4..032245f35 100644 --- a/changedetectionio/processors/__init__.py +++ b/changedetectionio/processors/__init__.py @@ -341,6 +341,18 @@ def get_processor_descriptions(): return descriptions +def wcag_text_color(hex_bg: str) -> str: + """Return #000000 or #ffffff for maximum WCAG contrast against hex_bg.""" + hex_bg = hex_bg.lstrip('#') + if len(hex_bg) != 6: + return '#000000' + r, g, b = (int(hex_bg[i:i+2], 16) / 255 for i in (0, 2, 4)) + def lin(c): + return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 + L = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b) + return '#000000' if L > 0.179 else '#ffffff' + + def generate_processor_badge_colors(processor_name): """ Generate consistent colors for a processor badge based on its name.