mirror of
https://github.com/LBRYFoundation/Watch-on-LBRY.git
synced 2025-09-01 17:55:16 +00:00
Use preact + sass for popup
* Styles were extracted out from popup.css and into common/style * Preact allows for reusable components and easier dynamic components * Easy transition to react or others while not being overbearing * Component specific style are locally imported and handled by parcel ButtonRadio is particulary nice in that it uses pre-exisitng button styling on radio buttons to make it easy to pick configurable options.
This commit is contained in:
parent
9f8e521fa6
commit
6e907c91e8
12 changed files with 944 additions and 146 deletions
781
package-lock.json
generated
781
package-lock.json
generated
File diff suppressed because it is too large
Load diff
10
package.json
10
package.json
|
@ -21,16 +21,20 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-typescript": "^7.10.4",
|
"@babel/preset-typescript": "^7.10.4",
|
||||||
"@types/chrome": "0.0.124",
|
"@types/chrome": "0.0.124",
|
||||||
|
"@types/classnames": "^2.2.10",
|
||||||
|
"@types/jest": "^26.0.14",
|
||||||
"@types/lodash": "^4.14.162",
|
"@types/lodash": "^4.14.162",
|
||||||
|
"braces": ">=2.3.1",
|
||||||
|
"classnames": "^2.2.6",
|
||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
"cross-env": "^7.0.2",
|
"cross-env": "^7.0.2",
|
||||||
"@types/jest": "^26.0.14",
|
|
||||||
"jest": "^26.5.3",
|
"jest": "^26.5.3",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
|
"node-forge": ">=0.10.0",
|
||||||
|
"node-sass": "^4.14.1",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"parcel-bundler": "^1.12.4",
|
"parcel-bundler": "^1.12.4",
|
||||||
"braces": ">=2.3.1",
|
"preact": "^10.5.4",
|
||||||
"node-forge": ">=0.10.0",
|
|
||||||
"typescript": "^4.0.3",
|
"typescript": "^4.0.3",
|
||||||
"web-ext": "^5.2.0"
|
"web-ext": "^5.2.0"
|
||||||
}
|
}
|
||||||
|
|
16
src/common/components/ButtonRadio.sass
Normal file
16
src/common/components/ButtonRadio.sass
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
@import '../style'
|
||||||
|
|
||||||
|
.ButtonRadio
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
|
||||||
|
.radio-button
|
||||||
|
@extend .button
|
||||||
|
margin: 6px
|
||||||
|
|
||||||
|
.radio-button.checked
|
||||||
|
@extend .button.active
|
||||||
|
|
||||||
|
input[type="radio"]
|
||||||
|
opacity: 0
|
||||||
|
position: absolute
|
31
src/common/components/ButtonRadio.tsx
Normal file
31
src/common/components/ButtonRadio.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { h } from 'preact';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import './ButtonRadio.sass';
|
||||||
|
|
||||||
|
export interface SelectionOption {
|
||||||
|
value: string
|
||||||
|
display: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ButtonRadioProps<T extends string | SelectionOption = string> {
|
||||||
|
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<T extends string | SelectionOption = string>({ name = 'buttonRadio', onChange, options, value }: ButtonRadioProps<T>) {
|
||||||
|
/** If it's a string, return the string, if it's a SelectionOption get the selection option property */
|
||||||
|
return <div className='ButtonRadio'>
|
||||||
|
{options.map(o => ({ o: getAttr(o, 'value'), display: getAttr(o, 'display') })).map(({ o, display }) =>
|
||||||
|
<div key={o} className={classnames('radio-button', { 'checked': value === o })}
|
||||||
|
onClick={() => o !== value && onChange(o)}>
|
||||||
|
<input name={name} value={o} type='radio' checked={value === o} />
|
||||||
|
<label>{display}</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>;
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { useEffect, useReducer } from 'preact/hooks'
|
||||||
|
|
||||||
export interface LbrySettings {
|
export interface LbrySettings {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
redirect: keyof typeof redirectDomains
|
redirect: keyof typeof redirectDomains
|
||||||
|
@ -13,3 +15,31 @@ export const redirectDomains = {
|
||||||
export function getSettingsAsync<K extends Array<keyof LbrySettings>>(...keys: K): Promise<Pick<LbrySettings, K[number]>> {
|
export function getSettingsAsync<K extends Array<keyof LbrySettings>>(...keys: K): Promise<Pick<LbrySettings, K[number]>> {
|
||||||
return new Promise(resolve => chrome.storage.local.get(keys, o => resolve(o as any)));
|
return new Promise(resolve => chrome.storage.local.get(keys, o => resolve(o as any)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook to read the settings from local storage
|
||||||
|
*
|
||||||
|
* @param initial the default value. Must have all relevant keys present and should not change
|
||||||
|
*/
|
||||||
|
export function useSettings<T extends object>(initial: T) {
|
||||||
|
const [state, dispatch] = useReducer((state, nstate: Partial<T>) => ({ ...state, ...nstate }), initial);
|
||||||
|
// register change listeners, gets current values, and cleans up the listeners on unload
|
||||||
|
useEffect(() => {
|
||||||
|
const changeListener = (changes: Record<string, chrome.storage.StorageChange>, areaName: string) => {
|
||||||
|
if (areaName !== 'local') return;
|
||||||
|
const changeSet = Object.keys(changes)
|
||||||
|
.filter(k => Object.keys(initial).includes(k))
|
||||||
|
.map(k => [k, changes[k].newValue]);
|
||||||
|
if (changeSet.length === 0) return; // no changes; no use dispatching
|
||||||
|
dispatch(Object.fromEntries(changeSet));
|
||||||
|
};
|
||||||
|
chrome.storage.onChanged.addListener(changeListener);
|
||||||
|
chrome.storage.local.get(Object.keys(initial), o => dispatch(o as Partial<T>));
|
||||||
|
return () => chrome.storage.onChanged.removeListener(changeListener);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A hook to read watch on lbry settings from local storage */
|
||||||
|
export const useLbrySettings = () => useSettings(DEFAULT_SETTINGS);
|
||||||
|
|
33
src/common/style.sass
Normal file
33
src/common/style.sass
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
$background-color: #191a1c !default
|
||||||
|
$text-color: whitesmoke !default
|
||||||
|
|
||||||
|
$btn-color: #075656 !default
|
||||||
|
$btn-select: teal !default
|
||||||
|
|
||||||
|
body
|
||||||
|
width: 400px
|
||||||
|
text-align: center
|
||||||
|
background-color: $background-color
|
||||||
|
color: $text-color
|
||||||
|
|
||||||
|
.container
|
||||||
|
display: block
|
||||||
|
text-align: center
|
||||||
|
margin: 0 32px
|
||||||
|
margin-bottom: 15px
|
||||||
|
|
||||||
|
.button
|
||||||
|
border-radius: 5px
|
||||||
|
background-color: $btn-color
|
||||||
|
border: 4px solid $btn-color
|
||||||
|
color: $text-color
|
||||||
|
font-size: 0.8rem
|
||||||
|
font-weight: 400
|
||||||
|
padding: 4px 15px
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
&.active
|
||||||
|
border: 4px solid $btn-select
|
||||||
|
|
||||||
|
&:focus
|
||||||
|
outline: none
|
|
@ -1,64 +0,0 @@
|
||||||
body {
|
|
||||||
width: 400px;
|
|
||||||
text-align: center;
|
|
||||||
background-color: #191a1c;
|
|
||||||
color: whitesmoke;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 80%;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container2 {
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 80%;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn1 {
|
|
||||||
border-radius: 5px;
|
|
||||||
background-color: #075656;
|
|
||||||
border: 4px solid #075656;
|
|
||||||
color: whitesmoke;
|
|
||||||
font-weight: 400;
|
|
||||||
padding: 4px 15px;
|
|
||||||
margin-right: 25px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 5px;
|
|
||||||
background-color: #075656;
|
|
||||||
border: 4px solid #075656;
|
|
||||||
color: whitesmoke;
|
|
||||||
font-weight: 400;
|
|
||||||
padding: 4px 15px;
|
|
||||||
margin-right: 25px;
|
|
||||||
}
|
|
||||||
button.active {
|
|
||||||
border: 4px solid teal;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border: 4px solid teal;
|
|
||||||
}
|
|
||||||
button:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
label {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin: 15px auto;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.span {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
background: black;
|
|
||||||
}
|
|
|
@ -1,29 +1,14 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" href="popup.css" />
|
<script src="popup.tsx" defer></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<label>Enable Redirection:</label>
|
<div id="root" />
|
||||||
<div class="enable">
|
|
||||||
<button type="button" class="button" name="enable" value="1" > YES </button>
|
|
||||||
<button type="button" class="button" name="disable" value="0" > NO </button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label> Where would you like to redirect ? </label>
|
|
||||||
<div class="redirect">
|
|
||||||
<button type="button" class="button" name="site" value="lbry.tv" > LBRY.tv </button>
|
|
||||||
<button type="button" class="button" name="app" value="app" > LBRY App </button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<label> Another usefull tools: </label>
|
|
||||||
<button type="button" class="btn1" id="btn1">Subscribtions Converter</button>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<script src="popup.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
|
|
||||||
const enable = document.querySelector('.enable button[name="enable"]');
|
|
||||||
const disable = document.querySelector('.enable button[name="disable"]');
|
|
||||||
|
|
||||||
const lbrySite = document.querySelector('.redirect button[name="site"]');
|
|
||||||
const lbryApp = document.querySelector('.redirect button[name="app"]');
|
|
||||||
|
|
||||||
chrome.storage.local.get(['enabled', 'redirect'], ({ enabled, redirect }) => {
|
|
||||||
|
|
||||||
const currentButton = enabled ? enable : disable;
|
|
||||||
currentButton.classList.add('active');
|
|
||||||
|
|
||||||
const currentRadio = !redirect ? lbrySite : redirect === 'lbry.tv' ? lbrySite : lbryApp;
|
|
||||||
currentRadio.classList.add('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
const checkElementForClass = (elToAdd, elToRemove) => {
|
|
||||||
if(!elToAdd.classList.contains('active')){
|
|
||||||
|
|
||||||
elToAdd.classList.add('active');
|
|
||||||
elToRemove.classList.remove('active');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const attachClick = (selector, handler) =>{
|
|
||||||
document.querySelector(selector).addEventListener('click', (event) => {
|
|
||||||
const element = event.target;
|
|
||||||
const name = event.target.getAttribute('name');
|
|
||||||
const value = event.target.getAttribute('value');
|
|
||||||
typeof handler==='function' ? handler(element, name, value): null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
attachClick('.enable', (element, name, value) => {
|
|
||||||
const parsedValue = !!+value;
|
|
||||||
if(name){
|
|
||||||
checkElementForClass(element, name === 'enable' ? disable : enable );
|
|
||||||
chrome.storage.local.set({ enabled: parsedValue });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
attachClick('.redirect', (element, name, value) => {
|
|
||||||
if(name){
|
|
||||||
checkElementForClass(element, name === 'site' ? lbryApp : lbrySite);
|
|
||||||
chrome.storage.local.set({ redirect: value });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var button = document.getElementById("btn1");
|
|
||||||
button.addEventListener("click", function(){
|
|
||||||
chrome.tabs.create({url:"/tools/YTtoLBRY.html"});
|
|
||||||
});
|
|
4
src/popup/popup.sass
Normal file
4
src/popup/popup.sass
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.radio-label
|
||||||
|
font-size: 1.1rem
|
||||||
|
margin: 15px auto
|
||||||
|
display: block
|
32
src/popup/popup.tsx
Normal file
32
src/popup/popup.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { h, render } from 'preact';
|
||||||
|
|
||||||
|
import ButtonRadio, { SelectionOption } from '../common/components/ButtonRadio';
|
||||||
|
import { redirectDomains, useLbrySettings } from '../common/settings';
|
||||||
|
|
||||||
|
import './popup.sass';
|
||||||
|
|
||||||
|
/** Utilty to set a setting in the browser */
|
||||||
|
const setSetting = (setting: string, value: any) => chrome.storage.local.set({ [setting]: value });
|
||||||
|
|
||||||
|
/** Gets all the options for redirect destinations as selection options */
|
||||||
|
const redirectOptions: SelectionOption[] = Object.entries(redirectDomains)
|
||||||
|
.map(([value, { display }]) => ({ value, display }));
|
||||||
|
|
||||||
|
function WatchOnLbryPopup() {
|
||||||
|
const { enabled, redirect } = useLbrySettings();
|
||||||
|
|
||||||
|
return <div className='container'>
|
||||||
|
<label className='radio-label'>Enable Redirection:</label>
|
||||||
|
<ButtonRadio value={enabled ? 'YES' : 'NO'} options={['YES', 'NO']}
|
||||||
|
onChange={enabled => setSetting('enabled', enabled.toLowerCase() === 'yes')} />
|
||||||
|
<label className='radio-label'>Where would you like to redirect?</label>
|
||||||
|
<ButtonRadio value={redirect as string} options={redirectOptions}
|
||||||
|
onChange={redirect => setSetting('redirect', redirect)} />
|
||||||
|
<label className='radio-label'>Other useful tools:</label>
|
||||||
|
<a href='/tools/YTtoLBRY.html' target='_blank'>
|
||||||
|
<button type='button' className='btn1 button is-primary'>Subscriptions Converter</button>
|
||||||
|
</a>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(<WatchOnLbryPopup />, document.getElementById('root')!);
|
|
@ -4,12 +4,14 @@
|
||||||
|
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
// "incremental": true, /* Enable incremental compilation */
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||||
"lib": ["esnext", "dom"], /* Specify library files to be included in the compilation. */
|
"lib": ["esnext", "dom"], /* Specify library files to be included in the compilation. */
|
||||||
"allowJs": true, /* Allow javascript files to be compiled. */
|
"allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
// "checkJs": true, /* Report errors in .js files. */
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||||
|
"jsxFactory": "h",
|
||||||
|
"jsxFragmentFactory": "Fragment",
|
||||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
"sourceMap": true, /* Generates corresponding '.map' file. */
|
"sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
|
@ -19,7 +21,7 @@
|
||||||
// "composite": true, /* Enable project compilation */
|
// "composite": true, /* Enable project compilation */
|
||||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||||
// "removeComments": true, /* Do not emit comments to output. */
|
// "removeComments": true, /* Do not emit comments to output. */
|
||||||
// "noEmit": true, /* Do not emit outputs. */
|
"noEmit": true, /* Do not emit outputs. */
|
||||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||||
|
|
Loading…
Add table
Reference in a new issue