Add extension sources with translations

This commit is contained in:
Ryze 2023-11-15 23:30:46 +03:00
parent 99e4f14b06
commit 9081823151
Signed by: ryze
GPG key ID: 9B296C5CEAEAAAC1
27 changed files with 1924 additions and 0 deletions

View file

@ -0,0 +1,158 @@
{
"extensionName": {
"message": "YouTube Customizer"
},
"extensionDescription": {
"message": "Changes layout of YouTube and adds QoL features."
},
"mainSection": {
"message": "Main"
},
"homeOption": {
"message": "Home"
},
"shortsOption": {
"message": "Shorts"
},
"subscriptionsOption": {
"message": "Subscriptions"
},
"libraryOption": {
"message": "Library (You)"
},
"channelOption": {
"message": "Your channel"
},
"historyOption": {
"message": "History"
},
"videosOption": {
"message": "Your videos"
},
"watchLaterOption": {
"message": "Watch Later"
},
"downloadsOption": {
"message": "Downloads"
},
"likedOption": {
"message": "Liked videos"
},
"playlistsOption": {
"message": "Playlists"
},
"channelsOption": {
"message": "Browse channels"
},
"exploreSection": {
"message": "Explore"
},
"trendingOption": {
"message": "Trending"
},
"musicOption": {
"message": "Music"
},
"filmsOption": {
"message": "Films"
},
"liveOption": {
"message": "Live"
},
"gamingOption": {
"message": "Gaming"
},
"newsOption": {
"message": "News"
},
"sportsOption": {
"message": "Sports"
},
"learningOption": {
"message": "Learning"
},
"fashionOption": {
"message": "Fashion & Beauty"
},
"ytMoreSection": {
"message": "More from YouTube"
},
"ytPremiumOption": {
"message": "YouTube Premium"
},
"ytStudioOption": {
"message": "YouTube Studio"
},
"ytMusicOption": {
"message": "YouTube Music"
},
"ytKidsOption": {
"message": "YouTube Kids"
},
"miscSection": {
"message": "Miscellaneous"
},
"settingsOption": {
"message": "Settings"
},
"reportOption": {
"message": "Report history"
},
"helpOption": {
"message": "Help"
},
"feedbackOption": {
"message": "Send feedback"
},
"topbarSection": {
"message": "Topbar"
},
"topbarLogoOption": {
"message": "Logo"
},
"topbarSearchOption": {
"message": "Search box"
},
"topbarVoiceSearchOption": {
"message": "Voice search"
},
"topbarCreateButtonOption": {
"message": "Create button"
},
"topbarNotificationsButtonOption": {
"message": "Notifications button"
},
"topbarAccountButtonOption": {
"message": "Account button"
},
"topbarSignInButtonOption": {
"message": "Sign-in button"
},
"otherSection": {
"message": "Other"
},
"disableSignInPromoOption": {
"message": "Disable sign-in promo"
},
"disableSubscriptionsOption": {
"message": "Disable sidebar subscriptions"
},
"disableFooterOption": {
"message": "Disable sidebar footer"
},
"startGuideClosedOption": {
"message": "Start sidebar closed"
},
"redirectHomeOption": {
"message": "Redirect Home to Subscriptions"
},
"ff2mpvEnabledOption": {
"message": "Launch ff2mpv on video click"
},
"enableAllButton": {
"message": "Enable all"
},
"disableAllButton": {
"message": "Disable all"
}
}

View file

@ -0,0 +1,158 @@
{
"extensionName": {
"message": "Personalizador de YouTube - YouTube Customizer"
},
"extensionDescription": {
"message": "Modifica el Layout de YouTube e implementa características de calidad de vida."
},
"mainSection": {
"message": "Principal"
},
"homeOption": {
"message": "Inicio"
},
"shortsOption": {
"message": "Shorts"
},
"subscriptionsOption": {
"message": "Suscripciones"
},
"libraryOption": {
"message": "Mi biblioteca (Tú)"
},
"channelOption": {
"message": "Tu canal"
},
"historyOption": {
"message": "Historial"
},
"videosOption": {
"message": "Mis vídeos"
},
"watchLaterOption": {
"message": "Ver más tarde"
},
"downloadsOption": {
"message": "Descargas"
},
"likedOption": {
"message": "Vídeos que me gustan"
},
"playlistsOption": {
"message": "Listas de reproducción"
},
"channelsOption": {
"message": "Explorar canales"
},
"exploreSection": {
"message": "Explorar"
},
"trendingOption": {
"message": "Tendencias"
},
"musicOption": {
"message": "Música"
},
"filmsOption": {
"message": "Películas"
},
"liveOption": {
"message": "En directo"
},
"gamingOption": {
"message": "Videojuegos"
},
"newsOption": {
"message": "Noticias"
},
"sportsOption": {
"message": "Deportes"
},
"learningOption": {
"message": "Aprendizaje"
},
"fashionOption": {
"message": "Moda y belleza"
},
"ytMoreSection": {
"message": "Más de YouTube"
},
"ytPremiumOption": {
"message": "YouTube Premium"
},
"ytStudioOption": {
"message": "YouTube Studio"
},
"ytMusicOption": {
"message": "YouTube Music"
},
"ytKidsOption": {
"message": "YouTube Kids"
},
"miscSection": {
"message": "Misceláneo"
},
"settingsOption": {
"message": "Configuración"
},
"reportOption": {
"message": "Historial de denuncias"
},
"helpOption": {
"message": "Ayuda"
},
"feedbackOption": {
"message": "Enviar sugerencias"
},
"topbarSection": {
"message": "Barra Superior"
},
"topbarLogoOption": {
"message": "Logotipo"
},
"topbarSearchOption": {
"message": "Buscador"
},
"topbarVoiceSearchOption": {
"message": "Búsqueda por voz"
},
"topbarCreateButtonOption": {
"message": "Crear Botón"
},
"topbarNotificationsButtonOption": {
"message": "Botón de Notificaciones"
},
"topbarAccountButtonOption": {
"message": "Botón de Perfíl"
},
"topbarSignInButtonOption": {
"message": "Botón de Inicio de Sesión"
},
"otherSection": {
"message": "Otro"
},
"disableSignInPromoOption": {
"message": "Deshabilitar promo de inicio de sesión"
},
"disableSubscriptionsOption": {
"message": "Deshabilitar panel lateral de suscripciones"
},
"disableFooterOption": {
"message": "Deshabilitar pie de página del panel lateral"
},
"startGuideClosedOption": {
"message": "Panel lateral de Comienzo Cerrado"
},
"redirectHomeOption": {
"message": "Redireccionar menú a suscripciones"
},
"ff2mpvEnabledOption": {
"message": "Reproducir vídeos usando ff2mpv"
},
"enableAllButton": {
"message": "Habilitar todo"
},
"disableAllButton": {
"message": "Deshabilitar todo"
}
}

