mirror of
https://github.com/LBRYFoundation/Watch-on-LBRY.git
synced 2025-08-23 17:47:26 +00:00
🔥 New UI and Madiator Finder Features
This commit is contained in:
parent
81f1742289
commit
3ee7e530d6
6 changed files with 403 additions and 83 deletions
|
@ -1,6 +1,6 @@
|
|||
:root {
|
||||
--color-master: #488e77;
|
||||
--color-slave: #458593;
|
||||
--color-master: #499375;
|
||||
--color-slave: #43889d;
|
||||
--color-error: rgb(245, 81, 69);
|
||||
--color-gradient-0: linear-gradient(130deg, var(--color-master), var(--color-slave));
|
||||
--color-gradient-1: linear-gradient(130deg, #ff7a18, #af002d 41.07%, #319197 76.05%);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { getExtensionSettingsAsync } from "./settings"
|
||||
import { setSetting } from "./useSettings"
|
||||
|
||||
export async function generateKeys() {
|
||||
async function generateKeys() {
|
||||
const keys = await window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: "RSASSA-PKCS1-v1_5",
|
||||
|
@ -58,31 +58,154 @@ export async function sign(data: string, privateKey: string) {
|
|||
)).toString('base64')
|
||||
}
|
||||
|
||||
export async function loginAndSetNickname() {
|
||||
const settings = await getExtensionSettingsAsync()
|
||||
export function resetProfileSettings() {
|
||||
setSetting('publicKey', null)
|
||||
setSetting('privateKey', null)
|
||||
}
|
||||
|
||||
let nickname;
|
||||
while (true)
|
||||
{
|
||||
export async function generateProfileAndSetNickname(overwrite = false) {
|
||||
let { publicKey, privateKey } = await getExtensionSettingsAsync()
|
||||
|
||||
let nickname
|
||||
while (true) {
|
||||
nickname = prompt("Pick a nickname")
|
||||
if (nickname) break
|
||||
if (nickname === null) return
|
||||
alert("Invalid nickname")
|
||||
}
|
||||
|
||||
if (!settings.privateKey || !settings.publicKey)
|
||||
if (overwrite || !privateKey || !publicKey) {
|
||||
resetProfileSettings()
|
||||
await generateKeys().then((keys) => {
|
||||
setSetting('publicKey', keys.publicKey)
|
||||
setSetting('privateKey', keys.privateKey)
|
||||
publicKey = keys.publicKey
|
||||
privateKey = keys.privateKey
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
const url = new URL('https://finder.madiator.com/api/v1/profile')
|
||||
url.searchParams.set('data', JSON.stringify({ nickname }))
|
||||
url.searchParams.set('keys', JSON.stringify({
|
||||
signature: await sign(url.searchParams.toString(), settings.privateKey!),
|
||||
publicKey: settings.publicKey
|
||||
signature: await sign(url.searchParams.toString(), privateKey!),
|
||||
publicKey
|
||||
}))
|
||||
const respond = await fetch(url.href, { method: "POST" })
|
||||
if (respond.ok) alert(`Your nickname has been set to ${nickname}`)
|
||||
if (respond.ok) {
|
||||
setSetting('publicKey', publicKey)
|
||||
setSetting('privateKey', privateKey)
|
||||
alert(`Your nickname has been set to ${nickname}`)
|
||||
}
|
||||
else alert((await respond.json()).message)
|
||||
}
|
||||
|
||||
export async function purgeProfile() {
|
||||
try {
|
||||
if (!confirm("This will purge all of your online and offline profile data.\nStill wanna continue?")) return
|
||||
const settings = await getExtensionSettingsAsync()
|
||||
|
||||
if (!settings.privateKey || !settings.publicKey)
|
||||
throw new Error('There is no profile to be purged.')
|
||||
|
||||
const url = new URL('https://finder.madiator.com/api/v1/profile/purge')
|
||||
url.searchParams.set('keys', JSON.stringify({
|
||||
signature: await sign(url.searchParams.toString(), settings.privateKey!),
|
||||
publicKey: settings.publicKey
|
||||
}))
|
||||
const respond = await fetch(url.href, { method: "POST" })
|
||||
if (respond.ok) {
|
||||
resetProfileSettings()
|
||||
alert(`Your profile has been purged`)
|
||||
}
|
||||
else throw new Error((await respond.json()).message)
|
||||
} catch (error: any) {
|
||||
alert(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProfile() {
|
||||
try {
|
||||
const settings = await getExtensionSettingsAsync()
|
||||
|
||||
if (!settings.privateKey || !settings.publicKey)
|
||||
throw new Error('There is no profile.')
|
||||
|
||||
const url = new URL('https://finder.madiator.com/api/v1/profile')
|
||||
url.searchParams.set('data', JSON.stringify({ publicKey: settings.publicKey }))
|
||||
url.searchParams.set('keys', JSON.stringify({
|
||||
signature: await sign(url.searchParams.toString(), settings.privateKey!),
|
||||
publicKey: settings.publicKey
|
||||
}))
|
||||
const respond = await fetch(url.href, { method: "GET" })
|
||||
if (respond.ok) {
|
||||
const profile = await respond.json() as { nickname: string, score: number, publickKey: string }
|
||||
return profile
|
||||
}
|
||||
else throw new Error((await respond.json()).message)
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
export function friendlyPublicKey(publicKey: string | null) {
|
||||
// This is copy paste of Madiator Finder's friendly public key
|
||||
return publicKey?.substring(publicKey.length - 64, publicKey.length - 32)
|
||||
}
|
||||
|
||||
function download(data: string, filename: string, type: string) {
|
||||
const file = new Blob([data], { type: type })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
window.URL.revokeObjectURL(url)
|
||||
})
|
||||
}
|
||||
|
||||
async function readFile() {
|
||||
return await new Promise<string | null>((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.readAsBinaryString(myFile)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
interface ExportedProfileKeysFile {
|
||||
publicKey: string
|
||||
privateKey: string
|
||||
}
|
||||
|
||||
export async function exportProfileKeysAsFile() {
|
||||
const { publicKey, privateKey } = await getExtensionSettingsAsync()
|
||||
|
||||
const json = JSON.stringify({
|
||||
publicKey,
|
||||
privateKey
|
||||
})
|
||||
|
||||
download(json, `watch-on-lbry-profile-export-${friendlyPublicKey(publicKey)}.wol-keys.json`, 'application/json')
|
||||
}
|
||||
|
||||
export async function importProfileKeysFromFile() {
|
||||
try {
|
||||
const json = await readFile()
|
||||
if (!json) throw new Error("Invalid")
|
||||
const { publicKey, privateKey } = JSON.parse(json) as ExportedProfileKeysFile
|
||||
setSetting('publicKey', publicKey)
|
||||
setSetting('privateKey', privateKey)
|
||||
} catch (error: any) {
|
||||
alert(error.message)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import { useEffect, useReducer } from 'preact/hooks'
|
||||
import { generateKeys } from './crypto'
|
||||
import { DEFAULT_SETTINGS, ExtensionSettings } from './settings'
|
||||
|
||||
/**
|
||||
|
@ -7,7 +6,7 @@ import { DEFAULT_SETTINGS, ExtensionSettings } from './settings'
|
|||
*
|
||||
* @param defaultSettings the default value. Must have all relevant keys present and should not change
|
||||
*/
|
||||
export function useSettings(defaultSettings: ExtensionSettings) {
|
||||
function useSettings(defaultSettings: ExtensionSettings) {
|
||||
const [state, dispatch] = useReducer((state, nstate: Partial<ExtensionSettings>) => ({ ...state, ...nstate }), defaultSettings)
|
||||
const settingsKeys = Object.keys(defaultSettings)
|
||||
// register change listeners, gets current values, and cleans up the listeners on unload
|
||||
|
|
|
@ -1,15 +1,55 @@
|
|||
.popup {
|
||||
width: 35em;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
:root {
|
||||
--color-master: #499375;
|
||||
--color-slave: #43889d;
|
||||
--color-error: rgb(245, 81, 69);
|
||||
--color-gradient-0: linear-gradient(130deg, var(--color-master), var(--color-slave));
|
||||
--color-gradient-1: linear-gradient(130deg, #ff7a18, #af002d 41.07%, #319197 76.05%);
|
||||
--color-dark: #0e1117;
|
||||
--color-light: rgb(235, 237, 241);
|
||||
--gradient-animation: gradient-animation 5s linear infinite alternate;
|
||||
}
|
||||
|
||||
:root {
|
||||
letter-spacing: .2ch;
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(to left top, var(--color-master), var(--color-slave));
|
||||
background-attachment: fixed;
|
||||
color: var(--color-light);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(19, 19, 19, 0.75);
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 2em;
|
||||
font-size: 1.75em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header {
|
||||
display: grid;
|
||||
position: sticky;
|
||||
|
@ -19,27 +59,99 @@ header {
|
|||
padding: .5em .2em;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-flex;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
padding: .5em 1em;
|
||||
|
||||
background: var(--color-dark);
|
||||
color: var(--color-light);
|
||||
|
||||
cursor: pointer;
|
||||
border-radius: .5em;
|
||||
}
|
||||
|
||||
.filled {
|
||||
background: var(--color-gradient-0);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
font-weight: bold;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.button.active {
|
||||
background: var(--color-gradient-0);
|
||||
}
|
||||
|
||||
.button.active::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--color-gradient-0);
|
||||
filter: blur(.5em);
|
||||
z-index: -1;
|
||||
border-radius: .5em;
|
||||
}
|
||||
|
||||
.button.disabled {
|
||||
filter: saturate(0);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.go-back {
|
||||
color: currentColor;
|
||||
text-decoration: none;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.popup {
|
||||
width: 35em;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.overlay::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: #0e1117;
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
.profile {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
align-items: center;
|
||||
gap: .25em;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.profile .name {
|
||||
display: inline-block;
|
||||
width: 10em;
|
||||
max-width: 100%;
|
||||
display: inline-flex;
|
||||
gap: .5em;
|
||||
place-items: center;
|
||||
max-width: min(10em, 100%);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: bold;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.profile::before {
|
||||
.profile .name::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 4ch;
|
||||
width: 1ch;
|
||||
aspect-ratio: 1/1;
|
||||
background: var(--color-gradient-0);
|
||||
border-radius: 100000%;
|
||||
|
@ -47,17 +159,14 @@ header {
|
|||
|
||||
main {
|
||||
display: grid;
|
||||
gap: 1em;
|
||||
padding: .25em;
|
||||
}
|
||||
gap: 2em;
|
||||
padding: 1.5em 0.5em;
|
||||
}
|
||||
|
||||
section {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: .75em;
|
||||
background: rgba(19, 19, 19, 0.75);
|
||||
border-radius: 1em;
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
.options {
|
||||
|
@ -66,4 +175,5 @@ section {
|
|||
grid-template-columns: repeat(auto-fit, minmax(3em, 1fr));
|
||||
justify-content: center;
|
||||
gap: .25em;
|
||||
padding: 0 1.5em;
|
||||
}
|
|
@ -1,72 +1,162 @@
|
|||
import { h, render } from 'preact'
|
||||
import { useState } from 'preact/hooks'
|
||||
import '../common/common.css'
|
||||
import { exportProfileKeysAsFile, friendlyPublicKey, generateProfileAndSetNickname, getProfile, importProfileKeysFromFile, purgeProfile, resetProfileSettings } from '../common/crypto'
|
||||
import { getTargetPlatfromSettingsEntiries, getYtUrlResolversSettingsEntiries } from '../common/settings'
|
||||
import { setSetting, useLbrySettings } from '../common/useSettings'
|
||||
import '../common/common.css'
|
||||
import './popup.css'
|
||||
import { LbryPathnameCache } from '../common/yt/urlCache'
|
||||
import { loginAndSetNickname } from '../common/crypto'
|
||||
import './popup.css'
|
||||
|
||||
|
||||
/** Gets all the options for redirect destinations as selection options */
|
||||
const targetPlatforms = getTargetPlatfromSettingsEntiries()
|
||||
|
||||
const ytUrlResolverOptions = getYtUrlResolversSettingsEntiries()
|
||||
|
||||
function WatchOnLbryPopup() {
|
||||
function WatchOnLbryPopup(params: { profile: Awaited<ReturnType<typeof getProfile>> | null }) {
|
||||
const { redirect, targetPlatform, urlResolver, privateKey, publicKey } = useLbrySettings()
|
||||
let [clearingCache, updateClearingCache] = useState(() => false)
|
||||
let [loading, updateLoading] = useState(() => false)
|
||||
let [popupRoute, updateRoute] = useState<string | null>(() => null)
|
||||
|
||||
const nickname = params.profile ? params.profile.nickname ?? 'No Nickname' : '...'
|
||||
|
||||
async function startAsyncOperation<T>(operation: Promise<T>) {
|
||||
try {
|
||||
updateLoading(true)
|
||||
await operation
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
finally {
|
||||
updateLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return <div className='popup'>
|
||||
<header>
|
||||
<div className='profile'>
|
||||
{publicKey
|
||||
? <span className='name'>{publicKey}</span>
|
||||
: <a className='button' onClick={() => loginAndSetNickname()}>Login</a>}
|
||||
? <span><b>Using As:</b> <a onClick={() => updateRoute('profile')} className='name link'>{nickname}</a></span>
|
||||
: <a className='button filled' onClick={() => updateRoute('profile')} href="#profile">Your Profile</a>}
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
<label>Pick a mode !</label>
|
||||
<div className='options'>
|
||||
<a onClick={() => setSetting('redirect', true)} className={`button ${redirect ? 'active' : ''}`}>
|
||||
Redirect
|
||||
</a>
|
||||
<a onClick={() => setSetting('redirect', false)} className={`button ${redirect ? '' : 'active'}`}>
|
||||
Show a button
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<label>Where would you like to redirect ?</label>
|
||||
<div className='options'>
|
||||
{targetPlatforms.map(([name, value]) =>
|
||||
<a onClick={() => setSetting('targetPlatform', name)} className={`button ${targetPlatform === name ? 'active' : ''}`}>
|
||||
{value.displayName}
|
||||
{
|
||||
popupRoute === 'profile' ?
|
||||
publicKey ?
|
||||
<main>
|
||||
<a onClick={() => updateRoute('')} className="go-back link">⇐ Back</a>
|
||||
<section>
|
||||
<label>{nickname}</label>
|
||||
<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>
|
||||
<div className='options'>
|
||||
<a onClick={() => startAsyncOperation(generateProfileAndSetNickname()).then(() => renderPopup())} className={`button active`}>
|
||||
Change Nickname
|
||||
</a>
|
||||
<a onClick={() => 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.") && resetProfileSettings() && renderPopup()} className={`button`}>
|
||||
Forget/Logout
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<label>Backup your account</label>
|
||||
<p>Import and export your unique keypair.</p>
|
||||
<div className='options'>
|
||||
<a onClick={() => exportProfileKeysAsFile()} className={`button active`}>
|
||||
Export
|
||||
</a>
|
||||
<a onClick={() => confirm("This will overwrite your old keypair.\nStill wanna continue?\n\nNOTE: Without keypair you can't purge your data online.\nSo if you wish to purge, please use purging instead.") && startAsyncOperation(importProfileKeysFromFile()).then(() => renderPopup())} className={`button`}>
|
||||
Import
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<label>Purge your profile and data!</label>
|
||||
<p>Purge your profile data online and offline.</p>
|
||||
<div className='options'>
|
||||
<a className="button filled">(╯°□°)╯︵ ┻━┻</a>
|
||||
<a onClick={() => startAsyncOperation(purgeProfile()).then(() => renderPopup())} className={`button`}>
|
||||
Purge Everything!!
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<label>Generate new profile</label>
|
||||
<p>Generate a new keypair.</p>
|
||||
<div className='options'>
|
||||
<a onClick={() => confirm("This will overwrite your old keypair.\nStill wanna continue?\n\nNOTE: Without keypair you can't purge your data online.\nSo if you wish to purge, please use purging instead.") && startAsyncOperation(generateProfileAndSetNickname(true)).then(() => renderPopup())} className={`button`}>
|
||||
Generate New Account
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
:
|
||||
<main>
|
||||
<section>
|
||||
<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>
|
||||
<div className='options'>
|
||||
<a onClick={() => startAsyncOperation(importProfileKeysFromFile()).then(() => renderPopup())} className={`button`}>
|
||||
Import
|
||||
</a>
|
||||
<a onClick={() => startAsyncOperation(generateProfileAndSetNickname()).then(() => renderPopup())} className={`button active`}>
|
||||
Generate
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
:
|
||||
<main>
|
||||
<section>
|
||||
<label>Pick a mode:</label>
|
||||
<div className='options'>
|
||||
<a onClick={() => setSetting('redirect', true)} className={`button ${redirect ? 'active' : ''}`}>
|
||||
Redirect
|
||||
</a>
|
||||
<a onClick={() => setSetting('redirect', false)} className={`button ${redirect ? '' : 'active'}`}>
|
||||
Show a button
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<label>Which platform you would like to redirect?</label>
|
||||
<div className='options'>
|
||||
{targetPlatforms.map(([name, value]) =>
|
||||
<a onClick={() => setSetting('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={() => setSetting('urlResolver', name)} className={`button ${urlResolver === name ? 'active' : ''}`}>
|
||||
{value.name}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<a onClick={() => startAsyncOperation(LbryPathnameCache.clearAll()).then(() => alert("Cleared Cache!"))} className={`button active`}>
|
||||
Clear Resolver Cache
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<label>Which resolver API you want to use ?</label>
|
||||
<div className='options'>
|
||||
{ytUrlResolverOptions.map(([name, value]) =>
|
||||
<a onClick={() => setSetting('urlResolver', name)} className={`button ${urlResolver === name ? 'active' : ''}`}>
|
||||
{value.name}
|
||||
</section>
|
||||
<section>
|
||||
<label>Tools</label>
|
||||
<a target='_blank' href='/tools/YTtoLBRY.html' className={`button filled`}>
|
||||
Subscription Converter
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<a onClick={async () => {
|
||||
updateClearingCache(true)
|
||||
await LbryPathnameCache.clearAll()
|
||||
updateClearingCache(false)
|
||||
alert("Cleared Cache!")
|
||||
}} className={`button active ${clearingCache ? 'disabled' : ''}`}>
|
||||
Clear Resolver Cache
|
||||
</a>
|
||||
</section>
|
||||
</main>
|
||||
</section>
|
||||
</main>
|
||||
}
|
||||
{loading && <div class="overlay">
|
||||
<span>Loading...</span>
|
||||
</div>}
|
||||
</div>
|
||||
}
|
||||
|
||||
render(<WatchOnLbryPopup />, document.getElementById('root')!)
|
||||
function renderPopup() {
|
||||
render(<WatchOnLbryPopup profile={null} />, document.getElementById('root')!)
|
||||
getProfile().then((profile) => render(<WatchOnLbryPopup profile={profile} />, document.getElementById('root')!))
|
||||
}
|
||||
|
||||
renderPopup()
|
|
@ -1,6 +1,4 @@
|
|||
import { generateKeys } from '../common/crypto'
|
||||
import { DEFAULT_SETTINGS, ExtensionSettings, getExtensionSettingsAsync } from '../common/settings'
|
||||
import { setSetting } from '../common/useSettings'
|
||||
|
||||
/** Reset settings to default value and update the browser badge text */
|
||||
async function initSettings() {
|
||||
|
|
Loading…
Add table
Reference in a new issue