30 Commits

Author SHA1 Message Date
semantic-release-bot
14f1b4b935 chore(release): 1.10.0
# [1.10.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.9.3...v1.10.0) (2025-11-20)

### Bug Fixes

* lighten debounce() usage, do not do full re-render on every change ([348d64f](348d64f356))
* made placeholder for textarea, added outline offset ([4f32be0](4f32be0b93))

### Features

* new tabbed layout ([18e167c](18e167cb7f))
* show all found words and allow jump to them (beta) ([1a4c91f](1a4c91fd5e))
2025-11-20 15:48:45 +03:00
1a4c91fd5e feat: show all found words and allow jump to them (beta) 2025-11-20 15:48:28 +03:00
1c58357418 chore(i18n): rephrased hint 2025-11-20 14:25:36 +03:00
521b3295e6 nit: show spinner while user settings are loaded 2025-11-20 14:13:49 +03:00
348d64f356 fix: lighten debounce() usage, do not do full re-render on every change 2025-11-20 14:09:43 +03:00
dfdc1742ec nit: add logo to header 2025-11-20 13:51:06 +03:00
18e167cb7f feat: new tabbed layout 2025-11-20 13:45:42 +03:00
0990543aa9 chore(i18n): update missing translations 2025-11-19 16:15:21 +03:00
4f32be0b93 fix: made placeholder for textarea, added outline offset 2025-11-19 16:12:55 +03:00
1ba701737e chore: add license 2025-11-19 10:02:59 +03:00
semantic-release-bot
d275a6fd0d chore(release): 1.9.3
## [1.9.3](https://github.com/obsqrbtz/goose-highlighter/compare/v1.9.2...v1.9.3) (2025-11-18)

### Bug Fixes

