refactor: make modals like widgets

This commit is contained in:
David Ralph
2021-03-20 12:55:20 +00:00
parent 0c71f0ebef
commit ab7681f3d0
44 changed files with 362 additions and 117 deletions

View File

@@ -0,0 +1,32 @@
import React from 'react';
import Settings from './tabs/Settings';
import Addons from './tabs/Addons';
import Marketplace from './tabs/Marketplace';
import Navigation from './tabs/backend/Tabs';
import './scss/index.scss';
export default function MainModal(props) {
const language = window.language;
return (
<div className='modal'>
<span className='closeModal' onClick={props.modalClose}>&times;</span>
<div>
<Navigation navbar={true}>
<div label={language.modals.main.navbar.settings}>
<Settings/>
</div>
<div label={language.modals.main.navbar.addons}>
<Addons/>
</div>
<div label={language.modals.main.navbar.marketplace}>
<Marketplace/>
</div>
</Navigation>
</div>
</div>
);
}

View File

@@ -0,0 +1,66 @@
import React from 'react';
import ArrowBackIcon from '@material-ui/icons/ArrowBack';
export default function Item(props) {
let warningHTML;
// For some reason it breaks sometimes so we use try/catch
try {
if (props.content.content.data.quote_api) {
warningHTML = (
<div className='productInformation'>
<ul>
<li className='header'>{this.props.language.quote_warning.title}</li>
<li id='updated'>{this.props.language.quote_warning.description}</li>
</ul>
</div>
);
}
} catch (e) {
// ignore
}
// prevent console error
let iconsrc = 'https://external-content.duckduckgo.com/iu/?u=' + props.data.icon;
if (!props.data.icon) {
iconsrc = null;
}
return (
<div id='item'>
<br/>
<ArrowBackIcon className='backArrow' onClick={props.function} />
<br/>
<h1>{props.data.name}</h1>
{props.button}
<br/><br/>
<img alt='product' draggable={false} src={iconsrc} />
<div className='informationContainer'>
<div className='productInformation'>
<h4>{props.language.information}</h4>
<ul>
<br/>
<li className='header'>{props.language.last_updated}</li>
<li>{props.data.updated}</li>
<br/>
<li className='header'>{props.language.version}</li>
<li>{props.data.version}</li>
<br/>
<li className='header'>{props.language.author}</li>
<li>{props.data.author}</li>
</ul>
</div>
<div className='productInformation'>
<ul>
<li className='header'>{props.language.notice.title}</li>
<li id='updated'>{props.language.notice.description}</li>
</ul>
</div>
{warningHTML}
</div>
<br/>
<h1>{props.language.overview}</h1>
<p className='description' dangerouslySetInnerHTML={{__html: props.data.description}}></p>
</div>
);
}

View File

@@ -0,0 +1,21 @@
import React from 'react';
export default function Items(props) {
// todo: add message here
if (props.items.length === 0) {
return null;
}
return (
<div className='items'>
{props.items.map((item) =>
<div className='item' onClick={() => props.toggleFunction(item.name)} key={item.name}>
<img alt='icon' draggable={false} src={'https://external-content.duckduckgo.com/iu/?u=' + item.icon_url} />
<div className='details'>
<h4>{item.display_name ? item.display_name : item.name}</h4>
<p>{item.author}</p>
</div>
</div>)}
</div>
);
}

View File

@@ -0,0 +1,119 @@
import React from 'react';
import LocalMallIcon from '@material-ui/icons/LocalMall';
import Item from '../Item';
import Items from '../Items';
import FileUpload from '../../settings/FileUpload';
import MarketplaceFunctions from '../../../../../modules/helpers/marketplace';
export default class Added extends React.PureComponent {
constructor(...args) {
super(...args);
this.state = {
installed: JSON.parse(localStorage.getItem('installed')),
current_data: {
type: '',
name: '',
content: {}
},
item_data: {
name: 'Name',
author: 'Author',
description: 'Description',
updated: '???',
version: '1.0.0',
icon: ''
},
button: ''
}
this.buttons = {
uninstall: <button className='removeFromMue' onClick={() => this.manage('uninstall')}>Remove</button>,
}
}
toggle(type, type2, data) {
if (type === 'item') {
const installed = JSON.parse(localStorage.getItem('installed'));
const info = installed.find(i => i.name === data);
this.setState({
current_data: {
type: type2,
name: data,
content: info
},
item_data: {
name: info.name,
author: info.author,
description: MarketplaceFunctions.urlParser(info.description.replace(/\n/g, '<br>')),
updated: 'Not Implemented',
version: info.version,
icon: info.screenshot_url
}
});
document.getElementById('item').style.display = 'block';
document.getElementById('marketplace').style.display = 'none';
} else {
document.getElementById('marketplace').style.display = 'block';
document.getElementById('item').style.display = 'none';
}
this.setState({
button: this.buttons.uninstall
});
}
manage(type, input) {
switch (type) {
case 'install':
MarketplaceFunctions.install(input.type, input, true);
break;
case 'uninstall':
MarketplaceFunctions.uninstall(this.state.current_data.name, this.state.current_data.content.type);
break;
default:
break;
}
toast(this.props.toastLanguage[type + 'ed']);
let button = '';
if (type === 'install') {
button = this.buttons.uninstall;
}
this.setState({
button: button,
installed: JSON.parse(localStorage.getItem('installed'))
});
}
render() {
let content = <Items items={this.state.installed} toggleFunction={(input) => this.toggle('item', 'addon', input)} />;
if (this.state.installed.length === 0) {
content = (
<div className='items'>
<div className='emptyMessage'>
<LocalMallIcon/>
<h1>Empty</h1>
<p className='description'>Nothing here (yet)</p>
<button className='goToMarket'>Take me there</button>
</div>
</div>
);
}
return (
<React.Fragment>
<div id='marketplace'>
<FileUpload id='file-input' type='settings' accept='application/json' loadFunction={(e) => this.manage('install', JSON.parse(e.target.result))} />
<button className='addToMue sideload' onClick={() => document.getElementById('file-input').click()}>Sideload</button>
{content}
</div>
</React.Fragment>
);
}
}

View File

