diff --git a/src/global.d.ts b/global.d.ts similarity index 96% rename from src/global.d.ts rename to global.d.ts index 55d74f2..ddd21c7 100644 --- a/src/global.d.ts +++ b/global.d.ts @@ -1,4 +1,4 @@ declare module '*.md' { var _: string export default _ -} +} \ No newline at end of file diff --git a/package.json b/package.json index 3b11278..2d74d2c 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "scripts": { "build:assets": "cpx \"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/scripts/*.{js,jsx,ts,tsx}\" \"src/**/*.html\"", - "watch:parcel": "parcel watch --no-hmr --no-source-maps \"src/scripts/*.{js,jsx,ts,tsx}\" \"src/**/*.html\"", + "build:parcel": "cross-env NODE_ENV=production parcel build --no-source-maps --no-minify \"src/**/*.{js,jsx,ts,tsx}\" \"src/**/*.html\"", + "watch:parcel": "parcel watch --no-hmr --no-source-maps \"src/**/*.{js,jsx,ts,tsx}\" \"src/**/*.html\"", "build:webext": "web-ext build --source-dir ./dist --overwrite-dest", "build": "npm-run-all -l -p build:parcel build:assets", "watch": "npm-run-all -l -p watch:parcel watch:assets", diff --git a/src/icons/lbry/lbry-logo.svg b/src/assets/icons/lbry/lbry-logo.svg similarity index 100% rename from src/icons/lbry/lbry-logo.svg rename to src/assets/icons/lbry/lbry-logo.svg diff --git a/src/icons/lbry/madiator-logo.svg b/src/assets/icons/lbry/madiator-logo.svg similarity index 100% rename from src/icons/lbry/madiator-logo.svg rename to src/assets/icons/lbry/madiator-logo.svg diff --git a/src/icons/lbry/odysee-logo.svg b/src/assets/icons/lbry/odysee-logo.svg similarity index 100% rename from src/icons/lbry/odysee-logo.svg rename to src/assets/icons/lbry/odysee-logo.svg diff --git a/src/icons/wol/default-monochrome-black.svg b/src/assets/icons/wol/default-monochrome-black.svg similarity index 100% rename from src/icons/wol/default-monochrome-black.svg rename to src/assets/icons/wol/default-monochrome-black.svg diff --git a/src/icons/wol/default-monochrome-white.svg b/src/assets/icons/wol/default-monochrome-white.svg similarity index 100% rename from src/icons/wol/default-monochrome-white.svg rename to src/assets/icons/wol/default-monochrome-white.svg diff --git a/src/icons/wol/default-transparent.svg b/src/assets/icons/wol/default-transparent.svg similarity index 100% rename from src/icons/wol/default-transparent.svg rename to src/assets/icons/wol/default-transparent.svg diff --git a/src/icons/wol/default.svg b/src/assets/icons/wol/default.svg similarity index 100% rename from src/icons/wol/default.svg rename to src/assets/icons/wol/default.svg diff --git a/src/icons/wol/icon128.png b/src/assets/icons/wol/icon128.png similarity index 100% rename from src/icons/wol/icon128.png rename to src/assets/icons/wol/icon128.png diff --git a/src/icons/wol/icon16.png b/src/assets/icons/wol/icon16.png similarity index 100% rename from src/icons/wol/icon16.png rename to src/assets/icons/wol/icon16.png diff --git a/src/icons/wol/icon48.png b/src/assets/icons/wol/icon48.png similarity index 100% rename from src/icons/wol/icon48.png rename to src/assets/icons/wol/icon48.png diff --git a/src/icons/wol/isolated-layout.svg b/src/assets/icons/wol/isolated-layout.svg similarity index 100% rename from src/icons/wol/isolated-layout.svg rename to src/assets/icons/wol/isolated-layout.svg diff --git a/src/icons/wol/isolated-monochrome-black.svg b/src/assets/icons/wol/isolated-monochrome-black.svg similarity index 100% rename from src/icons/wol/isolated-monochrome-black.svg rename to src/assets/icons/wol/isolated-monochrome-black.svg diff --git a/src/icons/wol/isolated-monochrome-white.svg b/src/assets/icons/wol/isolated-monochrome-white.svg similarity index 100% rename from src/icons/wol/isolated-monochrome-white.svg rename to src/assets/icons/wol/isolated-monochrome-white.svg diff --git a/src/common/common.css b/src/assets/styles/common.css similarity index 72% rename from src/common/common.css rename to src/assets/styles/common.css index 3a259ca..0d863d9 100644 --- a/src/common/common.css +++ b/src/assets/styles/common.css @@ -10,10 +10,17 @@ } :root { + font-size: .95rem; font-family: Arial, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Open Sans, Helvetica Neue, sans-serif; letter-spacing: .2ch; } +body#page { + --root-smallest-font-size: .95rem; + --root-font-size-relative-to-screen: 1; + font-size: max(var(--root-smallest-font-size), calc(min(1vw, 2vh) * var(--root-font-size-relative-to-screen))); +} + body { background: linear-gradient(to left top, var(--color-master), var(--color-slave)); background-attachment: fixed; @@ -24,7 +31,7 @@ body { body::before { content: ""; - position: absolute; + position: fixed; inset: 0; background: rgba(19, 19, 19, 0.75); } @@ -36,6 +43,30 @@ body::before { position: relative; } +a { + cursor: pointer; +} + +p, +ul, +ol, +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0; +} + +.options { + display: grid; + width: 100%; + grid-template-columns: repeat(auto-fit, minmax(3em, 1fr)); + justify-content: center; + gap: .25em; + padding: 0 1.5em; +} .button { display: inline-flex; @@ -48,14 +79,9 @@ body::before { color: var(--color-light); border-radius: .5em; -} + border: unset; -.filled { - background: var(--color-gradient-0); - background-clip: text; - -webkit-background-clip: text; - font-weight: bold; - color: transparent; + font-size: inherit; } .button.active { @@ -77,13 +103,36 @@ body::before { pointer-events: none; } -.options { +.filled { + background: var(--color-gradient-0); + background-clip: text; + -webkit-background-clip: text; + font-weight: bold; + color: transparent; +} + +.error { display: grid; - width: 100%; - grid-template-columns: repeat(auto-fit, minmax(3em, 1fr)); - justify-content: center; - gap: .25em; - padding: 0 1.5em; + grid-auto-flow: column; + gap: .5em; + align-items: center; + justify-content: start; + color: var(--color-error); +} + +.error::before { + content: "!"; + width: 2em; + aspect-ratio: 1/1; + + display: grid; + place-items: center; + letter-spacing: 0; + line-height: 0; + + border: .1em solid currentColor; + border-radius: 100000vw; + font-weight: bold; } .overlay { diff --git a/src/common/components/ButtonRadio.sass b/src/common/components/ButtonRadio.sass deleted file mode 100644 index 3109995..0000000 --- a/src/common/components/ButtonRadio.sass +++ /dev/null @@ -1,21 +0,0 @@ -@import '../style' - -.ButtonRadio - display: flex - justify-content: center - flex-wrap: wrap - gap: .25em - cursor: pointer - - * - cursor: pointer - - .radio-button - @extend .button - - .radio-button.checked - @extend .button.active - - input[type="radio"] - opacity: 0 - position: absolute diff --git a/src/common/components/ButtonRadio.tsx b/src/common/components/ButtonRadio.tsx deleted file mode 100644 index b38ce41..0000000 --- a/src/common/components/ButtonRadio.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import classnames from 'classnames' -import { h } from 'preact' -import './ButtonRadio.sass' - - -export interface SelectionOption { - value: string - display: string -} - -export interface ButtonRadioProps { - name?: string - onChange(redirect: string): void - value: T extends SelectionOption ? T['value'] : T - options: T[] -} - -const getAttr = (x: string | SelectionOption, key: keyof SelectionOption): string => typeof x === 'string' ? x : x[key] - -export default function ButtonRadio({ name = 'buttonRadio', onChange, options, value }: ButtonRadioProps) { - /** If it's a string, return the string, if it's a SelectionOption get the selection option property */ - return
- {options.map(o => ({ o: getAttr(o, 'value'), display: getAttr(o, 'display') })).map(({ o, display }) => -
o !== value && onChange(o)}> - - -
- )} -
-} diff --git a/src/common/lbry-url.spec.ts b/src/common/lbry-url.spec.ts deleted file mode 100644 index 14d30f0..0000000 --- a/src/common/lbry-url.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { appRedirectUrl, parseProtocolUrl } from './lbry-url' - -describe('web url parsing', () => { - const testCases: [string, string | undefined][] = [ - ['https://lbry.tv/@test:7/foo-123:7', 'lbry://@test:7/foo-123:7'], - ['https://lbry.tv/@test1:c/foo:8', 'lbry://@test1:c/foo:8'], - ['https://lbry.tv/@test1:0/foo-bar-2-baz-7:e#adasasddasdas123', 'lbry://@test1:0/foo-bar-2-baz-7:e'], - ['https://lbry.tv/@test:7', 'lbry://@test:7'], - ['https://lbry.tv/@test:c', 'lbry://@test:c'], - ['https://lbry.tv/$/discover?t=foo%20bar', undefined], - ['https://lbry.tv/$/signup?redirect=/@test1:0/foo-bar-2-baz-7:e#adasasddasdas123', undefined], - ] - - test.each(testCases)('redirect %s', (url, expected) => { - expect(appRedirectUrl(url)).toEqual(expected) - }) -}) - -describe('app url parsing', () => { - const testCases: Array<[string, string[]]> = [ - ['test', ['test']], - ['@test', ['@test']], - ['lbry://@test$1/stuff', ['@test$1', 'stuff']], - ] - - test.each(testCases)('redirect %s', (url, expected) => { - expect(parseProtocolUrl(url)).toEqual(expected) - }) -}) diff --git a/src/common/lbry-url.ts b/src/common/lbry-url.ts deleted file mode 100644 index 20873b4..0000000 --- a/src/common/lbry-url.ts +++ /dev/null @@ -1,142 +0,0 @@ -// Port of https://github.com/lbryio/lbry-sdk/blob/master/lbry/schema/url.py - -interface UrlOptions { - /** - * Whether or not to encodeURIComponent the path segments. - * Doing so is a workaround such that browsers interpret it as a valid URL in a way that the desktop app understands. - */ - encode?: boolean -} - -const invalidNamesRegex = /[^=&#:$@%*?;\"/\\<>%{}|^~`\[\]\u0000-\u0020\uD800-\uDFFF\uFFFE-\uFFFF]+/.source - -/** Creates a named regex group */ -const named = (name: string, regex: string) => `(?<${name}>${regex})` -/** Creates a non-capturing group */ -const group = (regex: string) => `(?:${regex})` -/** Allows for one of the patterns */ -const oneOf = (...choices: string[]) => group(choices.join('|')) -/** Create an lbry url claim */ -const claim = (name: string, prefix = '') => group( - named(`${name}_name`, prefix + invalidNamesRegex) - + oneOf( - group(':' + named(`${name}_claim_id`, '[0-9a-f]{1,40}')), - group('\\*' + named(`${name}_sequence`, '[1-9][0-9]*')), - group('\\$' + named(`${name}_amount_order`, '[1-9][0-9]*')) - ) + '?' -) - -/** Create an lbry url claim, but use the old pattern for claims */ -const legacyClaim = (name: string, prefix = '') => group( - named(`${name}_name`, prefix + invalidNamesRegex) - + oneOf( - group('#' + named(`${name}_claim_id`, '[0-9a-f]{1,40}')), - group(':' + named(`${name}_sequence`, '[1-9][0-9]*')), - group('\\$' + named(`${name}_amount_order`, '[1-9][0-9]*')) - ) + '?') - -export const builder = { named, group, oneOf, claim, legacyClaim, invalidNamesRegex } - -/** Creates a pattern to parse lbry protocol URLs. Unused, but I left it here. */ -function createProtocolUrlRegex(legacy = false) { - const claim = legacy ? builder.legacyClaim : builder.claim - return new RegExp('^' + named('scheme', 'lbry://') + '?' + oneOf( - group(claim('channel_with_stream', '@') + '/' + claim('stream_in_channel')), - claim('channel', '@'), - claim('stream'), - ) + '$') -} - -/** Creates a pattern to match lbry.tv style sites by their pathname */ -function createWebUrlRegex(legacy = false) { - const claim = legacy ? builder.legacyClaim : builder.claim - return new RegExp('^/' + oneOf( - group(claim('channel_with_stream', '@') + '/' + claim('stream_in_channel')), - claim('channel', '@'), - claim('stream'), - ) + '$') -} - -/** Pattern for lbry.tv style sites */ -export const URL_REGEX = createWebUrlRegex() -export const PROTOCOL_URL_REGEX = createProtocolUrlRegex() -const PROTOCOL_URL_REGEX_LEGACY = createProtocolUrlRegex(true) - -/** - * Encapsulates a lbry url path segment. - * Handles `StreamClaimNameAndModifier' and `ChannelClaimNameAndModifier` - */ -export class PathSegment { - constructor(public name: string, - public claimID?: string, - public sequence?: number, - public amountOrder?: number) { } - - static fromMatchGroup(segment: string, groups: Record) { - return new PathSegment( - groups[`${segment}_name`], - groups[`${segment}_claim_id`], - parseInt(groups[`${segment}_sequence`]), - parseInt(groups[`${segment}_amount_order`]) - ) - } - - /** Prints the segment */ - toString() { - if (this.claimID) return `${this.name}:${this.claimID}` - if (this.sequence) return `${this.name}*${this.sequence}` - if (this.amountOrder) return `${this.name}$${this.amountOrder}` - return this.name - } -} - -/** - * Utility function - * - * @param ptn pattern to use; specific to the patterns defined in this file - * @param url the url to try to parse - * @returns an array of path segments; if invalid, will return an empty array - */ -function patternSegmenter(ptn: RegExp, url: string, options: UrlOptions = { encode: false }): string[] { - const match = url.match(ptn)?.groups - if (!match) return [] - - const segments = match['channel_name'] ? ['channel'] - : match['channel_with_stream_name'] ? ['channel_with_stream', 'stream_in_channel'] - : match['stream_name'] ? ['stream'] - : null - - if (!segments) throw new Error(`${url} matched the overall pattern, but could not determine type`) - - return segments.map(s => PathSegment.fromMatchGroup(s, match).toString()) - .map(s => options.encode ? encodeURIComponent(s) : s) -} - -/** - * Produces the lbry protocl URL from the frontend URL - * - * @param url lbry frontend URL - * @param options options for the redirect - */ -export function appRedirectUrl(url: string, options?: UrlOptions): string | undefined { - const segments = patternSegmenter(URL_REGEX, new URL(url).pathname, options) - if (segments.length === 0) return - const path = segments.join('/') - - return `lbry://${path}` -} - -/** - * Parses a lbry protocol and returns its constituent path segments. Attempts the spec compliant and then the old URL schemes. - * - * @param url the lbry url - * @returns an array of path segments; if invalid, will return an empty array - */ -export function parseProtocolUrl(url: string, options: UrlOptions = { encode: false }): string[] { - for (const ptn of [PROTOCOL_URL_REGEX, PROTOCOL_URL_REGEX_LEGACY]) { - const segments = patternSegmenter(ptn, url, options) - if (segments.length === 0) continue - return segments - } - return [] -} diff --git a/src/common/style.sass b/src/common/style.sass deleted file mode 100644 index 452d0b4..0000000 --- a/src/common/style.sass +++ /dev/null @@ -1,33 +0,0 @@ -$background-color: #191a1c !default -$text-color: whitesmoke !default - -$btn-color: #075656 !default -$btn-select: teal !default - -body - width: 30em - text-align: center - background-color: $background-color - color: $text-color - font-family: sans-serif - padding: 1em - -.container - display: block - text-align: center - -.button - border-radius: .5em - background-color: $btn-color - border: .2em solid $btn-color - color: $text-color - font-size: 0.8rem - font-weight: 400 - padding: .5em - text-align: center - - &.active - border-color: $btn-select - - &:focus - outline: none diff --git a/src/common/useSettings.ts b/src/common/useSettings.ts deleted file mode 100644 index 5d7b8f1..0000000 --- a/src/common/useSettings.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useEffect, useReducer } from 'preact/hooks' -import { DEFAULT_SETTINGS, ExtensionSettings } from './settings' - -/** - * A hook to read the settings from local storage - * - * @param defaultSettings the default value. Must have all relevant keys present and should not change - */ -function useSettings(defaultSettings: ExtensionSettings) { - const [state, dispatch] = useReducer((state, nstate: Partial) => ({ ...state, ...nstate }), defaultSettings) - const settingsKeys = Object.keys(defaultSettings) - // register change listeners, gets current values, and cleans up the listeners on unload - useEffect(() => { - const changeListener = (changes: Record, areaName: string) => { - if (areaName !== 'local') return - const changeEntries = Object.keys(changes).filter((key) => settingsKeys.includes(key)).map((key) => [key, changes[key].newValue]) - if (changeEntries.length === 0) return // no changes; no use dispatching - dispatch(Object.fromEntries(changeEntries)) - } - - chrome.storage.onChanged.addListener(changeListener) - chrome.storage.local.get(settingsKeys, async (settings) => dispatch(settings)) - - return () => chrome.storage.onChanged.removeListener(changeListener) - }, []) - - return state -} - -/** Utilty to set a setting in the browser */ -export const setSetting = (setting: K, value: ExtensionSettings[K]) => chrome.storage.local.set({ [setting]: value }) - - -/** A hook to read watch on lbry settings from local storage */ -export const useLbrySettings = () => useSettings(DEFAULT_SETTINGS) \ No newline at end of file diff --git a/src/common/yt/auth.ts b/src/common/yt/auth.ts deleted file mode 100644 index 32ea04f..0000000 --- a/src/common/yt/auth.ts +++ /dev/null @@ -1,32 +0,0 @@ -import crypto from 'crypto' - -function generateKeys() { - // The `generateKeyPairSync` method accepts two arguments: - // 1. The type ok keys we want, which in this case is "rsa" - // 2. An object with the properties of the key - const keys = crypto.generateKeyPairSync("rsa", { - // The standard secure default length for RSA keys is 2048 bits - modulusLength: 2048, - }) - - return keys -} - -function encodeKeys(keys: { publicKey: Buffer, privateKey: Buffer }) { - return JSON.stringify({ publicKey: keys.publicKey.toString('base64'), privateKey: keys.privateKey.toString('base64') }) -} - -function decodeKeys(encodedKeys: string) { - const keysBase64 = JSON.parse(encodedKeys) - return { - publicKey: Buffer.from(keysBase64.publicKey), - privateKey: Buffer.from(keysBase64.privateKey) - } -} - -function sign(data: string, privateKey: Buffer) { - return crypto.sign("sha256", Buffer.from(data), { - key: privateKey, - padding: crypto.constants.RSA_PKCS1_PSS_PADDING, - }) -} \ No newline at end of file diff --git a/src/components/Row/index.tsx b/src/components/Row/index.tsx new file mode 100644 index 0000000..5e916a2 --- /dev/null +++ b/src/components/Row/index.tsx @@ -0,0 +1,33 @@ +import { ComponentChildren, h } from 'preact' +import './style.css' + +/* + Re-implementation of https://github.com/DeepDoge/svelte-responsive-row +*/ + +export function Row(params: { + children: ComponentChildren + type?: "fit" | "fill", + idealSize?: string, + gap?: string, + maxColumnCount?: number, + justifyItems?: "center" | "start" | "end" | "stretch" +}) { + if (!params.type) params.type = 'fill' + if (!params.gap) params.gap = '0' + if (!params.idealSize) params.idealSize = '100%' + if (!params.maxColumnCount) params.maxColumnCount = Number.MAX_SAFE_INTEGER + if (!params.justifyItems) params.justifyItems = 'center' + + return
+ {params.children} +
+} diff --git a/src/components/Row/style.css b/src/components/Row/style.css new file mode 100644 index 0000000..940b1d4 --- /dev/null +++ b/src/components/Row/style.css @@ -0,0 +1,6 @@ +.responsive-row { + display: grid; + grid-template-columns: repeat(var(--type), minmax(min(max(100% / var(--max-column-count) - var(--gap), var(--ideal-size)), 100%), 1fr)); + gap: var(--gap); + justify-items: var(--justify-items); +} \ No newline at end of file diff --git a/src/manifest.json b/src/manifest.json index cc31a2f..719cc8b 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -31,26 +31,26 @@ ], "background": { "scripts": [ - "scripts/storageSetup.js", + "settings/background.js", "scripts/background.js" ], "persistent": true }, "browser_action": { "default_title": "Watch on LBRY", - "default_popup": "popup/popup.html" + "default_popup": "pages/popup/index.html" }, "web_accessible_resources": [ - "popup.html", - "tools/YTtoLBRY.html", - "icons/lbry/lbry-logo.svg", - "icons/lbry/odysee-logo.svg", - "icons/lbry/madiator-logo.svg" + "pages/popup/index.html", + "pages/YTtoLBRY/index.html", + "assets/icons/lbry/lbry-logo.svg", + "assets/icons/lbry/odysee-logo.svg", + "assets/icons/lbry/madiator-logo.svg" ], "icons": { - "16": "icons/wol/icon16.png", - "48": "icons/wol/icon48.png", - "128": "icons/wol/icon128.png" + "16": "assets/icons/wol/icon16.png", + "48": "assets/icons/wol/icon48.png", + "128": "assets/icons/wol/icon128.png" }, "manifest_version": 2 -} +} \ No newline at end of file diff --git a/src/common/crypto.ts b/src/modules/crypto/index.ts similarity index 92% rename from src/common/crypto.ts rename to src/modules/crypto/index.ts index 54f7a84..0991102 100644 --- a/src/common/crypto.ts +++ b/src/modules/crypto/index.ts @@ -1,6 +1,5 @@ -import { getExtensionSettingsAsync, ytUrlResolversSettings } from "./settings" -import { setSetting } from "./useSettings" import path from 'path' +import { getExtensionSettingsAsync, setExtensionSetting, ytUrlResolversSettings } from "../../settings" async function generateKeys() { const keys = await window.crypto.subtle.generateKey( @@ -38,7 +37,6 @@ async function exportPublicKey(key: CryptoKey) { key ) const publicKey = Buffer.from(exported).toString('base64') - console.log(publicKey) return publicKey.substring(publicKeyPrefix.length, publicKeyPrefix.length + publicKeyLength) } @@ -64,8 +62,8 @@ export async function sign(data: string, privateKey: string) { } export function resetProfileSettings() { - setSetting('publicKey', null) - setSetting('privateKey', null) + setExtensionSetting('publicKey', null) + setExtensionSetting('privateKey', null) } async function apiRequest(method: 'GET' | 'POST', pathname: string, data: T) { @@ -73,9 +71,7 @@ async function apiRequest(method: 'GET' | 'POST', pathname: st /* const urlResolverSettings = ytUrlResolversSettings[settings.urlResolver] if (!urlResolverSettings.signRequest) throw new Error() */ - console.log(ytUrlResolversSettings) const url = new URL(ytUrlResolversSettings.madiatorFinder.href/* urlResolverSettings.href */) - console.log(url) url.pathname = path.join(url.pathname, pathname) url.searchParams.set('data', JSON.stringify(data)) @@ -113,8 +109,8 @@ export async function generateProfileAndSetNickname(overwrite = false) { publicKey = keys.publicKey privateKey = keys.privateKey }) - setSetting('publicKey', publicKey) - setSetting('privateKey', privateKey) + setExtensionSetting('publicKey', publicKey) + setExtensionSetting('privateKey', privateKey) } await apiRequest('POST', '/profile', { nickname }) alert(`Your nickname has been set to ${nickname}`) @@ -198,8 +194,8 @@ export async function importProfileKeysFromFile() { const json = await readFile() if (!json) throw new Error("Invalid") const { publicKey, privateKey } = JSON.parse(json) as ExportedProfileKeysFile - setSetting('publicKey', publicKey) - setSetting('privateKey', privateKey) + setExtensionSetting('publicKey', publicKey) + setExtensionSetting('privateKey', privateKey) } catch (error: any) { alert(error.message) } diff --git a/src/modules/file/index.ts b/src/modules/file/index.ts new file mode 100644 index 0000000..c1a9558 --- /dev/null +++ b/src/modules/file/index.ts @@ -0,0 +1,15 @@ +/** + * @param file to load + * @returns a promise with the file as a string + */ +export function getFileContent(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.addEventListener('load', event => resolve(event.target?.result as string || '')) + reader.addEventListener('error', () => { + reader.abort() + reject(new DOMException(`Could not read ${file.name}`)) + }) + reader.readAsText(file) + }) +} \ No newline at end of file diff --git a/src/common/yt/index.ts b/src/modules/yt/index.ts similarity index 82% rename from src/common/yt/index.ts rename to src/modules/yt/index.ts index 68184f5..243462e 100644 --- a/src/common/yt/index.ts +++ b/src/modules/yt/index.ts @@ -12,23 +12,6 @@ interface YtExportedJsonSubscription { } -/** - * @param file to load - * @returns a promise with the file as a string - */ -export function getFileContent(file: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader() - reader.addEventListener('load', event => resolve(event.target?.result as string || '')) - reader.addEventListener('error', () => { - reader.abort() - reject(new DOMException(`Could not read ${file.name}`)) - }) - reader.readAsText(file) - }) -} - - /** * Reads the array of YT channels from an OPML file * diff --git a/src/common/yt/urlCache.ts b/src/modules/yt/urlCache.ts similarity index 100% rename from src/common/yt/urlCache.ts rename to src/modules/yt/urlCache.ts diff --git a/src/common/yt/urlResolve.ts b/src/modules/yt/urlResolve.ts similarity index 89% rename from src/common/yt/urlResolve.ts rename to src/modules/yt/urlResolve.ts index 5985bfe..6792237 100644 --- a/src/common/yt/urlResolve.ts +++ b/src/modules/yt/urlResolve.ts @@ -1,8 +1,8 @@ import { chunk } from "lodash" -import { sign } from "../crypto" -import { getExtensionSettingsAsync, ytUrlResolversSettings } from "../settings" -import { LbryPathnameCache } from "./urlCache" import path from "path" +import { getExtensionSettingsAsync, ytUrlResolversSettings } from "../../settings" +import { sign } from "../crypto" +import { LbryPathnameCache } from "./urlCache" const QUERY_CHUNK_SIZE = 100 @@ -11,8 +11,10 @@ type Results = Record type Paramaters = YtUrlResolveItem[] interface ApiResponse { - channels?: Record - videos?: Record + data: { + channels?: Record + videos?: Record + } } export async function resolveById(params: Paramaters, progressCallback?: (progress: number) => void): Promise { @@ -37,7 +39,6 @@ export async function resolveById(params: Paramaters, progressCallback?: (progre // No cache found return item }))).filter((o) => o) as Paramaters - console.log(params) if (params.length === 0) return results @@ -54,9 +55,8 @@ export async function resolveById(params: Paramaters, progressCallback?: (progre const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) if (apiResponse.ok) { const response: ApiResponse = await apiResponse.json() - for (const item of params) - { - const lbryUrl = ((item.type === 'channel' ? response.channels : response.videos) ?? {})[item.id]?.replaceAll('#', ':') ?? null + for (const item of params) { + 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 await LbryPathnameCache.put(lbryUrl, item.id) @@ -76,7 +76,6 @@ export async function resolveById(params: Paramaters, progressCallback?: (progre if (progressCallback) progressCallback(++i / (chunks.length + 1)) Object.assign(results, await requestChunk(chunk)) } - console.log(results) if (progressCallback) progressCallback(1) return results diff --git a/src/tools/README.md b/src/pages/YTtoLBRY/README.md similarity index 96% rename from src/tools/README.md rename to src/pages/YTtoLBRY/README.md index 5653dfb..04a15ce 100644 --- a/src/tools/README.md +++ b/src/pages/YTtoLBRY/README.md @@ -3,4 +3,4 @@ 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. Once it's exported, open the archive and find `YouTube and YouTube Music/subscriptions/subscriptions.json` and upload it to the extension \ No newline at end of file diff --git a/src/pages/YTtoLBRY/index.html b/src/pages/YTtoLBRY/index.html new file mode 100644 index 0000000..6e10046 --- /dev/null +++ b/src/pages/YTtoLBRY/index.html @@ -0,0 +1,17 @@ + + + + + + Subscription Converter + + + + + + + +
+ + + \ No newline at end of file diff --git a/src/pages/YTtoLBRY/main.tsx b/src/pages/YTtoLBRY/main.tsx new file mode 100644 index 0000000..0294ce3 --- /dev/null +++ b/src/pages/YTtoLBRY/main.tsx @@ -0,0 +1,80 @@ +import { h, render } from 'preact' +import { useState } from 'preact/hooks' +import { getFileContent } from '../../modules/file' +import { getSubsFromCsv, getSubsFromJson, getSubsFromOpml } from '../../modules/yt' +import { resolveById } from '../../modules/yt/urlResolve' +import { targetPlatformSettings, useExtensionSettings } from '../../settings' +import readme from './README.md' + +async function getSubscribedChannelIdsFromFile(file: File) { + const ext = file.name.split('.').pop()?.toLowerCase() + const content = await getFileContent(file) + switch (ext) { + case 'xml': + case 'opml': + return getSubsFromOpml(content) + case 'csv': + return getSubsFromCsv(content) + default: + return getSubsFromJson(content) + } +} + +async function findChannels(channelIds: string[], progressCallback: Parameters['1']) { + const resultItems = await resolveById(channelIds.map((channelId) => ({ id: channelId, type: 'channel' })), progressCallback) + return Object.values(resultItems).map((item) => item.id) +} + +function Conversion() { + const [file, setFile] = useState(null as File | null) + const [progress, setProgress] = useState(0) + const [lbryChannelIds, setLbryChannels] = useState([] as Awaited>) + const settings = useExtensionSettings() + + let loading = progress > 0 && progress !== 1 + + return
+
{ + event.preventDefault() + if (file) setLbryChannels(await findChannels(await getSubscribedChannelIdsFromFile(file), (progress) => setProgress(progress))) + }} + > +
+ + setFile(event.currentTarget.files?.length ? event.currentTarget.files[0] : null)} /> +
+
+ +
+
+ { + progress === 1 && +
+ Results: + { + lbryChannelIds.length > 0 + ? lbryChannelIds.map((lbryChannelId) => + ) + : No Result + } +
+ } +
+} + +function YTtoLBRY() { + return
+ +