View file

@ -0,0 +1,158 @@
{
"extensionName": {
"message": "Personnalisateur YouTube - YouTube Customizer"
},
"extensionDescription": {
"message": "Change l'apparence de YouTube et ajoute des fonctionnalités de qualité de vie."
},
"mainSection": {
"message": "Principal"
},
"homeOption": {
"message": "Accueil"
},
"shortsOption": {
"message": "Shorts"
},
"subscriptionsOption": {
"message": "Abonnements"
},
"libraryOption": {
"message": "Bibliothèque (Vous)"
},
"channelOption": {
"message": "Votre chaîne"
},
"historyOption": {
"message": "Historique"
},
"videosOption": {
"message": "Vos vidéos"
},
"watchLaterOption": {
"message": "À regarder plus tard"
},
"downloadsOption": {
"message": "Téléchargements"
},
"likedOption": {
"message": "Vidéos \"J'aime\""
},
"playlistsOption": {
"message": "Playlists"
},
"channelsOption": {
"message": "Chaînes"
},
"exploreSection": {
"message": "Explorer"
},
"trendingOption": {
"message": "Tendancies"
},
"musicOption": {
"message": "Musique"
},
"filmsOption": {
"message": "Films"
},
"liveOption": {
"message": "Direct"
},
"gamingOption": {
"message": "Jeux vidéo"
},
"newsOption": {
"message": "Actualités"
},
"sportsOption": {
"message": "Sports"
},
"learningOption": {
"message": "Savoirs & Cultures"
},
"fashionOption": {
"message": "Mode et beauté"
},
"ytMoreSection": {
"message": "Autres contenus YouTube"
},
"ytPremiumOption": {
"message": "YouTube Premium"
},
"ytStudioOption": {
"message": "YouTube Studio"
},
"ytMusicOption": {
"message": "YouTube Music"
},
"ytKidsOption": {
"message": "YouTube Kids"
},
"miscSection": {
"message": "Divers"
},
"settingsOption": {
"message": "Paramètres"
},
"reportOption": {
"message": "Historique des signalements"
},
"helpOption": {
"message": "Aide"
},
"feedbackOption": {
"message": "Envoyer des commentaires"
},
"otherSection": {
"message": "Autres"
},
"topbarSection": {
"message": "En-tête de navigation"
},
"topbarLogoOption": {
"message": "Logo"
},
"topbarSearchOption": {
"message": "Boîte de recherche"
},
"topbarVoiceSearchOption": {
"message": "Recherche vocale"
},
"topbarCreateButtonOption": {
"message": "Bouton Créer"
},
"topbarNotificationsButtonOption": {
"message": "Bouton Notifications"
},
"topbarAccountButtonOption": {
"message": "Bouton Compte"
},
"topbarSignInButtonOption": {
"message": "Bouton Connexion"
},
"disableSignInPromoOption": {
"message": "Désactiver l'invite de connexion"
},
"disableSubscriptionsOption": {
"message": "Désactiver les abonnements dans la barre de navigation"
},
"disableFooterOption": {
"message": "Désactiver le pied de page de la barre de navigation"
},
"startGuideClosedOption": {
"message": "Démarrer avec la bare de navigation fermée"
},
"redirectHomeOption": {
"message": "Rediriger Accueil vers Abonnements"
},
"ff2mpvEnabledOption": {
"message": "Ouvrir les vidéos avec ff2mpv"
},
"enableAllButton": {
"message": "Tout activer"
},
"disableAllButton": {
"message": "Tout désactiver"
}
}

View file

@ -0,0 +1,158 @@
{
"extensionName": {
"message": "YouTube Customizer - YouTube személyreszabó"
},
"extensionDescription": {
"message": "Eltávolítja a fölösleges gombokat a YouTube oldalsávjáról"
},
"mainSection": {
"message": "Fő"
},
"homeOption": {
"message": "Kezdőlap"
},
"shortsOption": {
"message": "Shorts"
},
"subscriptionsOption": {
"message": "Feliratkozások"
},
"libraryOption": {
"message": "Könyvtár (Te)"
},
"channelOption": {
"message": "Saját csatorna"
},
"historyOption": {
"message": "Előzmények"
},
"videosOption": {
"message": "Videóid"
},
"watchLaterOption": {
"message": "Megnézendő videók"
},
"downloadsOption": {
"message": "Letöltések"
},
"likedOption": {
"message": "Kedvelt videók"
},
"playlistsOption": {
"message": "Lejátszási listák"
},
"channelsOption": {
"message": "Böngészés"
},
"exploreSection": {
"message": "Felfedezés"
},
"trendingOption": {
"message": "Felkapott"
},
"musicOption": {
"message": "Zene"
},
"filmsOption": {
"message": "Filmek"
},
"liveOption": {
"message": "Élő"
},
"gamingOption": {
"message": "Játék"
},
"newsOption": {
"message": "Hírek"
},
"sportsOption": {
"message": "Sports"
},
"learningOption": {
"message": "Tanulás"
},
"fashionOption": {
"message": "Divat és szépségápolás"
},
"ytMoreSection": {
"message": "Továbbiak a YouTube-ról"
},
"ytPremiumOption": {
"message": "YouTube Premium"
},
"ytStudioOption": {
"message": "YouTube Studio"
},
"ytMusicOption": {
"message": "YouTube Music"
},
"ytKidsOption": {
"message": "YouTube Kids"
},
"miscSection": {
"message": "Vegyes"
},
"settingsOption": {
"message": "Beállítások"
},
"reportOption": {
"message": "Bejelentési előzmények"
},
"helpOption": {
"message": "Súgó"
},
"feedbackOption": {
"message": "Visszajelzés küldése"
},
"topbarSection": {
"message": "Fejléc"
},
"topbarLogoOption": {
"message": "Logó"
},
"topbarSearchOption": {
"message": "Keresőmező"
},
"topbarVoiceSearchOption": {
"message": "Keresés hanggal"
},
"topbarCreateButtonOption": {
"message": "Létrehozás gomb"
},
"topbarNotificationsButtonOption": {
"message": "Értesítések gomb"
},
"topbarAccountButtonOption": {
"message": "Fiók gomb"
},
"topbarSignInButtonOption": {
"message": "Bejelentkezés gomb"
},
"otherSection": {
"message": "Egyéb"
},
"disableSignInPromoOption": {
"message": "Bejelentkeztető promó eltávolítása"
},
"disableSubscriptionsOption": {
"message": "Feliratkozások eltávolítása az oldalsávról"
},
"disableFooterOption": {
"message": "Lábjegyzet eltávolítása az oldalsávról"
},
"startGuideClosedOption": {
"message": "Indítás zárt oldalsávval"
},
"redirectHomeOption": {
"message": "A kezdőlap átirányítása a feliratkozásokhoz"
},
"ff2mpvEnabledOption": {
"message": "Videók megnyitása ff2mpv-vel"
},
"enableAllButton": {
"message": "Minden engedélyezése"
},
"disableAllButton": {
"message": "Minden tiltása"
}
}