@@ -0,0 +1,175 @@
import React from 'react';
import WifiOffIcon from '@material-ui/icons/WifiOff';
import ArrowBackIcon from '@material-ui/icons/ArrowBack';
import Item from '../Item';
import Items from '../Items';
import MarketplaceFunctions from '../../../../../modules/helpers/marketplace';
import { toast } from 'react-toastify';
export default class Marketplace extends React.PureComponent {
constructor(...args) {
super(...args);
this.state = {
items: [],
current_data: {
type: '',
name: '',
content: {}
},
button: '',
featured: {},
done: false,
item_data: {
name: 'Name',
author: 'Author',
description: 'Description',
updated: '???',
version: '1.0.0',
icon: ''
}
}
this.buttons = {
uninstall: <button className='removeFromMue' onClick={() => this.manage('uninstall')}>Remove</button>,
install: <button className='addToMue' onClick={() => this.manage('install')}>Add to Mue</button>
}
}
async toggle(type, data) {
switch (type) {
case 'item':
let info;
// get item info
try {
info = await (await fetch(`${window.window.constants.MARKETPLACE_URL}/item/${this.props.type}/${data}`)).json();
} catch (e) {
return toast(this.props.toastLanguage.error);
}
// check if already installed
let button = this.buttons.install;
const installed = JSON.parse(localStorage.getItem('installed'));
if (installed.some(item => item.name === data)) {
button = this.buttons.uninstall;
}
this.setState({
current_data: { type: this.props.type, name: data, content: info },
item_data: {
name: info.data.name,
author: info.data.author,
description: MarketplaceFunctions.urlParser(info.data.description.replace(/\n/g, '<br>')),
updated: info.updated,
version: info.data.version,
icon: info.data.screenshot_url
},
button: button
});
document.getElementById('marketplace').style.display = 'none';
document.getElementById('item').style.display = 'block';
break;
default:
document.getElementById('marketplace').style.display = 'block';
document.getElementById('item').style.display = 'none';
break;
}
}
async getItems() {
const { data } = await (await fetch(window.window.constants.MARKETPLACE_URL + '/all')).json();
const featured = await (await fetch(window.window.constants.MARKETPLACE_URL + '/featured')).json();
this.setState({
items: data[this.props.type],
featured: featured.data,
done: true
});
}
manage(type) {
switch (type) {
case 'install':
MarketplaceFunctions.install(this.state.current_data.type, this.state.current_data.content.data);
break;
case 'uninstall':
MarketplaceFunctions.uninstall(this.state.current_data.content.data.name, this.state.current_data.type);
break;
default:
break;
}
toast(this.props.toastLanguage[type + 'ed']);
this.setState({
button: (type === 'install') ? this.buttons.uninstall : this.buttons.install
});
}
componentDidMount() {
if (localStorage.getItem('animations') === 'true') {
document.getElementById('marketplace').classList.add('marketplaceanimation');
}
if (navigator.onLine === false || localStorage.getItem('offlineMode') === 'true') {
return;
}
this.getItems();
}
render() {
const errorMessage = (msg) => {
return (
<div id='marketplace'>
<div className='emptyMessage' style={{ 'marginTop': '20px', 'transform': 'translateY(80%)' }}>
{msg}
</div>
</div>
);
}
if (navigator.onLine === false) {
return errorMessage(
<React.Fragment>
<WifiOffIcon/>
<h1>Offline</h1>
<p className='description'>Mue down!</p>
</React.Fragment>
);
}
if (localStorage.getItem('offlineMode') === 'true') {
return errorMessage(
<React.Fragment>
<WifiOffIcon/>
<h1>Offline mode is enabled</h1>
<p className='description'>Please turn off offline mode to access the marketplace</p>
</React.Fragment>
);
}
if (this.state.done === false) {
return errorMessage(<h1>Loading</h1>);
}
return (
<React.Fragment>
<div id='marketplace'>
<div className='featured' style={{ 'backgroundColor': this.state.featured.colour }}>
<p>{this.state.featured.title}</p>
<h1>{this.state.featured.name}</h1>
<button className='addToMue' onClick={() => window.location.href = this.state.featured.buttonLink}>{this.state.featured.buttonText}</button>
</div>
<Items
items={this.state.items.slice(0, 3)}
toggleFunction={(input) => this.toggle('item', input)} />
</div>
</React.Fragment>
);
}
}

View File

@@ -0,0 +1,340 @@
@import '../../../../scss/variables';
@import '../../../../scss/mixins';
@import 'settings/main';
@import 'settings/buttons';
@import 'settings/dropdown';
.Modal {
color: map-get($modal, 'text');
background-color: map-get($modal, 'background');
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
border: none;
opacity: 1;
z-index: -2;
padding: 25px;
transition: 0.6s;
transition-timing-function: ease-in;
border-radius: map-get($modal, 'border-radius');
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
scrollbar-width: thin;
scrollbar-color: #34495e #bdc3c7;
position: relative;
&:focus {
outline: 0;
}
}
.modalLink {
color: #5352ed;
cursor: pointer;
padding-left: 10px;
&:hover {
opacity: 0.8;
}
}
.closeModal {
position: absolute;
top: 1rem;
right: 2rem;
font-size: 4em;
cursor: pointer;
&:hover {
color: grey;
}
}
.dark {
background-color: #2f3542 !important;
color: white !important;
.sidebar {
background-color: #2a303b !important;
}
.tab-list-active {
background: rgba(161, 159, 159, 0.8);
}
.tab-list-item {
&:hover {
background: rgba(161, 159, 159, 0.8);
}
}
}
.ReactModal__Html--open,
.ReactModal__Body--open {
overflow: hidden;
}
.Overlay {
position: fixed;
z-index: 100;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100vw;
height: 100vh;
display: flex;
align-items: baseline;
justify-content: center;
}
.ReactModal__Content {
min-height: calc(100vh - 30vh);
max-height: calc(100vh - 10vh);
box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.25);
overflow-y: auto;
position: relative;
}
@media only screen and (max-height: 700px) {
.ReactModal__Content {
min-height: 500px;
max-height: calc(100vh - 30vh);
}
}
@media only screen and (min-height: 700px) {
.ReactModal__Content {
min-height: 600px;
}
}
.ReactModal__Overlay {
opacity: 0;
transform: scale(0);
transition: all 300ms cubic-bezier(.47, 1.64, .41, .8);
}
.ReactModal__Overlay--after-open {
opacity: 1;
transform: scale(1);
}
.ReactModal__Overlay--before-close {
opacity: 0;
transform: scale(0);
}
ul.sidebar {
position: absolute;
top: 0;
left: 0;
margin: 0;
padding-left: 0;
height: 100vh;
background: #f0f0f0;
left: 0;
border-radius: 12px 0 0 12px;
text-align: left;
font-size: 24px;
h1 {
text-align: center;
}
svg,
li {
font-size: 30px;
}
svg {
vertical-align: middle;
font-size: 32px;
padding: 5px;
}
hr {
height: 3px;
background: rgba(196, 196, 196, 0.74);
width: 75%;
outline: none;
border: none;
}
}
li {
list-style: none;
font-size: 24px;
padding: 5px 30px 5px 30px;
cursor: pointer;
margin-top: 2px;
}
#modal {
position: absolute;
margin: auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 60%;
height: 80%;
}
.tab-list-active {
background: rgba(219, 219, 219, 0.72);
}
@media only screen and (max-height: 898px) {
ul.sidebar {
height: auto;
}
}
@media only screen and (max-width: 1200px) {
li.tab-list-item {
span {
display: none;
}
}
}
@media only screen and (max-width: 1200px) {
ul.sidebar {
h1 {
display: none;
}
}
}
.tab-list-item {
&:hover {
background: rgba(219, 219, 219, 0.8);
}
}
.tab-content {
position: absolute;
left: 25%;
h3 {
text-transform: uppercase;
}
}
@media only screen and (max-width: 1920px) {
.tab-content {
position: absolute;
left: 30%;
top: 15%;
}
}
@media only screen and (min-width: 1920px) {
.tab-content {
position: absolute;
left: 30%;
top: 7%;
}
}
@media only screen and (max-width: 1400px) {
.tab-content {
position: absolute;
left: 45%;
}
}
.navbar-item {
font-size: 22px;
font-weight: 500;
display: inline-flex;
}
.modalNavbar {
position: absolute;
left: 20rem;
top: 1rem;
justify-content: center;
svg {
margin-right: 0.5rem;
padding: 3px;
}
}
@media only screen and (max-width: 1200px) {
.modalNavbar {
left: 6rem;
}
}
.navbar-item {
span,
svg {
font-size: 1.1em !important;
}
&:hover {
background: none;
}
}
@media only screen and (max-width: 1200px) {
li.navbar-item {
span {
display: none;
}
}
}
@media only screen and (max-width: 1200px) {
.tabContent {
left: 20px;
}
}
@media only screen and (min-width: 1200px) {
ul.sidebar {
width: 310px;
align-items: center;
}
}
.navbar-item-active {
background: map-get($theme-colours, 'gradient');
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
svg {
color: orange;
}
&:hover {
opacity: .8;
background: map-get($theme-colours, 'gradient');
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
}
::-webkit-scrollbar {
width: 6px;
border-top-right-radius: map-get($modal, 'border-radius');
border-bottom-right-radius: map-get($modal, 'border-radius');
}
::-webkit-scrollbar-thumb {
background: #636e72;
border-top-right-radius: map-get($modal, 'border-radius');
border-bottom-right-radius: map-get($modal, 'border-radius');
}
.abouticon {
width: 96px;
height: auto;
border-radius: 50%;
padding-right: 5px;
}

View File

@@ -0,0 +1,205 @@
%settingsButton {
transition: ease 0.33s;
color: map-get($theme-colours, 'main');
cursor: pointer;
padding: 10px 30px;
font-size: 20px;
border-radius: 24px;
box-shadow: 0 5px 15px rgba(128, 161, 144, 0.4);
&:hover,
&:active {
outline: none;
background: none;
}
}
.dark %settingsButton {
box-shadow: none;
}
.refresh {
@extend %settingsButton;
background-color: map-get($button-colours, 'confirm');
border: 2px solid map-get($button-colours, 'confirm');
&:hover {
border: 2px solid map-get($button-colours, 'confirm');
color: map-get($button-colours, 'confirm');
}
}
.apply {
@extend %settingsButton;
margin-right: 20px;
background-color: map-get($button-colours, 'confirm');
border: 2px solid map-get($button-colours, 'confirm');
&:hover {
border: 2px solid map-get($button-colours, 'confirm');
color: map-get($button-colours, 'confirm');
}
}
.reset {
@extend %settingsButton;
margin-left: 5px;
background-color: map-get($button-colours, 'reset');
border: 2px solid map-get($button-colours, 'reset');
&:hover {
border: 2px solid map-get($button-colours, 'reset');
color: map-get($button-colours, 'reset');
}
}
.close {
@extend %settingsButton;
padding: 10px 50px 10px 50px;
background-color: map-get($button-colours, 'other');
border: 2px solid map-get($button-colours, 'other');
&:hover {
color: map-get($button-colours, 'other');
border: 2px solid map-get($button-colours, 'other');
}
}
.add {
@extend %settingsButton;
background-color: map-get($button-colours, 'other');
border: 2px solid map-get($button-colours, 'other');
&:hover {
color: map-get($button-colours, 'other');
border: 2px solid map-get($button-colours, 'other');
}
}
.export,
.uploadbg,
.import {
@extend %settingsButton;
background-color: map-get($button-colours, 'other');
border: 2px solid map-get($button-colours, 'other');
color: map-get($theme-colours, 'main');
&:hover {
color: map-get($button-colours, 'other');
}
}
.export,
.import {
margin-left: 20px;
}
%storebutton {
cursor: pointer;
font-size: 18px;
float: right;
padding: 5px 30px;
background: none;
border-radius: 24px;
transition: ease 0.33s;
border: 2px solid black;
&:hover {
background: #2d3436;
color: map-get($theme-colours, 'main');
;
}
}
.dark %storebutton {
border: 2px solid map-get($theme-colours, 'main');
color: map-get($theme-colours, 'main');
&:hover {
background: map-get($theme-colours, 'main');
color: #2d3436;
}
}
.removeFromMue {
@extend %storebutton;
border: 2px solid #ff4757;
color: #ff4757;
&:hover {
background: #ff4757;
color: map-get($theme-colours, 'main');
;
}
}
#item .addToMue,
#item .removeFromMue {
margin-top: 5px;
}
.addToMue {
@extend %storebutton;
}
.goToMarket {
float: none;
@extend %storebutton;
}
.sideload,
.seemore {
margin-top: 12px;
}
.stop {
outline: none;
border-image-slice: 1;
border-image-source: map-get($theme-colours, 'gradient');
font-size: 1.2em;
padding-left: 7px;
vertical-align: middle;
}
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
opacity: 1;
outline: none;
}
.pinNote {
@extend %settingsButton;
background-color: map-get($button-colours, 'confirm');
border: 2px solid map-get($button-colours, 'confirm');
color: map-get($theme-colours, 'main');
transition: 0s;
&:hover {
color: map-get($button-colours, 'confirm');
}
svg {
fill: currentColor;
width: 1em;
height: 1em;
display: inline-block;
font-size: 1.5rem;
transition: fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
flex-shrink: 0;
user-select: none;
}
}
.saveNote {
@extend %settingsButton;
background-color: map-get($button-colours, 'other');
border: 2px solid map-get($button-colours, 'other');
color: map-get($theme-colours, 'main');
transition: 0s;
display: inline;
margin: 5px;
&:hover {
color: map-get($button-colours, 'other');
}
}

