Skip to content

feat: Open in MultiView (Context Menu) #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: manifest-v3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions manifest.json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ const manifest = {
{
matches: ["*://*.youtube.com/*"],
js: ["src/pages/content/yt-watch/contentScript.ts"],
css: ["src/pages/content/yt-watch/contentStyle.css"],
all_frames: true,
run_at: "document_end",
run_at: "document_start",
},
{
matches: ["*://*.youtube.com/embed/*"],
Expand All @@ -61,7 +62,7 @@ const manifest = {
],
web_accessible_resources: [
{
resources: ["contentStyle.css"],
resources: ["src/pages/content/yt-watch/contentStyle.css"],
matches: ["*://*.youtube.com/*", "*://*.holodex.net/*"],
},
],
Expand Down
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.5",
"@types/webextension-polyfill": "^0.10.7",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.8.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
"chrome-types": "^0.1.320",
Expand Down
49 changes: 33 additions & 16 deletions src/pages/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,26 +50,44 @@ chrome.runtime.onInstalled.addListener(() => {
});

chrome.runtime.onInstalled.addListener(() => {
let targetList = [
"https://*.youtube.com/",
"https://*.youtube.com/feed/*",
"https://*.youtube.com/watch?*",
"https://*.youtube.com/shorts/*",
"https://*.youtube.com/channel*",
"https://*.youtube.com/@*",
];

chrome.contextMenus.create({
id: "openInHolodex",
title: "Open in Holodex",
contexts: ["link"],
documentUrlPatterns: ["https://*.youtube.com/*"],
targetUrlPatterns: [
"https://*.youtube.com/",
"https://*.youtube.com/feed/*",
"https://*.youtube.com/channel*",
"https://*.youtube.com/watch?*",
"https://*.youtube.com/shorts/*",
"https://*.youtube.com/@*",
],
targetUrlPatterns: targetList,
});

for (let i = 0; i < 2; targetList.pop(), i++);

chrome.contextMenus.create({
id: "openInMultiView",
title: "Open in MultiView",
contexts: ["link", "action"],
documentUrlPatterns: ["https://*.youtube.com/*"],
targetUrlPatterns: targetList,
});
});

chrome.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId === "openInHolodex" && tab && tab.url) {
const linkUrl = info.linkUrl || tab.url;
await openHolodexUrl(linkUrl, tab);
if (!(tab && tab.url)) return;
const linkUrl = info.linkUrl || tab.url;
let isMultiview = false;

switch (info.menuItemId) {
case "openInMultiView":
isMultiview = true;
case "openInHolodex":
await openHolodexUrl(linkUrl, tab, isMultiview);
}
});

Expand All @@ -79,10 +97,9 @@ chrome.action.onClicked.addListener(async (tab) => {
});

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.greeting === "ytButton clicked")
if (request.pageUrl && sender.tab) {
openHolodexUrl(request.pageUrl, sender.tab);
sendResponse();
}
if (request.greeting === "ytButton clicked" && request.pageUrl && sender.tab) {
openHolodexUrl(request.pageUrl, sender.tab);
sendResponse();
}
return true;
});
4 changes: 2 additions & 2 deletions src/pages/content/yt-player/injectPlayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ manager.handleAsk(
const potKey = window.sessionStorage.getItem("iU5q-!O9@$");
console.log(potKey)
const potValue = window.sessionStorage.getItem((potKey ?? "_").split(",")[1]);

// The first value should be either the user's visitor data or their datasync id (if they're logged in).
console.log(visitorData, potValue);
const potToken = decodeCachedPoToken(visitorData, potValue);
Expand Down Expand Up @@ -314,4 +314,4 @@ manager.handleAsk(
};
}
}
);
);
144 changes: 3 additions & 141 deletions src/pages/content/yt-watch/contentScript.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,11 @@
import {
Options,
inject,
getHolodexUrl,
searchObject,
CANONICAL_URL_REGEX,
} from "@src/utils";
import { runtime } from "webextension-polyfill";
import injectedFilename from "./inject?script&module"
import "./yt-watch.css"

// @ts-expect-error "Signal" is a JS lib
import Signal from "signal-promise";

// If openHolodexInNewTab=true, opens given URL in a new focused tab and returns true,
// or null if somehow unsuccessful.
// If openHolodexInNewTab=false, opens given URL in the same tab, preserving the tab's session history,
// and returns false.
async function openUrl(url: string) {
if (await Options.get("openHolodexInNewTab")) {
const newWindow = window.open(url);
if (newWindow) {
newWindow.focus();
return true;
}
return null;
} else {
window.location.assign(url);
return false;
}
}
import { Options } from "@src/utils";

