mirror of
https://github.com/LBRYFoundation/Watch-on-LBRY.git
synced 2025-08-23 17:47:26 +00:00
Watch-on-Lbry button
* Created a content script for YouTube that injects a styled button * Automatically pause the video when redirecting to the app The button location is rather finicky as certain polymer components seem to move around, causing random DOM elements to appear all over the place if it's not a "singleton" component. squash
This commit is contained in:
parent
40036013c2
commit
7954c29482
4 changed files with 139 additions and 36 deletions
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
|
|
108
src/scripts/ytContent.tsx
Normal file
108
src/scripts/ytContent.tsx
Normal file
|
@ -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<HTMLVideoElement>('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<void> {
|
||||
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<HTMLDivElement | void> {
|
||||
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 <div style={{ display: 'flex', justifyContent: 'center', flexDirection: 'column' }}>
|
||||
<a href={url} onClick={pauseVideo} role='button' children='Watch on Lbry'
|
||||
style={{
|
||||
borderRadius: '2px',
|
||||
backgroundColor: '#075656',
|
||||
border: '0',
|
||||
color: 'whitesmoke',
|
||||
padding: '10px 16px',
|
||||
marginRight: '5px',
|
||||
fontSize: '14px',
|
||||
textDecoration: 'none',
|
||||
}} />
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
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(<WatchOnLbryButton url={url} />, 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(<WatchOnLbryButton />, mountPoint))
|
||||
if (!req.url) return;
|
||||
handle(new URL(req.url));
|
||||
});
|
Loading…
Add table
Reference in a new issue