View File

@@ -0,0 +1,40 @@
.dropdown {
select {
margin-left: 10px;
width: 120px;
color: map-get($modal, 'text');
background: #f0f0f0;
border: none;
padding: 10px 10px;
border-radius: 5px;
}
select#language {
float: right;
}
}
// firefox dropdown
@supports (-moz-appearance:none) {
select {
-moz-appearance: none !important;
background: url('data:image/gif;base64,R0lGODlhBgAGAKEDAFVVVX9/f9TU1CgmNyH5BAEKAAMALAAAAAAGAAYAAAIODA4hCDKWxlhNvmCnGwUAOw==') right center no-repeat, linear-gradient(180deg, #ffb032 0%, #dd3b67 100%) !important;
background-position: calc(100% - 5px) center !important;
}
}
.react-date-picker__wrapper {
padding: 0.5rem 1rem !important;
box-sizing: border-box !important;
border-image-slice: 1 !important;
border-image-source: linear-gradient(90deg, #ffb032 0%, #dd3b67 100%) !important;
background: transparent !important;
color: #fff !important;
}
.react-date-picker__clear-button,
.react-calendar__navigation__prev2-button,
.react-calendar__navigation__next2-button {
display: none !important;
}

View File

@@ -0,0 +1,198 @@
.hidden {
display: none;
}
input {
&[type=text] {
width: 200px;
padding: 0.5rem 1rem;
box-sizing: border-box;
border-image-slice: 1;
border-image-source: map-get($theme-colours, 'gradient');
background: transparent;
}
&:checked+.slider {
background: map-get($theme-colours, 'gradient');
&:before {
-webkit-transform: translateX(26px);
transform: translateX(26px);
}
}
&:focus+.slider {
box-shadow: 0 0 1px #e67e22;
}
}
h4,
.switch {
display: inline;
font-size: 1.4em;
font-weight: 100;
}
h4,
#engines {
display: inline;
}
.section {
margin-bottom: 20px;
}
h4 {
cursor: pointer;
}
ul {
padding-left: 5px;
margin: 0;
>label {
vertical-align: middle;
}
}
li {
margin-top: 1px;
}
.range {
-webkit-appearance: none;
width: 200px;
height: 15px;
border-radius: 12px;
outline: none;
background: #ecf0f1;
border-radius: 12px;
box-shadow: 0 0 100px rgba(0, 0, 0, 0.3);
&::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 25px;
height: 25px;
border-radius: 12px;
background: map-get($theme-colours, 'gradient');
cursor: pointer;
}
&::-moz-range-thumb {
width: 25px;
height: 25px;
border-radius: 12px;
border: 0;
background: map-get($theme-colours, 'gradient');
cursor: pointer;
}
}
input[type=color] {
border-radius: 100%;
height: 30px;
width: 30px;
box-shadow: map-get($main-parts, 'shadow');
border: none;
outline: none;
-webkit-appearance: none;
vertical-align: middle;
&::-webkit-color-swatch-wrapper {
padding: 0;
}
&::-webkit-color-swatch {
border: none;
border-radius: 100%;
}
}
input[type=color]::-moz-color-swatch {
border-radius: 100%;
height: 30px;
width: 30px;
box-shadow: map-get($main-parts, 'shadow');
border: none;
outline: none;
-webkit-appearance: none;
vertical-align: middle;
&::-moz-color-swatch-wrapper {
padding: 0;
}
&::-moz-color-swatch {
border: none;
border-radius: 100%;
}
}
.customBackgroundHex {
font-size: 1.2em;
padding-left: 7px;
vertical-align: middle;
}
// Dark Theme
.dark {
#blurRange,
#brightnessRange {
background-color: #535c68;
}
#customBackground,
#backgroundAPI,
#searchEngine,
#language,
#greetingName,
#customSearchEngine {
color: white;
}
.choices {
background-color: white;
}
.modalLink {
color: #3498db;
}
}
.nodropdown {
cursor: default;
}
.newFeature {
color: #ff4757;
font-size: 12px;
}
#customcss,
#customjs {
font-family: Consolas !important;
border: solid 1px black !important;
margin-left: 0px;
}
.MuiCheckbox-colorPrimary.Mui-checked {
color: map-get($button-colours, 'reset') !important;
}
.reminder-info {
position: absolute;
bottom: 20px;
right: 20px;
padding: 20px;
color: #000;
background: #f0f0f0;
max-width: 300px;
border-radius: 0.7em;
display: none;
h1 {
font-size: 1em;
}
}

