refactor: Move misc options to different folder

This commit is contained in:
alexsparkes
2024-03-01 21:24:42 +00:00
parent 038185b656
commit 603a6c5acc
16 changed files with 62 additions and 31 deletions

View File

@@ -0,0 +1,383 @@
import variables from 'config/variables';
import { PureComponent } from 'react';
import { MdEmail, MdContactPage } from 'react-icons/md';
import { FaDiscord, FaTwitter } from 'react-icons/fa';
import { SiGithubsponsors, SiOpencollective } from 'react-icons/si';
import { BiDonateHeart } from 'react-icons/bi';
import { Tooltip } from 'components/Elements';
import other_contributors from 'utils/data/other_contributors.json';
class About extends PureComponent {
constructor() {
super();
this.state = {
contributors: [],
sponsors: [],
other_contributors: [],
photographers: [],
update: variables.getMessage('modals.main.settings.sections.about.version.checking_update'),
loading: variables.getMessage('modals.main.loading'),
image: document.body.classList.contains('dark')
? 'src/assets/icons/mue_about.png'
: 'src/assets/icons/mue_about.png',
};
this.controller = new AbortController();
}
async getGitHubData() {
let contributors, sponsors, photographers, versionData;
try {
versionData = await (
await fetch(
variables.constants.GITHUB_URL +
'/repos/' +
variables.constants.ORG_NAME +
'/' +
variables.constants.REPO_NAME +
'/releases',
{ signal: this.controller.signal },
)
).json();
contributors = await (
await fetch(
variables.constants.GITHUB_URL +
'/repos/' +
variables.constants.ORG_NAME +
'/' +
variables.constants.REPO_NAME +
'/contributors',
{ signal: this.controller.signal },
)
).json();
sponsors = (
await (
await fetch(variables.constants.SPONSORS_URL + '/list', {
signal: this.controller.signal,
})
).json()
).sponsors;
photographers = await (
await fetch(variables.constants.API_URL + '/images/photographers', {
signal: this.controller.signal,
})
).json();
} catch (e) {
if (this.controller.signal.aborted === true) {
return;
}
return this.setState({
update: variables.getMessage('modals.main.settings.sections.about.version.error.title'),
loading: variables.getMessage(
'modals.main.settings.sections.about.version.error.description',
),
});
}
if (sponsors.length === 0) {
sponsors = [{ handle: 'empty' }];
}
if (this.controller.signal.aborted === true) {
return;
}
const newVersion = versionData[0].tag_name;
let update = variables.getMessage('modals.main.settings.sections.about.version.no_update');
if (
Number(variables.constants.VERSION.replaceAll('.', '')) <
Number(newVersion.replaceAll('.', ''))
) {
update = `${variables.getMessage(
'modals.main.settings.sections.about.version.update_available',
)}: ${newVersion}`;
}
this.setState({
// exclude bots
contributors: contributors.filter((contributor) => !contributor.login.includes('bot')),
sponsors,
update,
other_contributors,
photographers,
loading: null,
});
}
componentDidMount() {
if (navigator.onLine === false || localStorage.getItem('offlineMode') === 'true') {
this.setState({
update: variables.getMessage('modals.main.settings.sections.about.version.checking_update'),
loading: variables.getMessage('modals.main.marketplace.offline.description'),
});
return;
}
this.getGitHubData();
}
componentWillUnmount() {
// stop making requests
this.controller.abort();
}
render() {
return (
<>
<div className="settingsRow" style={{ justifyContent: 'center' }}>
<div style={{ display: 'flex', flexFlow: 'column', gap: '5px' }}>
<img draggable={false} className="aboutLogo" src={this.state.image} alt="Logo" />
<div className="aboutText">
<span className="title">Mue, By Kaiso</span>
<span className="subtitle">
{variables.getMessage('modals.main.settings.sections.about.version.title')}{' '}
{variables.constants.VERSION}
</span>
<span className="subtitle">({this.state.update})</span>
</div>
<div>
<span className="subtitle">
Copyright 2018-
{new Date().getFullYear()}{' '}
<a
className="link"
href={
'https://github.com/' +
variables.constants.ORG_NAME +
'/' +
variables.constants.REPO_NAME +
'/graphs/contributors'
}
target="_blank"
rel="noopener noreferrer"
>
The Mue Authors
</a>
,{' '}
</span>
<span className="subtitle">
Copyright 2023-2024{' '}
<a
className="link"
href="https://kaiso.one"
target="_blank"
rel="noopener noreferrer"
>
{' '}
Kaiso One Ltd
</a>
</span>
</div>
<span className="subtitle">Licensed under the BSD-3-Clause License</span>
<span className="subtitle">
<a
href={variables.constants.PRIVACY_URL}
className="link"
target="_blank"
rel="noopener noreferrer"
>
{variables.getMessage('modals.welcome.sections.privacy.links.privacy_policy')}
</a>
</span>
</div>
</div>
<div className="settingsRow" style={{ flexFlow: 'column', alignItems: 'flex-start' }}>
<span className="title">
{variables.getMessage('modals.main.settings.sections.about.contact_us')}
</span>
<div className="aboutContact">
<a
className="donateButton"
href="https://muetab.com/contact"
target="_blank"
rel="noopener noreferrer"
>
<MdContactPage />
{variables.getMessage('modals.main.settings.sections.about.form_button')}
</a>
<Tooltip title={'Email'}>
<a
href={'mailto:' + variables.constants.EMAIL}
target="_blank"
rel="noopener noreferrer"
>
<MdEmail />
</a>
</Tooltip>
<Tooltip title={'Twitter'}>
<a
href={'https://twitter.com/' + variables.constants.TWITTER_HANDLE}
target="_blank"
rel="noopener noreferrer"
>
<FaTwitter />
</a>
</Tooltip>
<Tooltip title={'Discord'}>
<a
href={'https://discord.gg/' + variables.constants.DISCORD_SERVER}
target="_blank"
rel="noopener noreferrer"
>
<FaDiscord />
</a>
</Tooltip>
</div>
</div>
<div className="settingsRow" style={{ flexFlow: 'column', alignItems: 'flex-start' }}>
<span className="title">
{variables.getMessage('modals.main.settings.sections.about.support_mue')}
</span>
<p>{variables.getMessage('modals.main.settings.sections.about.support_subtitle')}</p>
<div className="aboutContact">
<a
className="donateButton"
href={'https://opencollective.com/' + variables.constants.OPENCOLLECTIVE_USERNAME}
target="_blank"
rel="noopener noreferrer"
>
<BiDonateHeart />
{variables.getMessage('modals.main.settings.sections.about.support_donate')}
</a>
<Tooltip title={'GitHub Sponsors'}>
<a
href={'https://github.com/sponsors/' + variables.constants.ORG_NAME}
target="_blank"
rel="noopener noreferrer"
>
<SiGithubsponsors />
</a>
</Tooltip>
<Tooltip title={'Open Collective'}>
<a
href={'https://opencollective.com/' + variables.constants.OPENCOLLECTIVE_USERNAME}
target="_blank"
rel="noopener noreferrer"
>
<SiOpencollective />
</a>
</Tooltip>
</div>
</div>
<div
className="settingsRow"
style={{ flexFlow: 'column', alignItems: 'flex-start', minHeight: '70px' }}
>
<span className="title">
{variables.getMessage('modals.main.settings.sections.about.resources_used.title')}
</span>
<span className="subtitle">
<a
href="https://www.pexels.com"
className="link"
target="_blank"
rel="noopener noreferrer"
>
Pexels
</a>
,{' '}
<a
href="https://unsplash.com"
className="link"
target="_blank"
rel="noopener noreferrer"
>
Unsplash
</a>{' '}
({variables.getMessage('modals.main.settings.sections.about.resources_used.bg_images')})
</span>
</div>
<div className="settingsRow" style={{ flexFlow: 'column', alignItems: 'flex-start' }}>
<span className="title">
{variables.getMessage('modals.main.settings.sections.about.contributors')}
</span>
<p>{this.state.loading}</p>
<div className="contributorImages">
{this.state.contributors.map(({ login, id }) => (
<Tooltip title={login} key={login}>
<a href={'https://github.com/' + login} target="_blank" rel="noopener noreferrer">
<img
draggable={false}
src={'https://avatars.githubusercontent.com/u/' + id + '?s=128'}
alt={login}
></img>
</a>
</Tooltip>
))}
{
// for those who contributed without opening a pull request
this.state.other_contributors.map(({ login, avatar_url }) => (
<Tooltip title={login} key={login}>
<a href={'https://github.com/' + login} target="_blank" rel="noopener noreferrer">
<img draggable={false} src={avatar_url + '&s=128'} alt={login}></img>
</a>
</Tooltip>
))
}
</div>
</div>
<div className="settingsRow" style={{ flexFlow: 'column', alignItems: 'flex-start' }}>
<span className="title">
{variables.getMessage('modals.main.settings.sections.about.supporters')}
</span>
<p>{this.state.loading}</p>
<div className="contributorImages">
{this.state.sponsors.map(({ handle, avatar }) => {
if (handle === 'empty') {
return (
<p>{variables.getMessage('modals.main.settings.sections.about.no_supporters')}</p>
);
}
return (
<Tooltip title={handle} key={handle}>
<a
href={'https://github.com/' + handle}
target="_blank"
rel="noopener noreferrer"
>
<img draggable={false} src={avatar.split('?')[0] + '?s=128'} alt={handle}></img>
</a>
</Tooltip>
);
})}
</div>
</div>
<div
className="settingsRow"
style={{
flexFlow: 'column',
alignItems: 'flex-start',
minHeight: '10px',
borderBottom: '0',
}}
>
<span className="title">
{variables.getMessage('modals.main.settings.sections.about.photographers')}
</span>
{!!this.state.loading ? <p>{this.state.loading}</p> : <></>}
<ul>
{this.state.photographers.map(({ name, count }) => (
<>
<li className="subtitle-photographers">
{name}
<span> ({count} images)</span>
</li>
</>
))}
</ul>
</div>
</>
);
}
}
export { About as default, About };

