diff --git a/extension/_locales/en/messages.json b/extension/_locales/en/messages.json
new file mode 100644
index 0000000..dd5a35a
--- /dev/null
+++ b/extension/_locales/en/messages.json
@@ -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"
+ }
+}
diff --git a/extension/_locales/es/messages.json b/extension/_locales/es/messages.json
new file mode 100644
index 0000000..68d4ec6
--- /dev/null
+++ b/extension/_locales/es/messages.json
@@ -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"
+ }
+}
diff --git a/extension/_locales/fr/messages.json b/extension/_locales/fr/messages.json
new file mode 100644
index 0000000..ad70275
--- /dev/null
+++ b/extension/_locales/fr/messages.json
@@ -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"
+ }
+}
diff --git a/extension/_locales/hu/messages.json b/extension/_locales/hu/messages.json
new file mode 100644
index 0000000..9df884a
--- /dev/null
+++ b/extension/_locales/hu/messages.json
@@ -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"
+ }
+}
diff --git a/extension/_locales/ru/messages.json b/extension/_locales/ru/messages.json
new file mode 100644
index 0000000..3f0caf7
--- /dev/null
+++ b/extension/_locales/ru/messages.json
@@ -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": "Выключить всё"
+ }
+}
diff --git a/extension/icon.svg b/extension/icon.svg
new file mode 100644
index 0000000..44d56e5
--- /dev/null
+++ b/extension/icon.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/extension/manifest.json b/extension/manifest.json
new file mode 100644
index 0000000..0e3c025
--- /dev/null
+++ b/extension/manifest.json
@@ -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"
+ ]
+ }
+
diff --git a/extension/settings/settings.html b/extension/settings/settings.html
new file mode 100644
index 0000000..865ec78
--- /dev/null
+++ b/extension/settings/settings.html
@@ -0,0 +1,68 @@
+
+
+
+
+ YouTube Customizer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/extension/settings/style.css b/extension/settings/style.css
new file mode 100644
index 0000000..6a0bf1a
--- /dev/null
+++ b/extension/settings/style.css
@@ -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);
+}
+
diff --git a/src/scripts/background/config.ts b/src/scripts/background/config.ts
new file mode 100644
index 0000000..b6eab60
--- /dev/null
+++ b/src/scripts/background/config.ts
@@ -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);
diff --git a/src/scripts/content/ytCustomizer.ts b/src/scripts/content/ytCustomizer.ts
new file mode 100644
index 0000000..b49b741
--- /dev/null
+++ b/src/scripts/content/ytCustomizer.ts
@@ -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();
+
diff --git a/src/scripts/page/detour.ts b/src/scripts/page/detour.ts
new file mode 100644
index 0000000..4d65da5
--- /dev/null
+++ b/src/scripts/page/detour.ts
@@ -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);
+ }
+}
diff --git a/src/scripts/page/ff2mpv.ts b/src/scripts/page/ff2mpv.ts
new file mode 100644
index 0000000..2da90e7
--- /dev/null
+++ b/src/scripts/page/ff2mpv.ts
@@ -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);
+ }
+}
diff --git a/src/scripts/page/guide.ts b/src/scripts/page/guide.ts
new file mode 100644
index 0000000..33b9682
--- /dev/null
+++ b/src/scripts/page/guide.ts
@@ -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 {
+ 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);
+}
diff --git a/src/scripts/page/guide/guideData.ts b/src/scripts/page/guide/guideData.ts
new file mode 100644
index 0000000..df7bb05
--- /dev/null
+++ b/src/scripts/page/guide/guideData.ts
@@ -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;
+}
diff --git a/src/scripts/page/page.ts b/src/scripts/page/page.ts
new file mode 100644
index 0000000..a226a9f
--- /dev/null
+++ b/src/scripts/page/page.ts
@@ -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, 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, 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 {
+ 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);
+ }
+}
diff --git a/src/scripts/page/page/pageData.ts b/src/scripts/page/page/pageData.ts
new file mode 100644
index 0000000..bd0e20a
--- /dev/null
+++ b/src/scripts/page/page/pageData.ts
@@ -0,0 +1,174 @@
+import * as store from "./store.js";
+import { getInner } from "../utils.js";
+
+async function hashStr(str: string): Promise {
+ 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 {
+ 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 {
+ 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 {
+ 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;
+}
diff --git a/src/scripts/page/page/store.ts b/src/scripts/page/page/store.ts
new file mode 100644
index 0000000..9495ee1
--- /dev/null
+++ b/src/scripts/page/page/store.ts
@@ -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);
+}
diff --git a/src/scripts/page/popup.ts b/src/scripts/page/popup.ts
new file mode 100644
index 0000000..64bbab3
--- /dev/null
+++ b/src/scripts/page/popup.ts
@@ -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);
+}
diff --git a/src/scripts/page/utils.ts b/src/scripts/page/utils.ts
new file mode 100644
index 0000000..7786e65
--- /dev/null
+++ b/src/scripts/page/utils.ts
@@ -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];
+}
diff --git a/src/scripts/page/ytCustomizer.ts b/src/scripts/page/ytCustomizer.ts
new file mode 100644
index 0000000..f746a87
--- /dev/null
+++ b/src/scripts/page/ytCustomizer.ts
@@ -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);
+}
diff --git a/src/scripts/page/ytTypes.d.ts b/src/scripts/page/ytTypes.d.ts
new file mode 100644
index 0000000..4b77cc9
--- /dev/null
+++ b/src/scripts/page/ytTypes.d.ts
@@ -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
+ }
+ }
+}
diff --git a/src/scripts/page/ytTypes/ytApp.d.ts b/src/scripts/page/ytTypes/ytApp.d.ts
new file mode 100644
index 0000000..e527cc9
--- /dev/null
+++ b/src/scripts/page/ytTypes/ytApp.d.ts
@@ -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
+}
diff --git a/src/scripts/page/ytTypes/ytGuide.d.ts b/src/scripts/page/ytTypes/ytGuide.d.ts
new file mode 100644
index 0000000..a408bdc
--- /dev/null
+++ b/src/scripts/page/ytTypes/ytGuide.d.ts
@@ -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[]
+}
diff --git a/src/scripts/page/ytTypes/ytPage.d.ts b/src/scripts/page/ytTypes/ytPage.d.ts
new file mode 100644
index 0000000..41d5f42
--- /dev/null
+++ b/src/scripts/page/ytTypes/ytPage.d.ts
@@ -0,0 +1,62 @@
+interface YTPageManager extends HTMLElement {
+ pagePool: {
+ pageNameToElement: Map
+ }
+}
+
+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
+}
diff --git a/src/scripts/page/ytTypes/ytPopup.d.ts b/src/scripts/page/ytTypes/ytPopup.d.ts
new file mode 100644
index 0000000..4c6d135
--- /dev/null
+++ b/src/scripts/page/ytTypes/ytPopup.d.ts
@@ -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"
+}
diff --git a/src/settings/settings.ts b/src/settings/settings.ts
new file mode 100644
index 0000000..6bc41a2
--- /dev/null
+++ b/src/settings/settings.ts
@@ -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;
+ 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;
+
+ for (const option of options) {
+ option.enabled = config[option.key];
+ }
+}
+
+window.customElements.define("config-option", ConfigOption);
+window.customElements.define("config-section", ConfigSection);
+populateOptions();
+