mirror of
https://github.com/obsqrbtz/goose-highlighter.git
synced 2026-04-08 20:19:06 +03:00
feat: show all found words and allow jump to them (beta)
This commit is contained in:
@@ -8,6 +8,7 @@ Goose Highlighter is a browser extension that allows you to highlight words on a
|
|||||||
- **Custom Colors:** Set background and foreground for each list or individual word.
|
- **Custom Colors:** Set background and foreground for each list or individual word.
|
||||||
- **Bulk Add:** Paste multiple words at once.
|
- **Bulk Add:** Paste multiple words at once.
|
||||||
- **Enable/Disable:** Toggle highlighting globally, per list, or per word.
|
- **Enable/Disable:** Toggle highlighting globally, per list, or per word.
|
||||||
|
- **Page Navigation:** View all highlights on the current page and jump to any occurrence with a single click.
|
||||||
- **Site Exceptions:** Add specific websites to an exceptions list to disable highlighting there.
|
- **Site Exceptions:** Add specific websites to an exceptions list to disable highlighting there.
|
||||||
- **Import/Export:** Backup or share your highlight lists and exceptions as JSON files.
|
- **Import/Export:** Backup or share your highlight lists and exceptions as JSON files.
|
||||||
|
|
||||||
|
|||||||
@@ -151,5 +151,26 @@
|
|||||||
},
|
},
|
||||||
"text_color_title": {
|
"text_color_title": {
|
||||||
"message": "Textfarbe"
|
"message": "Textfarbe"
|
||||||
|
},
|
||||||
|
"tab_page_highlights": {
|
||||||
|
"message": "Auf Seite"
|
||||||
|
},
|
||||||
|
"highlights_on_page": {
|
||||||
|
"message": "Hervorhebungen auf dieser Seite"
|
||||||
|
},
|
||||||
|
"total_highlights": {
|
||||||
|
"message": "Gesamt"
|
||||||
|
},
|
||||||
|
"refresh": {
|
||||||
|
"message": "Aktualisieren"
|
||||||
|
},
|
||||||
|
"no_highlights_on_page": {
|
||||||
|
"message": "Keine Hervorhebungen auf dieser Seite gefunden"
|
||||||
|
},
|
||||||
|
"previous": {
|
||||||
|
"message": "Vorherige"
|
||||||
|
},
|
||||||
|
"next": {
|
||||||
|
"message": "Nächste"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,5 +157,26 @@
|
|||||||
},
|
},
|
||||||
"text_color_title": {
|
"text_color_title": {
|
||||||
"message": "Text color"
|
"message": "Text color"
|
||||||
|
},
|
||||||
|
"tab_page_highlights": {
|
||||||
|
"message": "On Page"
|
||||||
|
},
|
||||||
|
"highlights_on_page": {
|
||||||
|
"message": "Highlights on This Page"
|
||||||
|
},
|
||||||
|
"total_highlights": {
|
||||||
|
"message": "Total"
|
||||||
|
},
|
||||||
|
"refresh": {
|
||||||
|
"message": "Refresh"
|
||||||
|
},
|
||||||
|
"no_highlights_on_page": {
|
||||||
|
"message": "No highlights found on this page"
|
||||||
|
},
|
||||||
|
"previous": {
|
||||||
|
"message": "Previous"
|
||||||
|
},
|
||||||
|
"next": {
|
||||||
|
"message": "Next"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,5 +157,26 @@
|
|||||||
},
|
},
|
||||||
"text_color_title": {
|
"text_color_title": {
|
||||||
"message": "Color de texto"
|
"message": "Color de texto"
|
||||||
|
},
|
||||||
|
"tab_page_highlights": {
|
||||||
|
"message": "En página"
|
||||||
|
},
|
||||||
|
"highlights_on_page": {
|
||||||
|
"message": "Resaltados en esta página"
|
||||||
|
},
|
||||||
|
"total_highlights": {
|
||||||
|
"message": "Total"
|
||||||
|
},
|
||||||
|
"refresh": {
|
||||||
|
"message": "Actualizar"
|
||||||
|
},
|
||||||
|
"no_highlights_on_page": {
|
||||||
|
"message": "No se encontraron resaltados en esta página"
|
||||||
|
},
|
||||||
|
"previous": {
|
||||||
|
"message": "Anterior"
|
||||||
|
},
|
||||||
|
"next": {
|
||||||
|
"message": "Siguiente"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,5 +157,26 @@
|
|||||||
},
|
},
|
||||||
"text_color_title": {
|
"text_color_title": {
|
||||||
"message": "Couleur du texte"
|
"message": "Couleur du texte"
|
||||||
|
},
|
||||||
|
"tab_page_highlights": {
|
||||||
|
"message": "Sur la page"
|
||||||
|
},
|
||||||
|
"highlights_on_page": {
|
||||||
|
"message": "Surlignages sur cette page"
|
||||||
|
},
|
||||||
|
"total_highlights": {
|
||||||
|
"message": "Total"
|
||||||
|
},
|
||||||
|
"refresh": {
|
||||||
|
"message": "Actualiser"
|
||||||
|
},
|
||||||
|
"no_highlights_on_page": {
|
||||||
|
"message": "Aucun surlignage trouvé sur cette page"
|
||||||
|
},
|
||||||
|
"previous": {
|
||||||
|
"message": "Précédent"
|
||||||
|
},
|
||||||
|
"next": {
|
||||||
|
"message": "Suivant"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,5 +157,26 @@
|
|||||||
},
|
},
|
||||||
"text_color_title": {
|
"text_color_title": {
|
||||||
"message": "पाठ रंग"
|
"message": "पाठ रंग"
|
||||||
|
},
|
||||||
|
"tab_page_highlights": {
|
||||||
|
"message": "पृष्ठ पर"
|
||||||
|
},
|
||||||
|
"highlights_on_page": {
|
||||||
|
"message": "इस पृष्ठ पर हाइलाइट"
|
||||||
|
},
|
||||||
|
"total_highlights": {
|
||||||
|
"message": "कुल"
|
||||||
|
},
|
||||||
|
"refresh": {
|
||||||
|
"message": "रीफ्रेश करें"
|
||||||
|
},
|
||||||
|
"no_highlights_on_page": {
|
||||||
|
"message": "इस पृष्ठ पर कोई हाइलाइट नहीं मिला"
|
||||||
|
},
|
||||||
|
"previous": {
|
||||||
|
"message": "पिछला"
|
||||||
|
},
|
||||||
|
"next": {
|
||||||
|
"message": "अगला"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,5 +157,26 @@
|
|||||||
},
|
},
|
||||||
"text_color_title": {
|
"text_color_title": {
|
||||||
"message": "Colore del testo"
|
"message": "Colore del testo"
|
||||||
|
},
|
||||||
|
"tab_page_highlights": {
|
||||||
|
"message": "Sulla pagina"
|
||||||
|
},
|
||||||
|
"highlights_on_page": {
|
||||||
|
"message": "Evidenziazioni su questa pagina"
|
||||||
|
},
|
||||||
|
"total_highlights": {
|
||||||
|
"message": "Totale"
|
||||||
|
},
|
||||||
|
"refresh": {
|
||||||
|
"message": "Aggiorna"
|
||||||
|
},
|
||||||
|
"no_highlights_on_page": {
|
||||||
|
"message": "Nessuna evidenziazione trovata su questa pagina"
|
||||||
|
},
|
||||||
|
"previous": {
|
||||||
|
"message": "Precedente"
|
||||||
|
},
|
||||||
|
"next": {
|
||||||
|
"message": "Successivo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,5 +157,26 @@
|
|||||||
},
|
},
|
||||||
"text_color_title": {
|
"text_color_title": {
|
||||||
"message": "文字色"
|
"message": "文字色"
|
||||||
|
},
|
||||||
|
"tab_page_highlights": {
|
||||||
|
"message": "ページ上"
|
||||||
|
},
|
||||||
|
"highlights_on_page": {
|
||||||
|
"message": "このページのハイライト"
|
||||||
|
},
|
||||||
|
"total_highlights": {
|
||||||
|
"message": "合計"
|
||||||
|
},
|
||||||
|
"refresh": {
|
||||||
|
"message": "更新"
|
||||||
|
},
|
||||||
|
"no_highlights_on_page": {
|
||||||
|
"message": "このページにハイライトが見つかりません"
|
||||||
|
},
|
||||||
|
"previous": {
|
||||||
|
"message": "前へ"
|
||||||
|
},
|
||||||
|
"next": {
|
||||||
|
"message": "次へ"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,5 +157,26 @@
|
|||||||
},
|
},
|
||||||
"text_color_title": {
|
"text_color_title": {
|
||||||
"message": "글자색"
|
"message": "글자색"
|
||||||
|
},
|
||||||
|
"tab_page_highlights": {
|
||||||
|
"message": "페이지에서"
|
||||||
|
},
|
||||||
|
"highlights_on_page": {
|
||||||
|
"message": "이 페이지의 하이라이트"
|
||||||
|
},
|
||||||
|
"total_highlights": {
|
||||||
|
"message": "전체"
|
||||||
|
},
|
||||||
|
"refresh": {
|
||||||
|
"message": "새로고침"
|
||||||
|
},
|
||||||
|
"no_highlights_on_page": {
|
||||||
|
"message": "이 페이지에서 하이라이트를 찾을 수 없습니다"
|
||||||
|
},
|
||||||
|
"previous": {
|
||||||
|
"message": "이전"
|
||||||
|
},
|
||||||
|
"next": {
|
||||||
|
"message": "다음"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,5 +157,26 @@
|
|||||||
},
|
},
|
||||||
"text_color_title": {
|
"text_color_title": {
|
||||||
"message": "Tekstkleur"
|
"message": "Tekstkleur"
|
||||||
|
},
|
||||||
|
"tab_page_highlights": {
|
||||||
|
"message": "Op pagina"
|
||||||
|
},
|
||||||
|
"highlights_on_page": {
|
||||||
|
"message": "Markeringen op deze pagina"
|
||||||
|
},
|
||||||
|
"total_highlights": {
|
||||||
|
"message": "Totaal"
|
||||||
|
},
|
||||||
|
"refresh": {
|
||||||
|
"message": "Vernieuwen"
|
||||||
|
},
|
||||||
|
"no_highlights_on_page": {
|
||||||
|
"message": "Geen markeringen gevonden op deze pagina"
|
||||||
|
},
|
||||||
|
"previous": {
|
||||||
|
"message": "Vorige"
|
||||||
|
},
|
||||||
|
"next": {
|
||||||
|
"message": "Volgende"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,5 +157,26 @@
|
|||||||
},
|
},
|
||||||
"text_color_title": {
|
"text_color_title": {
|
||||||
"message": "Kolor tekstu"
|
"message": "Kolor tekstu"
|
||||||
|
},
|
||||||
|
"tab_page_highlights": {
|
||||||
|
"message": "Na stronie"
|
||||||
|
},
|
||||||
|
"highlights_on_page": {
|
||||||
|
"message": "Podświetlenia na tej stronie"
|
||||||
|
},
|
||||||
|
"total_highlights": {
|
||||||
|
"message": "Łącznie"
|
||||||
|
},
|
||||||
|
"refresh": {
|
||||||
|
"message": "Odśwież"
|
||||||
|
},
|
||||||
|
"no_highlights_on_page": {
|
||||||
|
"message": "Nie znaleziono podświetleń na tej stronie"
|
||||||
|
},
|
||||||
|
"previous": {
|
||||||
|
"message": "Poprzedni"
|
||||||
|
},
|
||||||
|
"next": {
|
||||||
|
"message": "Następny"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,5 +151,26 @@
|
|||||||
},
|
},
|
||||||
"text_color_title": {
|
"text_color_title": {
|
||||||
"message": "Cor do texto"
|
"message": "Cor do texto"
|
||||||
|
},
|
||||||
|
"tab_page_highlights": {
|
||||||
|
"message": "Na página"
|
||||||
|
},
|
||||||
|
"highlights_on_page": {
|
||||||
|
"message": "Destaques nesta página"
|
||||||
|
},
|
||||||
|
"total_highlights": {
|
||||||
|
"message": "Total"
|
||||||
|
},
|
||||||
|
"refresh": {
|
||||||
|
"message": "Atualizar"
|
||||||
|
},
|
||||||
|
"no_highlights_on_page": {
|
||||||
|
"message": "Nenhum destaque encontrado nesta página"
|
||||||
|
},
|
||||||
|
"previous": {
|
||||||
|
"message": "Anterior"
|
||||||
|
},
|
||||||
|
"next": {
|
||||||
|
"message": "Próximo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,5 +157,26 @@
|
|||||||
},
|
},
|
||||||
"text_color_title": {
|
"text_color_title": {
|
||||||
"message": "Цвет текста"
|
"message": "Цвет текста"
|
||||||
|
},
|
||||||
|
"tab_page_highlights": {
|
||||||
|
"message": "На странице"
|
||||||
|
},
|
||||||
|
"highlights_on_page": {
|
||||||
|
"message": "Найдено на этой странице"
|
||||||
|
},
|
||||||
|
"total_highlights": {
|
||||||
|
"message": "Всего"
|
||||||
|
},
|
||||||
|
"refresh": {
|
||||||
|
"message": "Обновить"
|
||||||
|
},
|
||||||
|
"no_highlights_on_page": {
|
||||||
|
"message": "На этой странице не найдены слова из списка"
|
||||||
|
},
|
||||||
|
"previous": {
|
||||||
|
"message": "Предыдущее"
|
||||||
|
},
|
||||||
|
"next": {
|
||||||
|
"message": "Следующее"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,5 +157,26 @@
|
|||||||
},
|
},
|
||||||
"text_color_title": {
|
"text_color_title": {
|
||||||
"message": "Metin rengi"
|
"message": "Metin rengi"
|
||||||
|
},
|
||||||
|
"tab_page_highlights": {
|
||||||
|
"message": "Sayfada"
|
||||||
|
},
|
||||||
|
"highlights_on_page": {
|
||||||
|
"message": "Bu sayfadaki vurgular"
|
||||||
|
},
|
||||||
|
"total_highlights": {
|
||||||
|
"message": "Toplam"
|
||||||
|
},
|
||||||
|
"refresh": {
|
||||||
|
"message": "Yenile"
|
||||||
|
},
|
||||||
|
"no_highlights_on_page": {
|
||||||
|
"message": "Bu sayfada vurgu bulunamadı"
|
||||||
|
},
|
||||||
|
"previous": {
|
||||||
|
"message": "Önceki"
|
||||||
|
},
|
||||||
|
"next": {
|
||||||
|
"message": "Sonraki"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,5 +157,26 @@
|
|||||||
},
|
},
|
||||||
"text_color_title": {
|
"text_color_title": {
|
||||||
"message": "文字颜色"
|
"message": "文字颜色"
|
||||||
|
},
|
||||||
|
"tab_page_highlights": {
|
||||||
|
"message": "页面上"
|
||||||
|
},
|
||||||
|
"highlights_on_page": {
|
||||||
|
"message": "此页面上的高亮"
|
||||||
|
},
|
||||||
|
"total_highlights": {
|
||||||
|
"message": "总计"
|
||||||
|
},
|
||||||
|
"refresh": {
|
||||||
|
"message": "刷新"
|
||||||
|
},
|
||||||
|
"no_highlights_on_page": {
|
||||||
|
"message": "此页面上未找到高亮"
|
||||||
|
},
|
||||||
|
"previous": {
|
||||||
|
"message": "上一个"
|
||||||
|
},
|
||||||
|
"next": {
|
||||||
|
"message": "下一个"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
110
popup/popup.css
110
popup/popup.css
@@ -306,8 +306,8 @@ input[type="color"] {
|
|||||||
border: 1.5px solid var(--input-border);
|
border: 1.5px solid var(--input-border);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
width: 32px;
|
width: 24px;
|
||||||
height: 32px;
|
height: 24px;
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -597,13 +597,14 @@ input[type="file"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#wordList input[type="color"] {
|
#wordList input[type="color"] {
|
||||||
width: 26px;
|
width: 22px;
|
||||||
height: 26px;
|
height: 22px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
border: 1px solid var(--input-border);
|
border: 1px solid var(--input-border);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s;
|
||||||
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#wordList input[type="color"]:hover {
|
#wordList input[type="color"]:hover {
|
||||||
@@ -770,3 +771,104 @@ body::-webkit-scrollbar-corner,
|
|||||||
.exceptions-list::-webkit-scrollbar-corner {
|
.exceptions-list::-webkit-scrollbar-corner {
|
||||||
background: var(--section-bg);
|
background: var(--section-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Page Highlights Section */
|
||||||
|
.section[data-section="page-highlights"] {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-highlights-info {
|
||||||
|
font-size: 0.85em;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--input-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--input-border);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-highlights-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-top: 8px;
|
||||||
|
border: 1px solid var(--input-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--input-bg);
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-highlight-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid var(--input-border);
|
||||||
|
font-size: 0.85em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-highlight-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-highlight-item:hover {
|
||||||
|
background: var(--highlight-tag);
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
padding-left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-highlight-word {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
word-break: break-word;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-highlight-preview {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-highlight-count {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--accent-text);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75em;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-highlight-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-highlight-nav button {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 0.75em;
|
||||||
|
min-width: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-highlight-position {
|
||||||
|
font-size: 0.7em;
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-highlights-empty {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="tab-button active" data-tab="lists"><i class="fa-solid fa-list"></i> <span data-i18n="tab_lists">Lists</span></button>
|
<button class="tab-button active" data-tab="lists"><i class="fa-solid fa-list"></i> <span data-i18n="tab_lists">Lists</span></button>
|
||||||
<button class="tab-button" data-tab="words"><i class="fa-solid fa-tags"></i> <span data-i18n="tab_words">Words</span></button>
|
<button class="tab-button" data-tab="words"><i class="fa-solid fa-tags"></i> <span data-i18n="tab_words">Words</span></button>
|
||||||
|
<button class="tab-button" data-tab="page-highlights"><i class="fa-solid fa-location-dot"></i> <span data-i18n="tab_page_highlights">On Page</span></button>
|
||||||
<button class="tab-button" data-tab="exceptions"><i class="fa-solid fa-ban"></i> <span data-i18n="tab_exceptions">Exceptions</span></button>
|
<button class="tab-button" data-tab="exceptions"><i class="fa-solid fa-ban"></i> <span data-i18n="tab_exceptions">Exceptions</span></button>
|
||||||
<button class="tab-button" data-tab="options"><i class="fa-solid fa-sliders"></i> <span data-i18n="options">Options</span></button>
|
<button class="tab-button" data-tab="options"><i class="fa-solid fa-sliders"></i> <span data-i18n="options">Options</span></button>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,6 +93,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" data-tab-content="page-highlights">
|
||||||
|
<div class="section" data-section="page-highlights">
|
||||||
|
<h3><i class="fa-solid fa-location-dot"></i> <span data-i18n="highlights_on_page">Highlights on This Page</span></h3>
|
||||||
|
<div class="page-highlights-info">
|
||||||
|
<span data-i18n="total_highlights">Total:</span> <strong id="totalHighlightsCount">0</strong>
|
||||||
|
</div>
|
||||||
|
<button id="refreshHighlightsBtn"><i class="fa-solid fa-rotate"></i> <span data-i18n="refresh">Refresh</span></button>
|
||||||
|
<div id="pageHighlightsList" class="page-highlights-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tab-content" data-tab-content="exceptions">
|
<div class="tab-content" data-tab-content="exceptions">
|
||||||
<div class="section" data-section="exceptions">
|
<div class="section" data-section="exceptions">
|
||||||
<h3><i class="fa-solid fa-ban"></i> <span data-i18n="site_exceptions">Site Exceptions</span></h3>
|
<h3><i class="fa-solid fa-ban"></i> <span data-i18n="site_exceptions">Site Exceptions</span></h3>
|
||||||
|
|||||||
@@ -47,21 +47,28 @@ export class ContentScript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setupMessageListener(): void {
|
private setupMessageListener(): void {
|
||||||
MessageService.onMessage((message: MessageData) => {
|
MessageService.onMessage((message: MessageData, sender: any, sendResponse: (response?: any) => void) => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'WORD_LIST_UPDATED':
|
case 'WORD_LIST_UPDATED':
|
||||||
this.handleWordListUpdate();
|
this.handleWordListUpdate();
|
||||||
break;
|
return false;
|
||||||
case 'GLOBAL_TOGGLE_UPDATED':
|
case 'GLOBAL_TOGGLE_UPDATED':
|
||||||
this.handleGlobalToggleUpdate(message.enabled!);
|
this.handleGlobalToggleUpdate(message.enabled!);
|
||||||
break;
|
return false;
|
||||||
case 'MATCH_OPTIONS_UPDATED':
|
case 'MATCH_OPTIONS_UPDATED':
|
||||||
this.handleMatchOptionsUpdate(message.matchCase!, message.matchWhole!);
|
this.handleMatchOptionsUpdate(message.matchCase!, message.matchWhole!);
|
||||||
break;
|
return false;
|
||||||
case 'EXCEPTIONS_LIST_UPDATED':
|
case 'EXCEPTIONS_LIST_UPDATED':
|
||||||
this.handleExceptionsUpdate();
|
this.handleExceptionsUpdate();
|
||||||
break;
|
return false;
|
||||||
|
case 'GET_PAGE_HIGHLIGHTS':
|
||||||
|
this.handleGetPageHighlights(sendResponse);
|
||||||
|
return true;
|
||||||
|
case 'SCROLL_TO_HIGHLIGHT':
|
||||||
|
this.handleScrollToHighlight(message.word!, message.index!);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,4 +113,27 @@ export class ContentScript {
|
|||||||
this.isProcessing = false;
|
this.isProcessing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleGetPageHighlights(sendResponse: (response: any) => void): void {
|
||||||
|
const activeWords: ActiveWord[] = [];
|
||||||
|
|
||||||
|
for (const list of this.lists) {
|
||||||
|
if (!list.active) continue;
|
||||||
|
for (const word of list.words) {
|
||||||
|
if (!word.active) continue;
|
||||||
|
activeWords.push({
|
||||||
|
text: word.wordStr,
|
||||||
|
background: word.background || list.background,
|
||||||
|
foreground: word.foreground || list.foreground
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const highlights = this.highlightEngine.getPageHighlights(activeWords);
|
||||||
|
sendResponse({ highlights });
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleScrollToHighlight(word: string, index: number): void {
|
||||||
|
this.highlightEngine.scrollToHighlight(word, index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,8 +4,10 @@ import { DOMUtils } from '../utils/DOMUtils.js';
|
|||||||
export class HighlightEngine {
|
export class HighlightEngine {
|
||||||
private styleSheet: CSSStyleSheet | null = null;
|
private styleSheet: CSSStyleSheet | null = null;
|
||||||
private highlights = new Map<string, Highlight>();
|
private highlights = new Map<string, Highlight>();
|
||||||
|
private highlightsByWord = new Map<string, Range[]>();
|
||||||
private observer: MutationObserver;
|
private observer: MutationObserver;
|
||||||
private isHighlighting = false;
|
private isHighlighting = false;
|
||||||
|
private currentMatchCase = false;
|
||||||
|
|
||||||
constructor(private onUpdate: () => void) {
|
constructor(private onUpdate: () => void) {
|
||||||
this.observer = new MutationObserver(DOMUtils.debounce((mutations: MutationRecord[]) => {
|
this.observer = new MutationObserver(DOMUtils.debounce((mutations: MutationRecord[]) => {
|
||||||
@@ -106,6 +108,8 @@ export class HighlightEngine {
|
|||||||
if (this.isHighlighting) return;
|
if (this.isHighlighting) return;
|
||||||
this.isHighlighting = true;
|
this.isHighlighting = true;
|
||||||
|
|
||||||
|
this.currentMatchCase = matchCase;
|
||||||
|
|
||||||
this.observer.disconnect();
|
this.observer.disconnect();
|
||||||
this.clearHighlightsInternal();
|
this.clearHighlightsInternal();
|
||||||
|
|
||||||
@@ -143,6 +147,7 @@ export class HighlightEngine {
|
|||||||
const textNodes = this.getTextNodes();
|
const textNodes = this.getTextNodes();
|
||||||
|
|
||||||
const rangesByStyle = new Map<number, Range[]>();
|
const rangesByStyle = new Map<number, Range[]>();
|
||||||
|
this.highlightsByWord.clear();
|
||||||
|
|
||||||
for (const node of textNodes) {
|
for (const node of textNodes) {
|
||||||
if (!node.nodeValue) continue;
|
if (!node.nodeValue) continue;
|
||||||
@@ -164,6 +169,11 @@ export class HighlightEngine {
|
|||||||
rangesByStyle.set(styleIdx, []);
|
rangesByStyle.set(styleIdx, []);
|
||||||
}
|
}
|
||||||
rangesByStyle.get(styleIdx)!.push(range);
|
rangesByStyle.get(styleIdx)!.push(range);
|
||||||
|
|
||||||
|
if (!this.highlightsByWord.has(lookup)) {
|
||||||
|
this.highlightsByWord.set(lookup, []);
|
||||||
|
}
|
||||||
|
this.highlightsByWord.get(lookup)!.push(range);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,6 +197,7 @@ export class HighlightEngine {
|
|||||||
CSS.highlights.delete(name);
|
CSS.highlights.delete(name);
|
||||||
}
|
}
|
||||||
this.highlights.clear();
|
this.highlights.clear();
|
||||||
|
this.highlightsByWord.clear();
|
||||||
|
|
||||||
if (this.styleSheet && this.styleSheet.cssRules.length > 0) {
|
if (this.styleSheet && this.styleSheet.cssRules.length > 0) {
|
||||||
while (this.styleSheet.cssRules.length > 0) {
|
while (this.styleSheet.cssRules.length > 0) {
|
||||||
@@ -195,6 +206,67 @@ export class HighlightEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPageHighlights(activeWords: ActiveWord[]): Array<{ word: string; count: number; background: string; foreground: string }> {
|
||||||
|
const seen = new Map<string, { word: string; count: number; background: string; foreground: string }>();
|
||||||
|
|
||||||
|
for (const activeWord of activeWords) {
|
||||||
|
const lookup = this.currentMatchCase ? activeWord.text : activeWord.text.toLowerCase();
|
||||||
|
const ranges = this.highlightsByWord.get(lookup);
|
||||||
|
|
||||||
|
if (ranges && ranges.length > 0 && !seen.has(lookup)) {
|
||||||
|
seen.set(lookup, {
|
||||||
|
word: activeWord.text,
|
||||||
|
count: ranges.length,
|
||||||
|
background: activeWord.background,
|
||||||
|
foreground: activeWord.foreground
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(seen.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToHighlight(word: string, index: number): void {
|
||||||
|
const lookup = this.currentMatchCase ? word : word.toLowerCase();
|
||||||
|
const ranges = this.highlightsByWord.get(lookup);
|
||||||
|
|
||||||
|
if (!ranges || ranges.length === 0) return;
|
||||||
|
|
||||||
|
const targetIndex = Math.min(index, ranges.length - 1);
|
||||||
|
const range = ranges[targetIndex];
|
||||||
|
|
||||||
|
if (!range) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rect = range.getBoundingClientRect();
|
||||||
|
|
||||||
|
const absoluteTop = window.pageYOffset + rect.top;
|
||||||
|
const middle = absoluteTop - (window.innerHeight / 2) + (rect.height / 2);
|
||||||
|
|
||||||
|
window.scrollTo({
|
||||||
|
top: middle,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
|
||||||
|
const flashHighlight = new Highlight(range);
|
||||||
|
CSS.highlights.set('gh-flash', flashHighlight);
|
||||||
|
|
||||||
|
if (this.styleSheet) {
|
||||||
|
const flashRule = '::highlight(gh-flash) { background-color: rgba(255, 165, 0, 0.8); box-shadow: 0 0 10px 3px rgba(255, 165, 0, 0.8); }';
|
||||||
|
const ruleIndex = this.styleSheet.insertRule(flashRule, this.styleSheet.cssRules.length);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
CSS.highlights.delete('gh-flash');
|
||||||
|
if (this.styleSheet && ruleIndex < this.styleSheet.cssRules.length) {
|
||||||
|
this.styleSheet.deleteRule(ruleIndex);
|
||||||
|
}
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error scrolling to highlight:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stopObserving(): void {
|
stopObserving(): void {
|
||||||
this.observer.disconnect();
|
this.observer.disconnect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HighlightList, HighlightWord, ExportData } from '../types.js';
|
import { HighlightList, HighlightWord, ExportData, HighlightInfo } from '../types.js';
|
||||||
import { StorageService } from '../services/StorageService.js';
|
import { StorageService } from '../services/StorageService.js';
|
||||||
import { MessageService } from '../services/MessageService.js';
|
import { MessageService } from '../services/MessageService.js';
|
||||||
import { DOMUtils } from '../utils/DOMUtils.js';
|
import { DOMUtils } from '../utils/DOMUtils.js';
|
||||||
@@ -14,6 +14,8 @@ export class PopupController {
|
|||||||
private exceptionsList: string[] = [];
|
private exceptionsList: string[] = [];
|
||||||
private currentTabHost = '';
|
private currentTabHost = '';
|
||||||
private activeTab = 'lists';
|
private activeTab = 'lists';
|
||||||
|
private pageHighlights: Array<{ word: string; count: number; background: string; foreground: string }> = [];
|
||||||
|
private highlightIndices = new Map<string, number>();
|
||||||
|
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
await this.loadData();
|
await this.loadData();
|
||||||
@@ -99,6 +101,10 @@ export class PopupController {
|
|||||||
document.querySelectorAll('.tab-content').forEach(content => {
|
document.querySelectorAll('.tab-content').forEach(content => {
|
||||||
content.classList.toggle('active', content.getAttribute('data-tab-content') === tabName);
|
content.classList.toggle('active', content.getAttribute('data-tab-content') === tabName);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (tabName === 'page-highlights') {
|
||||||
|
this.loadPageHighlights();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupEventListeners(): void {
|
private setupEventListeners(): void {
|
||||||
@@ -106,6 +112,7 @@ export class PopupController {
|
|||||||
this.setupListManagement();
|
this.setupListManagement();
|
||||||
this.setupWordManagement();
|
this.setupWordManagement();
|
||||||
this.setupSettings();
|
this.setupSettings();
|
||||||
|
this.setupPageHighlights();
|
||||||
this.setupExceptions();
|
this.setupExceptions();
|
||||||
this.setupImportExport();
|
this.setupImportExport();
|
||||||
this.setupTheme();
|
this.setupTheme();
|
||||||
@@ -318,6 +325,110 @@ 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;
|
||||||
|
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
const word = item.dataset.word;
|
||||||
|
if (!word) return;
|
||||||
|
|
||||||
|
if (target.classList.contains('highlight-prev')) {
|
||||||
|
await this.navigateHighlight(word, -1);
|
||||||
|
} else if (target.classList.contains('highlight-next')) {
|
||||||
|
await this.navigateHighlight(word, 1);
|
||||||
|
} else {
|
||||||
|
await this.jumpToHighlight(word, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadPageHighlights(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const response = await MessageService.sendToActiveTab({ type: 'GET_PAGE_HIGHLIGHTS' });
|
||||||
|
|
||||||
|
if (response && response.highlights) {
|
||||||
|
this.pageHighlights = response.highlights;
|
||||||
|
this.highlightIndices.clear();
|
||||||
|
this.pageHighlights.forEach(h => this.highlightIndices.set(h.word, 0));
|
||||||
|
this.renderPageHighlights();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error loading page highlights:', e);
|
||||||
|
this.pageHighlights = [];
|
||||||
|
this.renderPageHighlights();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async jumpToHighlight(word: string, index: number): Promise<void> {
|
||||||
|
this.highlightIndices.set(word, index);
|
||||||
|
await MessageService.sendToActiveTab({
|
||||||
|
type: 'SCROLL_TO_HIGHLIGHT',
|
||||||
|
word,
|
||||||
|
index
|
||||||
|
});
|
||||||
|
this.renderPageHighlights();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async navigateHighlight(word: string, direction: number): Promise<void> {
|
||||||
|
const highlight = this.pageHighlights.find(h => h.word === word);
|
||||||
|
if (!highlight) return;
|
||||||
|
|
||||||
|
const currentIndex = this.highlightIndices.get(word) || 0;
|
||||||
|
let newIndex = currentIndex + direction;
|
||||||
|
|
||||||
|
if (newIndex < 0) newIndex = highlight.count - 1;
|
||||||
|
if (newIndex >= highlight.count) newIndex = 0;
|
||||||
|
|
||||||
|
await this.jumpToHighlight(word, newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
countElement.textContent = totalCount.toString();
|
||||||
|
|
||||||
|
if (this.pageHighlights.length === 0) {
|
||||||
|
container.innerHTML = `<div class="page-highlights-empty">${chrome.i18n.getMessage('no_highlights_on_page') || 'No highlights on this page'}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = this.pageHighlights.map(highlight => {
|
||||||
|
const currentIndex = this.highlightIndices.get(highlight.word) || 0;
|
||||||
|
return `
|
||||||
|
<div class="page-highlight-item" data-word="${DOMUtils.escapeHtml(highlight.word)}">
|
||||||
|
<div class="page-highlight-word">
|
||||||
|
<span class="page-highlight-preview" style="background-color: ${highlight.background}; color: ${highlight.foreground};">
|
||||||
|
${DOMUtils.escapeHtml(highlight.word)}
|
||||||
|
</span>
|
||||||
|
${highlight.count > 1 ? `<span class="page-highlight-position">${currentIndex + 1}/${highlight.count}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
<span class="page-highlight-count">${highlight.count}</span>
|
||||||
|
${highlight.count > 1 ? `
|
||||||
|
<div class="page-highlight-nav">
|
||||||
|
<button class="highlight-prev" title="${chrome.i18n.getMessage('previous') || 'Previous'}">
|
||||||
|
<i class="fa-solid fa-chevron-up"></i>
|
||||||
|
</button>
|
||||||
|
<button class="highlight-next" title="${chrome.i18n.getMessage('next') || 'Next'}">
|
||||||
|
<i class="fa-solid fa-chevron-down"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
private setupExceptions(): void {
|
private setupExceptions(): void {
|
||||||
document.getElementById('toggleExceptionBtn')?.addEventListener('click', async () => {
|
document.getElementById('toggleExceptionBtn')?.addEventListener('click', async () => {
|
||||||
if (!this.currentTabHost) return;
|
if (!this.currentTabHost) return;
|
||||||
|
|||||||
@@ -19,7 +19,15 @@ export class MessageService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static onMessage(callback: (message: MessageData) => void): void {
|
static onMessage(callback: (message: MessageData, sender: any, sendResponse: (response?: any) => void) => void | boolean): void {
|
||||||
chrome.runtime.onMessage.addListener(callback);
|
chrome.runtime.onMessage.addListener(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async sendToActiveTab(message: MessageData): Promise<any> {
|
||||||
|
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
|
if (tab.id) {
|
||||||
|
return chrome.tabs.sendMessage(tab.id, message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
12
src/types.ts
12
src/types.ts
@@ -28,11 +28,21 @@ export interface ActiveWord {
|
|||||||
foreground: string;
|
foreground: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HighlightInfo {
|
||||||
|
word: string;
|
||||||
|
count: number;
|
||||||
|
background: string;
|
||||||
|
foreground: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MessageData {
|
export interface MessageData {
|
||||||
type: 'WORD_LIST_UPDATED' | 'GLOBAL_TOGGLE_UPDATED' | 'MATCH_OPTIONS_UPDATED' | 'EXCEPTIONS_LIST_UPDATED';
|
type: 'WORD_LIST_UPDATED' | 'GLOBAL_TOGGLE_UPDATED' | 'MATCH_OPTIONS_UPDATED' | 'EXCEPTIONS_LIST_UPDATED' | 'GET_PAGE_HIGHLIGHTS' | 'PAGE_HIGHLIGHTS_RESPONSE' | 'SCROLL_TO_HIGHLIGHT';
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
matchCase?: boolean;
|
matchCase?: boolean;
|
||||||
matchWhole?: boolean;
|
matchWhole?: boolean;
|
||||||
|
highlights?: HighlightInfo[];
|
||||||
|
word?: string;
|
||||||
|
index?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExportData {
|
export interface ExportData {
|
||||||
|
|||||||
Reference in New Issue
Block a user