View File

@@ -0,0 +1,183 @@
import variables from 'config/variables';
import { useState } from 'react';
import Modal from 'react-modal';
import {
MdUpload as ImportIcon,
MdDownload as ExportIcon,
MdRestartAlt as ResetIcon,
MdDataUsage,
} from 'react-icons/md';
import { exportSettings, importSettings } from 'utils/settings';
import { FileUpload, Text, Switch, Dropdown } from 'components/Form/Settings';
import { ResetModal } from 'components/Elements';
import { Header, Section, Row, Content, Action } from 'components/Layout/Settings';
import time_zones from 'features/time/timezones.json';
function AdvancedOptions() {
const [resetModal, setResetModal] = useState(false);
const [data, setData] = useState(false);
const ADVANCED_SECTION = 'modals.main.settings.sections.advanced';
const Data = () => {
return (
<>
{localStorage.getItem('welcomePreview') !== 'true' && (
<div className="settingsRow">
<div className="content">
<span className="title">
{variables.getMessage('modals.main.settings.sections.advanced.data')}
</span>
<span className="subtitle">
{variables.getMessage('modals.main.settings.sections.advanced.data_subtitle')}
</span>
</div>
<div className="action activityButtons">
<button onClick={() => setResetModal(true)}>
{variables.getMessage('modals.main.settings.buttons.reset')}
<ResetIcon />
</button>
<button onClick={() => exportSettings()}>
{variables.getMessage('modals.main.settings.buttons.export')}
<ExportIcon />
</button>
<button onClick={() => document.getElementById('file-input').click()}>
{variables.getMessage('modals.main.settings.buttons.import')}
<ImportIcon />
</button>
</div>
</div>
)}
</>
);
};
let header;
if (data) {
header = (
<Header
title={variables.getMessage(`${ADVANCED_SECTION}.title`)}
secondaryTitle={variables.getMessage(`${ADVANCED_SECTION}.data`)}
goBack={() => setData(false)}
report={false}
/>
);
} else {
header = <Header title={variables.getMessage(`${ADVANCED_SECTION}.title`)} report={false} />;
}
return (
<>
{header}
{data ? (
<>
<Data />
<Modal
closeTimeoutMS={100}
onRequestClose={() => setResetModal(false)}
isOpen={resetModal}
className="Modal resetmodal mainModal"
overlayClassName="Overlay resetoverlay"
ariaHideApp={false}
>
<ResetModal modalClose={() => setResetModal(false)} />
</Modal>
</>
) : (
<>
<Section
title={variables.getMessage(`${ADVANCED_SECTION}.data`)}
subtitle={variables.getMessage(
'modals.main.settings.sections.appearance.accessibility.description',
)}
onClick={() => setData(true)}
icon={<MdDataUsage />}
/>
<Row>
<Content
title={variables.getMessage('modals.main.settings.sections.advanced.offline_mode')}
subtitle={variables.getMessage(
'modals.main.settings.sections.advanced.offline_subtitle',
)}
/>
<Action>
<Switch name="offlineMode" element=".other" />
</Action>
</Row>
<Row>
<Content
title={variables.getMessage('modals.main.settings.sections.advanced.timezone.title')}
subtitle={variables.getMessage(
'modals.main.settings.sections.advanced.timezone.subtitle',
)}
/>
<Action>
<Dropdown
name="timezone"
category="timezone"
items={[
{
value: 'auto',
text: variables.getMessage(
'modals.main.settings.sections.advanced.timezone.automatic',
),
},
...time_zones.map((timezone) => ({ value: timezone, text: timezone })),
]}
/>
</Action>
</Row>
<Row>
<Content
title={variables.getMessage('modals.main.settings.sections.advanced.tab_name')}
subtitle={variables.getMessage(
'modals.main.settings.sections.advanced.tab_name_subtitle',
)}
/>
<Action>
<Text name="tabName" default={variables.getMessage('tabname')} category="other" />
</Action>
</Row>
<FileUpload
id="file-input"
accept="application/json"
type="settings"
loadFunction={(e) => importSettings(e)}
/>
<Row>
<Content
title={variables.getMessage('modals.main.settings.sections.advanced.custom_css')}
subtitle={variables.getMessage(
'modals.main.settings.sections.advanced.custom_css_subtitle',
)}
/>
<Action>
<Text name="customcss" textarea={true} category="other" customcss={true} />
</Action>
</Row>
<Row final={true}>
<Content
title={variables.getMessage('modals.main.settings.sections.experimental.title')}
subtitle={variables.getMessage(
'modals.main.settings.sections.advanced.experimental_warning',
)}
/>
<Action>
<Switch
name="experimental"
text={variables.getMessage('modals.main.settings.enabled')}
element=".other"
/>
</Action>
</Row>
</>
)}
</>
);
}
export { AdvancedOptions as default, AdvancedOptions };