View file

@ -0,0 +1,158 @@
{
"extensionName": {
"message": "YouTube Customizer - Редактор YouTube"
},
"extensionDescription": {
"message": "Изменяет дизайн YouTube и добавляет полезные функции."
},
"mainSection": {
"message": "Главное"
},
"homeOption": {
"message": "Главная"
},
"shortsOption": {
"message": "Shorts"
},
"subscriptionsOption": {
"message": "Подписки"
},
"libraryOption": {
"message": "Библиотека (Вы)"
},
"channelOption": {
"message": "Мой канал"
},
"historyOption": {
"message": "История"
},
"videosOption": {
"message": "Ваш канал"
},
"watchLaterOption": {
"message": "Смотреть позже"
},
"downloadsOption": {
"message": "Скачанные"
},
"likedOption": {
"message": "Понравившиеся"
},
"playlistsOption": {
"message": "Плейлисты"
},
"channelsOption": {
"message": "Каталог каналов"
},
"exploreSection": {
"message": "Навигатор"
},
"trendingOption": {
"message": "В тренде"
},
"musicOption": {
"message": "Музыка"
},
"filmsOption": {
"message": "Фильмы"
},
"liveOption": {
"message": "Трансляции"
},
"gamingOption": {
"message": "Видеоигры"
},
"newsOption": {
"message": "Новости"
},
"sportsOption": {
"message": "Спорт"
},
"learningOption": {
"message": "Обучение"
},
"fashionOption": {
"message": "Мода и красота"
},
"ytMoreSection": {
"message": "Другие возможности"
},
"ytPremiumOption": {
"message": "YouTube Premium"
},
"ytStudioOption": {
"message": "Творческая студия YouTube"
},
"ytMusicOption": {
"message": "YouTube Music"
},
"ytKidsOption": {
"message": "YouTube Детям"
},
"miscSection": {
"message": "Дополнительное"
},
"settingsOption": {
"message": "Настройки"
},
"reportOption": {
"message": "Жалобы"
},
"helpOption": {
"message": "Справка"
},
"feedbackOption": {
"message": "Отправить отзыв"
},
"topbarSection": {
"message": "Верхняя панель"
},
"topbarLogoOption": {
"message": "Логотип"
},
"topbarSearchOption": {
"message": "Строка поиска"
},
"topbarVoiceSearchOption": {
"message": "Голосовой поиск"
},
"topbarCreateButtonOption": {
"message": "Кнопка \"Cоздать\""
},
"topbarNotificationsButtonOption": {
"message": "Кнопка уведомлений"
},
"topbarAccountButtonOption": {
"message": "Кнопка аккаунта"
},
"topbarSignInButtonOption": {
"message": "Кнопка входа"
},
"otherSection": {
"message": "Другое"
},
"disableSignInPromoOption": {
"message": "Убрать промо-панель входа"
},
"disableSubscriptionsOption": {
"message": "Убрать боковую панель подписок"
},
"disableFooterOption": {
"message": "Убрать футер боковой панели"
},
"startGuideClosedOption": {
"message": "Закрыть боковую панель"
},
"redirectHomeOption": {
"message": "Открывать подписки вместо главной страницы"
},
"ff2mpvEnabledOption": {
"message": "Открытывать видео в ff2mpv"
},
"enableAllButton": {
"message": "Включить всё"
},
"disableAllButton": {
"message": "Выключить всё"
}
}

3
extension/icon.svg Normal file
View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="7mm" height="7mm" version="1.1" viewBox="0 0 7 7" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><circle transform="scale(-1,1)" cx="-3.5" cy="3.5" r="3.5" fill="#0d1117"/><path d="m2.3917 1.4432 3.994e-4 4.1135 3.4461-2.0573z" fill="none" stroke="#3f1d9e" stroke-linejoin="round" stroke-width=".7"/><path d="m5.8681 4.8672a2.7344 2.7344 0 0 1-3.0758 1.274 2.7344 2.7344 0 0 1-2.0267-2.6412 2.7344 2.7344 0 0 1 2.0267-2.6412 2.7344 2.7344 0 0 1 3.0758 1.274" fill="none" stroke="#3f1d9e" stroke-linecap="round" stroke-width=".7"/></svg>

After

Width:  |  Height:  |  Size: 655 B

53
extension/manifest.json Normal file
View file

@ -0,0 +1,53 @@
{
"manifest_version": 2,
"name": "__MSG_extensionName__",
"description": "__MSG_extensionDescription__",
"default_locale": "en",
"version": "1.0.0",
"developer": {
"name": "Ryze",
"url": "https://github.com/ryze312"
},
"icons": {
"16": "icon.svg",
"32": "icon.svg",
"64": "icon.svg",
"128": "icon.svg",
"256": "icon.svg",
"512": "icon.svg",
"1024": "icon.svg"
},
"options_ui": {
"page": "settings/settings.html",
"open_in_tab": true
},
"browser_specific_settings": {
"gecko": {
"id": "{841bfda8-83e6-11ee-82a9-4bf1ae962c91}"
}
},
"background": {
"scripts": ["scripts/background/config.js"],
"type": "module",
"persistent": false
},
"content_scripts": [
{
"matches": ["*://*.youtube.com/*"],
"js": ["scripts/content/ytCustomizer.js"],
"run_at": "document_end",
"all_frames": false
}
],
"permissions": [
"storage",
"nativeMessaging"
]
}

