mirror of
https://github.com/LBRYFoundation/Watch-on-LBRY.git
synced 2025-08-31 01:11:31 +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": {
|
||||
"@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"
|
||||
}
|
||||
|
|
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 {
|
||||
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
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>
|
||||
<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>
|
||||
|
|
|
@ -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 */
|
||||
// "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'). */
|
||||
|
|
Loading…
Add table
Reference in a new issue