View File

@@ -0,0 +1,289 @@
import { memo, useState } from 'react';
import variables from 'config/variables';
import { Checkbox, Dropdown, Radio, Slider, Text } from 'components/Form/Settings';
import { Header, Section, Row, Content, Action } from 'components/Layout/Settings';
import { MdAccessibility } from 'react-icons/md';
import values from 'utils/data/slider_values.json';
function AppearanceOptions() {
const [accessibility, setAccessibility] = useState(false);
const ThemeSelection = () => {
return (
<Row>
<Content
title={variables.getMessage('modals.main.settings.sections.appearance.theme.title')}
subtitle={variables.getMessage(
'modals.main.settings.sections.appearance.theme.description',
)}
/>
<Action>
<Radio
name="theme"
options={[
{
name: variables.getMessage('modals.main.settings.sections.appearance.theme.auto'),
value: 'auto',
},
{
name: variables.getMessage('modals.main.settings.sections.appearance.theme.light'),
value: 'light',
},
{
name: variables.getMessage('modals.main.settings.sections.appearance.theme.dark'),
value: 'dark',
},
]}
category="other"
/>
</Action>
</Row>
);
};
const FontOptions = () => {
const fontWeight = 'modals.main.settings.sections.appearance.font.weight';
return (
<Row>
<Content
title={variables.getMessage('modals.main.settings.sections.appearance.font.title')}
subtitle={variables.getMessage(
'modals.main.settings.sections.appearance.font.description',
)}
/>
<Action>
<Checkbox
name="fontGoogle"
text={variables.getMessage('modals.main.settings.sections.appearance.font.google')}
category="other"
/>
<Text
title={variables.getMessage('modals.main.settings.sections.appearance.font.custom')}
name="font"
upperCaseFirst={true}
category="other"
/>
{/* names are taken from https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight */}
<Dropdown
label={variables.getMessage(
'modals.main.settings.sections.appearance.font.weight.title',
)}
name="fontweight"
category="other"
items={[
{
value: '100',
text: variables.getMessage(fontWeight + '.thin'),
},
{
value: '200',
text: variables.getMessage(fontWeight + '.extra_light'),
},
{
value: '300',
text: variables.getMessage(fontWeight + '.light'),
},
{
value: '400',
text: variables.getMessage(fontWeight + '.normal'),
},
{
value: '500',
text: variables.getMessage(fontWeight + '.medium'),
},
{
value: '600',
text: variables.getMessage(fontWeight + '.semi_bold'),
},
{
value: '700',
text: variables.getMessage(fontWeight + '.bold'),
},
{
value: '800',
text: variables.getMessage(fontWeight + '.extra_bold'),
},
]}
/>
<Dropdown
label={variables.getMessage(
'modals.main.settings.sections.appearance.font.style.title',
)}
name="fontstyle"
category="other"
items={[
{
value: 'normal',
text: variables.getMessage(
'modals.main.settings.sections.appearance.font.style.normal',
),
},
{
value: 'italic',
text: variables.getMessage(
'modals.main.settings.sections.appearance.font.style.italic',
),
},
{
value: 'oblique',
text: variables.getMessage(
'modals.main.settings.sections.appearance.font.style.oblique',
),
},
]}
/>
</Action>
</Row>
);
};
const WidgetStyle = () => {
return (
<Row>
<Content
title={variables.getMessage('modals.main.settings.sections.appearance.style.title')}
subtitle={variables.getMessage(
'modals.main.settings.sections.appearance.style.description',
)}
/>
<Action>
<Radio
name="widgetStyle"
element=".other"
options={[
{
name: variables.getMessage('modals.main.settings.sections.appearance.style.legacy'),
value: 'legacy',
},
{
name: variables.getMessage('modals.main.settings.sections.appearance.style.new'),
value: 'new',
},
]}
category="widgets"
/>
</Action>
</Row>
);
};
const AccessibilityOptions = () => {
return (
<Row final={true}>
<Content
title={variables.getMessage(
'modals.main.settings.sections.appearance.accessibility.title',
)}
subtitle={variables.getMessage(
'modals.main.settings.sections.appearance.accessibility.description',
)}
/>
<Action>
<Dropdown
label={variables.getMessage(
'modals.main.settings.sections.appearance.accessibility.text_shadow.title',
)}
name="textBorder"
category="other"
items={[
{
value: 'new',
text: variables.getMessage(
'modals.main.settings.sections.appearance.accessibility.text_shadow.new',
),
},
{
value: 'true',
text: variables.getMessage(
'modals.main.settings.sections.appearance.accessibility.text_shadow.old',
),
},
{
value: 'none',
text: variables.getMessage(
'modals.main.settings.sections.appearance.accessibility.text_shadow.none',
),
},
]}
/>
<Checkbox
text={variables.getMessage(
'modals.main.settings.sections.appearance.accessibility.animations',
)}
name="animations"
category="other"
/>
<Slider
title={variables.getMessage(
'modals.main.settings.sections.appearance.accessibility.toast_duration',
)}
name="toastDisplayTime"
default="2500"
step="100"
min="500"
max="5000"
marks={values.toast}
display={
' ' +
variables.getMessage(
'modals.main.settings.sections.appearance.accessibility.milliseconds',
)
}
/>
</Action>
</Row>
);
};
let header;
if (accessibility) {
header = (
<Header
title={variables.getMessage('modals.main.settings.sections.appearance.title')}
secondaryTitle={variables.getMessage(
'modals.main.settings.sections.appearance.accessibility.title',
)}
goBack={() => setAccessibility(false)}
report={false}
/>
);
} else {
header = (
<Header
title={variables.getMessage('modals.main.settings.sections.appearance.title')}
report={false}
/>
);
}
return (
<>
{header}
{accessibility ? (
<AccessibilityOptions />
) : (
<>
<Section
title={variables.getMessage(
'modals.main.settings.sections.appearance.accessibility.title',
)}
subtitle={variables.getMessage(
'modals.main.settings.sections.appearance.accessibility.description',
)}
icon={<MdAccessibility />}
onClick={() => setAccessibility(true)}
/>
<ThemeSelection />
<FontOptions />
<WidgetStyle />
</>
)}
</>
);
}
const MemoizedAppearanceOptions = memo(AppearanceOptions);
export { MemoizedAppearanceOptions as default, MemoizedAppearanceOptions as AppearanceOptions };