View File

@@ -0,0 +1,43 @@
import React from 'react';
import SettingsFunctions from '../../../../modules/helpers/settings';
import CheckboxUI from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel';
export default class Checkbox extends React.PureComponent {
constructor(...args) {
super(...args);
this.state = {
checked: (localStorage.getItem(this.props.name) === 'true')
};
}
handleChange(name) {
SettingsFunctions.setItem(name);
this.setState({
checked: (this.state.checked === true) ? false : true
});
}
render() {
let text = this.props.text;
if (this.props.newFeature) {
text = <span>{this.props.text} <span className='newFeature'> NEW</span></span>;
} else if (this.props.betaFeature) {
text = <span>{this.props.text} <span className='newFeature'> BETA</span></span>;
}
return (
<React.Fragment>
<FormControlLabel
control={<CheckboxUI name='checkedB' color='primary' checked={this.state.checked} onChange={() => this.handleChange(this.props.name)} />}
label={text}
/>
<br/>
</React.Fragment>
);
}
}

View File

@@ -0,0 +1,31 @@
import React from 'react';
export default class Dropdown extends React.PureComponent {
getLabel() {
return this.props.label ? <label htmlFor={this.props.name}>{this.props.label}</label> : null;
}
componentDidMount() {
document.getElementById(this.props.name).value = localStorage.getItem(this.props.name);
}
onChange = () => {
localStorage.setItem(this.props.name, document.getElementById(this.props.name).value);
if (this.props.onChange) {
this.props.onChange();
}
}
render() {
return (
<React.Fragment>
{this.getLabel()}
<div className='dropdown' style={{ display: 'inline' }}>
<select name={this.props.name} id={this.props.name} onChange={this.onChange}>
{this.props.children}
</select>
</div>
</React.Fragment>
);
}
}

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { toast } from 'react-toastify';
export default class FileUpload extends React.PureComponent {
componentDidMount() {
document.getElementById(this.props.id).onchange = (e) => {
const reader = new FileReader();
const file = e.target.files[0];
if (this.props.type === 'settings') {
reader.readAsText(file, 'UTF-8');
} else {
// background upload
if (file.size > 2000000) {
return toast('File is over 2MB');
}
reader.readAsDataURL(file);
}
reader.addEventListener('load', (e) => this.props.loadFunction(e));
};
}
render() {
return <input id={this.props.id} type='file' className='hidden' accept={this.props.accept} />;
}
}

View File

