🍙 new stuff and some changes

- channel buttons and redirect
- in button mode if there is no target try to find lbry url in the description
- used a loop in content script instead of events and stuff to make it less confusing
This commit is contained in:
Shiba 2022-07-02 15:15:36 +00:00
parent 66cb8ccccf
commit c895d53253
6 changed files with 217 additions and 288 deletions

97
package-lock.json generated
View file

@ -4826,91 +4826,6 @@
"timsort": "^0.3.0" "timsort": "^0.3.0"
} }
}, },
"css-loader": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz",
"integrity": "sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==",
"dev": true,
"requires": {
"icss-utils": "^5.1.0",
"postcss": "^8.4.7",
"postcss-modules-extract-imports": "^3.0.0",
"postcss-modules-local-by-default": "^4.0.0",
"postcss-modules-scope": "^3.0.0",
"postcss-modules-values": "^4.0.0",
"postcss-value-parser": "^4.2.0",
"semver": "^7.3.5"
},
"dependencies": {
"nanoid": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
"integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
"dev": true
},
"postcss": {
"version": "8.4.13",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.13.tgz",
"integrity": "sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA==",
"dev": true,
"requires": {
"nanoid": "^3.3.3",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
}
},
"postcss-modules-extract-imports": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
"integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
"dev": true
},
"postcss-modules-local-by-default": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz",
"integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==",
"dev": true,
"requires": {
"icss-utils": "^5.0.0",
"postcss-selector-parser": "^6.0.2",
"postcss-value-parser": "^4.1.0"
}
},
"postcss-modules-scope": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz",
"integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==",
"dev": true,
"requires": {
"postcss-selector-parser": "^6.0.4"
}
},
"postcss-modules-values": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
"integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
"dev": true,
"requires": {
"icss-utils": "^5.0.0"
}
},
"postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
"semver": {
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
}
}
}
},
"css-modules-loader-core": { "css-modules-loader-core": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/css-modules-loader-core/-/css-modules-loader-core-1.1.0.tgz", "resolved": "https://registry.npmjs.org/css-modules-loader-core/-/css-modules-loader-core-1.1.0.tgz",
@ -7923,12 +7838,6 @@
"integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=",
"dev": true "dev": true
}, },
"icss-utils": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
"integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
"dev": true
},
"ieee754": { "ieee754": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@ -14965,12 +14874,6 @@
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
"dev": true "dev": true
}, },
"source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true
},
"source-map-resolve": { "source-map-resolve": {
"version": "0.5.3", "version": "0.5.3",
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",

View file

@ -5,29 +5,19 @@
"scripts": { "scripts": {
"build:assets": "cpx \"src/**/*.{json,png,svg}\" dist/", "build:assets": "cpx \"src/**/*.{json,png,svg}\" dist/",
"watch:assets": "cpx --watch -v \"src/**/*.{json,png,svg}\" 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/**/*.{js,jsx,ts,tsx}\" \"src/**/*.html\" \"src/**/*.md\"", "build:parcel": "cross-env NODE_ENV=production parcel build --no-source-maps --no-minify \"src/**/*.{js,jsx,ts,tsx}\" \"src/**/*.html\" \"src/**/*.md\"",
"watch:parcel": "parcel watch --no-hmr --no-source-maps \"src/**/*.{js,jsx,ts,tsx}\" \"src/**/*.html\" \"src/**/*.md\"", "watch:parcel": "parcel watch --no-hmr --no-source-maps \"src/**/*.{js,jsx,ts,tsx}\" \"src/**/*.html\" \"src/**/*.md\"",
"build:webext": "web-ext build --source-dir ./dist --overwrite-dest", "build:webext": "web-ext build --source-dir ./dist --overwrite-dest",
"clear:dist": "rm -r ./dist ; mkdir ./dist", "clear:dist": "rm -r ./dist ; mkdir ./dist",
"build:base": "npm-run-all -l -p build:parcel build:assets", "build:base": "npm-run-all -l -p build:parcel build:assets",
"pick:manifest:v2": "cp -b ./manifest.v2.json ./dist/manifest.json", "pick:manifest:v2": "cp -b ./manifest.v2.json ./dist/manifest.json",
"pick:manifest:v3": "cp -b ./manifest.v3.json ./dist/manifest.json", "pick:manifest:v3": "cp -b ./manifest.v3.json ./dist/manifest.json",
"build:v2": "npm run clear:dist ; npm run build:base && npm run pick:manifest:v2", "build:v2": "npm run clear:dist ; npm run build:base && npm run pick:manifest:v2",
"build:v3": "npm run clear:dist ; npm run build:base && npm run pick:manifest:v3", "build:v3": "npm run clear:dist ; npm run build:base && npm run pick:manifest:v3",
"build": "rm -r ./build ; mkdir ./build && npm run build:v2 && zip -r ./build/manifest-v2.zip ./dist && npm run build:v3 && zip -r ./build/manifest-v3.zip ./dist", "build": "rm -r ./build ; mkdir ./build && npm run build:v2 && zip -r ./build/manifest-v2.zip ./dist && npm run build:v3 && zip -r ./build/manifest-v3.zip ./dist",
"watch:v2": "npm run clear:dist ; npm run pick:manifest:v2 && npm-run-all -l -p watch:parcel watch:assets", "watch:v2": "npm run clear:dist ; npm run pick:manifest:v2 && npm-run-all -l -p watch:parcel watch:assets",
"watch:v3": "npm run clear:dist ; npm run pick:manifest:v3 && npm-run-all -l -p watch:parcel watch:assets", "watch:v3": "npm run clear:dist ; npm run pick:manifest:v3 && npm-run-all -l -p watch:parcel watch:assets",
"watch": "npm run watch:v3", "watch": "npm run watch:v3",
"start:chrome": "web-ext run -t chromium --source-dir ./dist", "start:chrome": "web-ext run -t chromium --source-dir ./dist",
"start:firefox": "web-ext run --source-dir ./dist", "start:firefox": "web-ext run --source-dir ./dist",
"test": "jest" "test": "jest"

View file

@ -1,12 +1,13 @@
import { chunk } from "lodash" import { chunk } from "lodash"
import path from "path" import path from "path"
import { getExtensionSettingsAsync, ytUrlResolversSettings } from "../../settings" import { getExtensionSettingsAsync, SourcePlatform, ytUrlResolversSettings } from "../../settings"
import { sign } from "../crypto" import { sign } from "../crypto"
import { lbryUrlCache } from "./urlCache" import { lbryUrlCache } from "./urlCache"
const QUERY_CHUNK_SIZE = 100 const QUERY_CHUNK_SIZE = 100
export type YtUrlResolveItem = { type: 'video' | 'channel', id: string } export type ResolveUrlTypes = 'video' | 'channel'
export type YtUrlResolveItem = { type: ResolveUrlTypes, id: string }
type Results = Record<string, YtUrlResolveItem> type Results = Record<string, YtUrlResolveItem>
type Paramaters = YtUrlResolveItem[] type Paramaters = YtUrlResolveItem[]

View file

@ -24,5 +24,3 @@ chrome.runtime.onMessage.addListener(({ json }, sender, sendResponse) => {
return true return true
}) })
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => changeInfo.status === 'complete' && chrome.tabs.sendMessage(tabId, { message: 'url-changed' }))

View file

@ -1,198 +1,224 @@
import { h, render } from 'preact' import { h, render } from 'preact'
import { parseYouTubeURLTimeString } from '../modules/yt' import { parseYouTubeURLTimeString } from '../modules/yt'
import type { resolveById } from '../modules/yt/urlResolve' import type { resolveById, ResolveUrlTypes } from '../modules/yt/urlResolve'
import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, TargetPlatform, targetPlatformSettings } from '../settings' import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTargetPlatfromSettingsEntiries, SourcePlatform, sourcePlatfromSettings, TargetPlatform, targetPlatformSettings } from '../settings';
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,
backgroundImage: targetPlatform.theme,
fontWeight: 'bold',
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 requestResolveById(...params: Parameters<typeof resolveById>): ReturnType<typeof resolveById> {
const json = await new Promise<string | null | 'error'>((resolve) => chrome.runtime.sendMessage({ json: JSON.stringify(params) }, resolve))
if (json === 'error') throw new Error("Background error.")
return json ? JSON.parse(json) : null
}
// Start
(async () => { (async () => {
const settings = await getExtensionSettingsAsync() const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t))
let updater: (() => Promise<void>)
interface Target {
platform: TargetPlatform
lbryPathname: string
type: ResolveUrlTypes
time: number | null
}
const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
if (!sourcePlatform) return
const targetPlatforms = getTargetPlatfromSettingsEntiries()
const settings = await getExtensionSettingsAsync()
// Listen Settings Change // Listen Settings Change
chrome.storage.onChanged.addListener(async (changes, areaName) => { chrome.storage.onChanged.addListener(async (changes, areaName) => {
if (areaName !== 'local') return if (areaName !== 'local') return
Object.assign(settings, Object.fromEntries(Object.entries(changes).map(([key, change]) => [key, change.newValue]))) Object.assign(settings, Object.fromEntries(Object.entries(changes).map(([key, change]) => [key, change.newValue])))
if (changes.redirect) await onModeChange()
await updater()
}) })
/* const mountPoint = document.createElement('div')
* Gets messages from background script which relays tab update events. This is because there's no sensible way to detect mountPoint.style.display = 'flex'
* history.pushState changes from a content script
*/
// Listen URL Change
chrome.runtime.onMessage.addListener(({ message }, sender) => message === 'url-changed' && updater())
async function getTargetByURL(url: URL) { function WatchOnLbryButton({ target }: { target?: Target }) {
if (url.pathname === '/watch' && url.searchParams.has('v')) { if (!target) return null
const videoId = url.searchParams.get('v')! const url = getLbryUrlByTarget(target)
const result = await requestResolveById([{ id: videoId, type: 'video' }])
const target: Target | null = result?.[videoId] ? { lbryPathname: result[videoId].id, platfrom: targetPlatformSettings[settings.targetPlatform], time: null } : null
return target return <div style={{ display: 'flex', justifyContent: 'center', flexDirection: 'column' }}>
} <a href={`${url.href}`} target={target.platform === targetPlatformSettings.app ? '' : '_blank'} role='button'
else if (url.pathname.startsWith('/channel/')) { style={{
await requestResolveById([{ id: url.pathname.substring("/channel/".length), type: 'channel' }]) display: 'flex',
} alignItems: 'center',
else if (url.pathname.startsWith('/c/') || url.pathname.startsWith('/user/')) { justifyContent: 'center',
// We have to download the page content again because these parts of the page are not responsive gap: '12px',
// yt front end sucks anyway borderRadius: '2px',
const content = await (await fetch(location.href)).text() backgroundColor: target.platform.theme,
const prefix = `https://www.youtube.com/feeds/videos.xml?channel_id=` backgroundImage: target.platform.theme,
const suffix = `"` fontWeight: 'bold',
const startsAt = content.indexOf(prefix) + prefix.length border: '0',
const endsAt = content.indexOf(suffix, startsAt) color: 'whitesmoke',
await requestResolveById([{ id: content.substring(startsAt, endsAt), type: 'channel' }]) padding: '10px 16px',
} marginRight: target.type === 'channel' ? '10px' : '4px',
fontSize: '14px',
return null textDecoration: 'none',
...target.platform.button.style?.button,
}}>
<img src={target.platform.button.icon} height={16}
style={{ transform: 'scale(1.5)', ...target.platform.button.style?.icon }} />
<span>{target.platform.button.text}</span>
</a>
</div>
} }
async function redirectTo({ lbryPathname, platfrom, time }: Target) { function updateButton(target: Target | null): void {
const url = new URL(`${platfrom.domainPrefix}${lbryPathname}`) if (!target) return render(<WatchOnLbryButton />, mountPoint)
if (time) url.searchParams.set('t', time.toFixed(0)) const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
if (!sourcePlatform) return render(<WatchOnLbryButton />, mountPoint)
findVideoElement().then((videoElement) => { const mountBefore = document.querySelector(sourcePlatform.htmlQueries.mountButtonBefore[target.type])
videoElement.addEventListener('play', () => videoElement.pause(), { once: true }) if (!mountBefore) return render(<WatchOnLbryButton />, mountPoint)
videoElement.pause()
})
if (platfrom === targetPlatformSettings.app) { mountBefore.parentElement?.insertBefore(mountPoint, mountBefore)
if (document.hidden) await new Promise((resolve) => document.addEventListener('visibilitychange', resolve, { once: true })) render(<WatchOnLbryButton target={target} />, mountPoint)
}
// On redirect with app, people might choose to cancel browser's dialog async function findVideoElementAwait() {
// So we dont destroy the current window automatically for them const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
// And also we are keeping the same window for less distiraction if (!sourcePlatform) throw new Error(`Unknown source of: ${location.href}`)
if (settings.redirect) { let videoElement: HTMLVideoElement | null = null
location.replace(url.toString()) while (!(videoElement = document.querySelector(sourcePlatform.htmlQueries.videoPlayer))) await sleep(200)
return videoElement
}
async function getTargetsByURL(...urls: URL[]) {
const params: Parameters<typeof requestResolveById>[0] = []
const platform = targetPlatformSettings[settings.targetPlatform]
const datas: Record<string, { url: URL, type: ResolveUrlTypes }> = {}
for (const url of urls) {
if (url.pathname === '/watch' && url.searchParams.has('v')) {
const id = url.searchParams.get('v')!
const type: ResolveUrlTypes = 'video'
params.push({ id, type })
datas[id] = { url, type }
}
else if (url.pathname.startsWith('/channel/')) {
const id = url.pathname.substring("/channel/".length)
const type: ResolveUrlTypes = 'channel'
params.push({ id, type })
datas[id] = { url, type }
}
else if (url.pathname.startsWith('/c/') || url.pathname.startsWith('/user/')) {
// We have to download the page content again because these parts of the page are not responsive
// yt front end sucks anyway
const content = await (await fetch(location.href)).text()
const prefix = `https://www.youtube.com/feeds/videos.xml?channel_id=`
const suffix = `"`
const startsAt = content.indexOf(prefix) + prefix.length
const endsAt = content.indexOf(suffix, startsAt)
const id = content.substring(startsAt, endsAt)
const type: ResolveUrlTypes = 'channel'
params.push({ id, type })
datas[id] = { url, type }
}
}
const results = Object.entries(await requestResolveById(params))
const targets: Record<string, Target | null> = Object.fromEntries(results.map(([id, result]) => {
const data = datas[id]
if (!result) return [
data.url.href,
null
]
return [
data.url.href,
{
type: data.type,
lbryPathname: result?.id,
platform,
time: data.type === 'channel' ? null : (data.url.searchParams.has('t') ? parseYouTubeURLTimeString(data.url.searchParams.get('t')!) : null)
}
]
}))
return targets
}
// 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 requestResolveById(...params: Parameters<typeof resolveById>): ReturnType<typeof resolveById> {
const json = await new Promise<string | null | 'error'>((resolve) => chrome.runtime.sendMessage({ json: JSON.stringify(params) }, resolve))
if (json === 'error') throw new Error("Background error.")
return json ? JSON.parse(json) : null
}
function getLbryUrlByTarget(target: Target) {
const url = new URL(`${target.platform.domainPrefix}${target.lbryPathname}`)
if (target.time) url.searchParams.set('t', target.time.toFixed(0))
return url
}
let urlCache: URL | null = null
while (true) {
await sleep(500)
const url = new URL(location.href);
let target = (await getTargetsByURL(url))[url.href]
if (settings.redirect) {
if (!target) continue
if (url === urlCache) continue
const lbryURL = getLbryUrlByTarget(target)
findVideoElementAwait().then((videoElement) => {
videoElement.addEventListener('play', () => videoElement.pause(), { once: true })
videoElement.pause()
})
if (target.platform === targetPlatformSettings.app) {
if (document.hidden) await new Promise((resolve) => document.addEventListener('visibilitychange', resolve, { once: true }))
// Its not gonna be able to replace anyway
// This was empty window doesnt stay open
location.replace(lbryURL)
} }
else { else {
open(url.toString(), '_blank') open(lbryURL, '_blank')
if (window.history.length === 1) window.close() if (window.history.length === 1) window.close()
else window.history.back() 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 { else {
const mountPoint = await findButtonMountPoint() if (!target) {
const videoElement = await findVideoElement() const descriptionElement = document.querySelector(sourcePlatform.htmlQueries.videoDescription)
if (descriptionElement) {
const anchors = Array.from(descriptionElement.querySelectorAll<HTMLAnchorElement>('a'))
const getTime = () => videoElement.currentTime > 3 && videoElement.currentTime < videoElement.duration - 1 ? videoElement.currentTime : null for (const anchor of anchors) {
const url = new URL(anchor.href)
let lbryURL: URL | null = null
if (sourcePlatform === sourcePlatfromSettings['youtube.com']) {
if (!targetPlatforms.some(([key, platform]) => url.searchParams.get('q')?.startsWith(platform.domainPrefix))) continue
lbryURL = new URL(url.searchParams.get('q')!)
}
else {
if (!targetPlatforms.some(([key, platform]) => url.href.startsWith(platform.domainPrefix))) continue
lbryURL = new URL(url.href)
}
const onTimeUpdate = () => target && updateButton(mountPoint, Object.assign(target, { time: getTime() })) if (lbryURL) {
removeVideoTimeUpdateListener?.call(null) target = {
videoElement.addEventListener('timeupdate', onTimeUpdate) lbryPathname: lbryURL.pathname.substring(1),
removeVideoTimeUpdateListener = () => videoElement.removeEventListener('timeupdate', onTimeUpdate) time: null,
type: 'video',
updater = async () => { platform: targetPlatformSettings[settings.targetPlatform]
const url = new URL(location.href) }
target = await getTargetByURL(url) break
if (target) target.time = getTime() }
updateButton(mountPoint, target) }
}
} }
if (target) {
const videoElement = document.querySelector<HTMLVideoElement>(sourcePlatform.htmlQueries.videoPlayer)
if (videoElement) target.time = videoElement.currentTime > 3 && videoElement.currentTime < videoElement.duration - 1 ? videoElement.currentTime : null
}
// We run it anyway with null target to hide the button
updateButton(target)
} }
await updater()
urlCache = url
} }
await onModeChange()
})() })()