View File

@@ -0,0 +1,162 @@
import variables from 'config/variables';
import { PureComponent, createRef } from 'react';
import { MdOutlineWifiOff } from 'react-icons/md';
import Modal from 'react-modal';
import Lightbox from '../../marketplace/components/Elements/Lightbox/Lightbox';
class Changelog extends PureComponent {
constructor() {
super();
this.state = {
title: null,
showLightbox: false,
lightboxImg: null,
};
this.offlineMode = localStorage.getItem('offlineMode') === 'true';
this.controller = new AbortController();
this.changelog = createRef();
}
async getUpdate() {
const res = await fetch(variables.constants.BLOG_POST + '/index.json', {
signal: this.controller.signal,
});
if (res.status === 404) {
this.setState({ error: true });
return;
}
if (this.controller.signal.aborted === true) {
return;
}
const data = await res.json();
let date = new Date(data.date.split(' ')[0]);
date = date.toLocaleDateString(variables.languagecode.replace('_', '-'), {
year: 'numeric',
month: 'long',
day: 'numeric',
});
this.setState({
title: data.title,
date,
image: data.featured_image || null,
author: variables.getMessage('modals.main.settings.sections.changelog.by', {
author: data.authors.join(', '),
}),
content: data.markdown,
});
// lightbox etc
const images = this.changelog.current.getElementsByTagName('img');
const links = this.changelog.current.getElementsByTagName('a');
for (const img of images) {
img.draggable = false;
img.onclick = () => {
this.setState({
showLightbox: true,
lightboxImg: img.src,
});
};
}
// open in new tab
for (let link = 0; link < links.length; link++) {
links[link].target = '_blank';
links[link].rel = 'noopener noreferrer';
}
}
componentDidMount() {
if (navigator.onLine === false || this.offlineMode) {
return;
}
this.getUpdate();
}
componentWillUnmount() {
// stop making requests
this.controller.abort();
}
render() {
const errorMessage = (msg) => {
return (
<div className="emptyItems">
<div className="emptyMessage">{msg}</div>
</div>
);
};
if (navigator.onLine === false || this.offlineMode) {
return errorMessage(
<>
<MdOutlineWifiOff />
<h1>{variables.getMessage('modals.main.marketplace.offline.title')}</h1>
<p className="description">
{variables.getMessage('modals.main.marketplace.offline.description')}
</p>
</>,
);
}
if (this.state.error === true) {
return errorMessage(
<>
<MdOutlineWifiOff />
<span className="title">{variables.getMessage('modals.main.error_boundary.title')}</span>
<span className="subtitle">
{variables.getMessage('modals.main.error_boundary.message')}
</span>
</>,
);
}
if (!this.state.title) {
return errorMessage(
<div className="loaderHolder">
<div id="loader"></div>
<span className="subtitle">{variables.getMessage('modals.main.loading')}</span>
</div>,
);
}
return (
<div className="changelogtab" ref={this.changelog}>
<h1>{this.state.title}</h1>
<h5>
{this.state.author} {this.state.date}
</h5>
{this.state.image && (
<img
draggable={false}
src={this.state.image}
alt={this.state.title}
className="updateImage"
/>
)}
<div className="updateChangelog">{this.state.content}</div>
<Modal
closeTimeoutMS={100}
onRequestClose={() => this.setState({ showLightbox: false })}
isOpen={this.state.showLightbox}
className="Modal lightBoxModal"
overlayClassName="Overlay resetoverlay"
ariaHideApp={false}
>
<Lightbox
modalClose={() => this.setState({ showLightbox: false })}
img={this.state.lightboxImg}
/>
</Modal>
</div>
);
}
}
export { Changelog as default, Changelog };

