diff --git a/package.json b/package.json index 88e583d..b55d925 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "build:assets": "cpx \"src/**/*.{json,png}\" dist/", "watch:assets": "cpx --watch -v \"src/**/*.{json,png}\" dist/", "build:parcel": "cross-env NODE_ENV=production parcel build --no-source-maps --no-minify \"src/scripts/*.{js,ts}\" \"src/**/*.html\"", - "watch:parcel": "parcel watch --no-hmr --no-source-maps \"src/scripts/*.{js,ts}\" \"src/**/*.html\"", + "watch:parcel": "parcel watch --no-hmr --no-source-maps \"src/scripts/*.{js,jsx,ts,tsx}\" \"src/**/*.html\"", "build": "npm-run-all -l -p build:parcel build:assets", "watch": "npm-run-all -l -p watch:parcel watch:assets", "start:chrome": "web-ext run -t chromium --source-dir ./dist", diff --git a/src/manifest.json b/src/manifest.json index 4cb0da7..42132e7 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -2,8 +2,7 @@ "name": "Watch on LBRY", "version": "1.6.0", "permissions": [ - "https://www.youtube.com/watch?v=*", - "https://www.youtube.com/channel/*", + "https://www.youtube.com/", "https://invidio.us/channel/*", "https://invidio.us/watch?v=*", "https://api.lbry.com/*", @@ -11,6 +10,16 @@ "tabs", "storage" ], + "content_scripts": [ + { + "matches": [ + "https://www.youtube.com/*" + ], + "js": [ + "scripts/ytContent.js" + ] + } + ], "background": { "scripts": [ "scripts/storageSetup.js", diff --git a/src/scripts/tabOnUpdated.ts b/src/scripts/tabOnUpdated.ts index bb75ef9..4570d1a 100644 --- a/src/scripts/tabOnUpdated.ts +++ b/src/scripts/tabOnUpdated.ts @@ -1,43 +1,29 @@ -import { appRedirectUrl, parseProtocolUrl } from '../common/lbry-url'; -import { getSettingsAsync, redirectDomains } from '../common/settings'; -import { ytService } from '../common/yt'; +import { appRedirectUrl } from '../common/lbry-url'; +import { getSettingsAsync } from '../common/settings'; -function openApp(tabId: number, url: string) { +// handles lbry.tv -> lbry app redirect +chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, { url: tabUrl }) => { + const { enabled, redirect } = await getSettingsAsync('enabled', 'redirect'); + if (!enabled || redirect !== 'app' || !changeInfo.url || !tabUrl?.startsWith('https://lbry.tv/')) return; + + const url = appRedirectUrl(tabUrl, { encode: true }); + if (!url) return; chrome.tabs.update(tabId, { url }); alert('Opened link in LBRY App!'); // Better for UX since sometimes LBRY App doesn't take focus, if that is fixed, this can be removed - // Close tab if it lacks history and go back if it does chrome.tabs.executeScript(tabId, { code: `if (window.history.length === 1) { window.close(); } else { window.history.back(); - }` + } + document.querySelectorAll('video').forEach(v => v.pause()); + ` }); -} - -async function resolveYT(ytUrl: string) { - const descriptor = ytService.getId(ytUrl); - if (!descriptor) return; // can't parse YT url; may not be one - - const lbryProtocolUrl: string | null = await ytService.resolveById(descriptor).then(a => a[0]); - const segments = parseProtocolUrl(lbryProtocolUrl || '', { encode: true }); - if (segments.length === 0) return; - return segments.join('/'); -} - -chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, { url: tabUrl }) => { - const { enabled, redirect } = await getSettingsAsync('enabled', 'redirect'); - const urlPrefix = redirectDomains[redirect].prefix; - - if (!enabled || !changeInfo.url || !tabUrl) return; - - const url = redirect === 'app' && tabUrl.match(/\b(https:\/\/lbry.tv|lbry:\/\/)/g) ? appRedirectUrl(tabUrl, { encode: true }) - : await resolveYT(tabUrl); - - if (!url) return; - if (redirect === 'app') { - openApp(tabId, urlPrefix + url); - return; - } - chrome.tabs.executeScript(tabId, { code: `location.replace("${urlPrefix + url}")` }); +}); + +// relay youtube link changes to the content script +chrome.tabs.onUpdated.addListener((tabId, changeInfo, { url }) => { + if (!changeInfo.url || !url || + !(url.startsWith('https://www.youtube.com/watch?v=') || url.startsWith('https://www.youtube.com/channel/'))) return; + chrome.tabs.sendMessage(tabId, { url }); }); diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx new file mode 100644 index 0000000..3d65f95 --- /dev/null +++ b/src/scripts/ytContent.tsx @@ -0,0 +1,108 @@ +import { h, render } from 'preact'; + +import { parseProtocolUrl } from '../common/lbry-url'; +import { getSettingsAsync, LbrySettings, redirectDomains } from '../common/settings'; +import { YTDescriptor, ytService } from '../common/yt'; + +interface UpdaterOptions { + /** invoked if a redirect should be performed */ + onRedirect?(ctx: UpdateContext): void + /** invoked if a URL is found */ + onURL?(ctx: UpdateContext): void +} + +interface UpdateContext { + descriptor: YTDescriptor + url: string + enabled: boolean + redirect: LbrySettings['redirect'] +} + +function pauseVideo() { document.querySelectorAll('video').forEach(v => v.pause()); } + +function openApp(url: string) { + pauseVideo(); + location.assign(url); +} + +async function resolveYT(descriptor: YTDescriptor) { + const lbryProtocolUrl: string | null = await ytService.resolveById(descriptor).then(a => a[0]); + const segments = parseProtocolUrl(lbryProtocolUrl || '', { encode: true }); + if (segments.length === 0) return; + return segments.join('/'); +} + +/** Compute the URL and determine whether or not a redirect should be performed. Delegates the redirect to callbacks. */ +async function handleURLChange(url: URL | Location, { onRedirect, onURL }: UpdaterOptions): Promise { + const { enabled, redirect } = await getSettingsAsync('enabled', 'redirect'); + const urlPrefix = redirectDomains[redirect].prefix; + const descriptor = ytService.getId(url.href); + if (!descriptor) return; // couldn't get the ID, so we're done + const res = await resolveYT(descriptor); + if (!res) return; // couldn't find it on lbry, so we're done + + const ctx = { descriptor, url: urlPrefix + res, enabled, redirect }; + if (onURL) onURL(ctx); + if (enabled && onRedirect) onRedirect(ctx); +} + +/** Returns a mount point for the button */ +async function findMountPoint(): Promise { + const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t)); + let ownerBar = document.querySelector('ytd-video-owner-renderer'); + for (let i = 0; !ownerBar && i < 50; i++) { + await sleep(200); + ownerBar = document.querySelector('ytd-video-owner-renderer'); + } + + if (!ownerBar) return; + const div = document.createElement('div'); + div.style.display = 'flex'; + ownerBar.insertAdjacentElement('afterend', div); + return div; +} + +function WatchOnLbryButton({ url }: { url?: string }) { + if (!url) return null; + return
+ +
+} + + +const mountPointPromise = findMountPoint(); + +const handle = (url: URL | Location) => handleURLChange(url, { + async onURL({ descriptor: { type }, url }) { + const mountPoint = await mountPointPromise; + if (type !== 'video' || !mountPoint) return; + render(, mountPoint) + }, + onRedirect({ redirect, url }) { + if (redirect === 'app') return openApp(url); + location.replace(url); + }, +}); + +// handle the location on load of the page +handle(location); + +/* + * Gets messages from background script which relays tab update events. This is because there's no sensible way to detect + * history.pushState changes from a content script + */ +chrome.runtime.onMessage.addListener(async (req: { url: string }) => { + mountPointPromise.then(mountPoint => mountPoint && render(, mountPoint)) + if (!req.url) return; + handle(new URL(req.url)); +});