View file

@ -1,5 +1,6 @@
import type { JSX } from "preact" import type { JSX } from "preact"
import { useEffect, useReducer } from "preact/hooks" import { useEffect, useReducer } from "preact/hooks"
import type { ResolveUrlTypes } from "../modules/yt/urlResolve"
export interface ExtensionSettings { export interface ExtensionSettings {
redirect: boolean redirect: boolean
@ -112,8 +113,9 @@ export const targetPlatformSettings = {
const sourcePlatform = (o: { const sourcePlatform = (o: {
hostnames: string[] hostnames: string[]
htmlQueries: { htmlQueries: {
mountButtonBefore: string, mountButtonBefore: Record<ResolveUrlTypes, string>,
videoPlayer: string videoPlayer: string,
videoDescription: string
} }
}) => o }) => o
export type SourcePlatform = ReturnType<typeof sourcePlatform> export type SourcePlatform = ReturnType<typeof sourcePlatform>
@ -125,18 +127,27 @@ export function getSourcePlatfromSettingsFromHostname(hostname: string) {
return null return null
} }
export const sourcePlatfromSettings = { export const sourcePlatfromSettings = {
"yewtu.be": sourcePlatform({
hostnames: ['yewtu.be', 'vid.puffyan.us', 'invidio.xamh.de', 'invidious.kavin.rocks'],
htmlQueries: {
mountButtonBefore: '#watch-on-youtube',
videoPlayer: '#player-container video'
}
}),
"youtube.com": sourcePlatform({ "youtube.com": sourcePlatform({
hostnames: ['www.youtube.com'], hostnames: ['www.youtube.com'],
htmlQueries: { htmlQueries: {
mountButtonBefore: 'ytd-video-owner-renderer~#subscribe-button', mountButtonBefore: {
videoPlayer: '#ytd-player video' video: 'ytd-video-owner-renderer~#subscribe-button',
channel: '#channel-header-container #buttons'
},
videoPlayer: '#ytd-player video',
videoDescription: 'ytd-video-secondary-info-renderer #description'
}
}),
"yewtu.be": sourcePlatform({
hostnames: ['yewtu.be', 'vid.puffyan.us', 'invidio.xamh.de', 'invidious.kavin.rocks'],
htmlQueries: {
mountButtonBefore:
{
video: '#watch-on-youtube',
channel: '#subscribe'
},
videoPlayer: '#player-container video',
videoDescription: '#descriptionWrapper'
} }
}) })
} }