mirror of
https://github.com/LBRYFoundation/Watch-on-LBRY.git
synced 2025-08-23 17:47:26 +00:00
Compare commits
No commits in common. "master" and "2.0.0" have entirely different histories.
23 changed files with 499 additions and 736 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,6 +1,5 @@
|
||||||
.cache
|
.cache
|
||||||
dist
|
dist
|
||||||
build
|
|
||||||
node_modules
|
node_modules
|
||||||
web-ext-artifacts
|
web-ext-artifacts
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
|
|
@ -111,7 +111,7 @@ Please make sure to update tests as appropriate.
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
|
||||||
<a href=https://github.com/kbakdev>
|
<a href=https://github.com/53jk1>
|
||||||
<img src=https://avatars.githubusercontent.com/u/56700396?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Kacper Bąk/>
|
<img src=https://avatars.githubusercontent.com/u/56700396?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Kacper Bąk/>
|
||||||
<br />
|
<br />
|
||||||
<sub style="font-size:14px"><b>Kacper Bąk</b></sub>
|
<sub style="font-size:14px"><b>Kacper Bąk</b></sub>
|
||||||
|
|
5
global.d.ts
vendored
5
global.d.ts
vendored
|
@ -2,8 +2,3 @@ declare module '*.md' {
|
||||||
var _: string
|
var _: string
|
||||||
export default _
|
export default _
|
||||||
}
|
}
|
||||||
|
|
||||||
declare namespace chrome
|
|
||||||
{
|
|
||||||
export const action = chrome.browserAction
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
{
|
|
||||||
"manifest_version": 2,
|
|
||||||
"name": "Watch on LBRY",
|
|
||||||
"version": "2.0.1",
|
|
||||||
"icons": {
|
|
||||||
"16": "assets/icons/wol/icon16.png",
|
|
||||||
"48": "assets/icons/wol/icon48.png",
|
|
||||||
"128": "assets/icons/wol/icon128.png"
|
|
||||||
},
|
|
||||||
"permissions": [
|
|
||||||
"https://www.youtube.com/",
|
|
||||||
"https://yewtu.be/",
|
|
||||||
"https://vid.puffyan.us/",
|
|
||||||
"https://invidio.xamh.de/",
|
|
||||||
"https://invidious.kavin.rocks/",
|
|
||||||
"https://api.odysee.com/",
|
|
||||||
"https://lbry.tv/",
|
|
||||||
"https://odysee.com/",
|
|
||||||
"https://madiator.com/",
|
|
||||||
"https://finder.madiator.com/",
|
|
||||||
"tabs",
|
|
||||||
"storage"
|
|
||||||
],
|
|
||||||
"web_accessible_resources": [
|
|
||||||
"pages/popup/index.html",
|
|
||||||
"pages/YTtoLBRY/index.html",
|
|
||||||
"pages/import/index.html",
|
|
||||||
"assets/icons/lbry/lbry-logo.svg",
|
|
||||||
"assets/icons/lbry/odysee-logo.svg",
|
|
||||||
"assets/icons/lbry/madiator-logo.svg"
|
|
||||||
],
|
|
||||||
"browser_action": {
|
|
||||||
"default_title": "Watch on LBRY",
|
|
||||||
"default_popup": "pages/popup/index.html"
|
|
||||||
},
|
|
||||||
"content_scripts": [
|
|
||||||
{
|
|
||||||
"matches": [
|
|
||||||
"https://www.youtube.com/*",
|
|
||||||
"https://yewtu.be/*",
|
|
||||||
"https://vid.puffyan.us/*",
|
|
||||||
"https://invidio.xamh.de/*",
|
|
||||||
"https://invidious.kavin.rocks/*"
|
|
||||||
],
|
|
||||||
"js": [
|
|
||||||
"scripts/ytContent.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"background": {
|
|
||||||
"scripts": [
|
|
||||||
"service-worker-entry-point.js"
|
|
||||||
],
|
|
||||||
"persistent": true
|
|
||||||
}
|
|
||||||
}
|
|
97
package-lock.json
generated
97
package-lock.json
generated
|
@ -4826,6 +4826,91 @@
|
||||||
"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",
|
||||||
|
@ -7838,6 +7923,12 @@
|
||||||
"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",
|
||||||
|
@ -14874,6 +14965,12 @@
|
||||||
"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",
|
||||||
|
|
12
package.json
12
package.json
|
@ -8,16 +8,8 @@
|
||||||
"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",
|
"build": "npm-run-all -l -p build:parcel build:assets",
|
||||||
"build:base": "npm-run-all -l -p build:parcel build:assets",
|
"watch": "npm-run-all -l -p watch:parcel watch:assets",
|
||||||
"pick:manifest:v2": "cp -b ./manifest.v2.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: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",
|
|
||||||
"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": "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"
|
||||||
|
|
|
@ -46,6 +46,11 @@ export function createDialogManager() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DialogElement extends HTMLDivElement {
|
||||||
|
open: boolean
|
||||||
|
showModal(): void
|
||||||
|
}
|
||||||
|
|
||||||
export function Dialogs(params: { manager: ReturnType<typeof createDialogManager> }) {
|
export function Dialogs(params: { manager: ReturnType<typeof createDialogManager> }) {
|
||||||
const alerts = params.manager.useAlerts()
|
const alerts = params.manager.useAlerts()
|
||||||
let currentAlert = Object.values(alerts)[0]
|
let currentAlert = Object.values(alerts)[0]
|
||||||
|
@ -55,7 +60,7 @@ export function Dialogs(params: { manager: ReturnType<typeof createDialogManager
|
||||||
|
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
|
|
||||||
const dialog = useRef(null as any as HTMLDialogElement)
|
const dialog = useRef(null as any as DialogElement)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dialog.current) return
|
if (!dialog.current) return
|
||||||
if (!dialog.current.open) dialog.current.showModal()
|
if (!dialog.current.open) dialog.current.showModal()
|
||||||
|
@ -68,7 +73,6 @@ export function Dialogs(params: { manager: ReturnType<typeof createDialogManager
|
||||||
{`
|
{`
|
||||||
.alert-dialog
|
.alert-dialog
|
||||||
{
|
{
|
||||||
position: fixed;
|
|
||||||
border: none;
|
border: none;
|
||||||
background: var(--color-dark);
|
background: var(--color-dark);
|
||||||
color: var(--color-light);
|
color: var(--color-light);
|
||||||
|
|
|
@ -1,17 +1,7 @@
|
||||||
{
|
{
|
||||||
"manifest_version": 3,
|
|
||||||
"name": "Watch on LBRY",
|
"name": "Watch on LBRY",
|
||||||
"version": "2.0.1",
|
"version": "2.0.0",
|
||||||
"icons": {
|
|
||||||
"16": "assets/icons/wol/icon16.png",
|
|
||||||
"48": "assets/icons/wol/icon48.png",
|
|
||||||
"128": "assets/icons/wol/icon128.png"
|
|
||||||
},
|
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
|
||||||
"tabs"
|
|
||||||
],
|
|
||||||
"host_permissions": [
|
|
||||||
"https://www.youtube.com/",
|
"https://www.youtube.com/",
|
||||||
"https://yewtu.be/",
|
"https://yewtu.be/",
|
||||||
"https://vid.puffyan.us/",
|
"https://vid.puffyan.us/",
|
||||||
|
@ -21,24 +11,10 @@
|
||||||
"https://lbry.tv/",
|
"https://lbry.tv/",
|
||||||
"https://odysee.com/",
|
"https://odysee.com/",
|
||||||
"https://madiator.com/",
|
"https://madiator.com/",
|
||||||
"https://finder.madiator.com/"
|
"https://finder.madiator.com/",
|
||||||
|
"tabs",
|
||||||
|
"storage"
|
||||||
],
|
],
|
||||||
"web_accessible_resources": [{
|
|
||||||
"resources": [
|
|
||||||
"pages/popup/index.html",
|
|
||||||
"pages/YTtoLBRY/index.html",
|
|
||||||
"pages/import/index.html",
|
|
||||||
"assets/icons/lbry/lbry-logo.svg",
|
|
||||||
"assets/icons/lbry/odysee-logo.svg",
|
|
||||||
"assets/icons/lbry/madiator-logo.svg"
|
|
||||||
],
|
|
||||||
"matches": ["<all_urls>"],
|
|
||||||
"extension_ids": []
|
|
||||||
}],
|
|
||||||
"action": {
|
|
||||||
"default_title": "Watch on LBRY",
|
|
||||||
"default_popup": "pages/popup/index.html"
|
|
||||||
},
|
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": [
|
"matches": [
|
||||||
|
@ -54,6 +30,28 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"background": {
|
"background": {
|
||||||
"service_worker": "service-worker-entry-point.js"
|
"scripts": [
|
||||||
}
|
"settings/background.js",
|
||||||
|
"scripts/background.js"
|
||||||
|
],
|
||||||
|
"persistent": true
|
||||||
|
},
|
||||||
|
"browser_action": {
|
||||||
|
"default_title": "Watch on LBRY",
|
||||||
|
"default_popup": "pages/popup/index.html"
|
||||||
|
},
|
||||||
|
"web_accessible_resources": [
|
||||||
|
"pages/popup/index.html",
|
||||||
|
"pages/YTtoLBRY/index.html",
|
||||||
|
"pages/import/index.html",
|
||||||
|
"assets/icons/lbry/lbry-logo.svg",
|
||||||
|
"assets/icons/lbry/odysee-logo.svg",
|
||||||
|
"assets/icons/lbry/madiator-logo.svg"
|
||||||
|
],
|
||||||
|
"icons": {
|
||||||
|
"16": "assets/icons/wol/icon16.png",
|
||||||
|
"48": "assets/icons/wol/icon48.png",
|
||||||
|
"128": "assets/icons/wol/icon128.png"
|
||||||
|
},
|
||||||
|
"manifest_version": 2
|
||||||
}
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import type { DialogManager } from '../../components/dialogs'
|
import { DialogManager } from '../../components/dialogs'
|
||||||
import { getExtensionSettingsAsync, setExtensionSetting, ytUrlResolversSettings } from "../../settings"
|
import { getExtensionSettingsAsync, setExtensionSetting, ytUrlResolversSettings } from "../../settings"
|
||||||
import { getFileContent } from '../file'
|
import { getFileContent } from '../file'
|
||||||
|
|
||||||
async function generateKeys() {
|
async function generateKeys() {
|
||||||
const keys = await crypto.subtle.generateKey(
|
const keys = await window.crypto.subtle.generateKey(
|
||||||
{
|
{
|
||||||
name: "RSASSA-PKCS1-v1_5",
|
name: "RSASSA-PKCS1-v1_5",
|
||||||
// Consider using a 4096-bit key for systems that require long-term security
|
// Consider using a 4096-bit key for systems that require long-term security
|
||||||
|
@ -23,7 +23,7 @@ async function generateKeys() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportPrivateKey(key: CryptoKey) {
|
async function exportPrivateKey(key: CryptoKey) {
|
||||||
const exported = await crypto.subtle.exportKey(
|
const exported = await window.crypto.subtle.exportKey(
|
||||||
"pkcs8",
|
"pkcs8",
|
||||||
key
|
key
|
||||||
)
|
)
|
||||||
|
@ -34,7 +34,7 @@ const publicKeyPrefix = `MEwwDQYJKoZIhvcNAQEBBQADOwAwOAIxA`
|
||||||
const publicKeySuffix = `IDAQAB` //`wIDAQAB` `WIDAQAB`
|
const publicKeySuffix = `IDAQAB` //`wIDAQAB` `WIDAQAB`
|
||||||
const publicKeyLength = 65
|
const publicKeyLength = 65
|
||||||
async function exportPublicKey(key: CryptoKey) {
|
async function exportPublicKey(key: CryptoKey) {
|
||||||
const exported = await crypto.subtle.exportKey(
|
const exported = await window.crypto.subtle.exportKey(
|
||||||
"spki",
|
"spki",
|
||||||
key
|
key
|
||||||
)
|
)
|
||||||
|
@ -43,7 +43,7 @@ async function exportPublicKey(key: CryptoKey) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function importPrivateKey(base64: string) {
|
function importPrivateKey(base64: string) {
|
||||||
return crypto.subtle.importKey(
|
return window.crypto.subtle.importKey(
|
||||||
"pkcs8",
|
"pkcs8",
|
||||||
Buffer.from(base64, 'base64'),
|
Buffer.from(base64, 'base64'),
|
||||||
{
|
{
|
||||||
|
@ -56,7 +56,7 @@ function importPrivateKey(base64: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sign(data: string, privateKey: string) {
|
export async function sign(data: string, privateKey: string) {
|
||||||
return Buffer.from(await crypto.subtle.sign(
|
return Buffer.from(await window.crypto.subtle.sign(
|
||||||
{ name: "RSASSA-PKCS1-v1_5" },
|
{ name: "RSASSA-PKCS1-v1_5" },
|
||||||
await importPrivateKey(privateKey),
|
await importPrivateKey(privateKey),
|
||||||
await crypto.subtle.digest({ name: 'SHA-1' }, Buffer.from(data))
|
await crypto.subtle.digest({ name: 'SHA-1' }, Buffer.from(data))
|
||||||
|
@ -70,18 +70,22 @@ export function resetProfileSettings() {
|
||||||
|
|
||||||
async function apiRequest<T extends object>(method: 'GET' | 'POST', pathname: string, data: T) {
|
async function apiRequest<T extends object>(method: 'GET' | 'POST', pathname: string, data: T) {
|
||||||
const settings = await getExtensionSettingsAsync()
|
const settings = await getExtensionSettingsAsync()
|
||||||
|
/* const urlResolverSettings = ytUrlResolversSettings[settings.urlResolver]
|
||||||
|
if (!urlResolverSettings.signRequest) throw new Error() */
|
||||||
|
|
||||||
const url = new URL(ytUrlResolversSettings.madiatorFinder.href)
|
const url = new URL(ytUrlResolversSettings.madiatorFinder.href/* urlResolverSettings.href */)
|
||||||
url.pathname = path.join(url.pathname, pathname)
|
url.pathname = path.join(url.pathname, pathname)
|
||||||
url.searchParams.set('data', JSON.stringify(data))
|
url.searchParams.set('data', JSON.stringify(data))
|
||||||
|
|
||||||
if (!settings.privateKey || !settings.publicKey)
|
if (true/* requiresSignature */) {
|
||||||
throw new Error('There is no profile.')
|
if (!settings.privateKey || !settings.publicKey)
|
||||||
|
throw new Error('There is no profile.')
|
||||||
|
|
||||||
url.searchParams.set('keys', JSON.stringify({
|
url.searchParams.set('keys', JSON.stringify({
|
||||||
signature: await sign(url.searchParams.toString(), settings.privateKey!),
|
signature: await sign(url.searchParams.toString(), settings.privateKey!),
|
||||||
publicKey: settings.publicKey
|
publicKey: settings.publicKey
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
const respond = await fetch(url.href, { method })
|
const respond = await fetch(url.href, { method })
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
interface YtExportedJsonSubscription {
|
interface YtExportedJsonSubscription {
|
||||||
id: string
|
id: string
|
||||||
etag: string
|
etag: string
|
||||||
|
|
|
@ -1,21 +1,23 @@
|
||||||
// This should only work in background
|
// This should only work in background
|
||||||
if (typeof chrome.extension === 'undefined') throw new Error("YT urlCache can only be accessed from extension windows and service-workers.")
|
|
||||||
|
|
||||||
let db = new Promise<IDBDatabase>((resolve, reject) => {
|
let db: IDBDatabase | null = null
|
||||||
if (typeof self.indexedDB !== 'undefined') {
|
|
||||||
const openRequest = indexedDB.open("yt-url-resolver-cache")
|
if (typeof self.indexedDB !== 'undefined') {
|
||||||
openRequest.addEventListener('upgradeneeded', () => openRequest.result.createObjectStore("store").createIndex("expireAt", "expireAt"))
|
const openRequest = indexedDB.open("yt-url-resolver-cache")
|
||||||
openRequest.addEventListener('success', () => {
|
openRequest.addEventListener('upgradeneeded', () => openRequest.result.createObjectStore("store").createIndex("expireAt", "expireAt"))
|
||||||
resolve(openRequest.result)
|
|
||||||
clearExpired()
|
// Delete Expired
|
||||||
}, { once: true })
|
openRequest.addEventListener('success', () => {
|
||||||
}
|
db = openRequest.result
|
||||||
else reject(`IndexedDB not supported`)
|
clearExpired()
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
else console.warn(`IndexedDB not supported`)
|
||||||
|
|
||||||
async function clearExpired() {
|
async function clearExpired() {
|
||||||
return new Promise<void>(async (resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const transaction = (await db).transaction("store", "readwrite")
|
if (!db) throw new Error(`IDBDatabase not defined.`)
|
||||||
|
const transaction = db.transaction("store", "readwrite")
|
||||||
const range = IDBKeyRange.upperBound(new Date())
|
const range = IDBKeyRange.upperBound(new Date())
|
||||||
|
|
||||||
const expireAtCursorRequest = transaction.objectStore("store").index("expireAt").openCursor(range)
|
const expireAtCursorRequest = transaction.objectStore("store").index("expireAt").openCursor(range)
|
||||||
|
@ -36,8 +38,8 @@ async function clearExpired() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearAll() {
|
async function clearAll() {
|
||||||
return await new Promise<void>(async (resolve, reject) => {
|
return await new Promise<void>((resolve, reject) => {
|
||||||
const store = (await db).transaction("store", "readwrite").objectStore("store")
|
const store = db?.transaction("store", "readwrite").objectStore("store")
|
||||||
if (!store) return resolve()
|
if (!store) return resolve()
|
||||||
const request = store.clear()
|
const request = store.clear()
|
||||||
request.addEventListener('success', () => resolve())
|
request.addEventListener('success', () => resolve())
|
||||||
|
@ -46,8 +48,8 @@ async function clearAll() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function put(url: string | null, id: string): Promise<void> {
|
async function put(url: string | null, id: string): Promise<void> {
|
||||||
return await new Promise(async (resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
const store = (await db).transaction("store", "readwrite").objectStore("store")
|
const store = db?.transaction("store", "readwrite").objectStore("store")
|
||||||
if (!store) return resolve()
|
if (!store) return resolve()
|
||||||
const expireAt = !url ? new Date(Date.now() + 1 * 60 * 60 * 1000) : new Date(Date.now() + 15 * 24 * 60 * 60 * 1000)
|
const expireAt = !url ? new Date(Date.now() + 1 * 60 * 60 * 1000) : new Date(Date.now() + 15 * 24 * 60 * 60 * 1000)
|
||||||
const request = store.put({ value: url, expireAt }, id)
|
const request = store.put({ value: url, expireAt }, id)
|
||||||
|
@ -61,8 +63,8 @@ async function put(url: string | null, id: string): Promise<void> {
|
||||||
// null means there is cache of that id has no lbrypathname
|
// null means there is cache of that id has no lbrypathname
|
||||||
// undefined means there is no cache
|
// undefined means there is no cache
|
||||||
async function get(id: string): Promise<string | null | undefined> {
|
async function get(id: string): Promise<string | null | undefined> {
|
||||||
const response = (await new Promise(async (resolve, reject) => {
|
const response = (await new Promise((resolve, reject) => {
|
||||||
const store = (await db).transaction("store", "readonly").objectStore("store")
|
const store = db?.transaction("store", "readonly").objectStore("store")
|
||||||
if (!store) return reject(`Can't find object store.`)
|
if (!store) return reject(`Can't find object store.`)
|
||||||
|
|
||||||
const request = store.get(id)
|
const request = store.get(id)
|
||||||
|
@ -75,8 +77,9 @@ async function get(id: string): Promise<string | null | undefined> {
|
||||||
await clearExpired()
|
await clearExpired()
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
console.log('cache found', id, response.value)
|
||||||
return response.value
|
return response.value
|
||||||
}
|
}
|
||||||
|
|
||||||
export const lbryUrlCache = { put, get, clearAll }
|
export const LbryPathnameCache = { put, get, clearAll }
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { chunk } from "lodash"
|
import { chunk } from "lodash"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { getExtensionSettingsAsync, SourcePlatform, ytUrlResolversSettings } from "../../settings"
|
import { getExtensionSettingsAsync, ytUrlResolversSettings } from "../../settings"
|
||||||
import { sign } from "../crypto"
|
import { sign } from "../crypto"
|
||||||
import { lbryUrlCache } from "./urlCache"
|
import { LbryPathnameCache } from "./urlCache"
|
||||||
|
|
||||||
const QUERY_CHUNK_SIZE = 100
|
const QUERY_CHUNK_SIZE = 100
|
||||||
|
|
||||||
export type ResolveUrlTypes = 'video' | 'channel'
|
export type YtUrlResolveItem = { type: 'video' | 'channel', id: string }
|
||||||
export type YtUrlResolveItem = { type: ResolveUrlTypes, id: string }
|
|
||||||
type Results = Record<string, YtUrlResolveItem>
|
type Results = Record<string, YtUrlResolveItem>
|
||||||
type Paramaters = YtUrlResolveItem[]
|
type Paramaters = YtUrlResolveItem[]
|
||||||
|
|
||||||
|
@ -28,7 +27,7 @@ export async function resolveById(params: Paramaters, progressCallback?: (progre
|
||||||
// Check for cache first, add them to the results if there are any cache
|
// Check for cache first, add them to the results if there are any cache
|
||||||
// And remove them from the params, so we dont request for them
|
// And remove them from the params, so we dont request for them
|
||||||
params = (await Promise.all(params.map(async (item) => {
|
params = (await Promise.all(params.map(async (item) => {
|
||||||
const cachedLbryUrl = await lbryUrlCache.get(item.id)
|
const cachedLbryUrl = await LbryPathnameCache.get(item.id)
|
||||||
|
|
||||||
// Cache can be null, if there is no lbry url yet
|
// Cache can be null, if there is no lbry url yet
|
||||||
if (cachedLbryUrl !== undefined) {
|
if (cachedLbryUrl !== undefined) {
|
||||||
|
@ -53,18 +52,13 @@ export async function resolveById(params: Paramaters, progressCallback?: (progre
|
||||||
publicKey
|
publicKey
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const controller = new AbortController()
|
const apiResponse = await fetch(url.toString(), { cache: 'no-store' })
|
||||||
// 5 second timeout:
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 15000)
|
|
||||||
const apiResponse = await fetch(url.toString(), { cache: 'no-store', signal: controller.signal })
|
|
||||||
clearTimeout(timeoutId)
|
|
||||||
|
|
||||||
if (apiResponse.ok) {
|
if (apiResponse.ok) {
|
||||||
const response: ApiResponse = await apiResponse.json()
|
const response: ApiResponse = await apiResponse.json()
|
||||||
for (const item of params) {
|
for (const item of params) {
|
||||||
const lbryUrl = (item.type === 'channel' ? response.data.channels : response.data.videos)?.[item.id]?.replaceAll('#', ':') ?? null
|
const lbryUrl = (item.type === 'channel' ? response.data.channels : response.data.videos)?.[item.id]?.replaceAll('#', ':') ?? null
|
||||||
// we cache it no matter if its null or not
|
// we cache it no matter if its null or not
|
||||||
await lbryUrlCache.put(lbryUrl, item.id)
|
await LbryPathnameCache.put(lbryUrl, item.id)
|
||||||
|
|
||||||
if (lbryUrl) results[item.id] = { id: lbryUrl, type: item.type }
|
if (lbryUrl) results[item.id] = { id: lbryUrl, type: item.type }
|
||||||
}
|
}
|
||||||
|
|
6
src/pages/YTtoLBRY/README.md
Normal file
6
src/pages/YTtoLBRY/README.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# Getting your subscription data
|
||||||
|
|
||||||
|
1. Go to https://takeout.google.com/settings/takeout
|
||||||
|
2. Deselect everything except `YouTube and YouTube Music` and within that only select `subscriptions`
|
||||||
|
3. Go through the process and create the export
|
||||||
|
4. Once it's exported, open the archive and find `YouTube and YouTube Music/subscriptions/subscriptions.json` and upload it to the extension
|
|
@ -4,6 +4,7 @@ import { getFileContent } from '../../modules/file'
|
||||||
import { getSubsFromCsv, getSubsFromJson, getSubsFromOpml } from '../../modules/yt'
|
import { getSubsFromCsv, getSubsFromJson, getSubsFromOpml } from '../../modules/yt'
|
||||||
import { resolveById } from '../../modules/yt/urlResolve'
|
import { resolveById } from '../../modules/yt/urlResolve'
|
||||||
import { targetPlatformSettings, useExtensionSettings } from '../../settings'
|
import { targetPlatformSettings, useExtensionSettings } from '../../settings'
|
||||||
|
import readme from './README.md'
|
||||||
|
|
||||||
async function getSubscribedChannelIdsFromFile(file: File) {
|
async function getSubscribedChannelIdsFromFile(file: File) {
|
||||||
const ext = file.name.split('.').pop()?.toLowerCase()
|
const ext = file.name.split('.').pop()?.toLowerCase()
|
||||||
|
@ -70,15 +71,8 @@ function YTtoLBRY() {
|
||||||
<Conversion />
|
<Conversion />
|
||||||
<aside class="help">
|
<aside class="help">
|
||||||
<iframe allowFullScreen
|
<iframe allowFullScreen
|
||||||
src='https://odysee.com/$/embed/convert-subscriptions-from-YouTube-to-LBRY/36f3a010295afe1c55e91b63bcb2eabc028ec86c?r=8bgP4hEdbd9jwBJmhEaqP3dD75LzsUob' />
|
src='https://lbry.tv/$/embed/howtouseconverter/c9827448d6ac7a74ecdb972c5cdf9ddaf648a28e' />
|
||||||
<section><h1 id="getting-your-subscription-data">Getting your subscription data</h1>
|
<section dangerouslySetInnerHTML={{ __html: readme }} />
|
||||||
<ol>
|
|
||||||
<li>Go to <a href="https://takeout.google.com/settings/takeout" target='_blank'>https://takeout.google.com/settings/takeout</a></li>
|
|
||||||
<li>Deselect everything except <code>YouTube and YouTube Music</code> and within that only select <code>subscriptions</code></li>
|
|
||||||
<li>Go through the process and create the export</li>
|
|
||||||
<li>Once it's exported, open the archive and find <code>YouTube and YouTube Music/subscriptions/subscriptions.(json/csv/opml)</code> and upload it to the extension</li>
|
|
||||||
</ol>
|
|
||||||
</section>
|
|
||||||
</aside>
|
</aside>
|
||||||
</main>
|
</main>
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,21 +3,6 @@ import { useState } from 'preact/hooks'
|
||||||
import { createDialogManager, Dialogs } from '../../components/dialogs'
|
import { createDialogManager, Dialogs } from '../../components/dialogs'
|
||||||
import { importProfileKeysFromFile, inputKeyFile } from '../../modules/crypto'
|
import { importProfileKeysFromFile, inputKeyFile } from '../../modules/crypto'
|
||||||
|
|
||||||
export async function openImportPopup() {
|
|
||||||
const importPopupWindow = open(
|
|
||||||
'/pages/import/index.html',
|
|
||||||
'Import Profile',
|
|
||||||
[
|
|
||||||
`height=${Math.max(document.body.clientHeight, screen.height * .5)}`,
|
|
||||||
`width=${document.body.clientWidth}`,
|
|
||||||
`toolbar=0,menubar=0,location=0`,
|
|
||||||
`top=${screenY}`,
|
|
||||||
`left=${screenX}`
|
|
||||||
].join(','))
|
|
||||||
close()
|
|
||||||
importPopupWindow?.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
function ImportPage() {
|
function ImportPage() {
|
||||||
const [loading, updateLoading] = useState(() => false)
|
const [loading, updateLoading] = useState(() => false)
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,8 @@ import { h, render } from 'preact'
|
||||||
import { useState } from 'preact/hooks'
|
import { useState } from 'preact/hooks'
|
||||||
import { createDialogManager, Dialogs } from '../../components/dialogs'
|
import { createDialogManager, Dialogs } from '../../components/dialogs'
|
||||||
import { exportProfileKeysAsFile, friendlyPublicKey, generateProfileAndSetNickname, getProfile, purgeProfile, resetProfileSettings } from '../../modules/crypto'
|
import { exportProfileKeysAsFile, friendlyPublicKey, generateProfileAndSetNickname, getProfile, purgeProfile, resetProfileSettings } from '../../modules/crypto'
|
||||||
import { lbryUrlCache } from '../../modules/yt/urlCache'
|
import { LbryPathnameCache } from '../../modules/yt/urlCache'
|
||||||
import { getTargetPlatfromSettingsEntiries, getYtUrlResolversSettingsEntiries, setExtensionSetting, useExtensionSettings } from '../../settings'
|
import { getTargetPlatfromSettingsEntiries, getYtUrlResolversSettingsEntiries, setExtensionSetting, useExtensionSettings } from '../../settings'
|
||||||
import { openImportPopup } from '../import/main'
|
|
||||||
|
|
||||||
|
|
||||||
/** Gets all the options for redirect destinations as selection options */
|
/** Gets all the options for redirect destinations as selection options */
|
||||||
|
@ -12,9 +11,9 @@ const targetPlatforms = getTargetPlatfromSettingsEntiries()
|
||||||
const ytUrlResolverOptions = getYtUrlResolversSettingsEntiries()
|
const ytUrlResolverOptions = getYtUrlResolversSettingsEntiries()
|
||||||
|
|
||||||
function WatchOnLbryPopup(params: { profile: Awaited<ReturnType<typeof getProfile>> | null }) {
|
function WatchOnLbryPopup(params: { profile: Awaited<ReturnType<typeof getProfile>> | null }) {
|
||||||
const { targetPlatform, urlResolver, redirectChannel, redirectVideo, redirectVideoPlaylist, buttonVideoSub, buttonChannelSub, buttonVideoPlayer, privateKey, publicKey } = useExtensionSettings()
|
const { redirect, targetPlatform, urlResolver, privateKey, publicKey } = useExtensionSettings()
|
||||||
let [loading, updateLoading] = useState(() => false)
|
let [loading, updateLoading] = useState(() => false)
|
||||||
let [route, updateRoute] = useState<string>(() => '')
|
let [route, updateRoute] = useState<string | null>(() => null)
|
||||||
|
|
||||||
const dialogManager = createDialogManager()
|
const dialogManager = createDialogManager()
|
||||||
const nickname = params.profile ? params.profile.nickname ?? 'No Nickname' : '...'
|
const nickname = params.profile ? params.profile.nickname ?? 'No Nickname' : '...'
|
||||||
|
@ -32,33 +31,47 @@ function WatchOnLbryPopup(params: { profile: Awaited<ReturnType<typeof getProfil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function importButtonClick() {
|
||||||
|
const importPopupWindow = open(
|
||||||
|
'/pages/import/index.html',
|
||||||
|
'Import Profile',
|
||||||
|
[
|
||||||
|
`height=${Math.max(document.body.clientHeight, screen.height * .5)}`,
|
||||||
|
`width=${document.body.clientWidth}`,
|
||||||
|
`toolbar=0,menubar=0,location=0`,
|
||||||
|
`top=${screenY}`,
|
||||||
|
`left=${screenX}`
|
||||||
|
].join(','))
|
||||||
|
close()
|
||||||
|
importPopupWindow?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
return <div id='popup'>
|
return <div id='popup'>
|
||||||
<Dialogs manager={dialogManager} />
|
<Dialogs manager={dialogManager} />
|
||||||
{
|
{
|
||||||
|
publicKey
|
||||||
<header>
|
? <header>
|
||||||
{
|
|
||||||
publicKey &&
|
|
||||||
<section>
|
<section>
|
||||||
<label>{nickname}</label>
|
<label>{nickname}</label>
|
||||||
<p>{friendlyPublicKey(publicKey)}</p>
|
<p>{friendlyPublicKey(publicKey)}</p>
|
||||||
<span><b>Score: {params.profile?.score ?? '...'}</b> - <a target='_blank' href="https://finder.madiator.com/leaderboard" class="filled">🔗Leaderboard</a></span>
|
<span><b>Score: {params.profile?.score ?? '...'}</b> - <a target='_blank' href="https://finder.madiator.com/leaderboard" class="filled">🔗Leaderboard</a></span>
|
||||||
{urlResolver !== 'madiatorFinder' && <span class="error">You need to use Madiator Finder API for scoring to work</span>}
|
{urlResolver !== 'madiatorFinder' && <span class="error">You need to use Madiator Finder API for scoring to work</span>}
|
||||||
</section>
|
</section>
|
||||||
}
|
<section>
|
||||||
|
{
|
||||||
{
|
route === 'profile'
|
||||||
route !== ''
|
? <a onClick={() => updateRoute('')} className="filled">⇐ Back</a>
|
||||||
?
|
: <a className='filled' onClick={() => updateRoute('profile')} href="#profile">Profile Settings</a>
|
||||||
<section>
|
}
|
||||||
<a onClick={() => updateRoute('')} className="filled">⇐ Back</a>
|
</section>
|
||||||
</section>
|
</header>
|
||||||
:
|
: <header>
|
||||||
<section>
|
{
|
||||||
<a className='filled' onClick={() => updateRoute('profile')}>Profile Settings</a>
|
route === 'profile'
|
||||||
</section>
|
? <a onClick={() => updateRoute('')} className="filled">⇐ Back</a>
|
||||||
}
|
: <a className='filled' onClick={() => updateRoute('profile')} href="#profile">Profile Settings</a>
|
||||||
</header>
|
}
|
||||||
|
</header>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
route === 'profile' ?
|
route === 'profile' ?
|
||||||
|
@ -69,14 +82,11 @@ function WatchOnLbryPopup(params: { profile: Awaited<ReturnType<typeof getProfil
|
||||||
<a onClick={() => loads(generateProfileAndSetNickname(dialogManager)).then(() => renderPopup())} className={`button active`}>
|
<a onClick={() => loads(generateProfileAndSetNickname(dialogManager)).then(() => renderPopup())} className={`button active`}>
|
||||||
Change Nickname
|
Change Nickname
|
||||||
</a>
|
</a>
|
||||||
<a onClick={async () => {
|
<a onClick={async () =>
|
||||||
if (!await dialogManager.confirm("This will delete your keypair from this device."
|
await dialogManager.confirm("This will delete your keypair from this device.\nStill wanna continue?\n\nNOTE: Without keypair you can't purge your data online.\nSo if you wish to purge, please use purging instead.")
|
||||||
+ "\nStill wanna continue?"
|
&& resetProfileSettings()
|
||||||
+ "\n\nNOTE: Without keypair you can't purge your data online."
|
&& renderPopup()
|
||||||
+ "\nSo if you wish to purge, please use purging instead.")) return
|
}
|
||||||
resetProfileSettings()
|
|
||||||
renderPopup()
|
|
||||||
}}
|
|
||||||
className={`button`}
|
className={`button`}
|
||||||
>
|
>
|
||||||
Forget/Logout
|
Forget/Logout
|
||||||
|
@ -90,7 +100,7 @@ function WatchOnLbryPopup(params: { profile: Awaited<ReturnType<typeof getProfil
|
||||||
<a onClick={() => exportProfileKeysAsFile()} className={`button active`}>
|
<a onClick={() => exportProfileKeysAsFile()} className={`button active`}>
|
||||||
Export
|
Export
|
||||||
</a>
|
</a>
|
||||||
<a onClick={() => openImportPopup()}
|
<a onClick={() => importButtonClick()}
|
||||||
className={`button`}
|
className={`button`}
|
||||||
>
|
>
|
||||||
Import
|
Import
|
||||||
|
@ -101,7 +111,7 @@ function WatchOnLbryPopup(params: { profile: Awaited<ReturnType<typeof getProfil
|
||||||
<label>Purge your profile and data!</label>
|
<label>Purge your profile and data!</label>
|
||||||
<p>Purge your profile data online and offline.</p>
|
<p>Purge your profile data online and offline.</p>
|
||||||
<div className='options'>
|
<div className='options'>
|
||||||
<div className="center">
|
<div className="purge-aaaaaaa">
|
||||||
<span className='filled'>(╯°□°)╯︵ ┻━┻</span>
|
<span className='filled'>(╯°□°)╯︵ ┻━┻</span>
|
||||||
</div>
|
</div>
|
||||||
<a onClick={() => loads(purgeProfile(dialogManager)).then(() => renderPopup())} className={`button`}>
|
<a onClick={() => loads(purgeProfile(dialogManager)).then(() => renderPopup())} className={`button`}>
|
||||||
|
@ -129,7 +139,7 @@ function WatchOnLbryPopup(params: { profile: Awaited<ReturnType<typeof getProfil
|
||||||
<label>You don't have a profile.</label>
|
<label>You don't have a profile.</label>
|
||||||
<p>You can either import keypair for an existing profile or generate a new profile keypair.</p>
|
<p>You can either import keypair for an existing profile or generate a new profile keypair.</p>
|
||||||
<div className='options'>
|
<div className='options'>
|
||||||
<a onClick={() => openImportPopup()} className={`button`}>
|
<a onClick={() => importButtonClick()} className={`button`}>
|
||||||
Import
|
Import
|
||||||
</a>
|
</a>
|
||||||
<a onClick={() => loads(generateProfileAndSetNickname(dialogManager)).then(() => renderPopup())} className={`button active`}>
|
<a onClick={() => loads(generateProfileAndSetNickname(dialogManager)).then(() => renderPopup())} className={`button active`}>
|
||||||
|
@ -139,88 +149,48 @@ function WatchOnLbryPopup(params: { profile: Awaited<ReturnType<typeof getProfil
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
:
|
:
|
||||||
route === 'advanced' ?
|
<main>
|
||||||
<main>
|
<section>
|
||||||
<section>
|
<label>Pick a mode:</label>
|
||||||
<label>Which platform you would like to redirect to?</label>
|
<div className='options'>
|
||||||
<div className='options'>
|
<a onClick={() => setExtensionSetting('redirect', true)} className={`button ${redirect ? 'active' : ''}`}>
|
||||||
{targetPlatforms.map(([name, value]) =>
|
Redirect
|
||||||
<a onClick={() => setExtensionSetting('targetPlatform', name)} className={`button ${targetPlatform === name ? 'active' : ''}`}>
|
|
||||||
{value.displayName}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<label>Which resolver API you want to use?</label>
|
|
||||||
<div className='options'>
|
|
||||||
{ytUrlResolverOptions.map(([name, value]) =>
|
|
||||||
<a onClick={() => setExtensionSetting('urlResolver', name)} className={`button ${urlResolver === name ? 'active' : ''}`}>
|
|
||||||
{value.name}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<a onClick={() => loads(lbryUrlCache.clearAll().then(() => dialogManager.alert("Cleared Cache!")))} className={`button active`}>
|
|
||||||
Clear Resolver Cache
|
|
||||||
</a>
|
</a>
|
||||||
</section>
|
<a onClick={() => setExtensionSetting('redirect', false)} className={`button ${redirect ? '' : 'active'}`}>
|
||||||
</main>
|
Show a button
|
||||||
:
|
|
||||||
<main>
|
|
||||||
<section>
|
|
||||||
<label>Auto redirect when:</label>
|
|
||||||
<div className='options'>
|
|
||||||
<div class="toggle-option">
|
|
||||||
<span>Playing a video</span>
|
|
||||||
<a onClick={() => setExtensionSetting('redirectVideo', !redirectVideo)} className={`button ${redirectVideo ? 'active' : ''}`}>
|
|
||||||
{redirectVideo ? 'Active' : 'Deactive'}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="toggle-option">
|
|
||||||
<span>Playing a playlist</span>
|
|
||||||
<a onClick={() => setExtensionSetting('redirectVideoPlaylist', !redirectVideoPlaylist)} className={`button ${redirectVideoPlaylist ? 'active' : ''}`}>
|
|
||||||
{redirectVideoPlaylist ? 'Active' : 'Deactive'}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="toggle-option">
|
|
||||||
<span>Viewing a channel</span>
|
|
||||||
<a onClick={() => setExtensionSetting('redirectChannel', !redirectChannel)} className={`button ${redirectChannel ? 'active' : ''}`}>
|
|
||||||
{redirectChannel ? 'Active' : 'Deactive'}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<label>Show redirect button on:</label>
|
|
||||||
<div className='options'>
|
|
||||||
<div className="toggle-option">
|
|
||||||
<span>Video Page</span>
|
|
||||||
<a onClick={() => setExtensionSetting('buttonVideoSub', !buttonVideoSub)} className={`button ${buttonVideoSub ? 'active' : ''}`}>
|
|
||||||
{buttonVideoSub ? 'Active' : 'Deactive'}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="toggle-option">
|
|
||||||
<span>Channel Page</span>
|
|
||||||
<a onClick={() => setExtensionSetting('buttonChannelSub', !buttonChannelSub)} className={`button ${buttonChannelSub ? 'active' : ''}`}>
|
|
||||||
{buttonChannelSub ? 'Active' : 'Deactive'}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="toggle-option">
|
|
||||||
<span>Video Player</span>
|
|
||||||
<a onClick={() => setExtensionSetting('buttonVideoPlayer', !buttonVideoPlayer)} className={`button ${buttonVideoPlayer ? 'active' : ''}`}>
|
|
||||||
{buttonVideoPlayer ? 'Active' : 'Deactive'}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<label>Tools</label>
|
|
||||||
<a target='_blank' href='/pages/YTtoLBRY/index.html' className={`filled`}>
|
|
||||||
Subscription Converter
|
|
||||||
</a>
|
</a>
|
||||||
<a className='filled' onClick={() => updateRoute('advanced')}>Advanced Settings</a>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
<section>
|
||||||
|
<label>Which platform you would like to redirect?</label>
|
||||||
|
<div className='options'>
|
||||||
|
{targetPlatforms.map(([name, value]) =>
|
||||||
|
<a onClick={() => setExtensionSetting('targetPlatform', name)} className={`button ${targetPlatform === name ? 'active' : ''}`}>
|
||||||
|
{value.displayName}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<label>Which resolver API you want to use?</label>
|
||||||
|
<div className='options'>
|
||||||
|
{ytUrlResolverOptions.map(([name, value]) =>
|
||||||
|
<a onClick={() => setExtensionSetting('urlResolver', name)} className={`button ${urlResolver === name ? 'active' : ''}`}>
|
||||||
|
{value.name}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<a onClick={() => loads(LbryPathnameCache.clearAll().then(() => dialogManager.alert("Cleared Cache!")))} className={`button active`}>
|
||||||
|
Clear Resolver Cache
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<label>Tools</label>
|
||||||
|
<a target='_blank' href='/pages/YTtoLBRY/index.html' className={`filled`}>
|
||||||
|
Subscription Converter
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
}
|
}
|
||||||
{loading && <div class="overlay">
|
{loading && <div class="overlay">
|
||||||
<span>Loading...</span>
|
<span>Loading...</span>
|
||||||
|
|
|
@ -1,9 +1,3 @@
|
||||||
#popup {
|
|
||||||
width: 40em;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
header {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: .5em;
|
gap: .5em;
|
||||||
|
@ -27,29 +21,24 @@ section {
|
||||||
gap: .75em;
|
gap: .75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
section>label {
|
||||||
font-size: 1.75em;
|
font-size: 1.75em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
section>* {
|
section>.options {
|
||||||
padding: 0 1rem;
|
padding: 0 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.center {
|
#popup {
|
||||||
display: grid;
|
width: 35em;
|
||||||
place-items: center;
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.left {
|
.purge-aaaaaaa {
|
||||||
display: grid;
|
display: grid;
|
||||||
justify-items: start;
|
justify-items: center;
|
||||||
align-items: center;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-option {
|
|
||||||
display: grid;
|
|
||||||
gap: .5em;
|
|
||||||
}
|
}
|
|
@ -2,35 +2,27 @@ import { resolveById } from "../modules/yt/urlResolve"
|
||||||
|
|
||||||
const onGoingLbryPathnameRequest: Record<string, ReturnType<typeof resolveById>> = {}
|
const onGoingLbryPathnameRequest: Record<string, ReturnType<typeof resolveById>> = {}
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener(({ method, data }, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener(({ json }, sender, sendResponse) => {
|
||||||
function resolve(result: Awaited<ReturnType<typeof resolveById>>) {
|
function resolve(result: Awaited<ReturnType<typeof resolveById>>) {
|
||||||
sendResponse(JSON.stringify(result))
|
sendResponse(JSON.stringify(result))
|
||||||
}
|
}
|
||||||
(async () => {
|
(async () => {
|
||||||
|
try {
|
||||||
switch (method) {
|
const params: Parameters<typeof resolveById> = JSON.parse(json)
|
||||||
case 'openTab':
|
// Don't create a new Promise for same ID until on going one is over.
|
||||||
{
|
const promise = onGoingLbryPathnameRequest[json] ?? (onGoingLbryPathnameRequest[json] = resolveById(...params))
|
||||||
const { href }: { href: string } = JSON.parse(data)
|
console.log('lbrypathname request', params, await promise)
|
||||||
chrome.tabs.create({ url: href, active: sender.tab?.active, index: sender.tab ? sender.tab.index + 1 : undefined })
|
resolve(await promise)
|
||||||
}
|
} catch (error) {
|
||||||
break
|
sendResponse('error')
|
||||||
case 'resolveUrl':
|
console.error(error)
|
||||||
try {
|
}
|
||||||
const params: Parameters<typeof resolveById> = JSON.parse(data)
|
finally {
|
||||||
// Don't create a new Promise for same ID until on going one is over.
|
delete onGoingLbryPathnameRequest[json]
|
||||||
const promise = onGoingLbryPathnameRequest[data] ?? (onGoingLbryPathnameRequest[data] = resolveById(...params))
|
|
||||||
resolve(await promise)
|
|
||||||
} catch (error) {
|
|
||||||
sendResponse(`error: ${(error as any).toString()}`)
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
delete onGoingLbryPathnameRequest[data]
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => changeInfo.status === 'complete' && chrome.tabs.sendMessage(tabId, { message: 'url-changed' }))
|
|
@ -1,180 +1,121 @@
|
||||||
import { h, render } from 'preact'
|
import { h, render } from 'preact'
|
||||||
import { parseYouTubeURLTimeString } from '../modules/yt'
|
import { parseYouTubeURLTimeString } from '../modules/yt'
|
||||||
import type { resolveById, ResolveUrlTypes } from '../modules/yt/urlResolve'
|
import { resolveById } from '../modules/yt/urlResolve'
|
||||||
import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTargetPlatfromSettingsEntiries, SourcePlatform, sourcePlatfromSettings, TargetPlatform, targetPlatformSettings } from '../settings';
|
import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, 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,
|
||||||
|
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 sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t))
|
|
||||||
|
|
||||||
interface Target {
|
|
||||||
platform: TargetPlatform
|
|
||||||
lbryPathname: string
|
|
||||||
type: ResolveUrlTypes
|
|
||||||
time: number | null
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Source {
|
|
||||||
platform: SourcePlatform
|
|
||||||
id: string
|
|
||||||
type: ResolveUrlTypes
|
|
||||||
url: URL
|
|
||||||
time: number | null
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetPlatforms = getTargetPlatfromSettingsEntiries()
|
|
||||||
const settings = await getExtensionSettingsAsync()
|
const settings = await getExtensionSettingsAsync()
|
||||||
|
let updater: (() => Promise<void>)
|
||||||
|
|
||||||
// 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 buttonMountPoint = document.createElement('div')
|
/*
|
||||||
buttonMountPoint.style.display = 'inline-flex'
|
* 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
|
||||||
const playerButtonMountPoint = document.createElement('div')
|
*/
|
||||||
playerButtonMountPoint.style.display = 'inline-flex'
|
// Listen URL Change
|
||||||
|
chrome.runtime.onMessage.addListener(({ message }, sender) => message === 'url-changed' && updater())
|
||||||
function WatchOnLbryButton({ source, target }: { source?: Source, target?: Target }) {
|
|
||||||
if (!target || !source) return null
|
|
||||||
const url = getLbryUrlByTarget(target)
|
|
||||||
|
|
||||||
return <div
|
|
||||||
style={{
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateRows: '36px',
|
|
||||||
gridAutoColumns: 'auto',
|
|
||||||
alignContent: 'center'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<a href={`${url.href}`} target={target.platform === targetPlatformSettings.app ? '' : '_blank'} role='button'
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '7px',
|
|
||||||
borderRadius: '2px',
|
|
||||||
padding: '0 16px',
|
|
||||||
margin: '0 4px',
|
|
||||||
|
|
||||||
fontWeight: 'bold',
|
|
||||||
border: '0',
|
|
||||||
color: 'whitesmoke',
|
|
||||||
fontSize: '14px',
|
|
||||||
textDecoration: 'none',
|
|
||||||
backgroundColor: target.platform.theme,
|
|
||||||
backgroundImage: target.platform.theme,
|
|
||||||
...target.platform.button.style?.button,
|
|
||||||
}}
|
|
||||||
onClick={() => findVideoElementAwait(source).then((videoElement) => {
|
|
||||||
videoElement.pause()
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<img src={target.platform.button.icon} height={24} style={{ ...target.platform.button.style?.icon }} />
|
|
||||||
<span>{target.type === 'channel' ? 'Channel on' : 'Watch on'} {target.platform.button.platformNameText}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
function WatchOnLbryPlayerButton({ source, target }: { source?: Source, target?: Target }) {
|
|
||||||
if (!target || !source) return null
|
|
||||||
const url = getLbryUrlByTarget(target)
|
|
||||||
|
|
||||||
return <div
|
|
||||||
style={{
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateRows: '36px',
|
|
||||||
gridAutoColumns: 'auto',
|
|
||||||
alignContent: 'center'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<a href={`${url.href}`} target={target.platform === targetPlatformSettings.app ? '' : '_blank'} role='button'
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '7px',
|
|
||||||
borderRadius: '2px',
|
|
||||||
paddingRight: '10px',
|
|
||||||
|
|
||||||
fontWeight: 'bold',
|
|
||||||
border: '0',
|
|
||||||
color: 'whitesmoke',
|
|
||||||
fontSize: '14px',
|
|
||||||
textDecoration: 'none',
|
|
||||||
...target.platform.button.style?.button,
|
|
||||||
}}
|
|
||||||
onClick={() => findVideoElementAwait(source).then((videoElement) => {
|
|
||||||
videoElement.pause()
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<img src={target.platform.button.icon} height={24} style={{ ...target.platform.button.style?.icon }} />
|
|
||||||
<span>{target.type === 'channel' ? 'Channel on' : 'Watch on'} {target.platform.button.platformNameText}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateButtons(params: { source: Source, target: Target } | null): void {
|
|
||||||
if (!params) {
|
|
||||||
render(<WatchOnLbryButton />, buttonMountPoint)
|
|
||||||
render(<WatchOnLbryPlayerButton />, playerButtonMountPoint)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const mountPlayerButtonBefore = settings.buttonVideoPlayer ?
|
|
||||||
document.querySelector(params.source.platform.htmlQueries.mountPoints.mountPlayerButtonBefore) :
|
|
||||||
null
|
|
||||||
if (!mountPlayerButtonBefore) render(<WatchOnLbryPlayerButton />, playerButtonMountPoint)
|
|
||||||
else {
|
|
||||||
if (playerButtonMountPoint.getAttribute('data-id') !== params.source.id) {
|
|
||||||
mountPlayerButtonBefore.parentElement?.insertBefore(playerButtonMountPoint, mountPlayerButtonBefore)
|
|
||||||
playerButtonMountPoint.setAttribute('data-id', params.source.id)
|
|
||||||
}
|
|
||||||
render(<WatchOnLbryPlayerButton target={params.target} source={params.source} />, playerButtonMountPoint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const mountButtonBefore = settings[(`button${params.source.type[0].toUpperCase() + params.source.type.substring(1)}Sub`) as 'buttonVideoSub' | 'buttonChannelSub'] ?
|
|
||||||
document.querySelector(params.source.platform.htmlQueries.mountPoints.mountButtonBefore[params.source.type]) :
|
|
||||||
null
|
|
||||||
if (!mountButtonBefore) render(<WatchOnLbryButton />, buttonMountPoint)
|
|
||||||
else {
|
|
||||||
if (buttonMountPoint.getAttribute('data-id') !== params.source.id) {
|
|
||||||
mountButtonBefore.parentElement?.insertBefore(buttonMountPoint, mountButtonBefore)
|
|
||||||
buttonMountPoint.setAttribute('data-id', params.source.id)
|
|
||||||
}
|
|
||||||
render(<WatchOnLbryButton target={params.target} source={params.source} />, buttonMountPoint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function findVideoElementAwait(source: Source) {
|
|
||||||
let videoElement: HTMLVideoElement | null = null
|
|
||||||
while (!(videoElement = document.querySelector(source.platform.htmlQueries.videoPlayer))) await sleep(200)
|
|
||||||
return videoElement
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSourceByUrl(url: URL): Promise<Source | null> {
|
|
||||||
const platform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
|
|
||||||
if (!platform) return null
|
|
||||||
|
|
||||||
|
async function getTargetByURL(url: URL) {
|
||||||
if (url.pathname === '/watch' && url.searchParams.has('v')) {
|
if (url.pathname === '/watch' && url.searchParams.has('v')) {
|
||||||
return {
|
const videoId = url.searchParams.get('v')!
|
||||||
id: url.searchParams.get('v')!,
|
const result = await requestResolveById([{ id: videoId, type: 'video' }])
|
||||||
platform,
|
const target: Target | null = result?.[videoId] ? { lbryPathname: result[videoId].id, platfrom: targetPlatformSettings[settings.targetPlatform], time: null } : null
|
||||||
time: url.searchParams.has('t') ? parseYouTubeURLTimeString(url.searchParams.get('t')!) : null,
|
|
||||||
type: 'video',
|
return target
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (url.pathname.startsWith('/channel/')) {
|
else if (url.pathname.startsWith('/channel/')) {
|
||||||
return {
|
await requestResolveById([{ id: url.pathname.substring("/channel/".length), type: 'channel' }])
|
||||||
id: url.pathname.substring("/channel/".length),
|
|
||||||
platform,
|
|
||||||
time: null,
|
|
||||||
type: 'channel',
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (url.pathname.startsWith('/c/') || url.pathname.startsWith('/user/')) {
|
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
|
// We have to download the page content again because these parts of the page are not responsive
|
||||||
|
@ -184,180 +125,72 @@ import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTa
|
||||||
const suffix = `"`
|
const suffix = `"`
|
||||||
const startsAt = content.indexOf(prefix) + prefix.length
|
const startsAt = content.indexOf(prefix) + prefix.length
|
||||||
const endsAt = content.indexOf(suffix, startsAt)
|
const endsAt = content.indexOf(suffix, startsAt)
|
||||||
const id = content.substring(startsAt, endsAt)
|
await requestResolveById([{ id: content.substring(startsAt, endsAt), type: 'channel' }])
|
||||||
return {
|
|
||||||
id,
|
|
||||||
platform,
|
|
||||||
time: null,
|
|
||||||
type: 'channel',
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getTargetsBySources(...sources: Source[]) {
|
async function redirectTo({ lbryPathname, platfrom, time }: Target) {
|
||||||
const params: Parameters<typeof requestResolveById>[0] = sources.map((source) => ({ id: source.id, type: source.type }))
|
const url = new URL(`${platfrom.domainPrefix}${lbryPathname}`)
|
||||||
const platform = targetPlatformSettings[settings.targetPlatform]
|
|
||||||
|
|
||||||
const results = await requestResolveById(params) ?? []
|
if (time) url.searchParams.set('t', time.toFixed(0))
|
||||||
const targets: Record<string, Target | null> = Object.fromEntries(
|
|
||||||
sources.map((source) => {
|
|
||||||
const result = results[source.id]
|
|
||||||
if (!result) return [
|
|
||||||
source.id,
|
|
||||||
null
|
|
||||||
]
|
|
||||||
|
|
||||||
return [
|
findVideoElement().then((videoElement) => {
|
||||||
source.id,
|
videoElement.addEventListener('play', () => videoElement.pause(), { once: true })
|
||||||
{
|
videoElement.pause()
|
||||||
type: result.type,
|
})
|
||||||
lbryPathname: result.id,
|
|
||||||
platform,
|
|
||||||
time: source.time
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
return targets
|
if (platfrom === targetPlatformSettings.app) {
|
||||||
}
|
if (document.hidden) await new Promise((resolve) => document.addEventListener('visibilitychange', resolve, { once: true }))
|
||||||
// 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 response = await new Promise<string | null | 'error'>((resolve) => chrome.runtime.sendMessage({ method: 'resolveUrl', data: JSON.stringify(params) }, resolve))
|
|
||||||
if (response?.startsWith('error:')) {
|
|
||||||
console.error("Background error on:", params)
|
|
||||||
throw new Error(`Background error. ${response ?? ''}`)
|
|
||||||
}
|
|
||||||
return response ? JSON.parse(response) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request new tab
|
// On redirect with app, people might choose to cancel browser's dialog
|
||||||
async function openNewTab(url: URL) {
|
// So we dont destroy the current window automatically for them
|
||||||
chrome.runtime.sendMessage({ method: 'openTab', data: JSON.stringify({ href: url.href }) })
|
// And also we are keeping the same window for less distiraction
|
||||||
}
|
if (settings.redirect) {
|
||||||
|
location.replace(url.toString())
|
||||||
function findTargetFromSourcePage(source: Source): Target | null {
|
}
|
||||||
const linksContainer =
|
else {
|
||||||
source.type === 'video' ?
|
open(url.toString(), '_blank')
|
||||||
document.querySelector(source.platform.htmlQueries.videoDescription) :
|
if (window.history.length === 1) window.close()
|
||||||
source.platform.htmlQueries.channelLinks ? document.querySelector(source.platform.htmlQueries.channelLinks) : null
|
else window.history.back()
|
||||||
|
|
||||||
if (linksContainer) {
|
|
||||||
const anchors = Array.from(linksContainer.querySelectorAll<HTMLAnchorElement>('a'))
|
|
||||||
|
|
||||||
for (const anchor of anchors) {
|
|
||||||
if (!anchor.href) continue
|
|
||||||
const url = new URL(anchor.href)
|
|
||||||
let lbryURL: URL | null = null
|
|
||||||
|
|
||||||
// Extract real link from youtube's redirect link
|
|
||||||
if (source.platform === sourcePlatfromSettings['youtube.com']) {
|
|
||||||
if (!targetPlatforms.some(([key, platform]) => url.searchParams.get('q')?.startsWith(platform.domainPrefix))) continue
|
|
||||||
lbryURL = new URL(url.searchParams.get('q')!)
|
|
||||||
}
|
|
||||||
// Just directly use the link itself on other platforms
|
|
||||||
else {
|
|
||||||
if (!targetPlatforms.some(([key, platform]) => url.href.startsWith(platform.domainPrefix))) continue
|
|
||||||
lbryURL = new URL(url.href)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lbryURL) {
|
|
||||||
return {
|
|
||||||
lbryPathname: lbryURL.pathname.substring(1),
|
|
||||||
time: null,
|
|
||||||
type: lbryURL.pathname.substring(1).includes('/') ? 'video' : 'channel',
|
|
||||||
platform: targetPlatformSettings[settings.targetPlatform]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
else
|
||||||
|
location.replace(url.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLbryUrlByTarget(target: Target) {
|
let removeVideoTimeUpdateListener: (() => void) | null = null
|
||||||
const url = new URL(`${target.platform.domainPrefix}${target.lbryPathname}`)
|
async function onModeChange() {
|
||||||
if (target.time) url.searchParams.set('t', target.time.toFixed(0))
|
let target: Target | null = null
|
||||||
|
if (settings.redirect)
|
||||||
return url
|
updater = async () => {
|
||||||
}
|
const url = new URL(location.href)
|
||||||
|
target = await getTargetByURL(url)
|
||||||
|
if (!target) return
|
||||||
// Master Loop
|
target.time = url.searchParams.has('t') ? parseYouTubeURLTimeString(url.searchParams.get('t')!) : null
|
||||||
for (
|
redirectTo(target)
|
||||||
let url = new URL(location.href),
|
|
||||||
urlHrefCache: string | null = null;
|
|
||||||
;
|
|
||||||
urlHrefCache = url.href,
|
|
||||||
url = new URL(location.href)
|
|
||||||
) {
|
|
||||||
await sleep(500)
|
|
||||||
try {
|
|
||||||
const source = await getSourceByUrl(new URL(location.href))
|
|
||||||
if (!source) {
|
|
||||||
updateButtons(null)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
const target = (await getTargetsBySources(source))[source.id] ?? findTargetFromSourcePage(source)
|
else {
|
||||||
if (!target) {
|
const mountPoint = await findButtonMountPoint()
|
||||||
updateButtons(null)
|
const videoElement = await findVideoElement()
|
||||||
continue
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update Buttons
|
|
||||||
if (urlHrefCache !== url.href) updateButtons(null)
|
|
||||||
// If target is a video target add timestampt to it
|
|
||||||
if (target.type === 'video') {
|
|
||||||
const videoElement = document.querySelector<HTMLVideoElement>(source.platform.htmlQueries.videoPlayer)
|
|
||||||
if (videoElement) target.time = videoElement.currentTime > 3 && videoElement.currentTime < videoElement.duration - 1 ? videoElement.currentTime : null
|
|
||||||
}
|
|
||||||
updateButtons({ target, source })
|
|
||||||
|
|
||||||
// Redirect
|
|
||||||
if (
|
|
||||||
source.type === target.type &&
|
|
||||||
(
|
|
||||||
(
|
|
||||||
settings.redirectVideo &&
|
|
||||||
source.type === 'video' && !source.url.searchParams.has('list')
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
settings.redirectVideoPlaylist &&
|
|
||||||
source.type === 'video' && source.url.searchParams.has('list')
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
settings.redirectChannel &&
|
|
||||||
source.type === 'channel'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
if (url.href === urlHrefCache) continue
|
|
||||||
|
|
||||||
const lbryURL = getLbryUrlByTarget(target)
|
|
||||||
|
|
||||||
if (source.type === 'video') {
|
|
||||||
findVideoElementAwait(source).then((videoElement) => videoElement.pause())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.platform === targetPlatformSettings.app) {
|
|
||||||
if (document.hidden) await new Promise((resolve) => document.addEventListener('visibilitychange', resolve, { once: true }))
|
|
||||||
// Replace is being used so browser doesnt start an empty window
|
|
||||||
// Its not gonna be able to replace anyway, since its a LBRY Uri
|
|
||||||
location.replace(lbryURL)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
openNewTab(lbryURL)
|
|
||||||
if (window.history.length === 1)
|
|
||||||
window.close()
|
|
||||||
else
|
|
||||||
window.history.back()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
}
|
||||||
|
await updater()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await onModeChange()
|
||||||
})()
|
})()
|
|
@ -1,2 +0,0 @@
|
||||||
import './settings/background'
|
|
||||||
import './scripts/background'
|
|
|
@ -1,33 +1,31 @@
|
||||||
import { DEFAULT_SETTINGS, ExtensionSettings, getExtensionSettingsAsync, setExtensionSetting, targetPlatformSettings, ytUrlResolversSettings } from '../settings'
|
import { DEFAULT_SETTINGS, ExtensionSettings, getExtensionSettingsAsync, setExtensionSetting, targetPlatformSettings, ytUrlResolversSettings } from '.'
|
||||||
|
|
||||||
// This is for manifest v2 and v3
|
|
||||||
const chromeAction = chrome.action ?? chrome.browserAction
|
|
||||||
|
|
||||||
/** Reset settings to default value and update the browser badge text */
|
/** Reset settings to default value and update the browser badge text */
|
||||||
async function initSettings() {
|
async function initSettings() {
|
||||||
let settings = await getExtensionSettingsAsync()
|
let settings = await getExtensionSettingsAsync()
|
||||||
|
|
||||||
// get all the values that aren't set and use them as a change set
|
// get all the values that aren't set and use them as a change set
|
||||||
const invalidEntries = (Object.entries(DEFAULT_SETTINGS) as Array<[keyof ExtensionSettings, ExtensionSettings[keyof ExtensionSettings]]>)
|
const invalidEntries = (Object.entries(DEFAULT_SETTINGS) as Array<[keyof ExtensionSettings, ExtensionSettings[keyof ExtensionSettings]]>)
|
||||||
.filter(([k]) => settings[k] === undefined || settings[k] === null)
|
.filter(([k]) => settings[k] === undefined || settings[k] === null)
|
||||||
|
|
||||||
// fix our local var and set it in storage for later
|
// fix our local var and set it in storage for later
|
||||||
if (invalidEntries.length > 0) {
|
if (invalidEntries.length > 0) {
|
||||||
const changeSet = Object.fromEntries(invalidEntries)
|
const changeSet = Object.fromEntries(invalidEntries)
|
||||||
chrome.storage.local.set(changeSet)
|
chrome.storage.local.set(changeSet)
|
||||||
settings = await getExtensionSettingsAsync()
|
settings = await getExtensionSettingsAsync()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Object.keys(targetPlatformSettings).includes(settings.targetPlatform)) setExtensionSetting('targetPlatform', DEFAULT_SETTINGS.targetPlatform)
|
if (!Object.keys(targetPlatformSettings).includes(settings.targetPlatform)) setExtensionSetting('targetPlatform', DEFAULT_SETTINGS.targetPlatform)
|
||||||
if (!Object.keys(ytUrlResolversSettings).includes(settings.urlResolver)) setExtensionSetting('urlResolver', DEFAULT_SETTINGS.urlResolver)
|
if (!Object.keys(ytUrlResolversSettings).includes(settings.urlResolver)) setExtensionSetting('urlResolver', DEFAULT_SETTINGS.urlResolver)
|
||||||
|
|
||||||
// chromeAction.setBadgeText({ text: settings.redirect ? 'ON' : 'OFF' })
|
chrome.browserAction.setBadgeText({ text: settings.redirect ? 'ON' : 'OFF' })
|
||||||
}
|
}
|
||||||
|
|
||||||
/* chrome.storage.onChanged.addListener((changes, areaName) => {
|
chrome.storage.onChanged.addListener((changes, areaName) => {
|
||||||
if (areaName !== 'local' || !changes.redirect) return
|
if (areaName !== 'local' || !changes.redirect) return
|
||||||
chromeAction.setBadgeText({ text: changes.redirect.newValue ? 'ON' : 'OFF' })
|
chrome.browserAction.setBadgeText({ text: changes.redirect.newValue ? 'ON' : 'OFF' })
|
||||||
}) */
|
})
|
||||||
|
|
||||||
|
|
||||||
chrome.runtime.onStartup.addListener(initSettings)
|
chrome.runtime.onStartup.addListener(initSettings)
|
||||||
chrome.runtime.onInstalled.addListener(initSettings)
|
chrome.runtime.onInstalled.addListener(initSettings)
|
|
@ -1,29 +1,18 @@
|
||||||
import type { JSX } from "preact"
|
import { 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 extends Record<string, string | number | boolean | null | undefined>{
|
export interface ExtensionSettings {
|
||||||
|
redirect: boolean
|
||||||
targetPlatform: TargetPlatformName
|
targetPlatform: TargetPlatformName
|
||||||
urlResolver: YTUrlResolverName,
|
urlResolver: YTUrlResolverName
|
||||||
redirectVideo: boolean,
|
|
||||||
redirectChannel: boolean,
|
|
||||||
redirectVideoPlaylist: boolean,
|
|
||||||
buttonVideoSub: boolean
|
|
||||||
buttonVideoPlayer: boolean
|
|
||||||
buttonChannelSub: boolean
|
|
||||||
publicKey: string | null,
|
publicKey: string | null,
|
||||||
privateKey: string | null
|
privateKey: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: ExtensionSettings = {
|
export const DEFAULT_SETTINGS: ExtensionSettings = {
|
||||||
|
redirect: true,
|
||||||
targetPlatform: 'odysee',
|
targetPlatform: 'odysee',
|
||||||
urlResolver: 'odyseeApi',
|
urlResolver: 'odyseeApi',
|
||||||
redirectVideo: false,
|
|
||||||
redirectChannel: false,
|
|
||||||
redirectVideoPlaylist: false,
|
|
||||||
buttonVideoSub: true,
|
|
||||||
buttonVideoPlayer: true,
|
|
||||||
buttonChannelSub: true,
|
|
||||||
privateKey: null,
|
privateKey: null,
|
||||||
publicKey: null
|
publicKey: null
|
||||||
}
|
}
|
||||||
|
@ -70,7 +59,7 @@ const targetPlatform = (o: {
|
||||||
displayName: string
|
displayName: string
|
||||||
theme: string
|
theme: string
|
||||||
button: {
|
button: {
|
||||||
platformNameText: string,
|
text: string
|
||||||
icon: string
|
icon: string
|
||||||
style?:
|
style?:
|
||||||
{
|
{
|
||||||
|
@ -88,31 +77,31 @@ export const targetPlatformSettings = {
|
||||||
'madiator.com': targetPlatform({
|
'madiator.com': targetPlatform({
|
||||||
domainPrefix: 'https://madiator.com/',
|
domainPrefix: 'https://madiator.com/',
|
||||||
displayName: 'Madiator.com',
|
displayName: 'Madiator.com',
|
||||||
theme: 'linear-gradient(130deg, #499375, #43889d)',
|
theme: '#075656',
|
||||||
button: {
|
button: {
|
||||||
platformNameText: '',
|
text: 'Watch on',
|
||||||
icon: chrome.runtime.getURL('assets/icons/lbry/madiator-logo.svg'),
|
icon: chrome.runtime.getURL('assets/icons/lbry/madiator-logo.svg'),
|
||||||
style: {
|
style: {
|
||||||
button: { flexDirection: 'row-reverse' },
|
button: { flexDirection: 'row-reverse' },
|
||||||
icon: { }
|
icon: { transform: 'scale(1.2)' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
odysee: targetPlatform({
|
odysee: targetPlatform({
|
||||||
domainPrefix: 'https://odysee.com/',
|
domainPrefix: 'https://odysee.com/',
|
||||||
displayName: 'Odysee',
|
displayName: 'Odysee',
|
||||||
theme: 'linear-gradient(130deg, #c63d59, #f77937)',
|
theme: '#1e013b',
|
||||||
button: {
|
button: {
|
||||||
platformNameText: 'Odysee',
|
text: 'Watch on Odysee',
|
||||||
icon: chrome.runtime.getURL('assets/icons/lbry/odysee-logo.svg')
|
icon: chrome.runtime.getURL('assets/icons/lbry/odysee-logo.svg')
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
app: targetPlatform({
|
app: targetPlatform({
|
||||||
domainPrefix: 'lbry://',
|
domainPrefix: 'lbry://',
|
||||||
displayName: 'LBRY App',
|
displayName: 'LBRY App',
|
||||||
theme: 'linear-gradient(130deg, #499375, #43889d)',
|
theme: '#075656',
|
||||||
button: {
|
button: {
|
||||||
platformNameText: 'LBRY',
|
text: 'Watch on LBRY',
|
||||||
icon: chrome.runtime.getURL('assets/icons/lbry/lbry-logo.svg')
|
icon: chrome.runtime.getURL('assets/icons/lbry/lbry-logo.svg')
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -123,13 +112,8 @@ export const targetPlatformSettings = {
|
||||||
const sourcePlatform = (o: {
|
const sourcePlatform = (o: {
|
||||||
hostnames: string[]
|
hostnames: string[]
|
||||||
htmlQueries: {
|
htmlQueries: {
|
||||||
mountPoints: {
|
mountButtonBefore: string,
|
||||||
mountButtonBefore: Record<ResolveUrlTypes, string>,
|
videoPlayer: string
|
||||||
mountPlayerButtonBefore: string,
|
|
||||||
}
|
|
||||||
videoPlayer: string,
|
|
||||||
videoDescription: string
|
|
||||||
channelLinks: string
|
|
||||||
}
|
}
|
||||||
}) => o
|
}) => o
|
||||||
export type SourcePlatform = ReturnType<typeof sourcePlatform>
|
export type SourcePlatform = ReturnType<typeof sourcePlatform>
|
||||||
|
@ -141,35 +125,18 @@ export function getSourcePlatfromSettingsFromHostname(hostname: string) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
export const sourcePlatfromSettings = {
|
export const sourcePlatfromSettings = {
|
||||||
"youtube.com": sourcePlatform({
|
|
||||||
hostnames: ['www.youtube.com'],
|
|
||||||
htmlQueries: {
|
|
||||||
mountPoints: {
|
|
||||||
mountButtonBefore: {
|
|
||||||
video: 'ytd-video-owner-renderer~#subscribe-button',
|
|
||||||
channel: '#channel-header-container #buttons #subscribe-button'
|
|
||||||
},
|
|
||||||
mountPlayerButtonBefore: 'ytd-watch-flexy ytd-player .ytp-right-controls',
|
|
||||||
},
|
|
||||||
videoPlayer: '#ytd-player video',
|
|
||||||
videoDescription: 'ytd-video-secondary-info-renderer #description',
|
|
||||||
channelLinks: '#channel-header #links-holder'
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
"yewtu.be": sourcePlatform({
|
"yewtu.be": sourcePlatform({
|
||||||
hostnames: ['yewtu.be', 'vid.puffyan.us', 'invidio.xamh.de', 'invidious.kavin.rocks'],
|
hostnames: ['yewtu.be', 'vid.puffyan.us', 'invidio.xamh.de', 'invidious.kavin.rocks'],
|
||||||
htmlQueries: {
|
htmlQueries: {
|
||||||
mountPoints: {
|
mountButtonBefore: '#watch-on-youtube',
|
||||||
mountButtonBefore:
|
videoPlayer: '#player-container video'
|
||||||
{
|
}
|
||||||
video: '#subscribe',
|
}),
|
||||||
channel: '#subscribe'
|
"youtube.com": sourcePlatform({
|
||||||
},
|
hostnames: ['www.youtube.com'],
|
||||||
mountPlayerButtonBefore: '#player-container ~ .h-box > h1 > a',
|
htmlQueries: {
|
||||||
},
|
mountButtonBefore: 'ytd-video-owner-renderer~#subscribe-button',
|
||||||
videoPlayer: '#player-container video',
|
videoPlayer: '#ytd-player video'
|
||||||
videoDescription: '#descriptionWrapper',
|
|
||||||
channelLinks: '#descriptionWrapper'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,6 @@
|
||||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
"importsNotUsedAsValues": "error", /* Import types always with `import type` */
|
|
||||||
|
|
||||||
/* Module Resolution Options */
|
/* Module Resolution Options */
|
||||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
|
@ -54,6 +53,7 @@
|
||||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
|
||||||
/* Source Map Options */
|
/* Source Map Options */
|
||||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
|
Loading…
Add table
Reference in a new issue