Skip to content

Commit

Permalink
Add notification box support
Browse files Browse the repository at this point in the history
Fixes #64
  • Loading branch information
ajayyy committed Dec 11, 2024
1 parent 25d3e2b commit 0c86347
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 15 deletions.
2 changes: 2 additions & 0 deletions src/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { cleanPage } from "./utils/pageCleaner";
import { setupPageTitleHandler } from "./titles/pageTitleHandler";
import { setupWatchPageBrandingCleanup } from "./videoBranding/watchPageBrandingHandler";
import { addHotkeyListener } from "./utils/keybinds";
import { setupNotificationHandler } from "./videoBranding/notificationHandler";

cleanPage();
addCssToPage();
Expand All @@ -21,6 +22,7 @@ addTitleChangeListener(() => void replaceCurrentVideoBranding().catch(logError))
setupOptionChangeListener();
setupPageTitleHandler();
addHotkeyListener();
setupNotificationHandler().catch(logError);

setupTitlebarCleanup();
setupWatchPageBrandingCleanup();
Expand Down
5 changes: 4 additions & 1 deletion src/thumbnails/thumbnailRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,8 +529,10 @@ function getThumbnailSelector(brandingLocation: BrandingLocation): string {
case BrandingLocation.Watch:
case BrandingLocation.ChannelTrailer:
return ".ytp-cued-thumbnail-overlay-image";
case BrandingLocation.UpNextPreview:
case BrandingLocation.UpNextPreview:
return ".ytp-tooltip-bg";
case BrandingLocation.Notification:
return ".thumbnail-container img";
default:
throw new Error("Invalid branding location");
}
Expand Down Expand Up @@ -876,6 +878,7 @@ function resetToShowOriginalThumbnail(image: HTMLImageElement, brandingLocation:
|| brandingLocation === BrandingLocation.Autoplay
|| brandingLocation === BrandingLocation.EmbedSuggestions
|| brandingLocation === BrandingLocation.Related
|| brandingLocation === BrandingLocation.Notification
|| !!image.closest("ytd-grid-playlist-renderer")
|| isLiveCover(image)
|| image.parentElement?.classList.contains("ytp-cued-thumbnail-overlay")) {
Expand Down
49 changes: 36 additions & 13 deletions src/titles/titleRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { countTitleReplacement } from "../config/stats";
import { onMobile } from "../../maze-utils/src/pageInfo";
import { isFirefoxOrSafari, waitFor } from "../../maze-utils/src";
import { isSafari } from "../../maze-utils/src/config";
import { notificationToTitle, titleToNotificationFormat } from "../videoBranding/notificationHandler";

enum WatchPageType {
Video,
Expand All @@ -32,15 +33,15 @@ export async function replaceTitle(element: HTMLElement, videoID: VideoID, showC
if (brandingLocation === BrandingLocation.Watch) {
const currentWatchPageType = document.URL.includes("watch") ? WatchPageType.Video : WatchPageType.Miniplayer;

if (lastWatchVideoID && originalTitleElement?.textContent
&& videoID !== lastWatchVideoID && originalTitleElement.textContent === lastWatchTitle
if (lastWatchVideoID && getOriginalTitleText(originalTitleElement, brandingLocation)
&& videoID !== lastWatchVideoID && getOriginalTitleText(originalTitleElement, brandingLocation) === lastWatchTitle
&& lastUrlWatchPageType === currentWatchPageType) {
// Don't reset it if it hasn't changed videos yet, will be handled by title change listener
return false;
}

if (lastWatchVideoID !== videoID) {
lastWatchTitle = originalTitleElement?.textContent ?? "";
lastWatchTitle = getOriginalTitleText(originalTitleElement, brandingLocation);
lastWatchVideoID = videoID;
lastUrlWatchPageType = currentWatchPageType;
}
Expand Down Expand Up @@ -72,7 +73,7 @@ export async function replaceTitle(element: HTMLElement, videoID: VideoID, showC
if (!await isOnCorrectVideo(element, brandingLocation, videoID)) return false;

const title = titleData?.title;
const originalTitle = originalTitleElement?.textContent?.trim?.() ?? "";
const originalTitle = getOriginalTitleText(originalTitleElement, brandingLocation).trim();
if (title && await shouldUseCrowdsourcedTitles(videoID)
// If there are just formatting changes, and the user doesn't want those, don't replace
&& (await getTitleFormatting(videoID) !== TitleFormatting.Disable || originalTitle.toLowerCase() !== title.toLowerCase())
Expand All @@ -81,8 +82,8 @@ export async function replaceTitle(element: HTMLElement, videoID: VideoID, showC
const formattedTitle = await formatTitle(title, true, videoID);
if (!await isOnCorrectVideo(element, brandingLocation, videoID)) return false;

if (originalTitleElement?.textContent
&& originalTitleElement.textContent.trim() === formattedTitle) {
if (getOriginalTitleText(originalTitleElement, brandingLocation)
&& getOriginalTitleText(originalTitleElement, brandingLocation).trim() === formattedTitle) {
showOriginalTitle(element, brandingLocation);
return false;
}
Expand All @@ -95,12 +96,12 @@ export async function replaceTitle(element: HTMLElement, videoID: VideoID, showC
countTitleReplacement(videoID);
} else {
// innerText is blank when visibility hidden
if (originalTitleElement.textContent?.length === 0) {
if (getOriginalTitleText(originalTitleElement, brandingLocation).length === 0) {
await waitFor(() => originalTitleElement!.textContent!.length > 0, 5000).catch(() => null);
}

if (originalTitleElement.textContent) {
const originalText = originalTitleElement.textContent.trim();
if (getOriginalTitleText(originalTitleElement, brandingLocation)) {
const originalText = getOriginalTitleText(originalTitleElement, brandingLocation).trim();
const modifiedTitle = await formatTitle(originalText, false, videoID);
if (!await isOnCorrectVideo(element, brandingLocation, videoID)) return false;

Expand Down Expand Up @@ -183,7 +184,7 @@ function showOriginalTitle(element: HTMLElement, brandingLocation: BrandingLocat
if (Config.config!.showOriginalOnHover) {
findShowOriginalButton(originalTitleElement, brandingLocation).then((buttonElement) => {
if (buttonElement) {
buttonElement.title = originalTitleElement.textContent ?? "";
buttonElement.title = getOriginalTitleText(originalTitleElement, brandingLocation);
}
}).catch(logError);
}
Expand All @@ -194,11 +195,11 @@ function showOriginalTitle(element: HTMLElement, brandingLocation: BrandingLocat
originalTitleElement.style.setProperty("display", "inline-block", "important");
}

setCurrentVideoTitle(originalTitleElement.textContent ?? "");
setCurrentVideoTitle(getOriginalTitleText(originalTitleElement, brandingLocation));
break;
}
default: {
originalTitleElement.title = originalTitleElement.textContent?.trim() ?? "";
originalTitleElement.title = getOriginalTitleText(originalTitleElement, brandingLocation).trim();
break;
}
}
Expand Down Expand Up @@ -243,6 +244,10 @@ function setCustomTitle(title: string, element: HTMLElement, brandingLocation: B
const originalTitleElement = getOriginalTitleElement(element, brandingLocation);
const titleElement = getOrCreateTitleElement(element, brandingLocation, originalTitleElement);

if (brandingLocation === BrandingLocation.Notification) {
title = titleToNotificationFormat(title, originalTitleElement?.textContent ?? "");
}

// To support extensions like Tube Archivist that add nodes
const children = titleElement.childNodes;
if (children.length > 1) {
Expand Down Expand Up @@ -314,11 +319,13 @@ function getTitleSelector(brandingLocation: BrandingLocation): string[] {
return [".ytp-videowall-still-info-title"];
case BrandingLocation.EmbedSuggestions:
return [".ytp-suggestion-title"];
case BrandingLocation.UpNextPreview:
case BrandingLocation.UpNextPreview:
return [
".ytp-tooltip-text-no-title",
".ytp-tooltip-text"
];
case BrandingLocation.Notification:
return [".text yt-formatted-string"]
default:
throw new Error("Invalid branding location");
}
Expand All @@ -344,11 +351,18 @@ function createTitleElement(element: HTMLElement, originalTitleElement: HTMLElem
titleElement.style.textDecoration = "none";
}

if (brandingLocation === BrandingLocation.Notification) {
// For some reason you have to set before removing
titleElement.setAttribute("is-empty", "");
titleElement.removeAttribute("is-empty");
}

titleElement.classList.add("cbCustomTitle");

if (brandingLocation === BrandingLocation.EndRecommendations
|| brandingLocation === BrandingLocation.Autoplay
|| brandingLocation === BrandingLocation.EmbedSuggestions
|| brandingLocation === BrandingLocation.Notification
|| originalTitleElement.id === "movie-title"
|| (originalTitleElement.id === "title" && originalTitleElement.parentElement?.id === "description")) {
const container = document.createElement("div");
Expand Down Expand Up @@ -664,3 +678,12 @@ async function createShowOriginalButton(originalTitleElement: HTMLElement,

return buttonElement;
}

function getOriginalTitleText(originalTitleElement: HTMLElement, brandingLocation: BrandingLocation): string {
switch (brandingLocation) {
case BrandingLocation.Notification:
return notificationToTitle(originalTitleElement?.textContent ?? "");
default:
return originalTitleElement?.textContent ?? "";
}
}
80 changes: 80 additions & 0 deletions src/videoBranding/notificationHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { waitFor } from "../../maze-utils/src";
import { waitForElement } from "../../maze-utils/src/dom";
import { onMobile } from "../../maze-utils/src/pageInfo";
import { isOnInvidious } from "../../maze-utils/src/video";
import { getOriginalTitleElement } from "../titles/titleRenderer";
import { logError } from "../utils/logger";
import { BrandingLocation, replaceVideoCardBranding } from "./videoBranding";

let mutationObserver: MutationObserver | null = null;
async function onNotificationMenuOpened() {
const notificationMenu = await waitForElement("ytd-multi-page-menu-renderer")!;

if (mutationObserver) {
mutationObserver.disconnect();
} else {
mutationObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length > 0) {
for (const node of mutation.addedNodes) {
if (node instanceof HTMLElement && node.tagName.toLowerCase() === "ytd-notification-renderer") {
replaceNotificationBranding(node);
}
}
}
});
});
}

mutationObserver.observe(notificationMenu, { childList: true, subtree: true });
}

function replaceNotificationBranding(notification: HTMLElement) {
// Only if this notification format is supported
const originalTitle = getOriginalTitleElement(notification as HTMLElement, BrandingLocation.Notification)?.textContent;
if (originalTitle && notificationToTitle(originalTitle)) {
replaceVideoCardBranding(notification as HTMLElement, BrandingLocation.Notification).catch(logError);
}
}

export async function setupNotificationHandler() {
if (!onMobile() && !isOnInvidious()) {
const notificationButton = await waitFor(() => document.querySelector("ytd-notification-topbar-button-renderer"), 20000, 500);

if (notificationButton) {
notificationButton.addEventListener("click", () => void(onNotificationMenuOpened()));
}
}
}

const notificationFormats = [
"$CHANNEL$ uploaded: $TITLE$"
];
const channelTemplate = "$CHANNEL$";
const titleTemplate = "$TITLE$";
function formatToRegex(format: string): RegExp {
return new RegExp(format.replace(channelTemplate, "(.+)").replace(titleTemplate, "(.+)"), "i");
}
export function notificationToTitle(title: string): string {
for (const format of notificationFormats) {
const titleMatch = title.match(formatToRegex(format))?.[2];

if (titleMatch) {
return titleMatch;
}
}

return "";
}

export function titleToNotificationFormat(newTitle: string, originalTitle: string): string {
for (const format of notificationFormats) {
const titleMatch = originalTitle.match(formatToRegex(format))?.[2];

if (titleMatch) {
return originalTitle.replace(titleMatch, newTitle);
}
}

return "";
}
5 changes: 4 additions & 1 deletion src/videoBranding/videoBranding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export enum BrandingLocation {
Autoplay,
EndRecommendations,
EmbedSuggestions,
UpNextPreview
UpNextPreview,
Notification
}

export type ShowCustomBrandingInfo = {
Expand Down Expand Up @@ -259,6 +260,8 @@ export function getLinkElement(element: HTMLElement, brandingLocation: BrandingL
case BrandingLocation.EmbedSuggestions:
case BrandingLocation.UpNextPreview:
return element as HTMLAnchorElement;
case BrandingLocation.Notification:
return element.querySelector("a");
default:
throw new Error("Invalid branding location");
}
Expand Down

0 comments on commit 0c86347

Please sign in to comment.