Watch-on-LBRY/src/scripts/ytContent.tsx
2022-01-25 16:42:19 +00:00

181 lines
No EOL
6.7 KiB
TypeScript

import { h, render } from 'preact'
import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, TargetPlatform, targetPlatformSettings } from '../common/settings'
import { parseYouTubeURLTimeString } from '../common/yt'
const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t))
interface WatchOnLbryButtonParameters {
targetPlatform?: TargetPlatform
lbryPathname?: string
time?: number
}
interface Target {
platfrom: TargetPlatform
lbryPathname: string
time: number | null
}
function WatchOnLbryButton({ targetPlatform, lbryPathname, time }: WatchOnLbryButtonParameters) {
if (!lbryPathname || !targetPlatform) return null
const url = new URL(`${targetPlatform.domainPrefix}${lbryPathname}`)
if (time) url.searchParams.set('t', time.toFixed(0))
return <div style={{ display: 'flex', justifyContent: 'center', flexDirection: 'column' }}>
<a href={`${url.toString()}`} role='button'
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '12px',
borderRadius: '2px',
backgroundColor: targetPlatform.theme,
border: '0',
color: 'whitesmoke',
padding: '10px 16px',
marginRight: '4px',
fontSize: '14px',
textDecoration: 'none',
...targetPlatform.button.style?.button,
}}>
<img src={targetPlatform.button.icon} height={16}
style={{ transform: 'scale(1.5)', ...targetPlatform.button.style?.icon }} />
<span>{targetPlatform.button.text}</span>
</a>
</div>
}
function updateButton(mountPoint: HTMLDivElement, target: Target | null): void {
if (!target) return render(<WatchOnLbryButton />, mountPoint)
render(<WatchOnLbryButton targetPlatform={target.platfrom} lbryPathname={target.lbryPathname} time={target.time ?? undefined} />, mountPoint)
}
/** Returns a mount point for the button */
async function findButtonMountPoint(): Promise<HTMLDivElement> {
const id = 'watch-on-lbry-button-container'
let mountBefore: HTMLDivElement | null = null
const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
if (!sourcePlatform) throw new Error(`Unknown source of: ${location.href}`)
const exits: HTMLDivElement | null = document.querySelector(`#${id}`)
if (exits) return exits
while (!(mountBefore = document.querySelector(sourcePlatform.htmlQueries.mountButtonBefore))) await sleep(200)
const div = document.createElement('div')
div.id = id
div.style.display = 'flex'
div.style.alignItems = 'center'
mountBefore.parentElement?.insertBefore(div, mountBefore)
return div
}
async function findVideoElement() {
const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
if (!sourcePlatform) throw new Error(`Unknown source of: ${location.href}`)
let videoElement: HTMLVideoElement | null = null
while (!(videoElement = document.querySelector(sourcePlatform.htmlQueries.videoPlayer))) await sleep(200)
return videoElement
}
// We should get this from background, so the caching works and we don't get errors in the future if yt decides to impliment CORS
async function requestLbryPathname(videoId: string) {
const response = await new Promise<string | null | Error>((resolve) => chrome.runtime.sendMessage({ videoId }, resolve))
if (response instanceof Error) throw response
return response
}
// Start
(async () => {
const settings = await getExtensionSettingsAsync()
let updater: (() => Promise<void>)
// Listen Settings Change
chrome.storage.onChanged.addListener(async (changes, areaName) => {
if (areaName !== 'local') return
Object.assign(settings, Object.fromEntries(Object.entries(changes).map(([key, change]) => [key, change.newValue])))
if (changes.redirect) await onModeChange()
await updater()
})
/*
* 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
*/
// Listen URL Change
chrome.runtime.onMessage.addListener(() => updater())
async function getTargetByURL(url: URL) {
if (url.pathname !== '/watch') return null
const videoId = url.searchParams.get('v')
const lbryPathname = videoId && await requestLbryPathname(videoId)
const target: Target | null = lbryPathname ? { lbryPathname, platfrom: targetPlatformSettings[settings.targetPlatform], time: null } : null
return target
}
async function redirectTo({ lbryPathname, platfrom, time }: Target) {
const url = new URL(`${platfrom.domainPrefix}${lbryPathname}`)
if (time) url.searchParams.set('t', time.toFixed(0))
findVideoElement().then((videoElement) => {
videoElement.addEventListener('play', () => videoElement.pause(), { once: true })
videoElement.pause()
})
if (platfrom === targetPlatformSettings.app) {
if (document.hidden) await new Promise((resolve) => document.addEventListener('visibilitychange', resolve, { once: true }))
// On redirect with app, people might choose to cancel browser's dialog
// So we dont destroy the current window automatically for them
// And also we are keeping the same window for less distiraction
if (settings.redirect)
{
location.replace(url.toString())
}
else
{
open(url.toString(), '_blank')
if (window.history.length === 1) window.close()
else window.history.back()
}
}
else
location.replace(url.toString())
}
let removeVideoTimeUpdateListener: (() => void) | null = null
async function onModeChange() {
let target: Target | null = null
if (settings.redirect)
updater = async () => {
const url = new URL(location.href)
target = await getTargetByURL(url)
if (!target) return
target.time = url.searchParams.has('t') ? parseYouTubeURLTimeString(url.searchParams.get('t')!) : null
redirectTo(target)
}
else {
const mountPoint = await findButtonMountPoint()
const videoElement = await findVideoElement()
const getTime = () => videoElement.currentTime > 3 && videoElement.currentTime < videoElement.duration - 1 ? videoElement.currentTime : null
const onTimeUpdate = () => target && updateButton(mountPoint, Object.assign(target, { time: getTime() }))
removeVideoTimeUpdateListener?.call(null)
videoElement.addEventListener('timeupdate', onTimeUpdate)
removeVideoTimeUpdateListener = () => videoElement.removeEventListener('timeupdate', onTimeUpdate)
updater = async () => {
const url = new URL(location.href)
target = await getTargetByURL(url)
if (target) target.time = getTime()
updateButton(mountPoint, target)
}
}
}
await onModeChange()
})()