Prepare 4.0.0.3 extension release

This commit is contained in:
Anarios
2026-05-01 06:26:51 +02:00
parent 54ff493ea4
commit d6cbaadf05
36 changed files with 846 additions and 337 deletions
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "Patreon ilə daxil ol"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "Giriş tamamlanmadı. Yenidən cəhd edin."
},
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "Přihlásit se pomocí Patreonu"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "Přihlášení se nepodařilo dokončit. Zkuste to znovu."
},
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "Mit Patreon anmelden"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "Anmeldung konnte nicht abgeschlossen werden. Bitte versuche es erneut."
},
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "Σύνδεση με Patreon"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "Η σύνδεση δεν ολοκληρώθηκε. Δοκιμάστε ξανά."
},
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "Login with Patreon"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "Failed to complete login. Please try again."
},
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "Iniciar sesión con Patreon"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "No se pudo completar el inicio de sesión. Inténtalo de nuevo."
},
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "Se connecter avec Patreon"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "Échec de la connexion. Veuillez réessayer."
},
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "Bejelentkezés Patreonnal"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "A bejelentkezés nem sikerült. Próbáld újra."
},
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "Masuk dengan Patreon"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "Gagal menyelesaikan login. Coba lagi."
},
+13 -1
View File
@@ -83,8 +83,20 @@
"patreonLogin": {
"message": "Accedi con Patreon"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "Impossibile completare laccesso. Riprova."
"message": "Impossibile completare l'accesso. Riprova."
},
"patreonLoginStartFailed": {
"message": "Impossibile avviare laccesso con Patreon. Riprova."
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "Patreonでログイン"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "ログインを完了できませんでした。もう一度お試しください。"
},
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "Patreon으로 로그인"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "로그인을 완료하지 못했습니다. 다시 시도해 주세요."
},
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "Inloggen met Patreon"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "Aanmelden kon niet worden voltooid. Probeer het opnieuw."
},
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "Zaloguj się przez Patreon"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "Nie udało się dokończyć logowania. Spróbuj ponownie."
},
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "Entrar com o Patreon"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "Não foi possível concluir o login. Tente novamente."
},
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "Войти через Patreon"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "Не удалось завершить вход. Попробуйте еще раз."
},
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "Logga in med Patreon"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "Det gick inte att slutföra inloggningen. Försök igen."
},
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "Patreon ile giriş yap"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "Giriş tamamlanamadı. Lütfen tekrar deneyin."
},
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "Увійти через Patreon"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "Не вдалося завершити вхід. Спробуйте ще раз."
},
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "Đăng nhập bằng Patreon"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "Không thể hoàn tất đăng nhập. Vui lòng thử lại."
},
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "使用 Patreon 登录"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "登录未完成,请重试。"
},
@@ -83,6 +83,18 @@
"patreonLogin": {
"message": "使用 Patreon 登入"
},
"githubLoggedOutMessage": {
"message": "Premium features available for GitHub contributors"
},
"githubLogin": {
"message": "Login with GitHub (Contributor)"
},
"githubLoginStartFailed": {
"message": "Failed to initiate GitHub login. Please try again."
},
"githubLoginCompleteFailed": {
"message": "Failed to complete GitHub login. Please try again."
},
"patreonLoginCompleteFailed": {
"message": "無法完成登入,請再試一次。"
},
+135 -104
View File
@@ -106,13 +106,21 @@ ytd-menu-renderer.ytd-watch-metadata {
background: var(--yt-spec-base-background, #202020);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 16px;
padding: 12px 18px 10px;
margin-bottom: 16px;
color: var(--yt-spec-text-primary, #ffffff);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.18);
font-family: Roboto, Arial, sans-serif;
transition: box-shadow 0.2s ease, transform 0.2s ease, opacity 0.2s ease;
transition: box-shadow 0.2s ease, transform 0.2s ease, opacity 0.2s ease, padding 0.2s ease;
position: relative;
display: flex;
flex-direction: column;
gap: 0;
}
.ryd-premium-teaser.is-expanded {
gap: 12px;
padding: 12px 18px 16px;
}
.ryd-premium-teaser.is-loading {
@@ -179,53 +187,88 @@ ytd-menu-renderer.ytd-watch-metadata {
transform: translateY(-1px);
}
.ryd-premium-teaser__header {
.ryd-premium-teaser__summary {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: flex-start;
gap: 12px;
flex-wrap: wrap;
position: relative;
padding-right: 48px;
}
.ryd-premium-teaser__badge {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 999px;
background: rgba(59, 130, 246, 0.18);
color: #0f172a;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 600;
margin-bottom: 4px;
.ryd-premium-teaser__counts {
display: flex;
flex-wrap: wrap;
gap: 12px;
width: 100%;
padding-right: 5px;
}
.ryd-premium-teaser__title {
font-size: 18px;
margin: 4px 0;
font-weight: 600;
}
.ryd-premium-teaser__subtitle {
margin: 0;
font-size: 13px;
color: var(--yt-spec-text-secondary, #bcccdc);
}
.ryd-premium-teaser__actions {
.ryd-premium-teaser__count {
flex: 1 1 0;
min-width: 120px;
padding: 10px 14px;
border-radius: 12px;
background: rgba(148, 163, 184, 0.08);
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
text-align: left;
gap: 4px;
}
.ryd-premium-teaser__count-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--yt-spec-text-secondary, #9ca3af);
}
.ryd-premium-teaser__count-value {
font-size: 20px;
font-weight: 600;
line-height: 1.2;
}
.ryd-premium-teaser__toggle {
width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 6px;
background: rgba(148, 163, 184, 0.16);
color: var(--yt-spec-text-secondary, #94a3b8);
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease;
align-self: center;
margin: 8px auto 0;
order: 3;
}
.ryd-premium-teaser.is-expanded .ryd-premium-teaser__toggle {
margin-top: 0;
}
.ryd-premium-teaser__toggle:hover,
.ryd-premium-teaser__toggle:focus-visible {
background: rgba(148, 163, 184, 0.28);
color: var(--yt-spec-text-primary, #f8fafc);
outline: none;
}
.ryd-premium-teaser__toggle-icon {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid currentColor;
transition: transform 0.2s ease;
}
.ryd-premium-teaser.is-expanded .ryd-premium-teaser__toggle-icon {
transform: rotate(180deg);
}
.ryd-premium-teaser__close {
position: absolute;
top: 8px;
right: 8px;
width: 32px;
height: 32px;
border: none;
@@ -237,6 +280,9 @@ ytd-menu-renderer.ytd-watch-metadata {
justify-content: center;
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease;
position: absolute;
top: 0;
right: 0;
}
.ryd-premium-teaser__close:hover,
@@ -252,6 +298,54 @@ ytd-menu-renderer.ytd-watch-metadata {
font-weight: 600;
}
.ryd-premium-teaser__details {
display: none;
flex-direction: column;
gap: 12px;
order: 2;
}
.ryd-premium-teaser.is-expanded .ryd-premium-teaser__details {
display: flex;
}
.ryd-premium-teaser__title {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.ryd-premium-teaser__subtitle {
margin: 0;
font-size: 13px;
color: var(--yt-spec-text-secondary, #bcccdc);
line-height: 1.5;
}
.ryd-premium-teaser__highlights-title {
margin: 0 0 6px 0;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--yt-spec-text-secondary, #9ca3af);
}
.ryd-premium-teaser__highlights-list {
margin: 0;
padding-left: 18px;
display: grid;
gap: 6px;
font-size: 13px;
color: var(--yt-spec-text-secondary, #93adc8);
}
.ryd-premium-teaser__links {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
.ryd-premium-teaser__cta {
border: none;
background: rgba(59, 130, 246, 0.95);
@@ -280,82 +374,19 @@ ytd-menu-renderer.ytd-watch-metadata {
text-decoration: underline;
}
.ryd-premium-teaser__body {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.ryd-premium-teaser__stats {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.ryd-premium-teaser__stat {
flex: 1 1 140px;
background: rgba(148, 163, 184, 0.08);
border-radius: 12px;
padding: 12px;
}
.ryd-premium-teaser__stat-label {
display: block;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--yt-spec-text-secondary, #9ca3af);
margin-bottom: 6px;
}
.ryd-premium-teaser__stat-value {
font-size: 20px;
font-weight: 600;
}
.ryd-premium-teaser__status {
margin: 0;
font-size: 12px;
color: var(--yt-spec-text-secondary, #9ca3af);
}
.ryd-premium-teaser__breadcrumbs {
background: rgba(59, 130, 246, 0.08);
border-radius: 12px;
padding: 12px 16px;
}
.ryd-premium-teaser__breadcrumbs-title {
margin: 0 0 8px 0;
font-size: 13px;
font-weight: 600;
}
.ryd-premium-teaser__breadcrumbs-list {
margin: 0;
padding-left: 18px;
display: grid;
gap: 6px;
font-size: 13px;
color: var(--yt-spec-text-secondary, #93adc8);
}
.ryd-premium-teaser__breadcrumbs-list li {
padding-left: 4px;
}
html[dark] .ryd-premium-teaser__badge {
background: rgba(59, 130, 246, 0.28);
color: #dbeafe;
}
html[dark] .ryd-premium-teaser__stat {
html[dark] .ryd-premium-teaser__count {
background: rgba(148, 163, 184, 0.12);
}
html[dark] .ryd-premium-teaser__breadcrumbs {
background: rgba(59, 130, 246, 0.14);
html[dark] .ryd-premium-teaser__toggle:hover,
html[dark] .ryd-premium-teaser__toggle:focus-visible {
background: rgba(148, 163, 184, 0.22);
}
.ryd-analytics__header {
+4 -1
View File
@@ -12,7 +12,10 @@
"128": "icons/icon128.png"
},
"host_permissions": [
"*://*.youtube.com/*"
"*://*.youtube.com/*",
"*://returnyoutubedislikeapi.com/*",
"https://localhost:7258/*",
"http://localhost:7258/*"
],
"permissions": [
"storage"
+3 -1
View File
@@ -16,7 +16,9 @@
"activeTab",
"*://*.youtube.com/*",
"storage",
"*://returnyoutubedislikeapi.com/*"
"*://returnyoutubedislikeapi.com/*",
"https://localhost:7258/*",
"http://localhost:7258/*"
],
"browser_action": {
"default_popup": "popup.html"
+4
View File
@@ -48,6 +48,10 @@
</svg>
__MSG_patreonLogin__
</button>
<p style="color: var(--lightGrey); font-size: 0.85rem; margin: 0.6rem 0 0.4rem;" title="__MSG_githubLoggedOutMessage__">__MSG_githubLoggedOutMessage__</p>
<button class="m3-btn m3-primary" style="margin-top: 0.1rem;" id="github-login-btn" title="__MSG_githubLogin__">
__MSG_githubLogin__
</button>
</div>
<div id="patreon-logged-in" style="display: none;">
<div style="display: flex; align-items: center; margin-bottom: 0.5rem;">
+28
View File
@@ -99,6 +99,7 @@ function initPatreonAuth() {
const loggedOutView = document.getElementById("patreon-logged-out");
const loggedInView = document.getElementById("patreon-logged-in");
const loginBtn = document.getElementById("patreon-login-btn");
const githubLoginBtn = document.getElementById("github-login-btn");
const logoutBtn = document.getElementById("patreon-logout-btn");
const userAvatar = document.getElementById("patreon-user-avatar");
const userName = document.getElementById("patreon-user-name");
@@ -335,6 +336,33 @@ function initPatreonAuth() {
});
});
if (githubLoginBtn) {
githubLoginBtn.addEventListener("click", () => {
ensureIdentityPermission(async (granted) => {
const identityNow = getIdentityApi();
if (!granted || !identityNow || typeof identityNow.getRedirectURL !== "function") {
alert(chrome.i18n.getMessage("patreonPermissionRequired"));
return;
}
chrome.runtime.sendMessage({ message: "github_oauth_login" }, (resp) => {
if (chrome.runtime && chrome.runtime.lastError) {
console.error("GitHub login failed:", chrome.runtime.lastError.message);
alert(chrome.i18n.getMessage("githubLoginStartFailed"));
return;
}
if (resp && resp.success) {
const user = resp.user;
showLoggedInView(user);
} else {
console.error("GitHub login failed:", resp && resp.error);
alert(chrome.i18n.getMessage("githubLoginCompleteFailed"));
}
});
});
});
}
logoutBtn.addEventListener("click", () => {
chrome.storage.sync.remove(["patreonUser", "patreonSessionToken"], () => {
showLoggedOutView();
+56
View File
@@ -235,6 +235,62 @@ api.runtime.onMessage.addListener((request, sender, sendResponse) => {
}
})();
return true;
} else if (request.message === "github_oauth_login") {
(async () => {
try {
const idApi = getIdentityApi();
if (
!idApi ||
typeof (idApi.getRedirectURL || (isChrome() && chrome.identity && chrome.identity.getRedirectURL)) !==
"function"
) {
sendResponse({ success: false, error: "identity API not available" });
return;
}
const redirectUri =
isFirefox() && browser.identity.getRedirectURL
? browser.identity.getRedirectURL()
: isChrome() && chrome.identity.getRedirectURL
? chrome.identity.getRedirectURL()
: "";
const startRes = await fetch(
getApiEndpoint(`/api/auth/github/login?redirectUri=${encodeURIComponent(redirectUri)}`),
);
const startData = await startRes.json();
const responseUrl = await launchWebAuthFlow(startData.authUrl);
const { code, state } = extractOAuthParams(responseUrl);
if (!code) {
sendResponse({ success: false, error: "No authorization code received" });
return;
}
const exchangeRes = await fetch(getApiEndpoint("/api/auth/github/exchange"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
code,
state,
expectedState: startData.state,
redirectUri: startData.redirectUri || redirectUri,
}),
});
const authData = await exchangeRes.json();
if (authData && authData.success) {
handlePatreonAuthComplete(authData.user, authData.sessionToken, () => {
sendResponse({ success: true, user: authData.user });
});
} else {
sendResponse({ success: false, error: (authData && authData.error) || "OAuth exchange failed" });
}
} catch (e) {
console.error("github_oauth_login error", e);
sendResponse({ success: false, error: String((e && e.message) || e) });
}
})();
return true;
}
});
+13 -29
View File
@@ -1,13 +1,6 @@
import { getButtons, getDislikeButton, getLikeButton } from "./buttons";
import {
extConfig,
isMobile,
isLikesDisabled,
isNewDesign,
isRoundedDesign,
isShorts,
} from "./state";
import { getColorFromTheme, isInViewport } from "./utils";
import { extConfig, isMobile, isLikesDisabled, isNewDesign, isRoundedDesign, isShorts } from "./state";
import { getColorFromTheme, isInViewport, querySelector } from "./utils";
function createRateBar(likes, dislikes) {
let rateBar = document.getElementById("ryd-bar-container");
@@ -23,8 +16,7 @@ function createRateBar(likes, dislikes) {
parseFloat(window.getComputedStyle(getDislikeButton()).width) +
(isRoundedDesign() ? 0 : 8);
const widthPercent =
likes + dislikes > 0 ? (likes / (likes + dislikes)) * 100 : 50;
const widthPercent = likes + dislikes > 0 ? (likes / (likes + dislikes)) * 100 : 50;
var likePercentage = parseFloat(widthPercent.toFixed(1));
const dislikePercentage = (100 - likePercentage).toLocaleString();
@@ -61,13 +53,10 @@ function createRateBar(likes, dislikes) {
colorDislikeStyle = "; background-color: " + getColorFromTheme(false);
}
let actions =
isNewDesign() && getButtons().id === "top-level-buttons-computed"
isNewDesign() && getButtons() === querySelector(extConfig.selectors.rateBar.newDesignActions)
? getButtons()
: document.getElementById("menu-container");
(
actions ||
document.querySelector("ytm-slim-video-action-bar-renderer")
).insertAdjacentHTML(
: querySelector(extConfig.selectors.rateBar.oldDesignActions);
(actions || querySelector(extConfig.selectors.rateBar.mobileActionBar)).insertAdjacentHTML(
"beforeend",
`
<div class="ryd-tooltip ryd-tooltip-${isNewDesign() ? "new" : "old"}-design" style="width: ${widthPx}px">
@@ -91,28 +80,23 @@ function createRateBar(likes, dislikes) {
if (isNewDesign()) {
// Add border between info and comments
let descriptionAndActionsElement = document.getElementById("top-row");
descriptionAndActionsElement.style.borderBottom =
"1px solid var(--yt-spec-10-percent-layer)";
let descriptionAndActionsElement = querySelector(extConfig.selectors.rateBar.topRow);
descriptionAndActionsElement.style.borderBottom = "1px solid var(--yt-spec-10-percent-layer)";
descriptionAndActionsElement.style.paddingBottom = "10px";
// Fix like/dislike ratio bar offset in new UI
document.getElementById("actions-inner").style.width = "revert";
querySelector(extConfig.selectors.rateBar.actionsInner).style.width = "revert";
if (isRoundedDesign()) {
document.getElementById("actions").style.flexDirection =
"row-reverse";
querySelector(extConfig.selectors.rateBar.actions).style.flexDirection = "row-reverse";
}
}
} else {
document.querySelector(`.ryd-tooltip`).style.width = widthPx + "px";
document.getElementById("ryd-bar").style.width = widthPercent + "%";
document.querySelector("#ryd-dislike-tooltip > #tooltip").innerHTML =
tooltipInnerHTML;
document.querySelector("#ryd-dislike-tooltip > #tooltip").innerHTML = tooltipInnerHTML;
if (extConfig.coloredBar) {
document.getElementById("ryd-bar-container").style.backgroundColor =
getColorFromTheme(false);
document.getElementById("ryd-bar").style.backgroundColor =
getColorFromTheme(true);
document.getElementById("ryd-bar-container").style.backgroundColor = getColorFromTheme(false);
document.getElementById("ryd-bar").style.backgroundColor = getColorFromTheme(true);
}
}
}
+43 -49
View File
@@ -1,6 +1,14 @@
import { isMobile, isShorts, extConfig } from "./state";
import { isInViewport, querySelector, querySelectorAll } from "./utils";
function getNativeButton(buttonContainer) {
return querySelector(extConfig.selectors.buttons.nativeButton, buttonContainer);
}
function isSegmentedButtonLayout() {
return querySelector(extConfig.selectors.buttons.segmentedContainer, getButtons()) !== undefined;
}
function getButtons() {
//--- If Watching Youtube Shorts: ---//
if (isShorts()) {
@@ -34,17 +42,10 @@ function getButtons() {
}
function getLikeButton() {
return getButtons().children[0].tagName ===
"YTD-SEGMENTED-LIKE-DISLIKE-BUTTON-RENDERER"
return isSegmentedButtonLayout()
? querySelector(extConfig.selectors.buttons.likeButton.segmented) ??
querySelector(
extConfig.selectors.buttons.likeButton.segmentedGetButtons,
getButtons(),
)
: querySelector(
extConfig.selectors.buttons.likeButton.notSegmented,
getButtons(),
);
querySelector(extConfig.selectors.buttons.likeButton.segmentedGetButtons, getButtons())
: querySelector(extConfig.selectors.buttons.likeButton.notSegmented, getButtons());
}
function getLikeTextContainer() {
@@ -52,57 +53,52 @@ function getLikeTextContainer() {
}
function getDislikeButton() {
if (
getButtons().children[0].tagName ===
"YTD-SEGMENTED-LIKE-DISLIKE-BUTTON-RENDERER"
) {
if (isSegmentedButtonLayout()) {
return (
querySelector(extConfig.selectors.buttons.dislikeButton.segmented) ??
querySelector(
extConfig.selectors.buttons.dislikeButton.segmentedGetButtons,
getButtons(),
)
querySelector(extConfig.selectors.buttons.dislikeButton.segmentedGetButtons, getButtons())
);
}
const notSegmentedMatch = querySelector(
extConfig.selectors.buttons.dislikeButton.notSegmented,
getButtons(),
);
const notSegmentedMatch = querySelector(extConfig.selectors.buttons.dislikeButton.notSegmented, getButtons());
if (notSegmentedMatch != null) {
return notSegmentedMatch;
}
if (isShorts()) {
return querySelector(["#dislike-button"], getButtons());
return querySelector(extConfig.selectors.buttons.dislikeButton.shortsFallback, getButtons());
}
return null;
}
function getTextContainerTemplate() {
const likeButton = getLikeButton();
const parentTemplate =
querySelector(extConfig.selectors.likeTextContainerTemplateParent, likeButton) ??
querySelector(extConfig.selectors.likeTextContainerTemplateParent);
return querySelector(extConfig.selectors.likeTextContainerTemplate, likeButton) ?? parentTemplate?.parentNode;
}
function updateDislikeButtonShape(dislikeButton) {
for (const className of extConfig.selectors.buttonClasses.iconButton) {
dislikeButton.classList.remove(className);
}
for (const className of extConfig.selectors.buttonClasses.iconLeading) {
dislikeButton.classList.add(className);
}
}
function createDislikeTextContainer() {
const textNodeClone = (
getLikeButton().querySelector(
".yt-spec-button-shape-next__button-text-content",
) ||
getLikeButton().querySelector("button > div[class*='cbox']") ||
(
getLikeButton().querySelector('div > span[role="text"]') ||
document.querySelector(
'button > div.yt-spec-button-shape-next__button-text-content > span[role="text"]',
)
).parentNode
).cloneNode(true);
const insertPreChild = getDislikeButton().querySelector("button");
const textNodeClone = getTextContainerTemplate().cloneNode(true);
const dislikeButton = getNativeButton(getDislikeButton());
const insertPreChild = dislikeButton;
insertPreChild.insertBefore(textNodeClone, null);
getDislikeButton()
.querySelector("button")
.classList.remove("yt-spec-button-shape-next--icon-button");
getDislikeButton()
.querySelector("button")
.classList.add("yt-spec-button-shape-next--icon-leading");
if (textNodeClone.querySelector("span[role='text']") === null) {
updateDislikeButtonShape(dislikeButton);
if (querySelector(extConfig.selectors.textContainerInner, textNodeClone) === undefined) {
const span = document.createElement("span");
span.setAttribute("role", "text");
while (textNodeClone.firstChild) {
@@ -116,11 +112,13 @@ function createDislikeTextContainer() {
function getDislikeTextContainer() {
let result;
const nativeDislikeButton = getNativeButton(getDislikeButton());
for (const selector of extConfig.selectors.dislikeTextContainer) {
result = getDislikeButton().querySelector(selector);
if (result !== null) {
if (result !== null && result !== nativeDislikeButton) {
break;
}
result = null;
}
if (result == null) {
result = createDislikeTextContainer();
@@ -129,11 +127,7 @@ function getDislikeTextContainer() {
}
function checkForSignInButton() {
if (
document.querySelector(
"a[href^='https://accounts.google.com/ServiceLogin']",
)
) {
if (querySelector(extConfig.selectors.signInButton)) {
return true;
} else {
return false;
+1 -1
View File
@@ -1,5 +1,5 @@
const DEV_API_URL = "https://localhost:7258";
const PROD_API_URL = "https://returnyoutubedislikeapi.com";
const DEV_API_URL = PROD_API_URL;
const runtime = typeof chrome !== "undefined" ? chrome.runtime : null;
const manifest = typeof runtime?.getManifest === "function" ? runtime.getManifest() : null;
+7 -26
View File
@@ -1,15 +1,5 @@
import {
getBrowser,
getVideoId,
numberFormat,
createObserver,
} from "./utils";
import {
checkForSignInButton,
getButtons,
getDislikeButton,
getLikeButton,
} from "./buttons";
import { getBrowser, getVideoId, numberFormat, createObserver, querySelector } from "./utils";
import { checkForSignInButton, getButtons, getDislikeButton, getLikeButton } from "./buttons";
import {
NEUTRAL_STATE,
LIKED_STATE,
@@ -122,11 +112,8 @@ function createSmartimationObserver() {
smartimationObserver.container = null;
}
const smartimationContainer = getButtons().querySelector("yt-smartimation");
if (
smartimationContainer &&
smartimationObserver.container != smartimationContainer
) {
const smartimationContainer = querySelector(extConfig.selectors.buttons.smartimation, getButtons());
if (smartimationContainer && smartimationObserver.container != smartimationContainer) {
console.log("Initializing smartimation mutation observer");
smartimationObserver.disconnect();
smartimationObserver.observe(smartimationContainer);
@@ -136,9 +123,7 @@ function createSmartimationObserver() {
function storageChangeHandler(changes, area) {
if (changes.disableVoteSubmission !== undefined) {
handleDisableVoteSubmissionChangeEvent(
changes.disableVoteSubmission.newValue,
);
handleDisableVoteSubmissionChangeEvent(changes.disableVoteSubmission.newValue);
}
if (changes.coloredThumbs !== undefined) {
handleColoredThumbsChangeEvent(changes.coloredThumbs.newValue);
@@ -153,14 +138,10 @@ function storageChangeHandler(changes, area) {
handleNumberDisplayFormatChangeEvent(changes.numberDisplayFormat.newValue);
}
if (changes.numberDisplayReformatLikes !== undefined) {
handleNumberDisplayReformatLikesChangeEvent(
changes.numberDisplayReformatLikes.newValue,
);
handleNumberDisplayReformatLikesChangeEvent(changes.numberDisplayReformatLikes.newValue);
}
if (changes.hidePremiumTeaser !== undefined) {
handleHidePremiumTeaserChangeEvent(
changes.hidePremiumTeaser.newValue,
);
handleHidePremiumTeaserChangeEvent(changes.hidePremiumTeaser.newValue);
}
}
@@ -17,6 +17,7 @@ const teaserState = {
abortController: null,
suppressionReasons: new Set(),
storageListener: null,
expanded: false,
};
export async function initPremiumTeaser() {
@@ -226,32 +227,41 @@ function ensurePanel() {
}
const panel = document.createElement("section");
panel.className = "ryd-premium-teaser";
panel.className = "ryd-premium-teaser is-collapsed";
panel.innerHTML = createPanelMarkup();
container.insertBefore(panel, container.firstChild);
const cta = panel.querySelector("#ryd-premium-teaser-cta");
if (cta) {
cta.addEventListener("click", (event) => {
event.preventDefault();
openTab(PATREON_JOIN_URL);
});
}
const infoLink = panel.querySelector("#ryd-premium-teaser-learn");
if (infoLink) {
infoLink.addEventListener("click", (event) => {
event.preventDefault();
openTab(CHANGELOG_URL);
});
}
const dismissButton = panel.querySelector("#ryd-premium-teaser-close");
if (dismissButton) {
dismissButton.addEventListener("click", handleManualDismiss);
}
const toggle = panel.querySelector("#ryd-premium-teaser-toggle");
if (toggle) {
toggle.addEventListener("click", (event) => {
event.preventDefault();
setExpanded(!teaserState.expanded);
});
}
const expandCta = panel.querySelector("#ryd-premium-teaser-cta");
if (expandCta) {
expandCta.addEventListener("click", (event) => {
event.preventDefault();
openTab(PATREON_JOIN_URL);
});
}
const learnMore = panel.querySelector("#ryd-premium-teaser-learn");
if (learnMore) {
learnMore.addEventListener("click", (event) => {
event.preventDefault();
openTab(CHANGELOG_URL);
});
}
teaserState.panelElement = panel;
setExpanded(false);
return panel;
}
@@ -275,6 +285,7 @@ function resetState() {
teaserState.abortController.abort();
teaserState.abortController = null;
}
teaserState.expanded = false;
}
function setLoading(isLoading) {
@@ -301,16 +312,13 @@ function updateCounts(payload) {
if (!panel) return;
const dislikesValue = panel.querySelector("#ryd-premium-teaser-dislikes");
const rawDislikesValue = panel.querySelector("#ryd-premium-teaser-raw");
const likesValue = panel.querySelector("#ryd-premium-teaser-likes");
const dislikes = normalizeNumber(payload?.dislikes);
const rawDislikes = normalizeNumber(payload?.rawDislikes ?? payload?.dislikes);
const likes = normalizeNumber(payload?.likes ?? payload?.rawLikes);
const rawLikes = normalizeNumber(payload?.rawLikes ?? payload?.likes);
if (dislikesValue) dislikesValue.textContent = dislikes;
if (rawDislikesValue) rawDislikesValue.textContent = rawDislikes;
if (likesValue) likesValue.textContent = likes;
if (dislikesValue) dislikesValue.textContent = rawDislikes;
if (likesValue) likesValue.textContent = rawLikes;
}
function normalizeNumber(value) {
@@ -325,60 +333,66 @@ function normalizeNumber(value) {
}
function createPanelMarkup() {
const extensionName = localize("extensionName");
const title = localize("premiumTeaser_title");
const subtitle = localize("premiumTeaser_subtitle");
const ctaText = localize("premiumTeaser_cta");
const secondaryText = localize("premiumTeaser_learn");
const closeLabel = localize("hidePremiumTeaser");
const statRaw = localize("premiumTeaser_statRaw");
const statDislikes = localize("premiumTeaser_statDislikes");
const statDislikes = localize("premiumTeaser_statRaw");
const statLikes = localize("premiumTeaser_statLikes");
const breadcrumbsAria = localize("premiumTeaser_breadcrumbsAria");
const breadcrumbsTitle = localize("premiumTeaser_breadcrumbsTitle");
const step1 = localize("premiumTeaser_breadcrumbStep1");
const step2 = localize("premiumTeaser_breadcrumbStep2");
const step3 = localize("premiumTeaser_breadcrumbStep3");
const expandLabel = localize("premiumAnalytics_expand");
const collapseLabel = localize("premiumAnalytics_collapse");
const highlightsAria = localize("changelog_section_highlights");
const highlightsTitle = localize("changelog_section_highlights");
const highlightOne = localize("changelog_feature_timeline_title");
const highlightTwo = localize("changelog_feature_map_title");
const highlightThree = localize("changelog_feature_quality_title");
return `
<header class="ryd-premium-teaser__header">
<div>
<span class="ryd-premium-teaser__badge">${extensionName}</span>
<h2 class="ryd-premium-teaser__title">${title}</h2>
<p class="ryd-premium-teaser__subtitle">${subtitle}</p>
<button type="button" class="ryd-premium-teaser__close" id="ryd-premium-teaser-close" aria-label="${closeLabel}" title="${closeLabel}">
<span class="ryd-premium-teaser__close-icon" aria-hidden="true">&times;</span>
</button>
<div class="ryd-premium-teaser__summary">
<div class="ryd-premium-teaser__counts" role="status" aria-live="polite">
<div class="ryd-premium-teaser__count">
<span class="ryd-premium-teaser__count-label">${statLikes}</span>
<span class="ryd-premium-teaser__count-value" id="ryd-premium-teaser-likes">—</span>
</div>
<div class="ryd-premium-teaser__count">
<span class="ryd-premium-teaser__count-label">${statDislikes}</span>
<span class="ryd-premium-teaser__count-value" id="ryd-premium-teaser-dislikes">—</span>
</div>
</div>
<div class="ryd-premium-teaser__actions">
</div>
<button
type="button"
class="ryd-premium-teaser__toggle"
id="ryd-premium-teaser-toggle"
aria-expanded="false"
aria-controls="ryd-premium-teaser-details"
aria-label="${expandLabel}"
title="${expandLabel}"
data-expand-label="${expandLabel}"
data-collapse-label="${collapseLabel}"
>
<span class="ryd-premium-teaser__toggle-icon" aria-hidden="true"></span>
</button>
<div class="ryd-premium-teaser__details" id="ryd-premium-teaser-details" aria-hidden="true">
<h3 class="ryd-premium-teaser__title">${title}</h3>
<p class="ryd-premium-teaser__subtitle">${subtitle}</p>
<section class="ryd-premium-teaser__highlights" aria-label="${highlightsAria}">
<h4 class="ryd-premium-teaser__highlights-title">${highlightsTitle}</h4>
<ul class="ryd-premium-teaser__highlights-list">
<li>${highlightOne}</li>
<li>${highlightTwo}</li>
<li>${highlightThree}</li>
</ul>
</section>
<div class="ryd-premium-teaser__links">
<a href="${PATREON_JOIN_URL}" class="ryd-premium-teaser__cta" id="ryd-premium-teaser-cta">${ctaText}</a>
<a href="${CHANGELOG_URL}" class="ryd-premium-teaser__secondary" id="ryd-premium-teaser-learn">${secondaryText}</a>
</div>
<button type="button" class="ryd-premium-teaser__close" id="ryd-premium-teaser-close" aria-label="${closeLabel}" title="${closeLabel}">
<span class="ryd-premium-teaser__close-icon" aria-hidden="true">&times;</span>
</button>
</header>
<div class="ryd-premium-teaser__body">
<div class="ryd-premium-teaser__stats" role="status" aria-live="polite">
<div class="ryd-premium-teaser__stat">
<span class="ryd-premium-teaser__stat-label">${statRaw}</span>
<span class="ryd-premium-teaser__stat-value" id="ryd-premium-teaser-raw">—</span>
</div>
<div class="ryd-premium-teaser__stat">
<span class="ryd-premium-teaser__stat-label">${statDislikes}</span>
<span class="ryd-premium-teaser__stat-value" id="ryd-premium-teaser-dislikes">—</span>
</div>
<div class="ryd-premium-teaser__stat">
<span class="ryd-premium-teaser__stat-label">${statLikes}</span>
<span class="ryd-premium-teaser__stat-value" id="ryd-premium-teaser-likes">—</span>
</div>
</div>
<p class="ryd-premium-teaser__status" id="ryd-premium-teaser-status"></p>
<section class="ryd-premium-teaser__breadcrumbs" aria-label="${breadcrumbsAria}">
<h3 class="ryd-premium-teaser__breadcrumbs-title">${breadcrumbsTitle}</h3>
<ol class="ryd-premium-teaser__breadcrumbs-list">
<li>${step1}</li>
<li>${step2}</li>
<li>${step3}</li>
</ol>
</section>
</div>
`;
}
@@ -417,3 +431,30 @@ function handleManualDismiss(event) {
event?.stopPropagation?.();
applySettingsSuppression(true, true);
}
function setExpanded(next) {
teaserState.expanded = !!next;
const panel = teaserState.panelElement;
if (!panel) return;
panel.classList.toggle("is-expanded", teaserState.expanded);
panel.classList.toggle("is-collapsed", !teaserState.expanded);
const details = panel.querySelector("#ryd-premium-teaser-details");
if (details) {
details.toggleAttribute("hidden", !teaserState.expanded);
details.setAttribute("aria-hidden", teaserState.expanded ? "false" : "true");
}
const toggle = panel.querySelector("#ryd-premium-teaser-toggle");
if (toggle) {
const expandText = toggle.getAttribute("data-expand-label") || "";
const collapseText = toggle.getAttribute("data-collapse-label") || expandText;
toggle.setAttribute("aria-expanded", teaserState.expanded ? "true" : "false");
const nextLabel = teaserState.expanded ? collapseText : expandText;
if (nextLabel) {
toggle.setAttribute("aria-label", nextLabel);
toggle.setAttribute("title", nextLabel);
}
}
}
@@ -99,6 +99,7 @@ describe("premiumAnalytics.teaser", () => {
dislikes: 123,
rawDislikes: 456,
likes: 789,
rawLikes: 654,
}),
});
});
@@ -125,13 +126,37 @@ describe("premiumAnalytics.teaser", () => {
expect(panel).not.toBeNull();
expect(global.fetch).toHaveBeenCalledWith("https://api.test/votes?videoId=abcdefghijk", expect.any(Object));
const raw = panel.querySelector("#ryd-premium-teaser-raw")?.textContent;
const dislikes = panel.querySelector("#ryd-premium-teaser-dislikes")?.textContent;
const likes = panel.querySelector("#ryd-premium-teaser-likes")?.textContent;
const dislikes = panel.querySelector("#ryd-premium-teaser-dislikes")?.textContent;
const details = panel.querySelector("#ryd-premium-teaser-details");
expect(raw).toBe("456");
expect(dislikes).toBe("123");
expect(likes).toBe("789");
expect(likes).toBe("654");
expect(dislikes).toBe("456");
expect(panel?.classList.contains("is-collapsed")).toBe(true);
expect(details?.hasAttribute("hidden")).toBe(true);
});
it("toggles the expanded state when the toggle is clicked", async () => {
getVideoId.mockReturnValue("togglevideo");
await initPremiumTeaser();
await flushPromises();
const panel = document.querySelector(".ryd-premium-teaser");
expect(panel).not.toBeNull();
const toggle = panel.querySelector("#ryd-premium-teaser-toggle");
const details = panel.querySelector("#ryd-premium-teaser-details");
expect(toggle?.getAttribute("aria-expanded")).toBe("false");
expect(details?.hasAttribute("hidden")).toBe(true);
toggle?.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
await flushPromises();
expect(panel?.classList.contains("is-expanded")).toBe(true);
expect(toggle?.getAttribute("aria-expanded")).toBe("true");
expect(details?.hasAttribute("hidden")).toBe(false);
});
it("removes the panel when suppressed and restores it when unsuppressed", async () => {
+125 -41
View File
@@ -15,6 +15,113 @@ const LIKED_STATE = "LIKED_STATE";
const DISLIKED_STATE = "DISLIKED_STATE";
const NEUTRAL_STATE = "NEUTRAL_STATE";
const DEFAULT_SELECTORS = {
dislikeTextContainer: [
".yt-spec-button-shape-next__button-text-content",
".ytSpecButtonShapeNextButtonTextContent",
"#text",
"yt-formatted-string",
"span[role='text']",
],
likeTextContainer: [
".yt-spec-button-shape-next__button-text-content",
".ytSpecButtonShapeNextButtonTextContent",
"#text",
"yt-formatted-string",
"span[role='text']",
],
likeTextContainerTemplate: [
".yt-spec-button-shape-next__button-text-content",
".ytSpecButtonShapeNextButtonTextContent",
"button > div[class*='cbox']",
],
likeTextContainerTemplateParent: [
'div > span[role="text"]',
'button > div.yt-spec-button-shape-next__button-text-content > span[role="text"]',
],
textContainerInner: ["span[role='text']"],
buttons: {
shorts: {
mobile: ["ytm-like-button-renderer"],
desktop: ["reel-action-bar-view-model", "#like-button > ytd-like-button-renderer"],
},
regular: {
mobile: [".slim-video-action-bar-actions"],
desktopMenu: ["ytd-menu-renderer.ytd-watch-metadata > div"],
desktopNoMenu: ["#top-level-buttons-computed"],
},
segmentedContainer: ["ytd-segmented-like-dislike-button-renderer"],
nativeButton: ["button"],
mobileText: [".button-renderer-text"],
shortsToggleButton: ["tp-yt-paper-button#button"],
smartimation: ["yt-smartimation"],
likeButton: {
segmented: ["#segmented-like-button"],
segmentedGetButtons: [":first-child > :first-child"],
notSegmented: ["like-button-view-model", ":first-child"],
},
dislikeButton: {
segmented: ["#segmented-dislike-button"],
segmentedGetButtons: [":first-child > :nth-child(2)"],
notSegmented: ["dislike-button-view-model", ":nth-child(2)", "#dislike-button"],
shortsFallback: ["#dislike-button"],
},
},
buttonClasses: {
iconButton: ["yt-spec-button-shape-next--icon-button", "ytSpecButtonShapeNextIconButton"],
iconLeading: ["yt-spec-button-shape-next--icon-leading", "ytSpecButtonShapeNextIconLeading"],
},
activeButtonClasses: ["style-default-active"],
likeCountButton: ["yt-formatted-string#text", "button"],
videoLoaded: [
"ytd-watch-grid[video-id='{videoId}']",
"ytd-watch-flexy[video-id='{videoId}']",
'#player[loading="false"]:not([hidden])',
],
shortsLoaded: {
containers: [".reel-video-in-sequence-new"],
thumbnail: [".reel-video-in-sequence-thumbnail"],
renderer: ["ytd-reel-video-renderer"],
overlay: ["#experiment-overlay"],
},
rateBar: {
newDesignActions: ["#top-level-buttons-computed"],
oldDesignActions: ["#menu-container"],
mobileActionBar: ["ytm-slim-video-action-bar-renderer"],
topRow: ["#top-row"],
actionsInner: ["#actions-inner"],
actions: ["#actions"],
},
signInButton: ["a[href^='https://accounts.google.com/ServiceLogin']"],
menuContainer: ["#menu-container"],
roundedDesign: ["#segmented-like-button", "like-button-view-model"],
};
function cloneConfig(value) {
if (value === undefined) return undefined;
return JSON.parse(JSON.stringify(value));
}
function mergeConfig(defaultValue, apiValue) {
if (apiValue === undefined || apiValue === null) {
return cloneConfig(defaultValue);
}
if (Array.isArray(apiValue)) {
return [...apiValue];
}
if (typeof apiValue !== "object" || Array.isArray(defaultValue)) {
return apiValue;
}
const merged = cloneConfig(defaultValue ?? {});
for (const [key, value] of Object.entries(apiValue)) {
merged[key] = mergeConfig(defaultValue?.[key], value);
}
return merged;
}
let extConfig = {
disableVoteSubmission: false,
disableLogging: false,
@@ -26,33 +133,7 @@ let extConfig = {
tooltipPercentageMode: "dash_like",
numberDisplayReformatLikes: false,
hidePremiumTeaser: false,
selectors: {
dislikeTextContainer: [],
likeTextContainer: [],
buttons: {
shorts: {
mobile: [],
desktop: [],
},
regular: {
mobile: [],
desktopMenu: [],
desktopNoMenu: [],
},
likeButton: {
segmented: [],
segmentedGetButtons: [],
notSegmented: [],
},
dislikeButton: {
segmented: [],
segmentedGetButtons: [],
notSegmented: [],
},
},
menuContainer: [],
roundedDesign: [],
},
selectors: cloneConfig(DEFAULT_SELECTORS),
};
let storedData = {
@@ -112,28 +193,30 @@ if (isShorts() && !shortsObserver) {
function isLikesDisabled() {
// return true if the like button's text doesn't contain any number
if (isMobile()) {
return /^\D*$/.test(getButtons().children[0].querySelector(".button-renderer-text").innerText);
return /^\D*$/.test(querySelector(extConfig.selectors.buttons.mobileText, getButtons().children[0]).innerText);
}
return /^\D*$/.test(getLikeTextContainer().innerText);
}
function isVideoLiked() {
const likeButton = querySelector(extConfig.selectors.buttons.nativeButton, getLikeButton());
if (isMobile()) {
return getLikeButton().querySelector("button").getAttribute("aria-label") === "true";
return likeButton.getAttribute("aria-label") === "true";
}
return (
getLikeButton().classList.contains("style-default-active") ||
getLikeButton().querySelector("button")?.getAttribute("aria-pressed") === "true"
extConfig.selectors.activeButtonClasses.some((className) => getLikeButton().classList.contains(className)) ||
likeButton?.getAttribute("aria-pressed") === "true"
);
}
function isVideoDisliked() {
const dislikeButton = querySelector(extConfig.selectors.buttons.nativeButton, getDislikeButton());
if (isMobile()) {
return getDislikeButton().querySelector("button").getAttribute("aria-label") === "true";
return dislikeButton.getAttribute("aria-label") === "true";
}
return (
getDislikeButton().classList.contains("style-default-active") ||
getDislikeButton().querySelector("button")?.getAttribute("aria-pressed") === "true"
extConfig.selectors.activeButtonClasses.some((className) => getDislikeButton().classList.contains(className)) ||
dislikeButton?.getAttribute("aria-pressed") === "true"
);
}
@@ -162,14 +245,15 @@ function setDislikes(dislikesCount) {
let _dislikeText;
if (!isLikesDisabled()) {
if (isMobile()) {
getButtons().children[1].querySelector(".button-renderer-text").innerText = dislikesCount;
querySelector(extConfig.selectors.buttons.mobileText, getButtons().children[1]).innerText = dislikesCount;
return;
}
_dislikeText = dislikesCount;
} else {
console.log("likes count disabled by creator");
if (isMobile()) {
getButtons().children[1].querySelector(".button-renderer-text").innerText = localize("TextLikesDisabled");
querySelector(extConfig.selectors.buttons.mobileText, getButtons().children[1]).innerText =
localize("TextLikesDisabled");
return;
}
_dislikeText = localize("TextLikesDisabled");
@@ -188,8 +272,7 @@ function getLikeCountFromButton() {
return false;
}
let likeButton =
getLikeButton().querySelector("yt-formatted-string#text") ?? getLikeButton().querySelector("button");
let likeButton = querySelector(extConfig.selectors.likeCountButton, getLikeButton());
let likesStr = likeButton.getAttribute("aria-label").replace(/\D/g, "");
return likesStr.length > 0 ? parseInt(likesStr) : false;
@@ -213,8 +296,8 @@ function processResponse(response, storedData) {
if (extConfig.coloredThumbs === true) {
if (isShorts()) {
// for shorts, leave deactivated buttons in default color
let shortLikeButton = getLikeButton().querySelector("tp-yt-paper-button#button");
let shortDislikeButton = getDislikeButton().querySelector("tp-yt-paper-button#button");
let shortLikeButton = querySelector(extConfig.selectors.buttons.shortsToggleButton, getLikeButton());
let shortDislikeButton = querySelector(extConfig.selectors.buttons.shortsToggleButton, getDislikeButton());
if (shortLikeButton.getAttribute("aria-pressed") === "true") {
shortLikeButton.style.color = getColorFromTheme(true);
}
@@ -228,6 +311,7 @@ function processResponse(response, storedData) {
getDislikeButton().style.color = getColorFromTheme(false);
}
}
//Temporary disabling this - it breaks all places where getButtons()[1] is used
// createStarRating(response.rating, isMobile());
}
@@ -296,7 +380,7 @@ async function initializeSelectors() {
.catch((error) => {
console.error("Error fetching selectors:", error);
});
extConfig.selectors = result ?? extConfig.selectors;
extConfig.selectors = mergeConfig(DEFAULT_SELECTORS, result);
console.log(result);
}
+27 -15
View File
@@ -1,5 +1,18 @@
import { extConfig, isShorts } from "./state";
const DEFAULT_SHORTS_LOADED_SELECTORS = {
containers: [".reel-video-in-sequence-new"],
thumbnail: [".reel-video-in-sequence-thumbnail"],
renderer: ["ytd-reel-video-renderer"],
overlay: ["#experiment-overlay"],
};
const DEFAULT_VIDEO_LOADED_SELECTORS = [
"ytd-watch-grid[video-id='{videoId}']",
"ytd-watch-flexy[video-id='{videoId}']",
'#player[loading="false"]:not([hidden])',
];
function numberFormat(numberState) {
return getNumberFormatter(extConfig.numberDisplayFormat).format(numberState);
}
@@ -113,20 +126,22 @@ function isInViewport(element) {
function isShortsLoaded(videoId) {
if (!videoId) return false;
const selectors = extConfig.selectors.shortsLoaded ?? DEFAULT_SHORTS_LOADED_SELECTORS;
// Find all reel containers
const reelContainers = document.querySelectorAll(".reel-video-in-sequence-new");
const reelContainers = querySelectorAll(selectors.containers);
for (const container of reelContainers) {
// Check if this container's thumbnail matches our video ID
const thumbnail = container.querySelector(".reel-video-in-sequence-thumbnail");
const thumbnail = querySelector(selectors.thumbnail, container);
if (thumbnail) {
const bgImage = thumbnail.style.backgroundImage;
// YouTube thumbnail URLs contain the video ID in the format: /vi/VIDEO_ID/
if ((bgImage && bgImage.includes(`/${videoId}/`)) || (!bgImage && isInViewport(container))) {
// Check if this container has the renderer with visible experiment-overlay
const renderer = container.querySelector("ytd-reel-video-renderer");
const renderer = querySelector(selectors.renderer, container);
if (renderer) {
const experimentOverlay = renderer.querySelector("#experiment-overlay");
const experimentOverlay = querySelector(selectors.overlay, renderer);
if (
experimentOverlay &&
!experimentOverlay.hidden &&
@@ -151,15 +166,10 @@ function isVideoLoaded() {
return isShortsLoaded(videoId);
}
const videoLoadedSelectors = extConfig.selectors.videoLoaded ?? DEFAULT_VIDEO_LOADED_SELECTORS;
// Regular video checks
return (
// desktop: spring 2024 UI
document.querySelector(`ytd-watch-grid[video-id='${videoId}']`) !== null ||
// desktop: older UI
document.querySelector(`ytd-watch-flexy[video-id='${videoId}']`) !== null ||
// mobile: no video-id attribute
document.querySelector('#player[loading="false"]:not([hidden])') !== null
);
return querySelector(videoLoadedSelectors.map((selector) => selector.replace("{videoId}", videoId))) !== undefined;
}
const originalConsole = {
@@ -210,7 +220,8 @@ function getColorFromTheme(voteIsLike) {
function querySelector(selectors, element) {
let result;
for (const selector of selectors) {
for (const selector of Array.isArray(selectors) ? selectors : [selectors]) {
if (!selector) continue;
result = (element ?? document).querySelector(selector);
if (result !== null) {
return result;
@@ -220,13 +231,14 @@ function querySelector(selectors, element) {
function querySelectorAll(selectors) {
let result;
for (const selector of selectors) {
for (const selector of Array.isArray(selectors) ? selectors : [selectors]) {
if (!selector) continue;
result = document.querySelectorAll(selector);
if (result.length !== 0) {
return result;
}
}
return result;
return result ?? document.querySelectorAll("__ryd-missing-selector__");
}
function createObserver(options, callback) {