From f19892992270d9f04192ed0f4e49be39230d4ec7 Mon Sep 17 00:00:00 2001 From: Kevin Raoofi Date: Tue, 13 Oct 2020 13:55:53 -0400 Subject: [PATCH 1/5] Disable mininification + import changes Import changes should reduce the size of the bundle a little. Particularly, not getting the entirety of lodash. --- package.json | 2 +- src/common/settings.ts | 30 ------------------------------ src/common/useSettings.ts | 30 ++++++++++++++++++++++++++++++ src/common/yt.ts | 4 +++- src/popup/popup.tsx | 3 ++- 5 files changed, 36 insertions(+), 33 deletions(-) create mode 100644 src/common/useSettings.ts diff --git a/package.json b/package.json index f64f754..88e583d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "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 \"src/scripts/*.{js,ts}\" \"src/**/*.html\"", + "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\"", "build": "npm-run-all -l -p build:parcel build:assets", "watch": "npm-run-all -l -p watch:parcel watch:assets", diff --git a/src/common/settings.ts b/src/common/settings.ts index 7e039af..f333767 100644 --- a/src/common/settings.ts +++ b/src/common/settings.ts @@ -1,5 +1,3 @@ -import { useEffect, useReducer } from 'preact/hooks' - export interface LbrySettings { enabled: boolean redirect: keyof typeof redirectDomains @@ -16,31 +14,3 @@ export const redirectDomains = { export function getSettingsAsync>(...keys: K): Promise> { return new Promise(resolve => chrome.storage.local.get(keys, o => resolve(o as any))); } - -/** - * A hook to read the settings from local storage - * - * @param initial the default value. Must have all relevant keys present and should not change - */ -export function useSettings(initial: T) { - const [state, dispatch] = useReducer((state, nstate: Partial) => ({ ...state, ...nstate }), initial); - // register change listeners, gets current values, and cleans up the listeners on unload - useEffect(() => { - const changeListener = (changes: Record, areaName: string) => { - if (areaName !== 'local') return; - const changeSet = Object.keys(changes) - .filter(k => Object.keys(initial).includes(k)) - .map(k => [k, changes[k].newValue]); - if (changeSet.length === 0) return; // no changes; no use dispatching - dispatch(Object.fromEntries(changeSet)); - }; - chrome.storage.onChanged.addListener(changeListener); - chrome.storage.local.get(Object.keys(initial), o => dispatch(o as Partial)); - return () => chrome.storage.onChanged.removeListener(changeListener); - }, []); - - return state; -} - -/** A hook to read watch on lbry settings from local storage */ -export const useLbrySettings = () => useSettings(DEFAULT_SETTINGS); diff --git a/src/common/useSettings.ts b/src/common/useSettings.ts new file mode 100644 index 0000000..b1d9113 --- /dev/null +++ b/src/common/useSettings.ts @@ -0,0 +1,30 @@ +import { useReducer, useEffect } from 'preact/hooks'; +import { DEFAULT_SETTINGS } from './settings'; + +/** + * A hook to read the settings from local storage + * + * @param initial the default value. Must have all relevant keys present and should not change + */ +export function useSettings(initial: T) { + const [state, dispatch] = useReducer((state, nstate: Partial) => ({ ...state, ...nstate }), initial); + // register change listeners, gets current values, and cleans up the listeners on unload + useEffect(() => { + const changeListener = (changes: Record, areaName: string) => { + if (areaName !== 'local') return; + const changeSet = Object.keys(changes) + .filter(k => Object.keys(initial).includes(k)) + .map(k => [k, changes[k].newValue]); + if (changeSet.length === 0) return; // no changes; no use dispatching + dispatch(Object.fromEntries(changeSet)); + }; + chrome.storage.onChanged.addListener(changeListener); + chrome.storage.local.get(Object.keys(initial), o => dispatch(o as Partial)); + return () => chrome.storage.onChanged.removeListener(changeListener); + }, []); + + return state; + } + + /** A hook to read watch on lbry settings from local storage */ + export const useLbrySettings = () => useSettings(DEFAULT_SETTINGS); diff --git a/src/common/yt.ts b/src/common/yt.ts index 6a38534..be806e6 100644 --- a/src/common/yt.ts +++ b/src/common/yt.ts @@ -1,4 +1,6 @@ -import { chunk, groupBy, pickBy } from 'lodash'; +import chunk from 'lodash/chunk'; +import groupBy from 'lodash/groupBy'; +import pickBy from 'lodash/pickBy'; const LBRY_API_HOST = 'https://api.lbry.com'; const QUERY_CHUNK_SIZE = 300; diff --git a/src/popup/popup.tsx b/src/popup/popup.tsx index 5f350a4..9874700 100644 --- a/src/popup/popup.tsx +++ b/src/popup/popup.tsx @@ -1,7 +1,8 @@ import { h, render } from 'preact'; import ButtonRadio, { SelectionOption } from '../common/components/ButtonRadio'; -import { redirectDomains, useLbrySettings } from '../common/settings'; +import { redirectDomains } from '../common/settings'; +import { useLbrySettings } from '../common/useSettings'; import './popup.sass'; From 40036013c2f66c2dfceacdf929484fc173ff3b00 Mon Sep 17 00:00:00 2001 From: Kevin Raoofi Date: Mon, 19 Oct 2020 19:01:23 -0400 Subject: [PATCH 2/5] Set font-family to sans serif FireFox defaults to serif for non-button elements. This makes the look of the buttons consistent on both firefox and chrome based browsers. --- src/common/style.sass | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/style.sass b/src/common/style.sass index 9347c50..960e230 100644 --- a/src/common/style.sass +++ b/src/common/style.sass @@ -9,6 +9,7 @@ body text-align: center background-color: $background-color color: $text-color + font-family: sans-serif .container display: block From 7954c29482dc1c7b37abb10b5002216cb11df174 Mon Sep 17 00:00:00 2001 From: Kevin Raoofi Date: Wed, 28 Oct 2020 05:21:11 -0400 Subject: [PATCH 3/5] 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 --- package.json | 2 +- src/manifest.json | 13 ++++- src/scripts/tabOnUpdated.ts | 52 +++++++---------- src/scripts/ytContent.tsx | 108 ++++++++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 36 deletions(-) create mode 100644 src/scripts/ytContent.tsx 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)); +}); From 2e58a04333b775c7f2432138248db5db07dcbbe1 Mon Sep 17 00:00:00 2001 From: Kevin Raoofi Date: Wed, 28 Oct 2020 05:12:48 -0400 Subject: [PATCH 4/5] Update button on redirect change --- src/common/yt.ts | 2 +- src/scripts/ytContent.tsx | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/common/yt.ts b/src/common/yt.ts index be806e6..9ee3e53 100644 --- a/src/common/yt.ts +++ b/src/common/yt.ts @@ -89,7 +89,7 @@ export const ytService = { video_ids: groups['video']?.map(s => s.id).join(','), channel_ids: groups['channel']?.map(s => s.id).join(','), })); - return fetch(`${LBRY_API_HOST}/yt/resolve?${params}`) + return fetch(`${LBRY_API_HOST}/yt/resolve?${params}`, {cache: 'force-cache'}) .then(rsp => rsp.ok ? rsp.json() : null); })); diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index 3d65f95..ecd8dd9 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -106,3 +106,8 @@ chrome.runtime.onMessage.addListener(async (req: { url: string }) => { if (!req.url) return; handle(new URL(req.url)); }); + +chrome.storage.onChanged.addListener((changes, areaName) => { + if (areaName !== 'local' || !changes.redirect) return; + handle(new URL(location.href)) +}); From 08842c4ba868dfa39850e452cff6777de411484d Mon Sep 17 00:00:00 2001 From: Kevin Raoofi Date: Wed, 28 Oct 2020 05:13:06 -0400 Subject: [PATCH 5/5] Added SVG icon to button --- package.json | 2 +- src/icons/lbry-logo.svg | 79 +++++++++++++++++++++++++++++++++++++++ src/manifest.json | 3 +- src/scripts/ytContent.tsx | 7 +++- 4 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 src/icons/lbry-logo.svg diff --git a/package.json b/package.json index b55d925..4ec6e97 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "GPL-3.0", "scripts": { "build:assets": "cpx \"src/**/*.{json,png}\" dist/", - "watch:assets": "cpx --watch -v \"src/**/*.{json,png}\" dist/", + "watch:assets": "cpx --watch -v \"src/**/*.{json,png,svg}\" 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,jsx,ts,tsx}\" \"src/**/*.html\"", "build": "npm-run-all -l -p build:parcel build:assets", diff --git a/src/icons/lbry-logo.svg b/src/icons/lbry-logo.svg new file mode 100644 index 0000000..9f2352f --- /dev/null +++ b/src/icons/lbry-logo.svg @@ -0,0 +1,79 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/src/manifest.json b/src/manifest.json index 42132e7..b17d426 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -33,7 +33,8 @@ }, "web_accessible_resources": [ "popup.html", - "tools/YTtoLBRY.html" + "tools/YTtoLBRY.html", + "icons/lbry-logo.svg" ], "icons": { "16": "icons/icon16.png", diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index ecd8dd9..5c4fbe0 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -65,7 +65,12 @@ async function findMountPoint(): Promise { function WatchOnLbryButton({ url }: { url?: string }) { if (!url) return null; return } style={{ borderRadius: '2px', backgroundColor: '#075656',