mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-16 22:38:20 +00:00
feat: store the hide-clutter list as a delta, not a snapshot
Saving the editor used to persist the full effective list, which froze the
shipped defaults at that moment: every default added in a later version
never reached anyone who had touched the list once.
Storage now keeps only { removedDefaults, addedDomains }, and the effective
list is resolved against whatever ships in the running version. Entries are
tagged default or user, the editor groups them under comment headers, and
'#' lines are ignored on save. Lists saved by earlier versions are migrated
to the delta form on first read and the legacy key is removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -8,7 +8,7 @@ All notable changes to the KoalaSync browser extension and relay server.
|
||||
|
||||
### Added
|
||||
- **Extension: Same-origin frame video detection** — Finds and controls players that live inside a first-party iframe instead of the top document, so sites that wrap their player in their own frame sync without site-specific workarounds. Frame documents are watched for late-loading players and re-scanned after a frame reload; cross-origin frames stay out of reach by design.
|
||||
- **Extension: Editable Hide-Clutter list** — Adds a validated, deduplicated domain editor in Settings, prefilled with the shipped blacklist, with a defaults reset and per-device persistence in `chrome.storage.local`.
|
||||
- **Extension: Editable Hide-Clutter list** — Adds a validated, deduplicated domain editor in Settings, prefilled with the shipped blacklist, with a defaults reset and per-device persistence in `chrome.storage.local`. The editor groups entries into your own and the shipped defaults, and only your changes are stored, so domains added to the shipped list in later versions still reach you without overriding what you removed or added. Lists saved by an earlier version are migrated automatically.
|
||||
- **Extension: Independent default audio boost** — Adds a configurable `0–20 dB` output gain in half-decibel steps. The boost works with or without the compressor and applies live to the selected video tab.
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -465,7 +465,7 @@ function emitEpisodeLobbyForCurrentPrivacy() {
|
||||
const LEGACY_SYNC_KEYS = [
|
||||
'serverUrl', 'useCustomServer', 'roomId', 'password', 'chatKey',
|
||||
'chatEnabled', 'chatNotifications', 'chatPosition', 'chatSize', 'chatStartMode', 'chatReactionDisplay', 'username',
|
||||
'filterNoise', 'customBlacklistDomains', 'autoSyncNextEpisode', 'forceSyncMode',
|
||||
'filterNoise', 'customBlacklistDomains', 'blacklistOverrides', 'autoSyncNextEpisode', 'forceSyncMode',
|
||||
'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings',
|
||||
'titlePrivacyMode', 'sendTabTitle', 'mediaTitlePrivacyMode'
|
||||
];
|
||||
|
||||
@@ -98,8 +98,10 @@
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtert Nicht-Video-Tabs und irrelevante Domains heraus, um die Liste sauber zu halten",
|
||||
"BLACKLIST_EDIT": "Liste bearbeiten",
|
||||
"BLACKLIST_EDITOR_LABEL": "Ausgeblendete Domains",
|
||||
"BLACKLIST_EDITOR_HELP": "Eine Domain pro Zeile. URLs werden auf ihren Hostnamen reduziert. Speicherung nur auf diesem Gerät.",
|
||||
"BLACKLIST_EDITOR_HELP": "Eine Domain pro Zeile. Zeilen mit # sind Notizen. URLs werden auf ihren Hostnamen reduziert. Gespeichert werden nur deine Änderungen auf diesem Gerät, damit neue Standard-Domains weiterhin ankommen.",
|
||||
"BLACKLIST_RESET": "Standards wiederherstellen",
|
||||
"BLACKLIST_SECTION_USER": "Deine Einträge",
|
||||
"BLACKLIST_SECTION_DEFAULT": "Mitgelieferte Standards, werden automatisch aktuell gehalten",
|
||||
"BLACKLIST_SAVE": "Liste speichern",
|
||||
"BLACKLIST_STATUS_INVALID": "Nicht gespeichert. Bitte diese Einträge prüfen: {domains}",
|
||||
"BLACKLIST_STATUS_LIMIT": "Nicht gespeichert. Die Liste unterstützt höchstens {max} Domains.",
|
||||
|
||||
@@ -98,8 +98,10 @@
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filters out non-video tabs and unrelated domains to keep the list clean",
|
||||
"BLACKLIST_EDIT": "Edit list",
|
||||
"BLACKLIST_EDITOR_LABEL": "Hidden domains",
|
||||
"BLACKLIST_EDITOR_HELP": "One domain per line. URLs are reduced to their hostname. Stored only on this device.",
|
||||
"BLACKLIST_EDITOR_HELP": "One domain per line. Lines starting with # are notes. URLs are reduced to their hostname. Only your changes are stored on this device, so new default domains keep arriving.",
|
||||
"BLACKLIST_RESET": "Restore defaults",
|
||||
"BLACKLIST_SECTION_USER": "Your entries",
|
||||
"BLACKLIST_SECTION_DEFAULT": "Shipped defaults, kept up to date automatically",
|
||||
"BLACKLIST_SAVE": "Save list",
|
||||
"BLACKLIST_STATUS_INVALID": "Not saved. Check these entries: {domains}",
|
||||
"BLACKLIST_STATUS_LIMIT": "Not saved. The list supports at most {max} domains.",
|
||||
|
||||
@@ -98,8 +98,10 @@
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtra pestañas que no son de video y dominios no relacionados para mantener limpia la lista",
|
||||
"BLACKLIST_EDIT": "Editar lista",
|
||||
"BLACKLIST_EDITOR_LABEL": "Dominios ocultos",
|
||||
"BLACKLIST_EDITOR_HELP": "Un dominio por línea. Las URL se reducen a su nombre de host. Se guarda solo en este dispositivo.",
|
||||
"BLACKLIST_EDITOR_HELP": "Un dominio por línea. Las líneas que empiezan por # son notas. Las URL se reducen a su nombre de host. Solo se guardan tus cambios en este dispositivo, por lo que los nuevos dominios predeterminados siguen llegando.",
|
||||
"BLACKLIST_RESET": "Restaurar valores predeterminados",
|
||||
"BLACKLIST_SECTION_USER": "Tus entradas",
|
||||
"BLACKLIST_SECTION_DEFAULT": "Valores predeterminados incluidos, actualizados automáticamente",
|
||||
"BLACKLIST_SAVE": "Guardar lista",
|
||||
"BLACKLIST_STATUS_INVALID": "No se guardó. Revisa estas entradas: {domains}",
|
||||
"BLACKLIST_STATUS_LIMIT": "No se guardó. La lista admite como máximo {max} dominios.",
|
||||
|
||||
@@ -98,8 +98,10 @@
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtre les onglets non-vidéo et les domaines non pertinents pour garder la liste propre",
|
||||
"BLACKLIST_EDIT": "Modifier la liste",
|
||||
"BLACKLIST_EDITOR_LABEL": "Domaines masqués",
|
||||
"BLACKLIST_EDITOR_HELP": "Un domaine par ligne. Les URL sont réduites à leur nom d’hôte. Stockage uniquement sur cet appareil.",
|
||||
"BLACKLIST_EDITOR_HELP": "Un domaine par ligne. Les lignes commençant par # sont des notes. Les URL sont réduites à leur nom d’hôte. Seules vos modifications sont stockées sur cet appareil, les nouveaux domaines par défaut continuent donc d’arriver.",
|
||||
"BLACKLIST_RESET": "Restaurer les valeurs par défaut",
|
||||
"BLACKLIST_SECTION_USER": "Vos entrées",
|
||||
"BLACKLIST_SECTION_DEFAULT": "Valeurs par défaut fournies, mises à jour automatiquement",
|
||||
"BLACKLIST_SAVE": "Enregistrer la liste",
|
||||
"BLACKLIST_STATUS_INVALID": "Non enregistré. Vérifiez ces entrées : {domains}",
|
||||
"BLACKLIST_STATUS_LIMIT": "Non enregistré. La liste accepte au maximum {max} domaines.",
|
||||
|
||||
@@ -98,8 +98,10 @@
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Mantiene pulito l'elenco filtrando le schede non pertinenti",
|
||||
"BLACKLIST_EDIT": "Modifica elenco",
|
||||
"BLACKLIST_EDITOR_LABEL": "Domini nascosti",
|
||||
"BLACKLIST_EDITOR_HELP": "Un dominio per riga. Gli URL vengono ridotti al nome host. Salvato solo su questo dispositivo.",
|
||||
"BLACKLIST_EDITOR_HELP": "Un dominio per riga. Le righe che iniziano con # sono note. Gli URL vengono ridotti al nome host. Su questo dispositivo vengono salvate solo le tue modifiche, così i nuovi domini predefiniti continuano ad arrivare.",
|
||||
"BLACKLIST_RESET": "Ripristina predefiniti",
|
||||
"BLACKLIST_SECTION_USER": "Le tue voci",
|
||||
"BLACKLIST_SECTION_DEFAULT": "Predefiniti inclusi, aggiornati automaticamente",
|
||||
"BLACKLIST_SAVE": "Salva elenco",
|
||||
"BLACKLIST_STATUS_INVALID": "Non salvato. Controlla queste voci: {domains}",
|
||||
"BLACKLIST_STATUS_LIMIT": "Non salvato. L'elenco supporta al massimo {max} domini.",
|
||||
|
||||
@@ -98,8 +98,10 @@
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "リストをすっきりさせるため、ビデオのないタブや無関係なドメインをフィルタリングします",
|
||||
"BLACKLIST_EDIT": "リストを編集",
|
||||
"BLACKLIST_EDITOR_LABEL": "非表示ドメイン",
|
||||
"BLACKLIST_EDITOR_HELP": "1行に1つのドメインを入力します。URLはホスト名に変換され、この端末にのみ保存されます。",
|
||||
"BLACKLIST_EDITOR_HELP": "1行に1つのドメインを入力します。# で始まる行はメモです。URLはホスト名に変換されます。この端末に保存されるのは変更点だけなので、新しい既定ドメインも引き続き追加されます。",
|
||||
"BLACKLIST_RESET": "既定値に戻す",
|
||||
"BLACKLIST_SECTION_USER": "あなたの追加分",
|
||||
"BLACKLIST_SECTION_DEFAULT": "同梱の既定値(自動的に最新へ更新)",
|
||||
"BLACKLIST_SAVE": "リストを保存",
|
||||
"BLACKLIST_STATUS_INVALID": "保存されませんでした。次の項目を確認してください: {domains}",
|
||||
"BLACKLIST_STATUS_LIMIT": "保存されませんでした。リストは最大{max}ドメインまでです。",
|
||||
|
||||
@@ -98,8 +98,10 @@
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "목록을 깔끔하게 유지하기 위해 비디오가 없는 탭과 관련 없는 도메인을 필터링합니다",
|
||||
"BLACKLIST_EDIT": "목록 편집",
|
||||
"BLACKLIST_EDITOR_LABEL": "숨긴 도메인",
|
||||
"BLACKLIST_EDITOR_HELP": "한 줄에 도메인 하나를 입력하세요. URL은 호스트 이름으로 변환되며 이 기기에만 저장됩니다.",
|
||||
"BLACKLIST_EDITOR_HELP": "한 줄에 도메인 하나를 입력하세요. #으로 시작하는 줄은 메모입니다. URL은 호스트 이름으로 변환됩니다. 이 기기에는 변경 사항만 저장되므로 새 기본 도메인도 계속 반영됩니다.",
|
||||
"BLACKLIST_RESET": "기본값 복원",
|
||||
"BLACKLIST_SECTION_USER": "직접 추가한 항목",
|
||||
"BLACKLIST_SECTION_DEFAULT": "기본 제공 목록 (자동으로 최신 상태 유지)",
|
||||
"BLACKLIST_SAVE": "목록 저장",
|
||||
"BLACKLIST_STATUS_INVALID": "저장되지 않았습니다. 다음 항목을 확인하세요: {domains}",
|
||||
"BLACKLIST_STATUS_LIMIT": "저장되지 않았습니다. 목록은 최대 {max}개 도메인을 지원합니다.",
|
||||
|
||||
@@ -98,8 +98,10 @@
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtert tabbladen zonder video's en niet-gerelateerde domeinen om de lijst schoon te houden",
|
||||
"BLACKLIST_EDIT": "Lijst bewerken",
|
||||
"BLACKLIST_EDITOR_LABEL": "Verborgen domeinen",
|
||||
"BLACKLIST_EDITOR_HELP": "Eén domein per regel. URL's worden teruggebracht tot hun hostnaam. Alleen opgeslagen op dit apparaat.",
|
||||
"BLACKLIST_EDITOR_HELP": "Eén domein per regel. Regels die met # beginnen zijn notities. URL's worden teruggebracht tot hun hostnaam. Alleen jouw wijzigingen worden op dit apparaat opgeslagen, zodat nieuwe standaarddomeinen blijven binnenkomen.",
|
||||
"BLACKLIST_RESET": "Standaardwaarden herstellen",
|
||||
"BLACKLIST_SECTION_USER": "Jouw items",
|
||||
"BLACKLIST_SECTION_DEFAULT": "Meegeleverde standaarden, automatisch bijgewerkt",
|
||||
"BLACKLIST_SAVE": "Lijst opslaan",
|
||||
"BLACKLIST_STATUS_INVALID": "Niet opgeslagen. Controleer deze items: {domains}",
|
||||
"BLACKLIST_STATUS_LIMIT": "Niet opgeslagen. De lijst ondersteunt maximaal {max} domeinen.",
|
||||
|
||||
@@ -98,8 +98,10 @@
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtruje karty bez wideo i niepowiązane domeny, aby zachować porządek",
|
||||
"BLACKLIST_EDIT": "Edytuj listę",
|
||||
"BLACKLIST_EDITOR_LABEL": "Ukryte domeny",
|
||||
"BLACKLIST_EDITOR_HELP": "Jedna domena w wierszu. Adresy URL są skracane do nazwy hosta. Zapis tylko na tym urządzeniu.",
|
||||
"BLACKLIST_EDITOR_HELP": "Jedna domena w wierszu. Wiersze zaczynające się od # to notatki. Adresy URL są skracane do nazwy hosta. Na tym urządzeniu zapisywane są tylko Twoje zmiany, dzięki czemu nowe domyślne domeny nadal się pojawiają.",
|
||||
"BLACKLIST_RESET": "Przywróć domyślne",
|
||||
"BLACKLIST_SECTION_USER": "Twoje wpisy",
|
||||
"BLACKLIST_SECTION_DEFAULT": "Dołączone domyślne, aktualizowane automatycznie",
|
||||
"BLACKLIST_SAVE": "Zapisz listę",
|
||||
"BLACKLIST_STATUS_INVALID": "Nie zapisano. Sprawdź te wpisy: {domains}",
|
||||
"BLACKLIST_STATUS_LIMIT": "Nie zapisano. Lista obsługuje maksymalnie {max} domen.",
|
||||
|
||||
@@ -98,8 +98,10 @@
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtra abas não relacionadas para manter a lista limpa",
|
||||
"BLACKLIST_EDIT": "Editar lista",
|
||||
"BLACKLIST_EDITOR_LABEL": "Domínios ocultos",
|
||||
"BLACKLIST_EDITOR_HELP": "Um domínio por linha. URLs são reduzidas ao nome do host. Salvo somente neste dispositivo.",
|
||||
"BLACKLIST_EDITOR_HELP": "Um domínio por linha. Linhas começando com # são anotações. URLs são reduzidas ao nome do host. Somente suas alterações são salvas neste dispositivo, então novos domínios padrão continuam chegando.",
|
||||
"BLACKLIST_RESET": "Restaurar padrões",
|
||||
"BLACKLIST_SECTION_USER": "Suas entradas",
|
||||
"BLACKLIST_SECTION_DEFAULT": "Padrões inclusos, mantidos atualizados automaticamente",
|
||||
"BLACKLIST_SAVE": "Salvar lista",
|
||||
"BLACKLIST_STATUS_INVALID": "Não foi salvo. Verifique estas entradas: {domains}",
|
||||
"BLACKLIST_STATUS_LIMIT": "Não foi salvo. A lista aceita no máximo {max} domínios.",
|
||||
|
||||
@@ -98,8 +98,10 @@
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Filtra separadores não relacionados para manter a lista limpa",
|
||||
"BLACKLIST_EDIT": "Editar lista",
|
||||
"BLACKLIST_EDITOR_LABEL": "Domínios ocultos",
|
||||
"BLACKLIST_EDITOR_HELP": "Um domínio por linha. Os URL são reduzidos ao nome do anfitrião. Guardado apenas neste dispositivo.",
|
||||
"BLACKLIST_EDITOR_HELP": "Um domínio por linha. As linhas iniciadas por # são notas. Os URL são reduzidos ao nome do anfitrião. Apenas as suas alterações são guardadas neste dispositivo, pelo que os novos domínios predefinidos continuam a chegar.",
|
||||
"BLACKLIST_RESET": "Repor predefinições",
|
||||
"BLACKLIST_SECTION_USER": "As suas entradas",
|
||||
"BLACKLIST_SECTION_DEFAULT": "Predefinições incluídas, mantidas atualizadas automaticamente",
|
||||
"BLACKLIST_SAVE": "Guardar lista",
|
||||
"BLACKLIST_STATUS_INVALID": "Não foi guardado. Verifique estas entradas: {domains}",
|
||||
"BLACKLIST_STATUS_LIMIT": "Não foi guardado. A lista suporta no máximo {max} domínios.",
|
||||
|
||||
@@ -98,8 +98,10 @@
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Скрывает вкладки без видео и несвязанные домены для порядка",
|
||||
"BLACKLIST_EDIT": "Изменить список",
|
||||
"BLACKLIST_EDITOR_LABEL": "Скрытые домены",
|
||||
"BLACKLIST_EDITOR_HELP": "Один домен в строке. URL сокращаются до имени хоста. Хранится только на этом устройстве.",
|
||||
"BLACKLIST_EDITOR_HELP": "Один домен в строке. Строки, начинающиеся с #, — это заметки. URL сокращаются до имени хоста. На устройстве хранятся только ваши изменения, поэтому новые домены из списка по умолчанию продолжают появляться.",
|
||||
"BLACKLIST_RESET": "Восстановить значения по умолчанию",
|
||||
"BLACKLIST_SECTION_USER": "Ваши записи",
|
||||
"BLACKLIST_SECTION_DEFAULT": "Встроенный список, обновляется автоматически",
|
||||
"BLACKLIST_SAVE": "Сохранить список",
|
||||
"BLACKLIST_STATUS_INVALID": "Не сохранено. Проверьте записи: {domains}",
|
||||
"BLACKLIST_STATUS_LIMIT": "Не сохранено. Список поддерживает не более {max} доменов.",
|
||||
|
||||
@@ -98,8 +98,10 @@
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Listeyi temiz tutmak için video olmayan sekmeleri ve alakasız alan adlarını filtreler",
|
||||
"BLACKLIST_EDIT": "Listeyi düzenle",
|
||||
"BLACKLIST_EDITOR_LABEL": "Gizli alan adları",
|
||||
"BLACKLIST_EDITOR_HELP": "Her satıra bir alan adı. URL'ler ana makine adına indirgenir. Yalnızca bu cihazda saklanır.",
|
||||
"BLACKLIST_EDITOR_HELP": "Her satıra bir alan adı. # ile başlayan satırlar nottur. URL'ler ana makine adına indirgenir. Bu cihazda yalnızca değişiklikleriniz saklanır, böylece yeni varsayılan alan adları gelmeye devam eder.",
|
||||
"BLACKLIST_RESET": "Varsayılanları geri yükle",
|
||||
"BLACKLIST_SECTION_USER": "Kendi girdileriniz",
|
||||
"BLACKLIST_SECTION_DEFAULT": "Birlikte gelen varsayılanlar, otomatik olarak güncel tutulur",
|
||||
"BLACKLIST_SAVE": "Listeyi kaydet",
|
||||
"BLACKLIST_STATUS_INVALID": "Kaydedilmedi. Şu girdileri kontrol edin: {domains}",
|
||||
"BLACKLIST_STATUS_LIMIT": "Kaydedilmedi. Liste en fazla {max} alan adını destekler.",
|
||||
|
||||
@@ -98,8 +98,10 @@
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "Відфільтровує вкладки, не пов’язані з відео, і непов’язані домени, щоб зберегти список чистим",
|
||||
"BLACKLIST_EDIT": "Редагувати список",
|
||||
"BLACKLIST_EDITOR_LABEL": "Приховані домени",
|
||||
"BLACKLIST_EDITOR_HELP": "Один домен у рядку. URL скорочуються до імені хоста. Зберігається лише на цьому пристрої.",
|
||||
"BLACKLIST_EDITOR_HELP": "Один домен у рядку. Рядки, що починаються з #, є примітками. URL скорочуються до імені хоста. На цьому пристрої зберігаються лише ваші зміни, тому нові типові домени продовжують надходити.",
|
||||
"BLACKLIST_RESET": "Відновити типові значення",
|
||||
"BLACKLIST_SECTION_USER": "Ваші записи",
|
||||
"BLACKLIST_SECTION_DEFAULT": "Вбудований список, оновлюється автоматично",
|
||||
"BLACKLIST_SAVE": "Зберегти список",
|
||||
"BLACKLIST_STATUS_INVALID": "Не збережено. Перевірте записи: {domains}",
|
||||
"BLACKLIST_STATUS_LIMIT": "Не збережено. Список підтримує щонайбільше {max} доменів.",
|
||||
|
||||
@@ -98,8 +98,10 @@
|
||||
"LABEL_HIDE_CLUTTER_TOOLTIP": "过滤掉非视频选项卡和不相关的域以保持列表干净",
|
||||
"BLACKLIST_EDIT": "编辑列表",
|
||||
"BLACKLIST_EDITOR_LABEL": "隐藏的域名",
|
||||
"BLACKLIST_EDITOR_HELP": "每行一个域名。URL 会转换为主机名,并且仅保存在此设备上。",
|
||||
"BLACKLIST_EDITOR_HELP": "每行一个域名。以 # 开头的行是备注。URL 会转换为主机名。此设备仅保存你的改动,因此新的默认域名会持续加入。",
|
||||
"BLACKLIST_RESET": "恢复默认值",
|
||||
"BLACKLIST_SECTION_USER": "你添加的条目",
|
||||
"BLACKLIST_SECTION_DEFAULT": "内置默认列表(自动保持最新)",
|
||||
"BLACKLIST_SAVE": "保存列表",
|
||||
"BLACKLIST_STATUS_INVALID": "未保存。请检查这些条目:{domains}",
|
||||
"BLACKLIST_STATUS_LIMIT": "未保存。列表最多支持 {max} 个域名。",
|
||||
|
||||
@@ -1765,7 +1765,7 @@
|
||||
</div>
|
||||
<div id="blacklistEditor" class="settings-editor" hidden>
|
||||
<label for="blacklistDomains" data-i18n="BLACKLIST_EDITOR_LABEL">Hidden domains</label>
|
||||
<p class="settings-note" data-i18n="BLACKLIST_EDITOR_HELP">One domain per line. URLs are reduced to their hostname. Stored only on this device.</p>
|
||||
<p class="settings-note" data-i18n="BLACKLIST_EDITOR_HELP">One domain per line. Lines starting with # are notes. URLs are reduced to their hostname. Only your changes are stored on this device, so new default domains keep arriving.</p>
|
||||
<textarea id="blacklistDomains" rows="8" maxlength="20000" spellcheck="false"></textarea>
|
||||
<div class="settings-editor-actions">
|
||||
<button type="button" id="blacklistReset" class="secondary" data-i18n="BLACKLIST_RESET">Restore defaults</button>
|
||||
|
||||
+51
-13
@@ -1,9 +1,16 @@
|
||||
import { EVENTS, OFFICIAL_LANDING_PAGE_URL, SUPPORT_URL, getReviewUrl } from './shared/constants.js';
|
||||
import {
|
||||
BLACKLIST_OVERRIDES_STORAGE_KEY,
|
||||
BLACKLIST_SOURCE_DEFAULT,
|
||||
BLACKLIST_SOURCE_USER,
|
||||
CUSTOM_BLACKLIST_STORAGE_KEY,
|
||||
MAX_BLACKLIST_DOMAINS,
|
||||
createEmptyBlacklistOverrides,
|
||||
deriveBlacklistOverrides,
|
||||
getBlacklistEntries,
|
||||
getEffectiveBlacklistDomains,
|
||||
isUrlBlacklisted,
|
||||
normalizeBlacklistOverrides,
|
||||
parseBlacklistDomains
|
||||
} from './shared/blacklist.js';
|
||||
import { getAvatarForName, generateUsername, USERNAME_ADJECTIVES, USERNAME_NOUNS } from './shared/names.js';
|
||||
@@ -355,7 +362,7 @@ function setRoomRefreshCooldown() {
|
||||
async function init() {
|
||||
// Local-only by design — settings and room credentials never come from
|
||||
// storage.sync (only onboardingComplete + dismissedHints live there).
|
||||
const localData = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'chatKey', 'chatEnabled', 'chatNotifications', 'chatPosition', 'chatSize', 'chatStartMode', 'chatReactionDisplay', 'username', 'filterNoise', CUSTOM_BLACKLIST_STORAGE_KEY, 'autoSyncNextEpisode', 'sendTabTitle', 'mediaTitlePrivacyMode', 'titlePrivacyMode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings', 'activeTab', 'themeMode', 'themePalette']);
|
||||
const localData = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'chatKey', 'chatEnabled', 'chatNotifications', 'chatPosition', 'chatSize', 'chatStartMode', 'chatReactionDisplay', 'username', 'filterNoise', 'autoSyncNextEpisode', 'sendTabTitle', 'mediaTitlePrivacyMode', 'titlePrivacyMode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings', 'activeTab', 'themeMode', 'themePalette']);
|
||||
|
||||
let activeLang = localData.locale;
|
||||
if (!activeLang) {
|
||||
@@ -385,7 +392,7 @@ async function init() {
|
||||
elements.username.value = username;
|
||||
syncDevToolsVisibility();
|
||||
if (elements.filterNoise) elements.filterNoise.checked = localData.filterNoise !== false;
|
||||
renderBlacklistEditor(localData[CUSTOM_BLACKLIST_STORAGE_KEY]);
|
||||
readBlacklistOverrides().then(overrides => renderBlacklistEditor(overrides));
|
||||
if (elements.autoSyncNextEpisode) elements.autoSyncNextEpisode.checked = localData.autoSyncNextEpisode !== false;
|
||||
if (elements.chatEnabled) elements.chatEnabled.checked = localData.chatEnabled === true;
|
||||
if (elements.chatNotifications) elements.chatNotifications.checked = localData.chatNotifications !== false;
|
||||
@@ -1134,10 +1141,39 @@ function setBlacklistStatus(message = '', state = '') {
|
||||
elements.blacklistStatus.dataset.state = state;
|
||||
}
|
||||
|
||||
function renderBlacklistEditor(storedDomains, preserveStatus = false) {
|
||||
// Reads the delta, transparently migrating a legacy full-list snapshot so users
|
||||
// who saved before v3.1.0 start receiving newly shipped defaults again.
|
||||
async function readBlacklistOverrides() {
|
||||
const data = await chrome.storage.local.get([BLACKLIST_OVERRIDES_STORAGE_KEY, CUSTOM_BLACKLIST_STORAGE_KEY]);
|
||||
const stored = data[BLACKLIST_OVERRIDES_STORAGE_KEY];
|
||||
if (stored && typeof stored === 'object' && !Array.isArray(stored)) {
|
||||
return normalizeBlacklistOverrides(stored);
|
||||
}
|
||||
|
||||
const legacy = data[CUSTOM_BLACKLIST_STORAGE_KEY];
|
||||
if (Array.isArray(legacy)) {
|
||||
const migrated = deriveBlacklistOverrides(legacy);
|
||||
await chrome.storage.local.set({ [BLACKLIST_OVERRIDES_STORAGE_KEY]: migrated });
|
||||
await chrome.storage.local.remove(CUSTOM_BLACKLIST_STORAGE_KEY);
|
||||
return migrated;
|
||||
}
|
||||
|
||||
return createEmptyBlacklistOverrides();
|
||||
}
|
||||
|
||||
// Groups the editor into "yours" and "shipped" with comment headers. The
|
||||
// grouping is cosmetic: on save each line is classified by whether it is a
|
||||
// shipped default, not by the section it was typed under.
|
||||
function renderBlacklistEditor(overrides, preserveStatus = false) {
|
||||
if (!elements.blacklistDomains) return;
|
||||
const domains = getEffectiveBlacklistDomains(storedDomains);
|
||||
elements.blacklistDomains.value = domains.join('\n');
|
||||
const entries = getBlacklistEntries(overrides);
|
||||
const userDomains = entries.filter(e => e.source === BLACKLIST_SOURCE_USER).map(e => e.domain);
|
||||
const defaultDomains = entries.filter(e => e.source === BLACKLIST_SOURCE_DEFAULT).map(e => e.domain);
|
||||
|
||||
const lines = [`# ${getMessage('BLACKLIST_SECTION_USER')}`, ...userDomains, ''];
|
||||
lines.push(`# ${getMessage('BLACKLIST_SECTION_DEFAULT')}`, ...defaultDomains);
|
||||
|
||||
elements.blacklistDomains.value = lines.join('\n');
|
||||
if (!preserveStatus) setBlacklistStatus();
|
||||
}
|
||||
|
||||
@@ -1160,15 +1196,17 @@ async function saveBlacklistDomains() {
|
||||
return;
|
||||
}
|
||||
|
||||
await chrome.storage.local.set({ [CUSTOM_BLACKLIST_STORAGE_KEY]: domains });
|
||||
renderBlacklistEditor(domains);
|
||||
const previous = await readBlacklistOverrides();
|
||||
const overrides = deriveBlacklistOverrides(domains, previous);
|
||||
await chrome.storage.local.set({ [BLACKLIST_OVERRIDES_STORAGE_KEY]: overrides });
|
||||
renderBlacklistEditor(overrides);
|
||||
setBlacklistStatus(getMessage('BLACKLIST_STATUS_SAVED', { count: domains.length }), 'success');
|
||||
await populateTabs();
|
||||
}
|
||||
|
||||
async function resetBlacklistDomains() {
|
||||
await chrome.storage.local.remove(CUSTOM_BLACKLIST_STORAGE_KEY);
|
||||
renderBlacklistEditor();
|
||||
await chrome.storage.local.remove([BLACKLIST_OVERRIDES_STORAGE_KEY, CUSTOM_BLACKLIST_STORAGE_KEY]);
|
||||
renderBlacklistEditor(createEmptyBlacklistOverrides());
|
||||
setBlacklistStatus(getMessage('BLACKLIST_STATUS_DEFAULTS'), 'success');
|
||||
await populateTabs();
|
||||
}
|
||||
@@ -1177,8 +1215,8 @@ async function populateTabs(providedPeers = null, providedTargetTabId = null) {
|
||||
const token = {};
|
||||
populateTabsToken = token;
|
||||
|
||||
const data = await chrome.storage.local.get(['filterNoise', CUSTOM_BLACKLIST_STORAGE_KEY]);
|
||||
const blacklistDomains = getEffectiveBlacklistDomains(data[CUSTOM_BLACKLIST_STORAGE_KEY]);
|
||||
const data = await chrome.storage.local.get(['filterNoise']);
|
||||
const blacklistDomains = getEffectiveBlacklistDomains(await readBlacklistOverrides());
|
||||
const isFilterActive = data.filterNoise !== false;
|
||||
|
||||
let currentTargetTabId = providedTargetTabId;
|
||||
@@ -1652,8 +1690,8 @@ if (elements.audioSettingsLink) {
|
||||
}
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area !== 'local' || !changes[CUSTOM_BLACKLIST_STORAGE_KEY]) return;
|
||||
renderBlacklistEditor(changes[CUSTOM_BLACKLIST_STORAGE_KEY].newValue, true);
|
||||
if (area !== 'local' || !changes[BLACKLIST_OVERRIDES_STORAGE_KEY]) return;
|
||||
renderBlacklistEditor(normalizeBlacklistOverrides(changes[BLACKLIST_OVERRIDES_STORAGE_KEY].newValue), true);
|
||||
populateTabs();
|
||||
});
|
||||
|
||||
|
||||
@@ -6,10 +6,17 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
BLACKLIST_DOMAINS,
|
||||
BLACKLIST_OVERRIDES_STORAGE_KEY,
|
||||
BLACKLIST_SOURCE_DEFAULT,
|
||||
BLACKLIST_SOURCE_USER,
|
||||
CUSTOM_BLACKLIST_STORAGE_KEY,
|
||||
createEmptyBlacklistOverrides,
|
||||
deriveBlacklistOverrides,
|
||||
getBlacklistEntries,
|
||||
getEffectiveBlacklistDomains,
|
||||
isUrlBlacklisted,
|
||||
normalizeBlacklistDomain,
|
||||
normalizeBlacklistOverrides,
|
||||
parseBlacklistDomains
|
||||
} from '../shared/blacklist.js';
|
||||
|
||||
@@ -34,9 +41,92 @@ assert.equal(isUrlBlacklisted('https://mail.google.com/inbox', ['google.com']),
|
||||
assert.equal(isUrlBlacklisted('https://notgoogle.com/', ['google.com']), false, 'lookalike domains do not match');
|
||||
assert.equal(isUrlBlacklisted('not a url', ['example.com']), false, 'invalid URLs are ignored');
|
||||
|
||||
// --- Delta storage: shipped defaults keep flowing in after the user edits ---
|
||||
|
||||
assert.equal(BLACKLIST_OVERRIDES_STORAGE_KEY, 'blacklistOverrides');
|
||||
assert.deepEqual(createEmptyBlacklistOverrides(), { removedDefaults: [], addedDomains: [] });
|
||||
|
||||
// A user who removes two defaults and adds one of their own.
|
||||
const edited = BLACKLIST_DOMAINS
|
||||
.filter(domain => domain !== 'reddit.com' && domain !== 'imgur.com')
|
||||
.concat(['videos.example']);
|
||||
const overrides = deriveBlacklistOverrides(edited);
|
||||
assert.deepEqual(overrides.removedDefaults, ['reddit.com', 'imgur.com'], 'only the removed defaults are stored');
|
||||
assert.deepEqual(overrides.addedDomains, ['videos.example'], 'only the added domains are stored');
|
||||
|
||||
const effective = getEffectiveBlacklistDomains(overrides);
|
||||
assert.equal(effective.includes('reddit.com'), false, 'a removed default stays removed');
|
||||
assert.equal(effective.includes('videos.example'), true, 'an added domain stays added');
|
||||
|
||||
// The property that makes newly shipped defaults reach existing users: every
|
||||
// shipped domain the user did not explicitly remove is part of the result, so a
|
||||
// default added in a later version cannot be missing from a stored delta.
|
||||
const removedSet = new Set(overrides.removedDefaults);
|
||||
for (const domain of BLACKLIST_DOMAINS) {
|
||||
assert.equal(
|
||||
effective.includes(domain) || removedSet.has(domain),
|
||||
true,
|
||||
`shipped default ${domain} must be present unless explicitly removed`
|
||||
);
|
||||
}
|
||||
|
||||
// Legacy full-list snapshots migrate to the delta form.
|
||||
assert.deepEqual(
|
||||
deriveBlacklistOverrides(edited),
|
||||
normalizeBlacklistOverrides(overrides),
|
||||
'a legacy snapshot produces the same delta'
|
||||
);
|
||||
assert.deepEqual(getEffectiveBlacklistDomains(undefined), BLACKLIST_DOMAINS, 'no stored delta uses shipped defaults');
|
||||
assert.deepEqual(getEffectiveBlacklistDomains([]), [], 'a legacy empty snapshot still means no filtering');
|
||||
|
||||
// Re-adding a removed default clears the removal instead of stacking state.
|
||||
const readded = deriveBlacklistOverrides(effective.concat(['reddit.com']), overrides);
|
||||
assert.equal(readded.removedDefaults.includes('reddit.com'), false, 're-adding a default clears its removal');
|
||||
|
||||
// A domain the user added explicitly stays tagged as theirs even once the same
|
||||
// domain ships as a default, so dropping the default does not drop their entry.
|
||||
const stillUser = deriveBlacklistOverrides(['google.com'], { removedDefaults: [], addedDomains: ['google.com'] });
|
||||
assert.deepEqual(stillUser.addedDomains, ['google.com'], 'an explicit addition survives becoming a default');
|
||||
|
||||
// Contradictory stored state resolves in favour of the addition.
|
||||
assert.deepEqual(
|
||||
normalizeBlacklistOverrides({ removedDefaults: ['example.com'], addedDomains: ['example.com'] }),
|
||||
{ removedDefaults: [], addedDomains: ['example.com'] },
|
||||
'a domain cannot be removed and added at once'
|
||||
);
|
||||
assert.deepEqual(normalizeBlacklistOverrides('nonsense'), createEmptyBlacklistOverrides(), 'garbage storage falls back to defaults');
|
||||
|
||||
// Entries are tagged so the editor can show what came from where.
|
||||
const entries = getBlacklistEntries(overrides);
|
||||
assert.equal(entries.find(e => e.domain === 'videos.example').source, BLACKLIST_SOURCE_USER);
|
||||
assert.equal(entries.find(e => e.domain === 'google.com').source, BLACKLIST_SOURCE_DEFAULT);
|
||||
|
||||
// Comment lines are editor notes, not domains, and never count as invalid.
|
||||
const withComments = parseBlacklistDomains('# your entries\nvideos.example\n\n#shipped defaults\ngoogle.com');
|
||||
assert.deepEqual(withComments.domains, ['videos.example', 'google.com'], 'comment lines are skipped');
|
||||
assert.deepEqual(withComments.invalid, [], 'comment lines are not reported as invalid');
|
||||
|
||||
// Round trip through the grouped editor body: rendering with comment headers
|
||||
// and saving it again must not change the stored delta.
|
||||
const rendered = [
|
||||
'# Your entries',
|
||||
...entries.filter(e => e.source === BLACKLIST_SOURCE_USER).map(e => e.domain),
|
||||
'',
|
||||
'# Shipped defaults',
|
||||
...entries.filter(e => e.source === BLACKLIST_SOURCE_DEFAULT).map(e => e.domain)
|
||||
].join('\n');
|
||||
const roundTripped = parseBlacklistDomains(rendered);
|
||||
assert.deepEqual(roundTripped.invalid, [], 'the rendered editor body contains no invalid entries');
|
||||
assert.deepEqual(
|
||||
deriveBlacklistOverrides(roundTripped.domains, overrides),
|
||||
normalizeBlacklistOverrides(overrides),
|
||||
'render then save leaves the delta unchanged'
|
||||
);
|
||||
|
||||
const popupSource = fs.readFileSync(path.join(repoRoot, 'extension/popup.js'), 'utf8');
|
||||
assert.match(popupSource, /chrome\.storage\.local\.set\(\{ \[CUSTOM_BLACKLIST_STORAGE_KEY\]: domains \}\)/, 'custom list is saved locally');
|
||||
assert.doesNotMatch(popupSource, /chrome\.storage\.sync\.set\(\{ \[CUSTOM_BLACKLIST_STORAGE_KEY\]/, 'custom list is never synced');
|
||||
assert.match(popupSource, /chrome\.storage\.local\.set\(\{ \[BLACKLIST_OVERRIDES_STORAGE_KEY\]: overrides \}\)/, 'the delta is saved locally');
|
||||
assert.doesNotMatch(popupSource, /chrome\.storage\.sync\.set\(\{ \[(?:BLACKLIST_OVERRIDES|CUSTOM_BLACKLIST)_STORAGE_KEY\]/, 'the list is never synced');
|
||||
assert.match(popupSource, /chrome\.storage\.local\.remove\(CUSTOM_BLACKLIST_STORAGE_KEY\)/, 'the legacy snapshot is cleaned up after migration');
|
||||
assert.match(popupSource, /isUrlBlacklisted\(tab\.url, blacklistDomains\)/, 'tab filtering uses the effective custom list');
|
||||
|
||||
// A broad parent domain must not hide a host with a dedicated player path,
|
||||
|
||||
+96
-3
@@ -188,9 +188,29 @@ export const BLACKLIST_SUFFIX_EXCEPTIONS = [
|
||||
'drive.google.com'
|
||||
];
|
||||
|
||||
/**
|
||||
* Legacy key (<= v3.0.x): held a full snapshot of the effective list, which
|
||||
* froze the shipped defaults at the moment the user first saved. Read once for
|
||||
* migration, then replaced by BLACKLIST_OVERRIDES_STORAGE_KEY.
|
||||
*/
|
||||
export const CUSTOM_BLACKLIST_STORAGE_KEY = 'customBlacklistDomains';
|
||||
|
||||
/**
|
||||
* Current key. Stores only the delta against the shipped list:
|
||||
* { removedDefaults: string[], addedDomains: string[] }
|
||||
* so newly shipped defaults reach existing users without touching the
|
||||
* defaults they removed or the domains they added themselves.
|
||||
*/
|
||||
export const BLACKLIST_OVERRIDES_STORAGE_KEY = 'blacklistOverrides';
|
||||
|
||||
export const MAX_BLACKLIST_DOMAINS = 500;
|
||||
|
||||
export const BLACKLIST_SOURCE_DEFAULT = 'default';
|
||||
export const BLACKLIST_SOURCE_USER = 'user';
|
||||
|
||||
/** Lines starting with this marker are editor notes, not domains. */
|
||||
export const BLACKLIST_COMMENT_PREFIX = '#';
|
||||
|
||||
/**
|
||||
* Normalize a user-entered domain or URL to a hostname.
|
||||
* Returns null when the value cannot safely be used as a hostname filter.
|
||||
@@ -226,6 +246,7 @@ export function parseBlacklistDomains(value) {
|
||||
const seen = new Set();
|
||||
|
||||
for (const entry of entries) {
|
||||
if (typeof entry === 'string' && entry.trim().startsWith(BLACKLIST_COMMENT_PREFIX)) continue;
|
||||
const normalized = normalizeBlacklistDomain(entry);
|
||||
if (normalized === '') continue;
|
||||
if (normalized === null) {
|
||||
@@ -241,9 +262,81 @@ export function parseBlacklistDomains(value) {
|
||||
return { domains, invalid };
|
||||
}
|
||||
|
||||
export function getEffectiveBlacklistDomains(storedDomains) {
|
||||
if (!Array.isArray(storedDomains)) return [...BLACKLIST_DOMAINS];
|
||||
return parseBlacklistDomains(storedDomains).domains.slice(0, MAX_BLACKLIST_DOMAINS);
|
||||
export function createEmptyBlacklistOverrides() {
|
||||
return { removedDefaults: [], addedDomains: [] };
|
||||
}
|
||||
|
||||
export function normalizeBlacklistOverrides(raw) {
|
||||
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return createEmptyBlacklistOverrides();
|
||||
|
||||
const addedDomains = parseBlacklistDomains(Array.isArray(raw.addedDomains) ? raw.addedDomains : []).domains;
|
||||
const added = new Set(addedDomains);
|
||||
// A domain cannot be removed and added at the same time; the addition wins
|
||||
// so a re-added default is never filtered out by a stale removal entry.
|
||||
const removedDefaults = parseBlacklistDomains(Array.isArray(raw.removedDefaults) ? raw.removedDefaults : [])
|
||||
.domains.filter(domain => !added.has(domain));
|
||||
|
||||
return { removedDefaults, addedDomains };
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a fully edited list back into a delta.
|
||||
*
|
||||
* `previous` keeps an explicit user addition tagged as such even after the same
|
||||
* domain later ships as a default, so dropping it from the defaults does not
|
||||
* silently drop it from that user's list.
|
||||
*/
|
||||
export function deriveBlacklistOverrides(submittedDomains, previous = null) {
|
||||
const { domains } = parseBlacklistDomains(submittedDomains);
|
||||
const kept = new Set(domains);
|
||||
const shipped = new Set(BLACKLIST_DOMAINS);
|
||||
const previouslyAdded = new Set(normalizeBlacklistOverrides(previous).addedDomains);
|
||||
|
||||
return {
|
||||
removedDefaults: BLACKLIST_DOMAINS.filter(domain => !kept.has(domain)),
|
||||
addedDomains: domains.filter(domain => !shipped.has(domain) || previouslyAdded.has(domain))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts the current delta object, a legacy full-list array, or nothing.
|
||||
*/
|
||||
export function toBlacklistOverrides(stored) {
|
||||
if (Array.isArray(stored)) return deriveBlacklistOverrides(stored);
|
||||
if (stored && typeof stored === 'object') return normalizeBlacklistOverrides(stored);
|
||||
return createEmptyBlacklistOverrides();
|
||||
}
|
||||
|
||||
export function getEffectiveBlacklistDomains(stored) {
|
||||
const { removedDefaults, addedDomains } = toBlacklistOverrides(stored);
|
||||
const removed = new Set(removedDefaults);
|
||||
const domains = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const domain of BLACKLIST_DOMAINS) {
|
||||
if (removed.has(domain) || seen.has(domain)) continue;
|
||||
seen.add(domain);
|
||||
domains.push(domain);
|
||||
}
|
||||
for (const domain of addedDomains) {
|
||||
if (seen.has(domain)) continue;
|
||||
seen.add(domain);
|
||||
domains.push(domain);
|
||||
}
|
||||
|
||||
return domains.slice(0, MAX_BLACKLIST_DOMAINS);
|
||||
}
|
||||
|
||||
/**
|
||||
* The effective list tagged by origin, for an editor that shows the user which
|
||||
* entries are theirs and which arrive with the extension.
|
||||
*/
|
||||
export function getBlacklistEntries(stored) {
|
||||
const added = new Set(toBlacklistOverrides(stored).addedDomains);
|
||||
return getEffectiveBlacklistDomains(stored).map(domain => ({
|
||||
domain,
|
||||
source: added.has(domain) ? BLACKLIST_SOURCE_USER : BLACKLIST_SOURCE_DEFAULT
|
||||
}));
|
||||
}
|
||||
|
||||
export function isUrlBlacklisted(rawUrl, domains = BLACKLIST_DOMAINS) {
|
||||
|
||||
Reference in New Issue
Block a user