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