View file

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>YouTube Customizer</title>
<link rel="icon" type="image/x-icon" href="../icon.svg">
<link rel="stylesheet" type="text/css" href="style.css">
<script src="settings.js" defer></script>
</head>
<body>
<div id="options">
<config-section msg="mainSection">
<config-option msg="homeOption" key="WHAT_TO_WATCH"></config-option>
<config-option msg="shortsOption" key="TAB_SHORTS"></config-option>
<config-option msg="subscriptionsOption" key="SUBSCRIPTIONS"></config-option>
<config-option msg="libraryOption" key="VIDEO_LIBRARY_WHITE"></config-option>
<config-option msg="channelOption" key="ACCOUNT_BOX"></config-option>
<config-option msg="historyOption" key="WATCH_HISTORY"></config-option>
<config-option msg="videosOption" key="MY_VIDEOS"></config-option>
<config-option msg="watchLaterOption" key="WATCH_LATER"></config-option>
<config-option msg="downloadsOption" key="OFFLINE_DOWNLOAD"></config-option>
<config-option msg="likedOption" key="LIKES_PLAYLIST"></config-option>
<config-option msg="playlistsOption" key="PLAYLISTS"></config-option>
<config-option msg="channelsOption" key="ADD_CIRCLE"></config-option>
</config-section>
<config-section msg="exploreSection">
<config-option msg="trendingOption" key="TRENDING"></config-option>
<config-option msg="musicOption" key="MUSIC"></config-option>
<config-option msg="filmsOption" key="CLAPPERBOARD"></config-option>
<config-option msg="liveOption" key="LIVE"></config-option>
<config-option msg="gamingOption" key="GAMING_LOGO"></config-option>
<config-option msg="newsOption" key="NEWS"></config-option>
<config-option msg="sportsOption" key="TROPHY"></config-option>
<config-option msg="learningOption" key="COURSE"></config-option>
<config-option msg="fashionOption" key="FASHION_LOGO"></config-option>
</config-section>
<config-section msg="ytMoreSection">
<config-option msg="ytPremiumOption" key="YOUTUBE_RED_LOGO"></config-option>
<config-option msg="ytStudioOption" key="CREATOR_STUDIO_RED_LOGO"></config-option>
<config-option msg="ytMusicOption" key="YOUTUBE_MUSIC"></config-option>
<config-option msg="ytKidsOption" key="YOUTUBE_KIDS_ROUND"></config-option>
</config-section>
<config-section msg="miscSection">
<config-option msg="settingsOption" key="SETTINGS"></config-option>
<config-option msg="reportOption" key="FLAG"></config-option>
<config-option msg="helpOption" key="HELP"></config-option>
<config-option msg="feedbackOption" key="FEEDBACK"></config-option>
</config-section>
<config-section msg="topbarSection">
<config-option msg="topbarLogoOption" key="topbarLogo"></config-option>
<config-option msg="topbarSearchOption" key="topbarSearch"></config-option>
<config-option msg="topbarVoiceSearchOption" key="topbarVoiceSearchButton"></config-option>
<config-option msg="topbarCreateButtonOption" key="topbarCreateButton"></config-option>
<config-option msg="topbarNotificationsButtonOption" key="topbarNotificationsButton"></config-option>
<config-option msg="topbarAccountButtonOption" key="topbarAccountButton"></config-option>
<config-option msg="topbarSignInButtonOption" key="topbarSignInButton"></config-option>
</config-section>
<config-section msg="otherSection">
<config-option msg="disableSignInPromoOption" key="disableSigninPromo"></config-option>
<config-option msg="disableSubscriptionsOption" key="disableSubscriptions"></config-option>
<config-option msg="disableFooterOption" key="disableFooter"></config-option>
<config-option msg="startGuideClosedOption" key="startGuideClosed"></config-option>
<config-option msg="redirectHomeOption" key="redirectHome"></config-option>
<config-option msg="ff2mpvEnabledOption" key="ff2mpvEnabled"></config-option>
</config-section>
</div>
</body>
</html>

View file

@ -0,0 +1,97 @@
:root {
--background-color: #160F1D;
--config-section-background-color: #6241BD;
--config-button-disabled: #27194E;
--config-button-enabled: #3F1D9E;
--shadow: 4px 4px 10px #2C0633;
--glow: 10px 10px 10px #2C0633;
}
html, body {
margin: 0;
}
body {
background-color: var(--background-color);
color: #EEEEEE;
font-size: 1.2vw;
font-family: "Open Sans", "Roboto", "Arial", "sans-serif";
}
#options {
display: grid;
grid-template-columns: repeat(auto-fit, 20em);
gap: 2vh;
justify-content: center;
width: 80vw;
margin: auto;
padding: 30px;
border-radius: 2vh;
}
config-section {
display: flex;
flex-direction: column;
text-align: center;
padding: 20px;
border-radius: 3vh;
background: var(--config-section-background-color);
box-shadow: var(--glow);
}
config-option {
margin: 2px;
padding: 10px;
border-radius: 1vh;
background-color: var(--config-button-disabled);
cursor: pointer;
transition: ease-out 0.2s;
}
config-option:hover {
transform: translate(-4px, -4px);
box-shadow: var(--shadow);
}
.config-option-enabled {
background-color: var(--config-button-enabled);
}
.config-heading {
margin: 0;
margin-bottom: 10px;
}
.config-buttons {
display: flex;
margin-top: auto;
padding-top: 10px;
}
.config-buttons button {
margin-left: auto;
margin-right: auto;
padding: 10px;
border: none;
border-radius: 1vh;
background-color: var(--config-button-enabled);
color: inherit;
font-size: inherit;
cursor: inherit;
transition: ease-out 0.1s;
}
.config-buttons button:hover {
transform: translate(-4px, -4px);
box-shadow: var(--shadow);
}

View file