* use CSS Custom Highlight API to avoid dom modifications (fixes [#1](https://github.com/obsqrbtz/goose-highlighter/issues/1)) ([3f2bb60](3f2bb6080b))
2025-11-19 01:26:16 +03:00
3f2bb6080b fix: use CSS Custom Highlight API to avoid dom modifications (fixes #1) 2025-11-19 01:25:55 +03:00
3da28a2ad7 chore: update deps 2025-11-18 20:58:59 +03:00
semantic-release-bot
4f575d9534 chore(release): 1.9.2
## [1.9.2](https://github.com/obsqrbtz/goose-highlighter/compare/v1.9.1...v1.9.2) (2025-11-14)

### Bug Fixes

* **highlight:** prevent creating extra <span>'s ([#1](https://github.com/obsqrbtz/goose-highlighter/issues/1)) ([affddd3](affddd3dbc))
2025-11-14 13:40:56 +03:00
affddd3dbc fix(highlight): prevent creating extra <span>'s (#1) 2025-11-14 13:40:43 +03:00
semantic-release-bot
c8334f9e68 chore(release): 1.9.1
## [1.9.1](https://github.com/obsqrbtz/goose-highlighter/compare/v1.9.0...v1.9.1) (2025-11-05)

### Bug Fixes

* remove halowen styling ([172aa75](172aa7583b))
2025-11-05 14:59:42 +03:00
172aa7583b fix: remove halowen styling 2025-11-05 14:59:27 +03:00
semantic-release-bot
6d7d9ac151 chore(release): 1.9.0
# [1.9.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.5...v1.9.0) (2025-10-31)

### Features

* haloween styling ([5ef380e](5ef380e544))
2025-10-31 11:17:28 +03:00
5ef380e544 feat: haloween styling 2025-10-31 11:17:08 +03:00
semantic-release-bot
c634f6bc8b chore(release): 1.8.5
## [1.8.5](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.4...v1.8.5) (2025-10-29)

### Bug Fixes

* highlight colors when multiple list have different configurations ([67577c8](67577c89cf))
2025-10-29 12:30:37 +03:00
67577c89cf fix: highlight colors when multiple list have different configurations 2025-10-29 12:29:55 +03:00
semantic-release-bot
326e585021 chore(release): 1.8.4
## [1.8.4](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.3...v1.8.4) (2025-10-28)

### Bug Fixes

* do not re-highlight when already processing highlights ([8be53f3](8be53f3240))
2025-10-28 15:32:14 +03:00
8be53f3240 fix: do not re-highlight when already processing highlights 2025-10-28 15:31:46 +03:00
f07617fa55 Merge branch 'main' of https://github.com/obsqrbtz/goose-highlighter 2025-10-09 16:18:53 +03:00
e79874922a added logo 2025-10-09 16:18:36 +03:00
71216cbcd9 ci: update manifest in publish workflow 2025-10-08 17:54:06 +03:00
f292bd7149 nit: added footer with version and github. 2025-10-08 16:32:04 +03:00
584ced252f ci: publish on version tag push 2025-10-08 16:26:32 +03:00
semantic-release-bot
ff5752da84 chore(release): 1.8.3
## [1.8.3](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.2...v1.8.3) (2025-10-08)

### Bug Fixes

* stop observing when highlightting is disabled ([d7c8dbb](d7c8dbb5f0))
2025-10-08 16:11:52 +03:00
d7c8dbb5f0 fix: stop observing when highlightting is disabled 2025-10-08 16:11:25 +03:00
33 changed files with 2993 additions and 2030 deletions

View File

@@ -2,7 +2,8 @@ name: Publish Chrome Extension
on: on:
push: push:
branches: [ main ] tags:
- 'v*'
jobs: jobs:
publish: publish:
@@ -32,6 +33,9 @@ jobs:
id: version id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Update manifest version
run: node scripts/update-manifest-version.js ${{ steps.version.outputs.VERSION }}
- name: Create zip package - name: Create zip package
run: | run: |
zip -r goose-highlighter.zip . -x '*.git*' 'node_modules/*' 'src/*' 'scripts/*' 'versioning.md' '.releaserc.json' 'package.json' 'package-lock.json' 'README.md' 'tsconfig.json' 'eslint.config.mjs' zip -r goose-highlighter.zip . -x '*.git*' 'node_modules/*' 'src/*' 'scripts/*' 'versioning.md' '.releaserc.json' 'package.json' 'package-lock.json' 'README.md' 'tsconfig.json' 'eslint.config.mjs'

View File

@@ -1,3 +1,2 @@
{ {
"kiroAgent.configureMCP": "Enabled"
} }

View File

@@ -1,3 +1,66 @@
# [1.10.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.9.3...v1.10.0) (2025-11-20)
### Bug Fixes
* lighten debounce() usage, do not do full re-render on every change ([348d64f](https://github.com/obsqrbtz/goose-highlighter/commit/348d64f35693c11e7b14edcbe59b910195974950))
* made placeholder for textarea, added outline offset ([4f32be0](https://github.com/obsqrbtz/goose-highlighter/commit/4f32be0b93b5a39dcb034b4a15bbeca05add0a1f))
### Features
* new tabbed layout ([18e167c](https://github.com/obsqrbtz/goose-highlighter/commit/18e167cb7f2e758e09b201f7eff4cdbad080774e))
* show all found words and allow jump to them (beta) ([1a4c91f](https://github.com/obsqrbtz/goose-highlighter/commit/1a4c91fd5e35cc2227a580465ba9078200200623))
## [1.9.3](https://github.com/obsqrbtz/goose-highlighter/compare/v1.9.2...v1.9.3) (2025-11-18)
### Bug Fixes
* use CSS Custom Highlight API to avoid dom modifications (fixes [#1](https://github.com/obsqrbtz/goose-highlighter/issues/1)) ([3f2bb60](https://github.com/obsqrbtz/goose-highlighter/commit/3f2bb6080ba3a9ac0599ad6594f0d877c12bb62f))
## [1.9.2](https://github.com/obsqrbtz/goose-highlighter/compare/v1.9.1...v1.9.2) (2025-11-14)
### Bug Fixes
* **highlight:** prevent creating extra <span>'s ([#1](https://github.com/obsqrbtz/goose-highlighter/issues/1)) ([affddd3](https://github.com/obsqrbtz/goose-highlighter/commit/affddd3dbc7de30100ca134ec65f4dc090275ca5))
## [1.9.1](https://github.com/obsqrbtz/goose-highlighter/compare/v1.9.0...v1.9.1) (2025-11-05)
### Bug Fixes
* remove halowen styling ([172aa75](https://github.com/obsqrbtz/goose-highlighter/commit/172aa7583b325761af43c780db4ac61dc4bda99b))
# [1.9.0](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.5...v1.9.0) (2025-10-31)
### Features
* haloween styling ([5ef380e](https://github.com/obsqrbtz/goose-highlighter/commit/5ef380e54447f45f7360dd4b7b84456aae55bfee))
## [1.8.5](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.4...v1.8.5) (2025-10-29)
### Bug Fixes
* highlight colors when multiple list have different configurations ([67577c8](https://github.com/obsqrbtz/goose-highlighter/commit/67577c89cffca1ab6d40a8913e51b7c3c6f91c85))
## [1.8.4](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.3...v1.8.4) (2025-10-28)
### Bug Fixes
* do not re-highlight when already processing highlights ([8be53f3](https://github.com/obsqrbtz/goose-highlighter/commit/8be53f32402c2f0f228ca003ef3805c5ff0b6e88))
## [1.8.3](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.2...v1.8.3) (2025-10-08)
### Bug Fixes
* stop observing when highlightting is disabled ([d7c8dbb](https://github.com/obsqrbtz/goose-highlighter/commit/d7c8dbb5f0011afe83739841218aa737794074e3))
## [1.8.2](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.1...v1.8.2) (2025-10-08) ## [1.8.2](https://github.com/obsqrbtz/goose-highlighter/compare/v1.8.1...v1.8.2) (2025-10-08)

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Daniel Dada
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,4 +1,4 @@
# Goose Highlighter # <img src="img/logo.png" alt="Goose Highlighter Logo" width="32" style="vertical-align: middle;"> Goose Highlighter
Goose Highlighter is a browser extension that allows you to highlight words on any webpage. Goose Highlighter is a browser extension that allows you to highlight words on any webpage.
@@ -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.

View File

@@ -26,8 +26,11 @@
"enable_highlight": { "enable_highlight": {
"message": "Hervorhebung aktivieren" "message": "Hervorhebung aktivieren"
}, },
"apply": {
"message": "Anwenden"
},
"paste_hint": { "paste_hint": {
"message": "Wörter hier einfügen" "message": "Fügen Sie hier Wörter oder Phrasen ein. Jedes neue Wort/jede neue Phrase sollte in der nächsten Zeile beginnen."
}, },
"apply_paste": { "apply_paste": {
"message": "Wörter hinzufügen" "message": "Wörter hinzufügen"
@@ -102,7 +105,7 @@
"message": "Zu Ausnahmen hinzufügen" "message": "Zu Ausnahmen hinzufügen"
}, },
"remove_exception": { "remove_exception": {
"message": "Aus Ausnahmen entfernen" "message": "Aktuelle entfernen"
}, },
"manage_exceptions": { "manage_exceptions": {
"message": "Verwalten" "message": "Verwalten"
@@ -118,5 +121,56 @@
}, },
"remove": { "remove": {
"message": "Entfernen" "message": "Entfernen"
},
"tab_lists": {
"message": "Listen"
},
"tab_words": {
"message": "Wörter"
},
"tab_exceptions": {
"message": "Ausnahmen"
},
"no_exceptions": {
"message": "Keine Ausnahmen"
},
"toggle_highlighting_title": {
"message": "Hervorhebung umschalten"
},
"toggle_dark_mode_title": {
"message": "Dunkelmodus umschalten"
},
"select_title": {
"message": "Auswählen"
},
"word_placeholder": {
"message": "Wort oder Phrase"
},
"background_color_title": {
"message": "Hintergrundfarbe"
},
"text_color_title": {
"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"
} }
} }

View File

@@ -26,8 +26,11 @@
"enable_highlight": { "enable_highlight": {
"message": "Enable Highlight" "message": "Enable Highlight"
}, },
"apply": {
"message": "Apply"
},
"paste_hint": { "paste_hint": {
"message": "Paste words here" "message": "Paste words or phrases here. Each new word/phrase should start from next line."
}, },
"apply_paste": { "apply_paste": {
"message": "Add Words" "message": "Add Words"
@@ -108,7 +111,7 @@
"message": "Add to Exceptions" "message": "Add to Exceptions"
}, },
"remove_exception": { "remove_exception": {
"message": "Remove from Exceptions" "message": "Remove current"
}, },
"manage_exceptions": { "manage_exceptions": {
"message": "Manage" "message": "Manage"
@@ -124,5 +127,56 @@
}, },
"remove": { "remove": {
"message": "Remove" "message": "Remove"
},
"tab_lists": {
"message": "Lists"
},
"tab_words": {
"message": "Words"
},
"tab_exceptions": {
"message": "Exceptions"
},
"no_exceptions": {
"message": "No exceptions"
},
"toggle_highlighting_title": {
"message": "Toggle highlighting"
},
"toggle_dark_mode_title": {
"message": "Toggle dark mode"
},
"select_title": {
"message": "Select"
},
"word_placeholder": {
"message": "Word or phrase"
},
"background_color_title": {
"message": "Background color"
},
"text_color_title": {
"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"
} }
} }

View File

@@ -26,8 +26,11 @@
"enable_highlight": { "enable_highlight": {
"message": "Activar resaltado" "message": "Activar resaltado"
}, },
"apply": {
"message": "Aplicar"
},
"paste_hint": { "paste_hint": {
"message": "Pega las palabras aquí" "message": "Pegue palabras o frases aquí. Cada nueva palabra/frase debe comenzar en la siguiente línea."
}, },
"apply_paste": { "apply_paste": {
"message": "Agregar palabras" "message": "Agregar palabras"
@@ -108,7 +111,7 @@
"message": "Agregar a excepciones" "message": "Agregar a excepciones"
}, },
"remove_exception": { "remove_exception": {
"message": "Quitar de excepciones" "message": "Eliminar actual"
}, },
"manage_exceptions": { "manage_exceptions": {
"message": "Gestionar" "message": "Gestionar"
@@ -124,5 +127,56 @@
}, },
"remove": { "remove": {
"message": "Eliminar" "message": "Eliminar"
},
"tab_lists": {
"message": "Listas"
},
"tab_words": {
"message": "Palabras"
},
"tab_exceptions": {
"message": "Excepciones"
},
"no_exceptions": {
"message": "Sin excepciones"
},
"toggle_highlighting_title": {
"message": "Alternar resaltado"
},
"toggle_dark_mode_title": {
"message": "Alternar modo oscuro"
},
"select_title": {
"message": "Seleccionar"
},
"word_placeholder": {
"message": "Palabra o frase"
},
"background_color_title": {
"message": "Color de fondo"
},
"text_color_title": {
"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"
} }
} }

View File

@@ -26,8 +26,11 @@
"enable_highlight": { "enable_highlight": {
"message": "Activer la surbrillance" "message": "Activer la surbrillance"
}, },
"apply": {
"message": "Appliquer"
},
"paste_hint": { "paste_hint": {
"message": "Collez les mots ici" "message": "Collez des mots ou des phrases ici. Chaque nouveau mot/phrase doit commencer sur la ligne suivante."
}, },
"apply_paste": { "apply_paste": {
"message": "Ajouter des mots" "message": "Ajouter des mots"
@@ -108,7 +111,7 @@
"message": "Ajouter aux exceptions" "message": "Ajouter aux exceptions"
}, },
"remove_exception": { "remove_exception": {
"message": "Retirer des exceptions" "message": "Supprimer l'actuel"
}, },
"manage_exceptions": { "manage_exceptions": {
"message": "Gérer" "message": "Gérer"
@@ -124,5 +127,56 @@
}, },
"remove": { "remove": {
"message": "Supprimer" "message": "Supprimer"
},
"tab_lists": {
"message": "Listes"
},
"tab_words": {
"message": "Mots"
},
"tab_exceptions": {
"message": "Exceptions"
},
"no_exceptions": {
"message": "Aucune exception"
},
"toggle_highlighting_title": {
"message": "Activer/désactiver la surbrillance"
},
"toggle_dark_mode_title": {
"message": "Activer/désactiver le mode sombre"
},
"select_title": {
"message": "Sélectionner"
},
"word_placeholder": {
"message": "Mot ou phrase"
},
"background_color_title": {
"message": "Couleur d'arrière-plan"
},
"text_color_title": {
"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"
} }
} }

View File

@@ -26,8 +26,11 @@
"enable_highlight": { "enable_highlight": {
"message": "हाइलाइट सक्षम करें" "message": "हाइलाइट सक्षम करें"
}, },
"apply": {
"message": "लागू करें"
},
"paste_hint": { "paste_hint": {
"message": "यहाँ शब्द चिपकाएँ" "message": "यहाँ शब्द या वाक्यांश चिपकाएँ। प्रत्येक नया शब्द/वाक्यांश अगली पंक्ति से शुरू होना चाहिए।"
}, },
"apply_paste": { "apply_paste": {
"message": "शब्द जोड़ें" "message": "शब्द जोड़ें"
@@ -108,7 +111,7 @@
"message": "अपवादों में जोड़ें" "message": "अपवादों में जोड़ें"
}, },
"remove_exception": { "remove_exception": {
"message": "अपवादों से हटाएं" "message": "वर्तमान हटाएं"
}, },
"manage_exceptions": { "manage_exceptions": {
"message": "प्रबंधित करें" "message": "प्रबंधित करें"
@@ -124,5 +127,56 @@
}, },
"remove": { "remove": {
"message": "हटाएं" "message": "हटाएं"
},
"tab_lists": {
"message": "सूचियाँ"
},
"tab_words": {
"message": "शब्द"
},
"tab_exceptions": {
"message": "अपवाद"
},
"no_exceptions": {
"message": "कोई अपवाद नहीं"
},
"toggle_highlighting_title": {
"message": "हाइलाइटिंग टॉगल करें"
},
"toggle_dark_mode_title": {
"message": "डार्क मोड टॉगल करें"
},
"select_title": {
"message": "चुनें"
},
"word_placeholder": {
"message": "शब्द या वाक्यांश"
},
"background_color_title": {
"message": "पृष्ठभूमि रंग"
},
"text_color_title": {
"message": "पाठ रंग"
},
"tab_page_highlights": {
"message": "पृष्ठ पर"
},
"highlights_on_page": {
"message": "इस पृष्ठ पर हाइलाइट"
},
"total_highlights": {
"message": "कुल"
},
"refresh": {
"message": "रीफ्रेश करें"
},
"no_highlights_on_page": {
"message": "इस पृष्ठ पर कोई हाइलाइट नहीं मिला"
},
"previous": {
"message": "पिछला"
},
"next": {
"message": "अगला"
} }
} }

View File

@@ -26,8 +26,11 @@
"enable_highlight": { "enable_highlight": {
"message": "Abilita evidenziazione" "message": "Abilita evidenziazione"
}, },
"apply": {
"message": "Applica"
},
"paste_hint": { "paste_hint": {
"message": "Incolla le parole qui" "message": "Incolla parole o frasi qui. Ogni nuova parola/frase deve iniziare dalla riga successiva."
}, },
"apply_paste": { "apply_paste": {
"message": "Aggiungi parole" "message": "Aggiungi parole"
@@ -108,7 +111,7 @@
"message": "Aggiungi alle eccezioni" "message": "Aggiungi alle eccezioni"
}, },
"remove_exception": { "remove_exception": {
"message": "Rimuovi dalle eccezioni" "message": "Rimuovi corrente"
}, },
"manage_exceptions": { "manage_exceptions": {
"message": "Gestisci" "message": "Gestisci"
@@ -124,5 +127,56 @@
}, },
"remove": { "remove": {
"message": "Rimuovi" "message": "Rimuovi"
},
"tab_lists": {
"message": "Elenchi"
},
"tab_words": {
"message": "Parole"
},
"tab_exceptions": {
"message": "Eccezioni"
},
"no_exceptions": {
"message": "Nessuna eccezione"
},
"toggle_highlighting_title": {
"message": "Attiva/disattiva evidenziazione"
},
"toggle_dark_mode_title": {
"message": "Attiva/disattiva modalità scura"
},
"select_title": {
"message": "Seleziona"
},
"word_placeholder": {
"message": "Parola o frase"
},
"background_color_title": {
"message": "Colore di sfondo"
},
"text_color_title": {
"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"
} }
} }

View File

@@ -26,8 +26,11 @@
"enable_highlight": { "enable_highlight": {
"message": "ハイライトを有効にする" "message": "ハイライトを有効にする"
}, },
"apply": {
"message": "適用"
},
"paste_hint": { "paste_hint": {
"message": "ここに単語を貼り付けてください" "message": "ここに単語またはフレーズを貼り付けてください。各新しい単語/フレーズは次の行から始める必要があります。"
}, },
"apply_paste": { "apply_paste": {
"message": "単語を追加" "message": "単語を追加"
@@ -108,7 +111,7 @@
"message": "例外に追加" "message": "例外に追加"
}, },
"remove_exception": { "remove_exception": {
"message": "例外から削除" "message": "現在を削除"
}, },
"manage_exceptions": { "manage_exceptions": {
"message": "管理" "message": "管理"
@@ -124,5 +127,56 @@
}, },
"remove": { "remove": {
"message": "削除" "message": "削除"
},
"tab_lists": {
"message": "リスト"
},
"tab_words": {
"message": "単語"
},
"tab_exceptions": {
"message": "例外"
},
"no_exceptions": {
"message": "例外なし"
},
"toggle_highlighting_title": {
"message": "ハイライトの切り替え"
},
"toggle_dark_mode_title": {
"message": "ダークモードの切り替え"
},
"select_title": {
"message": "選択"
},
"word_placeholder": {
"message": "単語またはフレーズ"
},
"background_color_title": {
"message": "背景色"
},
"text_color_title": {
"message": "文字色"
},
"tab_page_highlights": {
"message": "ページ上"
},
"highlights_on_page": {
"message": "このページのハイライト"
},
"total_highlights": {
"message": "合計"
},
"refresh": {
"message": "更新"
},
"no_highlights_on_page": {
"message": "このページにハイライトが見つかりません"
},
"previous": {
"message": "前へ"
},
"next": {
"message": "次へ"
} }
} }

View File

@@ -26,8 +26,11 @@
"enable_highlight": { "enable_highlight": {
"message": "하이라이트 활성화" "message": "하이라이트 활성화"
}, },
"apply": {
"message": "적용"
},
"paste_hint": { "paste_hint": {
"message": "여기에 단어를 붙여넣기" "message": "여기에 단어나 구문을 붙여넣으세요. 각 새 단어/구문은 다음 줄에서 시작해야 합니다."
}, },
"apply_paste": { "apply_paste": {
"message": "단어 추가" "message": "단어 추가"
@@ -108,7 +111,7 @@
"message": "예외에 추가" "message": "예외에 추가"
}, },
"remove_exception": { "remove_exception": {
"message": "예외에서 제거" "message": "현재 제거"
}, },
"manage_exceptions": { "manage_exceptions": {
"message": "관리" "message": "관리"
@@ -124,5 +127,56 @@
}, },
"remove": { "remove": {
"message": "제거" "message": "제거"
},
"tab_lists": {
"message": "리스트"
},
"tab_words": {
"message": "단어"
},
"tab_exceptions": {
"message": "예외"
},
"no_exceptions": {
"message": "예외 없음"
},
"toggle_highlighting_title": {
"message": "하이라이트 전환"
},
"toggle_dark_mode_title": {
"message": "다크 모드 전환"
},
"select_title": {
"message": "선택"
},
"word_placeholder": {
"message": "단어 또는 구문"
},
"background_color_title": {
"message": "배경색"
},
"text_color_title": {
"message": "글자색"
},
"tab_page_highlights": {
"message": "페이지에서"
},
"highlights_on_page": {
"message": "이 페이지의 하이라이트"
},
"total_highlights": {
"message": "전체"
},
"refresh": {
"message": "새로고침"
},
"no_highlights_on_page": {
"message": "이 페이지에서 하이라이트를 찾을 수 없습니다"
},
"previous": {
"message": "이전"
},
"next": {
"message": "다음"
} }
} }

View File

@@ -26,8 +26,11 @@
"enable_highlight": { "enable_highlight": {
"message": "Markeren inschakelen" "message": "Markeren inschakelen"
}, },
"apply": {
"message": "Toepassen"
},
"paste_hint": { "paste_hint": {
"message": "Plak hier de woorden" "message": "Plak hier woorden of zinnen. Elk nieuw woord/zin moet op de volgende regel beginnen."
}, },
"apply_paste": { "apply_paste": {
"message": "Woorden toevoegen" "message": "Woorden toevoegen"
@@ -108,7 +111,7 @@
"message": "Toevoegen aan uitzonderingen" "message": "Toevoegen aan uitzonderingen"
}, },
"remove_exception": { "remove_exception": {
"message": "Verwijderen uit uitzonderingen" "message": "Huidige verwijderen"
}, },
"manage_exceptions": { "manage_exceptions": {
"message": "Beheren" "message": "Beheren"
@@ -124,5 +127,56 @@
}, },
"remove": { "remove": {
"message": "Verwijderen" "message": "Verwijderen"
},
"tab_lists": {
"message": "Lijsten"
},
"tab_words": {
"message": "Woorden"
},
"tab_exceptions": {
"message": "Uitzonderingen"
},
"no_exceptions": {
"message": "Geen uitzonderingen"
},
"toggle_highlighting_title": {
"message": "Markeren in-/uitschakelen"
},
"toggle_dark_mode_title": {
"message": "Donkere modus in-/uitschakelen"
},
"select_title": {
"message": "Selecteren"
},
"word_placeholder": {
"message": "Woord of zin"
},
"background_color_title": {
"message": "Achtergrondkleur"
},
"text_color_title": {
"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"
} }
} }

View File

@@ -26,8 +26,11 @@
"enable_highlight": { "enable_highlight": {
"message": "Włącz podświetlanie" "message": "Włącz podświetlanie"
}, },
"apply": {
"message": "Zastosuj"
},
"paste_hint": { "paste_hint": {
"message": "Wklej tutaj słowa" "message": "Wklej tutaj słowa lub frazy. Każde nowe słowo/fraza powinno zaczynać się od następnej linii."
}, },
"apply_paste": { "apply_paste": {
"message": "Dodaj słowa" "message": "Dodaj słowa"
@@ -108,7 +111,7 @@
"message": "Dodaj do wyjątków" "message": "Dodaj do wyjątków"
}, },
"remove_exception": { "remove_exception": {
"message": "Usuń z wyjątków" "message": "Usuń bieżący"
}, },
"manage_exceptions": { "manage_exceptions": {
"message": "Zarządzaj" "message": "Zarządzaj"
@@ -124,5 +127,56 @@
}, },
"remove": { "remove": {
"message": "Usuń" "message": "Usuń"
},
"tab_lists": {
"message": "Listy"
},
"tab_words": {
"message": "Słowa"
},
"tab_exceptions": {
"message": "Wyjątki"
},
"no_exceptions": {
"message": "Brak wyjątków"
},
"toggle_highlighting_title": {
"message": "Przełącz podświetlanie"
},
"toggle_dark_mode_title": {
"message": "Przełącz tryb ciemny"
},
"select_title": {
"message": "Zaznacz"
},
"word_placeholder": {
"message": "Słowo lub fraza"
},
"background_color_title": {
"message": "Kolor tła"
},
"text_color_title": {
"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"
} }
} }

View File

@@ -26,8 +26,11 @@
"enable_highlight": { "enable_highlight": {
"message": "Ativar destaque" "message": "Ativar destaque"
}, },
"apply": {
"message": "Aplicar"
},
"paste_hint": { "paste_hint": {
"message": "Cole as palavras aqui" "message": "Cole palavras ou frases aqui. Cada nova palavra/frase deve começar na próxima linha."
}, },
"apply_paste": { "apply_paste": {
"message": "Adicionar palavras" "message": "Adicionar palavras"
@@ -102,7 +105,7 @@
"message": "Adicionar às exceções" "message": "Adicionar às exceções"
}, },
"remove_exception": { "remove_exception": {
"message": "Remover das exceções" "message": "Remover atual"
}, },
"manage_exceptions": { "manage_exceptions": {
"message": "Gerenciar" "message": "Gerenciar"
@@ -118,5 +121,56 @@
}, },
"remove": { "remove": {
"message": "Remover" "message": "Remover"
},
"tab_lists": {
"message": "Listas"
},
"tab_words": {
"message": "Palavras"
},
"tab_exceptions": {
"message": "Exceções"
},
"no_exceptions": {
"message": "Sem exceções"
},
"toggle_highlighting_title": {
"message": "Alternar destaque"
},
"toggle_dark_mode_title": {
"message": "Alternar modo escuro"
},
"select_title": {
"message": "Selecionar"
},
"word_placeholder": {
"message": "Palavra ou frase"
},
"background_color_title": {
"message": "Cor de fundo"
},
"text_color_title": {
"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"
} }
} }

View File

@@ -26,8 +26,11 @@
"enable_highlight": { "enable_highlight": {
"message": "Включить выделение" "message": "Включить выделение"
}, },
"apply": {
"message": "Применить"
},
"paste_hint": { "paste_hint": {
"message": "Вставьте список слов здесь" "message": "Вставьте сюда слова или фразы. Каждое новое слово/фраза должны начинаться с новой строки."
}, },
"apply_paste": { "apply_paste": {
"message": "Добавить слова" "message": "Добавить слова"
@@ -124,5 +127,56 @@
}, },
"remove": { "remove": {
"message": "Удалить" "message": "Удалить"
},
"tab_lists": {
"message": "Списки"
},
"tab_words": {
"message": "Слова"
},
"tab_exceptions": {
"message": "Исключения"
},
"no_exceptions": {
"message": "Нет исключений"
},
"toggle_highlighting_title": {
"message": "Выделение(вкл/выкл)"
},
"toggle_dark_mode_title": {
"message": "Темная/светлая тема"
},
"select_title": {
"message": "Выбрать"
},
"word_placeholder": {
"message": "Слово или фраза"
},
"background_color_title": {
"message": "Цвет фона"
},
"text_color_title": {
"message": "Цвет текста"
},
"tab_page_highlights": {
"message": "На странице"
},
"highlights_on_page": {
"message": "Найдено на этой странице"
},
"total_highlights": {
"message": "Всего"
},
"refresh": {
"message": "Обновить"
},
"no_highlights_on_page": {
"message": "На этой странице не найдены слова из списка"
},
"previous": {
"message": "Предыдущее"
},
"next": {
"message": "Следующее"
} }
} }