@@ -0,0 +1,14 @@
import React from 'react';
export default function ResetModal(props) {
return (
<div className='welcomeContent'>
<span className='closeModal' onClick={props.modalClose}>&times;</span>
<div className='welcomeModalText'>
reset text
<button className='close' onClick={props.modalClose}>yes</button>
<button className='close' onClick={props.modalClose}>no</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,83 @@
import React from 'react';
import Tooltip from '@material-ui/core/Tooltip';
const other_contributors = require('../../../../../modules/other_contributors.json');
const { version } = require('../../../../../../package.json');
export default class About extends React.PureComponent {
constructor(...args) {
super(...args);
this.state = {
contributors: [],
sponsors: [],
other_contributors: [],
update: window.language.modals.main.settings.sections.about.version.checking_update
}
this.language = window.language.modals.main.settings.sections.about;
}
async getGitHubData() {
const contributors = await (await fetch('https://api.github.com/repos/mue/mue/contributors')).json();
const { sponsors } = await (await fetch(window.constants.SPONSORS_URL + '/list')).json();
const versionData = await (await fetch('https://api.github.com/repos/mue/mue/releases')).json();
const newVersion = versionData[0].tag_name;
let updateMsg = this.language.version.no_update;
if (version < newVersion) {
updateMsg = `${this.language.version.update_available}: ${newVersion}`;
}
this.setState({
contributors: contributors.filter((contributor) => !contributor.login.includes('bot')),
sponsors: sponsors,
update: updateMsg,
other_contributors: other_contributors
});
}
componentDidMount() {
if (localStorage.getItem('offlineMode') === 'true') {
this.setState({
update: this.language.version.offline_mode
});
return;
}
this.getGitHubData();
}
render() {
return (
<div>
<h2>{this.language.title}</h2>
<img draggable='false' style={{'height': '100px', 'width': 'auto'}} src='https://raw.githubusercontent.com/mue/branding/master/logo/logo_horizontal.png' alt='Mue logo'></img>
<p>{this.language.copyright} 2018-{new Date().getFullYear()} Mue Tab (BSD-3 License)</p>
<p>{this.language.version.title} {version} ({this.state.update})</p>
<h3>{this.language.resources_used.title}</h3>
<p>Pexels ({this.language.resources_used.bg_images})</p>
<p>Google ({this.language.resources_used.pin_icon})</p>
<p>Undraw ({this.language.resources_used.welcome_img})</p>
<h3>{this.language.contributors}</h3>
{this.state.contributors.map((item) =>
<Tooltip title={item.login} placement='top' key={item.login}>
<a href={'https://github.com/' + item.login} target='_blank' rel='noopener noreferrer'><img draggable='false' className='abouticon' src={item.avatar_url + '&size=256'} alt={item.login}></img></a>
</Tooltip>
)}
{ // for those who contributed without opening a pull request
this.state.other_contributors.map((item) =>
<Tooltip title={item.login} placement='top' key={item.login}>
<a href={'https://github.com/' + item.login} target='_blank' rel='noopener noreferrer'><img draggable='false' className='abouticon' src={item.avatar_url + '&size=256'} alt={item.login}></img></a>
</Tooltip>
)}
<h3>{this.language.supporters}</h3>
{this.state.sponsors.map((item) =>
<Tooltip title={item.handle} placement='top' key={item.handle}>
<a href={item.profile} target='_blank' rel='noopener noreferrer'><img draggable='false' className='abouticon' src={item.avatar + '&size=256'} alt={item.handle}></img></a>
</Tooltip>
)}
</div>
);
}
}

View File

@@ -0,0 +1,80 @@
import React from 'react';
import Checkbox from '../Checkbox';
import FileUpload from '../FileUpload';
import ResetModal from '../ResetModal';
import SettingsFunctions from '../../../../../modules/helpers/settings';
import { toast } from 'react-toastify';
import Modal from 'react-modal';
export default class AdvancedSettings extends React.PureComponent {
constructor(...args) {
super(...args);
this.state = {
resetModal: false
};
this.language = window.language.modals.main.settings;
}
resetItem(type) {
document.getElementById(type).value = '';
toast(this.language.toasts.reset);
}
settingsImport(e) {
const content = JSON.parse(e.target.result);
for (const key of Object.keys(content)) {
localStorage.setItem(key, content[key]);
}
toast(this.language.toasts.imported);
}
componentDidMount() {
document.getElementById('customcss').value = localStorage.getItem('customcss');
document.getElementById('customjs').value = localStorage.getItem('customjs');
}
componentDidUpdate() {
localStorage.setItem('customcss', document.getElementById('customcss').value);
localStorage.setItem('customjs', document.getElementById('customjs').value);
}
render() {
const { advanced } = this.language.sections;
return (
<div>
<h2>{advanced.title}</h2>
<Checkbox name='offlineMode' text={advanced.offline_mode} />
<h3>{advanced.data}</h3>
<button className='reset' onClick={() => this.setState({ resetModal: true })}>{this.language.buttons.reset}</button>
<button className='export' onClick={() => SettingsFunctions.exportSettings()}>{this.language.buttons.export}</button>
<button className='import' onClick={() => document.getElementById('file-input').click()}>{this.language.buttons.import}</button>
<FileUpload id='file-input' accept='application/json' type='settings' loadFunction={(e) => this.settingsImport(e)} />
<h3>{advanced.customisation}</h3>
<ul>
<p>{advanced.custom_css} <span className='modalLink' onClick={() => this.resetItem('customcss')}>{this.language.buttons.reset}</span></p>
<textarea id='customcss'></textarea>
</ul>
<ul>
<p>{advanced.custom_js} <span className='modalLink' onClick={() => this.resetItem('customjs')}>{this.language.buttons.reset}</span></p>
<textarea id='customjs'></textarea>
</ul>
<h3>{this.language.sections.experimental.title}</h3>
<p>{advanced.experimental_warning}</p>
<Checkbox name='experimental' text={this.language.enabled} />
<Modal onRequestClose={() => this.setState({ resetModal: false })} isOpen={this.state.resetModal} className={'modal'} overlayClassName={'Overlay'} ariaHideApp={false}>
<ResetModal modalClose={() => this.setState({ resetModal: false })} />
</Modal>
</div>
);
}
}

View File

@@ -0,0 +1,115 @@
import React from 'react';
import Checkbox from '../Checkbox';
import Dropdown from '../Dropdown';
import { toast } from 'react-toastify';
export default class AppearanceSettings extends React.PureComponent {
constructor(...args) {
super(...args);
this.state = {
zoom: localStorage.getItem('zoom'),
toast_duration: localStorage.getItem('toastDisplayTime'),
font: localStorage.getItem('font') || ''
};
this.language = window.language.modals.main.settings;
}
resetItem(key) {
switch (key) {
case 'zoom':
localStorage.setItem('zoom', 100);
this.setState({
zoom: 100
});
break;
case 'toast_duration':
localStorage.setItem('toastDisplayTime', 2500);
this.setState({
toast_duration: 2500
});
break;
case 'font':
localStorage.setItem('font', '');
this.setState({
font: ''
});
break;
default:
toast('resetItem requires a key!');
}
toast(this.language.toasts.reset);
}
componentDidUpdate() {
localStorage.setItem('zoom', this.state.zoom);
localStorage.setItem('toastDisplayTime', this.state.toast_duration);
localStorage.setItem('font', this.state.font.charAt(0).toUpperCase() + this.state.font.slice(1));
}
render() {
const { appearance } = this.language.sections;
return (
<div>
<h2>{appearance.title}</h2>
<Checkbox name='darkTheme' text={appearance.dark_theme} />
<Checkbox name='brightnessTime' text={appearance.night_mode} />
<h3>{appearance.navbar.title}</h3>
<Checkbox name='notesEnabled' text={appearance.navbar.notes} />
<Checkbox name='refresh' text={appearance.navbar.refresh} />
<h3>{appearance.font.title}</h3>
<ul>
<p>{appearance.font.custom} <span className='modalLink' onClick={() => this.resetItem('font')}>{this.language.buttons.reset}</span></p>
<input type='text' value={this.state.font} onChange={(e) => this.setState({ font: e.target.value })}></input>
</ul>
<br />
<Dropdown
label='Font Weight'
name='fontweight'
id='fontWeight'
onChange={() => localStorage.setItem('fontWeight', document.getElementById('fontWeight').value)}>
{/* names are taken from https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight */}
<option className='choices' value='100'>Thin</option>
<option className='choices' value='200'>Extra-Light</option>
<option className='choices' value='300'>Light</option>
<option className='choices' value='400'>Normal</option>
<option className='choices' value='500'>Medium</option>
<option className='choices' value='600'>Semi-Bold</option>
<option className='choices' value='700'>Bold</option>
<option className='choices' value='800'>Extra-Bold</option>
</Dropdown>
<br /><br />
<Dropdown
label='Font Style'
name='fontstyle'
id='fontStyle'
onChange={() => localStorage.setItem('fontStyle', document.getElementById('fontStyle').value)}>
<option className='choices' value='normal'>Normal</option>
<option className='choices' value='italic'>Italic</option>
<option className='choices' value='oblique'>Oblique</option>
</Dropdown>
<br /><br />
<Checkbox name='fontGoogle' text={appearance.font.google} />
<h3>{appearance.accessibility.title}</h3>
<Checkbox name='animations' text={appearance.animations} betaFeature={true} />
<ul>
<p>{appearance.accessibility.zoom} ({this.state.zoom}%) <span className='modalLink' onClick={() => this.resetItem('zoom')}>{this.language.buttons.reset}</span></p>
<input className='range' type='range' min='50' max='200' value={this.state.zoom} onChange={(event) => this.setState({ zoom: event.target.value })} />
</ul>
<ul>
<p>{appearance.accessibility.toast_duration} ({this.state.toast_duration} {appearance.accessibility.milliseconds}) <span className='modalLink' onClick={() => this.resetItem('toast_duration')}>{this.language.buttons.reset}</span></p>
<input className='range' type='range' min='500' max='5000' value={this.state.toast_duration} onChange={(event) => this.setState({ toast_duration: event.target.value })} />
</ul>
</div>
);
}
}

View File

@@ -0,0 +1,265 @@
import React from 'react';
import Checkbox from '../Checkbox';
import Dropdown from '../Dropdown';
import FileUpload from '../FileUpload';
import { ColorPicker } from 'react-color-gradient-picker';
import hexToRgb from '../../../../../modules/helpers/background/hexToRgb';
import rgbToHex from '../../../../../modules/helpers/background/rgbToHex';
import { toast } from 'react-toastify';
import 'react-color-gradient-picker/dist/index.css';
import '../../../../../scss/react-color-picker-gradient-picker-custom-styles.scss';
export default class BackgroundSettings extends React.PureComponent {
DefaultGradientSettings = { 'angle': '180', 'gradient': [{ 'colour': window.language.modals.main.settings.sections.background.source.disabled, 'stop': 0 }], 'type': 'linear' };
GradientPickerInitalState = undefined;
constructor(...args) {
super(...args);
this.state = {
blur: localStorage.getItem('blur'),
brightness: localStorage.getItem('brightness'),
customBackground: localStorage.getItem('customBackground') || '',
gradientSettings: this.DefaultGradientSettings
};
this.language = window.language.modals.main.settings;
}
resetItem(key) {
switch (key) {
case 'customBackgroundColour':
localStorage.setItem('customBackgroundColour', '');
this.setState({
gradientSettings: this.DefaultGradientSettings
});
break;
case 'customBackground':
localStorage.setItem('customBackground', '');
this.setState({
customBackground: ''
});
break;
case 'blur':
localStorage.setItem('blur', 0);
this.setState({
blur: 0
});
break;
case 'brightness':
localStorage.setItem('brightness', 100);
this.setState({
brightness: 100
});
break;
default:
toast('resetItem requires a key!');
}
toast(this.language.toasts.reset);
}
InitializeColorPickerState(gradientSettings) {
this.GradientPickerInitalState = {
points: gradientSettings.gradient.map((g) => {
const rgb = hexToRgb(g.colour);
return {
left: +g.stop,
red: rgb.red,
green: rgb.green,
blue: rgb.blue,
alpha: 1
};
}),
degree: + gradientSettings.angle,
type: gradientSettings.type
};
}
componentDidMount() {
const hex = localStorage.getItem('customBackgroundColour');
let gradientSettings = undefined;
if (hex !== '') {
try {
gradientSettings = JSON.parse(hex);
} catch (e) {
// Disregard exception.
}
}
if (gradientSettings === undefined) {
gradientSettings = this.DefaultGradientSettings;
}
this.setState({
gradientSettings
});
}
onGradientChange = (event, index) => {
const newValue = event.target.value;
const name = event.target.name;
this.setState((s) => {
const newState = {
gradientSettings: {
...s.gradientSettings,
gradient: s.gradientSettings.gradient.map((g, i) => i === index ? { ...g, [name]: newValue } : g)
}
};
return newState;
});
}
addColour = () => {
this.setState((s) => {
const [lastGradient, ...initGradients] = s.gradientSettings.gradient.reverse();
const newState = {
gradientSettings: {
...s.gradientSettings,
gradient: [...initGradients, lastGradient, { 'colour': localStorage.getItem('darkTheme') === 'true' ? '#000000' : '#ffffff', stop: 100 }].sort((a, b) => (a.stop > b.stop) ? 1 : -1)
}
};
return newState;
});
}
currentGradientSettings = () => {
if (typeof this.state.gradientSettings === 'object' && this.state.gradientSettings.gradient.every(g => g.colour !== this.language.sections.background.source.disabled)) {
const clampNumber = (num, a, b) => Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));
return JSON.stringify({
...this.state.gradientSettings,
gradient: [...this.state.gradientSettings.gradient.map(g => { return { ...g, stop: clampNumber(+g.stop, 0, 100) } })].sort((a, b) => (a.stop > b.stop) ? 1 : -1)
});
}
return this.language.sections.background.source.disabled;
}
onColorPickerChange = (attrs, name) => {
if (process.env.NODE_ENV === 'development') {
console.log(attrs, name);
}
this.setState({
gradientSettings: {
'angle': attrs.degree,
'gradient': attrs.points.map((p) => {
return {
'colour': '#' + rgbToHex(p.red, p.green, p.blue),
'stop': p.left
}}),
'type': attrs.type
}
});
};
fileUpload(e) {
localStorage.setItem('customBackground', e.target.result);
this.setState({
customBackground: e.target.result
});
}
componentDidUpdate() {
localStorage.setItem('blur', this.state.blur);
localStorage.setItem('brightness', this.state.brightness);
localStorage.setItem('customBackground', this.state.customBackground);
if (document.getElementById('customBackgroundHex').value !== this.language.sections.background.source.disabled) {
localStorage.setItem('customBackgroundColour', document.getElementById('customBackgroundHex').value);
}
}
render() {
const { background } = this.language.sections;
let colourSettings = null;
if (typeof this.state.gradientSettings === 'object') {
const gradientHasMoreThanOneColour = this.state.gradientSettings.gradient.length > 1;
let gradientInputs;
if (gradientHasMoreThanOneColour) {
if (this.GradientPickerInitalState === undefined) {
this.InitializeColorPickerState(this.state.gradientSettings);
}
gradientInputs = (
<ColorPicker
onStartChange={(color) => this.onColorPickerChange(color, 'start')}
onChange={(color) => this.onColorPickerChange(color, 'change')}
onEndChange={(color) => this.onColorPickerChange(color, 'end')}
gradient={this.GradientPickerInitalState}
isGradient />
);
} else {
gradientInputs = this.state.gradientSettings.gradient.map((g, i) => {
return (
<div key={i}>
<input id={'colour_' + i} type='color' name='colour' className='colour' onChange={(event) => this.onGradientChange(event, i)} value={g.colour}></input>
<label htmlFor={'colour_' + i} className='customBackgroundHex'>{g.colour}</label>
</div>
);
});
}
colourSettings = (
<div>
{gradientInputs}
{this.state.gradientSettings.gradient[0].colour !== background.source.disabled &&
!gradientHasMoreThanOneColour ? (<button type='button' className='add' onClick={this.addColour}>{background.source.add_colour}</button>) : null
}
</div>
);
}
return (
<div>
<h2>{background.title}</h2>
<Checkbox name='background' text='Enabled' />
<h3>{background.buttons.title}</h3>
<ul>
<Checkbox name='view' text={background.buttons.view} />
<Checkbox name='favouriteEnabled' text={background.buttons.favourite} />
</ul>
<h3>{background.effects.title}</h3>
<ul>
<p>{background.effects.blur} ({this.state.blur}%) <span className='modalLink' onClick={() => this.resetItem('blur')}>{this.language.buttons.reset}</span></p>
<input className='range' type='range' min='0' max='100' value={this.state.blur} onChange={(event) => this.setState({ blur: event.target.value })} />
</ul>
<ul>
<p>{background.effects.brightness} ({this.state.brightness}%) <span className='modalLink' onClick={() => this.resetItem('brightness')}>{this.language.buttons.reset}</span></p>
<input className='range' type='range' min='0' max='100' value={this.state.brightness} onChange={(event) => this.setState({ brightness: event.target.value })} />
</ul>
<h3>{background.source.title}</h3>
<ul>
<Dropdown label={background.source.api} name='backgroundAPI'>
<option className='choices' value='mue'>Mue</option>
<option className='choices' value='unsplash'>Unsplash</option>
</Dropdown>
</ul>
<ul>
<p>{background.source.custom_url} <span className='modalLink' onClick={() => this.resetItem('customBackground')}>{this.language.buttons.reset}</span></p>
<input type='text' value={this.state.customBackground} onChange={(e) => this.setState({ customBackground: e.target.value })}></input>
</ul>
<ul>
<p>{background.source.custom_background} <span className='modalLink' onClick={() => this.resetItem('customBackground')}>{this.language.buttons.reset}</span></p>
<button className='uploadbg' onClick={() => document.getElementById('bg-input').click()}>{background.source.upload}</button>
<FileUpload id='bg-input' accept='image/jpeg, image/png, image/webp, image/webm, image/gif' loadFunction={(e) => this.fileUpload(e)} />
</ul>
<ul>
<p>{background.source.custom_colour} <span className='modalLink' onClick={() => this.resetItem('customBackgroundColour')}>{this.language.buttons.reset}</span></p>
<input id='customBackgroundHex' type='hidden' value={this.currentGradientSettings()} />
{colourSettings}
</ul>
</div>
);
}
}