@ -0,0 +1,111 @@
declare global {
interface Window {
ytCustomizerConfig: Config
}
}
export type Config = {
[key: string]: boolean,
WHAT_TO_WATCH: boolean,
TAB_SHORTS: boolean,
SUBSCRIPTIONS: boolean,
VIDEO_LIBRARY_WHITE: boolean,
ACCOUNT_BOX: boolean,
WATCH_HISTORY: boolean,
MY_VIDEOS: boolean,
WATCH_LATER: boolean,
OFFLINE_DOWNLOAD: boolean,
LIKES_PLAYLIST: boolean,
PLAYLISTS: boolean,
ADD_CIRCLE: boolean,
TRENDING: boolean,
MUSIC: boolean,
CLAPPERBOARD: boolean,
LIVE: boolean,
GAMING_LOGO: boolean,
NEWS: boolean,
TROPHY: boolean,
COURSE: boolean,
FASHION_LOGO: boolean,
YOUTUBE_RED_LOGO: boolean,
CREATOR_STUDIO_RED_LOGO: boolean,
YOUTUBE_MUSIC: boolean,
YOUTUBE_KIDS_ROUND: boolean,
SETTINGS: boolean,
FLAG: boolean,
HELP: boolean,
FEEDBACK: boolean,
topbarLogo: boolean,
topbarSearch: boolean,
topbarVoiceSearchButton: boolean,
topbarCreateButton: boolean,
topbarNotificationsButton: boolean,
topbarAccountButton: boolean,
topbarSignInButton: boolean,
disableSigninPromo: boolean,
disableSubscriptions: boolean,
disableFooter: boolean,
startGuideClosed: boolean,
redirectHome: boolean
ff2mpvEnabled: boolean
}
const defaultConfig: Config = {
WHAT_TO_WATCH: true,
TAB_SHORTS: true,
SUBSCRIPTIONS: true,
VIDEO_LIBRARY_WHITE: true,
ACCOUNT_BOX: true,
WATCH_HISTORY: true,
MY_VIDEOS: true,
WATCH_LATER: true,
OFFLINE_DOWNLOAD: true,
LIKES_PLAYLIST: true,
PLAYLISTS: true,
ADD_CIRCLE: true,
TRENDING: true,
MUSIC: true,
CLAPPERBOARD: true,
LIVE: true,
GAMING_LOGO: true,
NEWS: true,
TROPHY: true,
COURSE: true,
FASHION_LOGO: true,
YOUTUBE_RED_LOGO: true,
CREATOR_STUDIO_RED_LOGO: true,
YOUTUBE_MUSIC: true,
YOUTUBE_KIDS_ROUND: true,
SETTINGS: true,
FLAG: true,
HELP: true,
FEEDBACK: true,
topbarLogo: true,
topbarSearch: true,
topbarVoiceSearchButton: true,
topbarCreateButton: true,
topbarNotificationsButton: true,
topbarAccountButton: true,
topbarSignInButton: true,
disableSigninPromo: false,
disableSubscriptions: false,
disableFooter: false,
startGuideClosed: false,
redirectHome: false,
ff2mpvEnabled: false
}
export let config: Config;
browser.runtime.onInstalled.addListener((event) => {
if (event.reason === "install") {
browser.storage.sync.set(defaultConfig);
}
});
browser.runtime.onMessage.addListener((videoId) => {
browser.runtime.sendNativeMessage("ff2mpv", {url: `https://youtu.be/${videoId}`});
});
browser.storage.sync.get()
.then((installedConfig) => config = installedConfig as Config);

View file

@ -0,0 +1,26 @@
function ff2mpv(videoId: string) {
browser.runtime.sendMessage(videoId);
}
// Firefox doesn't support ExecutionWorld.MAIN yet
// So just inject manually
// See: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld
async function injectScript() {
const config = await browser.storage.sync.get();
// SAFETY: Cannot import Config from config.ts, because content scripts aren't modules
// SAFETY: WrappedJObject and exportFunction aren't defined in @types module, so use ts-ignore
// @ts-ignore
window.wrappedJSObject.ytCustomizerConfig = cloneInto(config, window);
// @ts-ignore
exportFunction(ff2mpv, window, {defineAs: "ff2mpv"})
const script = document.createElement("script");
script.type = "module";
script.src = browser.runtime.getURL("scripts/page/ytCustomizer.js");
document.head.appendChild(script);
}
injectScript();

View file

@ -0,0 +1,6 @@
export function detourFunc(func: Function, origFunc: Function): Function {
return function (this: any, ...args: any[]) {
args.push(origFunc);
return func.apply(this, args);
}
}

View file

@ -0,0 +1,30 @@
import { detourFunc } from "./detour.js";
import * as popup from "./popup.js";
function openMpvPopup(app: YTApp) {
const confirmButton: YTPopupButton = popup.createButton("Close", YTButtonStyle.BlueText, YTButtonSize.Default);
const confirmDialog: YTConfirmDialog = popup.createConfirmDialog("YouTube Layout Customizer", "Video has opened in mpv", confirmButton);
const confirmPopup = popup.createConfirmPopup(confirmDialog);
popup.openPopup(app, YTPopupType.Dialog, confirmPopup);
}
function detourNavigate(this: YTApp, event: any, eventData: YTNavigateEventData, origFunc: Function): any {
const videoId = eventData.endpoint.watchEndpoint?.videoId;
if (videoId) {
window.ff2mpv(videoId);
openMpvPopup(this);
return;
}
return origFunc.call(this, event, eventData);
}
export default function setupDetour(app: YTApp) {
if (window.ytCustomizerConfig.ff2mpvEnabled) {
app.onYtNavigate = detourFunc(detourNavigate, app.onYtNavigate);
}
}

36
src/scripts/page/guide.ts Normal file
View file

@ -0,0 +1,36 @@
import { detourFunc } from "./detour.js";
import * as guideData from "./guide/guideData.js";
// Detour guide fetch to modify guide data
async function detourGuideFetch(this: YTGuideService, origFunc: Function): Promise<YTGuideData> {
let guide = await origFunc.call(this);
guide = guideData.getNewGuide(guide);
return guide;
}
// Detour guide after it's init once to disable footer
// Guide renderer gets created only after calling setGuideDataAfterInit
function detourAfterInit(this: YTGuideService, data: any, origFunc: Function): any {
let ret = origFunc.call(this, data);
if (window.ytCustomizerConfig.disableFooter) {
const guideRenderer = document.getElementById("guide-renderer") as YTGuideRenderer;
guideRenderer.showFooter = false;
}
if (window.ytCustomizerConfig.startGuideClosed) {
// NOTE: closeGuide doesn't update guideUserStateOpened, do it manually
this.closeGuide();
this.guideUserStateOpened = false;
}
// Restore original function, we don't need do this more than once
this.setGuideDataAfterInit = origFunc;
return ret;
}
export default function setupDetour(guideService: YTGuideService) {
guideService.fetchGuideData = detourFunc(detourGuideFetch, guideService.fetchGuideData);
guideService.setGuideDataAfterInit = detourFunc(detourAfterInit, guideService.setGuideDataAfterInit);
}

View file