View File

@@ -26,8 +26,11 @@
"enable_highlight": { "enable_highlight": {
"message": "Vurgulamayı Etkinleştir" "message": "Vurgulamayı Etkinleştir"
}, },
"apply": {
"message": "Uygula"
},
"paste_hint": { "paste_hint": {
"message": "Kelimeleri buraya yapıştırın" "message": "Kelimeleri veya ifadeleri buraya yapıştırın. Her yeni kelime/ifade bir sonraki satırdan başlamalıdır."
}, },
"apply_paste": { "apply_paste": {
"message": "Kelimeleri Ekle" "message": "Kelimeleri Ekle"
@@ -108,7 +111,7 @@
"message": "İstisnalara Ekle" "message": "İstisnalara Ekle"
}, },
"remove_exception": { "remove_exception": {
"message": "İstisnalardan Çıkar" "message": "Mevcut olanı kaldır"
}, },
"manage_exceptions": { "manage_exceptions": {
"message": "Yönet" "message": "Yönet"
@@ -124,5 +127,56 @@
}, },
"remove": { "remove": {
"message": "Kaldır" "message": "Kaldır"
},
"tab_lists": {
"message": "Listeler"
},
"tab_words": {
"message": "Kelimeler"
},
"tab_exceptions": {
"message": "İstisnalar"
},
"no_exceptions": {
"message": "İstisna yok"
},
"toggle_highlighting_title": {
"message": "Vurgulamayı aç/kapat"
},
"toggle_dark_mode_title": {
"message": "Karanlık modu aç/kapat"
},
"select_title": {
"message": "Seç"
},
"word_placeholder": {
"message": "Kelime veya ifade"
},
"background_color_title": {
"message": "Arka plan rengi"
},
"text_color_title": {
"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"
} }
} }