View File

@@ -0,0 +1,56 @@
import React from 'react';
export default class Changelog extends React.PureComponent {
constructor(...args) {
super(...args);
this.state = {
title: this.props.language.title,
date: null,
content: this.props.language.title,
html: this.props.language.loading,
image: null
};
}
async getUpdate() {
const data = await (await fetch(window.constants.API_URL + '/getUpdate')).json();
if (data.statusCode === 500 || data.title === null) {
const supportText = `<br/><p>${this.props.language.contact_support}: <a target='_blank' class='modalLink' href='https://muetab.com/contact'>https://muetab.com/contact</a></p>`;
return this.setState({
title: this.props.language.error.title,
html: this.props.language.error.description + supportText
});
}
this.setState({
title: data.title,
date: data.published,
image: data.image || null,
author: data.author,
html: data.content
});
}
componentDidMount() {
if (localStorage.getItem('offlineMode') === 'true') {
return this.setState({
title: this.props.language.offline.title,
html: this.props.language.offline.description
});
}
this.getUpdate();
}
render() {
return (
<div>
<h1 style={{ 'marginBottom': '-10px' }}>{this.state.title}</h1>
<h5 style={{ 'lineHeight': '0px' }}>{this.state.date}</h5>
{this.state.image ? <img draggable='false' src={this.state.image} alt='Update'></img> : null}
<p dangerouslySetInnerHTML={{ __html: this.state.html }}></p>
</div>
);
}
}

View File

@@ -0,0 +1,9 @@
import React from 'react';
import Checkbox from '../Checkbox';
export default function ExperimentalSettings (props) {
return (
<div>
</div>
);
}

View File