@ -0,0 +1,108 @@
import * as utils from "../utils.js";
function shouldRemoveRenderer(renderer: YTGuideDataItem): boolean {
// Removing guide entries based on their icon
const inner = utils.getInner(renderer) as YTGuideDataRenderer;
const iconType = inner.icon.iconType;
return !window.ytCustomizerConfig[iconType];
}
function getNewCollapsableSection(section: YTGuideDataItem): any {
const inner = utils.getInner(section);
inner.sectionItems = getNewRenderers(inner.sectionItems);
if (shouldRemoveRenderer(inner.headerEntry)) {
// Don't leave the header empty
// Replace with the first collapsed item
inner.headerEntry = inner.sectionItems.shift();
}
if (inner.sectionItems.length > 0) {
return section;
}
// If only the header left, unpack and just use it
return inner.headerEntry ? inner.headerEntry : null;
}
function getNewCollapsableRenderer(renderer: YTGuideDataItem): any {
const inner = utils.getInner(renderer) as YTGuideDataCollapsibleRenderer;
inner.expandableItems = getNewRenderers(inner.expandableItems);
return inner.expandableItems.length > 0 ? renderer : null;
}
function getNewDownloadsRenderer(renderer: YTGuideDataItem): any {
const inner = utils.getInner(renderer) as YTGuideDataDownloads;
return shouldRemoveRenderer(inner.entryRenderer) ? null : renderer;
}
function getNewRenderer(renderer: YTGuideDataItem): any {
switch (utils.getInnerName(renderer)) {
case "guideCollapsibleSectionEntryRenderer": {
return getNewCollapsableSection(renderer);
}
case "guideCollapsibleEntryRenderer": {
return getNewCollapsableRenderer(renderer);
}
case "guideDownloadsEntryRenderer": {
return getNewDownloadsRenderer(renderer);
}
default: {
return shouldRemoveRenderer(renderer) ? null : renderer;
}
}
}
function getNewRenderers(renderers: YTGuideDataItem[]): any {
const newRenderers = [];
for (const renderer of renderers) {
const newRenderer = getNewRenderer(renderer);
if (newRenderer) {
newRenderers.push(newRenderer);
}
}
return newRenderers;
}
function modifySection(section: YTGuideDataItem): any {
const inner = utils.getInner(section) as YTGuideSection;
inner.items = getNewRenderers(inner.items);
return inner.items.length > 0 ? section : null;
}
function getNewSection(section: YTGuideDataItem): any {
switch (utils.getInnerName(section)) {
case "guideSigninPromoRenderer": {
return window.ytCustomizerConfig.disableSigninPromo ? null : section;
}
case "guideSubscriptionsSectionRenderer": {
return window.ytCustomizerConfig.disableSubscriptions ? null : section;
}
default: {
return modifySection(section);
}
}
}
function getNewSections(sections: YTGuideDataItem[]): any {
const newSections = [];
for (const section of sections) {
const newSection = getNewSection(section);
if (newSection) {
newSections.push(newSection);
}
}
return newSections;
}
export function getNewGuide(guide: YTGuideData): YTGuideData {
guide.items = getNewSections(guide.items);
return guide;
}

55
src/scripts/page/page.ts Normal file
View file

@ -0,0 +1,55 @@
import { detourFunc } from "./detour.js";
import * as pageData from "./page/pageData.js";
function getBrowseId(endpoint: YTNavigationEndpoint): string | null {
const browseEndpoint = endpoint.browseEndpoint;
if (browseEndpoint) {
return browseEndpoint.browseId;
}
return null;
}
// Detour setter and getter of pagePool
// To reuse the same ytd-browse element for home and subscriptions tab
function detourPageSet(this: Map<string, HTMLElement>, name: string, page: YTPage, origFunc: Function): any {
if (name === "home") {
name = "subscriptions"
page.pageSubtype = "subscriptions";
}
return origFunc.call(this, name, page);
}
function detourPageGet(this: Map<string, HTMLElement>, name: string, origFunc: Function): any {
if (name === "home") {
name = "subscriptions"
}
return origFunc.call(this, name);
}
// Detour page fetch to modify page data
async function detourPageFetch(this: YTApp, event: any, eventData: YTPageFetchEventData, origFunc: Function): Promise<any> {
const page = eventData.pageData.response;
const browseId = getBrowseId(eventData.pageData.endpoint);
eventData.pageData.response = await pageData.getNewPageData(this, page, browseId);
return origFunc.call(this, event, eventData);
}
export default function setupDetour(app: YTApp, pageManager: YTPageManager) {
app.onYtPageDataFetched = detourFunc(detourPageFetch, app.onYtPageDataFetched);
if (window.ytCustomizerConfig.redirectHome) {
const pageMap = pageManager.pagePool.pageNameToElement;
// SAFETY: Replacing setter and getter of Map
// SAFETY: TypeScript tries to set them as Map keys instead
// @ts-ignore
pageMap.set = detourFunc(detourPageSet, pageMap.set);
// @ts-ignore
pageMap.get = detourFunc(detourPageGet, pageMap.get);
}
}

View file