// Holodex button injected into YT pages
(async () => {
if (!(await Options.get("holodexButtonInYoutube"))) return;
console.log("[Holodex+] yt-watch script loaded");

const pageType = {shorts: false, watch: false}
let pageType = {shorts: false, watch: false}
let pageUrl: string;
let rendering = false;

Expand Down Expand Up @@ -70,7 +40,7 @@ async function openUrl(url: string) {
</svg>
`;

const ytElement = pageType.shorts
let ytElement = pageType.shorts
? document.getElementById("share-button")
: target.querySelector("yt-button-view-model");
if (!ytElement) return;
Expand Down Expand Up @@ -163,111 +133,3 @@ async function openUrl(url: string) {
}).observe(ytdApp, { childList: true, subtree: true });
});
})();


// openHolodexUrl handler

// Note regarding the Promise.resolve below:
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage
// "If you only want the listener to respond to messages of a certain type, you must define the listener as a non-async function,
// and return a Promise only for the messages the listener is meant to respond to — and otherwise return false or undefined"
runtime.onMessage.addListener((message) => {
if (message?.command !== "openHolodexUrl") return;
console.debug("[Holodex+] handling openHolodexUrl message");
return Promise.resolve(openHolodexUrl(message?.link));
});

async function openHolodexUrl(link = window.location.href) {
const url = await getHolodexUrl(link, findCanonicalUrl);
if (!url) return null;
const newTabOpened = await openUrl(url);
console.debug(
"[Holodex+]",
newTabOpened ? "new tab created:" : "updated tab:",
url
);
return { url, newTabOpened };
}

// Finds the "canonical URL" for a YT page, from which we can derive the Holodex URL.
async function findCanonicalUrl() {
if (!pageData) {
console.debug("[Holodex+] waiting for page data to become available...");
await pageDataSignal.wait(3000);
if (!pageData) {
console.log(
"[Holodex+] page data still unavailable - will default to fetch fallback to find canonical URL"
);
return null;
}
}
console.debug("[Holodex+] page data from", pageDataLabel, pageData);
const canonicalUrl = getCanonicalUrlFromData(pageData);
console.debug("[Holodex+] found canonical URL:", canonicalUrl);
return canonicalUrl;
}

// The canonical URL is available in link[rel="canonical"] and some other element attrs/content,
// but it does not update when internally navigating to another page,
// i.e. a user clicks a YT link from within a YT page.
// We can derive the canonical URL from ytd-app.data, or yt* global vars initially,
// but those are managed by YT's own scripts and thus inaccessible from the content script context.
// While we can access both in the page context via an injected page script,
// it's a PITA to round-trip messages between content script and injected page script.
// Instead, there are events we can hook into to broadcast data updates to this content script.
// See yt-watch.inject.ts
let pageData: unknown = null;
let pageDataLabel: string; // for debug logging
const pageDataSignal = new Signal(); // actually a condition variable in concurrency parlance
window.addEventListener("message", (evt: MessageEvent) => {
if (
evt.origin !== window.location.origin ||
evt.source !== window ||
!evt.data?.pageData
)
return;
//console.debug("[Holodex+] received pageData message:", evt.data);
({ pageData, pageDataLabel } = evt.data);
if (pageData) pageDataSignal.notify();
});


inject(injectedFilename);



// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getCanonicalUrlFromData(pageData: any) {
// Note: Not using pageData.url since it can e.g. be live/<videoId> which is not canonical.
// Following should be compatible with the fallback fetch in the background script,
// that is, the first canonical URL found on the page.
let canonicalUrl: string | null = null;
switch (pageData.page) {
case "watch":
case "shorts": {
const videoId = pageData.playerResponse?.videoDetails?.videoId;
if (videoId) {
// Technically, shorts canonical URL should /shorts/<videoId> but watch page works.
canonicalUrl = "https://www.youtube.com/watch?v=" + videoId;
}
break;
}
case "channel":
// data.response.microformat.microformatDataRenderer.urlCanonical also works.
canonicalUrl =
pageData.response?.metadata?.channelMetadataRenderer?.channelUrl;
break;
case "playlist": // not directly supported due to lack of corresponding Holodex page, so fall-through.
default:
// Find the first canonical URL found in data, which should also be the first canonical URL
// found in the whole page, which is what the fetch fallback in the background script does.
canonicalUrl = searchObject(pageData, (item) => {
if (typeof item.val === "string") {
const match = item.val.match(CANONICAL_URL_REGEX);
if (match) return "https://www.youtube.com" + match[0];
}
return null;
});
}
return canonicalUrl;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,15 @@
.yt-watch-holodex-icon path:nth-child(1) {
fill: var(--yt-button-icon-button-text-color, var(--yt-spec-text-secondary));
}

.yt-watch-holodex-icon path:nth-child(2) {
stroke: var(--yt-button-icon-button-text-color, var(--yt-spec-text-secondary));
}

.yt-watch-holodex-btn:hover .yt-watch-holodex-icon>path:nth-child(1) {
.yt-watch-holodex-btn:hover .yt-watch-holodex-icon > path:nth-child(1) {
fill: #f06292;
}

.yt-watch-holodex-btn:hover .yt-watch-holodex-icon>path:nth-child(2) {
.yt-watch-holodex-btn:hover .yt-watch-holodex-icon > path:nth-child(2) {
stroke: #5da2f2;
}

Expand All @@ -42,8 +41,8 @@ yt-holodex-button-shape {
inset: auto 68px auto auto !important;
}

@media screen and (max-width: 1200px) {
.yt-watch-holodex-label {
display: none;
}
}
/*@media screen and (max-width: 1200px) {*/
/* .yt-watch-holodex-label {*/
/* display: none;*/
/* }*/
/*}*/
61 changes: 0 additions & 61 deletions src/pages/content/yt-watch/inject.ts

This file was deleted.

Loading