Add extension sources with translations
This commit is contained in:
parent
99e4f14b06
commit
9081823151
27 changed files with 1924 additions and 0 deletions
158
extension/_locales/en/messages.json
Normal file
158
extension/_locales/en/messages.json
Normal 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"
|
||||
}
|
||||
}
|
158
extension/_locales/es/messages.json
Normal file
158
extension/_locales/es/messages.json
Normal 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"
|
||||
}
|
||||
}
|
158
extension/_locales/fr/messages.json
Normal file
158
extension/_locales/fr/messages.json
Normal 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"
|
||||
}
|
||||
}
|
158
extension/_locales/hu/messages.json
Normal file
158
extension/_locales/hu/messages.json
Normal 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"
|
||||
}
|
||||
}
|
158
extension/_locales/ru/messages.json
Normal file
158
extension/_locales/ru/messages.json
Normal 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
3
extension/icon.svg
Normal 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
53
extension/manifest.json
Normal 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"
|
||||
]
|
||||
}
|
||||
|
68
extension/settings/settings.html
Normal file
68
extension/settings/settings.html
Normal 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>
|
97
extension/settings/style.css
Normal file
97
extension/settings/style.css
Normal 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);
|
||||
}
|
||||
|
111
src/scripts/background/config.ts
Normal file
111
src/scripts/background/config.ts
Normal 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);
|
26
src/scripts/content/ytCustomizer.ts
Normal file
26
src/scripts/content/ytCustomizer.ts
Normal 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();
|
||||
|
6
src/scripts/page/detour.ts
Normal file
6
src/scripts/page/detour.ts
Normal 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);
|
||||
}
|
||||
}
|
30
src/scripts/page/ff2mpv.ts
Normal file
30
src/scripts/page/ff2mpv.ts
Normal 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
36
src/scripts/page/guide.ts
Normal 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);
|
||||
}
|
108
src/scripts/page/guide/guideData.ts
Normal file
108
src/scripts/page/guide/guideData.ts
Normal 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
55
src/scripts/page/page.ts
Normal 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);
|
||||
}
|
||||
}
|
174
src/scripts/page/page/pageData.ts
Normal file
174
src/scripts/page/page/pageData.ts
Normal 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;
|
||||
}
|
15
src/scripts/page/page/store.ts
Normal file
15
src/scripts/page/page/store.ts
Normal 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
51
src/scripts/page/popup.ts
Normal 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);
|
||||
}
|
7
src/scripts/page/utils.ts
Normal file
7
src/scripts/page/utils.ts
Normal 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];
|
||||
}
|
15
src/scripts/page/ytCustomizer.ts
Normal file
15
src/scripts/page/ytCustomizer.ts
Normal 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
12
src/scripts/page/ytTypes.d.ts
vendored
Normal 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
18
src/scripts/page/ytTypes/ytApp.d.ts
vendored
Normal 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
44
src/scripts/page/ytTypes/ytGuide.d.ts
vendored
Normal 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
62
src/scripts/page/ytTypes/ytPage.d.ts
vendored
Normal 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
47
src/scripts/page/ytTypes/ytPopup.d.ts
vendored
Normal 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
96
src/settings/settings.ts
Normal 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();
|
||||
|
Loading…
Reference in a new issue