diff --git a/package-lock.json b/package-lock.json index 86aaa75..a88d951 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4826,6 +4826,91 @@ "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": { "version": "1.1.0", "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=", "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": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -14874,6 +14965,12 @@ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "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": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", diff --git a/package.json b/package.json index 2d74d2c..0db5333 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/**/*.{js,jsx,ts,tsx}\" \"src/**/*.html\"", - "watch:parcel": "parcel watch --no-hmr --no-source-maps \"src/**/*.{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\" \"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": "npm-run-all -l -p build:parcel build:assets", "watch": "npm-run-all -l -p watch:parcel watch:assets", diff --git a/src/assets/styles/common.css b/src/assets/styles/common.css index 0d863d9..bf219fb 100644 --- a/src/assets/styles/common.css +++ b/src/assets/styles/common.css @@ -9,8 +9,8 @@ --gradient-animation: gradient-animation 5s linear infinite alternate; } -:root { - font-size: .95rem; +body { + font-size: .75rem; font-family: Arial, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Open Sans, Helvetica Neue, sans-serif; letter-spacing: .2ch; } @@ -59,13 +59,17 @@ h6 { margin: 0; } +pre { + margin: 0; + white-space: break-spaces; +} + .options { display: grid; width: 100%; grid-template-columns: repeat(auto-fit, minmax(3em, 1fr)); justify-content: center; gap: .25em; - padding: 0 1.5em; } .button { @@ -81,7 +85,8 @@ h6 { border-radius: .5em; border: unset; - font-size: inherit; + font: inherit; + cursor: pointer; } .button.active { diff --git a/src/components/Row/index.tsx b/src/components/Row/index.tsx deleted file mode 100644 index 5e916a2..0000000 --- a/src/components/Row/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 940b1d4..0000000 --- a/src/components/Row/style.css +++ /dev/null @@ -1,6 +0,0 @@ -.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/components/dialogs.tsx b/src/components/dialogs.tsx new file mode 100644 index 0000000..6135e96 --- /dev/null +++ b/src/components/dialogs.tsx @@ -0,0 +1,146 @@ +import { h } from 'preact' +import { useEffect, useRef, useState } from 'preact/hooks' + +type Message = string +type Alert = { type: 'alert' | 'prompt' | 'confirm', message: Message, resolve: (data: string | boolean | null) => void } +export type DialogManager = ReturnType + +export function createDialogManager() { + const [alerts, setAlerts] = useState({} as Record) + const id = crypto.randomUUID() + + function add(alert: Alert) { + setAlerts({ ...alerts, alert }) + } + function remove() { + delete alerts[id] + setAlerts({ ...alerts }) + } + + return { + useAlerts() { return alerts }, + async alert(message: Message) { + return await new Promise((resolve) => add({ + message, type: 'alert', resolve: () => { + resolve() + remove() + } + })) + }, + async prompt(message: Message) { + return await new Promise((resolve) => add({ + message, type: 'prompt', resolve: (data) => { + resolve(data?.toString() ?? null) + remove() + } + })) + }, + async confirm(message: Message) { + return await new Promise((resolve) => add({ + message, type: 'confirm', resolve: (data) => { + resolve(!!data) + remove() + } + })) + } + } +} + +interface DialogElement extends HTMLDivElement { + open: boolean + showModal(): void +} + +export function Dialogs(params: { manager: ReturnType }) { + const alerts = params.manager.useAlerts() + let currentAlert = Object.values(alerts)[0] + if (!currentAlert) return + + const [value, setValue] = useState(null as Parameters[0]) + + let cancelled = false + + const dialog = useRef(null as any as DialogElement) + useEffect(() => { + if (!dialog.current) return + if (!dialog.current.open) dialog.current.showModal() + const onClose = () => currentAlert.resolve(null) + dialog.current.addEventListener('close', onClose) + return dialog.current.removeEventListener('close', onClose) + }) + return + +
{ + event.preventDefault() + currentAlert.resolve(cancelled ? null : currentAlert.type === 'confirm' ? true : value) + }}> +
+
{currentAlert.message}
+ {currentAlert.type === 'prompt' && setValue(event.currentTarget.value)} />} +
+
+ {/* This is here to capture, return key */} + + + {currentAlert.type !== 'alert' && } + +
+
+
+} diff --git a/src/manifest.json b/src/manifest.json index 719cc8b..685e9aa 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -43,6 +43,7 @@ "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" diff --git a/src/modules/crypto/index.ts b/src/modules/crypto/index.ts index 0991102..442e06a 100644 --- a/src/modules/crypto/index.ts +++ b/src/modules/crypto/index.ts @@ -1,5 +1,7 @@ import path from 'path' +import { DialogManager } from '../../components/dialogs' import { getExtensionSettingsAsync, setExtensionSetting, ytUrlResolversSettings } from "../../settings" +import { getFileContent } from '../file' async function generateKeys() { const keys = await window.crypto.subtle.generateKey( @@ -91,15 +93,15 @@ async function apiRequest(method: 'GET' | 'POST', pathname: st throw new Error((await respond.json()).message) } -export async function generateProfileAndSetNickname(overwrite = false) { +export async function generateProfileAndSetNickname(dialogManager: DialogManager, overwrite = false) { let { publicKey, privateKey } = await getExtensionSettingsAsync() let nickname while (true) { - nickname = prompt("Pick a nickname") + nickname = await dialogManager.prompt("Pick a nickname") if (nickname) break if (nickname === null) return - alert("Invalid nickname") + await dialogManager.alert("Invalid nickname") } try { @@ -113,21 +115,21 @@ export async function generateProfileAndSetNickname(overwrite = false) { setExtensionSetting('privateKey', privateKey) } await apiRequest('POST', '/profile', { nickname }) - alert(`Your nickname has been set to ${nickname}`) + await dialogManager.alert(`Your nickname has been set to ${nickname}`) } catch (error: any) { resetProfileSettings() - alert(error.message) + await dialogManager.alert(error.message) } } -export async function purgeProfile() { +export async function purgeProfile(dialogManager: DialogManager) { try { - if (!confirm("This will purge all of your online and offline profile data.\nStill wanna continue?")) return + if (!await dialogManager.confirm("This will purge all of your online and offline profile data.\nStill wanna continue?")) return await apiRequest('POST', '/profile/purge', {}) resetProfileSettings() - alert(`Your profile has been purged`) + await dialogManager.alert(`Your profile has been purged`) } catch (error: any) { - alert(error.message) + await dialogManager.alert(error.message) } } @@ -155,22 +157,13 @@ function download(data: string, filename: string, type: string) { }) } -async function readFile() { - return await new Promise((resolve) => { - const input = document.createElement("input") - input.type = 'file' - input.accept = '.wol-keys.json' - - input.click() - input.addEventListener("change", () => { - if (!input.files?.[0]) return - const myFile = input.files[0] - const reader = new FileReader() - - reader.addEventListener('load', () => resolve(reader.result?.toString() ?? null)) - reader.readAsText(myFile) - }) - }) +// Using callback here because there is no good solution for detecting cancel event +export function inputKeyFile(callback: (file: File | null) => void) { + const input = document.createElement("input") + input.type = 'file' + input.accept = '.wol-keys.json' + input.click() + input.addEventListener("change", () => callback(input.files?.[0] ?? null)) } interface ExportedProfileKeysFile { @@ -189,14 +182,23 @@ export async function exportProfileKeysAsFile() { download(json, `watch-on-lbry-profile-export-${friendlyPublicKey(publicKey)}.wol-keys.json`, 'application/json') } -export async function importProfileKeysFromFile() { +export async function importProfileKeysFromFile(dialogManager: DialogManager, file: File) { try { - const json = await readFile() - if (!json) throw new Error("Invalid") + let settings = await getExtensionSettingsAsync() + if (settings.publicKey && !await dialogManager.confirm( + "This will overwrite your old keypair." + + "\nStill wanna continue?\n\n" + + "NOTE: Without keypair you can't purge your data online.\n" + + "So if you wish to purge, please use purging instead." + )) return false + const json = await getFileContent(file) + if (!json) return false const { publicKey, privateKey } = JSON.parse(json) as ExportedProfileKeysFile setExtensionSetting('publicKey', publicKey) setExtensionSetting('privateKey', privateKey) + return true } catch (error: any) { - alert(error.message) + await dialogManager.alert(error.message) + return false } } \ No newline at end of file diff --git a/src/pages/import/index.html b/src/pages/import/index.html new file mode 100644 index 0000000..685f6e3 --- /dev/null +++ b/src/pages/import/index.html @@ -0,0 +1,14 @@ + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/src/pages/import/main.tsx b/src/pages/import/main.tsx new file mode 100644 index 0000000..7c71e81 --- /dev/null +++ b/src/pages/import/main.tsx @@ -0,0 +1,47 @@ +import { h, render } from 'preact' +import { useState } from 'preact/hooks' +import { createDialogManager, Dialogs } from '../../components/dialogs' +import { importProfileKeysFromFile, inputKeyFile } from '../../modules/crypto' + +function ImportPage() { + const [loading, updateLoading] = useState(() => false) + + async function loads(operation: Promise) { + try { + updateLoading(true) + await operation + } catch (error) { + console.error(error) + } + finally { + updateLoading(false) + } + } + + function importProfile() { + inputKeyFile(async (file) => file && await loads( + importProfileKeysFromFile(dialogManager, file) + .then((success) => success && (location.pathname = '/pages/popup/index.html')) + )) + } + + const dialogManager = createDialogManager() + + return +} + +render(, document.getElementById('root')!) diff --git a/src/pages/import/style.css b/src/pages/import/style.css new file mode 100644 index 0000000..fedf420 --- /dev/null +++ b/src/pages/import/style.css @@ -0,0 +1,35 @@ +header { + display: grid; + gap: .5em; + padding: .75em; + position: sticky; + top: 0; + background: rgba(19, 19, 19, 0.5); + justify-items: center; +} + +main { + display: grid; + gap: 2em; + padding: 1.5em 0.5em; +} + +section { + display: grid; + justify-items: center; + text-align: center; + gap: .75em; +} + +section label { + font-size: 1.75em; + font-weight: bold; + text-align: center; +} + +#popup { + width: 35em; + max-width: 100%; + overflow: hidden; + margin: auto; +} \ No newline at end of file diff --git a/src/pages/popup/main.tsx b/src/pages/popup/main.tsx index 818c7fe..37c7faf 100644 --- a/src/pages/popup/main.tsx +++ b/src/pages/popup/main.tsx @@ -1,6 +1,7 @@ import { h, render } from 'preact' import { useState } from 'preact/hooks' -import { exportProfileKeysAsFile, friendlyPublicKey, generateProfileAndSetNickname, getProfile, importProfileKeysFromFile, purgeProfile, resetProfileSettings } from '../../modules/crypto' +import { createDialogManager, Dialogs } from '../../components/dialogs' +import { exportProfileKeysAsFile, friendlyPublicKey, generateProfileAndSetNickname, getProfile, purgeProfile, resetProfileSettings } from '../../modules/crypto' import { LbryPathnameCache } from '../../modules/yt/urlCache' import { getTargetPlatfromSettingsEntiries, getYtUrlResolversSettingsEntiries, setExtensionSetting, useExtensionSettings } from '../../settings' @@ -12,11 +13,13 @@ const ytUrlResolverOptions = getYtUrlResolversSettingsEntiries() function WatchOnLbryPopup(params: { profile: Awaited> | null }) { const { redirect, targetPlatform, urlResolver, privateKey, publicKey } = useExtensionSettings() let [loading, updateLoading] = useState(() => false) - let [popupRoute, updateRoute] = useState(() => null) + let [route, updateRoute] = useState(() => null) + const dialogManager = createDialogManager() const nickname = params.profile ? params.profile.nickname ?? 'No Nickname' : '...' - async function startAsyncOperation(operation: Promise) { + + async function loads(operation: Promise) { try { updateLoading(true) await operation @@ -28,8 +31,22 @@ function WatchOnLbryPopup(params: { profile: Awaited + 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(',')) + importPopupWindow?.focus() + } + return @@ -85,8 +110,10 @@ function WatchOnLbryPopup(params: { profile: AwaitedPurge your profile and data!

Purge your profile data online and offline.

@@ -95,7 +122,11 @@ function WatchOnLbryPopup(params: { profile: AwaitedGenerate new profile

Generate a new keypair.

@@ -107,10 +138,10 @@ function WatchOnLbryPopup(params: { profile: AwaitedYou don't have a profile.

You can either import keypair for an existing profile or generate a new profile keypair.

@@ -148,13 +179,13 @@ function WatchOnLbryPopup(params: { profile: Awaited )}
- startAsyncOperation(LbryPathnameCache.clearAll()).then(() => alert("Cleared Cache!"))} className={`button active`}> + loads(LbryPathnameCache.clearAll().then(() => dialogManager.alert("Cleared Cache!")))} className={`button active`}> Clear Resolver Cache
- + Subscription Converter
diff --git a/src/pages/popup/style.css b/src/pages/popup/style.css index 8adad0d..bb6f577 100644 --- a/src/pages/popup/style.css +++ b/src/pages/popup/style.css @@ -5,6 +5,7 @@ header { position: sticky; top: 0; background: rgba(19, 19, 19, 0.5); + justify-items: center; } main { @@ -16,17 +17,28 @@ main { section { display: grid; justify-items: center; + text-align: center; gap: .75em; } -section label { +section>label { font-size: 1.75em; font-weight: bold; text-align: center; } +section>.options { + padding: 0 1.5em; +} + #popup { width: 35em; max-width: 100%; overflow: hidden; + margin: auto; +} + +.purge-aaaaaaa { + display: grid; + justify-items: center; } \ No newline at end of file