diff --git a/src/common/components/ButtonRadio.sass b/src/common/components/ButtonRadio.sass index c92b557..f8d28e5 100644 --- a/src/common/components/ButtonRadio.sass +++ b/src/common/components/ButtonRadio.sass @@ -3,10 +3,11 @@ .ButtonRadio display: flex justify-content: center + flex-wrap: wrap + gap: .25em .radio-button @extend .button - margin: 6px .radio-button.checked @extend .button.active diff --git a/src/common/settings.ts b/src/common/settings.ts index feab124..eca1314 100644 --- a/src/common/settings.ts +++ b/src/common/settings.ts @@ -1,29 +1,76 @@ -export type PlatformName = 'madiator.com' | 'odysee' | 'app' +export interface ExtensionSettings { + redirect: boolean + targetPlatform: TargetPlatformName +} + +export const DEFAULT_SETTINGS: ExtensionSettings = { redirect: true, targetPlatform: 'odysee' }; + +export function getExtensionSettingsAsync>(...keys: K): Promise> { + return new Promise(resolve => chrome.storage.local.get(keys, o => resolve(o as any))); +} + -export interface PlatformSettings -{ + +export type TargetPlatformName = 'madiator.com' | 'odysee' | 'app' +export interface TargetPlatformSettings { domainPrefix: string - display: string + displayName: string theme: string } -export const platformSettings: Record = { - 'madiator.com': { domainPrefix: 'https://madiator.com/', display: 'Madiator.com', theme: '#075656' }, - odysee: { domainPrefix: 'https://odysee.com/', display: 'Odysee', theme: '#1e013b' }, - app: { domainPrefix: 'lbry://', display: 'App', theme: '#075656' }, +export const TargetPlatformSettings: Record = { + 'madiator.com': { + domainPrefix: 'https://madiator.com/', + displayName: 'Madiator.com', + theme: '#075656' + }, + odysee: { + domainPrefix: 'https://odysee.com/', + displayName: 'Odysee', + theme: '#1e013b' + }, + app: { + domainPrefix: 'lbry://', + displayName: 'LBRY App', + theme: '#075656' + }, }; -export const getPlatfromSettingsEntiries = () => { - return Object.entries(platformSettings) as any as [Extract, PlatformSettings][] +export const getTargetPlatfromSettingsEntiries = () => { + return Object.entries(TargetPlatformSettings) as any as [Extract, TargetPlatformSettings][] } -export interface LbrySettings { - enabled: boolean - platform: PlatformName -} -export const DEFAULT_SETTINGS: LbrySettings = { enabled: true, platform: 'odysee' }; -export function getSettingsAsync>(...keys: K): Promise> { - return new Promise(resolve => chrome.storage.local.get(keys, o => resolve(o as any))); +export type SourcePlatfromName = 'youtube.com' | 'yewtu.be' +export interface SourcePlatfromSettings { + hostnames: string[] + htmlQueries: { + mountButtonBefore: string, + videoPlayer: string + } } + +export const SourcePlatfromSettings: Record = { + "yewtu.be": { + hostnames: ['yewtu.be'], + htmlQueries: { + mountButtonBefore: '#watch-on-youtube', + videoPlayer: '#player-container video' + } + }, + "youtube.com": { + hostnames: ['www.youtube.com'], + htmlQueries: { + mountButtonBefore: 'ytd-video-owner-renderer~#subscribe-button', + videoPlayer: '#ytd-player video' + } + } +} + +export function getSourcePlatfromSettingsFromHostname(hostname: string) { + const values = Object.values(SourcePlatfromSettings) + for (const settings of values) + if (settings.hostnames.includes(hostname)) return settings + return null +} \ No newline at end of file diff --git a/src/common/style.sass b/src/common/style.sass index 960e230..452d0b4 100644 --- a/src/common/style.sass +++ b/src/common/style.sass @@ -5,30 +5,29 @@ $btn-color: #075656 !default $btn-select: teal !default body - width: 400px + width: 30em text-align: center background-color: $background-color color: $text-color font-family: sans-serif + padding: 1em .container display: block text-align: center - margin: 0 32px - margin-bottom: 15px .button - border-radius: 5px + border-radius: .5em background-color: $btn-color - border: 4px solid $btn-color + border: .2em solid $btn-color color: $text-color font-size: 0.8rem font-weight: 400 - padding: 4px 15px + padding: .5em text-align: center &.active - border: 4px solid $btn-select + border-color: $btn-select &:focus outline: none diff --git a/src/common/yt.ts b/src/common/yt.ts index 1e2f08d..1bec869 100644 --- a/src/common/yt.ts +++ b/src/common/yt.ts @@ -14,7 +14,7 @@ interface YtResolverResponse { }; } -interface YtSubscription { +interface YtExportedJsonSubscription { id: string; etag: string; title: string; @@ -72,7 +72,7 @@ export const ytService = { * @returns the channel IDs */ readJson(jsonContents: string): string[] { - const subscriptions: YtSubscription[] = JSON.parse(jsonContents); + const subscriptions: YtExportedJsonSubscription[] = JSON.parse(jsonContents); jsonContents = '' return subscriptions.map(sub => sub.snippet.resourceId.channelId); }, @@ -86,7 +86,7 @@ export const ytService = { readCsv(csvContent: string): string[] { const rows = csvContent.split('\n') csvContent = '' - return rows.map((row) => row.substr(0, row.indexOf(','))) + return rows.map((row) => row.substring(0, row.indexOf(','))) }, /** diff --git a/src/manifest.json b/src/manifest.json index 2324d95..15df083 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -3,8 +3,7 @@ "version": "1.7.5", "permissions": [ "https://www.youtube.com/", - "https://invidio.us/channel/*", - "https://invidio.us/watch?v=*", + "https://yewtu.be/", "https://api.odysee.com/*", "https://lbry.tv/*", "https://odysee.com/*", @@ -15,7 +14,8 @@ "content_scripts": [ { "matches": [ - "https://www.youtube.com/*" + "https://www.youtube.com/*", + "https://yewtu.be/*" ], "js": [ "scripts/ytContent.js" diff --git a/src/popup/popup.sass b/src/popup/popup.sass index 0a40cd3..3cbb7e0 100644 --- a/src/popup/popup.sass +++ b/src/popup/popup.sass @@ -1,4 +1,13 @@ .radio-label font-size: 1.1rem - margin: 15px auto display: block + +.container + display: grid + grid-auto-flow: row + gap: 1.5em + +.container > section + display: grid + grid-auto-flow: row + gap: 1em \ No newline at end of file diff --git a/src/popup/popup.tsx b/src/popup/popup.tsx index c7ef962..fbab6a6 100644 --- a/src/popup/popup.tsx +++ b/src/popup/popup.tsx @@ -1,32 +1,36 @@ import { h, render } from 'preact' import ButtonRadio, { SelectionOption } from '../common/components/ButtonRadio' -import { getPlatfromSettingsEntiries, LbrySettings, PlatformName } from '../common/settings' +import { getTargetPlatfromSettingsEntiries, ExtensionSettings, TargetPlatformName } from '../common/settings' import { useLbrySettings } from '../common/useSettings' import './popup.sass' - - /** Utilty to set a setting in the browser */ -const setSetting = (setting: K, value: LbrySettings[K]) => chrome.storage.local.set({ [setting]: value }); +const setSetting = (setting: K, value: ExtensionSettings[K]) => chrome.storage.local.set({ [setting]: value }); /** Gets all the options for redirect destinations as selection options */ -const platformOptions: SelectionOption[] = getPlatfromSettingsEntiries() - .map(([value, { display }]) => ({ value, display })); +const platformOptions: SelectionOption[] = getTargetPlatfromSettingsEntiries() + .map(([value, { displayName: display }]) => ({ value, display })); function WatchOnLbryPopup() { - const { enabled, platform } = useLbrySettings(); + const { redirect, targetPlatform } = useLbrySettings(); return
- - setSetting('enabled', enabled.toLowerCase() === 'yes')} /> - - setSetting('platform', platform)} /> - - - - +
+ + setSetting('redirect', redirect.toLowerCase() === 'yes')} /> +
+
+ + setSetting('targetPlatform', platform)} /> +
+
+ + + + +
; } diff --git a/src/scripts/storageSetup.ts b/src/scripts/storageSetup.ts index d637431..f574108 100644 --- a/src/scripts/storageSetup.ts +++ b/src/scripts/storageSetup.ts @@ -1,11 +1,11 @@ -import { DEFAULT_SETTINGS, LbrySettings, getSettingsAsync } from '../common/settings'; +import { DEFAULT_SETTINGS, ExtensionSettings, getExtensionSettingsAsync } from '../common/settings'; /** Reset settings to default value and update the browser badge text */ async function initSettings() { - const settings = await getSettingsAsync(...Object.keys(DEFAULT_SETTINGS) as Array); + const settings = await getExtensionSettingsAsync(...Object.keys(DEFAULT_SETTINGS) as Array); // get all the values that aren't set and use them as a change set - const invalidEntries = (Object.entries(DEFAULT_SETTINGS) as Array<[keyof LbrySettings, LbrySettings[keyof LbrySettings]]>) + const invalidEntries = (Object.entries(DEFAULT_SETTINGS) as Array<[keyof ExtensionSettings, ExtensionSettings[keyof ExtensionSettings]]>) .filter(([k]) => settings[k] === null || settings[k] === undefined); // fix our local var and set it in storage for later @@ -15,12 +15,12 @@ async function initSettings() { chrome.storage.local.set(changeSet); } - chrome.browserAction.setBadgeText({ text: settings.enabled ? 'ON' : 'OFF' }); + chrome.browserAction.setBadgeText({ text: settings.redirect ? 'ON' : 'OFF' }); } chrome.storage.onChanged.addListener((changes, areaName) => { - if (areaName !== 'local' || !changes.enabled) return; - chrome.browserAction.setBadgeText({ text: changes.enabled.newValue ? 'ON' : 'OFF' }); + if (areaName !== 'local' || !changes.redirect) return; + chrome.browserAction.setBadgeText({ text: changes.redirect.newValue ? 'ON' : 'OFF' }); }); diff --git a/src/scripts/tabOnUpdated.ts b/src/scripts/tabOnUpdated.ts index aeb8316..f5b9fce 100644 --- a/src/scripts/tabOnUpdated.ts +++ b/src/scripts/tabOnUpdated.ts @@ -1,12 +1,12 @@ import { appRedirectUrl, parseProtocolUrl } from '../common/lbry-url' -import { getSettingsAsync, PlatformName } from '../common/settings' +import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, TargetPlatformName } from '../common/settings' import { YTDescriptor, ytService } from '../common/yt' export interface UpdateContext { descriptor: YTDescriptor /** LBRY URL fragment */ - pathname: string - enabled: boolean - platform: PlatformName + lbryPathname: string + redirect: boolean + targetPlatform: TargetPlatformName } async function resolveYT(descriptor: YTDescriptor) { @@ -16,26 +16,31 @@ async function resolveYT(descriptor: YTDescriptor) { return segments.join('/'); } -const pathnameCache: Record = {}; +const lbryPathnameCache: Record = {}; -async function ctxFromURL(url: string): Promise { - if (!url || !(url.startsWith('https://www.youtube.com/watch?v=') || url.startsWith('https://www.youtube.com/channel/'))) return; - url = new URL(url).href; - const { enabled, platform } = await getSettingsAsync('enabled', 'platform'); - const descriptor = ytService.getId(url); +async function ctxFromURL(href: string): Promise { + if (!href) return; + + const url = new URL(href); + if (!getSourcePlatfromSettingsFromHostname(url.hostname)) return + if (url.pathname.startsWith('/watch?')) return + if (url.pathname.startsWith('/channel?')) return + + const { redirect, targetPlatform } = await getExtensionSettingsAsync('redirect', 'targetPlatform'); + const descriptor = ytService.getId(href); if (!descriptor) return; // couldn't get the ID, so we're done - const res = url in pathnameCache ? pathnameCache[url] : await resolveYT(descriptor); - pathnameCache[url] = res; + const res = href in lbryPathnameCache ? lbryPathnameCache[href] : await resolveYT(descriptor); + lbryPathnameCache[href] = res; if (!res) return; // couldn't find it on lbry, so we're done - return { descriptor, pathname: res, enabled, platform }; + return { descriptor, lbryPathname: res, redirect, targetPlatform }; } // handles lbry.tv -> lbry app redirect chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, { url: tabUrl }) => { - const { enabled, platform } = await getSettingsAsync('enabled', 'platform'); - if (!enabled || platform !== 'app' || !changeInfo.url || !tabUrl?.startsWith('https://odysee.com/')) return; + const { redirect, targetPlatform } = await getExtensionSettingsAsync('redirect', 'targetPlatform'); + if (!redirect || targetPlatform !== 'app' || !changeInfo.url || !tabUrl?.startsWith('https://odysee.com/')) return; const url = appRedirectUrl(tabUrl, { encode: true }); if (!url) return; @@ -61,7 +66,5 @@ chrome.runtime.onMessage.addListener(({ url }: { url: string }, sender, sendResp // 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; - - ctxFromURL(url).then(ctx => chrome.tabs.sendMessage(tabId, ctx)); + if (url) ctxFromURL(url).then(ctx => chrome.tabs.sendMessage(tabId, ctx)); }); diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index 317882f..9e2d290 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -1,9 +1,11 @@ -import { PlatformName, platformSettings } from '../common/settings' +import { getSourcePlatfromSettingsFromHostname, TargetPlatformName, TargetPlatformSettings } from '../common/settings' import type { UpdateContext } from '../scripts/tabOnUpdated' import { h, JSX, render } from 'preact' const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t)); +function pauseAllVideos() { document.querySelectorAll('video').forEach(v => v.pause()); } + interface ButtonSettings { text: string icon: string @@ -14,7 +16,7 @@ interface ButtonSettings { } } -const buttonSettings: Record = { +const buttonSettings: Record = { app: { text: 'Watch on LBRY', icon: chrome.runtime.getURL('icons/lbry/lbry-logo.svg') @@ -35,32 +37,32 @@ const buttonSettings: Record = { interface ButtonParameters { - platform?: PlatformName - pathname?: string + targetPlatform?: TargetPlatformName + lbryPathname?: string time?: number } -export function WatchOnLbryButton({ platform = 'app', pathname, time }: ButtonParameters) { - if (!pathname || !platform) return null; - const platformSetting = platformSettings[platform]; - const buttonSetting = buttonSettings[platform]; +export function WatchOnLbryButton({ targetPlatform = 'app', lbryPathname, time }: ButtonParameters = {}) { + if (!lbryPathname || !targetPlatform) return null; + const targetPlatformSetting = TargetPlatformSettings[targetPlatform]; + const buttonSetting = buttonSettings[targetPlatform]; - const url = new URL(`${platformSetting.domainPrefix}${pathname}`) + const url = new URL(`${targetPlatformSetting.domainPrefix}${lbryPathname}`) if (time) url.searchParams.append('t', time.toFixed(0)) return
- { - 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'); - } + let mountBefore: HTMLDivElement | null = null + const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname) + if (!sourcePlatform) throw new Error(`Unknown source of: ${location.href}`) + + while (!(mountBefore = document.querySelector(sourcePlatform.htmlQueries.mountButtonBefore))) await sleep(200); - if (!ownerBar) return; const div = document.createElement('div'); div.style.display = 'flex'; - ownerBar.insertAdjacentElement('afterend', div); - + div.style.alignItems = 'center' + mountBefore.parentElement?.insertBefore(div, mountBefore) mountPoint = div } let videoElement: HTMLVideoElement | null = null; async function findVideoElement() { - while(!(videoElement = document.querySelector('#ytd-player video'))) await sleep(200) - videoElement.addEventListener('timeupdate', () => updateButton(ctxCache)) -} + const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname) + if (!sourcePlatform) throw new Error(`Unknown source of: ${location.href}`) -function pauseVideo() { document.querySelectorAll('video').forEach(v => v.pause()); } + while(!(videoElement = document.querySelector(sourcePlatform.htmlQueries.videoPlayer))) await sleep(200) -function openApp(url: string) { - pauseVideo(); - location.assign(url); + videoElement.addEventListener('timeupdate', () => updateButton(ctxCache)) } /** Compute the URL and determine whether or not a redirect should be performed. Delegates the redirect to callbacks. */ let ctxCache: UpdateContext | null = null -function handleURLChange (ctx: UpdateContext | null) { +function handleURLChange (ctx: UpdateContext | null): void { ctxCache = ctx updateButton(ctx) - if (ctx?.enabled) redirectTo(ctx) + if (ctx?.redirect) redirectTo(ctx) } -function updateButton(ctx: UpdateContext | null) { +function updateButton(ctx: UpdateContext | null): void { if (!mountPoint) return if (!ctx) return render(, mountPoint) if (ctx.descriptor.type !== 'video') return; const time = videoElement?.currentTime ?? 0 - const pathname = ctx.pathname - const platform = ctx.platform + const lbryPathname = ctx.lbryPathname + const targetPlatform = ctx.targetPlatform - render(, mountPoint) + render(, mountPoint) } -function redirectTo({ platform, pathname }: UpdateContext) { +function redirectTo({ targetPlatform, lbryPathname }: UpdateContext): void { const parseYouTubeTime = (timeString: string) => { const signs = timeString.replace(/[0-9]/g, '') @@ -139,13 +137,18 @@ function redirectTo({ platform, pathname }: UpdateContext) { return total.toString() } - const platformSetting = platformSettings[platform]; - const url = new URL(`${platformSetting.domainPrefix}${pathname}`) + const targetPlatformSetting = TargetPlatformSettings[targetPlatform]; + const url = new URL(`${targetPlatformSetting.domainPrefix}${lbryPathname}`) const time = new URL(location.href).searchParams.get('t') if (time) url.searchParams.append('t', parseYouTubeTime(time)) - if (platform === 'app') return openApp(url.toString()); + if (targetPlatform === 'app') + { + pauseAllVideos(); + location.assign(url); + return + } location.replace(url.toString()); } @@ -170,5 +173,5 @@ chrome.runtime.onMessage.addListener(async (ctx: UpdateContext) => handleURLChan /** On settings change */ chrome.storage.onChanged.addListener(async (changes, areaName) => { if (areaName !== 'local') return; - if (changes.platform) handleURLChange(await requestCtxFromUrl(location.href)) + if (changes.targetPlatform) handleURLChange(await requestCtxFromUrl(location.href)) }); \ No newline at end of file diff --git a/src/tools/YTtoLBRY.tsx b/src/tools/YTtoLBRY.tsx index 107c30d..a82abc6 100644 --- a/src/tools/YTtoLBRY.tsx +++ b/src/tools/YTtoLBRY.tsx @@ -1,6 +1,6 @@ import { h, render } from 'preact' import { useState } from 'preact/hooks' -import { getSettingsAsync, platformSettings } from '../common/settings' +import { getExtensionSettingsAsync, TargetPlatformSettings } from '../common/settings' import { getFileContent, ytService } from '../common/yt' import readme from './README.md' @@ -20,8 +20,8 @@ async function lbryChannelsFromFile(file: File) { ext === 'csv' ? ytService.readCsv : ytService.readJson)(await getFileContent(file))) const lbryUrls = await ytService.resolveById(...Array.from(ids).map(id => ({ id, type: 'channel' } as const))); - const { platform } = await getSettingsAsync('platform'); - const urlPrefix = platformSettings[platform].domainPrefix; + const { targetPlatform: platform } = await getExtensionSettingsAsync('targetPlatform'); + const urlPrefix = TargetPlatformSettings[platform].domainPrefix; return lbryUrls.map(channel => urlPrefix + channel); }