View File

@@ -0,0 +1,82 @@
import variables from 'config/variables';
import { useState, memo } from 'react';
import Checkbox from '../../../components/Form/Settings/Checkbox/Checkbox';
import Slider from '../../../components/Form/Settings/Slider/Slider';
import { TextField } from '@mui/material';
import EventBus from 'utils/eventbus';
import values from 'utils/data/slider_values.json';
import { Row, Content, Action } from '../../../components/Layout/Settings/Item/SettingsItem';
function ExperimentalOptions() {
const [eventType, setEventType] = useState();
const [eventName, setEventName] = useState();
return (
<>
<span className="mainTitle">
{variables.getMessage('modals.main.settings.sections.experimental.title')}
</span>
<span className="subtitle">
{variables.getMessage('modals.main.settings.sections.experimental.warning')}
</span>
<Row>
<Content
title={variables.getMessage('modals.main.settings.sections.experimental.developer')}
/>
<Action>
<Checkbox name="debug" text="Debug hotkey (Ctrl + #)" element=".other" />
<Slider
title="Debug timeout"
name="debugtimeout"
min="0"
max="5000"
default="0"
step="100"
marks={values.experimental}
element=".other"
/>
<p style={{ textAlign: 'left' }}>Send Event</p>
<TextField
label={'Type'}
value={eventType}
onChange={(e) => setEventType(e.target.value)}
spellCheck={false}
varient="outlined"
InputLabelProps={{ shrink: true }}
/>
<TextField
label={'Name'}
value={eventName}
onChange={(e) => setEventName(e.target.value)}
spellCheck={false}
varient="outlined"
InputLabelProps={{ shrink: true }}
/>
<button className="uploadbg" onClick={() => EventBus.emit(eventType, eventName)}>
Send
</button>
</Action>
</Row>
<Row final={true}>
<Content title="Data" />
<Action>
<button
className="reset"
style={{ marginLeft: '0px' }}
onClick={() => localStorage.clear()}
>
Clear LocalStorage
</button>
</Action>
</Row>
</>
);
}
const MemoizedExperimentalOptions = memo(ExperimentalOptions);
export {
MemoizedExperimentalOptions as default,
MemoizedExperimentalOptions as ExperimentalOptions,
};

