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:
Kevin Raoofi 2020-10-13 04:55:17 -04:00
parent 9f8e521fa6
commit 6e907c91e8
12 changed files with 944 additions and 146 deletions

781
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -21,16 +21,20 @@
"devDependencies": {
"@babel/preset-typescript": "^7.10.4",
"@types/chrome": "0.0.124",
"@types/classnames": "^2.2.10",
"@types/jest": "^26.0.14",
"@types/lodash": "^4.14.162",
"braces": ">=2.3.1",
"classnames": "^2.2.6",
"cpx": "^1.5.0",
"cross-env": "^7.0.2",
"@types/jest": "^26.0.14",
"jest": "^26.5.3",
"lodash": "^4.17.20",
"node-forge": ">=0.10.0",
"node-sass": "^4.14.1",
"npm-run-all": "^4.1.5",
"parcel-bundler": "^1.12.4",
"braces": ">=2.3.1",
"node-forge": ">=0.10.0",
"preact": "^10.5.4",
"typescript": "^4.0.3",
"web-ext": "^5.2.0"
}

View 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

View 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>;
}

View file

@ -1,3 +1,5 @@
import { useEffect, useReducer } from 'preact/hooks'
export interface LbrySettings {
enabled: boolean
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]>> {
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
View 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

View file

@ -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;
}

View file

@ -1,29 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="popup.css" />
<script src="popup.tsx" defer></script>
</head>
<body>
<div class="container">
<label>Enable Redirection:</label>
<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 class="container">
<div id="root" />
</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>
</html>

View file

@ -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
View file

@ -0,0 +1,4 @@
.radio-label
font-size: 1.1rem
margin: 15px auto
display: block

32
src/popup/popup.tsx Normal file
View 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')!);

View file

@ -4,12 +4,14 @@
/* Basic Options */
// "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'. */
"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. */
// "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. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
@ -19,7 +21,7 @@
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "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'. */
// "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'). */