@ -0,0 +1,174 @@
import * as store from "./store.js";
import { getInner } from "../utils.js";
async function hashStr(str: string): Promise<string> {
const bytes = new TextEncoder().encode(str);
const hash = await crypto.subtle.digest("SHA-1", bytes);
const hashBuffer = new DataView(hash);
let hashStr = "";
for (let offset = 0; offset < hashBuffer.byteLength; offset += 4) {
const num = hashBuffer.getUint32(offset, false); // NOTE: Big Endian here is intentional, otherwise hex is reversed
hashStr += num.toString(16).padStart(8, '0');
}
return hashStr;
}
function getSAPISID(): string {
const cookie = document.cookie;
const start = cookie.indexOf("SAPISID=") + 8;
const end = cookie.indexOf(';', start);
return cookie.substring(start, end);
}
// Auth token format is
// SAPISIDHASH {unix_seconds}_{hash}
// Where {hash} is SHA1 hash computed from the following string
// {unix_seconds} {SAPISID} {origin_url}
// See: https://stackoverflow.com/questions/16907352/reverse-engineering-javascript-behind-google-button#32065323
async function getAuthToken(): Promise<string> {
const unixSeconds = Math.trunc(Date.now() / 1000);
const SAPISID = getSAPISID();
const hash = await hashStr(`${unixSeconds} ${SAPISID} https://www.youtube.com`);
const token = `SAPISIDHASH ${unixSeconds}_${hash}`;
return token;
}
// YouTube uses signals to indicate the action to execute on button press
// Unfortunately not all signals are clearly defined,
// Thus we have to resort to determening and defining them ourselves
function getSignal(button: YTButtonRenderer): string {
if (button.menuRequest) {
return button.menuRequest.signalServiceEndpoint.signal;
}
if (button.navigationEndpoint?.signInEndpoint) {
return "SIGNIN";
}
return "CREATE"
}
async function updateCache(app: YTApp, pageData: YTPageData, browseId: string | null) {
if (browseId !== "FEwhat_to_watch" && browseId !== "FEsubscriptions") {
return;
}
// If we're on the home page, change page data to subscriptions page data
// Then duplicate it to subscriptions key, so we won't have to fetch it again.
// Otherwise, we're on subscriptions page, just copy it to home key.
let responseStore = app.ephemeralResponseStore;
if (browseId === "FEwhat_to_watch") {
await store.changePageData("service:browse/browseId:FEwhat_to_watch", pageData, responseStore);
await store.duplicate("service:browse/browseId:FEwhat_to_watch", "service:browse/browseId:FEsubscriptions", responseStore);
} else {
await store.duplicate("service:browse/browseId:FEsubscriptions", "service:browse/browseId:FEwhat_to_watch", responseStore);
}
}
async function getSubscriptionsPageData(): Promise<YTPageData> {
const ytConfig = window.yt.config_;
const context = ytConfig["INNERTUBE_CONTEXT"];
const apiKey = ytConfig["INNERTUBE_API_KEY"];
const loggedIn = ytConfig["LOGGED_IN"];
const url = `https://www.youtube.com/youtubei/v1/browse?key=${apiKey}&prettyPrint=false`
const response = await fetch(url, {
method: "POST",
mode: "cors",
headers: {
Authorization: loggedIn ? await getAuthToken() : "",
"Content-Type": "application/json"
},
body: JSON.stringify({
browseId: "FEsubscriptions",
context: context
})
});
const pageData = await response.json();
return pageData;
}
function getNewButtons(buttons: any[]): any[] {
const config = window.ytCustomizerConfig;
const newButtons = [];
for (const button of buttons) {
const innerButton = getInner(button) as YTButtonRenderer;
const signal = getSignal(innerButton);
let push = true;
switch(signal) {
case "CREATE": {
push = config.topbarCreateButton;
break;
}
case "GET_NOTIFICATIONS_MENU": {
push = config.topbarNotificationsButton;
break;
}
case "GET_ACCOUNT_MENU": {
push = config.topbarAccountButton;
break;
}
case "SIGNIN": {
push = config.topbarSignInButton;
break;
}
}
if (push) {
newButtons.push(button);
}
}
return newButtons;
}
function modifyPageData(pageData: YTPageData): YTPageData {
const config = window.ytCustomizerConfig;
const topbar = pageData.topbar.desktopTopbarRenderer;
if (!config.topbarLogo) {
delete topbar.logo;
}
if (!config.topbarSearch) {
delete topbar.searchbox;
}
if (!config.topbarVoiceSearchButton) {
delete topbar.voiceSearchButton;
}
topbar.topbarButtons = getNewButtons(topbar.topbarButtons);
pageData.customized = true;
return pageData;
}
export async function getNewPageData(app: YTApp, pageData: YTPageData, browseId: string | null): Promise<YTPageData> {
if (pageData.customized) {
// Page was cached, no need to modify
return pageData;
}
if (window.ytCustomizerConfig.redirectHome && browseId === "FEwhat_to_watch") {
pageData = await getSubscriptionsPageData();
}
pageData = modifyPageData(pageData);
if (window.ytCustomizerConfig.redirectHome) {
// Update cache since pageData reference possibly has changed
updateCache(app, pageData, browseId);
}
return pageData;
}

View file

@ -0,0 +1,15 @@
export async function changePageData(key: string, pageData: YTPageData, responseStore: YTResponseStore) {
const response = await responseStore.get(key);
const copy = {...response.data};
copy.innertubeResponse = pageData;
responseStore.putInternal(key, copy);
}
export async function duplicate(copyFromKey: string, copyToKey: string, responseStore: YTResponseStore) {
const response = await responseStore.get(copyFromKey);
const copy = {...response.data};
copy.key = copyToKey
responseStore.putInternal(copyToKey, copy);
}

51
src/scripts/page/popup.ts Normal file
View file

@ -0,0 +1,51 @@
export function createConfirmDialog(title: string, dialogMessage: string, confirmButton?: YTPopupButton , cancelButton?: YTPopupButton): YTConfirmDialog {
const confirmDialog: YTConfirmDialog = {
title: createMessage(title),
dialogMessages: [ createMessage(dialogMessage) ],
};
if (confirmButton) {
confirmDialog.confirmButton = confirmButton;
}
if (cancelButton) {
confirmDialog.cancelButton = cancelButton;
}
return confirmDialog;
}
export function createConfirmPopup(confirmDialog: YTConfirmDialog): YTPopup {
return {
confirmDialogRenderer: confirmDialog
}
}
export function createMessage(message: string): YTMessage {
return {
runs: [{
text: message
}]
}
}
export function createButton(text: string, style: YTButtonStyle, size: YTButtonSize): YTPopupButton {
return {
buttonRenderer: {
text: createMessage(text),
style: style,
size: size
}
}
}
export function openPopup(app: YTApp, popupType: YTPopupType, popup: YTPopup) {
const openPopupAction = {
openPopupAction: {
popupType: popupType,
popup: popup
}
}
app.openPopup(openPopupAction);
}

View file

@ -0,0 +1,7 @@
export function getInner(obj: Object): any {
return Object.values(obj)[0];
}
export function getInnerName(obj: Object): string | undefined {
return Object.keys(obj)[0];
}

View file

@ -0,0 +1,15 @@
import setupPageDetour from "./page.js";
import setupGuideDetour from "./guide.js";
import setupFF2MpvDetour from "./ff2mpv.js";
// TODO: Do the general review and clean up before release
{
const app = document.getElementsByTagName("ytd-app")[0] as YTApp;
const pageManager = document.getElementById("page-manager") as YTPageManager;
const guideService = document.getElementById("guide-service") as YTGuideService;
setupPageDetour(app, pageManager);
setupGuideDetour(guideService);
setupFF2MpvDetour(app);
}

12
src/scripts/page/ytTypes.d.ts vendored Normal file
View file

@ -0,0 +1,12 @@
// These types directly correspond to the objects defined by YouTube itself
// Unfortunately a lot of them are defined badly
// We use plain "any" or "Function" when we don't have enough type information
// Or a way to distinguish it from the other types, in an array for example
interface Window {
yt: {
config_: {
[key: string]: any
}
}
}

