From 068013486a43ed2329602e88ae6d365eebac0bf9 Mon Sep 17 00:00:00 2001 From: Daniel Dada Date: Wed, 11 Feb 2026 11:15:29 +0300 Subject: [PATCH] feat: group and filter items by list in "on page" section --- _locales/de/messages.json | 14 ++ _locales/en/messages.json | 29 ++- _locales/es/messages.json | 14 ++ _locales/fr/messages.json | 14 ++ _locales/hi/messages.json | 14 ++ _locales/it/messages.json | 14 ++ _locales/ja/messages.json | 14 ++ _locales/ko/messages.json | 14 ++ _locales/nl/messages.json | 14 ++ _locales/pl/messages.json | 14 ++ _locales/pt_BR/messages.json | 14 ++ _locales/ru/messages.json | 14 ++ _locales/tr/messages.json | 14 ++ _locales/zh_CN/messages.json | 14 ++ popup/popup.css | 373 +++++++++++++++++++++++++-------- popup/popup.html | 53 +++-- shared/colors.css | 4 + shared/ui-components.css | 36 +++- src/background.ts | 11 +- src/content/ContentScript.ts | 40 ++-- src/content/HighlightEngine.ts | 31 ++- src/popup/PopupController.ts | 309 ++++++++++++++++++++------- src/types.ts | 12 +- 23 files changed, 853 insertions(+), 227 deletions(-) diff --git a/_locales/de/messages.json b/_locales/de/messages.json index c8663f5..efcf5b9 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -116,9 +116,14 @@ "manage_exceptions": { "message": "Verwalten" }, + "exceptions_mode": { "message": "Modus:" }, + "exceptions_mode_blacklist": { "message": "Blacklist — auf diesen Sites nicht hervorheben" }, + "exceptions_mode_whitelist": { "message": "Whitelist — nur auf diesen Sites hervorheben" }, "exceptions_list": { "message": "Ausnahme-Websites:" }, + "exceptions_list_blacklist": { "message": "Sites ausschließen (Blacklist):" }, + "exceptions_list_whitelist": { "message": "Sites einschließen (Whitelist):" }, "clear_all": { "message": "Alle löschen" }, @@ -167,6 +172,15 @@ "total_highlights": { "message": "Gesamt" }, + "total_matches": { + "message": "Treffer gesamt" + }, + "group_by_list": { + "message": "Nach Liste gruppieren" + }, + "other": { + "message": "Sonstige" + }, "refresh": { "message": "Aktualisieren" }, diff --git a/_locales/en/messages.json b/_locales/en/messages.json index bb23a84..9e563a1 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -122,11 +122,26 @@ "manage_exceptions": { "message": "Manage" }, + "exceptions_mode": { + "message": "Mode:" + }, + "exceptions_mode_blacklist": { + "message": "Blacklist — don't highlight on these sites" + }, + "exceptions_mode_whitelist": { + "message": "Whitelist — only highlight on these sites" + }, "exceptions_list": { "message": "Exception Sites:" }, + "exceptions_list_blacklist": { + "message": "Sites to exclude (blacklist):" + }, + "exceptions_list_whitelist": { + "message": "Sites to include (whitelist):" + }, "clear_all": { - "message": "Clear All" + "message": "Clear all" }, "confirm_clear_exceptions": { "message": "Are you sure you want to clear all exceptions?" @@ -173,6 +188,18 @@ "total_highlights": { "message": "Total" }, + "total_matches": { + "message": "Total matches" + }, + "group_by_list": { + "message": "Group by list" + }, + "other": { + "message": "Other" + }, + "exception_domain_placeholder": { + "message": "example.com" + }, "refresh": { "message": "Refresh" }, diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 2aa4da7..c86c2bf 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -122,9 +122,14 @@ "manage_exceptions": { "message": "Gestionar" }, + "exceptions_mode": { "message": "Modo:" }, + "exceptions_mode_blacklist": { "message": "Lista negra — no resaltar en estos sitios" }, + "exceptions_mode_whitelist": { "message": "Lista blanca — solo resaltar en estos sitios" }, "exceptions_list": { "message": "Sitios de excepción:" }, + "exceptions_list_blacklist": { "message": "Sitios a excluir (lista negra):" }, + "exceptions_list_whitelist": { "message": "Sitios a incluir (lista blanca):" }, "clear_all": { "message": "Limpiar todo" }, @@ -173,6 +178,15 @@ "total_highlights": { "message": "Total" }, + "total_matches": { + "message": "Coincidencias totales" + }, + "group_by_list": { + "message": "Agrupar por lista" + }, + "other": { + "message": "Otros" + }, "refresh": { "message": "Actualizar" }, diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index e6837fd..eec0a70 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -122,9 +122,14 @@ "manage_exceptions": { "message": "Gérer" }, + "exceptions_mode": { "message": "Mode :" }, + "exceptions_mode_blacklist": { "message": "Liste noire — ne pas surligner sur ces sites" }, + "exceptions_mode_whitelist": { "message": "Liste blanche — surligner uniquement sur ces sites" }, "exceptions_list": { "message": "Sites d'exception :" }, + "exceptions_list_blacklist": { "message": "Sites à exclure (liste noire) :" }, + "exceptions_list_whitelist": { "message": "Sites à inclure (liste blanche) :" }, "clear_all": { "message": "Tout effacer" }, @@ -173,6 +178,15 @@ "total_highlights": { "message": "Total" }, + "total_matches": { + "message": "Correspondances totales" + }, + "group_by_list": { + "message": "Grouper par liste" + }, + "other": { + "message": "Autres" + }, "refresh": { "message": "Actualiser" }, diff --git a/_locales/hi/messages.json b/_locales/hi/messages.json index 0ac772d..861c3ee 100644 --- a/_locales/hi/messages.json +++ b/_locales/hi/messages.json @@ -122,9 +122,14 @@ "manage_exceptions": { "message": "प्रबंधित करें" }, + "exceptions_mode": { "message": "मोड:" }, + "exceptions_mode_blacklist": { "message": "ब्लैकलिस्ट — इन साइटों पर हाइलाइट न करें" }, + "exceptions_mode_whitelist": { "message": "व्हाइटलिस्ट — केवल इन साइटों पर हाइलाइट करें" }, "exceptions_list": { "message": "अपवाद साइटें:" }, + "exceptions_list_blacklist": { "message": "बहिष्कृत साइटें (ब्लैकलिस्ट):" }, + "exceptions_list_whitelist": { "message": "शामिल साइटें (व्हाइटलिस्ट):" }, "clear_all": { "message": "सभी साफ करें" }, @@ -173,6 +178,15 @@ "total_highlights": { "message": "कुल" }, + "total_matches": { + "message": "कुल मिलान" + }, + "group_by_list": { + "message": "सूची के अनुसार समूहित करें" + }, + "other": { + "message": "अन्य" + }, "refresh": { "message": "रीफ्रेश करें" }, diff --git a/_locales/it/messages.json b/_locales/it/messages.json index ddeb16d..cf232ee 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -122,9 +122,14 @@ "manage_exceptions": { "message": "Gestisci" }, + "exceptions_mode": { "message": "Modalità:" }, + "exceptions_mode_blacklist": { "message": "Lista nera — non evidenziare su questi siti" }, + "exceptions_mode_whitelist": { "message": "Lista bianca — evidenziare solo su questi siti" }, "exceptions_list": { "message": "Siti di eccezione:" }, + "exceptions_list_blacklist": { "message": "Siti da escludere (lista nera):" }, + "exceptions_list_whitelist": { "message": "Siti da includere (lista bianca):" }, "clear_all": { "message": "Cancella tutto" }, @@ -173,6 +178,15 @@ "total_highlights": { "message": "Totale" }, + "total_matches": { + "message": "Corrispondenze totali" + }, + "group_by_list": { + "message": "Raggruppa per lista" + }, + "other": { + "message": "Altri" + }, "refresh": { "message": "Aggiorna" }, diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 1ccd326..726e2be 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -122,9 +122,14 @@ "manage_exceptions": { "message": "管理" }, + "exceptions_mode": { "message": "モード:" }, + "exceptions_mode_blacklist": { "message": "ブラックリスト — これらのサイトではハイライトしない" }, + "exceptions_mode_whitelist": { "message": "ホワイトリスト — これらのサイトでのみハイライト" }, "exceptions_list": { "message": "例外サイト:" }, + "exceptions_list_blacklist": { "message": "除外するサイト(ブラックリスト):" }, + "exceptions_list_whitelist": { "message": "含めるサイト(ホワイトリスト):" }, "clear_all": { "message": "すべてクリア" }, @@ -173,6 +178,15 @@ "total_highlights": { "message": "合計" }, + "total_matches": { + "message": "一致数の合計" + }, + "group_by_list": { + "message": "リストでグループ化" + }, + "other": { + "message": "その他" + }, "refresh": { "message": "更新" }, diff --git a/_locales/ko/messages.json b/_locales/ko/messages.json index 95d4403..ea484a2 100644 --- a/_locales/ko/messages.json +++ b/_locales/ko/messages.json @@ -122,9 +122,14 @@ "manage_exceptions": { "message": "관리" }, + "exceptions_mode": { "message": "모드:" }, + "exceptions_mode_blacklist": { "message": "차단 목록 — 이 사이트에서 강조 안 함" }, + "exceptions_mode_whitelist": { "message": "허용 목록 — 이 사이트에서만 강조" }, "exceptions_list": { "message": "예외 사이트:" }, + "exceptions_list_blacklist": { "message": "제외할 사이트 (차단 목록):" }, + "exceptions_list_whitelist": { "message": "포함할 사이트 (허용 목록):" }, "clear_all": { "message": "모두 지우기" }, @@ -173,6 +178,15 @@ "total_highlights": { "message": "전체" }, + "total_matches": { + "message": "총 일치 수" + }, + "group_by_list": { + "message": "목록별로 그룹화" + }, + "other": { + "message": "기타" + }, "refresh": { "message": "새로고침" }, diff --git a/_locales/nl/messages.json b/_locales/nl/messages.json index 83df711..37fc558 100644 --- a/_locales/nl/messages.json +++ b/_locales/nl/messages.json @@ -122,9 +122,14 @@ "manage_exceptions": { "message": "Beheren" }, + "exceptions_mode": { "message": "Modus:" }, + "exceptions_mode_blacklist": { "message": "Zwarte lijst — niet markeren op deze sites" }, + "exceptions_mode_whitelist": { "message": "Witte lijst — alleen markeren op deze sites" }, "exceptions_list": { "message": "Uitzondering sites:" }, + "exceptions_list_blacklist": { "message": "Uit te sluiten sites (zwarte lijst):" }, + "exceptions_list_whitelist": { "message": "Toe te voegen sites (witte lijst):" }, "clear_all": { "message": "Alles wissen" }, @@ -173,6 +178,15 @@ "total_highlights": { "message": "Totaal" }, + "total_matches": { + "message": "Totaal aantal matches" + }, + "group_by_list": { + "message": "Groeperen op lijst" + }, + "other": { + "message": "Overige" + }, "refresh": { "message": "Vernieuwen" }, diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index d0d164c..8001187 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -122,9 +122,14 @@ "manage_exceptions": { "message": "Zarządzaj" }, + "exceptions_mode": { "message": "Tryb:" }, + "exceptions_mode_blacklist": { "message": "Czarna lista — nie podświetlaj na tych stronach" }, + "exceptions_mode_whitelist": { "message": "Biała lista — podświetlaj tylko na tych stronach" }, "exceptions_list": { "message": "Strony wyjątków:" }, + "exceptions_list_blacklist": { "message": "Strony do wykluczenia (czarna lista):" }, + "exceptions_list_whitelist": { "message": "Strony do uwzględnienia (biała lista):" }, "clear_all": { "message": "Wyczyść wszystko" }, @@ -173,6 +178,15 @@ "total_highlights": { "message": "Łącznie" }, + "total_matches": { + "message": "Łączna liczba dopasowań" + }, + "group_by_list": { + "message": "Grupuj według listy" + }, + "other": { + "message": "Inne" + }, "refresh": { "message": "Odśwież" }, diff --git a/_locales/pt_BR/messages.json b/_locales/pt_BR/messages.json index d37c977..bed56a4 100644 --- a/_locales/pt_BR/messages.json +++ b/_locales/pt_BR/messages.json @@ -122,9 +122,14 @@ "manage_exceptions": { "message": "Gerenciar" }, + "exceptions_mode": { "message": "Modo:" }, + "exceptions_mode_blacklist": { "message": "Lista negra — não destacar nestes sites" }, + "exceptions_mode_whitelist": { "message": "Lista branca — destacar apenas nestes sites" }, "exceptions_list": { "message": "Sites de exceção:" }, + "exceptions_list_blacklist": { "message": "Sites a excluir (lista negra):" }, + "exceptions_list_whitelist": { "message": "Sites a incluir (lista branca):" }, "clear_all": { "message": "Limpar tudo" }, @@ -173,6 +178,15 @@ "total_highlights": { "message": "Total" }, + "total_matches": { + "message": "Total de correspondências" + }, + "group_by_list": { + "message": "Agrupar por lista" + }, + "other": { + "message": "Outros" + }, "refresh": { "message": "Atualizar" }, diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 9b5238d..e51e20a 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -122,9 +122,14 @@ "manage_exceptions": { "message": "Управление" }, + "exceptions_mode": { "message": "Режим:" }, + "exceptions_mode_blacklist": { "message": "Чёрный список — не подсвечивать на этих сайтах" }, + "exceptions_mode_whitelist": { "message": "Белый список — подсвечивать только на этих сайтах" }, "exceptions_list": { "message": "Сайты-исключения:" }, + "exceptions_list_blacklist": { "message": "Исключить сайты (чёрный список):" }, + "exceptions_list_whitelist": { "message": "Включить сайты (белый список):" }, "clear_all": { "message": "Очистить все" }, @@ -173,6 +178,15 @@ "total_highlights": { "message": "Всего" }, + "total_matches": { + "message": "Всего совпадений" + }, + "group_by_list": { + "message": "Группировать по списку" + }, + "other": { + "message": "Прочее" + }, "refresh": { "message": "Обновить" }, diff --git a/_locales/tr/messages.json b/_locales/tr/messages.json index 762c7d7..6456b2b 100644 --- a/_locales/tr/messages.json +++ b/_locales/tr/messages.json @@ -122,9 +122,14 @@ "manage_exceptions": { "message": "Yönet" }, + "exceptions_mode": { "message": "Mod:" }, + "exceptions_mode_blacklist": { "message": "Kara liste — bu sitelerde vurgulama" }, + "exceptions_mode_whitelist": { "message": "Beyaz liste — yalnızca bu sitelerde vurgula" }, "exceptions_list": { "message": "İstisna Siteleri:" }, + "exceptions_list_blacklist": { "message": "Hariç tutulacak siteler (kara liste):" }, + "exceptions_list_whitelist": { "message": "Dahil edilecek siteler (beyaz liste):" }, "clear_all": { "message": "Hepsini Temizle" }, @@ -173,6 +178,15 @@ "total_highlights": { "message": "Toplam" }, + "total_matches": { + "message": "Toplam eşleşme" + }, + "group_by_list": { + "message": "Listeye göre grupla" + }, + "other": { + "message": "Diğer" + }, "refresh": { "message": "Yenile" }, diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index c9b623e..1521960 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -122,9 +122,14 @@ "manage_exceptions": { "message": "管理" }, + "exceptions_mode": { "message": "模式:" }, + "exceptions_mode_blacklist": { "message": "黑名单 — 不在这些网站上高亮" }, + "exceptions_mode_whitelist": { "message": "白名单 — 仅在这些网站上高亮" }, "exceptions_list": { "message": "例外网站:" }, + "exceptions_list_blacklist": { "message": "要排除的网站(黑名单):" }, + "exceptions_list_whitelist": { "message": "要包含的网站(白名单):" }, "clear_all": { "message": "清除全部" }, @@ -173,6 +178,15 @@ "total_highlights": { "message": "总计" }, + "total_matches": { + "message": "总匹配数" + }, + "group_by_list": { + "message": "按列表分组" + }, + "other": { + "message": "其他" + }, "refresh": { "message": "刷新" }, diff --git a/popup/popup.css b/popup/popup.css index bfa6075..6addc17 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -799,52 +799,108 @@ body { .page-highlights-section { display: flex; flex-direction: column; - gap: 6px; + gap: 10px; flex: 1; min-height: 0; } -.page-highlights-info-card { +.page-highlights-header-row { display: flex; align-items: center; - justify-content: space-between; - padding: 8px 10px; - background: var(--input-bg); - border: 1px solid var(--input-border); - border-radius: 8px; - font-size: 14px; - color: var(--text-color); -} - -.page-highlights-info-card strong { - font-weight: 600; - color: var(--accent); -} - -.refresh-button { - width: 100%; - min-height: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; gap: 8px; - background: var(--input-bg); - border: 1px solid var(--input-border); - border-radius: 8px; - cursor: pointer; - font-size: 14px; - color: var(--text-color); - transition: all 0.2s; flex-shrink: 0; } -.refresh-button:hover { +.page-highlights-total-label { + font-size: var(--text-xs); + font-weight: 600; + letter-spacing: var(--tracking-wide); + text-transform: uppercase; + color: var(--text-color); + opacity: 0.7; + flex-shrink: 0; +} + +.page-highlights-total-count { + flex: 1; + font-size: var(--text-base); + font-weight: 600; + color: var(--accent); + text-align: right; +} + +.page-highlights-controls { + display: flex; + flex-direction: column; + gap: 10px; + flex-shrink: 0; +} + +.page-highlights-group-toggle { + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; + padding: 8px 10px; + margin: 0 -2px; + border-radius: 6px; + font-size: var(--text-sm); + color: var(--text-color); + background: var(--input-bg); + border: 1px solid var(--input-border); + transition: background 0.2s, border-color 0.2s; +} + +.page-highlights-group-toggle:hover { background: var(--section-bg); } -.refresh-button i { - font-size: 16px; +.page-highlights-group-toggle .switch-wrapper { + flex-shrink: 0; +} + +.page-highlights-group-label { + flex: 1; + font-weight: 500; + user-select: none; +} + +.page-highlights-filters { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; +} + +.page-highlights-filter-chip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + font-size: var(--text-xs); + border-radius: 6px; + cursor: pointer; + background: var(--input-bg); + border: 1px solid var(--input-border); + color: var(--text-color); + transition: all 0.2s; +} + +.page-highlights-filter-chip:hover { + background: var(--section-bg); +} + +.page-highlights-filter-chip.active { + border-color: var(--list-color, var(--accent)); + background: color-mix(in srgb, var(--list-color, var(--accent)) 14%, var(--input-bg)); + color: var(--text-color); +} + +.page-highlights-filter-chip .filter-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; } .page-highlights-list { @@ -852,7 +908,7 @@ body { overflow-y: auto; background: var(--input-bg); border: 1px solid var(--input-border); - border-radius: 8px; + border-radius: 6px; min-height: 0; } @@ -864,25 +920,74 @@ body { opacity: 0.6; } +.page-highlights-group-section { + border-bottom: 1px solid var(--input-border); +} + +.page-highlights-group-section:last-child { + border-bottom: none; +} + +.page-highlights-group-header { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + cursor: pointer; + font-size: var(--text-xs); + font-weight: 600; + letter-spacing: var(--tracking-wide); + text-transform: uppercase; + color: var(--text-color); + opacity: 0.7; + background: var(--section-bg); + transition: background 0.2s; +} + +.page-highlights-group-header:hover { + opacity: 0.9; +} + +.page-highlights-group-header i { + font-size: 10px; + transition: transform 0.2s; +} + +.page-highlights-group-section.collapsed .page-highlights-group-header i { + transform: rotate(-90deg); +} + +.page-highlights-group-header .group-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + .page-highlight-item { display: flex; align-items: center; justify-content: space-between; gap: 12px; - padding: 10px 12px; + padding: 6px 12px 6px 14px; border-bottom: 1px solid var(--input-border); cursor: pointer; transition: all 0.2s; + border-left: 3px solid transparent; + background: var(--input-bg); } + .page-highlight-item:last-child { border-bottom: none; } .page-highlight-item:hover { background: var(--section-bg); - border-left: 3px solid var(--accent); - padding-left: 9px; +} + +.page-highlight-item[style*="--item-tint"] { + background: color-mix(in srgb, var(--item-tint) 4%, var(--input-bg)); } .page-highlight-word { @@ -891,14 +996,31 @@ body { align-items: center; gap: 8px; min-width: 0; + flex-wrap: wrap; } .page-highlight-preview { - display: inline-block; - padding: 3px 8px; - border-radius: 4px; - font-size: 13px; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 2px 0; + font-size: var(--text-base); font-weight: 500; + color: var(--text-color); +} + +.page-highlight-preview .preview-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; +} + +.page-highlight-list-tag { + font-size: var(--text-xs); + color: var(--text-color); + opacity: 0.5; + font-weight: 400; } .page-highlight-position { @@ -949,69 +1071,156 @@ body { min-height: 0; } -.exception-toggle-btn { +.exceptions-mode-card { + padding: 6px 10px; + background: var(--input-bg); + border: 1px solid var(--input-border); + border-radius: 6px; + display: flex; + flex-direction: column; + gap: 4px; + flex-shrink: 0; +} + +.exceptions-mode-label { + font-size: var(--text-xs); + font-weight: 500; + letter-spacing: var(--tracking-wide); + text-transform: uppercase; + color: var(--text-color); + opacity: 0.6; +} + +.exceptions-mode-select { width: 100%; min-height: 32px; + padding: 0 10px; + font-size: var(--text-base); + color: var(--text-color); + background: var(--input-bg); + border: 1px solid var(--input-border); + border-radius: 6px; + cursor: pointer; +} + +.exceptions-add-row { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; +} + +.exception-domain-input { + flex: 1; + min-width: 0; height: 32px; + padding: 0 10px; + font-size: var(--text-base); + background: var(--input-bg); + border: 1px solid var(--input-border); + border-radius: 6px; + color: var(--text-color); +} + +.exception-domain-input:focus { + outline: none; + border-color: var(--accent); +} + +.exception-add-btn { + width: 32px; + height: 32px; + min-width: 32px; display: flex; align-items: center; justify-content: center; - gap: 8px; - background: var(--input-bg); - border: 1px solid var(--input-border); - border-radius: 8px; + border-radius: 6px; cursor: pointer; - font-size: 14px; - color: var(--text-color); transition: all 0.2s; flex-shrink: 0; } -.exception-toggle-btn:hover { - background: var(--section-bg); -} - -.exception-toggle-btn.danger { - background: rgba(239, 68, 68, 0.1); - border-color: rgba(239, 68, 68, 0.3); - color: var(--danger); -} - -.exception-toggle-btn.danger:hover { - background: rgba(239, 68, 68, 0.2); -} - -.exception-toggle-btn i { - font-size: 16px; +.exception-add-btn i { + font-size: 14px; } .exceptions-list-wrapper { display: flex; flex-direction: column; - gap: 6px; + gap: 4px; flex: 1; min-height: 0; } +.exceptions-list-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + flex-shrink: 0; +} + +.clear-exceptions-link { + background: none; + border: none; + padding: 0; + font-size: var(--text-xs); + font-weight: 500; + color: var(--text-color); + opacity: 0.6; + cursor: pointer; + transition: opacity 0.2s, color 0.2s; +} + +.clear-exceptions-link:hover { + opacity: 1; + color: var(--danger); + text-decoration: underline; +} + .exceptions-list { flex: 1; overflow-y: auto; background: var(--input-bg); border: 1px solid var(--input-border); - border-radius: 8px; + border-radius: 6px; min-height: 0; } .exception-item { display: flex; align-items: center; - justify-content: space-between; - gap: 12px; - padding: 10px 12px; + gap: 10px; + padding: 8px 12px; border-bottom: 1px solid var(--input-border); transition: background 0.2s; } +.exception-item .exception-domain-icon { + width: 22px; + height: 22px; + min-width: 22px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 10px; + color: var(--text-color); + opacity: 0.5; + background: var(--section-bg); + border-radius: 50%; +} + +.exception-item .exception-domain-icon i { + font-size: 10px; +} + +.exception-item.exception-empty { + justify-content: center; + color: var(--text-color); + opacity: 0.6; +} + .exception-item:last-child { border-bottom: none; } @@ -1022,10 +1231,11 @@ body { .exception-domain { flex: 1; - font-size: 14px; + font-size: var(--text-base); color: var(--text-color); word-break: break-word; line-height: 1.4; + min-width: 0; } .exception-remove { @@ -1054,29 +1264,6 @@ body { font-size: 12px; } -.clear-exceptions-btn { - width: 100%; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - background: rgba(239, 68, 68, 0.1); - border: 1px solid rgba(239, 68, 68, 0.3); - color: var(--danger); - border-radius: 8px; - cursor: pointer; - font-size: 14px; - transition: all 0.2s; -} - -.clear-exceptions-btn:hover { - background: rgba(239, 68, 68, 0.2); -} - -.clear-exceptions-btn i { - font-size: 16px; -} /* Settings Overlay */ .settings-overlay { diff --git a/popup/popup.html b/popup/popup.html index 40bb8a4..cbc0f16 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -198,16 +198,20 @@
-
- Total: - 0 +
+ Total matches + 0 +
+
+ +
- - -
@@ -221,23 +225,26 @@ Site Exceptions - - - +
+ Mode + +
+
+ + +
- +
+ Exception Sites + +
- -
diff --git a/shared/colors.css b/shared/colors.css index 955a123..d9aa276 100644 --- a/shared/colors.css +++ b/shared/colors.css @@ -34,6 +34,8 @@ --section-bg: #ffffff; --panel-bg: #ffffff; --switch-bg: #e0e0e0; + --switch-thumb: #ffffff; + --switch-track-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); --checkbox-accent: #ff8c00; --checkbox-border: #d0d0d0; --focus-ring: 0 0 0 3px rgba(255, 140, 0, 0.25); @@ -64,6 +66,8 @@ body.dark { --section-bg: #121212; --panel-bg: #121212; --switch-bg: #2a2a2a; + --switch-thumb: #e5e5e5; + --switch-track-shadow: 0 1px 3px rgba(0, 0, 0, 0.25); --checkbox-accent: #ff8c00; --checkbox-border: #333333; --focus-ring: 0 0 0 3px rgba(255, 140, 0, 0.3); diff --git a/shared/ui-components.css b/shared/ui-components.css index eccd47b..18acc98 100644 --- a/shared/ui-components.css +++ b/shared/ui-components.css @@ -93,8 +93,8 @@ button:disabled { .switch-wrapper { position: relative; display: inline-block; - width: 40px; - height: 22px; + width: 42px; + height: 24px; cursor: pointer; flex-shrink: 0; } @@ -114,30 +114,46 @@ button:disabled { right: 0; bottom: 0; background-color: var(--input-border); - transition: 0.3s; - border-radius: 11px; + border-radius: 9999px; + box-shadow: var(--switch-track-shadow); + transition: background-color 0.25s ease, box-shadow 0.2s ease; + backface-visibility: hidden; + -webkit-backface-visibility: hidden; } .switch-slider:before { position: absolute; content: ""; - height: 16px; - width: 16px; - left: 2px; - bottom: 2px; - background-color: white; - transition: 0.3s; + width: 18px; + height: 18px; + left: 3px; + top: 3px; + background-color: var(--switch-thumb); border-radius: 50%; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.04); + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); + backface-visibility: hidden; + -webkit-backface-visibility: hidden; } .switch-input:checked + .switch-slider { background-color: var(--accent); + box-shadow: var(--switch-track-shadow); } .switch-input:checked + .switch-slider:before { transform: translateX(18px); } +.switch-wrapper:hover .switch-slider:before { + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.22), 0 0 0 1px rgba(0, 0, 0, 0.04); +} + +.switch-input:focus-visible + .switch-slider { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + /* Switch Toggle (Checkbox Style) */ input[type="checkbox"].switch { diff --git a/src/background.ts b/src/background.ts index c524703..24931c5 100644 --- a/src/background.ts +++ b/src/background.ts @@ -38,9 +38,16 @@ class BackgroundService { private setupInstallListener(): void { chrome.runtime.onInstalled.addListener(async (): Promise => { - const data = await StorageService.get(['exceptionsList']); + const data = await StorageService.get(['exceptionsList', 'exceptionsMode']); + const updates: { exceptionsList?: string[]; exceptionsMode?: 'blacklist' | 'whitelist' } = {}; if (!data.exceptionsList) { - await StorageService.update('exceptionsList', []); + updates.exceptionsList = []; + } + if (data.exceptionsMode !== 'blacklist' && data.exceptionsMode !== 'whitelist') { + updates.exceptionsMode = 'blacklist'; + } + if (Object.keys(updates).length > 0) { + await StorageService.set(updates); } }); } diff --git a/src/content/ContentScript.ts b/src/content/ContentScript.ts index bc71a22..ab98fbf 100644 --- a/src/content/ContentScript.ts +++ b/src/content/ContentScript.ts @@ -1,4 +1,4 @@ -import { HighlightList, MessageData } from '../types.js'; +import { HighlightList, MessageData, ExceptionsMode } from '../types.js'; import { StorageService } from '../services/StorageService.js'; import { MessageService } from '../services/MessageService.js'; import { HighlightEngine } from './HighlightEngine.js'; @@ -7,7 +7,8 @@ export class ContentScript { private lists: HighlightList[] = []; private isGlobalHighlightEnabled = true; private exceptionsList: string[] = []; - private isCurrentSiteException = false; + private exceptionsMode: ExceptionsMode = 'blacklist'; + private shouldSkipDueToExceptions = false; private matchCase = false; private matchWhole = false; private highlightEngine: HighlightEngine; @@ -26,11 +27,12 @@ export class ContentScript { private async loadSettings(): Promise { const data = await StorageService.get([ - 'lists', - 'globalHighlightEnabled', - 'matchCaseEnabled', - 'matchWholeEnabled', - 'exceptionsList' + 'lists', + 'globalHighlightEnabled', + 'matchCaseEnabled', + 'matchWholeEnabled', + 'exceptionsList', + 'exceptionsMode' ]); this.lists = data.lists || []; @@ -38,12 +40,17 @@ export class ContentScript { this.matchCase = data.matchCaseEnabled ?? false; this.matchWhole = data.matchWholeEnabled ?? false; this.exceptionsList = data.exceptionsList || []; - this.isCurrentSiteException = this.checkCurrentSiteException(); + this.exceptionsMode = data.exceptionsMode === 'whitelist' ? 'whitelist' : 'blacklist'; + this.shouldSkipDueToExceptions = this.computeShouldSkipDueToExceptions(); } - private checkCurrentSiteException(): boolean { + private computeShouldSkipDueToExceptions(): boolean { const currentHostname = window.location.hostname; - return this.exceptionsList.includes(currentHostname); + const isInList = this.exceptionsList.includes(currentHostname); + if (this.exceptionsMode === 'blacklist') { + return isInList; + } + return !isInList; } private setupMessageListener(): void { @@ -91,9 +98,10 @@ export class ContentScript { } private async handleExceptionsUpdate(): Promise { - const data = await StorageService.get(['exceptionsList']); + const data = await StorageService.get(['exceptionsList', 'exceptionsMode']); this.exceptionsList = data.exceptionsList || []; - this.isCurrentSiteException = this.checkCurrentSiteException(); + this.exceptionsMode = data.exceptionsMode === 'whitelist' ? 'whitelist' : 'blacklist'; + this.shouldSkipDueToExceptions = this.computeShouldSkipDueToExceptions(); this.processHighlights(); } @@ -102,7 +110,7 @@ export class ContentScript { this.isProcessing = true; try { - if (!this.isGlobalHighlightEnabled || this.isCurrentSiteException) { + if (!this.isGlobalHighlightEnabled || this.shouldSkipDueToExceptions) { this.highlightEngine.clearHighlights(); this.highlightEngine.stopObserving(); return; @@ -124,13 +132,15 @@ export class ContentScript { activeWords.push({ text: word.wordStr, background: word.background || list.background, - foreground: word.foreground || list.foreground + foreground: word.foreground || list.foreground, + listId: list.id, + listName: list.name || 'Default' }); } } const highlights = this.highlightEngine.getPageHighlights(activeWords); - sendResponse({ highlights }); + sendResponse({ highlights, lists: this.lists.filter(l => l.active) }); } private handleScrollToHighlight(word: string, index: number): void { diff --git a/src/content/HighlightEngine.ts b/src/content/HighlightEngine.ts index e8ac8f4..bea6a63 100644 --- a/src/content/HighlightEngine.ts +++ b/src/content/HighlightEngine.ts @@ -419,8 +419,8 @@ export class HighlightEngine { } } - getPageHighlights(activeWords: ActiveWord[]): Array<{ word: string; count: number; background: string; foreground: string }> { - const seen = new Map(); + getPageHighlights(activeWords: ActiveWord[]): Array<{ word: string; count: number; background: string; foreground: string; listId?: number; listName?: string; listNames: string[] }> { + const seen = new Map(); for (const activeWord of activeWords) { const lookup = this.currentMatchCase ? activeWord.text : activeWord.text.toLowerCase(); @@ -429,13 +429,26 @@ export class HighlightEngine { const totalCount = (ranges?.length || 0) + (textareaMatches?.length || 0); - if (totalCount > 0 && !seen.has(lookup)) { - seen.set(lookup, { - word: activeWord.text, - count: totalCount, - background: activeWord.background, - foreground: activeWord.foreground - }); + if (totalCount > 0) { + const listName = activeWord.listName || 'Default'; + const listId = activeWord.listId; + + if (seen.has(lookup)) { + const existing = seen.get(lookup)!; + if (listName && !existing.listNames.includes(listName)) { + existing.listNames.push(listName); + } + } else { + seen.set(lookup, { + word: activeWord.text, + count: totalCount, + background: activeWord.background, + foreground: activeWord.foreground, + listId, + listName, + listNames: [listName] + }); + } } } diff --git a/src/popup/PopupController.ts b/src/popup/PopupController.ts index 3c4ef11..d773d1d 100644 --- a/src/popup/PopupController.ts +++ b/src/popup/PopupController.ts @@ -1,4 +1,4 @@ -import { HighlightList, HighlightWord, HighlightInfo, ExportData } from '../types.js'; +import { HighlightList, HighlightWord, HighlightInfo, ExportData, ExceptionsMode } from '../types.js'; import { StorageService } from '../services/StorageService.js'; import { MessageService } from '../services/MessageService.js'; import { DOMUtils } from '../utils/DOMUtils.js'; @@ -15,9 +15,14 @@ export class PopupController { private matchCaseEnabled = false; private matchWholeEnabled = false; private exceptionsList: string[] = []; + private exceptionsMode: ExceptionsMode = 'blacklist'; private currentTabHost = ''; private activeTab = 'lists'; - private pageHighlights: Array<{ word: string; count: number; background: string; foreground: string }> = []; + private pageHighlights: Array<{ word: string; count: number; background: string; foreground: string; listId?: number; listName?: string; listNames: string[] }> = []; + private pageHighlightsActiveLists: Array<{ id: number; name: string; background: string }> = []; + private pageHighlightsGroupByList = false; + private pageHighlightsListFilter = new Set(); + private pageHighlightsCollapsedGroups = new Set(); private highlightIndices = new Map(); private wordMenuOpenForIndex: number | null = null; private wordMenuCopyOnly = false; @@ -54,6 +59,7 @@ export class PopupController { this.matchCaseEnabled = data.matchCaseEnabled ?? false; this.matchWholeEnabled = data.matchWholeEnabled ?? false; this.exceptionsList = data.exceptionsList || []; + this.exceptionsMode = data.exceptionsMode === 'whitelist' ? 'whitelist' : 'blacklist'; if (this.lists.length === 0) { this.lists.push({ @@ -93,6 +99,8 @@ export class PopupController { wordSearchQuery?: string; currentPage?: number; scrollPositions?: Record; + pageHighlightsGroupByList?: boolean; + pageHighlightsListFilter?: number[]; }; if (typeof state.activeTab === 'string' && state.activeTab !== 'options') { this.activeTab = state.activeTab; @@ -109,18 +117,26 @@ export class PopupController { if (state.scrollPositions && typeof state.scrollPositions === 'object') { this.scrollPositions = { ...state.scrollPositions }; } + if (typeof state.pageHighlightsGroupByList === 'boolean') { + this.pageHighlightsGroupByList = state.pageHighlightsGroupByList; + } + if (Array.isArray(state.pageHighlightsListFilter)) { + this.pageHighlightsListFilter = new Set(state.pageHighlightsListFilter); + } } catch { // keep defaults } } - private getPopupStatePayload(): { activeTab: string; currentListIndex: number; wordSearchQuery: string; currentPage: number; scrollPositions: Record } { + private getPopupStatePayload(): { activeTab: string; currentListIndex: number; wordSearchQuery: string; currentPage: number; scrollPositions: Record; pageHighlightsGroupByList: boolean; pageHighlightsListFilter: number[] } { return { activeTab: this.activeTab, currentListIndex: this.currentListIndex, wordSearchQuery: this.wordSearchQuery, currentPage: this.currentPage, - scrollPositions: this.scrollPositions + scrollPositions: this.scrollPositions, + pageHighlightsGroupByList: this.pageHighlightsGroupByList, + pageHighlightsListFilter: Array.from(this.pageHighlightsListFilter) }; } @@ -268,7 +284,8 @@ export class PopupController { document.getElementById('exportSettingsBtn')?.addEventListener('click', () => { const data: ExportData = { lists: this.lists, - exceptionsList: [...this.exceptionsList] + exceptionsList: [...this.exceptionsList], + exceptionsMode: this.exceptionsMode }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); @@ -320,6 +337,11 @@ export class PopupController { exceptionsApplied = true; } + if (obj.exceptionsMode === 'whitelist' || obj.exceptionsMode === 'blacklist') { + this.exceptionsMode = obj.exceptionsMode; + exceptionsApplied = true; + } + if (!listsApplied && !exceptionsApplied) { alert(chrome.i18n.getMessage('invalid_import_format') || 'Invalid file format. Please select a valid export file.'); importSettingsInput.value = ''; @@ -908,14 +930,24 @@ export class PopupController { } private setupPageHighlights(): void { - document.getElementById('refreshHighlightsBtn')?.addEventListener('click', async () => { - await this.loadPageHighlights(); - }); - document.getElementById('pageHighlightsList')?.addEventListener('click', async (e) => { const target = e.target as HTMLElement; - const item = target.closest('.page-highlight-item') as HTMLElement; + const groupHeader = target.closest('.page-highlights-group-header'); + if (groupHeader) { + const section = groupHeader.closest('.page-highlights-group-section'); + const groupKey = section?.getAttribute('data-group'); + if (groupKey) { + if (this.pageHighlightsCollapsedGroups.has(groupKey)) { + this.pageHighlightsCollapsedGroups.delete(groupKey); + } else { + this.pageHighlightsCollapsedGroups.add(groupKey); + } + this.renderPageHighlights(); + return; + } + } + const item = target.closest('.page-highlight-item') as HTMLElement; if (!item) return; const word = item.dataset.word; @@ -934,6 +966,12 @@ export class PopupController { await this.jumpToHighlight(word, currentIndex); } }); + + document.getElementById('pageHighlightsGroupByList')?.addEventListener('change', (e) => { + this.pageHighlightsGroupByList = (e.target as HTMLInputElement).checked; + this.savePopupState(); + this.renderPageHighlights(); + }); } private async loadPageHighlights(): Promise { @@ -941,15 +979,25 @@ export class PopupController { const response = await MessageService.sendToActiveTab({ type: 'GET_PAGE_HIGHLIGHTS' }); if (response && response.highlights) { - this.pageHighlights = response.highlights; + this.pageHighlights = response.highlights.map((h: { word: string; count: number; background: string; foreground: string; listId?: number; listName?: string; listNames?: string[] }) => ({ + ...h, + listNames: h.listNames || (h.listName ? [h.listName] : []) + })); + this.pageHighlightsActiveLists = response.lists || []; + if (this.pageHighlightsListFilter.size === 0 && this.pageHighlightsActiveLists.length > 0) { + this.pageHighlightsListFilter = new Set(this.pageHighlightsActiveLists.map((l: { id: number }) => l.id)); + } this.highlightIndices.clear(); this.pageHighlights.forEach(h => this.highlightIndices.set(h.word, 0)); this.renderPageHighlights(); + this.renderPageHighlightsFilters(); } } catch (e) { console.error('Error loading page highlights:', e); this.pageHighlights = []; + this.pageHighlightsActiveLists = []; this.renderPageHighlights(); + this.renderPageHighlightsFilters(); } } @@ -976,70 +1024,155 @@ export class PopupController { await this.jumpToHighlight(word, newIndex); } + private passesListFilter(h: { listId?: number; listNames: string[] }): boolean { + if (this.pageHighlightsListFilter.size === 0) return true; + const wordListIds = new Set(); + if (h.listId !== undefined) wordListIds.add(h.listId); + for (const name of h.listNames) { + const list = this.pageHighlightsActiveLists.find(l => l.name === name); + if (list) wordListIds.add(list.id); + } + return [...wordListIds].some(id => this.pageHighlightsListFilter.has(id)); + } + + private renderPageHighlightsItem(highlight: { word: string; count: number; background: string; foreground: string; listNames: string[] }): string { + const currentIndex = this.highlightIndices.get(highlight.word) || 0; + return ` +
+
+ + + ${DOMUtils.escapeHtml(highlight.word)} + + ${highlight.count > 1 ? `${currentIndex + 1}/${highlight.count}` : ''} +
+ ${highlight.count > 1 ? ` +
+ + +
+ ` : ''} +
+ `; + } + private renderPageHighlights(): void { const container = document.getElementById('pageHighlightsList'); const countElement = document.getElementById('totalHighlightsCount'); if (!container || !countElement) return; - const totalCount = this.pageHighlights.reduce((sum, h) => sum + h.count, 0); + const filtered = this.pageHighlights.filter(h => this.passesListFilter(h)); + const totalCount = filtered.reduce((sum, h) => sum + h.count, 0); countElement.textContent = totalCount.toString(); - if (this.pageHighlights.length === 0) { + if (filtered.length === 0) { container.innerHTML = `
${chrome.i18n.getMessage('no_highlights_on_page') || 'No highlights on this page'}
`; return; } - container.innerHTML = this.pageHighlights.map(highlight => { - const currentIndex = this.highlightIndices.get(highlight.word) || 0; - return ` -
-
- - ${DOMUtils.escapeHtml(highlight.word)} - - ${highlight.count > 1 ? `${currentIndex + 1}/${highlight.count}` : ''} -
- ${highlight.count > 1 ? ` -
- - + if (this.pageHighlightsGroupByList && this.pageHighlightsActiveLists.length > 0) { + const listIds = new Set(this.pageHighlightsActiveLists.map(l => l.id).filter(id => this.pageHighlightsListFilter.has(id) || this.pageHighlightsListFilter.size === 0)); + const groupOrder = this.pageHighlightsActiveLists.filter(l => listIds.has(l.id)); + let html = ''; + for (const list of groupOrder) { + const items = filtered.filter(h => h.listId === list.id || (h.listNames && h.listNames.includes(list.name))); + if (items.length === 0) continue; + const groupKey = `list-${list.id}`; + const collapsed = this.pageHighlightsCollapsedGroups.has(groupKey); + const chevron = collapsed ? 'fa-chevron-right' : 'fa-chevron-down'; + html += ` +
+
+ + + ${DOMUtils.escapeHtml(list.name)} + (${items.reduce((s, i) => s + i.count, 0)})
- ` : ''} -
- `; - }).join(''); + ${collapsed ? '' : items.map(h => this.renderPageHighlightsItem(h)).join('')} +
+ `; + } + const ungrouped = filtered.filter(h => !groupOrder.some(l => h.listId === l.id || (h.listNames && h.listNames.includes(l.name)))); + if (ungrouped.length > 0) { + const groupKey = 'list-other'; + const collapsed = this.pageHighlightsCollapsedGroups.has(groupKey); + const chevron = collapsed ? 'fa-chevron-right' : 'fa-chevron-down'; + html += ` +
+
+ + ${chrome.i18n.getMessage('other') || 'Other'} +
+ ${collapsed ? '' : ungrouped.map(h => this.renderPageHighlightsItem(h)).join('')} +
+ `; + } + container.innerHTML = html; + } else { + container.innerHTML = filtered.map(h => this.renderPageHighlightsItem(h)).join(''); + } + if (this.activeTab === 'page-highlights') { requestAnimationFrame(() => this.restoreScrollPositions()); } } + private renderPageHighlightsFilters(): void { + const container = document.getElementById('pageHighlightsListFilters'); + if (!container) return; + if (this.pageHighlightsActiveLists.length <= 1) { + container.innerHTML = ''; + return; + } + const allSelected = this.pageHighlightsListFilter.size === 0 || this.pageHighlightsListFilter.size === this.pageHighlightsActiveLists.length; + container.innerHTML = this.pageHighlightsActiveLists.map(list => { + const active = this.pageHighlightsListFilter.size === 0 || this.pageHighlightsListFilter.has(list.id); + const bg = DOMUtils.escapeHtml(list.background); + return ` + + `; + }).join(''); + + container.querySelectorAll('.page-highlights-filter-chip').forEach(btn => { + btn.addEventListener('click', () => { + const id = Number((btn as HTMLElement).dataset.listId); + if (this.pageHighlightsListFilter.has(id)) { + this.pageHighlightsListFilter.delete(id); + } else { + this.pageHighlightsListFilter.add(id); + } + this.savePopupState(); + this.renderPageHighlights(); + this.renderPageHighlightsFilters(); + }); + }); + } + private setupExceptions(): void { - document.getElementById('toggleExceptionBtn')?.addEventListener('click', async () => { - if (!this.currentTabHost) return; - - const isException = this.exceptionsList.includes(this.currentTabHost); - - if (isException) { - this.exceptionsList = this.exceptionsList.filter(domain => domain !== this.currentTabHost); - } else { - this.exceptionsList.push(this.currentTabHost); - } - - this.updateExceptionButton(); - this.renderExceptions(); - await StorageService.update('exceptionsList', this.exceptionsList); + document.getElementById('exceptionsModeSelect')?.addEventListener('change', async (e) => { + const value = (e.target as HTMLSelectElement).value; + this.exceptionsMode = value === 'whitelist' ? 'whitelist' : 'blacklist'; + await StorageService.update('exceptionsMode', this.exceptionsMode); MessageService.sendToAllTabs({ type: 'EXCEPTIONS_LIST_UPDATED' }); + this.updateExceptionsModeLabel(); + }); + + document.getElementById('addExceptionBtn')?.addEventListener('click', () => this.addExceptionFromInput()); + (document.getElementById('exceptionDomainInput') as HTMLInputElement)?.addEventListener('keydown', (e) => { + if (e.key === 'Enter') this.addExceptionFromInput(); }); document.getElementById('clearExceptionsBtn')?.addEventListener('click', async () => { if (confirm(chrome.i18n.getMessage('confirm_clear_exceptions') || 'Clear all exceptions?')) { this.exceptionsList = []; - this.updateExceptionButton(); this.renderExceptions(); await StorageService.update('exceptionsList', this.exceptionsList); MessageService.sendToAllTabs({ type: 'EXCEPTIONS_LIST_UPDATED' }); @@ -1051,7 +1184,6 @@ export class PopupController { if (button) { const domain = (button as HTMLElement).dataset.domain!; this.exceptionsList = this.exceptionsList.filter(d => d !== domain); - this.updateExceptionButton(); this.renderExceptions(); await StorageService.update('exceptionsList', this.exceptionsList); MessageService.sendToAllTabs({ type: 'EXCEPTIONS_LIST_UPDATED' }); @@ -1191,7 +1323,8 @@ export class PopupController { globalHighlightEnabled: this.globalHighlightEnabled, matchCaseEnabled: this.matchCaseEnabled, matchWholeEnabled: this.matchWholeEnabled, - exceptionsList: this.exceptionsList + exceptionsList: this.exceptionsList, + exceptionsMode: this.exceptionsMode }); this.renderLists(); @@ -1201,7 +1334,7 @@ export class PopupController { private setupStorageSync(): void { chrome.storage.onChanged.addListener((changes, areaName) => { if (areaName !== 'local') return; - if (changes.lists || changes.globalHighlightEnabled || changes.matchCaseEnabled || changes.matchWholeEnabled || changes.exceptionsList) { + if (changes.lists || changes.globalHighlightEnabled || changes.matchCaseEnabled || changes.matchWholeEnabled || changes.exceptionsList || changes.exceptionsMode) { this.reloadFromStorage(); } }); @@ -1214,6 +1347,7 @@ export class PopupController { this.matchCaseEnabled = data.matchCaseEnabled ?? false; this.matchWholeEnabled = data.matchWholeEnabled ?? false; this.exceptionsList = data.exceptionsList || []; + this.exceptionsMode = data.exceptionsMode === 'whitelist' ? 'whitelist' : 'blacklist'; if (this.lists.length === 0) { this.lists.push({ @@ -1238,7 +1372,8 @@ export class PopupController { this.renderLists(); this.renderWords(); this.renderExceptions(); - this.updateExceptionButton(); + this.updateExceptionsModeSelect(); + this.updateExceptionsModeLabel(); this.updateFormValues(); } @@ -1465,38 +1600,64 @@ export class PopupController { `; } - private updateExceptionButton(): void { - const toggleBtn = document.getElementById('toggleExceptionBtn'); - const btnText = document.getElementById('exceptionBtnText'); - - if (!toggleBtn || !btnText || !this.currentTabHost) return; - - const isException = this.exceptionsList.includes(this.currentTabHost); - - if (isException) { - btnText.textContent = chrome.i18n.getMessage('remove_exception') || 'Remove from Exceptions'; - toggleBtn.classList.add('danger'); - const icon = toggleBtn.querySelector('i'); - if (icon) icon.className = 'fa-solid fa-trash'; - } else { - btnText.textContent = chrome.i18n.getMessage('add_exception') || 'Add to Exceptions'; - toggleBtn.classList.remove('danger'); - const icon = toggleBtn.querySelector('i'); - if (icon) icon.className = 'fa-solid fa-plus'; + private normalizeDomain(input: string): string | null { + const raw = input.trim().toLowerCase(); + if (!raw) return null; + try { + if (raw.includes('.')) { + const url = raw.startsWith('http') ? new URL(raw) : new URL(`https://${raw}`); + return url.hostname; + } + return raw; + } catch { + return raw; } } + private addExceptionFromInput(): void { + const input = document.getElementById('exceptionDomainInput') as HTMLInputElement; + if (!input) return; + + const domain = this.normalizeDomain(input.value); + if (!domain) return; + + if (this.exceptionsList.includes(domain)) { + input.value = ''; + return; + } + + this.exceptionsList.push(domain); + input.value = ''; + this.renderExceptions(); + StorageService.update('exceptionsList', this.exceptionsList).then(() => { + MessageService.sendToAllTabs({ type: 'EXCEPTIONS_LIST_UPDATED' }); + }); + } + + private updateExceptionsModeSelect(): void { + const select = document.getElementById('exceptionsModeSelect') as HTMLSelectElement | null; + if (select) select.value = this.exceptionsMode; + } + + private updateExceptionsModeLabel(): void { + const label = document.getElementById('exceptionsListLabel'); + if (!label) return; + const key = this.exceptionsMode === 'whitelist' ? 'exceptions_list_whitelist' : 'exceptions_list_blacklist'; + label.textContent = chrome.i18n.getMessage(key) || (this.exceptionsMode === 'whitelist' ? 'Sites to highlight (whitelist):' : 'Sites to exclude (blacklist):'); + } + private renderExceptions(): void { const container = document.getElementById('exceptionsList'); if (!container) return; if (this.exceptionsList.length === 0) { - container.innerHTML = `
${chrome.i18n.getMessage('no_exceptions') || 'No exceptions'}
`; + container.innerHTML = `
${chrome.i18n.getMessage('no_exceptions') || 'No exceptions'}
`; return; } container.innerHTML = this.exceptionsList.map(domain => `
+ ${DOMUtils.escapeHtml(domain)}