View File

@@ -26,8 +26,11 @@
"enable_highlight": { "enable_highlight": {
"message": "启用高亮" "message": "启用高亮"
}, },
"apply": {
"message": "应用"
},
"paste_hint": { "paste_hint": {
"message": "在此粘贴单词" "message": "在此粘贴单词或短语。每个新单词/短语应从下一行开始。"
}, },
"apply_paste": { "apply_paste": {
"message": "添加单词" "message": "添加单词"
@@ -108,7 +111,7 @@
"message": "添加到例外" "message": "添加到例外"
}, },
"remove_exception": { "remove_exception": {
"message": "从例外中移除" "message": "移除当前"
}, },
"manage_exceptions": { "manage_exceptions": {
"message": "管理" "message": "管理"
@@ -124,5 +127,56 @@
}, },
"remove": { "remove": {
"message": "移除" "message": "移除"
},
"tab_lists": {
"message": "列表"
},
"tab_words": {
"message": "单词"
},
"tab_exceptions": {
"message": "例外"
},
"no_exceptions": {
"message": "无例外"
},
"toggle_highlighting_title": {
"message": "切换高亮"
},
"toggle_dark_mode_title": {
"message": "切换暗黑模式"
},
"select_title": {
"message": "选择"
},
"word_placeholder": {
"message": "单词或短语"
},
"background_color_title": {
"message": "背景颜色"
},
"text_color_title": {
"message": "文字颜色"
},
"tab_page_highlights": {
"message": "页面上"
},
"highlights_on_page": {
"message": "此页面上的高亮"
},
"total_highlights": {
"message": "总计"
},
"refresh": {
"message": "刷新"
},
"no_highlights_on_page": {
"message": "此页面上未找到高亮"
},
"previous": {
"message": "上一个"
},
"next": {
"message": "下一个"
} }
} }

BIN
img/logo-outlined.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