View File

@@ -0,0 +1,113 @@
import variables from 'config/variables';
import { PureComponent } from 'react';
import { MdOutlineOpenInNew } from 'react-icons/md';
import { Radio } from 'components/Form/Settings';
import languages from '@/i18n/languages.json';
class LanguageOptions extends PureComponent {
constructor() {
super();
this.state = {
quoteLanguages: [
{
name: variables.getMessage('modals.main.loading'),
value: 'loading',
},
],
};
this.controller = new AbortController();
}
async getquoteLanguages() {
const data = await (
await fetch(variables.constants.API_URL + '/quotes/languages', {
signal: this.controller.signal,
})
).json();
if (this.controller.signal.aborted === true) {
return;
}
const quoteLanguages = data.map((language) => {
return {
name: languages.find((l) => l.value === language.name)
? languages.find((l) => l.value === language.name).name
: 'English',
value: language,
};
});
this.setState({
quoteLanguages,
});
}
componentDidMount() {
if (navigator.onLine === false || localStorage.getItem('offlineMode') === 'true') {
return this.setState({
quoteLanguages: [
{
name: variables.getMessage('modals.main.marketplace.offline.description'),
value: 'loading',
},
],
});
}
this.getquoteLanguages();
}
componentWillUnmount() {
// stop making requests
this.controller.abort();
}
render() {
return (
<>
<div className="modalHeader">
<span className="mainTitle">
{variables.getMessage('modals.main.settings.sections.language.title')}
</span>
<div className="headerActions">
{/*<a className="link" href="" target="_blank" rel="noopener noreferrer">
Improve
<MdOutlineOpenInNew />
</a>*/}
<a
className="link"
href="https://hosted.weblate.org/new-lang/mue/mue-tab/"
target="_blank"
rel="noopener noreferrer"
>
Add translation
<MdOutlineOpenInNew />
</a>
</div>
</div>
<div className="languageSettings">
<Radio name="language" options={languages} element=".other" />
</div>
<span className="mainTitle">
{variables.getMessage('modals.main.settings.sections.language.quote')}
</span>
<div className="languageSettings">
<Radio
name="quoteLanguage"
options={this.state.quoteLanguages.map((language) => {
return { name: language.name, value: language.value.name };
})}
defaultValue={this.state.quoteLanguages[0].name}
category="quote"
/>
</div>
</>
);
}
}
export { LanguageOptions as default, LanguageOptions };

View File

@@ -0,0 +1,197 @@
import variables from 'config/variables';
import { PureComponent } from 'react';
import { MdOutlineDragIndicator } from 'react-icons/md';
import { sortableContainer, sortableElement } from '@muetab/react-sortable-hoc';
import { toast } from 'react-toastify';
import Greeting from './overview_skeletons/Greeting';
import Clock from './overview_skeletons/Clock';
import Quote from './overview_skeletons/Quote';
import QuickLinks from './overview_skeletons/QuickLinks';
import Date from './overview_skeletons/Date';
import Message from './overview_skeletons/Message';
import EventBus from 'utils/eventbus';
const widget_name = {
greeting: variables.getMessage('modals.main.settings.sections.greeting.title'),
time: variables.getMessage('modals.main.settings.sections.time.title'),
quicklinks: variables.getMessage('modals.main.settings.sections.quicklinks.title'),
quote: variables.getMessage('modals.main.settings.sections.quote.title'),
date: variables.getMessage('modals.main.settings.sections.date.title'),
message: variables.getMessage('modals.main.settings.sections.message.title'),
};
const SortableItem = sortableElement(({ value }) => (
<li className="sortableItem">
{widget_name[value]}
<MdOutlineDragIndicator />
</li>
));
const SortableContainer = sortableContainer(({ children }) => (
<ul className="sortablecontainer">{children}</ul>
));
class Overview extends PureComponent {
constructor() {
super();
this.state = {
items: JSON.parse(localStorage.getItem('order')),
news: {
title: '',
date: '',
description: '',
link: '',
linkText: '',
},
newsDone: false,
};
}
arrayMove(array, oldIndex, newIndex) {
const result = Array.from(array);
const [removed] = result.splice(oldIndex, 1);
result.splice(newIndex, 0, removed);
return result;
}
onSortEnd = ({ oldIndex, newIndex }) => {
this.setState({
items: this.arrayMove(this.state.items, oldIndex, newIndex),
});
};
reset = () => {
localStorage.setItem(
'order',
JSON.stringify(['greeting', 'time', 'quicklinks', 'quote', 'date', 'message']),
);
this.setState({
items: JSON.parse(localStorage.getItem('order')),
});
toast(variables.getMessage('toasts.reset'));
};
enabled = (setting) => {
switch (setting) {
case 'quicklinks':
return localStorage.getItem('quicklinksenabled') === 'true';
default:
return localStorage.getItem(setting) === 'true';
}
};
getTab = (value) => {
switch (value) {
case 'greeting':
return <Greeting />;
case 'time':
return <Clock />;
case 'quicklinks':
return <QuickLinks />;
case 'quote':
return <Quote />;
case 'date':
return <Date />;
case 'message':
return <Message />;
default:
return null;
}
};
async getNews() {
const data = await (await fetch(variables.constants.API_URL + '/news')).json();
data.date = new window.Date(data.date).toLocaleDateString(
variables.languagecode.replace('_', '-'),
{
year: 'numeric',
month: 'long',
day: 'numeric',
},
);
this.setState({
news: data,
newsDone: true,
});
}
componentDidUpdate() {
localStorage.setItem('order', JSON.stringify(this.state.items));
variables.stats.postEvent('setting', 'Widget order');
EventBus.emit('refresh', 'widgets');
}
componentDidMount() {
this.getNews();
}
render() {
return (
<>
<span className="mainTitle">
{variables.getMessage('modals.main.marketplace.product.overview')}
</span>
{/*<span className="title">{variables.getMessage('modals.main.marketplace.product.overview')}</span>
<span className="link" onClick={this.reset}>
{variables.getMessage('modals.main.settings.buttons.reset')}
</span>*/}
<div className="overviewGrid">
<div>
<span className="title">{variables.getMessage('modals.welcome.buttons.preview')}</span>
<div className="tabPreview">
<div className="previewItem" style={{ maxWidth: '50%' }}>
{this.state.items.map((value, index) => {
if (!this.enabled(value)) {
return null;
}
return (
<div className="previewItem" key={`item-${value}`} index={index}>
{' '}
{this.getTab(value)}
</div>
);
})}
</div>
</div>
<div className="overviewNews">
<span className="title">{this.state.news.title}</span>
<span className="subtitle">{this.state.news.date}</span>
<span className="content">{this.state.news.description}</span>
<a className="link" href={this.state.news.link}>
{this.state.news.linkText}
</a>
</div>
</div>
<div>
<span className="title">
{variables.getMessage('modals.main.settings.sections.order.title')}
</span>
<SortableContainer
onSortEnd={this.onSortEnd}
lockAxis="y"
lockToContainerEdges
disableAutoscroll
>
{this.state.items.map((value, index) => {
if (!this.enabled(value)) {
return null;
}
return <SortableItem key={`item-${value}`} index={index} value={value} />;
})}
</SortableContainer>
</div>
</div>
</>
);
}
}
export { Overview as default, Overview };