@@ -0,0 +1,66 @@
import React from 'react';
import Checkbox from '../Checkbox';
import DatePicker from 'react-date-picker';
import { toast } from 'react-toastify';
export default class GreetingSettings extends React.PureComponent {
constructor(...args) {
super(...args);
this.state = {
birthday: new Date(localStorage.getItem('birthday')) || new Date(),
greetingName: localStorage.getItem('greetingName') || ''
};
this.language = window.language.modals.main.settings;
}
resetItem() {
this.setState({
greetingName: ''
});
toast(this.language.toasts.reset);
}
changeDate(data) {
//soon
if (data === 'reset') {
return;
}
localStorage.setItem('birthday', data);
this.setState({
birthday: data
});
}
componentDidUpdate() {
localStorage.setItem('greetingName', this.state.greetingName);
}
render() {
const { greeting } = this.language.sections;
return (
<div>
<h2>{greeting.title}</h2>
<Checkbox name='greeting' text={this.language.enabled} />
<Checkbox name='events' text={greeting.events} />
<Checkbox name='defaultGreetingMessage' text={greeting.default} />
<ul>
<p>{greeting.name} <span className='modalLink' onClick={() => this.resetItem()}>{this.language.buttons.reset}</span></p>
<input type='text' value={this.state.greetingName} onChange={(e) => this.setState({ greetingName: e.target.value })}></input>
</ul>
<h3>{greeting.birthday}</h3>
<Checkbox name='birthdayenabled' text={this.language.enabled} />
<ul>
<p>{greeting.birthday_date}</p>
<DatePicker onChange={(data) => this.changeDate(data)} value={this.state.birthday}/>
</ul>
</div>
);
}
}

View File

@@ -0,0 +1,25 @@
import React from 'react';
import Dropdown from '../Dropdown';
const languages = require('../../../../../modules/languages.json');
export default function LanguageSettings () {
const language = window.language.modals.main.settings.sections.language;
return (
<div>
<h2>{language.title}</h2>
<Dropdown label={language.title} name='language' id='language' onChange={() => localStorage.setItem('language', document.getElementById('language').value)}>
{languages.map(language =>
<option className='choices' value={language.code} key={language.code}>{language.text}</option>
)}
</Dropdown>
<br/>
<Dropdown label={language.quote} name='quotelanguage' id='quotelanguage' onChange={() => localStorage.setItem('quotelanguage', document.getElementById('quotelanguage').value)}>
<option className='choices' value='English'>English</option>
<option className='choices' value='French'>Français</option>
</Dropdown>
</div>
);
}

View File

@@ -0,0 +1,69 @@
import React from 'react';
import Checkbox from '../Checkbox';
import { toast } from 'react-toastify';
export default class QuoteSettings extends React.PureComponent {
constructor(...args) {
super(...args);
this.state = {
customQuote: localStorage.getItem('customQuote') || '',
customQuoteAuthor: localStorage.getItem('customQuoteAuthor') || 'Unknown'
};
this.language = window.language.modals.main.settings;
}
resetItem(key) {
switch (key) {
case 'customQuote':
localStorage.setItem('customQuote', '');
this.setState({
customQuote: ''
});
break;
case 'customQuoteAuthor':
localStorage.setItem('customQuoteAuthor', '');
this.setState({
customQuoteAuthor: 'Unknown'
});
break;
default:
toast('resetItem requires a key!');
}
toast(this.language.toasts.reset);
}
componentDidUpdate() {
localStorage.setItem('customQuote', this.state.customQuote);
localStorage.setItem('customQuoteAuthor', this.state.customQuoteAuthor);
}
render() {
const { quote } = this.language.sections;
return (
<div>
<h2>{quote.title}</h2>
<Checkbox name='quote' text={this.language.enabled}/>
<Checkbox name='authorLink' text={quote.author_link}/>
<ul>
<p>{quote.custom} <span className='modalLink' onClick={() => this.resetItem('customQuote')}>{this.language.buttons.reset}</span></p>
<input type='text' value={this.state.customQuote} onChange={(e) => this.setState({ customQuote: e.target.value })}></input>
</ul>
<ul>
<p>{quote.custom_author} <span className='modalLink' onClick={() => this.resetItem('customQuoteAuthor')}>{this.language.buttons.reset}</span></p>
<input type='text' value={this.state.customQuoteAuthor} onChange={(e) => this.setState({ customQuoteAuthor: e.target.value })}></input>
</ul>
<h3>{quote.buttons.title}</h3>
<Checkbox name='copyButton' text={quote.buttons.copy}/>
<Checkbox name='tweetButton' text={quote.buttons.tweet}/>
<Checkbox name='favouriteQuoteEnabled' text={quote.buttons.favourite}/>
</div>
);
}
}

View File

@@ -0,0 +1,84 @@
import React from 'react';
import { toast } from 'react-toastify';
import Dropdown from '../Dropdown';
import Checkbox from '../Checkbox';
const searchEngines = require('../../../../widgets/search/search_engines.json');
export default class SearchSettings extends React.PureComponent {
resetSearch() {
localStorage.removeItem('customSearchEngine');
document.getElementById('customSearchEngine').value = '';
toast(this.props.language.toasts.reset);
}
componentDidMount() {
const searchEngine = localStorage.getItem('searchEngine');
if (searchEngine === 'custom') {
const input = document.getElementById('searchEngineInput');
input.style.display = 'block';
input.enabled = 'true';
document.getElementById('customSearchEngine').value = localStorage.getItem('customSearchEngine');
} else {
localStorage.removeItem('customSearchEngine');
}
document.getElementById('searchEngine').value = searchEngine;
}
componentDidUpdate() {
if (document.getElementById('searchEngineInput').enabled === 'true') {
const input = document.getElementById('customSearchEngine').value;
if (input) {
localStorage.setItem('searchEngine', 'custom');
localStorage.setItem('customSearchEngine', input);
}
}
}
setSearchEngine(input) {
const searchEngineInput = document.getElementById('searchEngineInput');
if (input === 'custom') {
searchEngineInput.enabled = 'true';
searchEngineInput.style.display = 'block';
} else {
searchEngineInput.style.display = 'none';
searchEngineInput.enabled = 'false';
localStorage.setItem('searchEngine', input);
}
}
render() {
const language = window.language.modals.main.settings;
const { search } = language.sections;
return (
<div className='section'>
<h2>{search.title}</h2>
<Checkbox name='searchBar' text={language.enabled} />
<Checkbox name='voiceSearch' text={search.voice_search} />
<ul>
<Dropdown label={search.search_engine}
name='searchEngine'
id='searchEngine'
onChange={() => this.setSearchEngine(document.getElementById('searchEngine').value)} >
{searchEngines.map((engine) =>
<option key={engine.name} className='choices' value={engine.settingsName}>{engine.name}</option>
)}
<option className='choices' value='custom'>{search.custom.split(' ')[0]}</option>
</Dropdown>
</ul>
<ul id='searchEngineInput' style={{ display: 'none' }}>
<p style={{ 'marginTop': '0px' }}>{search.custom} <span className='modalLink' onClick={() => this.resetSearch()}>{language.reset}</span></p>
<input type='text' id='customSearchEngine'></input>
</ul>
</div>
);
}
}

View File