BIN
img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "__MSG_extension_name__", "name": "__MSG_extension_name__",
"description": "__MSG_extension_description__", "description": "__MSG_extension_description__",
"version": "1.8.2", "version": "1.10.0",
"default_locale": "en", "default_locale": "en",
"permissions": [ "permissions": [
"scripting", "scripting",

2228
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@
"@types/chrome": "^0.0.270", "@types/chrome": "^0.0.270",
"eslint": "^9.30.0", "eslint": "^9.30.0",
"globals": "^16.2.0", "globals": "^16.2.0",
"semantic-release": "^24.2.5", "semantic-release": "^24.2.9",
"typescript": "^5.6.0" "typescript": "^5.6.0"
}, },
"scripts": { "scripts": {

File diff suppressed because it is too large Load Diff

View File

@@ -11,52 +11,37 @@
</head> </head>
<body class="dark"> <body class="dark">
<div class="loading-overlay">
<div class="loading-spinner"></div>
</div>
<div class="container"> <div class="container">
<div class="header-bar"> <div class="header-bar">
<span class="title"> <span class="title">
<i class="fa-solid fa-highlighter"></i> Goose Highlighter <img src="../img/logo-outlined.png" alt="Goose Highlighter" style="height: 28px; vertical-align: middle; margin-right: 8px;"> Goose Highlighter
</span> </span>
<div class="icon-toggles"> <div class="icon-toggles">
<label class="icon-toggle" title="Toggle highlighting"> <label class="icon-toggle" data-i18n-title="toggle_highlighting_title" title="Toggle highlighting">
<input type="checkbox" class="hidden-toggle" id="globalHighlightToggle" /> <input type="checkbox" class="hidden-toggle" id="globalHighlightToggle" />
<i class="toggle-icon global-icon fa-solid"></i> <i class="toggle-icon global-icon fa-solid"></i>
</label> </label>
<label class="icon-toggle" title="Toggle dark mode"> <label class="icon-toggle" data-i18n-title="toggle_dark_mode_title" title="Toggle dark mode">
<input type="checkbox" class="hidden-toggle" id="themeToggle" /> <input type="checkbox" class="hidden-toggle" id="themeToggle" />
<i class="toggle-icon theme-icon fa-solid"></i> <i class="toggle-icon theme-icon fa-solid"></i>
</label> </label>
</div> </div>
</div> </div>
<div class="section" data-section="exceptions"> <div class="tabs">
<div class="section-header"> <button class="tab-button active" data-tab="lists"><i class="fa-solid fa-list"></i> <span data-i18n="tab_lists">Lists</span></button>
<h2><i class="fa-solid fa-ban"></i> <span data-i18n="site_exceptions">Site Exceptions</span></h2> <button class="tab-button" data-tab="words"><i class="fa-solid fa-tags"></i> <span data-i18n="tab_words">Words</span></button>
<button class="collapse-toggle" data-target="exceptions"> <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>
<i class="fa-solid fa-chevron-up"></i> <button class="tab-button" data-tab="exceptions"><i class="fa-solid fa-ban"></i> <span data-i18n="tab_exceptions">Exceptions</span></button>
</button> <button class="tab-button" data-tab="options"><i class="fa-solid fa-sliders"></i> <span data-i18n="options">Options</span></button>
</div>
<div class="section-content" id="exceptions-content">
<div class="button-row">
<button id="toggleExceptionBtn"><i class="fa-solid fa-ban"></i> <span id="exceptionBtnText" data-i18n="add_exception">Add to Exceptions</span></button>
<button id="manageExceptionsBtn"><i class="fa-solid fa-list"></i> <span data-i18n="manage_exceptions">Manage</span></button>
</div>
<div id="exceptionsPanel" class="exceptions-panel" style="display: none;">
<h3 data-i18n="exceptions_list">Exception Sites:</h3>
<div id="exceptionsList" class="exceptions-list"></div>
<button id="clearExceptionsBtn" class="danger"><i class="fa-solid fa-trash"></i> <span data-i18n="clear_all">Clear All</span></button>
</div>
</div>
</div> </div>
<div class="tab-content active" data-tab-content="lists">
<div class="section" data-section="lists"> <div class="section" data-section="lists">
<div class="section-header">
<h2><i class="fa-solid fa-list"></i> <span data-i18n="highlight_lists">Highlight Lists</span></h2>
<button class="collapse-toggle" data-target="lists">
<i class="fa-solid fa-chevron-up"></i>
</button>
</div>
<div class="section-content" id="lists-content">
<label for="listSelect" data-i18n="select_list">Select List:</label> <label for="listSelect" data-i18n="select_list">Select List:</label>
<select id="listSelect"></select> <select id="listSelect"></select>
<div class="button-row"> <div class="button-row">
@@ -65,16 +50,9 @@
data-i18n="delete_list">Delete</span></button> data-i18n="delete_list">Delete</span></button>
</div> </div>
</div> </div>
</div>
<div class="section" data-section="settings"> <div class="section" data-section="settings">
<div class="section-header"> <h3><i class="fa-solid fa-gear"></i> <span data-i18n="list_settings">List Settings</span></h3>
<h2><i class="fa-solid fa-gear"></i> <span data-i18n="list_settings">List Settings</span></h2>
<button class="collapse-toggle" data-target="settings">
<i class="fa-solid fa-chevron-up"></i>
</button>
</div>
<div class="section-content" id="settings-content">
<label><span data-i18n="list_name">List Name:</span> <input type="text" id="listName" /></label> <label><span data-i18n="list_name">List Name:</span> <input type="text" id="listName" /></label>
<div class="color-row"> <div class="color-row">
<div class="color-label"> <div class="color-label">
@@ -94,28 +72,15 @@
</div> </div>
</div> </div>
<div class="tab-content" data-tab-content="words">
<div class="section" data-section="addwords"> <div class="section" data-section="addwords">
<div class="section-header"> <h3><i class="fa-solid fa-pen"></i> <span data-i18n="add_words">Add Words</span></h3>
<h2><i class="fa-solid fa-pen"></i> <span data-i18n="add_words">Add Words</span></h2>
<button class="collapse-toggle" data-target="addwords">
<i class="fa-solid fa-chevron-up"></i>
</button>
</div>
<div class="section-content" id="addwords-content">
<textarea id="bulkPaste" data-i18n="paste_hint" placeholder="Paste words here..."></textarea> <textarea id="bulkPaste" data-i18n="paste_hint" placeholder="Paste words here..."></textarea>
<button id="addWordsBtn"><span data-i18n="apply_paste">Add Words</span></button> <button id="addWordsBtn"><span data-i18n="apply_paste">Add Words</span></button>
</div> </div>
</div>
<div class="section" data-section="wordlist"> <div class="section" data-section="wordlist">
<div class="section-header"> <h3><i class="fa-solid fa-tags"></i> <span data-i18n="word_list">Word List</span> (<span id="wordCount">0</span>)</h3>
<h2><i class="fa-solid fa-tags"></i> <span data-i18n="word_list">Word List</span>(<span id="wordCount">0</span>)
</h2>
<button class="collapse-toggle" data-target="wordlist">
<i class="fa-solid fa-chevron-up"></i>
</button>
</div>
<div class="section-content" id="wordlist-content">
<div class="button-row wrap"> <div class="button-row wrap">
<button id="selectAllBtn"><span data-i18n="select_all">Select All</span></button> <button id="selectAllBtn"><span data-i18n="select_all">Select All</span></button>
<button id="deselectAllBtn"><span data-i18n="deselect_all">Clear</span></button> <button id="deselectAllBtn"><span data-i18n="deselect_all">Clear</span></button>
@@ -128,14 +93,32 @@
</div> </div>
</div> </div>
<div class="section" data-section="options"> <div class="tab-content" data-tab-content="page-highlights">
<div class="section-header"> <div class="section" data-section="page-highlights">
<h2><i class="fa-solid fa-sliders"></i> <span data-i18n="options">Options</span></h2> <h3><i class="fa-solid fa-location-dot"></i> <span data-i18n="highlights_on_page">Highlights on This Page</span></h3>
<button class="collapse-toggle" data-target="options"> <div class="page-highlights-info">
<i class="fa-solid fa-chevron-up"></i> <span data-i18n="total_highlights">Total:</span> <strong id="totalHighlightsCount">0</strong>
</button>
</div> </div>
<div class="section-content" id="options-content"> <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="section" data-section="exceptions">
<h3><i class="fa-solid fa-ban"></i> <span data-i18n="site_exceptions">Site Exceptions</span></h3>
<div class="button-row">
<button id="toggleExceptionBtn"><i class="fa-solid fa-plus"></i> <span id="exceptionBtnText" data-i18n="add_exception">Add to Exceptions</span></button>
</div>
<h3 data-i18n="exceptions_list">Exception Sites:</h3>
<div id="exceptionsList" class="exceptions-list"></div>
<button id="clearExceptionsBtn" class="danger"><i class="fa-solid fa-trash"></i> <span data-i18n="clear_all">Clear All</span></button>
</div>
</div>
<div class="tab-content" data-tab-content="options">
<div class="section" data-section="options">
<h3><i class="fa-solid fa-sliders"></i> <span data-i18n="options">Options</span></h3>
<div class="button-row" style="margin-bottom:8px;"> <div class="button-row" style="margin-bottom:8px;">
<label><input type="checkbox" id="matchCase" /> <span data-i18n="match_case">Match Case</span></label> <label><input type="checkbox" id="matchCase" /> <span data-i18n="match_case">Match Case</span></label>
<label><input type="checkbox" id="matchWhole" /> <span data-i18n="match_whole">Match Whole Word</span></label> <label><input type="checkbox" id="matchWhole" /> <span data-i18n="match_whole">Match Whole Word</span></label>
@@ -149,6 +132,16 @@
</div> </div>
</div> </div>
<footer class="footer">
<div class="footer-content">
<span class="version">v<span id="version-number">...</span></span>
<a href="https://github.com/obsqrbtz/goose-highlighter" target="_blank" class="github-link">
<i class="fa-brands fa-github"></i>
<span>GitHub</span>
</a>
</div>
</footer>
<script type="module" src="../dist/popup/popup.js"></script> <script type="module" src="../dist/popup/popup.js"></script>
</body> </body>

View File

@@ -2,7 +2,6 @@ import { HighlightList, MessageData } 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 { HighlightEngine } from './HighlightEngine.js'; import { HighlightEngine } from './HighlightEngine.js';
import { DOMUtils } from '../utils/DOMUtils.js';
export class ContentScript { export class ContentScript {
private lists: HighlightList[] = []; private lists: HighlightList[] = [];
@@ -12,6 +11,7 @@ export class ContentScript {
private matchCase = false; private matchCase = false;
private matchWhole = false; private matchWhole = false;
private highlightEngine: HighlightEngine; private highlightEngine: HighlightEngine;
private isProcessing = false;
constructor() { constructor() {
this.highlightEngine = new HighlightEngine(() => this.processHighlights()); this.highlightEngine = new HighlightEngine(() => this.processHighlights());
@@ -21,7 +21,6 @@ export class ContentScript {
private async initialize(): Promise<void> { private async initialize(): Promise<void> {
await this.loadSettings(); await this.loadSettings();
this.setupMessageListener(); this.setupMessageListener();
this.setupScrollHandler();
this.processHighlights(); this.processHighlights();
} }
@@ -48,28 +47,31 @@ 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;
}); });
} }
private setupScrollHandler(): void {
const debouncedProcess = DOMUtils.debounce(() => this.processHighlights(), 300);
window.addEventListener('scroll', debouncedProcess);
}
private async handleWordListUpdate(): Promise<void> { private async handleWordListUpdate(): Promise<void> {
const data = await StorageService.get(['lists']); const data = await StorageService.get(['lists']);
@@ -96,11 +98,42 @@ export class ContentScript {
} }
private processHighlights(): void { private processHighlights(): void {
if (this.isProcessing) return;
this.isProcessing = true;
try {
if (!this.isGlobalHighlightEnabled || this.isCurrentSiteException) { if (!this.isGlobalHighlightEnabled || this.isCurrentSiteException) {
this.highlightEngine.clearHighlights(); this.highlightEngine.clearHighlights();
this.highlightEngine.stopObserving();
return; return;
} }
this.highlightEngine.highlight(this.lists, this.matchCase, this.matchWhole); this.highlightEngine.highlight(this.lists, this.matchCase, this.matchWhole);
} finally {
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);
} }
} }

View File

@@ -1,13 +1,32 @@
import { HighlightList, ActiveWord } from '../types.js'; import { HighlightList, ActiveWord, CONSTANTS } from '../types.js';
import { DOMUtils } from '../utils/DOMUtils.js'; import { DOMUtils } from '../utils/DOMUtils.js';
export class HighlightEngine { export class HighlightEngine {
private styleSheet: CSSStyleSheet | null = null; private styleSheet: CSSStyleSheet | null = null;
private wordStyleMap = new Map<string, string>(); private highlights = new Map<string, Highlight>();
private highlightsByWord = new Map<string, Range[]>();
private observer: MutationObserver; private observer: MutationObserver;
private isHighlighting = false;
private currentMatchCase = false;
constructor(private onUpdate: () => void) { constructor(private onUpdate: () => void) {
this.observer = new MutationObserver(DOMUtils.debounce(onUpdate, 300)); this.observer = new MutationObserver(DOMUtils.debounce((mutations: MutationRecord[]) => {
if (this.isHighlighting) return;
const hasContentChanges = mutations.some((mutation: MutationRecord) => {
if (mutation.type !== 'childList') return false;
const allNodes = [...Array.from(mutation.addedNodes), ...Array.from(mutation.removedNodes)];
return allNodes.some(node => {
if (node.nodeType === Node.TEXT_NODE) return true;
if (node instanceof Element) return true;
return false;
});
});
if (hasContentChanges) {
this.onUpdate();
}
}, CONSTANTS.DEBOUNCE_DELAY));
} }
private initializeStyleSheet(): void { private initializeStyleSheet(): void {
@@ -19,40 +38,30 @@ export class HighlightEngine {
} }
} }
private updateWordStyles(activeWords: ActiveWord[]): void { private updateHighlightStyles(activeWords: ActiveWord[]): void {
this.initializeStyleSheet(); this.initializeStyleSheet();
while (this.styleSheet!.cssRules.length > 0) { while (this.styleSheet!.cssRules.length > 0) {
this.styleSheet!.deleteRule(0); this.styleSheet!.deleteRule(0);
} }
this.wordStyleMap.clear(); const uniqueStyles = new Map<string, number>();
const uniqueStyles = new Map<string, string>(); let styleIndex = 0;
for (const word of activeWords) { for (const word of activeWords) {
const styleKey = `${word.background}-${word.foreground}`; const styleKey = `${word.background}-${word.foreground}`;
if (!uniqueStyles.has(styleKey)) { if (!uniqueStyles.has(styleKey)) {
const className = `highlighted-word-${uniqueStyles.size}`; uniqueStyles.set(styleKey, styleIndex);
uniqueStyles.set(styleKey, className); const rule = `::highlight(gh-${styleIndex}) { background-color: ${word.background}; color: ${word.foreground}; }`;
const rule = `.${className} { background: ${word.background}; color: ${word.foreground}; padding: 0 2px; }`;
this.styleSheet!.insertRule(rule, this.styleSheet!.cssRules.length); this.styleSheet!.insertRule(rule, this.styleSheet!.cssRules.length);
styleIndex++;
} }
const lookup = word.text;
this.wordStyleMap.set(lookup, uniqueStyles.get(styleKey)!);
} }
} }
clearHighlights(): void { clearHighlights(): void {
const highlightedElements = document.querySelectorAll('[data-gh]'); this.observer.disconnect();
highlightedElements.forEach(element => { this.clearHighlightsInternal();
const parent = element.parentNode;
if (parent) {
parent.replaceChild(document.createTextNode(element.textContent || ''), element);
parent.normalize();
}
});
} }
private getTextNodes(): Text[] { private getTextNodes(): Text[] {
@@ -62,9 +71,6 @@ export class HighlightEngine {
NodeFilter.SHOW_TEXT, NodeFilter.SHOW_TEXT,
{ {
acceptNode: (node: Text) => { acceptNode: (node: Text) => {
if (node.parentNode && (node.parentNode as Element).hasAttribute('data-gh')) {
return NodeFilter.FILTER_REJECT;
}
if (node.parentNode && ['SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME'].includes(node.parentNode.nodeName)) { if (node.parentNode && ['SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME'].includes(node.parentNode.nodeName)) {
return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_REJECT;
} }
@@ -99,25 +105,38 @@ export class HighlightEngine {
} }
highlight(lists: HighlightList[], matchCase: boolean, matchWhole: boolean): void { highlight(lists: HighlightList[], matchCase: boolean, matchWhole: boolean): void {
if (this.isHighlighting) return;
this.isHighlighting = true;
this.currentMatchCase = matchCase;
this.observer.disconnect(); this.observer.disconnect();
this.clearHighlights(); this.clearHighlightsInternal();
const activeWords = this.extractActiveWords(lists); const activeWords = this.extractActiveWords(lists);
if (activeWords.length === 0) { if (activeWords.length === 0) {
this.startObserving(); this.startObserving();
this.isHighlighting = false;
return; return;
} }
this.updateWordStyles(activeWords); this.updateHighlightStyles(activeWords);
const styleMap = new Map<string, number>();
const uniqueStyles = new Map<string, number>();
let styleIndex = 0;
const wordMap = new Map<string, ActiveWord>();
for (const word of activeWords) { for (const word of activeWords) {
const key = matchCase ? word.text : word.text.toLowerCase(); const styleKey = `${word.background}-${word.foreground}`;
wordMap.set(key, word); if (!uniqueStyles.has(styleKey)) {
uniqueStyles.set(styleKey, styleIndex++);
}
const lookup = matchCase ? word.text : word.text.toLowerCase();
styleMap.set(lookup, uniqueStyles.get(styleKey)!);
} }
const flags = matchCase ? 'gu' : 'giu'; const flags = matchCase ? 'gu' : 'giu';
let wordsPattern = Array.from(wordMap.keys()).map(DOMUtils.escapeRegex).join('|'); let wordsPattern = Array.from(styleMap.keys()).map(DOMUtils.escapeRegex).join('|');
if (matchWhole) { if (matchWhole) {
wordsPattern = `(?:(?<!\\p{L})|^)(${wordsPattern})(?:(?!\\p{L})|$)`; wordsPattern = `(?:(?<!\\p{L})|^)(${wordsPattern})(?:(?!\\p{L})|$)`;
@@ -127,30 +146,136 @@ export class HighlightEngine {
const pattern = new RegExp(`(${wordsPattern})`, flags); const pattern = new RegExp(`(${wordsPattern})`, flags);
const textNodes = this.getTextNodes(); const textNodes = this.getTextNodes();
const rangesByStyle = new Map<number, Range[]>();
this.highlightsByWord.clear();
for (const node of textNodes) { for (const node of textNodes) {
if (!node.nodeValue || !pattern.test(node.nodeValue)) continue; if (!node.nodeValue) continue;
const span = document.createElement('span'); const text = node.nodeValue;
span.innerHTML = node.nodeValue.replace(pattern, (match) => { pattern.lastIndex = 0;
const lookup = matchCase ? match : match.toLowerCase(); let match;
const className = this.wordStyleMap.get(lookup) || 'highlighted-word-0';
return `<span data-gh class="${className}">${match}</span>`;
});
node.parentNode?.replaceChild(span, node); while ((match = pattern.exec(text)) !== null) {
const lookup = matchCase ? match[0] : match[0].toLowerCase();
const styleIdx = styleMap.get(lookup);
if (styleIdx !== undefined) {
const range = new Range();
range.setStart(node, match.index);
range.setEnd(node, match.index + match[0].length);
if (!rangesByStyle.has(styleIdx)) {
rangesByStyle.set(styleIdx, []);
}
rangesByStyle.get(styleIdx)!.push(range);
if (!this.highlightsByWord.has(lookup)) {
this.highlightsByWord.set(lookup, []);
}
this.highlightsByWord.get(lookup)!.push(range);
}
}
}
for (const [styleIdx, ranges] of rangesByStyle) {
const highlight = new Highlight(...ranges);
const highlightName = `gh-${styleIdx}`;
this.highlights.set(highlightName, highlight);
CSS.highlights.set(highlightName, highlight);
} }
} catch (e) { } catch (e) {
console.error('Regex error:', e); console.error('Regex error:', e);
} }
this.startObserving(); this.startObserving();
this.isHighlighting = false;
}
private clearHighlightsInternal(): void {
for (const name of this.highlights.keys()) {
CSS.highlights.delete(name);
}
this.highlights.clear();
this.highlightsByWord.clear();
if (this.styleSheet && this.styleSheet.cssRules.length > 0) {
while (this.styleSheet.cssRules.length > 0) {
this.styleSheet.deleteRule(0);
}
}
}
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 {
this.observer.disconnect();
} }
private startObserving(): void { private startObserving(): void {
this.observer.observe(document.body, { this.observer.observe(document.body, {
childList: true, childList: true,
subtree: true, subtree: true,
characterData: true attributes: false
}); });
} }

View File

@@ -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';
@@ -13,15 +13,26 @@ export class PopupController {
private matchWholeEnabled = false; private matchWholeEnabled = false;
private exceptionsList: string[] = []; private exceptionsList: string[] = [];
private currentTabHost = ''; private currentTabHost = '';
private sectionStates: Record<string, boolean> = {}; 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();
await this.getCurrentTab(); await this.getCurrentTab();
this.loadSectionStates(); this.loadActiveTab();
this.initializeSectionStates(); this.translateTitles();
this.setupEventListeners(); this.setupEventListeners();
this.render(); this.render();
this.hideLoadingOverlay();
}
private hideLoadingOverlay(): void {
const overlay = document.querySelector('.loading-overlay');
if (overlay) {
overlay.classList.add('hidden');
setTimeout(() => overlay.remove(), 200);
}
} }
private async loadData(): Promise<void> { private async loadData(): Promise<void> {
@@ -56,78 +67,66 @@ export class PopupController {
} }
} }
private loadSectionStates(): void { private loadActiveTab(): void {
const saved = localStorage.getItem('goose-highlighter-section-states'); const saved = localStorage.getItem('goose-highlighter-active-tab');
if (saved) { if (saved) {
try { this.activeTab = saved;
this.sectionStates = JSON.parse(saved);
} catch {
this.sectionStates = {};
}
} }
} }
private saveSectionStates(): void { private translateTitles(): void {
localStorage.setItem('goose-highlighter-section-states', JSON.stringify(this.sectionStates)); document.querySelectorAll('[data-i18n-title]').forEach(element => {
const key = element.getAttribute('data-i18n-title');
if (key) {
const translation = chrome.i18n.getMessage(key);
if (translation) {
element.setAttribute('title', translation);
} }
private initializeSectionStates(): void {
Object.keys(this.sectionStates).forEach(sectionName => {
const section = document.querySelector(`[data-section="${sectionName}"]`);
if (section && this.sectionStates[sectionName]) {
section.classList.add('collapsed');
} }
}); });
} }
private saveActiveTab(): void {
localStorage.setItem('goose-highlighter-active-tab', this.activeTab);
}
private switchTab(tabName: string): void {
this.activeTab = tabName;
this.saveActiveTab();
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-tab') === tabName);
});
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.toggle('active', content.getAttribute('data-tab-content') === tabName);
});
if (tabName === 'page-highlights') {
this.loadPageHighlights();
}
}
private setupEventListeners(): void { private setupEventListeners(): void {
this.setupSectionToggles(); this.setupTabs();
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();
} }
private setupSectionToggles(): void { private setupTabs(): void {
document.querySelectorAll('.collapse-toggle').forEach(button => { document.querySelectorAll('.tab-button').forEach(button => {
button.addEventListener('click', (e) => { button.addEventListener('click', () => {
e.stopPropagation(); const tabName = (button as HTMLElement).getAttribute('data-tab');
const targetSection = (button as HTMLElement).getAttribute('data-target'); if (tabName) this.switchTab(tabName);
if (targetSection) this.toggleSection(targetSection);
}); });
}); });
document.querySelectorAll('.section-header').forEach(header => { this.switchTab(this.activeTab);
header.addEventListener('click', (e) => {
if ((e.target as HTMLElement).tagName === 'BUTTON' ||
(e.target as HTMLElement).tagName === 'INPUT' ||
(e.target as HTMLElement).closest('button')) {
return;
}
const section = (header as HTMLElement).closest('.section');
const sectionName = section?.getAttribute('data-section');
if (sectionName) this.toggleSection(sectionName);
});
});
}
private toggleSection(sectionName: string): void {
const section = document.querySelector(`[data-section="${sectionName}"]`);
if (!section) return;
const isCollapsed = section.classList.contains('collapsed');
if (isCollapsed) {
section.classList.remove('collapsed');
this.sectionStates[sectionName] = false;
} else {
section.classList.add('collapsed');
this.sectionStates[sectionName] = true;
}
this.saveSectionStates();
} }
private setupListManagement(): void { private setupListManagement(): void {
@@ -199,8 +198,7 @@ export class PopupController {
private setupWordListEvents(wordList: HTMLDivElement): void { private setupWordListEvents(wordList: HTMLDivElement): void {
wordList.addEventListener('change', (e) => { wordList.addEventListener('change', (e) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
if (target.type === 'checkbox') { if (target.type === 'checkbox' && target.dataset.index != null) {
if (target.dataset.index != null) {
const index = +target.dataset.index; const index = +target.dataset.index;
if (target.checked) { if (target.checked) {
this.selectedCheckboxes.add(index); this.selectedCheckboxes.add(index);
@@ -208,15 +206,10 @@ export class PopupController {
this.selectedCheckboxes.delete(index); this.selectedCheckboxes.delete(index);
} }
this.renderWords(); this.renderWords();
} else if (target.dataset.activeEdit != null) {
const index = +target.dataset.activeEdit;
this.lists[this.currentListIndex].words[index].active = target.checked;
this.save();
}
} }
}); });
wordList.addEventListener('input', (e) => { wordList.addEventListener('change', (e) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
const index = +(target.dataset.bgEdit ?? target.dataset.fgEdit ?? -1); const index = +(target.dataset.bgEdit ?? target.dataset.fgEdit ?? -1);
if (index === -1) return; if (index === -1) return;
@@ -242,13 +235,14 @@ export class PopupController {
} }
}); });
let scrollTimeout: number; let scrolling = false;
wordList.addEventListener('scroll', () => { wordList.addEventListener('scroll', () => {
if (scrollTimeout) return; if (scrolling) return;
scrollTimeout = window.setTimeout(() => { scrolling = true;
requestAnimationFrame(() => this.renderWords()); requestAnimationFrame(() => {
scrollTimeout = 0; this.renderWords();
}, 16); scrolling = false;
});
}); });
} }
@@ -301,24 +295,142 @@ export class PopupController {
const matchCase = document.getElementById('matchCase') as HTMLInputElement; const matchCase = document.getElementById('matchCase') as HTMLInputElement;
const matchWhole = document.getElementById('matchWhole') as HTMLInputElement; const matchWhole = document.getElementById('matchWhole') as HTMLInputElement;
globalToggle.addEventListener('change', () => { globalToggle.addEventListener('change', async () => {
this.globalHighlightEnabled = globalToggle.checked; this.globalHighlightEnabled = globalToggle.checked;
this.updateGlobalToggleState(); await StorageService.update('globalHighlightEnabled', this.globalHighlightEnabled);
MessageService.sendToAllTabs({
type: 'GLOBAL_TOGGLE_UPDATED',
enabled: this.globalHighlightEnabled
});
}); });
matchCase.addEventListener('change', () => { matchCase.addEventListener('change', async () => {
this.matchCaseEnabled = matchCase.checked; this.matchCaseEnabled = matchCase.checked;
this.save(); await StorageService.update('matchCaseEnabled', this.matchCaseEnabled);
MessageService.sendToAllTabs({
type: 'MATCH_OPTIONS_UPDATED',
matchCase: this.matchCaseEnabled,
matchWhole: this.matchWholeEnabled
});
}); });
matchWhole.addEventListener('change', () => { matchWhole.addEventListener('change', async () => {
this.matchWholeEnabled = matchWhole.checked; this.matchWholeEnabled = matchWhole.checked;
this.save(); await StorageService.update('matchWholeEnabled', this.matchWholeEnabled);
MessageService.sendToAllTabs({
type: 'MATCH_OPTIONS_UPDATED',
matchCase: this.matchCaseEnabled,
matchWhole: this.matchWholeEnabled
});
}); });
} }
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', () => { document.getElementById('toggleExceptionBtn')?.addEventListener('click', async () => {
if (!this.currentTabHost) return; if (!this.currentTabHost) return;
const isException = this.exceptionsList.includes(this.currentTabHost); const isException = this.exceptionsList.includes(this.currentTabHost);
@@ -331,34 +443,29 @@ export class PopupController {
this.updateExceptionButton(); this.updateExceptionButton();
this.renderExceptions(); this.renderExceptions();
this.save(); await StorageService.update('exceptionsList', this.exceptionsList);
MessageService.sendToAllTabs({ type: 'EXCEPTIONS_LIST_UPDATED' });
}); });
document.getElementById('manageExceptionsBtn')?.addEventListener('click', () => { document.getElementById('clearExceptionsBtn')?.addEventListener('click', async () => {
const panel = document.getElementById('exceptionsPanel');
if (panel) {
const isVisible = panel.style.display !== 'none';
panel.style.display = isVisible ? 'none' : 'block';
}
});
document.getElementById('clearExceptionsBtn')?.addEventListener('click', () => {
if (confirm(chrome.i18n.getMessage('confirm_clear_exceptions') || 'Clear all exceptions?')) { if (confirm(chrome.i18n.getMessage('confirm_clear_exceptions') || 'Clear all exceptions?')) {
this.exceptionsList = []; this.exceptionsList = [];
this.updateExceptionButton(); this.updateExceptionButton();
this.renderExceptions(); this.renderExceptions();
this.save(); await StorageService.update('exceptionsList', this.exceptionsList);
MessageService.sendToAllTabs({ type: 'EXCEPTIONS_LIST_UPDATED' });
} }
}); });
document.getElementById('exceptionsList')?.addEventListener('click', (e) => { document.getElementById('exceptionsList')?.addEventListener('click', async (e) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
if (target.classList.contains('exception-remove')) { if (target.classList.contains('exception-remove')) {
const domain = target.dataset.domain!; const domain = target.dataset.domain!;
this.exceptionsList = this.exceptionsList.filter(d => d !== domain); this.exceptionsList = this.exceptionsList.filter(d => d !== domain);
this.updateExceptionButton(); this.updateExceptionButton();
this.renderExceptions(); this.renderExceptions();
this.save(); await StorageService.update('exceptionsList', this.exceptionsList);
MessageService.sendToAllTabs({ type: 'EXCEPTIONS_LIST_UPDATED' });
} }
}); });
} }
@@ -467,29 +574,13 @@ export class PopupController {
exceptionsList: this.exceptionsList exceptionsList: this.exceptionsList
}); });
this.render(); this.renderLists();
MessageService.sendToAllTabs({ type: 'WORD_LIST_UPDATED' }); MessageService.sendToAllTabs({ type: 'WORD_LIST_UPDATED' });
MessageService.sendToAllTabs({
type: 'GLOBAL_TOGGLE_UPDATED',
enabled: this.globalHighlightEnabled
});
MessageService.sendToAllTabs({
type: 'MATCH_OPTIONS_UPDATED',
matchCase: this.matchCaseEnabled,
matchWhole: this.matchWholeEnabled
});
MessageService.sendToAllTabs({ type: 'EXCEPTIONS_LIST_UPDATED' });
} }
private async updateGlobalToggleState(): Promise<void> {
await StorageService.update('globalHighlightEnabled', this.globalHighlightEnabled);
MessageService.sendToAllTabs({
type: 'GLOBAL_TOGGLE_UPDATED',
enabled: this.globalHighlightEnabled
});
}
private render(): void { private render(): void {
this.renderLists(); this.renderLists();
@@ -527,19 +618,19 @@ export class PopupController {
} }
const itemHeight = 32; const itemHeight = 32;
const containerHeight = wordList.clientHeight; const itemSpacing = 2;
const totalItemHeight = itemHeight + itemSpacing;
const containerHeight = wordList.clientHeight || 250;
const scrollTop = wordList.scrollTop; const scrollTop = wordList.scrollTop;
const startIndex = Math.floor(scrollTop / itemHeight); const startIndex = Math.max(0, Math.floor(scrollTop / totalItemHeight) - 1);
const endIndex = Math.min( const visibleCount = Math.ceil(containerHeight / totalItemHeight);
startIndex + Math.ceil(containerHeight / itemHeight) + 2, const endIndex = Math.min(startIndex + visibleCount + 2, filteredWords.length);
filteredWords.length
);
wordList.innerHTML = ''; wordList.innerHTML = '';
const spacer = document.createElement('div'); const spacer = document.createElement('div');
spacer.style.position = 'relative'; spacer.style.position = 'relative';
spacer.style.height = `${filteredWords.length * itemHeight}px`; spacer.style.height = `${filteredWords.length * totalItemHeight}px`;
spacer.style.width = '100%'; spacer.style.width = '100%';
for (let i = startIndex; i < endIndex; i++) { for (let i = startIndex; i < endIndex; i++) {
@@ -561,32 +652,22 @@ export class PopupController {
private createWordItem(word: HighlightWord, realIndex: number, displayIndex: number, itemHeight: number): HTMLDivElement { private createWordItem(word: HighlightWord, realIndex: number, displayIndex: number, itemHeight: number): HTMLDivElement {
const container = document.createElement('div'); const container = document.createElement('div');
container.className = 'word-item';
if (word.active === false) {
container.classList.add('disabled');
}
container.style.cssText = ` container.style.cssText = `
height: ${itemHeight}px;
position: absolute; position: absolute;
top: ${displayIndex * itemHeight}px; top: ${displayIndex * (itemHeight + 2)}px;
width: calc(100% - 8px);
left: 4px;
right: 4px;
display: flex;
align-items: center;
gap: 6px;
padding: 0 4px;
box-sizing: border-box;
background: var(--highlight-tag);
border: 1px solid var(--highlight-tag-border);
`; `;
const list = this.lists[this.currentListIndex]; const list = this.lists[this.currentListIndex];
container.innerHTML = ` container.innerHTML = `
<input type="checkbox" class="word-checkbox" data-index="${realIndex}" ${this.selectedCheckboxes.has(realIndex) ? 'checked' : ''}> <input type="checkbox" class="word-checkbox" data-index="${realIndex}" ${this.selectedCheckboxes.has(realIndex) ? 'checked' : ''} title="${chrome.i18n.getMessage('select_title') || 'Select'}">
<input type="text" value="${DOMUtils.escapeHtml(word.wordStr)}" data-word-edit="${realIndex}" style="flex-grow: 1; min-width: 0; padding: 4px 8px; border-radius: 4px; border: 1px solid var(--input-border); background-color: var(--input-bg); color: var(--text-color);"> <input type="text" value="${DOMUtils.escapeHtml(word.wordStr)}" data-word-edit="${realIndex}" placeholder="${chrome.i18n.getMessage('word_placeholder') || 'Word or phrase'}">
<input type="color" value="${word.background || list.background}" data-bg-edit="${realIndex}" style="width: 24px; height: 24px; flex-shrink: 0;"> <input type="color" value="${word.background || list.background}" data-bg-edit="${realIndex}" title="${chrome.i18n.getMessage('background_color_title') || 'Background color'}">
<input type="color" value="${word.foreground || list.foreground}" data-fg-edit="${realIndex}" style="width: 24px; height: 24px; flex-shrink: 0;"> <input type="color" value="${word.foreground || list.foreground}" data-fg-edit="${realIndex}" title="${chrome.i18n.getMessage('text_color_title') || 'Text color'}">
<label class="word-active" style="display: flex; align-items: center; gap: 4px; flex-shrink: 0;">
<input type="checkbox" ${word.active !== false ? 'checked' : ''} data-active-edit="${realIndex}" class="switch">
</label>
`; `;
return container; return container;
@@ -604,12 +685,12 @@ export class PopupController {
btnText.textContent = chrome.i18n.getMessage('remove_exception') || 'Remove from Exceptions'; btnText.textContent = chrome.i18n.getMessage('remove_exception') || 'Remove from Exceptions';
toggleBtn.className = 'danger'; toggleBtn.className = 'danger';
const icon = toggleBtn.querySelector('i'); const icon = toggleBtn.querySelector('i');
if (icon) icon.className = 'fa-solid fa-check'; if (icon) icon.className = 'fa-solid fa-trash';
} else { } else {
btnText.textContent = chrome.i18n.getMessage('add_exception') || 'Add to Exceptions'; btnText.textContent = chrome.i18n.getMessage('add_exception') || 'Add to Exceptions';
toggleBtn.className = ''; toggleBtn.className = '';
const icon = toggleBtn.querySelector('i'); const icon = toggleBtn.querySelector('i');
if (icon) icon.className = 'fa-solid fa-ban'; if (icon) icon.className = 'fa-solid fa-plus';
} }
} }
@@ -618,7 +699,7 @@ export class PopupController {
if (!container) return; if (!container) return;
if (this.exceptionsList.length === 0) { if (this.exceptionsList.length === 0) {
container.innerHTML = '<div class="exception-item">No exceptions</div>'; container.innerHTML = `<div class="exception-item">${chrome.i18n.getMessage('no_exceptions') || 'No exceptions'}</div>`;
return; return;
} }

View File

@@ -6,7 +6,7 @@ function localizePage(): void {
const message = (element as HTMLElement).dataset.i18n!; const message = (element as HTMLElement).dataset.i18n!;
const localizedText = chrome.i18n.getMessage(message); const localizedText = chrome.i18n.getMessage(message);
if (localizedText) { if (localizedText) {
if (element.tagName === 'INPUT' && (element as HTMLInputElement).hasAttribute('placeholder')) { if ((element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') && (element as HTMLInputElement).hasAttribute('placeholder')) {
(element as HTMLInputElement).placeholder = localizedText; (element as HTMLInputElement).placeholder = localizedText;
} else { } else {
element.textContent = localizedText; element.textContent = localizedText;
@@ -15,8 +15,17 @@ function localizePage(): void {
}); });
} }
function displayVersion(): void {
const manifest = chrome.runtime.getManifest();
const versionElement = document.getElementById('version-number');
if (versionElement && manifest.version) {
versionElement.textContent = manifest.version;
}
}
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
localizePage(); localizePage();
displayVersion();
const controller = new PopupController(); const controller = new PopupController();
await controller.initialize(); await controller.initialize();
}); });

View File

@@ -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;
}
} }

View File

@@ -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 {
@@ -50,6 +60,6 @@ export const DEFAULT_STORAGE: StorageData = {
export const CONSTANTS = { export const CONSTANTS = {
WORD_ITEM_HEIGHT: 32, WORD_ITEM_HEIGHT: 32,
DEBOUNCE_DELAY: 300, DEBOUNCE_DELAY: 150,
SCROLL_THROTTLE: 16 SCROLL_THROTTLE: 16
} as const; } as const;

27
src/types/css-highlights.d.ts vendored Normal file
View File

@@ -0,0 +1,27 @@
// CSS Highlights API type declarations
interface Highlight {
new(...ranges: Range[]): Highlight;
add(range: Range): void;
clear(): void;
delete(range: Range): boolean;
has(range: Range): boolean;
readonly size: number;
}
interface HighlightRegistry {
set(name: string, highlight: Highlight): void;
get(name: string): Highlight | undefined;
delete(name: string): boolean;
clear(): void;
has(name: string): boolean;
readonly size: number;
}
interface CSS {
highlights: HighlightRegistry;
}
declare var Highlight: {
prototype: Highlight;
new(...ranges: Range[]): Highlight;
};