View File

@@ -0,0 +1,228 @@
/* eslint-disable array-callback-return */
import variables from 'config/variables';
import { PureComponent } from 'react';
import { MdShowChart, MdRestartAlt, MdDownload } from 'react-icons/md';
import { FaTrophy } from 'react-icons/fa';
import { toast } from 'react-toastify';
import { Button } from 'components/Elements';
import { Header, CustomActions } from 'components/Layout/Settings';
import { saveFile } from 'utils/saveFile';
import { translations, achievements } from 'utils/achievements';
class Stats extends PureComponent {
constructor() {
super();
this.state = {
stats: JSON.parse(localStorage.getItem('statsData')) || {},
achievements,
};
}
getAchievements() {
const achievements = this.state.achievements;
achievements.forEach((achievement) => {
switch (achievement.condition.type) {
case 'tabsOpened':
if (this.state.stats['tabs-opened'] >= achievement.condition.amount) {
achievement.achieved = true;
}
break;
case 'addonInstall':
if (this.state.stats.marketplace) {
if (this.state.stats.marketplace['install'] >= achievement.condition.amount) {
achievement.achieved = true;
}
}
break;
default:
break;
}
});
this.setState({
achievements,
});
}
getUnlockedCount() {
let count = 0;
this.state.achievements.forEach((achievement) => {
if (achievement.achieved) {
count++;
}
});
return count;
}
resetStats() {
localStorage.setItem('statsData', JSON.stringify({}));
this.setState({
stats: {},
});
toast(variables.getMessage('toasts.stats_reset'));
this.getAchievements();
this.forceUpdate();
}
downloadStats() {
let date = new Date();
// Format the date as YYYY-MM-DD_HH-MM-SS
let formattedDate = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}_${date.getHours().toString().padStart(2, '0')}-${date.getMinutes().toString().padStart(2, '0')}-${date.getSeconds().toString().padStart(2, '0')}`;
let filename = `mue_stats_${formattedDate}.json`;
saveFile(JSON.stringify(this.state.stats, null, 2), filename);
}
getLocalisedAchievementData(id) {
const localised = translations[variables.languagecode][id] ||
translations.en_GB[id] || { name: id, description: '' };
return {
name: localised.name,
description: localised.description,
};
}
componentDidMount() {
this.getAchievements();
this.forceUpdate();
}
render() {
const achievementElement = (key, id, achieved) => {
const { name, description } = this.getLocalisedAchievementData(id);
return (
<div className="achievement" key={key}>
<FaTrophy />
<div className={'achievementContent' + (achieved ? ' achieved' : '')}>
<span>{name}</span>
<span className="subtitle">{description}</span>
</div>
</div>
);
};
const STATS_SECTION = 'modals.main.settings.sections.stats';
return (
<>
<Header title={variables.getMessage(`${STATS_SECTION}.title`)} report={false}>
<CustomActions>
<Button
type="settings"
onClick={() => this.downloadStats()}
icon={<MdDownload />}
label={variables.getMessage('widgets.background.download')}
/>
<Button
type="settings"
onClick={() => this.resetStats()}
icon={<MdRestartAlt />}
label={variables.getMessage('modals.main.settings.buttons.reset')}
/>
</CustomActions>
</Header>
<div className="stats">
<div className="statSection rightPanel">
<div className="statIcon">
<MdShowChart />
</div>
<div className="statGrid">
<div>
<span className="subtitle">
{variables.getMessage(`${STATS_SECTION}.sections.tabs_opened`)}{' '}
</span>
<span>{this.state.stats['tabs-opened'] || 0}</span>
</div>
<div>
<span className="subtitle">
{variables.getMessage(
'modals.main.settings.sections.stats.sections.backgrounds_favourited',
)}{' '}
</span>
<span>
{this.state.stats.feature
? this.state.stats.feature['background-favourite'] || 0
: 0}
</span>
</div>
<div>
<span className="subtitle">
{variables.getMessage(
'modals.main.settings.sections.stats.sections.backgrounds_downloaded',
)}{' '}
</span>
<span>
{this.state.stats.feature
? this.state.stats.feature['background-download'] || 0
: 0}
</span>
</div>
<div>
<span className="subtitle">
{variables.getMessage(
'modals.main.settings.sections.stats.sections.quotes_favourited',
)}{' '}
</span>
<span>
{this.state.stats.feature ? this.state.stats.feature['quoted-favourite'] || 0 : 0}
</span>
</div>
<div>
<span className="subtitle">
{variables.getMessage(
'modals.main.settings.sections.stats.sections.quicklinks_added',
)}{' '}
</span>
<span>
{this.state.stats.feature ? this.state.stats.feature['quicklink-add'] || 0 : 0}
</span>
</div>
<div>
<span className="subtitle">
{variables.getMessage(
'modals.main.settings.sections.stats.sections.settings_changed',
)}{' '}
</span>
<span>
{this.state.stats.setting ? Object.keys(this.state.stats.setting).length : 0}
</span>
</div>
<div>
<span className="subtitle">
{variables.getMessage(
'modals.main.settings.sections.stats.sections.addons_installed',
)}{' '}
</span>
<span>
{this.state.stats.marketplace ? this.state.stats.marketplace['install'] : 0}
</span>
</div>
</div>
</div>
<div className="statSection leftPanel">
<span className="title">{variables.getMessage(`${STATS_SECTION}.achievements`)}</span>
<br />
<span className="subtitle">
{variables.getMessage(`${STATS_SECTION}.unlocked`, {
count: this.getUnlockedCount() + '/' + this.state.achievements.length,
})}
</span>
</div>
<div className="achievements">
{this.state.achievements.map((achievement, index) => {
if (achievement.achieved) {
return achievementElement(index, achievement.id, achievement.achieved);
}
})}
</div>
</div>
</>
);
}
}
export { Stats as default, Stats };

View File

@@ -0,0 +1,8 @@
export * from './About';
export * from './Advanced';
export * from './Appearance';
export * from './Changelog';
export * from './Experimental';
export * from './Language';
export * from './Overview';
export * from './Stats';

View File

@@ -0,0 +1,31 @@
import { Suspense, lazy, memo } from 'react';
const Analog = lazy(() => import('react-clock'));
function ClockSkeleton() {
if (localStorage.getItem('timeType') === 'analogue') {
return (
<Suspense fallback={<></>}>
<div className="clockBackground">
<Analog
className="analogclock clock-container"
value={'2022-10-07T17:00:00+00:00'}
size={50}
/>
</div>
</Suspense>
);
} else if (localStorage.getItem('timeType') === 'percentageComplete') {
return <span className="new-clock clock-container clockSkeleton">68%</span>;
} else if (localStorage.getItem('timeType') === 'verticalClock') {
return (
<span className="new-clock clock-container" style={{ fontSize: '30px' }}>
<div className="hour">10</div>
<div className="minute">23</div>
</span>
);
} else {
return <span className="new-clock clock-container clockSkeleton">10:24</span>;
}
}
export default memo(ClockSkeleton);

View File

@@ -0,0 +1,7 @@
import { memo } from 'react';
function DateSkeleton() {
return <span className="date">Thursday January 1st</span>;
}
export default memo(DateSkeleton);

View File

@@ -0,0 +1,7 @@
import { memo } from 'react';
function GreetingSkeleton() {
return <span className="greeting">Good Morning</span>;
}
export default memo(GreetingSkeleton);

View File

@@ -0,0 +1,14 @@
import { memo } from 'react';
function MessageSkeleton() {
return (
<h2 className="message">
<span>
Message
<br />
</span>
</h2>
);
}
export default memo(MessageSkeleton);

View File

@@ -0,0 +1,26 @@
import { memo } from 'react';
import { FaDiscord, FaTwitter } from 'react-icons/fa';
import { SiGithubsponsors, SiOpencollective } from 'react-icons/si';
function QuicklinksSkeleton() {
return (
<div className="quickLinksSkeleton">
<div className="circles">
<div>
<FaDiscord />
</div>
<div>
<FaTwitter />
</div>
<div>
<SiGithubsponsors />
</div>
<div>
<SiOpencollective />
</div>
</div>
</div>
);
}
export default memo(QuicklinksSkeleton);

View File

@@ -0,0 +1,21 @@
import { memo } from 'react';
import { MdPerson } from 'react-icons/md';
function QuoteSkeleton() {
return (
<div className="quoteSkeleton">
<span className="subtitle">"Never gonna give you up"</span>
<div className="skeletonAuthor">
<div>
<MdPerson />
</div>
<div className="text">
<span className="title">Rick Astley</span>
<span className="subtitle">English singer-songwriter</span>
</div>
</div>
</div>
);
}
export default memo(QuoteSkeleton);