@@ -0,0 +1,89 @@
import React from 'react';
import Checkbox from '../Checkbox';
import Dropdown from '../Dropdown';
export default class TimeSettings extends React.PureComponent {
constructor(...args) {
super(...args);
this.state = {
timeType: localStorage.getItem('timeType') || 'digital'
};
this.language = window.language.modals.main.settings;
}
changeType() {
const value = document.getElementById('timeType').value;
localStorage.setItem('timeType', value);
this.setState({
timeType: value
});
}
render() {
const { time } = this.language.sections;
let timeSettings;
const digitalSettings = (
<React.Fragment>
<h3>{time.digital.title}</h3>
<Checkbox name='seconds' text={time.digital.seconds} />
<Checkbox name='24hour' text={time.digital.twentyfourhour} />
<Checkbox name='ampm' text={time.digital.ampm} />
<Checkbox name='zero' text={time.digital.zero} />
</React.Fragment>
);
const analogSettings = (
<React.Fragment>
<h3>{time.analogue.title}</h3>
<Checkbox name='secondHand' text={time.analogue.second_hand} />
<Checkbox name='minuteHand' text={time.analogue.minute_hand} />
<Checkbox name='hourHand' text={time.analogue.hour_hand} />
<Checkbox name='hourMarks' text={time.analogue.hour_marks} />
<Checkbox name='minuteMarks' text={time.analogue.minute_marks} />
</React.Fragment>
);
switch (this.state.timeType) {
case 'digital': timeSettings = digitalSettings; break;
case 'analogue': timeSettings = analogSettings; break;
default: timeSettings = null; break;
}
return (
<div>
<h2>{time.title}</h2>
<Checkbox name='time' text={this.language.enabled} />
<Dropdown label='Type' name='timeType' onChange={() => this.changeType()}>
<option className='choices' value='digital'>{time.digital.title}</option>
<option className='choices' value='analogue'>{time.analogue.title}</option>
<option className='choices' value='percentageComplete'>{time.percentage_complete}</option>
</Dropdown>
{timeSettings}
<h3>{time.date.title}</h3>
<Checkbox name='date' text={this.language.enabled} />
<Checkbox name='dayofweek' text={time.date.day_of_week} />
<Checkbox name='weeknumber' text={time.date.week_number} />
<Checkbox name='datenth' text={time.date.datenth} />
<Checkbox name='short' text={time.date.short_date} betaFeature={true} />
<Dropdown label={time.date.short_format} name='dateFormat'>
<option className='choices' value='DMY'>DMY</option>
<option className='choices' value='MDY'>MDY</option>
<option className='choices' value='YMD'>YMD</option>
</Dropdown>
<br/>
<br/>
<Dropdown label={time.date.short_separator.title} name='shortFormat'>
<option className='choices' value='dots'>{time.date.short_separator.dots}</option>
<option className='choices' value='dash'>{time.date.short_separator.dash}</option>
<option className='choices' value='gaps'>{time.date.short_separator.gaps}</option>
<option className='choices' value='slashes'>{time.date.short_separator.slashes}</option>
</Dropdown>
</div>
);
}
}

View File

@@ -0,0 +1,20 @@
import React from 'react';
import Added from '../marketplace/sections/Added';
import AddonsTabs from './backend/Tabs';
export default function Addons (props) {
return (
<React.Fragment>
<AddonsTabs>
<div label='Added'>
<Added/>
</div>
<div label=''>
</div>
</AddonsTabs>
</React.Fragment>
);
}

View File

@@ -0,0 +1,19 @@
import React from 'react';
import MarketplaceBackend from '../marketplace/sections/Marketplace';
import MarketplaceTabs from './backend/Tabs';
export default function Marketplace (props) {
return (
<React.Fragment>
<MarketplaceTabs>
<div label='Photo Packs'>
<MarketplaceBackend type='photo_packs'/>
</div>
<div label='Quote Packs'>
<MarketplaceBackend type='quote_packs'/>
</div>
</MarketplaceTabs>
</React.Fragment>
);
}

View File

@@ -0,0 +1,40 @@
import React from 'react';
import About from '../settings/sections/About';
import Language from '../settings/sections/Language';
import Search from '../settings/sections/Search';
import Greeting from '../settings/sections/Greeting';
import Time from '../settings/sections/Time';
import Quote from '../settings/sections/Quote';
import Appearance from '../settings/sections/Appearance';
import Background from '../settings/sections/Background';
import Advanced from '../settings/sections/Advanced';
import Changelog from '../settings/sections/Changelog';
import SettingsTabs from './backend/Tabs';
export default function Settings () {
const language = window.language.modals.main.settings.sections;
return (
<React.Fragment>
<SettingsTabs>
<div label={language.time.title}><Time/></div>
<div label={language.quote.title}><Quote/></div>
<div label={language.greeting.title}><Greeting/></div>
<div label={language.background.title}><Background/></div>
<div label={language.search.title}><Search/></div>
<div label={language.appearance.title}><Appearance/></div>
<div label={language.language.title}><Language/></div>
<div label={language.advanced.title}><Advanced/></div>
<div label='Experimental'></div>
<div label={language.changelog}><Changelog/></div>
<div label={language.about.title}><About/></div>
</SettingsTabs>
<div className='reminder-info'>
<h1>IMPORTANT INFO</h1>
<p>In order for changes to take place, the page must be refreshed.</p>
</div>
</React.Fragment>
);
}

View File

@@ -0,0 +1,76 @@
import React from 'react';
// Navbar
import Settings from '@material-ui/icons/Settings';
import Addons from '@material-ui/icons/Widgets';
import Marketplace from '@material-ui/icons/ShoppingBasket';
// Settings
import Time from '@material-ui/icons/AccessAlarm';
import Greeting from '@material-ui/icons/EmojiPeople';
import Quote from '@material-ui/icons/FormatQuote';
import Background from '@material-ui/icons/Photo';
import Search from '@material-ui/icons/Search';
import Appearance from '@material-ui/icons/FormatPaint';
import Language from '@material-ui/icons/Translate';
import Changelog from '@material-ui/icons/NewReleasesRounded';
import About from '@material-ui/icons/Info';
import Experimental from '@material-ui/icons/BugReport';
// Store
import Colors from '@material-ui/icons/ColorLens';
import Plugins from '@material-ui/icons/Widgets';
import Added from '@material-ui/icons/AddCircle';
export default function Tab(props) {
let className = 'tab-list-item';
if (props.currentTab === props.label) {
className += ' tab-list-active';
}
if (props.navbar === true) {
className = 'navbar-item';
if (props.currentTab === props.label) {
className += ' navbar-item-active';
}
}
let icon, divider;
switch (props.label) {
// Navbar
case 'Settings': icon = <Settings/>; break;
case 'My Add-ons': icon = <Addons/>; break;
case 'Marketplace': icon = <Marketplace/>; break;
// Settings
case 'Time': icon = <Time/>; break;
case 'Greeting': icon = <Greeting/>; break;
case 'Quote': icon = <Quote/>; break;
case 'Background': icon = <Background/>; break;
case 'Search': icon = <Search/>; break;
case 'Appearance': icon = <Appearance/>; break;
case 'Language': icon = <Language/>; divider = true; break;
case 'Advanced': icon = <Settings/>; break;
case 'Experimental': icon = <Experimental/>; divider = true; break;
case 'Change Log': icon = <Changelog/>; break;
case 'About': icon = <About/>; break;
// Store
case 'Themes': icon = <Colors/>; break;
case 'Photo Packs': icon = <Background/>; break;
case 'Quote Packs': icon = <Quote/>; break;
case 'Plugins': icon = <Plugins/>; divider = true; break;
case 'Added': icon = <Added/>; break;
default: break;
}
return (
<React.Fragment>
<li className={className} onClick={() => props.onClick(props.label)}>
{icon} <span>{props.label}</span>
</li>
{(divider === true) ? <div><hr/></div> : null}
</React.Fragment>
)
}

View File

@@ -0,0 +1,59 @@
import React from 'react';
import Tab from './Tab';
import ErrorBoundary from '../../../ErrorBoundary';
export default class Tabs extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
currentTab: this.props.children[0].props.label
};
}
onClick = (tab) => {
this.setState({
currentTab: tab
});
};
render() {
let className = 'sidebar';
let tabClass = 'tab-content';
let optionsText = (<h1>Options</h1>);
if (this.props.navbar) {
className = 'modalNavbar';
tabClass = '';
optionsText = '';
}
return (
<span className='tabs'>
<ul className={className}>
{optionsText}
{this.props.children.map((tab) => {
return (
<Tab
currentTab={this.state.currentTab}
key={tab.props.label}
label={tab.props.label}
onClick={this.onClick}
navbar={this.props.navbar || false}
/>
);
})}
</ul>
<div className={tabClass}>
<ErrorBoundary>
{this.props.children.map((child) => {
if (child.props.label !== this.state.currentTab) return undefined;
return child.props.children;
})}
</ErrorBoundary>
</div>
</span>
);
}
}