18
src/scripts/page/ytTypes/ytApp.d.ts vendored Normal file
View file

@ -0,0 +1,18 @@
interface YTApp extends HTMLElement {
ephemeralResponseStore: YTResponseStore
onYtPageDataFetched: Function
onYtNavigate: Function
openPopup: (action: YTAction) => void
}
interface YTResponseStore {
get: (key: string) => Promise<{
data: YTResponse
}>,
putInternal: (key: string, response: YTResponse) => void
}
interface YTResponse {
key: string
innertubeResponse: YTPageData
}

44
src/scripts/page/ytTypes/ytGuide.d.ts vendored Normal file
View file

@ -0,0 +1,44 @@
interface YTGuideService extends HTMLElement {
guideUserStateOpened: boolean,
setGuideDataAfterInit: Function,
fetchGuideData: Function,
closeGuide: () => void
}
interface YTGuideRenderer extends HTMLElement {
showFooter: boolean
}
interface YTGuideData {
items: YTGuideDataItem[]
}
interface YTGuideSection {
items: YTGuideDataItem[]
}
// This is wrapper for the guide item itself
// with only one key as the name of the type, holding the item object.
// We use utils.getInner and utils.getInnerName to find out what item it holds
interface YTGuideDataItem {
[key: string]: any
}
interface YTGuideDataRenderer {
icon: {
iconType: string
}
}
interface YTGuideDataDownloads {
entryRenderer: YTGuideDataItem
}
interface YTGuideDataCollapsibleSection {
headerEntry: YTGuideDataItem
sectionItems: YTGuideDataItem[]
}
interface YTGuideDataCollapsibleRenderer {
expandableItems: YTGuideDataItem[]
}

62
src/scripts/page/ytTypes/ytPage.d.ts vendored Normal file
View file

@ -0,0 +1,62 @@
interface YTPageManager extends HTMLElement {
pagePool: {
pageNameToElement: Map<string, YTPage>
}
}
interface YTPage {
pageSubtype: string
}
interface YTPageData {
contents: any[]
header: any[],
topbar: {
desktopTopbarRenderer: YTTopbar,
}
customized?: boolean
}
interface YTTopbar {
logo: any
searchbox: any,
voiceSearchButton: any
topbarButtons: any[],
}
interface YTButtonRenderer {
menuRequest?: YTMenuRequest,
menuRenderer?: any
navigationEndpoint?: YTNavigationEndpoint
}
interface YTMenuRequest {
signalServiceEndpoint: {
signal: string
}
}
interface YTNavigationEndpoint {
browseEndpoint?: YTBrowseEndpoint
signInEndpoint?: any
watchEndpoint?: YTWatchEndpoint
}
interface YTBrowseEndpoint {
browseId: string
}
interface YTWatchEndpoint {
videoId: string
}
interface YTPageFetchEventData {
pageData: {
response: YTPageData
endpoint: YTNavigationEndpoint
}
}
interface YTNavigateEventData {
endpoint: YTNavigationEndpoint
}

47
src/scripts/page/ytTypes/ytPopup.d.ts vendored Normal file
View file

@ -0,0 +1,47 @@
interface YTAction {
openPopupAction?: YTOpenPopupAction
}
interface YTOpenPopupAction {
popupType: YTPopupType,
popup: YTPopup
}
interface YTPopup {
confirmDialogRenderer?: YTConfirmDialog
}
interface YTConfirmDialog {
title: YTMessage
dialogMessages: YTMessage[]
confirmButton?: YTPopupButton
cancelButton?: YTPopupButton
}
interface YTPopupButton {
buttonRenderer: {
style: YTButtonStyle,
size: YTButtonSize,
text: YTMessage
}
}
interface YTMessage {
runs: YTText[]
}
interface YTText {
text: String
}
declare const enum YTPopupType {
Dialog = "DIALOG"
}
declare const enum YTButtonStyle {
BlueText = "STYLE_BLUE_TEXT"
}
declare const enum YTButtonSize {
Default = "SIZE_DEFAULT"
}

96
src/settings/settings.ts Normal file
View file

@ -0,0 +1,96 @@
class ConfigOption extends HTMLElement {
constructor() {
super();
const localizedText = browser.i18n.getMessage(this.i18nMessage);
const text = document.createTextNode(localizedText);
this.append(text);
this.addEventListener("change", () => setConfigOption(this.key, this.enabled));
this.addEventListener("click", () => this.enabled = !this.enabled);
}
get i18nMessage(): string {
return this.getAttribute("msg") as string;
}
get key(): string {
return this.getAttribute("key") as string;
}
set enabled(value: boolean) {
if (value) {
this.setAttribute("enabled", "");
this.className = "config-option-enabled";
} else {
this.removeAttribute("enabled");
this.className = "";
}
this.dispatchEvent(new Event("change"));
}
get enabled(): boolean {
return this.hasAttribute("enabled");
}
}
class ConfigSection extends HTMLElement {
constructor() {
super();
const heading = document.createElement("h4");
const localizedHeading = browser.i18n.getMessage(this.i18nMessage);
const text = document.createTextNode(localizedHeading);
heading.className = "config-heading";
heading.appendChild(text);
const enableButton = document.createElement("button");
const localizedEnable = browser.i18n.getMessage("enableAllButton");
const enableText = document.createTextNode(localizedEnable);
enableButton.addEventListener("click", () => this.enabled = true);
enableButton.appendChild(enableText);
const disableButton = document.createElement("button");
const localizedDisable = browser.i18n.getMessage("disableAllButton");
const disableText = document.createTextNode(localizedDisable);
disableButton.addEventListener("click", () => this.enabled = false);
disableButton.appendChild(disableText);
const buttons = document.createElement("div");
buttons.className = "config-buttons";
buttons.append(enableButton, disableButton);
this.prepend(heading);
this.append(buttons);
}
set enabled(value: boolean) {
const options = this.getElementsByTagName("config-option") as HTMLCollectionOf<ConfigOption>;
for (const option of options) {
option.enabled = value;
}
}
get i18nMessage(): string {
return this.getAttribute("msg") as string;
}
}
async function setConfigOption(key: string, value: any) {
browser.storage.sync.set({[key]: value})
}
async function populateOptions() {
const config = await browser.storage.sync.get();
const options = document.getElementsByTagName("config-option") as HTMLCollectionOf<ConfigOption>;
for (const option of options) {
option.enabled = config[option.key];
}
}
window.customElements.define("config-option", ConfigOption);
window.customElements.define("config-section", ConfigSection);
populateOptions();