mirror of
https://github.com/mue/mue.git
synced 2026-07-18 14:34:12 +02:00
refactor: split quicklinks, move features to functional components
This commit is contained in:
@@ -1,27 +1,20 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } from 'react';
|
||||
import { memo, useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { MdStar, MdStarBorder } from 'react-icons/md';
|
||||
|
||||
class Favourite extends PureComponent {
|
||||
buttons = {
|
||||
favourited: <MdStar onClick={() => this.favourite()} className="topicons" />,
|
||||
unfavourited: <MdStarBorder onClick={() => this.favourite()} className="topicons" />,
|
||||
const Favourite = memo(({ tooltipText, credit, offline, pun }) => {
|
||||
const getInitialButton = () => {
|
||||
return localStorage.getItem('favourite') ? 'favourited' : 'unfavourited';
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
favourited: localStorage.getItem('favourite')
|
||||
? this.buttons.favourited
|
||||
: this.buttons.unfavourited,
|
||||
};
|
||||
}
|
||||
const [favourited, setFavourited] = useState(getInitialButton());
|
||||
const previousFavouritedRef = useRef(favourited);
|
||||
|
||||
async favourite() {
|
||||
const favourite = useCallback(async () => {
|
||||
if (localStorage.getItem('favourite')) {
|
||||
localStorage.removeItem('favourite');
|
||||
this.setState({ favourited: this.buttons.unfavourited });
|
||||
this.props.tooltipText(variables.getMessage('widgets.quote.favourite'));
|
||||
setFavourited('unfavourited');
|
||||
tooltipText(variables.getMessage('widgets.quote.favourite'));
|
||||
variables.stats.postEvent('feature', 'Background favourite');
|
||||
} else {
|
||||
const type = localStorage.getItem('backgroundType');
|
||||
@@ -71,12 +64,12 @@ class Favourite extends PureComponent {
|
||||
JSON.stringify({
|
||||
type,
|
||||
url,
|
||||
credit: this.props.credit || '',
|
||||
credit: credit || '',
|
||||
location: location?.innerText,
|
||||
camera: camera?.innerText,
|
||||
resolution: document.getElementById('infoResolution').textContent || '',
|
||||
offline: this.props.offline,
|
||||
pun: this.props.pun,
|
||||
offline,
|
||||
pun,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -84,39 +77,47 @@ class Favourite extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ favourited: this.buttons.favourited });
|
||||
this.props.tooltipText(variables.getMessage('widgets.quote.unfavourite'));
|
||||
setFavourited('favourited');
|
||||
tooltipText(variables.getMessage('widgets.quote.unfavourite'));
|
||||
variables.stats.postEvent('feature', 'Background unfavourite');
|
||||
}
|
||||
}
|
||||
}, [tooltipText, credit, offline, pun]);
|
||||
|
||||
componentDidMount() {
|
||||
this.updateTooltip();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.favourited !== this.state.favourited) {
|
||||
this.updateTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
updateTooltip() {
|
||||
if (this.props.tooltipText) {
|
||||
this.props.tooltipText(
|
||||
const updateTooltip = useCallback(() => {
|
||||
if (tooltipText) {
|
||||
tooltipText(
|
||||
localStorage.getItem('favourite')
|
||||
? variables.getMessage('widgets.quote.unfavourite')
|
||||
: variables.getMessage('widgets.quote.favourite'),
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [tooltipText]);
|
||||
|
||||
render() {
|
||||
if (localStorage.getItem('backgroundType') === 'colour') {
|
||||
return null;
|
||||
// componentDidMount
|
||||
useEffect(() => {
|
||||
updateTooltip();
|
||||
}, [updateTooltip]);
|
||||
|
||||
// componentDidUpdate - only when favourited changes
|
||||
useEffect(() => {
|
||||
if (previousFavouritedRef.current !== favourited) {
|
||||
updateTooltip();
|
||||
previousFavouritedRef.current = favourited;
|
||||
}
|
||||
}, [favourited, updateTooltip]);
|
||||
|
||||
return this.state.favourited;
|
||||
if (localStorage.getItem('backgroundType') === 'colour') {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const buttons = {
|
||||
favourited: <MdStar onClick={favourite} className="topicons" />,
|
||||
unfavourited: <MdStarBorder onClick={favourite} className="topicons" />,
|
||||
};
|
||||
|
||||
return buttons[favourited];
|
||||
});
|
||||
|
||||
Favourite.displayName = 'Favourite';
|
||||
|
||||
export default Favourite;
|
||||
|
||||
@@ -1,440 +1,229 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } from 'react';
|
||||
import { MdSource, MdOutlineKeyboardArrowRight, MdOutlineAutoAwesome } from 'react-icons/md';
|
||||
import { memo, useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { MdSource, MdOutlineAutoAwesome } from 'react-icons/md';
|
||||
|
||||
import { Header } from 'components/Layout/Settings';
|
||||
import { Checkbox, Dropdown, Slider, Radio, Text, ChipSelect } from 'components/Form/Settings';
|
||||
import { Row, Content, Action } from 'components/Layout/Settings/Item';
|
||||
//import Text from 'components/Form/Settings/Text/Text';
|
||||
import { Dropdown } from 'components/Form/Settings';
|
||||
|
||||
import ColourSettings from './Colour';
|
||||
import CustomSettings from './Custom';
|
||||
import APISettings from './sections/APISettings';
|
||||
import DisplaySettings from './sections/DisplaySettings';
|
||||
import EffectsSettings from './sections/EffectsSettings';
|
||||
import SourceSection from './sections/SourceSection';
|
||||
import NavigationCard from './sections/NavigationCard';
|
||||
|
||||
import values from 'utils/data/slider_values.json';
|
||||
import { APIQualityOptions, backgroundImageEffects, getBackgroundOptionItems } from './optionTypes';
|
||||
import { getBackgroundOptionItems } from './optionTypes';
|
||||
|
||||
class BackgroundOptions extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
backgroundType: localStorage.getItem('backgroundType') || 'api',
|
||||
backgroundFilter: localStorage.getItem('backgroundFilter') || 'none',
|
||||
backgroundCategories: [variables.getMessage('modals.main.loading')],
|
||||
backgroundAPI: localStorage.getItem('backgroundAPI') || 'mue',
|
||||
marketplaceEnabled: localStorage.getItem('photo_packs'),
|
||||
effects: false,
|
||||
backgroundSettingsSection: false,
|
||||
};
|
||||
this.controller = new AbortController();
|
||||
}
|
||||
const BackgroundOptions = memo(() => {
|
||||
const [backgroundType, setBackgroundType] = useState(
|
||||
localStorage.getItem('backgroundType') || 'api',
|
||||
);
|
||||
const [backgroundFilter, setBackgroundFilter] = useState(
|
||||
localStorage.getItem('backgroundFilter') || 'none',
|
||||
);
|
||||
const [backgroundCategories, setBackgroundCategories] = useState([
|
||||
variables.getMessage('modals.main.loading'),
|
||||
]);
|
||||
const [backgroundCategoriesOG, setBackgroundCategoriesOG] = useState([]);
|
||||
const [backgroundAPI, setBackgroundAPI] = useState(localStorage.getItem('backgroundAPI') || 'mue');
|
||||
const [marketplaceEnabled] = useState(localStorage.getItem('photo_packs'));
|
||||
const [effects, setEffects] = useState(false);
|
||||
const [backgroundSettingsSection, setBackgroundSettingsSection] = useState(false);
|
||||
|
||||
const controllerRef = useRef(null);
|
||||
|
||||
async getBackgroundCategories() {
|
||||
const getBackgroundCategories = useCallback(async () => {
|
||||
const data = await (
|
||||
await fetch(variables.constants.API_URL + '/images/categories', {
|
||||
signal: this.controller.signal,
|
||||
signal: controllerRef.current.signal,
|
||||
})
|
||||
).json();
|
||||
|
||||
if (this.controller.signal.aborted === true) {
|
||||
if (controllerRef.current.signal.aborted === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.backgroundAPI !== 'mue') {
|
||||
if (backgroundAPI !== 'mue') {
|
||||
// remove counts from unsplash categories
|
||||
data.forEach((category) => {
|
||||
delete category.count;
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
backgroundCategories: data,
|
||||
backgroundCategoriesOG: data,
|
||||
});
|
||||
}
|
||||
setBackgroundCategories(data);
|
||||
setBackgroundCategoriesOG(data);
|
||||
}, [backgroundAPI]);
|
||||
|
||||
updateAPI(e) {
|
||||
const updateAPI = useCallback((e) => {
|
||||
localStorage.setItem('nextImage', null);
|
||||
if (e === 'mue') {
|
||||
this.setState({
|
||||
backgroundCategories: this.state.backgroundCategoriesOG,
|
||||
backgroundAPI: 'mue',
|
||||
});
|
||||
setBackgroundCategories(backgroundCategoriesOG);
|
||||
setBackgroundAPI('mue');
|
||||
} else {
|
||||
const data = this.state.backgroundCategories;
|
||||
const data = [...backgroundCategories];
|
||||
data.forEach((category) => {
|
||||
delete category.count;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
backgroundAPI: 'unsplash',
|
||||
backgroundCategories: data,
|
||||
});
|
||||
setBackgroundAPI('unsplash');
|
||||
setBackgroundCategories(data);
|
||||
}
|
||||
}
|
||||
}, [backgroundCategories, backgroundCategoriesOG]);
|
||||
|
||||
useEffect(() => {
|
||||
controllerRef.current = new AbortController();
|
||||
|
||||
componentDidMount() {
|
||||
if (navigator.onLine === false || localStorage.getItem('offlineMode') === 'true') {
|
||||
return this.setState({
|
||||
backgroundCategories: [variables.getMessage('modals.update.offline.title')],
|
||||
});
|
||||
setBackgroundCategories([variables.getMessage('modals.update.offline.title')]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.getBackgroundCategories();
|
||||
}
|
||||
getBackgroundCategories();
|
||||
|
||||
componentWillUnmount() {
|
||||
// stop making requests
|
||||
this.controller.abort();
|
||||
}
|
||||
return () => {
|
||||
// stop making requests
|
||||
controllerRef.current.abort();
|
||||
};
|
||||
}, [getBackgroundCategories]);
|
||||
|
||||
render() {
|
||||
const APISettings = (
|
||||
<>
|
||||
<Row final={this.state.backgroundAPI === 'mue'}>
|
||||
<Content
|
||||
title={variables.getMessage('modals.main.settings.sections.background.api')}
|
||||
subtitle={variables.getMessage('modals.main.settings.sections.background.api_subtitle')}
|
||||
/>
|
||||
<Action>
|
||||
{this.state.backgroundCategories[0] === variables.getMessage('modals.main.loading') ? (
|
||||
<>
|
||||
<Dropdown
|
||||
label={variables.getMessage('modals.main.settings.sections.background.category')}
|
||||
name="apiCategories"
|
||||
items={[
|
||||
{
|
||||
value: 'loading',
|
||||
text: variables.getMessage('modals.main.loading'),
|
||||
},
|
||||
{
|
||||
value: 'loading',
|
||||
text: variables.getMessage('modals.main.loading'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<ChipSelect
|
||||
label={variables.getMessage('modals.main.settings.sections.background.categories')}
|
||||
options={this.state.backgroundCategories}
|
||||
name="apiCategories"
|
||||
/>
|
||||
)}
|
||||
<Dropdown
|
||||
label={variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.quality.title',
|
||||
)}
|
||||
name="apiQuality"
|
||||
element=".other"
|
||||
items={APIQualityOptions}
|
||||
/>
|
||||
<Radio
|
||||
title="API"
|
||||
options={[
|
||||
{
|
||||
name: 'Mue',
|
||||
value: 'mue',
|
||||
},
|
||||
{
|
||||
name: 'Unsplash',
|
||||
value: 'unsplash',
|
||||
},
|
||||
]}
|
||||
name="backgroundAPI"
|
||||
category="background"
|
||||
element="#backgroundImage"
|
||||
onChange={(e) => this.updateAPI(e)}
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
{this.state.backgroundAPI === 'unsplash' && (
|
||||
<Row final={true}>
|
||||
<Content
|
||||
title={variables.getMessage(
|
||||
'modals.main.settings.sections.background.unsplash.title',
|
||||
)}
|
||||
subtitle={variables.getMessage(
|
||||
'modals.main.settings.sections.background.unsplash.subtitle',
|
||||
)}
|
||||
/>
|
||||
<Action>
|
||||
<Text
|
||||
title={variables.getMessage('modals.main.settings.sections.background.unsplash.id')}
|
||||
subtitle={variables.getMessage(
|
||||
'modals.main.settings.sections.background.unsplash.id_subtitle',
|
||||
)}
|
||||
placeholder="e.g. 123456, 654321"
|
||||
name="unsplashCollections"
|
||||
category="background"
|
||||
element="#backgroundImage"
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
let backgroundSettings = APISettings;
|
||||
switch (this.state.backgroundType) {
|
||||
const getBackgroundSettings = () => {
|
||||
switch (backgroundType) {
|
||||
case 'custom':
|
||||
backgroundSettings = <CustomSettings />;
|
||||
break;
|
||||
return <CustomSettings />;
|
||||
case 'colour':
|
||||
backgroundSettings = <ColourSettings />;
|
||||
break;
|
||||
return <ColourSettings />;
|
||||
case 'random_colour':
|
||||
case 'random_gradient':
|
||||
backgroundSettings = <></>;
|
||||
break;
|
||||
return null;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
localStorage.getItem('photo_packs') &&
|
||||
this.state.backgroundType !== 'custom' &&
|
||||
this.state.backgroundType !== 'colour' &&
|
||||
this.state.backgroundType !== 'api'
|
||||
backgroundType !== 'custom' &&
|
||||
backgroundType !== 'colour' &&
|
||||
backgroundType !== 'api'
|
||||
) {
|
||||
backgroundSettings = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
const usingImage =
|
||||
this.state.backgroundType !== 'colour' &&
|
||||
this.state.backgroundType !== 'random_colour' &&
|
||||
this.state.backgroundType !== 'random_gradient';
|
||||
return (
|
||||
<APISettings
|
||||
backgroundAPI={backgroundAPI}
|
||||
backgroundCategories={backgroundCategories}
|
||||
onUpdateAPI={updateAPI}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
let header;
|
||||
if (this.state.effects === true) {
|
||||
header = (
|
||||
const usingImage =
|
||||
backgroundType !== 'colour' &&
|
||||
backgroundType !== 'random_colour' &&
|
||||
backgroundType !== 'random_gradient';
|
||||
|
||||
const showEffects = backgroundType === 'api' || backgroundType === 'custom' || marketplaceEnabled;
|
||||
|
||||
const getHeader = () => {
|
||||
if (effects) {
|
||||
return (
|
||||
<Header
|
||||
title={variables.getMessage('modals.main.settings.sections.background.title')}
|
||||
secondaryTitle={variables.getMessage(
|
||||
'modals.main.settings.sections.background.effects.title',
|
||||
)}
|
||||
goBack={() => this.setState({ effects: false })}
|
||||
goBack={() => setEffects(false)}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.backgroundSettingsSection === true) {
|
||||
header = (
|
||||
}
|
||||
|
||||
if (backgroundSettingsSection) {
|
||||
return (
|
||||
<Header
|
||||
title={variables.getMessage('modals.main.settings.sections.background.title')}
|
||||
secondaryTitle={variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.title',
|
||||
)}
|
||||
goBack={() => this.setState({ backgroundSettingsSection: false })}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
header = (
|
||||
<Header
|
||||
title={variables.getMessage('modals.main.settings.sections.background.title')}
|
||||
setting="background"
|
||||
category="background"
|
||||
element="#backgroundImage"
|
||||
goBack={() => setBackgroundSettingsSection(false)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{header}
|
||||
{this.state.backgroundSettingsSection !== true && this.state.effects !== true ? (
|
||||
<>
|
||||
<div
|
||||
className="moreSettings"
|
||||
onClick={() => this.setState({ backgroundSettingsSection: true })}
|
||||
>
|
||||
<div className="left">
|
||||
<MdSource />
|
||||
<div className="content">
|
||||
<span className="title">
|
||||
{variables.getMessage('modals.main.settings.sections.background.source.title')}
|
||||
</span>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.subtitle',
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="action">
|
||||
<Dropdown
|
||||
label={variables.getMessage(
|
||||
'modals.main.settings.sections.background.type.title',
|
||||
)}
|
||||
name="backgroundType"
|
||||
onChange={(value) => this.setState({ backgroundType: value })}
|
||||
category="background"
|
||||
items={getBackgroundOptionItems(this.state.marketplaceEnabled)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{this.state.backgroundType === 'api' ||
|
||||
this.state.backgroundType === 'custom' ||
|
||||
this.state.marketplaceEnabled ? (
|
||||
<>
|
||||
<div className="moreSettings" onClick={() => this.setState({ effects: true })}>
|
||||
<div className="left">
|
||||
<MdOutlineAutoAwesome />
|
||||
<div className="content">
|
||||
<span className="title">
|
||||
{variables.getMessage(
|
||||
'modals.main.settings.sections.background.effects.title',
|
||||
)}
|
||||
</span>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage(
|
||||
'modals.main.settings.sections.background.effects.subtitle',
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="action">
|
||||
{' '}
|
||||
<MdOutlineKeyboardArrowRight />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
{this.state.backgroundSettingsSection !== true &&
|
||||
this.state.effects !== true &&
|
||||
(this.state.backgroundType === 'api' ||
|
||||
this.state.backgroundType === 'custom' ||
|
||||
this.state.marketplaceEnabled) ? (
|
||||
<Row final={true}>
|
||||
<Content
|
||||
title={variables.getMessage('modals.main.settings.sections.background.display')}
|
||||
subtitle={variables.getMessage(
|
||||
'modals.main.settings.sections.background.display_subtitle',
|
||||
<Header
|
||||
title={variables.getMessage('modals.main.settings.sections.background.title')}
|
||||
setting="background"
|
||||
category="background"
|
||||
element="#backgroundImage"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{getHeader()}
|
||||
|
||||
{!backgroundSettingsSection && !effects && (
|
||||
<>
|
||||
<NavigationCard
|
||||
icon={MdSource}
|
||||
title={variables.getMessage('modals.main.settings.sections.background.source.title')}
|
||||
subtitle={variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.subtitle',
|
||||
)}
|
||||
onClick={() => setBackgroundSettingsSection(true)}
|
||||
action={
|
||||
<Dropdown
|
||||
label={variables.getMessage('modals.main.settings.sections.background.type.title')}
|
||||
name="backgroundType"
|
||||
onChange={(value) => setBackgroundType(value)}
|
||||
category="background"
|
||||
items={getBackgroundOptionItems(marketplaceEnabled)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{showEffects && (
|
||||
<NavigationCard
|
||||
icon={MdOutlineAutoAwesome}
|
||||
title={variables.getMessage(
|
||||
'modals.main.settings.sections.background.effects.title',
|
||||
)}
|
||||
/>
|
||||
<Action>
|
||||
<Checkbox
|
||||
name="bgtransition"
|
||||
text={variables.getMessage('modals.main.settings.sections.background.transition')}
|
||||
element=".other"
|
||||
disabled={!usingImage}
|
||||
/>
|
||||
<Checkbox
|
||||
name="photoInformation"
|
||||
text={variables.getMessage(
|
||||
'modals.main.settings.sections.background.photo_information',
|
||||
)}
|
||||
element=".other"
|
||||
/>
|
||||
<Checkbox
|
||||
name="photoMap"
|
||||
text={variables.getMessage('modals.main.settings.sections.background.show_map')}
|
||||
element=".other"
|
||||
disabled={!usingImage}
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
) : null}
|
||||
{this.state.backgroundSettingsSection && (
|
||||
<>
|
||||
<Row
|
||||
final={
|
||||
this.state.backgroundType === 'random_colour' ||
|
||||
this.state.backgroundType === 'random_gradient'
|
||||
}
|
||||
>
|
||||
<Content
|
||||
title={variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.title',
|
||||
)}
|
||||
subtitle={variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.subtitle',
|
||||
)}
|
||||
/>
|
||||
<Action>
|
||||
<Dropdown
|
||||
label={variables.getMessage(
|
||||
'modals.main.settings.sections.background.type.title',
|
||||
)}
|
||||
name="backgroundType"
|
||||
onChange={(value) => this.setState({ backgroundType: value })}
|
||||
category="background"
|
||||
items={getBackgroundOptionItems(this.state.marketplaceEnabled)}
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
{/* todo: ideally refactor all of this file, but we need interval to appear on marketplace too */}
|
||||
{backgroundSettings}
|
||||
</>
|
||||
)}
|
||||
{(this.state.backgroundType === 'api' ||
|
||||
this.state.backgroundType === 'custom' ||
|
||||
this.state.marketplaceEnabled) &&
|
||||
this.state.effects ? (
|
||||
<Row final={true}>
|
||||
<Content
|
||||
title={variables.getMessage('modals.main.settings.sections.background.effects.title')}
|
||||
subtitle={variables.getMessage(
|
||||
'modals.main.settings.sections.background.effects.subtitle',
|
||||
)}
|
||||
onClick={() => setEffects(true)}
|
||||
/>
|
||||
<Action>
|
||||
<Slider
|
||||
title={variables.getMessage(
|
||||
'modals.main.settings.sections.background.effects.blur',
|
||||
)}
|
||||
name="blur"
|
||||
min="0"
|
||||
max="100"
|
||||
default="0"
|
||||
display="%"
|
||||
marks={values.background}
|
||||
category="backgroundeffect"
|
||||
element="#backgroundImage"
|
||||
/>
|
||||
<Slider
|
||||
title={variables.getMessage(
|
||||
'modals.main.settings.sections.background.effects.brightness',
|
||||
)}
|
||||
name="brightness"
|
||||
min="0"
|
||||
max="100"
|
||||
default="90"
|
||||
display="%"
|
||||
marks={values.background}
|
||||
category="backgroundeffect"
|
||||
element="#backgroundImage"
|
||||
/>
|
||||
<Dropdown
|
||||
label={variables.getMessage(
|
||||
'modals.main.settings.sections.background.effects.filters.title',
|
||||
)}
|
||||
name="backgroundFilter"
|
||||
onChange={(value) => this.setState({ backgroundFilter: value })}
|
||||
category="backgroundeffect"
|
||||
element="#backgroundImage"
|
||||
items={backgroundImageEffects}
|
||||
/>
|
||||
{this.state.backgroundFilter !== 'none' && (
|
||||
<Slider
|
||||
title={variables.getMessage(
|
||||
'modals.main.settings.sections.background.effects.filters.amount',
|
||||
)}
|
||||
name="backgroundFilterAmount"
|
||||
min="0"
|
||||
max="100"
|
||||
default="0"
|
||||
display="%"
|
||||
marks={values.background}
|
||||
category="backgroundeffect"
|
||||
element="#backgroundImage"
|
||||
/>
|
||||
)}
|
||||
</Action>
|
||||
</Row>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!backgroundSettingsSection && !effects && showEffects && (
|
||||
<DisplaySettings usingImage={usingImage} />
|
||||
)}
|
||||
|
||||
{backgroundSettingsSection && (
|
||||
<>
|
||||
<SourceSection
|
||||
backgroundType={backgroundType}
|
||||
marketplaceEnabled={marketplaceEnabled}
|
||||
onTypeChange={(value) => setBackgroundType(value)}
|
||||
/>
|
||||
{getBackgroundSettings()}
|
||||
</>
|
||||
)}
|
||||
|
||||
{showEffects && effects && (
|
||||
<EffectsSettings
|
||||
backgroundFilter={backgroundFilter}
|
||||
onFilterChange={(value) => setBackgroundFilter(value)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
BackgroundOptions.displayName = 'BackgroundOptions';
|
||||
|
||||
export { BackgroundOptions as default, BackgroundOptions };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent, createRef } from 'react';
|
||||
import { memo, useRef, useState, useEffect, useCallback } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
MdCancel,
|
||||
@@ -19,68 +19,245 @@ import Modal from 'react-modal';
|
||||
|
||||
import CustomURLModal from './CustomURLModal';
|
||||
|
||||
export default class CustomSettings extends PureComponent {
|
||||
getMessage = (text, obj) => variables.getMessage(text, obj || {});
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
customBackground: this.getCustom(),
|
||||
customURLModal: false,
|
||||
urlError: '',
|
||||
};
|
||||
this.customDnd = createRef(null);
|
||||
const getCustom = () => {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(localStorage.getItem('customBackground'));
|
||||
} catch (e) {
|
||||
data = [localStorage.getItem('customBackground')];
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
resetCustom = () => {
|
||||
const CustomSettings = memo(() => {
|
||||
const [customBackground, setCustomBackground] = useState(getCustom());
|
||||
const [customURLModal, setCustomURLModal] = useState(false);
|
||||
const [urlError, setUrlError] = useState('');
|
||||
const [currentBackgroundIndex, setCurrentBackgroundIndex] = useState(0);
|
||||
const customDnd = useRef(null);
|
||||
|
||||
const resetCustom = useCallback(() => {
|
||||
localStorage.setItem('customBackground', '[]');
|
||||
this.setState({
|
||||
customBackground: [],
|
||||
});
|
||||
setCustomBackground([]);
|
||||
toast(variables.getMessage('toasts.reset'));
|
||||
EventBus.emit('refresh', 'background');
|
||||
};
|
||||
}, []);
|
||||
|
||||
customBackground(e, index) {
|
||||
const handleCustomBackground = useCallback((e, index) => {
|
||||
const result = e.target.result;
|
||||
|
||||
const customBackground = this.state.customBackground;
|
||||
customBackground[index || customBackground.length] = result;
|
||||
|
||||
this.setState({
|
||||
customBackground,
|
||||
setCustomBackground((prev) => {
|
||||
const updated = [...prev];
|
||||
updated[index || updated.length] = result;
|
||||
localStorage.setItem('customBackground', JSON.stringify(updated));
|
||||
document.querySelector('.reminder-info').style.display = 'flex';
|
||||
localStorage.setItem('showReminder', true);
|
||||
return updated;
|
||||
});
|
||||
}, []);
|
||||
|
||||
this.forceUpdate();
|
||||
const modifyCustomBackground = useCallback((type, index) => {
|
||||
setCustomBackground((prev) => {
|
||||
const updated = [...prev];
|
||||
if (type === 'add') {
|
||||
updated.push('');
|
||||
} else {
|
||||
updated.splice(index, 1);
|
||||
}
|
||||
localStorage.setItem('customBackground', JSON.stringify(updated));
|
||||
document.querySelector('.reminder-info').style.display = 'flex';
|
||||
localStorage.setItem('showReminder', true);
|
||||
return updated;
|
||||
});
|
||||
}, []);
|
||||
|
||||
localStorage.setItem('customBackground', JSON.stringify(customBackground));
|
||||
document.querySelector('.reminder-info').style.display = 'flex';
|
||||
localStorage.setItem('showReminder', true);
|
||||
}
|
||||
const uploadCustomBackground = useCallback(() => {
|
||||
const newIndex = customBackground.length;
|
||||
document.getElementById('bg-input').setAttribute('index', newIndex);
|
||||
document.getElementById('bg-input').click();
|
||||
setCurrentBackgroundIndex(newIndex);
|
||||
}, [customBackground.length]);
|
||||
|
||||
modifyCustomBackground(type, index) {
|
||||
const customBackground = this.state.customBackground;
|
||||
if (type === 'add') {
|
||||
customBackground.push('');
|
||||
} else {
|
||||
customBackground.splice(index, 1);
|
||||
const addCustomURL = useCallback((e) => {
|
||||
// regex: https://ihateregex.io/expr/url/
|
||||
const urlRegex =
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._~#=]{1,256}\.[a-zA-Z0-9()]{1,63}\b([-a-zA-Z0-9()!@:%_.~#?&=]*)/;
|
||||
if (urlRegex.test(e) === false) {
|
||||
return setUrlError(variables.getMessage('widgets.quicklinks.url_error'));
|
||||
}
|
||||
|
||||
this.setState({
|
||||
customBackground,
|
||||
});
|
||||
this.forceUpdate();
|
||||
const newIndex = customBackground.length;
|
||||
setCustomURLModal(false);
|
||||
setCurrentBackgroundIndex(newIndex);
|
||||
handleCustomBackground({ target: { result: e } }, newIndex);
|
||||
}, [customBackground.length, handleCustomBackground]);
|
||||
|
||||
localStorage.setItem('customBackground', JSON.stringify(customBackground));
|
||||
document.querySelector('.reminder-info').style.display = 'flex';
|
||||
localStorage.setItem('showReminder', true);
|
||||
}
|
||||
useEffect(() => {
|
||||
const dnd = customDnd.current;
|
||||
if (!dnd) return;
|
||||
|
||||
videoCustomSettings = () => {
|
||||
const hasVideo = this.state.customBackground.filter((bg) => videoCheck(bg));
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
if (hasVideo.length > 0) {
|
||||
return (
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
const file = e.dataTransfer.files[0];
|
||||
const settings = {};
|
||||
|
||||
Object.keys(localStorage).forEach((key) => {
|
||||
settings[key] = localStorage.getItem(key);
|
||||
});
|
||||
|
||||
const settingsSize = new TextEncoder().encode(JSON.stringify(settings)).length;
|
||||
|
||||
if (videoCheck(file.type) === true) {
|
||||
if (settingsSize + file.size > 4850000) {
|
||||
return toast(variables.getMessage('toasts.no_storage'));
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
handleCustomBackground({ target: { result: reader.result } }, currentBackgroundIndex);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
return;
|
||||
}
|
||||
|
||||
compressAccurately(file, {
|
||||
size: 450,
|
||||
accuracy: 0.9,
|
||||
}).then(async (res) => {
|
||||
if (settingsSize + res.size > 4850000) {
|
||||
return toast(variables.getMessage('toasts.no_storage'));
|
||||
}
|
||||
|
||||
handleCustomBackground(
|
||||
{
|
||||
target: {
|
||||
result: await filetoDataURL(res),
|
||||
},
|
||||
},
|
||||
currentBackgroundIndex,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
dnd.ondragover = handleDragOver;
|
||||
dnd.ondragenter = handleDragOver;
|
||||
dnd.ondrop = handleDrop;
|
||||
|
||||
return () => {
|
||||
if (dnd) {
|
||||
dnd.ondragover = null;
|
||||
dnd.ondragenter = null;
|
||||
dnd.ondrop = null;
|
||||
}
|
||||
};
|
||||
}, [currentBackgroundIndex, handleCustomBackground]);
|
||||
|
||||
const hasVideo = customBackground.filter((bg) => videoCheck(bg)).length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dropzone" ref={customDnd}>
|
||||
<div className="imagesTopBar">
|
||||
<div>
|
||||
<MdAddPhotoAlternate />
|
||||
<div>
|
||||
<span className="title">
|
||||
{variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.custom_title',
|
||||
)}
|
||||
</span>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.custom_description',
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="topbarbuttons">
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={uploadCustomBackground}
|
||||
icon={<MdOutlineFileUpload />}
|
||||
label={variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.upload',
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => setCustomURLModal(true)}
|
||||
icon={<MdAddLink />}
|
||||
label={variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.add_url',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dropzone-content">
|
||||
{customBackground.length > 0 ? (
|
||||
<div className="images-row">
|
||||
{customBackground.map((url, index) => (
|
||||
<div key={index}>
|
||||
<img
|
||||
alt={'Custom background ' + (index || 0)}
|
||||
src={`${!videoCheck(url) ? customBackground[index] : ''}`}
|
||||
/>
|
||||
{videoCheck(url) && <MdPersonalVideo className="customvideoicon" />}
|
||||
{customBackground.length > 0 && (
|
||||
<Tooltip
|
||||
title={variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.remove',
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => modifyCustomBackground('remove', index)}
|
||||
icon={<MdCancel />}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="photosEmpty">
|
||||
<div className="emptyNewMessage">
|
||||
<MdAddPhotoAlternate />
|
||||
<span className="title">
|
||||
{variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.drop_to_upload',
|
||||
)}
|
||||
</span>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.formats',
|
||||
{
|
||||
list: 'jpeg, png, webp, webm, gif, mp4, webm, ogg',
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={uploadCustomBackground}
|
||||
icon={<MdFolder />}
|
||||
label={variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.select',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<FileUpload
|
||||
id="bg-input"
|
||||
accept="image/jpeg, image/png, image/webp, image/webm, image/gif, video/mp4, video/webm, video/ogg"
|
||||
loadFunction={(e) => handleCustomBackground(e, currentBackgroundIndex)}
|
||||
/>
|
||||
{hasVideo && (
|
||||
<>
|
||||
<Checkbox
|
||||
name="backgroundVideoLoop"
|
||||
@@ -95,212 +272,25 @@ export default class CustomSettings extends PureComponent {
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
getCustom() {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(localStorage.getItem('customBackground'));
|
||||
} catch (e) {
|
||||
data = [localStorage.getItem('customBackground')];
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
uploadCustomBackground() {
|
||||
document.getElementById('bg-input').setAttribute('index', this.state.customBackground.length);
|
||||
document.getElementById('bg-input').click();
|
||||
// to fix loadFunction
|
||||
this.setState({
|
||||
currentBackgroundIndex: this.state.customBackground.length,
|
||||
});
|
||||
}
|
||||
|
||||
addCustomURL(e) {
|
||||
// regex: https://ihateregex.io/expr/url/
|
||||
|
||||
const urlRegex =
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._~#=]{1,256}\.[a-zA-Z0-9()]{1,63}\b([-a-zA-Z0-9()!@:%_.~#?&=]*)/;
|
||||
if (urlRegex.test(e) === false) {
|
||||
return this.setState({
|
||||
urlError: variables.getMessage('widgets.quicklinks.url_error'),
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
customURLModal: false,
|
||||
currentBackgroundIndex: this.state.customBackground.length,
|
||||
});
|
||||
this.customBackground({ target: { value: e } }, true, this.state.customBackground.length);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const dnd = this.customDnd.current;
|
||||
dnd.ondragover = dnd.ondragenter = (e) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// todo: make this get from FileUpload.jsx to prevent duplication
|
||||
dnd.ondrop = (e) => {
|
||||
e.preventDefault();
|
||||
const file = e.dataTransfer.files[0];
|
||||
const settings = {};
|
||||
|
||||
Object.keys(localStorage).forEach((key) => {
|
||||
settings[key] = localStorage.getItem(key);
|
||||
});
|
||||
|
||||
const settingsSize = new TextEncoder().encode(JSON.stringify(settings)).length;
|
||||
if (videoCheck(file.type) === true) {
|
||||
if (settingsSize + file.size > 4850000) {
|
||||
return toast(variables.getMessage('toasts.no_storage'));
|
||||
}
|
||||
|
||||
return this.customBackground(file, this.state.currentBackgroundIndex);
|
||||
}
|
||||
|
||||
compressAccurately(file, {
|
||||
size: 450,
|
||||
accuracy: 0.9,
|
||||
}).then(async (res) => {
|
||||
if (settingsSize + res.size > 4850000) {
|
||||
return toast(variables.getMessage('toasts.no_storage'));
|
||||
}
|
||||
|
||||
this.customBackground(
|
||||
{
|
||||
target: {
|
||||
result: await filetoDataURL(res),
|
||||
},
|
||||
},
|
||||
this.state.currentBackgroundIndex,
|
||||
);
|
||||
});
|
||||
e.preventDefault();
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<div className="dropzone" ref={this.customDnd}>
|
||||
<div className="imagesTopBar">
|
||||
<div>
|
||||
<MdAddPhotoAlternate />
|
||||
<div>
|
||||
<span className="title">
|
||||
{variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.custom_title',
|
||||
)}
|
||||
</span>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.custom_description',
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="topbarbuttons">
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.uploadCustomBackground()}
|
||||
icon={<MdOutlineFileUpload />}
|
||||
label={variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.upload',
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.setState({ customURLModal: true })}
|
||||
icon={<MdAddLink />}
|
||||
label={variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.add_url',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dropzone-content">
|
||||
{this.state.customBackground.length > 0 ? (
|
||||
<div className="images-row">
|
||||
{this.state.customBackground.map((url, index) => (
|
||||
<div key={index}>
|
||||
<img
|
||||
alt={'Custom background ' + (index || 0)}
|
||||
src={`${!videoCheck(url) ? this.state.customBackground[index] : ''}`}
|
||||
/>
|
||||
{videoCheck(url) && <MdPersonalVideo className="customvideoicon" />}
|
||||
{this.state.customBackground.length > 0 && (
|
||||
<Tooltip
|
||||
title={variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.remove',
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.modifyCustomBackground('remove', index)}
|
||||
icon={<MdCancel />}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="photosEmpty">
|
||||
<div className="emptyNewMessage">
|
||||
<MdAddPhotoAlternate />
|
||||
<span className="title">
|
||||
{variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.drop_to_upload',
|
||||
)}
|
||||
</span>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.formats',
|
||||
{
|
||||
list: 'jpeg, png, webp, webm, gif, mp4, webm, ogg',
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.uploadCustomBackground()}
|
||||
icon={<MdFolder />}
|
||||
label={variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.select',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<FileUpload
|
||||
id="bg-input"
|
||||
accept="image/jpeg, image/png, image/webp, image/webm, image/gif, video/mp4, video/webm, video/ogg"
|
||||
loadFunction={(e) => this.customBackground(e, this.state.currentBackgroundIndex)}
|
||||
)}
|
||||
<Modal
|
||||
closeTimeoutMS={100}
|
||||
onRequestClose={() => setCustomURLModal(false)}
|
||||
isOpen={customURLModal}
|
||||
className="Modal resetmodal mainModal"
|
||||
overlayClassName="Overlay resetoverlay"
|
||||
ariaHideApp={false}
|
||||
>
|
||||
<CustomURLModal
|
||||
modalClose={addCustomURL}
|
||||
urlError={urlError}
|
||||
modalCloseOnly={() => setCustomURLModal(false)}
|
||||
/>
|
||||
{this.videoCustomSettings()}
|
||||
<Modal
|
||||
closeTimeoutMS={100}
|
||||
onRequestClose={() => this.setState({ customURLModal: false })}
|
||||
isOpen={this.state.customURLModal}
|
||||
className="Modal resetmodal mainModal"
|
||||
overlayClassName="Overlay resetoverlay"
|
||||
ariaHideApp={false}
|
||||
>
|
||||
<CustomURLModal
|
||||
modalClose={(e) => this.addCustomURL(e)}
|
||||
urlError={this.state.urlError}
|
||||
modalCloseOnly={() => this.setState({ customURLModal: false })}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
CustomSettings.displayName = 'CustomSettings';
|
||||
|
||||
export default CustomSettings;
|
||||
|
||||
92
src/features/background/options/sections/APISettings.jsx
Normal file
92
src/features/background/options/sections/APISettings.jsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import variables from 'config/variables';
|
||||
import { Dropdown, Radio, Text, ChipSelect } from 'components/Form/Settings';
|
||||
import { Row, Content, Action } from 'components/Layout/Settings/Item';
|
||||
import { APIQualityOptions } from '../optionTypes';
|
||||
|
||||
const APISettings = ({ backgroundAPI, backgroundCategories, onUpdateAPI }) => {
|
||||
return (
|
||||
<>
|
||||
<Row final={backgroundAPI === 'mue'}>
|
||||
<Content
|
||||
title={variables.getMessage('modals.main.settings.sections.background.api')}
|
||||
subtitle={variables.getMessage('modals.main.settings.sections.background.api_subtitle')}
|
||||
/>
|
||||
<Action>
|
||||
{backgroundCategories[0] === variables.getMessage('modals.main.loading') ? (
|
||||
<>
|
||||
<Dropdown
|
||||
label={variables.getMessage('modals.main.settings.sections.background.category')}
|
||||
name="apiCategories"
|
||||
items={[
|
||||
{
|
||||
value: 'loading',
|
||||
text: variables.getMessage('modals.main.loading'),
|
||||
},
|
||||
{
|
||||
value: 'loading',
|
||||
text: variables.getMessage('modals.main.loading'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<ChipSelect
|
||||
label={variables.getMessage('modals.main.settings.sections.background.categories')}
|
||||
options={backgroundCategories}
|
||||
name="apiCategories"
|
||||
/>
|
||||
)}
|
||||
<Dropdown
|
||||
label={variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.quality.title',
|
||||
)}
|
||||
name="apiQuality"
|
||||
element=".other"
|
||||
items={APIQualityOptions}
|
||||
/>
|
||||
<Radio
|
||||
title="API"
|
||||
options={[
|
||||
{
|
||||
name: 'Mue',
|
||||
value: 'mue',
|
||||
},
|
||||
{
|
||||
name: 'Unsplash',
|
||||
value: 'unsplash',
|
||||
},
|
||||
]}
|
||||
name="backgroundAPI"
|
||||
category="background"
|
||||
element="#backgroundImage"
|
||||
onChange={onUpdateAPI}
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
{backgroundAPI === 'unsplash' && (
|
||||
<Row final={true}>
|
||||
<Content
|
||||
title={variables.getMessage('modals.main.settings.sections.background.unsplash.title')}
|
||||
subtitle={variables.getMessage(
|
||||
'modals.main.settings.sections.background.unsplash.subtitle',
|
||||
)}
|
||||
/>
|
||||
<Action>
|
||||
<Text
|
||||
title={variables.getMessage('modals.main.settings.sections.background.unsplash.id')}
|
||||
subtitle={variables.getMessage(
|
||||
'modals.main.settings.sections.background.unsplash.id_subtitle',
|
||||
)}
|
||||
placeholder="e.g. 123456, 654321"
|
||||
name="unsplashCollections"
|
||||
category="background"
|
||||
element="#backgroundImage"
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default APISettings;
|
||||
37
src/features/background/options/sections/DisplaySettings.jsx
Normal file
37
src/features/background/options/sections/DisplaySettings.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import variables from 'config/variables';
|
||||
import { Checkbox } from 'components/Form/Settings';
|
||||
import { Row, Content, Action } from 'components/Layout/Settings/Item';
|
||||
|
||||
const DisplaySettings = ({ usingImage }) => {
|
||||
return (
|
||||
<Row final={true}>
|
||||
<Content
|
||||
title={variables.getMessage('modals.main.settings.sections.background.display')}
|
||||
subtitle={variables.getMessage(
|
||||
'modals.main.settings.sections.background.display_subtitle',
|
||||
)}
|
||||
/>
|
||||
<Action>
|
||||
<Checkbox
|
||||
name="bgtransition"
|
||||
text={variables.getMessage('modals.main.settings.sections.background.transition')}
|
||||
element=".other"
|
||||
disabled={!usingImage}
|
||||
/>
|
||||
<Checkbox
|
||||
name="photoInformation"
|
||||
text={variables.getMessage('modals.main.settings.sections.background.photo_information')}
|
||||
element=".other"
|
||||
/>
|
||||
<Checkbox
|
||||
name="photoMap"
|
||||
text={variables.getMessage('modals.main.settings.sections.background.show_map')}
|
||||
element=".other"
|
||||
disabled={!usingImage}
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisplaySettings;
|
||||
71
src/features/background/options/sections/EffectsSettings.jsx
Normal file
71
src/features/background/options/sections/EffectsSettings.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import variables from 'config/variables';
|
||||
import { Dropdown, Slider } from 'components/Form/Settings';
|
||||
import { Row, Content, Action } from 'components/Layout/Settings/Item';
|
||||
import values from 'utils/data/slider_values.json';
|
||||
import { backgroundImageEffects } from '../optionTypes';
|
||||
|
||||
const EffectsSettings = ({ backgroundFilter, onFilterChange }) => {
|
||||
return (
|
||||
<Row final={true}>
|
||||
<Content
|
||||
title={variables.getMessage('modals.main.settings.sections.background.effects.title')}
|
||||
subtitle={variables.getMessage(
|
||||
'modals.main.settings.sections.background.effects.subtitle',
|
||||
)}
|
||||
/>
|
||||
<Action>
|
||||
<Slider
|
||||
title={variables.getMessage('modals.main.settings.sections.background.effects.blur')}
|
||||
name="blur"
|
||||
min="0"
|
||||
max="100"
|
||||
default="0"
|
||||
display="%"
|
||||
marks={values.background}
|
||||
category="backgroundeffect"
|
||||
element="#backgroundImage"
|
||||
/>
|
||||
<Slider
|
||||
title={variables.getMessage(
|
||||
'modals.main.settings.sections.background.effects.brightness',
|
||||
)}
|
||||
name="brightness"
|
||||
min="0"
|
||||
max="100"
|
||||
default="90"
|
||||
display="%"
|
||||
marks={values.background}
|
||||
category="backgroundeffect"
|
||||
element="#backgroundImage"
|
||||
/>
|
||||
<Dropdown
|
||||
label={variables.getMessage(
|
||||
'modals.main.settings.sections.background.effects.filters.title',
|
||||
)}
|
||||
name="backgroundFilter"
|
||||
onChange={onFilterChange}
|
||||
category="backgroundeffect"
|
||||
element="#backgroundImage"
|
||||
items={backgroundImageEffects}
|
||||
/>
|
||||
{backgroundFilter !== 'none' && (
|
||||
<Slider
|
||||
title={variables.getMessage(
|
||||
'modals.main.settings.sections.background.effects.filters.amount',
|
||||
)}
|
||||
name="backgroundFilterAmount"
|
||||
min="0"
|
||||
max="100"
|
||||
default="0"
|
||||
display="%"
|
||||
marks={values.background}
|
||||
category="backgroundeffect"
|
||||
element="#backgroundImage"
|
||||
/>
|
||||
)}
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default EffectsSettings;
|
||||
20
src/features/background/options/sections/NavigationCard.jsx
Normal file
20
src/features/background/options/sections/NavigationCard.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { MdOutlineKeyboardArrowRight } from 'react-icons/md';
|
||||
|
||||
const NavigationCard = ({ icon: Icon, title, subtitle, onClick, action }) => {
|
||||
return (
|
||||
<div className="moreSettings" onClick={onClick}>
|
||||
<div className="left">
|
||||
<Icon />
|
||||
<div className="content">
|
||||
<span className="title">{title}</span>
|
||||
<span className="subtitle">{subtitle}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="action">
|
||||
{action || <MdOutlineKeyboardArrowRight />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavigationCard;
|
||||
30
src/features/background/options/sections/SourceSection.jsx
Normal file
30
src/features/background/options/sections/SourceSection.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import variables from 'config/variables';
|
||||
import { Dropdown } from 'components/Form/Settings';
|
||||
import { Row, Content, Action } from 'components/Layout/Settings/Item';
|
||||
import { getBackgroundOptionItems } from '../optionTypes';
|
||||
|
||||
const SourceSection = ({ backgroundType, marketplaceEnabled, onTypeChange }) => {
|
||||
return (
|
||||
<Row
|
||||
final={backgroundType === 'random_colour' || backgroundType === 'random_gradient'}
|
||||
>
|
||||
<Content
|
||||
title={variables.getMessage('modals.main.settings.sections.background.source.title')}
|
||||
subtitle={variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.subtitle',
|
||||
)}
|
||||
/>
|
||||
<Action>
|
||||
<Dropdown
|
||||
label={variables.getMessage('modals.main.settings.sections.background.type.title')}
|
||||
name="backgroundType"
|
||||
onChange={onTypeChange}
|
||||
category="background"
|
||||
items={getBackgroundOptionItems(marketplaceEnabled)}
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default SourceSection;
|
||||
@@ -1,5 +1,5 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } from 'react';
|
||||
import { memo, useState, useEffect, useCallback } from 'react';
|
||||
import { MdUpdate, MdOutlineExtensionOff, MdSendTimeExtension } from 'react-icons/md';
|
||||
import { toast } from 'react-toastify';
|
||||
import Modal from 'react-modal';
|
||||
@@ -13,37 +13,32 @@ import { Button } from 'components/Elements';
|
||||
|
||||
import { install, uninstall, urlParser } from 'utils/marketplace';
|
||||
|
||||
export default class Added extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
installed: JSON.parse(localStorage.getItem('installed')),
|
||||
item: {},
|
||||
button: '',
|
||||
showFailed: false,
|
||||
failedReason: '',
|
||||
};
|
||||
this.buttons = {
|
||||
uninstall: (
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.uninstall()}
|
||||
label={variables.getMessage('modals.main.marketplace.product.buttons.remove')}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
const Added = memo(() => {
|
||||
const [installed, setInstalled] = useState(JSON.parse(localStorage.getItem('installed')));
|
||||
const [item, setItem] = useState({});
|
||||
const [showFailed, setShowFailed] = useState(false);
|
||||
const [failedReason, setFailedReason] = useState('');
|
||||
|
||||
installAddon(input) {
|
||||
let failedReason = '';
|
||||
const uninstallItem = useCallback(() => {
|
||||
uninstall(item.type, item.display_name);
|
||||
toast(variables.getMessage('toasts.uninstalled'));
|
||||
|
||||
setItem({});
|
||||
setInstalled(JSON.parse(localStorage.getItem('installed')));
|
||||
|
||||
variables.stats.postEvent('marketplace', 'Uninstall');
|
||||
}, [item.type, item.display_name]);
|
||||
|
||||
const installAddon = useCallback((input) => {
|
||||
let failedReasonText = '';
|
||||
if (!input.name) {
|
||||
failedReason = variables.getMessage('modals.main.addons.sideload.errors.no_name');
|
||||
failedReasonText = variables.getMessage('modals.main.addons.sideload.errors.no_name');
|
||||
} else if (!input.author) {
|
||||
failedReason = variables.getMessage('modals.main.addons.sideload.errors.no_author');
|
||||
failedReasonText = variables.getMessage('modals.main.addons.sideload.errors.no_author');
|
||||
} else if (!input.type) {
|
||||
failedReason = variables.getMessage('modals.main.addons.sideload.errors.no_type');
|
||||
failedReasonText = variables.getMessage('modals.main.addons.sideload.errors.no_type');
|
||||
} else if (!input.version) {
|
||||
failedReason = variables.getMessage('modals.main.addons.sideload.errors.no_version');
|
||||
failedReasonText = variables.getMessage('modals.main.addons.sideload.errors.no_version');
|
||||
} else if (
|
||||
input.type === 'photos' &&
|
||||
(!input.photos ||
|
||||
@@ -53,84 +48,69 @@ export default class Added extends PureComponent {
|
||||
!input.photos[0].photographer ||
|
||||
!input.photos[0].location)
|
||||
) {
|
||||
failedReason = variables.getMessage('modals.main.addons.sideload.errors.invalid_photos');
|
||||
failedReasonText = variables.getMessage('modals.main.addons.sideload.errors.invalid_photos');
|
||||
} else if (
|
||||
input.type === 'quotes' &&
|
||||
(!input.quotes || !input.quotes.length || !input.quotes[0].quote || !input.quotes[0].author)
|
||||
) {
|
||||
failedReason = variables.getMessage('modals.main.addons.sideload.errors.invalid_quotes');
|
||||
failedReasonText = variables.getMessage('modals.main.addons.sideload.errors.invalid_quotes');
|
||||
}
|
||||
|
||||
if (failedReason !== '' && this.state.showFailed === false) {
|
||||
return this.setState({ failedReason, showFailed: true });
|
||||
if (failedReasonText !== '') {
|
||||
setFailedReason(failedReasonText);
|
||||
setShowFailed(true);
|
||||
return;
|
||||
}
|
||||
|
||||
install(input.type, input, true, false);
|
||||
toast(variables.getMessage('toasts.installed'));
|
||||
variables.stats.postEvent('marketplace', 'Sideload');
|
||||
this.setState({ installed: JSON.parse(localStorage.getItem('installed')) });
|
||||
}
|
||||
setInstalled(JSON.parse(localStorage.getItem('installed')));
|
||||
}, []);
|
||||
|
||||
getSideloadButton() {
|
||||
const getSideloadButton = useCallback(() => {
|
||||
return (
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => document.getElementById('file-input').click()}
|
||||
ref={this.customDnd}
|
||||
icon={<MdSendTimeExtension />}
|
||||
label={variables.getMessage('modals.main.addons.sideload.title')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
toggle(type, data) {
|
||||
const toggle = useCallback((type, data) => {
|
||||
if (type === 'item') {
|
||||
const installed = JSON.parse(localStorage.getItem('installed'));
|
||||
const info = { data: installed.find((i) => i.name === data.name) };
|
||||
const installedItems = JSON.parse(localStorage.getItem('installed'));
|
||||
const info = { data: installedItems.find((i) => i.name === data.name) };
|
||||
|
||||
this.setState({
|
||||
item: {
|
||||
type: info.data.type,
|
||||
display_name: info.data.name,
|
||||
author: info.data.author,
|
||||
description: urlParser(info.data.description.replace(/\n/g, '<br>')),
|
||||
//updated: info.updated,
|
||||
version: info.data.version,
|
||||
icon: info.data.screenshot_url,
|
||||
data: info.data,
|
||||
},
|
||||
button: this.buttons.uninstall,
|
||||
setItem({
|
||||
type: info.data.type,
|
||||
display_name: info.data.name,
|
||||
author: info.data.author,
|
||||
description: urlParser(info.data.description.replace(/\n/g, '<br>')),
|
||||
version: info.data.version,
|
||||
icon: info.data.screenshot_url,
|
||||
data: info.data,
|
||||
});
|
||||
|
||||
variables.stats.postEvent('marketplace', 'ItemPage viewed');
|
||||
} else {
|
||||
this.setState({ item: {} });
|
||||
setItem({});
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
uninstall() {
|
||||
uninstall(this.state.item.type, this.state.item.display_name);
|
||||
|
||||
toast(variables.getMessage('toasts.uninstalled'));
|
||||
|
||||
this.setState({
|
||||
button: '',
|
||||
installed: JSON.parse(localStorage.getItem('installed')),
|
||||
item: {},
|
||||
});
|
||||
|
||||
variables.stats.postEvent('marketplace', 'Uninstall');
|
||||
}
|
||||
|
||||
sortAddons(value, sendEvent) {
|
||||
const installed = JSON.parse(localStorage.getItem('installed'));
|
||||
const sortAddons = useCallback((value, sendEvent) => {
|
||||
const installedItems = JSON.parse(localStorage.getItem('installed'));
|
||||
|
||||
switch (value) {
|
||||
case 'newest':
|
||||
installed.reverse();
|
||||
installedItems.reverse();
|
||||
break;
|
||||
case 'oldest':
|
||||
break;
|
||||
case 'a-z':
|
||||
installed.sort((a, b) => {
|
||||
installedItems.sort((a, b) => {
|
||||
if (a.display_name < b.display_name) {
|
||||
return -1;
|
||||
}
|
||||
@@ -141,23 +121,23 @@ export default class Added extends PureComponent {
|
||||
});
|
||||
break;
|
||||
case 'z-a':
|
||||
installed.sort();
|
||||
installed.reverse();
|
||||
installedItems.sort();
|
||||
installedItems.reverse();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.setState({ installed });
|
||||
setInstalled(installedItems);
|
||||
|
||||
if (sendEvent) {
|
||||
variables.stats.postEvent('marketplace', 'Sort');
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
updateCheck() {
|
||||
const updateCheck = useCallback(() => {
|
||||
let updates = 0;
|
||||
this.state.installed.forEach(async (item) => {
|
||||
installed.forEach(async (item) => {
|
||||
const data = await (
|
||||
await fetch(variables.constants.API_URL + 'marketplace/item/' + item.name)
|
||||
).json();
|
||||
@@ -171,11 +151,11 @@ export default class Added extends PureComponent {
|
||||
} else {
|
||||
toast(variables.getMessage('modals.main.addons.no_updates'));
|
||||
}
|
||||
}
|
||||
}, [installed]);
|
||||
|
||||
removeAll() {
|
||||
const removeAll = useCallback(() => {
|
||||
try {
|
||||
this.state.installed.forEach((item) => {
|
||||
installed.forEach((item) => {
|
||||
uninstall(item.type, item.name);
|
||||
});
|
||||
} catch {
|
||||
@@ -183,121 +163,121 @@ export default class Added extends PureComponent {
|
||||
}
|
||||
|
||||
localStorage.setItem('installed', JSON.stringify([]));
|
||||
|
||||
toast(variables.getMessage('toasts.uninstalled_all'));
|
||||
setInstalled([]);
|
||||
}, [installed]);
|
||||
|
||||
this.setState({ installed: [] });
|
||||
useEffect(() => {
|
||||
sortAddons(localStorage.getItem('sortAddons'), false);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
this.forceUpdate();
|
||||
}
|
||||
const button = item.display_name ? (
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={uninstallItem}
|
||||
label={variables.getMessage('modals.main.marketplace.product.buttons.remove')}
|
||||
/>
|
||||
) : '';
|
||||
|
||||
componentDidMount() {
|
||||
this.sortAddons(localStorage.getItem('sortAddons'), false);
|
||||
}
|
||||
|
||||
render() {
|
||||
const sideLoadBackendElements = () => (
|
||||
<>
|
||||
<Modal
|
||||
closeTimeoutMS={100}
|
||||
onRequestClose={() => this.setState({ showFailed: false })}
|
||||
isOpen={this.state.showFailed}
|
||||
className="Modal resetmodal mainModal resetmodal"
|
||||
overlayClassName="Overlay resetoverlay"
|
||||
ariaHideApp={false}
|
||||
>
|
||||
<SideloadFailedModal
|
||||
modalClose={() => this.setState({ showFailed: false })}
|
||||
reason={this.state.failedReason}
|
||||
/>
|
||||
</Modal>
|
||||
<FileUpload
|
||||
id="file-input"
|
||||
type="settings"
|
||||
accept="application/json"
|
||||
loadFunction={(e) => this.installAddon(JSON.parse(e))}
|
||||
const sideLoadBackendElements = () => (
|
||||
<>
|
||||
<Modal
|
||||
closeTimeoutMS={100}
|
||||
onRequestClose={() => setShowFailed(false)}
|
||||
isOpen={showFailed}
|
||||
className="Modal resetmodal mainModal resetmodal"
|
||||
overlayClassName="Overlay resetoverlay"
|
||||
ariaHideApp={false}
|
||||
>
|
||||
<SideloadFailedModal
|
||||
modalClose={() => setShowFailed(false)}
|
||||
reason={failedReason}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
if (this.state.installed.length === 0) {
|
||||
return (
|
||||
<>
|
||||
<Header title={variables.getMessage('modals.main.navbar.addons')} report={false}>
|
||||
<CustomActions>{this.getSideloadButton()}</CustomActions>
|
||||
</Header>
|
||||
{sideLoadBackendElements()}
|
||||
<div className="emptyItems">
|
||||
<div className="emptyNewMessage">
|
||||
<MdOutlineExtensionOff />
|
||||
<span className="title">
|
||||
{variables.getMessage('modals.main.addons.empty.title')}
|
||||
</span>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage('modals.main.addons.empty.description')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.item.display_name) {
|
||||
return (
|
||||
<ItemPage
|
||||
data={this.state.item}
|
||||
button={this.state.button}
|
||||
addons={true}
|
||||
toggleFunction={() => this.toggle()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
</Modal>
|
||||
<FileUpload
|
||||
id="file-input"
|
||||
type="settings"
|
||||
accept="application/json"
|
||||
loadFunction={(e) => installAddon(JSON.parse(e))}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
if (installed.length === 0) {
|
||||
return (
|
||||
<>
|
||||
<Header title={variables.getMessage('modals.main.addons.added')} report={false}>
|
||||
<CustomActions>
|
||||
{this.getSideloadButton()}
|
||||
{sideLoadBackendElements()}
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.updateCheck()}
|
||||
icon={<MdUpdate />}
|
||||
label={variables.getMessage('modals.main.addons.check_updates')}
|
||||
/>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.removeAll()}
|
||||
icon={<MdOutlineExtensionOff />}
|
||||
label="Remove all addons"
|
||||
/>
|
||||
{/*<Button
|
||||
type="settings"
|
||||
onClick={() => document.getElementById('file-input').click()}
|
||||
icon={<MdSendTimeExtension />}
|
||||
label={variables.getMessage('modals.main.addons.sideload.title')}
|
||||
` />*/}
|
||||
</CustomActions>
|
||||
<Header title={variables.getMessage('modals.main.navbar.addons')} report={false}>
|
||||
<CustomActions>{getSideloadButton()}</CustomActions>
|
||||
</Header>
|
||||
<Dropdown
|
||||
label={variables.getMessage('modals.main.addons.sort.title')}
|
||||
name="sortAddons"
|
||||
onChange={(value) => this.sortAddons(value)}
|
||||
items={[
|
||||
{ value: 'newest', text: variables.getMessage('modals.main.addons.sort.newest') },
|
||||
{ value: 'oldest', text: variables.getMessage('modals.main.addons.sort.oldest') },
|
||||
{ value: 'a-z', text: variables.getMessage('modals.main.addons.sort.a_z') },
|
||||
{ value: 'z-a', text: variables.getMessage('modals.main.addons.sort.z_a') },
|
||||
]}
|
||||
/>
|
||||
<Items
|
||||
items={this.state.installed}
|
||||
isAdded={true}
|
||||
filter=""
|
||||
toggleFunction={(input) => this.toggle('item', input)}
|
||||
showCreateYourOwn={false}
|
||||
/>
|
||||
{sideLoadBackendElements()}
|
||||
<div className="emptyItems">
|
||||
<div className="emptyNewMessage">
|
||||
<MdOutlineExtensionOff />
|
||||
<span className="title">
|
||||
{variables.getMessage('modals.main.addons.empty.title')}
|
||||
</span>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage('modals.main.addons.empty.description')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.display_name) {
|
||||
return (
|
||||
<ItemPage
|
||||
data={item}
|
||||
button={button}
|
||||
addons={true}
|
||||
toggleFunction={() => toggle()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title={variables.getMessage('modals.main.addons.added')} report={false}>
|
||||
<CustomActions>
|
||||
{getSideloadButton()}
|
||||
{sideLoadBackendElements()}
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={updateCheck}
|
||||
icon={<MdUpdate />}
|
||||
label={variables.getMessage('modals.main.addons.check_updates')}
|
||||
/>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={removeAll}
|
||||
icon={<MdOutlineExtensionOff />}
|
||||
label="Remove all addons"
|
||||
/>
|
||||
</CustomActions>
|
||||
</Header>
|
||||
<Dropdown
|
||||
label={variables.getMessage('modals.main.addons.sort.title')}
|
||||
name="sortAddons"
|
||||
onChange={(value) => sortAddons(value)}
|
||||
items={[
|
||||
{ value: 'newest', text: variables.getMessage('modals.main.addons.sort.newest') },
|
||||
{ value: 'oldest', text: variables.getMessage('modals.main.addons.sort.oldest') },
|
||||
{ value: 'a-z', text: variables.getMessage('modals.main.addons.sort.a_z') },
|
||||
{ value: 'z-a', text: variables.getMessage('modals.main.addons.sort.z_a') },
|
||||
]}
|
||||
/>
|
||||
<Items
|
||||
items={installed}
|
||||
isAdded={true}
|
||||
filter=""
|
||||
toggleFunction={(input) => toggle('item', input)}
|
||||
showCreateYourOwn={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
Added.displayName = 'Added';
|
||||
|
||||
export default Added;
|
||||
|
||||
@@ -1,41 +1,35 @@
|
||||
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } from 'react';
|
||||
import { MdOutlineExtensionOff } from 'react-icons/md';
|
||||
import { Button } from 'components/Elements';
|
||||
|
||||
export default class Create extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<div className="flexTopMarketplace">
|
||||
<span className="mainTitle">
|
||||
{variables.getMessage('modals.main.addons.create.title')}
|
||||
const Create = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="flexTopMarketplace">
|
||||
<span className="mainTitle">
|
||||
{variables.getMessage('modals.main.addons.create.title')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="emptyItems">
|
||||
<div className="emptyNewMessage">
|
||||
<MdOutlineExtensionOff />
|
||||
<span className="title">
|
||||
{variables.getMessage('modals.main.addons.create.moved_title')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="emptyItems">
|
||||
<div className="emptyNewMessage">
|
||||
<MdOutlineExtensionOff />
|
||||
<span className="title">
|
||||
{variables.getMessage('modals.main.addons.create.moved_title')}
|
||||
</span>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage('modals.main.addons.create.moved_description')}
|
||||
</span>
|
||||
<div className="createButtons">
|
||||
<Button
|
||||
type="settings"
|
||||
label={variables.getMessage('modals.main.addons.create.moved_button')}
|
||||
/>
|
||||
</div>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage('modals.main.addons.create.moved_description')}
|
||||
</span>
|
||||
<div className="createButtons">
|
||||
<Button
|
||||
type="settings"
|
||||
label={variables.getMessage('modals.main.addons.create.moved_button')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { Create as default, Create };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { MdCancel, MdAdd, MdOutlineTextsms } from 'react-icons/md';
|
||||
import { toast } from 'react-toastify';
|
||||
import { TextareaAutosize } from '@mui/material';
|
||||
@@ -8,142 +8,126 @@ import { Header, Row, Content, Action, PreferencesWrapper } from 'components/Lay
|
||||
import { Button } from 'components/Elements';
|
||||
import EventBus from 'utils/eventbus';
|
||||
|
||||
class MessageOptions extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
messages: JSON.parse(localStorage.getItem('messages')) || [],
|
||||
};
|
||||
}
|
||||
const MessageOptions = () => {
|
||||
const [messages, setMessages] = useState(JSON.parse(localStorage.getItem('messages')) || []);
|
||||
|
||||
reset = () => {
|
||||
const reset = () => {
|
||||
localStorage.setItem('messages', '[]');
|
||||
this.setState({
|
||||
messages: [],
|
||||
});
|
||||
toast(variables.getMessage(this.languagecode, 'toasts.reset'));
|
||||
setMessages([]);
|
||||
toast(variables.getMessage('toasts.reset'));
|
||||
EventBus.emit('refresh', 'message');
|
||||
};
|
||||
|
||||
modifyMessage(type, index) {
|
||||
const messages = this.state.messages;
|
||||
const modifyMessage = (type, index) => {
|
||||
const updatedMessages = [...messages];
|
||||
if (type === 'add') {
|
||||
messages.push('');
|
||||
updatedMessages.push('');
|
||||
} else {
|
||||
messages.splice(index, 1);
|
||||
updatedMessages.splice(index, 1);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
messages,
|
||||
});
|
||||
this.forceUpdate();
|
||||
setMessages(updatedMessages);
|
||||
localStorage.setItem('messages', JSON.stringify(updatedMessages));
|
||||
};
|
||||
|
||||
localStorage.setItem('messages', JSON.stringify(messages));
|
||||
}
|
||||
|
||||
message(e, text, index) {
|
||||
const message = (e, text, index) => {
|
||||
const result = text === true ? e.target.value : e.target.result;
|
||||
|
||||
const messages = this.state.messages;
|
||||
messages[index] = result;
|
||||
this.setState({
|
||||
messages,
|
||||
});
|
||||
this.forceUpdate();
|
||||
const updatedMessages = [...messages];
|
||||
updatedMessages[index] = result;
|
||||
setMessages(updatedMessages);
|
||||
|
||||
localStorage.setItem('messages', JSON.stringify(messages));
|
||||
localStorage.setItem('messages', JSON.stringify(updatedMessages));
|
||||
document.querySelector('.reminder-info').style.display = 'flex';
|
||||
localStorage.setItem('showReminder', true);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const MESSAGE_SECTION = 'modals.main.settings.sections.message';
|
||||
const MESSAGE_SECTION = 'modals.main.settings.sections.message';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
title={variables.getMessage(`${MESSAGE_SECTION}.title`)}
|
||||
setting="message"
|
||||
category="message"
|
||||
element=".message"
|
||||
zoomSetting="zoomMessage"
|
||||
visibilityToggle={true}
|
||||
/>
|
||||
<PreferencesWrapper
|
||||
setting="message"
|
||||
visibilityToggle={true}
|
||||
category="message"
|
||||
zoomSetting="zoomMessage"
|
||||
>
|
||||
<Row final={true}>
|
||||
<Content title={variables.getMessage(`${MESSAGE_SECTION}.messages`)} />
|
||||
<Action>
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
title={variables.getMessage(`${MESSAGE_SECTION}.title`)}
|
||||
setting="message"
|
||||
category="message"
|
||||
element=".message"
|
||||
zoomSetting="zoomMessage"
|
||||
visibilityToggle={true}
|
||||
/>
|
||||
<PreferencesWrapper
|
||||
setting="message"
|
||||
visibilityToggle={true}
|
||||
category="message"
|
||||
zoomSetting="zoomMessage"
|
||||
>
|
||||
<Row final={true}>
|
||||
<Content title={variables.getMessage(`${MESSAGE_SECTION}.messages`)} />
|
||||
<Action>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => modifyMessage('add')}
|
||||
icon={<MdAdd />}
|
||||
label={variables.getMessage(`${MESSAGE_SECTION}.add`)}
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
<div className="messagesContainer">
|
||||
{messages.map((_url, index) => (
|
||||
<div className="messageMap" key={index}>
|
||||
<div className="flexGrow">
|
||||
<div className="icon">
|
||||
<MdOutlineTextsms />
|
||||
</div>
|
||||
<div className="messageText">
|
||||
<span className="subtitle">
|
||||
{variables.getMessage(`${MESSAGE_SECTION}.title`)}
|
||||
</span>
|
||||
<TextareaAutosize
|
||||
value={messages[index]}
|
||||
placeholder={variables.getMessage(
|
||||
'modals.main.settings.sections.message.content',
|
||||
)}
|
||||
onChange={(e) => message(e, true, index)}
|
||||
varient="outlined"
|
||||
style={{ padding: '0' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="messageAction">
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => modifyMessage('remove', index)}
|
||||
icon={<MdCancel />}
|
||||
label={variables.getMessage('modals.main.marketplace.product.buttons.remove')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{messages.length === 0 && (
|
||||
<div className="photosEmpty">
|
||||
<div className="emptyNewMessage">
|
||||
<MdOutlineTextsms />
|
||||
<span className="title">
|
||||
{variables.getMessage(`${MESSAGE_SECTION}.no_messages`)}
|
||||
</span>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage(`${MESSAGE_SECTION}.add_some`)}
|
||||
</span>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.modifyMessage('add')}
|
||||
onClick={() => modifyMessage('add')}
|
||||
icon={<MdAdd />}
|
||||
label={variables.getMessage(`${MESSAGE_SECTION}.add`)}
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
<div className="messagesContainer">
|
||||
{this.state.messages.map((_url, index) => (
|
||||
<div className="messageMap" key={index}>
|
||||
<div className="flexGrow">
|
||||
<div className="icon">
|
||||
<MdOutlineTextsms />
|
||||
</div>
|
||||
<div className="messageText">
|
||||
<span className="subtitle">
|
||||
{variables.getMessage(`${MESSAGE_SECTION}.title`)}
|
||||
</span>
|
||||
<TextareaAutosize
|
||||
value={this.state.messages[index]}
|
||||
placeholder={variables.getMessage(
|
||||
'modals.main.settings.sections.message.content',
|
||||
)}
|
||||
onChange={(e) => this.message(e, true, index)}
|
||||
varient="outlined"
|
||||
style={{ padding: '0' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="messageAction">
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.modifyMessage('remove', index)}
|
||||
icon={<MdCancel />}
|
||||
label={variables.getMessage('modals.main.marketplace.product.buttons.remove')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{this.state.messages.length === 0 && (
|
||||
<div className="photosEmpty">
|
||||
<div className="emptyNewMessage">
|
||||
<MdOutlineTextsms />
|
||||
<span className="title">
|
||||
{variables.getMessage(`${MESSAGE_SECTION}.no_messages`)}
|
||||
</span>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage(`${MESSAGE_SECTION}.add_some`)}
|
||||
</span>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.modifyMessage('add')}
|
||||
icon={<MdAdd />}
|
||||
label={variables.getMessage(`${MESSAGE_SECTION}.add`)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PreferencesWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</PreferencesWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { MessageOptions as default, MessageOptions };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Modal from 'react-modal';
|
||||
|
||||
import { MainModal } from 'components/Elements';
|
||||
@@ -11,28 +11,21 @@ import { parseDeepLink, shouldAutoOpenModal } from 'utils/deepLinking';
|
||||
|
||||
import Welcome from 'features/welcome/Welcome';
|
||||
|
||||
export default class Modals extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
mainModal: false,
|
||||
updateModal: false,
|
||||
welcomeModal: false,
|
||||
appsModal: false,
|
||||
preview: false,
|
||||
deepLinkData: null,
|
||||
};
|
||||
}
|
||||
const Modals = () => {
|
||||
const [mainModal, setMainModal] = useState(false);
|
||||
const [updateModal, setUpdateModal] = useState(false);
|
||||
const [welcomeModal, setWelcomeModal] = useState(false);
|
||||
const [appsModal, setAppsModal] = useState(false);
|
||||
const [preview, setPreview] = useState(false);
|
||||
const [deepLinkData, setDeepLinkData] = useState(null);
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
// Check for deep link first (has priority)
|
||||
if (shouldAutoOpenModal()) {
|
||||
const deepLinkData = parseDeepLink();
|
||||
this.setState({
|
||||
mainModal: true,
|
||||
deepLinkData,
|
||||
});
|
||||
variables.stats.postEvent('modal', `Opened via deep link: ${deepLinkData.tab}`);
|
||||
const linkData = parseDeepLink();
|
||||
setMainModal(true);
|
||||
setDeepLinkData(linkData);
|
||||
variables.stats.postEvent('modal', `Opened via deep link: ${linkData.tab}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,9 +33,7 @@ export default class Modals extends PureComponent {
|
||||
localStorage.getItem('showWelcome') === 'true' &&
|
||||
window.location.search !== '?nointro=true'
|
||||
) {
|
||||
this.setState({
|
||||
welcomeModal: true,
|
||||
});
|
||||
setWelcomeModal(true);
|
||||
variables.stats.postEvent('modal', 'Opened welcome');
|
||||
}
|
||||
|
||||
@@ -56,71 +47,69 @@ export default class Modals extends PureComponent {
|
||||
|
||||
// hide refresh reminder once the user has refreshed the page
|
||||
localStorage.setItem('showReminder', false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
closeWelcome() {
|
||||
const closeWelcome = () => {
|
||||
localStorage.setItem('showWelcome', false);
|
||||
this.setState({
|
||||
welcomeModal: false,
|
||||
});
|
||||
setWelcomeModal(false);
|
||||
EventBus.emit('refresh', 'widgetsWelcomeDone');
|
||||
EventBus.emit('refresh', 'widgets');
|
||||
EventBus.emit('refresh', 'backgroundwelcome');
|
||||
}
|
||||
};
|
||||
|
||||
previewWelcome() {
|
||||
const previewWelcome = () => {
|
||||
localStorage.setItem('showWelcome', false);
|
||||
localStorage.setItem('welcomePreview', true);
|
||||
this.setState({
|
||||
welcomeModal: false,
|
||||
preview: true,
|
||||
});
|
||||
setWelcomeModal(false);
|
||||
setPreview(true);
|
||||
EventBus.emit('refresh', 'widgetsWelcome');
|
||||
}
|
||||
};
|
||||
|
||||
toggleModal(type, action) {
|
||||
this.setState({
|
||||
[type]: action,
|
||||
});
|
||||
const toggleModal = (type, action) => {
|
||||
const modalSetters = {
|
||||
mainModal: setMainModal,
|
||||
updateModal: setUpdateModal,
|
||||
welcomeModal: setWelcomeModal,
|
||||
appsModal: setAppsModal,
|
||||
};
|
||||
|
||||
if (modalSetters[type]) {
|
||||
modalSetters[type](action);
|
||||
}
|
||||
|
||||
if (action !== false) {
|
||||
variables.stats.postEvent('modal', `Opened ${type.replace('Modal', '')}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
{this.state.welcomeModal === false && (
|
||||
<Navbar openModal={(modal) => this.toggleModal(modal, true)} />
|
||||
)}
|
||||
<Modal
|
||||
closeTimeoutMS={300}
|
||||
id="modal"
|
||||
onRequestClose={() => this.toggleModal('mainModal', false)}
|
||||
isOpen={this.state.mainModal}
|
||||
className="Modal mainModal"
|
||||
overlayClassName="Overlay"
|
||||
ariaHideApp={false}
|
||||
>
|
||||
<MainModal
|
||||
modalClose={() => this.toggleModal('mainModal', false)}
|
||||
deepLinkData={this.state.deepLinkData}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal
|
||||
closeTimeoutMS={300}
|
||||
onRequestClose={() => this.closeWelcome()}
|
||||
isOpen={this.state.welcomeModal}
|
||||
className="Modal welcomemodal mainModal"
|
||||
overlayClassName="Overlay mainModal"
|
||||
shouldCloseOnOverlayClick={false}
|
||||
ariaHideApp={false}
|
||||
>
|
||||
<Welcome modalClose={() => this.closeWelcome()} modalSkip={() => this.previewWelcome()} />
|
||||
</Modal>
|
||||
{this.state.preview && <Preview setup={() => window.location.reload()} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{welcomeModal === false && <Navbar openModal={(modal) => toggleModal(modal, true)} />}
|
||||
<Modal
|
||||
closeTimeoutMS={300}
|
||||
id="modal"
|
||||
onRequestClose={() => toggleModal('mainModal', false)}
|
||||
isOpen={mainModal}
|
||||
className="Modal mainModal"
|
||||
overlayClassName="Overlay"
|
||||
ariaHideApp={false}
|
||||
>
|
||||
<MainModal modalClose={() => toggleModal('mainModal', false)} deepLinkData={deepLinkData} />
|
||||
</Modal>
|
||||
<Modal
|
||||
closeTimeoutMS={300}
|
||||
onRequestClose={() => closeWelcome()}
|
||||
isOpen={welcomeModal}
|
||||
className="Modal welcomemodal mainModal"
|
||||
overlayClassName="Overlay mainModal"
|
||||
shouldCloseOnOverlayClick={false}
|
||||
ariaHideApp={false}
|
||||
>
|
||||
<Welcome modalClose={() => closeWelcome()} modalSkip={() => previewWelcome()} />
|
||||
</Modal>
|
||||
{preview && <Preview setup={() => window.location.reload()} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { Modals as default, Modals };
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent, createRef } from 'react';
|
||||
import { useState, useEffect, useRef } 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();
|
||||
}
|
||||
const Changelog = () => {
|
||||
const [title, setTitle] = useState(null);
|
||||
const [content, setContent] = useState(null);
|
||||
const [date, setDate] = useState(null);
|
||||
const [image, setImage] = useState(null);
|
||||
const [error, setError] = useState(false);
|
||||
const [showLightbox, setShowLightbox] = useState(false);
|
||||
const [lightboxImg, setLightboxImg] = useState(null);
|
||||
|
||||
const offlineMode = localStorage.getItem('offlineMode') === 'true';
|
||||
const controllerRef = useRef(new AbortController());
|
||||
const changelog = useRef();
|
||||
|
||||
parseMarkdown = (text) => {
|
||||
const parseMarkdown = (text) => {
|
||||
if (typeof text !== 'string') {
|
||||
throw new Error('Input must be a string');
|
||||
}
|
||||
@@ -46,15 +46,15 @@ class Changelog extends PureComponent {
|
||||
return text;
|
||||
};
|
||||
|
||||
async getUpdate() {
|
||||
const getUpdate = async () => {
|
||||
const releases = await fetch(
|
||||
`https://api.github.com/repos/${variables.constants.ORG_NAME}/${variables.constants.REPO_NAME}/releases`,
|
||||
{
|
||||
signal: this.controller.signal,
|
||||
signal: controllerRef.current.signal,
|
||||
},
|
||||
);
|
||||
|
||||
if (this.controller.signal.aborted === true) {
|
||||
if (controllerRef.current.signal.aborted === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,109 +67,105 @@ class Changelog extends PureComponent {
|
||||
}
|
||||
|
||||
// request the changelog
|
||||
const res = await fetch(release.url, { signal: this.controller.signal });
|
||||
const res = await fetch(release.url, { signal: controllerRef.current.signal });
|
||||
|
||||
if (res.status === 404) {
|
||||
this.setState({ error: true });
|
||||
setError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.controller.signal.aborted === true) {
|
||||
if (controllerRef.current.signal.aborted === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const changelog = await res.json();
|
||||
this.setState({
|
||||
title: changelog.name,
|
||||
content: this.parseMarkdown(changelog.body),
|
||||
date: new Date(changelog.published_at).toLocaleDateString(),
|
||||
});
|
||||
}
|
||||
setTitle(changelog.name);
|
||||
setContent(parseMarkdown(changelog.body));
|
||||
setDate(new Date(changelog.published_at).toLocaleDateString());
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (navigator.onLine === false || this.offlineMode) {
|
||||
useEffect(() => {
|
||||
if (navigator.onLine === false || offlineMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.getUpdate();
|
||||
}
|
||||
getUpdate();
|
||||
|
||||
componentWillUnmount() {
|
||||
// stop making requests
|
||||
this.controller.abort();
|
||||
}
|
||||
|
||||
render() {
|
||||
const errorMessage = (msg) => {
|
||||
return (
|
||||
<div className="emptyItems">
|
||||
<div className="emptyMessage">{msg}</div>
|
||||
</div>
|
||||
);
|
||||
return () => {
|
||||
// stop making requests
|
||||
controllerRef.current.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
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>,
|
||||
);
|
||||
}
|
||||
|
||||
const errorMessage = (msg) => {
|
||||
return (
|
||||
<div className="modalInfoPage changelogtab" ref={this.changelog}>
|
||||
<span className="mainTitle">{this.state.title}</span>
|
||||
<span className="subtitle">Released on {this.state.date}</span>
|
||||
{this.state.image && (
|
||||
<img
|
||||
draggable={false}
|
||||
src={this.state.image}
|
||||
alt={this.state.title}
|
||||
className="updateImage"
|
||||
/>
|
||||
)}
|
||||
<div className="updateChangelog" dangerouslySetInnerHTML={{ __html: this.state.content }} />
|
||||
<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 className="emptyItems">
|
||||
<div className="emptyMessage">{msg}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (navigator.onLine === false || 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 (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 (!title) {
|
||||
return errorMessage(
|
||||
<div className="loaderHolder">
|
||||
<div id="loader"></div>
|
||||
<span className="subtitle">{variables.getMessage('modals.main.loading')}</span>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modalInfoPage changelogtab" ref={changelog}>
|
||||
<span className="mainTitle">{title}</span>
|
||||
<span className="subtitle">Released on {date}</span>
|
||||
{image && (
|
||||
<img
|
||||
draggable={false}
|
||||
src={image}
|
||||
alt={title}
|
||||
className="updateImage"
|
||||
/>
|
||||
)}
|
||||
<div className="updateChangelog" dangerouslySetInnerHTML={{ __html: content }} />
|
||||
<Modal
|
||||
closeTimeoutMS={100}
|
||||
onRequestClose={() => setShowLightbox(false)}
|
||||
isOpen={showLightbox}
|
||||
className="Modal lightBoxModal"
|
||||
overlayClassName="Overlay resetoverlay"
|
||||
ariaHideApp={false}
|
||||
>
|
||||
<Lightbox
|
||||
modalClose={() => setShowLightbox(false)}
|
||||
img={lightboxImg}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Changelog as default, Changelog };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
import { MdOutlineOpenInNew } from 'react-icons/md';
|
||||
|
||||
@@ -7,32 +7,28 @@ 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();
|
||||
}
|
||||
const LanguageOptions = () => {
|
||||
const [quoteLanguages, setQuoteLanguages] = useState([
|
||||
{
|
||||
name: variables.getMessage('modals.main.loading'),
|
||||
value: 'loading',
|
||||
},
|
||||
]);
|
||||
|
||||
async getquoteLanguages() {
|
||||
const controllerRef = useRef(new AbortController());
|
||||
|
||||
const getquoteLanguages = async () => {
|
||||
const data = await (
|
||||
await fetch(variables.constants.API_URL + '/quotes/languages', {
|
||||
signal: this.controller.signal,
|
||||
signal: controllerRef.current.signal,
|
||||
})
|
||||
).json();
|
||||
|
||||
if (this.controller.signal.aborted === true) {
|
||||
if (controllerRef.current.signal.aborted === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const quoteLanguages = data.map((language) => {
|
||||
const fetchedQuoteLanguages = data.map((language) => {
|
||||
return {
|
||||
name: languages.find((l) => l.value === language.name)
|
||||
? languages.find((l) => l.value === language.name).name
|
||||
@@ -41,69 +37,64 @@ class LanguageOptions extends PureComponent {
|
||||
};
|
||||
});
|
||||
|
||||
this.setState({
|
||||
quoteLanguages,
|
||||
});
|
||||
}
|
||||
setQuoteLanguages(fetchedQuoteLanguages);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
if (navigator.onLine === false || localStorage.getItem('offlineMode') === 'true') {
|
||||
return this.setState({
|
||||
quoteLanguages: [
|
||||
{
|
||||
name: variables.getMessage('modals.main.marketplace.offline.description'),
|
||||
value: 'loading',
|
||||
},
|
||||
],
|
||||
});
|
||||
setQuoteLanguages([
|
||||
{
|
||||
name: variables.getMessage('modals.main.marketplace.offline.description'),
|
||||
value: 'loading',
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.getquoteLanguages();
|
||||
}
|
||||
getquoteLanguages();
|
||||
|
||||
componentWillUnmount() {
|
||||
// stop making requests
|
||||
this.controller.abort();
|
||||
}
|
||||
return () => {
|
||||
// stop making requests
|
||||
controllerRef.current.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="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>
|
||||
return (
|
||||
<>
|
||||
<div className="modalHeader">
|
||||
<span className="mainTitle">
|
||||
{variables.getMessage('modals.main.settings.sections.language.quote')}
|
||||
{variables.getMessage('modals.main.settings.sections.language.title')}
|
||||
</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 className="headerActions">
|
||||
<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={quoteLanguages.map((language) => {
|
||||
return { name: language.name, value: language.value.name };
|
||||
})}
|
||||
defaultValue={quoteLanguages[0].name}
|
||||
category="quote"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { LanguageOptions as default, LanguageOptions };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PureComponent, Fragment, Suspense, lazy } from 'react';
|
||||
import { useState, useEffect, Fragment, Suspense, lazy, useMemo } from 'react';
|
||||
|
||||
import Clock from '../../time/Clock';
|
||||
import Greeting from '../../greeting/Greeting';
|
||||
@@ -17,74 +17,69 @@ import EventBus from 'utils/eventbus';
|
||||
// as seen here it is ridiculously large
|
||||
const Weather = lazy(() => import('../../weather/Weather'));
|
||||
|
||||
export default class Widgets extends PureComponent {
|
||||
online = localStorage.getItem('offlineMode') === 'false';
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
order: JSON.parse(localStorage.getItem('order')),
|
||||
welcome: localStorage.getItem('showWelcome'),
|
||||
};
|
||||
// widgets we can re-order
|
||||
this.widgets = {
|
||||
time: this.enabled('time') && <Clock />,
|
||||
greeting: this.enabled('greeting') && <Greeting />,
|
||||
quote: this.enabled('quote') && <Quote />,
|
||||
date: this.enabled('date') && <Date />,
|
||||
quicklinks: this.enabled('quicklinksenabled') && this.online ? <QuickLinks /> : null,
|
||||
message: this.enabled('message') && <Message />,
|
||||
};
|
||||
}
|
||||
const Widgets = () => {
|
||||
const online = localStorage.getItem('offlineMode') === 'false';
|
||||
const [order, setOrder] = useState(JSON.parse(localStorage.getItem('order')));
|
||||
const [welcome, setWelcome] = useState(localStorage.getItem('showWelcome'));
|
||||
|
||||
enabled(key) {
|
||||
const enabled = (key) => {
|
||||
return localStorage.getItem(key) === 'true';
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
// widgets we can re-order
|
||||
const widgets = useMemo(
|
||||
() => ({
|
||||
time: enabled('time') && <Clock />,
|
||||
greeting: enabled('greeting') && <Greeting />,
|
||||
quote: enabled('quote') && <Quote />,
|
||||
date: enabled('date') && <Date />,
|
||||
quicklinks: enabled('quicklinksenabled') && online ? <QuickLinks /> : null,
|
||||
message: enabled('message') && <Message />,
|
||||
}),
|
||||
[order], // Re-create widgets when order changes
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleRefresh = (data) => {
|
||||
switch (data) {
|
||||
case 'widgets':
|
||||
return this.setState({
|
||||
order: JSON.parse(localStorage.getItem('order')),
|
||||
});
|
||||
return setOrder(JSON.parse(localStorage.getItem('order')));
|
||||
case 'widgetsWelcome':
|
||||
this.setState({
|
||||
welcome: localStorage.getItem('showWelcome'),
|
||||
});
|
||||
setWelcome(localStorage.getItem('showWelcome'));
|
||||
localStorage.setItem('showWelcome', true);
|
||||
window.onbeforeunload = () => {
|
||||
localStorage.clear();
|
||||
};
|
||||
break;
|
||||
case 'widgetsWelcomeDone':
|
||||
this.setState({
|
||||
welcome: localStorage.getItem('showWelcome'),
|
||||
});
|
||||
return (window.onbeforeunload = null);
|
||||
setWelcome(localStorage.getItem('showWelcome'));
|
||||
window.onbeforeunload = null;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
}
|
||||
EventBus.on('refresh', handleRefresh);
|
||||
return () => {
|
||||
EventBus.off('refresh');
|
||||
};
|
||||
}, []);
|
||||
|
||||
render() {
|
||||
// don't show when welcome is there
|
||||
return this.state.welcome !== 'false' ? (
|
||||
<WidgetsLayout />
|
||||
) : (
|
||||
<WidgetsLayout>
|
||||
<Suspense fallback={<></>}>
|
||||
{this.enabled('searchBar') && <Search />}
|
||||
{this.state.order.map((element, key) => (
|
||||
<Fragment key={key}>{this.widgets[element]}</Fragment>
|
||||
))}
|
||||
{this.enabled('weatherEnabled') && this.online ? <Weather /> : null}
|
||||
</Suspense>
|
||||
</WidgetsLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
// don't show when welcome is there
|
||||
return welcome !== 'false' ? (
|
||||
<WidgetsLayout />
|
||||
) : (
|
||||
<WidgetsLayout>
|
||||
<Suspense fallback={<></>}>
|
||||
{enabled('searchBar') && <Search />}
|
||||
{order.map((element, key) => (
|
||||
<Fragment key={key}>{widgets[element]}</Fragment>
|
||||
))}
|
||||
{enabled('weatherEnabled') && online ? <Weather /> : null}
|
||||
</Suspense>
|
||||
</WidgetsLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export { Widgets as default, Widgets };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent, createRef } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
import { MdSettings } from 'react-icons/md';
|
||||
|
||||
@@ -10,26 +10,22 @@ import EventBus from 'utils/eventbus';
|
||||
|
||||
import './scss/index.scss';
|
||||
|
||||
class Navbar extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.navbarContainer = createRef();
|
||||
this.state = {
|
||||
classList: localStorage.getItem('widgetStyle') === 'legacy' ? 'navbar old' : 'navbar new',
|
||||
refreshText: '',
|
||||
refreshEnabled: localStorage.getItem('refresh'),
|
||||
refreshOption: localStorage.getItem('refreshOption') || '',
|
||||
appsOpen: false,
|
||||
};
|
||||
}
|
||||
const Navbar = ({ openModal }) => {
|
||||
const navbarContainer = useRef();
|
||||
const [classList] = useState(
|
||||
localStorage.getItem('widgetStyle') === 'legacy' ? 'navbar old' : 'navbar new',
|
||||
);
|
||||
const [refreshText, setRefreshText] = useState('');
|
||||
const [refreshEnabled, setRefreshEnabled] = useState(localStorage.getItem('refresh'));
|
||||
const [refreshOption, setRefreshOption] = useState(localStorage.getItem('refreshOption') || '');
|
||||
const [appsOpen, setAppsOpen] = useState(false);
|
||||
const [zoomFontSize, setZoomFontSize] = useState('1.2rem');
|
||||
|
||||
setZoom() {
|
||||
this.setState({
|
||||
zoomFontSize: Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem',
|
||||
});
|
||||
}
|
||||
const setZoom = () => {
|
||||
setZoomFontSize(Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem');
|
||||
};
|
||||
|
||||
updateRefreshText() {
|
||||
const updateRefreshText = () => {
|
||||
let refreshText;
|
||||
switch (localStorage.getItem('refreshOption')) {
|
||||
case 'background':
|
||||
@@ -51,38 +47,35 @@ class Navbar extends PureComponent {
|
||||
break;
|
||||
}
|
||||
|
||||
this.setState({ refreshText });
|
||||
}
|
||||
setRefreshText(refreshText);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
useEffect(() => {
|
||||
const handleRefresh = (data) => {
|
||||
if (data === 'navbar' || data === 'background') {
|
||||
this.setState({
|
||||
refreshEnabled: localStorage.getItem('refresh'),
|
||||
refreshOption: localStorage.getItem('refreshOption'),
|
||||
});
|
||||
|
||||
this.forceUpdate();
|
||||
setRefreshEnabled(localStorage.getItem('refresh'));
|
||||
setRefreshOption(localStorage.getItem('refreshOption'));
|
||||
|
||||
try {
|
||||
this.updateRefreshText();
|
||||
this.setZoom();
|
||||
updateRefreshText();
|
||||
setZoom();
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.updateRefreshText();
|
||||
this.setZoom();
|
||||
}
|
||||
updateRefreshText();
|
||||
setZoom();
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
}
|
||||
EventBus.on('refresh', handleRefresh);
|
||||
return () => {
|
||||
EventBus.off('refresh');
|
||||
};
|
||||
}, []);
|
||||
|
||||
refresh() {
|
||||
switch (this.state.refreshOption) {
|
||||
const refresh = () => {
|
||||
switch (refreshOption) {
|
||||
case 'background':
|
||||
return EventBus.emit('refresh', 'backgroundrefresh');
|
||||
case 'quote':
|
||||
@@ -93,59 +86,47 @@ class Navbar extends PureComponent {
|
||||
default:
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const backgroundEnabled = localStorage.getItem('background') === 'true';
|
||||
const backgroundEnabled = localStorage.getItem('background') === 'true';
|
||||
|
||||
const navbar = (
|
||||
<div className="navbar-container">
|
||||
<div className={this.state.classList}>
|
||||
{localStorage.getItem('view') === 'true' && backgroundEnabled ? (
|
||||
<Maximise fontSize={this.state.zoomFontSize} />
|
||||
) : null}
|
||||
{localStorage.getItem('notesEnabled') === 'true' && (
|
||||
<Notes fontSize={this.state.zoomFontSize} />
|
||||
)}
|
||||
{localStorage.getItem('todoEnabled') === 'true' && (
|
||||
<Todo fontSize={this.state.zoomFontSize} />
|
||||
)}
|
||||
{localStorage.getItem('appsEnabled') === 'true' && (
|
||||
<Apps fontSize={this.state.zoomFontSize} />
|
||||
)}
|
||||
const navbar = (
|
||||
<div className="navbar-container">
|
||||
<div className={classList}>
|
||||
{localStorage.getItem('view') === 'true' && backgroundEnabled ? (
|
||||
<Maximise fontSize={zoomFontSize} />
|
||||
) : null}
|
||||
{localStorage.getItem('notesEnabled') === 'true' && <Notes fontSize={zoomFontSize} />}
|
||||
{localStorage.getItem('todoEnabled') === 'true' && <Todo fontSize={zoomFontSize} />}
|
||||
{localStorage.getItem('appsEnabled') === 'true' && <Apps fontSize={zoomFontSize} />}
|
||||
|
||||
{this.state.refreshEnabled !== 'false' && <Refresh fontSize={this.state.zoomFontSize} />}
|
||||
{refreshEnabled !== 'false' && <Refresh fontSize={zoomFontSize} />}
|
||||
|
||||
<Tooltip
|
||||
title={variables.getMessage('modals.main.navbar.settings', {
|
||||
type: variables.getMessage(
|
||||
'modals.main.navbar.tooltips.refresh_' + this.refreshValue,
|
||||
),
|
||||
<Tooltip
|
||||
title={variables.getMessage('modals.main.navbar.settings', {
|
||||
type: variables.getMessage('modals.main.navbar.tooltips.refresh_' + refreshOption),
|
||||
})}
|
||||
>
|
||||
<button
|
||||
className="navbarButton"
|
||||
onClick={() => openModal('mainModal')}
|
||||
style={{ fontSize: zoomFontSize }}
|
||||
aria-label={variables.getMessage('modals.main.navbar.settings', {
|
||||
type: variables.getMessage('modals.main.navbar.tooltips.refresh_' + refreshOption),
|
||||
})}
|
||||
>
|
||||
<button
|
||||
className="navbarButton"
|
||||
onClick={() => this.props.openModal('mainModal')}
|
||||
style={{ fontSize: this.state.zoomFontSize }}
|
||||
aria-label={variables.getMessage('modals.main.navbar.settings', {
|
||||
type: variables.getMessage(
|
||||
'modals.main.navbar.tooltips.refresh_' + this.refreshValue,
|
||||
),
|
||||
})}
|
||||
>
|
||||
<MdSettings className="settings-icon topicons" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<MdSettings className="settings-icon topicons" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
);
|
||||
|
||||
return localStorage.getItem('navbarHover') === 'true' ? (
|
||||
<div className="navbar-hover">{navbar}</div>
|
||||
) : (
|
||||
navbar
|
||||
);
|
||||
}
|
||||
}
|
||||
return localStorage.getItem('navbarHover') === 'true' ? (
|
||||
<div className="navbar-hover">{navbar}</div>
|
||||
) : (
|
||||
navbar
|
||||
);
|
||||
};
|
||||
|
||||
export { Navbar as default, Navbar };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// TODO: make it work with pins or on click or smth
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent, memo, useState } from 'react';
|
||||
import { memo, useState, useEffect } from 'react';
|
||||
|
||||
import { MdPlaylistRemove, MdOutlineApps } from 'react-icons/md';
|
||||
import { Tooltip } from 'components/Elements';
|
||||
@@ -8,124 +8,121 @@ import { Tooltip } from 'components/Elements';
|
||||
import { shift, useFloating } from '@floating-ui/react-dom';
|
||||
import EventBus from 'utils/eventbus';
|
||||
|
||||
class Apps extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
apps: JSON.parse(localStorage.getItem('applinks')),
|
||||
visibility: localStorage.getItem('appsPinned') === 'true' ? 'visible' : 'hidden',
|
||||
marginLeft: localStorage.getItem('refresh') === 'false' ? '-200px' : '-130px',
|
||||
showApps: localStorage.getItem('appsPinned') === 'true',
|
||||
};
|
||||
}
|
||||
const Apps = ({ appsRef, floatRef, position, xPosition, yPosition }) => {
|
||||
const [apps, setApps] = useState(JSON.parse(localStorage.getItem('applinks')));
|
||||
const [visibility, setVisibility] = useState(
|
||||
localStorage.getItem('appsPinned') === 'true' ? 'visible' : 'hidden',
|
||||
);
|
||||
const [marginLeft, setMarginLeft] = useState(
|
||||
localStorage.getItem('refresh') === 'false' ? '-200px' : '-130px',
|
||||
);
|
||||
const [showApps, setShowApps] = useState(localStorage.getItem('appsPinned') === 'true');
|
||||
const [zoomFontSize, setZoomFontSize] = useState('1.2rem');
|
||||
|
||||
setZoom() {
|
||||
this.setState({
|
||||
zoomFontSize: Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem',
|
||||
});
|
||||
}
|
||||
const setZoom = () => {
|
||||
setZoomFontSize(Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem');
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
useEffect(() => {
|
||||
const handleRefresh = (data) => {
|
||||
if (data === 'navbar') {
|
||||
this.forceUpdate();
|
||||
setApps(JSON.parse(localStorage.getItem('applinks')));
|
||||
try {
|
||||
this.setZoom();
|
||||
setZoom();
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.setZoom();
|
||||
}
|
||||
setZoom();
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
}
|
||||
EventBus.on('refresh', handleRefresh);
|
||||
return () => {
|
||||
EventBus.off('refresh', handleRefresh);
|
||||
};
|
||||
}, []);
|
||||
|
||||
showApps() {
|
||||
this.setState({ showApps: true });
|
||||
}
|
||||
const handleShowApps = () => {
|
||||
setShowApps(true);
|
||||
};
|
||||
|
||||
hideApps() {
|
||||
this.setState({ showApps: localStorage.getItem('AppsPinned') === 'true' });
|
||||
}
|
||||
const handleHideApps = () => {
|
||||
setShowApps(localStorage.getItem('AppsPinned') === 'true');
|
||||
};
|
||||
|
||||
render() {
|
||||
const appsInfo = this.state.apps;
|
||||
const appsInfo = apps;
|
||||
|
||||
return (
|
||||
<div className="notes" onMouseLeave={() => this.hideApps()} onFocus={() => this.showApps()}>
|
||||
<button
|
||||
className="navbarButton"
|
||||
onMouseEnter={() => this.showApps()}
|
||||
onFocus={() => this.hideApps()}
|
||||
onBlur={() => this.showApps()}
|
||||
ref={this.props.appsRef}
|
||||
style={{ fontSize: this.state.zoomFontSize }}
|
||||
return (
|
||||
<div className="notes" onMouseLeave={handleHideApps} onFocus={handleShowApps}>
|
||||
<button
|
||||
className="navbarButton"
|
||||
onMouseEnter={handleShowApps}
|
||||
onFocus={handleHideApps}
|
||||
onBlur={handleShowApps}
|
||||
ref={appsRef}
|
||||
style={{ fontSize: zoomFontSize }}
|
||||
>
|
||||
<MdOutlineApps className="topicons" />
|
||||
</button>
|
||||
{showApps && (
|
||||
<span
|
||||
className="notesContainer"
|
||||
ref={floatRef}
|
||||
style={{
|
||||
position: position,
|
||||
top: yPosition ?? '44px',
|
||||
left: xPosition ?? '',
|
||||
}}
|
||||
>
|
||||
<MdOutlineApps className="topicons" />
|
||||
</button>
|
||||
{this.state.showApps && (
|
||||
<span
|
||||
className="notesContainer"
|
||||
ref={this.props.floatRef}
|
||||
style={{
|
||||
position: this.props.position,
|
||||
top: this.props.yPosition ?? '44px',
|
||||
left: this.props.xPosition ?? '',
|
||||
}}
|
||||
>
|
||||
<div className="flexTodo">
|
||||
<div className="topBarNotes" style={{ display: 'flex' }}>
|
||||
<MdOutlineApps />
|
||||
<span>{variables.getMessage('widgets.navbar.apps.title')}</span>
|
||||
<div className="flexTodo">
|
||||
<div className="topBarNotes" style={{ display: 'flex' }}>
|
||||
<MdOutlineApps />
|
||||
<span>{variables.getMessage('widgets.navbar.apps.title')}</span>
|
||||
</div>
|
||||
</div>
|
||||
{appsInfo.length > 0 ? (
|
||||
<div className="appsShortcutContainer">
|
||||
{appsInfo.map((info, i) => (
|
||||
<Tooltip
|
||||
title={info.name.split(' ')[0]}
|
||||
subtitle={info.name.split(' ').slice(1).join(' ')}
|
||||
key={i}
|
||||
>
|
||||
<a href={info.url} className="appsIcon">
|
||||
<img
|
||||
src={
|
||||
info.icon === ''
|
||||
? `https://icon.horse/icon/ ${info.url.replace('https://', '').replace('http://', '')}`
|
||||
: info.icon
|
||||
}
|
||||
width="40px"
|
||||
height="40px"
|
||||
alt="Google"
|
||||
/>
|
||||
<span>{info.name}</span>
|
||||
</a>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="todosEmpty">
|
||||
<div className="emptyNewMessage">
|
||||
<MdPlaylistRemove />
|
||||
<span className="title">
|
||||
{variables.language.getMessage(
|
||||
variables.languagecode,
|
||||
'widgets.navbar.apps.no_apps',
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{appsInfo.length > 0 ? (
|
||||
<div className="appsShortcutContainer">
|
||||
{appsInfo.map((info, i) => (
|
||||
<Tooltip
|
||||
title={info.name.split(' ')[0]}
|
||||
subtitle={info.name.split(' ').slice(1).join(' ')}
|
||||
key={i}
|
||||
>
|
||||
<a href={info.url} className="appsIcon">
|
||||
<img
|
||||
src={
|
||||
info.icon === ''
|
||||
? `https://icon.horse/icon/ ${info.url.replace('https://', '').replace('http://', '')}`
|
||||
: info.icon
|
||||
}
|
||||
width="40px"
|
||||
height="40px"
|
||||
alt="Google"
|
||||
/>
|
||||
<span>{info.name}</span>
|
||||
</a>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="todosEmpty">
|
||||
<div className="emptyNewMessage">
|
||||
<MdPlaylistRemove />
|
||||
<span className="title">
|
||||
{variables.language.getMessage(
|
||||
variables.languagecode,
|
||||
'widgets.navbar.apps.no_apps',
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function AppsWrapper() {
|
||||
const [reference, setReference] = useState(null);
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } from 'react';
|
||||
import { memo, useState, useCallback } from 'react';
|
||||
|
||||
import { MdCropFree } from 'react-icons/md';
|
||||
|
||||
import { Tooltip } from 'components/Elements';
|
||||
class Maximise extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
hidden: false,
|
||||
};
|
||||
}
|
||||
|
||||
setAttribute(blur, brightness, filter) {
|
||||
const Maximise = memo(({ fontSize }) => {
|
||||
const [hidden, setHidden] = useState(false);
|
||||
|
||||
const setAttribute = useCallback((blur, brightness, filter) => {
|
||||
// don't attempt to modify the background if it isn't an image
|
||||
const backgroundType = localStorage.getItem('backgroundType');
|
||||
if (
|
||||
@@ -43,48 +39,41 @@ class Maximise extends PureComponent {
|
||||
: ''
|
||||
};`,
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
maximise = () => {
|
||||
const maximise = useCallback(() => {
|
||||
// hide widgets
|
||||
const widgets = document.getElementById('widgets');
|
||||
this.state.hidden === false
|
||||
? (widgets.style.display = 'none')
|
||||
: (widgets.style.display = 'flex');
|
||||
|
||||
if (this.state.hidden === false) {
|
||||
this.setState({
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
this.setAttribute(0, 100);
|
||||
|
||||
if (!hidden) {
|
||||
widgets.style.display = 'none';
|
||||
setHidden(true);
|
||||
setAttribute(0, 100);
|
||||
variables.stats.postEvent('feature', 'Background maximise');
|
||||
} else {
|
||||
this.setState({
|
||||
hidden: false,
|
||||
});
|
||||
|
||||
this.setAttribute(localStorage.getItem('blur'), localStorage.getItem('brightness'), true);
|
||||
widgets.style.display = 'flex';
|
||||
setHidden(false);
|
||||
setAttribute(localStorage.getItem('blur'), localStorage.getItem('brightness'), true);
|
||||
variables.stats.postEvent('feature', 'Background unmaximise');
|
||||
}
|
||||
};
|
||||
}, [hidden, setAttribute]);
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Tooltip
|
||||
title={variables.getMessage('modals.main.settings.sections.background.buttons.view')}
|
||||
return (
|
||||
<Tooltip
|
||||
title={variables.getMessage('modals.main.settings.sections.background.buttons.view')}
|
||||
>
|
||||
<button
|
||||
className="navbarButton"
|
||||
style={{ fontSize }}
|
||||
onClick={maximise}
|
||||
aria-label={variables.getMessage('modals.main.settings.sections.background.buttons.view')}
|
||||
>
|
||||
<button
|
||||
className="navbarButton"
|
||||
style={{ fontSize: this.props.fontSize }}
|
||||
onClick={this.maximise}
|
||||
aria-label={variables.getMessage('modals.main.settings.sections.background.buttons.view')}
|
||||
>
|
||||
<MdCropFree className="topicons" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
<MdCropFree className="topicons" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
Maximise.displayName = 'Maximise';
|
||||
|
||||
export { Maximise as default, Maximise };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent, memo, useState } from 'react';
|
||||
import { memo, useState, useEffect } from 'react';
|
||||
|
||||
import { MdContentCopy, MdAssignment, MdPushPin, MdDownload } from 'react-icons/md';
|
||||
import { useFloating, shift } from '@floating-ui/react-dom';
|
||||
@@ -10,137 +10,119 @@ import { Tooltip } from 'components/Elements';
|
||||
import { saveFile } from 'utils/saveFile';
|
||||
import EventBus from 'utils/eventbus';
|
||||
|
||||
class Notes extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
notes: localStorage.getItem('notes') || '',
|
||||
visibility: localStorage.getItem('notesPinned') === 'true' ? 'visible' : 'hidden',
|
||||
showNotes: localStorage.getItem('notesPinned') === 'true' ? true : false,
|
||||
};
|
||||
}
|
||||
const Notes = ({ notesRef, floatRef, position, xPosition, yPosition }) => {
|
||||
const [notes, setNotes] = useState(localStorage.getItem('notes') || '');
|
||||
const [showNotes, setShowNotes] = useState(localStorage.getItem('notesPinned') === 'true');
|
||||
const [zoomFontSize, setZoomFontSize] = useState('1.2rem');
|
||||
|
||||
setZoom() {
|
||||
this.setState({
|
||||
zoomFontSize: Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem',
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
useEffect(() => {
|
||||
const handleRefresh = (data) => {
|
||||
if (data === 'navbar') {
|
||||
this.forceUpdate();
|
||||
try {
|
||||
this.setZoom();
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
setZoomFontSize(Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.setZoom();
|
||||
}
|
||||
setZoomFontSize(Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem');
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
}
|
||||
EventBus.on('refresh', handleRefresh);
|
||||
return () => {
|
||||
EventBus.off('refresh');
|
||||
};
|
||||
}, []);
|
||||
|
||||
setNotes = (e) => {
|
||||
const handleSetNotes = (e) => {
|
||||
localStorage.setItem('notes', e.target.value);
|
||||
this.setState({ notes: e.target.value });
|
||||
setNotes(e.target.value);
|
||||
};
|
||||
|
||||
showNotes() {
|
||||
this.setState({ showNotes: true });
|
||||
}
|
||||
const handleShowNotes = () => {
|
||||
setShowNotes(true);
|
||||
};
|
||||
|
||||
hideNotes() {
|
||||
this.setState({ showNotes: localStorage.getItem('notesPinned') === 'true' });
|
||||
}
|
||||
const handleHideNotes = () => {
|
||||
setShowNotes(localStorage.getItem('notesPinned') === 'true');
|
||||
};
|
||||
|
||||
pin() {
|
||||
const handlePin = () => {
|
||||
variables.stats.postEvent('feature', 'Notes pin');
|
||||
const notesPinned = localStorage.getItem('notesPinned') === 'true';
|
||||
localStorage.setItem('notesPinned', !notesPinned);
|
||||
this.setState({ showNotes: !notesPinned });
|
||||
}
|
||||
setShowNotes(!notesPinned);
|
||||
};
|
||||
|
||||
copy() {
|
||||
const handleCopy = () => {
|
||||
variables.stats.postEvent('feature', 'Notes copied');
|
||||
navigator.clipboard.writeText(this.state.notes);
|
||||
navigator.clipboard.writeText(notes);
|
||||
toast(variables.getMessage('toasts.notes'));
|
||||
}
|
||||
};
|
||||
|
||||
download() {
|
||||
const notes = localStorage.getItem('notes');
|
||||
const handleDownload = () => {
|
||||
if (!notes || notes === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
variables.stats.postEvent('feature', 'Notes download');
|
||||
saveFile(this.state.notes, 'mue-notes.txt', 'text/plain');
|
||||
}
|
||||
saveFile(notes, 'mue-notes.txt', 'text/plain');
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="notes" onMouseLeave={() => this.hideNotes()} onFocus={() => this.showNotes()}>
|
||||
<button
|
||||
className="navbarButton"
|
||||
onMouseEnter={() => this.showNotes()}
|
||||
onFocus={() => this.showNotes()}
|
||||
onBlur={() => this.hideNotes()}
|
||||
ref={this.props.notesRef}
|
||||
style={{ fontSize: this.state.zoomFontSize }}
|
||||
aria-label={variables.getMessage('widgets.navbar.notes.title')}
|
||||
return (
|
||||
<div className="notes" onMouseLeave={handleHideNotes} onFocus={handleShowNotes}>
|
||||
<button
|
||||
className="navbarButton"
|
||||
onMouseEnter={handleShowNotes}
|
||||
onFocus={handleShowNotes}
|
||||
onBlur={handleHideNotes}
|
||||
ref={notesRef}
|
||||
style={{ fontSize: zoomFontSize }}
|
||||
aria-label={variables.getMessage('widgets.navbar.notes.title')}
|
||||
>
|
||||
<MdAssignment className="topicons" />
|
||||
</button>
|
||||
{showNotes && (
|
||||
<span
|
||||
className="notesContainer"
|
||||
ref={floatRef}
|
||||
style={{
|
||||
position: position,
|
||||
top: yPosition ?? '44',
|
||||
left: xPosition ?? '',
|
||||
}}
|
||||
>
|
||||
<MdAssignment className="topicons" />
|
||||
</button>
|
||||
{this.state.showNotes && (
|
||||
<span
|
||||
className="notesContainer"
|
||||
ref={this.props.floatRef}
|
||||
style={{
|
||||
position: this.props.position,
|
||||
top: this.props.yPosition ?? '44',
|
||||
left: this.props.xPosition ?? '',
|
||||
}}
|
||||
>
|
||||
<div className="flexNotes">
|
||||
<div className="topBarNotes" style={{ display: 'flex' }}>
|
||||
<MdAssignment />
|
||||
<span>{variables.getMessage('widgets.navbar.notes.title')}</span>
|
||||
</div>
|
||||
<div className="notes-buttons">
|
||||
<Tooltip title={variables.getMessage('widgets.navbar.todo.pin')}>
|
||||
<button onClick={() => this.pin()}>
|
||||
<MdPushPin />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip title={variables.getMessage('widgets.quote.copy')}>
|
||||
<button onClick={() => this.copy()} disabled={this.state.notes === ''}>
|
||||
<MdContentCopy />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip title={variables.getMessage('widgets.background.download')}>
|
||||
<button onClick={() => this.download()} disabled={this.state.notes === ''}>
|
||||
<MdDownload />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<TextareaAutosize
|
||||
placeholder={variables.getMessage('widgets.navbar.notes.placeholder')}
|
||||
value={this.state.notes}
|
||||
onChange={this.setNotes}
|
||||
minRows={5}
|
||||
maxLength={10000}
|
||||
/>
|
||||
<div className="flexNotes">
|
||||
<div className="topBarNotes" style={{ display: 'flex' }}>
|
||||
<MdAssignment />
|
||||
<span>{variables.getMessage('widgets.navbar.notes.title')}</span>
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
<div className="notes-buttons">
|
||||
<Tooltip title={variables.getMessage('widgets.navbar.todo.pin')}>
|
||||
<button onClick={handlePin}>
|
||||
<MdPushPin />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip title={variables.getMessage('widgets.quote.copy')}>
|
||||
<button onClick={handleCopy} disabled={notes === ''}>
|
||||
<MdContentCopy />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip title={variables.getMessage('widgets.background.download')}>
|
||||
<button onClick={handleDownload} disabled={notes === ''}>
|
||||
<MdDownload />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<TextareaAutosize
|
||||
placeholder={variables.getMessage('widgets.navbar.notes.placeholder')}
|
||||
value={notes}
|
||||
onChange={handleSetNotes}
|
||||
minRows={5}
|
||||
maxLength={10000}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function NotesWrapper() {
|
||||
const [reference, setReference] = useState(null);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PureComponent, createRef } from 'react';
|
||||
import { memo, useRef, useState, useEffect, useCallback } from 'react';
|
||||
import { Tooltip } from 'components/Elements';
|
||||
|
||||
import EventBus from 'utils/eventbus';
|
||||
@@ -18,18 +18,13 @@ const readQuicklinks = () => {
|
||||
}
|
||||
};
|
||||
|
||||
class QuickLinks extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
items: readQuicklinks(),
|
||||
forceUpdate: 0, // Used to force complete re-render
|
||||
};
|
||||
this.quicklinksContainer = createRef();
|
||||
}
|
||||
const QuickLinks = memo(() => {
|
||||
const [items, setItems] = useState(readQuicklinks());
|
||||
const [forceUpdate, setForceUpdate] = useState(0); // Used to force complete re-render
|
||||
const quicklinksContainer = useRef(null);
|
||||
|
||||
// widget zoom
|
||||
setZoom(element) {
|
||||
const setZoom = useCallback((element) => {
|
||||
if (!element) return;
|
||||
|
||||
const zoom = localStorage.getItem('zoomQuicklinks') || 100;
|
||||
@@ -42,103 +37,109 @@ class QuickLinks extends PureComponent {
|
||||
img.style.height = `${30 * Number(zoom / 100)}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
useEffect(() => {
|
||||
const handleRefresh = (data) => {
|
||||
if (data === 'quicklinks') {
|
||||
if (localStorage.getItem('quicklinksenabled') === 'false') {
|
||||
return (this.quicklinksContainer.current.style.display = 'none');
|
||||
if (quicklinksContainer.current) {
|
||||
quicklinksContainer.current.style.display = 'none';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.quicklinksContainer.current.style.display = 'flex';
|
||||
if (quicklinksContainer.current) {
|
||||
quicklinksContainer.current.style.display = 'flex';
|
||||
}
|
||||
|
||||
this.setState({
|
||||
items: readQuicklinks(),
|
||||
forceUpdate: Date.now(),
|
||||
}, () => {
|
||||
this.setZoom(this.quicklinksContainer.current);
|
||||
});
|
||||
setItems(readQuicklinks());
|
||||
setForceUpdate(Date.now());
|
||||
}
|
||||
});
|
||||
|
||||
this.setZoom(this.quicklinksContainer.current);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
}
|
||||
|
||||
render() {
|
||||
let target,
|
||||
rel = null;
|
||||
if (localStorage.getItem('quicklinksnewtab') === 'true') {
|
||||
target = '_blank';
|
||||
rel = 'noopener noreferrer';
|
||||
}
|
||||
|
||||
const tooltipEnabled = localStorage.getItem('quicklinkstooltip');
|
||||
|
||||
const quickLink = (item, index) => {
|
||||
if (localStorage.getItem('quickLinksStyle') === 'text') {
|
||||
return (
|
||||
<a
|
||||
className="quicklinkstext"
|
||||
key={`quicklink-${item.key}-${index}`}
|
||||
href={item.url}
|
||||
target={target}
|
||||
rel={rel}
|
||||
draggable={false}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const img =
|
||||
item.icon ||
|
||||
'https://icon.horse/icon/' + item.url.replace('https://', '').replace('http://', '');
|
||||
|
||||
if (localStorage.getItem('quickLinksStyle') === 'metro') {
|
||||
return (
|
||||
<a
|
||||
className="quickLinksMetro"
|
||||
key={`quicklink-${item.key}-${index}`}
|
||||
href={item.url}
|
||||
target={target}
|
||||
rel={rel}
|
||||
draggable={false}
|
||||
>
|
||||
<img src={img} alt={item.name} draggable={false} />
|
||||
<span className="subtitle">{item.name}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const link = (
|
||||
<a key={`quicklink-${item.key}-${index}`} href={item.url} target={target} rel={rel} draggable={false}>
|
||||
<img src={img} alt={item.name} draggable={false} />
|
||||
</a>
|
||||
);
|
||||
|
||||
return tooltipEnabled === 'true' ? (
|
||||
<Tooltip title={item.name} placement="bottom" key={`quicklink-${item.key}-${index}`}>
|
||||
{link}
|
||||
</Tooltip>
|
||||
) : (
|
||||
link
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="quicklinkscontainer"
|
||||
ref={this.quicklinksContainer}
|
||||
>
|
||||
{this.state.items && this.state.items.map((item, index) => quickLink(item, index))}
|
||||
</div>
|
||||
);
|
||||
EventBus.on('refresh', handleRefresh);
|
||||
|
||||
setZoom(quicklinksContainer.current);
|
||||
|
||||
return () => {
|
||||
EventBus.off('refresh', handleRefresh);
|
||||
};
|
||||
}, [setZoom]);
|
||||
|
||||
useEffect(() => {
|
||||
setZoom(quicklinksContainer.current);
|
||||
}, [items, forceUpdate, setZoom]);
|
||||
|
||||
let target, rel = null;
|
||||
if (localStorage.getItem('quicklinksnewtab') === 'true') {
|
||||
target = '_blank';
|
||||
rel = 'noopener noreferrer';
|
||||
}
|
||||
}
|
||||
|
||||
const tooltipEnabled = localStorage.getItem('quicklinkstooltip');
|
||||
|
||||
const quickLink = (item, index) => {
|
||||
if (localStorage.getItem('quickLinksStyle') === 'text') {
|
||||
return (
|
||||
<a
|
||||
className="quicklinkstext"
|
||||
key={`quicklink-${item.key}-${index}`}
|
||||
href={item.url}
|
||||
target={target}
|
||||
rel={rel}
|
||||
draggable={false}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const img =
|
||||
item.icon ||
|
||||
'https://icon.horse/icon/' + item.url.replace('https://', '').replace('http://', '');
|
||||
|
||||
if (localStorage.getItem('quickLinksStyle') === 'metro') {
|
||||
return (
|
||||
<a
|
||||
className="quickLinksMetro"
|
||||
key={`quicklink-${item.key}-${index}`}
|
||||
href={item.url}
|
||||
target={target}
|
||||
rel={rel}
|
||||
draggable={false}
|
||||
>
|
||||
<img src={img} alt={item.name} draggable={false} />
|
||||
<span className="subtitle">{item.name}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const link = (
|
||||
<a key={`quicklink-${item.key}-${index}`} href={item.url} target={target} rel={rel} draggable={false}>
|
||||
<img src={img} alt={item.name} draggable={false} />
|
||||
</a>
|
||||
);
|
||||
|
||||
return tooltipEnabled === 'true' ? (
|
||||
<Tooltip title={item.name} placement="bottom" key={`quicklink-${item.key}-${index}`}>
|
||||
{link}
|
||||
</Tooltip>
|
||||
) : (
|
||||
link
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="quicklinkscontainer"
|
||||
ref={quicklinksContainer}
|
||||
>
|
||||
{items && items.map((item, index) => quickLink(item, index))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
QuickLinks.displayName = 'QuickLinks';
|
||||
|
||||
export { QuickLinks as default, QuickLinks };
|
||||
@@ -1,190 +1,58 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent, createRef } from 'react';
|
||||
import { MdAddLink, MdLinkOff, MdOutlineDragIndicator, MdEdit, MdDelete } from 'react-icons/md';
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragOverlay,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { MdAddLink, MdLinkOff } from 'react-icons/md';
|
||||
import { arrayMove } from '@dnd-kit/sortable';
|
||||
import { Header, Row, Content, Action, PreferencesWrapper } from 'components/Layout/Settings';
|
||||
import { Checkbox, Dropdown } from 'components/Form/Settings';
|
||||
import { Button } from 'components/Elements';
|
||||
import Modal from 'react-modal';
|
||||
|
||||
import { AddModal } from 'components/Elements/AddModal';
|
||||
import { SortableList } from './components';
|
||||
import { readQuicklinks } from './utils/quicklinksUtils';
|
||||
|
||||
import EventBus from 'utils/eventbus';
|
||||
import { getTitleFromUrl, isValidUrl } from 'utils/links';
|
||||
|
||||
const readQuicklinks = () => {
|
||||
try {
|
||||
const raw = localStorage.getItem('quicklinks');
|
||||
if (!raw) return [];
|
||||
const data = JSON.parse(raw);
|
||||
return Array.isArray(data) ? data : [];
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse quicklinks from localStorage. Resetting to []', e);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
const QuickLinksOptions = () => {
|
||||
const [items, setItems] = useState(readQuicklinks());
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [urlError, setUrlError] = useState('');
|
||||
const [iconError, setIconError] = useState('');
|
||||
const [edit, setEdit] = useState(false);
|
||||
const [editData, setEditData] = useState('');
|
||||
const [enabled, setEnabled] = useState(localStorage.getItem('quicklinksenabled') !== 'false');
|
||||
|
||||
const DragHandle = () => (
|
||||
<div className="quicklink-drag-handle" aria-hidden="true">
|
||||
<MdOutlineDragIndicator />
|
||||
</div>
|
||||
);
|
||||
const quicklinksContainer = useRef();
|
||||
const silenceEventRef = useRef(false);
|
||||
|
||||
const SortableItem = ({ value, enabled, startEditLink, deleteLink }) => {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: value.key,
|
||||
disabled: !enabled,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
|
||||
const getIconUrl = (item) => {
|
||||
return (
|
||||
item.icon ||
|
||||
'https://icon.horse/icon/' + item.url.replace('https://', '').replace('http://', '')
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className={`quicklink-item ${!enabled ? 'disabled' : ''}`}
|
||||
role="listitem"
|
||||
>
|
||||
<DragHandle />
|
||||
<div className="quicklink-icon">
|
||||
<img src={getIconUrl(value)} alt={value.name} draggable={false} />
|
||||
</div>
|
||||
<div className="quicklink-content">
|
||||
<div className="quicklink-name">{value.name}</div>
|
||||
<div className="quicklink-url">{value.url}</div>
|
||||
</div>
|
||||
<div className="quicklink-actions">
|
||||
<button
|
||||
className="quicklink-action-btn"
|
||||
onClick={(e) => {
|
||||
if (!enabled) return;
|
||||
e.stopPropagation();
|
||||
startEditLink(value);
|
||||
}}
|
||||
title="Edit"
|
||||
disabled={!enabled}
|
||||
aria-disabled={!enabled}
|
||||
>
|
||||
<MdEdit />
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button
|
||||
className="quicklink-action-btn quicklink-remove-btn"
|
||||
onClick={(e) => {
|
||||
if (!enabled) return;
|
||||
e.stopPropagation();
|
||||
deleteLink(value.key, e);
|
||||
}}
|
||||
title="Remove"
|
||||
disabled={!enabled}
|
||||
aria-disabled={!enabled}
|
||||
>
|
||||
<MdDelete />
|
||||
<span>Remove</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SortableList = ({ items, enabled, onDragEnd, startEditLink, deleteLink }) => {
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
|
||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
||||
);
|
||||
|
||||
return (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
|
||||
<SortableContext items={items.map((item) => item.key)} strategy={verticalListSortingStrategy}>
|
||||
<div className="quicklinks-list" role="list">
|
||||
{items.map((item) => (
|
||||
<SortableItem
|
||||
key={item.key}
|
||||
value={item}
|
||||
enabled={enabled}
|
||||
startEditLink={startEditLink}
|
||||
deleteLink={deleteLink}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
class QuickLinksOptions extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
items: readQuicklinks(),
|
||||
showAddModal: false,
|
||||
urlError: '',
|
||||
iconError: '',
|
||||
edit: false,
|
||||
editData: '',
|
||||
enabled: localStorage.getItem('quicklinksenabled') !== 'false',
|
||||
};
|
||||
this.quicklinksContainer = createRef();
|
||||
this.silenceEvent = false;
|
||||
this.handleRefresh = null;
|
||||
}
|
||||
|
||||
setContainerDisplay(enabled) {
|
||||
if (!this.quicklinksContainer || !this.quicklinksContainer.current) return;
|
||||
const el = this.quicklinksContainer.current;
|
||||
const setContainerDisplay = (enabled) => {
|
||||
if (!quicklinksContainer || !quicklinksContainer.current) return;
|
||||
const el = quicklinksContainer.current;
|
||||
el.classList.toggle('disabled', !enabled);
|
||||
if (!enabled) {
|
||||
el.setAttribute('aria-hidden', 'true');
|
||||
} else {
|
||||
el.removeAttribute('aria-hidden');
|
||||
}
|
||||
}
|
||||
deleteLink(key, event) {
|
||||
};
|
||||
|
||||
const deleteLink = (key, event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const stored = readQuicklinks();
|
||||
const data = stored.filter((i) => i.key !== key);
|
||||
this.silenceEvent = true;
|
||||
silenceEventRef.current = true;
|
||||
localStorage.setItem('quicklinks', JSON.stringify(data));
|
||||
this.setState({ items: data }, () => {
|
||||
variables.stats.postEvent('feature', 'Quicklink delete');
|
||||
EventBus.emit('refresh', 'quicklinks');
|
||||
setTimeout(() => {
|
||||
this.silenceEvent = false;
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
setItems(data);
|
||||
variables.stats.postEvent('feature', 'Quicklink delete');
|
||||
EventBus.emit('refresh', 'quicklinks');
|
||||
setTimeout(() => {
|
||||
silenceEventRef.current = false;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
async addLink(name, url, icon) {
|
||||
const addLink = async (name, url, icon) => {
|
||||
const data = readQuicklinks();
|
||||
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
@@ -192,11 +60,13 @@ class QuickLinksOptions extends PureComponent {
|
||||
}
|
||||
|
||||
if (url.length <= 0 || isValidUrl(url) === false) {
|
||||
return this.setState({ urlError: variables.getMessage('widgets.quicklinks.url_error') });
|
||||
setUrlError(variables.getMessage('widgets.quicklinks.url_error'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (icon.length > 0 && isValidUrl(icon) === false) {
|
||||
return this.setState({ iconError: variables.getMessage('widgets.quicklinks.url_error') });
|
||||
setIconError(variables.getMessage('widgets.quicklinks.url_error'));
|
||||
return;
|
||||
}
|
||||
|
||||
const newItem = {
|
||||
@@ -208,22 +78,26 @@ class QuickLinksOptions extends PureComponent {
|
||||
|
||||
data.push(newItem);
|
||||
|
||||
this.silenceEvent = true;
|
||||
silenceEventRef.current = true;
|
||||
localStorage.setItem('quicklinks', JSON.stringify(data));
|
||||
this.setState({ items: data, showAddModal: false, urlError: '', iconError: '' }, () => {
|
||||
variables.stats.postEvent('feature', 'Quicklink add');
|
||||
EventBus.emit('refresh', 'quicklinks');
|
||||
setTimeout(() => {
|
||||
this.silenceEvent = false;
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
setItems(data);
|
||||
setShowAddModal(false);
|
||||
setUrlError('');
|
||||
setIconError('');
|
||||
variables.stats.postEvent('feature', 'Quicklink add');
|
||||
EventBus.emit('refresh', 'quicklinks');
|
||||
setTimeout(() => {
|
||||
silenceEventRef.current = false;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
startEditLink(data) {
|
||||
this.setState({ edit: true, editData: data, showAddModal: true });
|
||||
}
|
||||
const startEditLink = (data) => {
|
||||
setEdit(true);
|
||||
setEditData(data);
|
||||
setShowAddModal(true);
|
||||
};
|
||||
|
||||
async editLink(og, name, url, icon) {
|
||||
const editLink = async (og, name, url, icon) => {
|
||||
const data = readQuicklinks();
|
||||
const dataobj = data.find((i) => i.key === og.key);
|
||||
if (!dataobj) return;
|
||||
@@ -232,233 +106,220 @@ class QuickLinksOptions extends PureComponent {
|
||||
dataobj.url = url;
|
||||
dataobj.icon = icon || '';
|
||||
|
||||
this.silenceEvent = true;
|
||||
silenceEventRef.current = true;
|
||||
localStorage.setItem('quicklinks', JSON.stringify(data));
|
||||
this.setState({ items: data, showAddModal: false, edit: false }, () => {
|
||||
EventBus.emit('refresh', 'quicklinks');
|
||||
setTimeout(() => {
|
||||
this.silenceEvent = false;
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
arrayMove = (array, oldIndex, newIndex) => {
|
||||
const result = Array.from(array);
|
||||
const [removed] = result.splice(oldIndex, 1);
|
||||
result.splice(newIndex, 0, removed);
|
||||
return result;
|
||||
setItems(data);
|
||||
setShowAddModal(false);
|
||||
setEdit(false);
|
||||
EventBus.emit('refresh', 'quicklinks');
|
||||
setTimeout(() => {
|
||||
silenceEventRef.current = false;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
handleDragEnd = (event) => {
|
||||
const handleDragEnd = (event) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (!over || !this.state.enabled) return;
|
||||
if (!over || !enabled) return;
|
||||
if (active.id === over.id) return;
|
||||
|
||||
const oldIndex = this.state.items.findIndex((item) => item.key === active.id);
|
||||
const newIndex = this.state.items.findIndex((item) => item.key === over.id);
|
||||
const oldIndex = items.findIndex((item) => item.key === active.id);
|
||||
const newIndex = items.findIndex((item) => item.key === over.id);
|
||||
|
||||
if (oldIndex === -1 || newIndex === -1) return;
|
||||
|
||||
const newItems = arrayMove(this.state.items, oldIndex, newIndex);
|
||||
const newItems = arrayMove(items, oldIndex, newIndex);
|
||||
|
||||
this.silenceEvent = true;
|
||||
this.setState({ items: newItems }, () => {
|
||||
localStorage.setItem('quicklinks', JSON.stringify(newItems));
|
||||
EventBus.emit('refresh', 'quicklinks');
|
||||
setTimeout(() => {
|
||||
this.silenceEvent = false;
|
||||
}, 0);
|
||||
});
|
||||
silenceEventRef.current = true;
|
||||
setItems(newItems);
|
||||
localStorage.setItem('quicklinks', JSON.stringify(newItems));
|
||||
EventBus.emit('refresh', 'quicklinks');
|
||||
setTimeout(() => {
|
||||
silenceEventRef.current = false;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.setContainerDisplay(this.state.enabled);
|
||||
useEffect(() => {
|
||||
setContainerDisplay(enabled);
|
||||
}, [enabled]);
|
||||
|
||||
this.handleRefresh = (data) => {
|
||||
useEffect(() => {
|
||||
setContainerDisplay(enabled);
|
||||
|
||||
const handleRefresh = (data) => {
|
||||
if (data !== 'quicklinks') return;
|
||||
if (this.silenceEvent) return;
|
||||
if (silenceEventRef.current) return;
|
||||
|
||||
const enabled = localStorage.getItem('quicklinksenabled') !== 'false';
|
||||
const newEnabled = localStorage.getItem('quicklinksenabled') !== 'false';
|
||||
const newItems = readQuicklinks();
|
||||
const oldItems = this.state.items || [];
|
||||
const oldKeys = new Set(oldItems.map((i) => i.key));
|
||||
const newKeys = new Set(newItems.map((i) => i.key));
|
||||
const oldItems = items || [];
|
||||
|
||||
const keysEqual =
|
||||
oldItems.length === newItems.length && oldItems.every((i) => newKeys.has(i.key));
|
||||
oldItems.length === newItems.length &&
|
||||
oldItems.every((i) => newItems.some((n) => n.key === i.key));
|
||||
|
||||
if (enabled !== this.state.enabled || !keysEqual) {
|
||||
this.setContainerDisplay(enabled);
|
||||
this.setState({ items: newItems, enabled });
|
||||
if (newEnabled !== enabled || !keysEqual) {
|
||||
setContainerDisplay(newEnabled);
|
||||
setItems(newItems);
|
||||
setEnabled(newEnabled);
|
||||
}
|
||||
};
|
||||
|
||||
EventBus.on('refresh', this.handleRefresh);
|
||||
}
|
||||
EventBus.on('refresh', handleRefresh);
|
||||
return () => {
|
||||
EventBus.off('refresh', handleRefresh);
|
||||
};
|
||||
}, [enabled, items]);
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.enabled !== this.state.enabled) {
|
||||
this.setContainerDisplay(this.state.enabled);
|
||||
}
|
||||
}
|
||||
const QUICKLINKS_SECTION = 'modals.main.settings.sections.quicklinks';
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.handleRefresh) {
|
||||
EventBus.off('refresh', this.handleRefresh);
|
||||
} else {
|
||||
try {
|
||||
EventBus.off('refresh');
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const QUICKLINKS_SECTION = 'modals.main.settings.sections.quicklinks';
|
||||
const { enabled } = this.state;
|
||||
|
||||
const AdditionalSettings = () => (
|
||||
<Row>
|
||||
<Content
|
||||
title={variables.getMessage('modals.main.settings.additional_settings')}
|
||||
subtitle={variables.getMessage(`${QUICKLINKS_SECTION}.additional`)}
|
||||
/>
|
||||
<Action>
|
||||
<Checkbox
|
||||
name="quicklinksnewtab"
|
||||
text={variables.getMessage(`${QUICKLINKS_SECTION}.open_new`)}
|
||||
category="quicklinks"
|
||||
/>
|
||||
<Checkbox
|
||||
name="quicklinkstooltip"
|
||||
text={variables.getMessage(`${QUICKLINKS_SECTION}.tooltip`)}
|
||||
category="quicklinks"
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
|
||||
const StylingOptions = () => (
|
||||
<Row>
|
||||
<Content
|
||||
title={variables.getMessage(`${QUICKLINKS_SECTION}.styling`)}
|
||||
subtitle={variables.getMessage(
|
||||
'modals.main.settings.sections.quicklinks.styling_description',
|
||||
)}
|
||||
/>
|
||||
<Action>
|
||||
<Dropdown
|
||||
label={variables.getMessage(`${QUICKLINKS_SECTION}.style`)}
|
||||
name="quickLinksStyle"
|
||||
category="quicklinks"
|
||||
items={[
|
||||
{ value: 'icon', text: variables.getMessage(`${QUICKLINKS_SECTION}.options.icon`) },
|
||||
{
|
||||
value: 'text_only',
|
||||
text: variables.getMessage(`${QUICKLINKS_SECTION}.options.text_only`),
|
||||
},
|
||||
{ value: 'metro', text: variables.getMessage(`${QUICKLINKS_SECTION}.options.metro`) },
|
||||
]}
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
|
||||
const AddLink = () => (
|
||||
<Row final={true}>
|
||||
<Content title={variables.getMessage(`${QUICKLINKS_SECTION}.title`)} />
|
||||
<Action>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => enabled && this.setState({ showAddModal: true })}
|
||||
icon={<MdAddLink />}
|
||||
label={variables.getMessage(`${QUICKLINKS_SECTION}.add_link`)}
|
||||
disabled={!enabled}
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
title={variables.getMessage(`${QUICKLINKS_SECTION}.title`)}
|
||||
setting="quicklinksenabled"
|
||||
const AdditionalSettings = () => (
|
||||
<Row>
|
||||
<Content
|
||||
title={variables.getMessage('modals.main.settings.additional_settings')}
|
||||
subtitle={variables.getMessage(`${QUICKLINKS_SECTION}.additional`)}
|
||||
/>
|
||||
<Action>
|
||||
<Checkbox
|
||||
name="quicklinksnewtab"
|
||||
text={variables.getMessage(`${QUICKLINKS_SECTION}.open_new`)}
|
||||
category="quicklinks"
|
||||
element=".quicklinks-container"
|
||||
zoomSetting="zoomQuicklinks"
|
||||
visibilityToggle={true}
|
||||
/>
|
||||
|
||||
<PreferencesWrapper
|
||||
setting="quicklinksenabled"
|
||||
<Checkbox
|
||||
name="quicklinkstooltip"
|
||||
text={variables.getMessage(`${QUICKLINKS_SECTION}.tooltip`)}
|
||||
category="quicklinks"
|
||||
visibilityToggle={true}
|
||||
zoomSetting="zoomQuicklinks"
|
||||
>
|
||||
<AdditionalSettings />
|
||||
<StylingOptions />
|
||||
<AddLink />
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
|
||||
{this.state.items.length === 0 && (
|
||||
<div className="photosEmpty">
|
||||
<div className="emptyNewMessage">
|
||||
<MdLinkOff />
|
||||
<span className="title">
|
||||
{variables.getMessage(`${QUICKLINKS_SECTION}.no_quicklinks`)}
|
||||
</span>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage('modals.main.settings.sections.message.add_some')}
|
||||
</span>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.setState({ showAddModal: true })}
|
||||
icon={<MdAddLink />}
|
||||
label={variables.getMessage(`${QUICKLINKS_SECTION}.add_link`)}
|
||||
/>
|
||||
</div>
|
||||
const StylingOptions = () => (
|
||||
<Row>
|
||||
<Content
|
||||
title={variables.getMessage(`${QUICKLINKS_SECTION}.styling`)}
|
||||
subtitle={variables.getMessage(
|
||||
'modals.main.settings.sections.quicklinks.styling_description',
|
||||
)}
|
||||
/>
|
||||
<Action>
|
||||
<Dropdown
|
||||
label={variables.getMessage(`${QUICKLINKS_SECTION}.style`)}
|
||||
name="quickLinksStyle"
|
||||
category="quicklinks"
|
||||
items={[
|
||||
{ value: 'icon', text: variables.getMessage(`${QUICKLINKS_SECTION}.options.icon`) },
|
||||
{
|
||||
value: 'text_only',
|
||||
text: variables.getMessage(`${QUICKLINKS_SECTION}.options.text_only`),
|
||||
},
|
||||
{ value: 'metro', text: variables.getMessage(`${QUICKLINKS_SECTION}.options.metro`) },
|
||||
]}
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
|
||||
const AddLink = () => (
|
||||
<Row final={true}>
|
||||
<Content title={variables.getMessage(`${QUICKLINKS_SECTION}.title`)} />
|
||||
<Action>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => enabled && setShowAddModal(true)}
|
||||
icon={<MdAddLink />}
|
||||
label={variables.getMessage(`${QUICKLINKS_SECTION}.add_link`)}
|
||||
disabled={!enabled}
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
title={variables.getMessage(`${QUICKLINKS_SECTION}.title`)}
|
||||
setting="quicklinksenabled"
|
||||
category="quicklinks"
|
||||
element=".quicklinks-container"
|
||||
zoomSetting="zoomQuicklinks"
|
||||
visibilityToggle={true}
|
||||
/>
|
||||
|
||||
<PreferencesWrapper
|
||||
setting="quicklinksenabled"
|
||||
category="quicklinks"
|
||||
visibilityToggle={true}
|
||||
zoomSetting="zoomQuicklinks"
|
||||
>
|
||||
<AdditionalSettings />
|
||||
<StylingOptions />
|
||||
<AddLink />
|
||||
|
||||
{items.length === 0 && (
|
||||
<div className="photosEmpty">
|
||||
<div className="emptyNewMessage">
|
||||
<MdLinkOff />
|
||||
<span className="title">
|
||||
{variables.getMessage(`${QUICKLINKS_SECTION}.no_quicklinks`)}
|
||||
</span>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage('modals.main.settings.sections.message.add_some')}
|
||||
</span>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => setShowAddModal(true)}
|
||||
icon={<MdAddLink />}
|
||||
label={variables.getMessage(`${QUICKLINKS_SECTION}.add_link`)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PreferencesWrapper>
|
||||
<div
|
||||
className={`quicklinks-container ${!enabled ? 'disabled' : ''}`}
|
||||
ref={this.quicklinksContainer}
|
||||
aria-hidden={!enabled}
|
||||
>
|
||||
<div className={`messagesContainer ${!enabled ? 'disabled' : ''}`}>
|
||||
<SortableList
|
||||
items={this.state.items}
|
||||
enabled={enabled}
|
||||
onDragEnd={this.handleDragEnd}
|
||||
startEditLink={(data) => this.startEditLink(data)}
|
||||
deleteLink={(key, e) => this.deleteLink(key, e)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
closeTimeoutMS={100}
|
||||
onRequestClose={() => this.setState({ showAddModal: false, urlError: '', iconError: '' })}
|
||||
isOpen={this.state.showAddModal}
|
||||
className="Modal resetmodal mainModal"
|
||||
overlayClassName="Overlay resetoverlay"
|
||||
ariaHideApp={false}
|
||||
>
|
||||
<AddModal
|
||||
urlError={this.state.urlError}
|
||||
addLink={(name, url, icon) => this.addLink(name, url, icon)}
|
||||
editLink={(og, name, url, icon) => this.editLink(og, name, url, icon)}
|
||||
edit={this.state.edit}
|
||||
editData={this.state.editData}
|
||||
closeModal={() =>
|
||||
this.setState({ showAddModal: false, urlError: '', iconError: '', edit: false })
|
||||
}
|
||||
)}
|
||||
</PreferencesWrapper>
|
||||
<div
|
||||
className={`quicklinks-container ${!enabled ? 'disabled' : ''}`}
|
||||
ref={quicklinksContainer}
|
||||
aria-hidden={!enabled}
|
||||
>
|
||||
<div className={`messagesContainer ${!enabled ? 'disabled' : ''}`}>
|
||||
<SortableList
|
||||
items={items}
|
||||
enabled={enabled}
|
||||
onDragEnd={handleDragEnd}
|
||||
startEditLink={(data) => startEditLink(data)}
|
||||
deleteLink={(key, e) => deleteLink(key, e)}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
closeTimeoutMS={100}
|
||||
onRequestClose={() => {
|
||||
setShowAddModal(false);
|
||||
setUrlError('');
|
||||
setIconError('');
|
||||
}}
|
||||
isOpen={showAddModal}
|
||||
className="Modal resetmodal mainModal"
|
||||
overlayClassName="Overlay resetoverlay"
|
||||
ariaHideApp={false}
|
||||
>
|
||||
<AddModal
|
||||
urlError={urlError}
|
||||
addLink={(name, url, icon) => addLink(name, url, icon)}
|
||||
editLink={(og, name, url, icon) => editLink(og, name, url, icon)}
|
||||
edit={edit}
|
||||
editData={editData}
|
||||
closeModal={() => {
|
||||
setShowAddModal(false);
|
||||
setUrlError('');
|
||||
setIconError('');
|
||||
setEdit(false);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { QuickLinksOptions as default, QuickLinksOptions };
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { MdOutlineDragIndicator } from 'react-icons/md';
|
||||
|
||||
export const DragHandle = () => (
|
||||
<div className="quicklink-drag-handle" aria-hidden="true">
|
||||
<MdOutlineDragIndicator />
|
||||
</div>
|
||||
);
|
||||
74
src/features/quicklinks/options/components/SortableItem.jsx
Normal file
74
src/features/quicklinks/options/components/SortableItem.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { MdEdit, MdDelete } from 'react-icons/md';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { DragHandle } from './DragHandle';
|
||||
|
||||
export const SortableItem = ({ value, enabled, startEditLink, deleteLink }) => {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: value.key,
|
||||
disabled: !enabled,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
|
||||
const getIconUrl = (item) => {
|
||||
return (
|
||||
item.icon ||
|
||||
'https://icon.horse/icon/' + item.url.replace('https://', '').replace('http://', '')
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className={`quicklink-item ${!enabled ? 'disabled' : ''}`}
|
||||
role="listitem"
|
||||
>
|
||||
<DragHandle />
|
||||
<div className="quicklink-icon">
|
||||
<img src={getIconUrl(value)} alt={value.name} draggable={false} />
|
||||
</div>
|
||||
<div className="quicklink-content">
|
||||
<div className="quicklink-name">{value.name}</div>
|
||||
<div className="quicklink-url">{value.url}</div>
|
||||
</div>
|
||||
<div className="quicklink-actions">
|
||||
<button
|
||||
className="quicklink-action-btn"
|
||||
onClick={(e) => {
|
||||
if (!enabled) return;
|
||||
e.stopPropagation();
|
||||
startEditLink(value);
|
||||
}}
|
||||
title="Edit"
|
||||
disabled={!enabled}
|
||||
aria-disabled={!enabled}
|
||||
>
|
||||
<MdEdit />
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button
|
||||
className="quicklink-action-btn quicklink-remove-btn"
|
||||
onClick={(e) => {
|
||||
if (!enabled) return;
|
||||
e.stopPropagation();
|
||||
deleteLink(value.key, e);
|
||||
}}
|
||||
title="Remove"
|
||||
disabled={!enabled}
|
||||
aria-disabled={!enabled}
|
||||
>
|
||||
<MdDelete />
|
||||
<span>Remove</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
39
src/features/quicklinks/options/components/SortableList.jsx
Normal file
39
src/features/quicklinks/options/components/SortableList.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { SortableItem } from './SortableItem';
|
||||
|
||||
export const SortableList = ({ items, enabled, onDragEnd, startEditLink, deleteLink }) => {
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
|
||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
||||
);
|
||||
|
||||
return (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
|
||||
<SortableContext items={items.map((item) => item.key)} strategy={verticalListSortingStrategy}>
|
||||
<div className="quicklinks-list" role="list">
|
||||
{items.map((item) => (
|
||||
<SortableItem
|
||||
key={item.key}
|
||||
value={item}
|
||||
enabled={enabled}
|
||||
startEditLink={startEditLink}
|
||||
deleteLink={deleteLink}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
3
src/features/quicklinks/options/components/index.jsx
Normal file
3
src/features/quicklinks/options/components/index.jsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './DragHandle';
|
||||
export * from './SortableItem';
|
||||
export * from './SortableList';
|
||||
11
src/features/quicklinks/options/utils/quicklinksUtils.js
Normal file
11
src/features/quicklinks/options/utils/quicklinksUtils.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export const readQuicklinks = () => {
|
||||
try {
|
||||
const raw = localStorage.getItem('quicklinks');
|
||||
if (!raw) return [];
|
||||
const data = JSON.parse(raw);
|
||||
return Array.isArray(data) ? data : [];
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse quicklinks from localStorage. Resetting to []', e);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import variables from 'config/variables';
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { MdCancel, MdAdd, MdSource, MdOutlineFormatQuote } from 'react-icons/md';
|
||||
import TextareaAutosize from '@mui/material/TextareaAutosize';
|
||||
|
||||
@@ -17,281 +17,272 @@ import { Button } from 'components/Elements';
|
||||
import { toast } from 'react-toastify';
|
||||
import EventBus from 'utils/eventbus';
|
||||
|
||||
class QuoteOptions extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
quoteType: localStorage.getItem('quoteType') || 'api',
|
||||
customQuote: this.getCustom(),
|
||||
sourceSection: false,
|
||||
};
|
||||
}
|
||||
|
||||
resetCustom = () => {
|
||||
localStorage.setItem('customQuote', '[{"quote": "", "author": ""}]');
|
||||
this.setState({ customQuote: [{ quote: '', author: '' }] });
|
||||
toast(variables.getMessage('toasts.reset'));
|
||||
EventBus.emit('refresh', 'background');
|
||||
};
|
||||
|
||||
customQuote(e, text, index, type) {
|
||||
const result = text === true ? e.target.value : e.target.result;
|
||||
|
||||
const customQuote = this.state.customQuote;
|
||||
customQuote[index][type] = result;
|
||||
this.setState({ customQuote });
|
||||
this.forceUpdate();
|
||||
|
||||
localStorage.setItem('customQuote', JSON.stringify(customQuote));
|
||||
document.querySelector('.reminder-info').style.display = 'flex';
|
||||
localStorage.setItem('showReminder', true);
|
||||
}
|
||||
|
||||
modifyCustomQuote(type, index) {
|
||||
const customQuote = this.state.customQuote;
|
||||
if (type === 'add') {
|
||||
customQuote.push({ quote: '', author: '' });
|
||||
} else {
|
||||
customQuote.splice(index, 1);
|
||||
}
|
||||
|
||||
this.setState({ customQuote });
|
||||
this.forceUpdate();
|
||||
|
||||
localStorage.setItem('customQuote', JSON.stringify(customQuote));
|
||||
}
|
||||
|
||||
getCustom() {
|
||||
const QuoteOptions = () => {
|
||||
const getCustom = () => {
|
||||
let data = JSON.parse(localStorage.getItem('customQuote'));
|
||||
if (data === null) {
|
||||
data = [];
|
||||
}
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const QUOTE_SECTION = 'modals.main.settings.sections.quote';
|
||||
const [quoteType, setQuoteType] = useState(localStorage.getItem('quoteType') || 'api');
|
||||
const [customQuote, setCustomQuote] = useState(getCustom());
|
||||
const [sourceSection, setSourceSection] = useState(false);
|
||||
|
||||
const ButtonOptions = () => {
|
||||
return (
|
||||
<Row>
|
||||
<Content
|
||||
title={variables.getMessage(`${QUOTE_SECTION}.buttons.title`)}
|
||||
subtitle={variables.getMessage('modals.main.settings.sections.quote.buttons.subtitle')}
|
||||
/>
|
||||
<Action>
|
||||
<Checkbox
|
||||
name="copyButton"
|
||||
text={variables.getMessage(`${QUOTE_SECTION}.buttons.copy`)}
|
||||
category="quote"
|
||||
/>
|
||||
<Checkbox
|
||||
name="quoteShareButton"
|
||||
text={variables.getMessage('widgets.quote.share')}
|
||||
category="quote"
|
||||
/>
|
||||
<Checkbox
|
||||
name="favouriteQuoteEnabled"
|
||||
text={variables.getMessage(`${QUOTE_SECTION}.buttons.favourite`)}
|
||||
category="quote"
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
const resetCustom = () => {
|
||||
localStorage.setItem('customQuote', '[{"quote": "", "author": ""}]');
|
||||
setCustomQuote([{ quote: '', author: '' }]);
|
||||
toast(variables.getMessage('toasts.reset'));
|
||||
EventBus.emit('refresh', 'background');
|
||||
};
|
||||
|
||||
const SourceDropdown = () => {
|
||||
return (
|
||||
<Dropdown
|
||||
name="quoteType"
|
||||
label={variables.getMessage('modals.main.settings.sections.background.type.title')}
|
||||
onChange={(value) => this.setState({ quoteType: value })}
|
||||
category="quote"
|
||||
items={[
|
||||
localStorage.getItem('quote_packs') && {
|
||||
value: 'quote_pack',
|
||||
text: variables.getMessage('modals.main.navbar.marketplace'),
|
||||
},
|
||||
{
|
||||
value: 'api',
|
||||
text: variables.getMessage('modals.main.settings.sections.background.type.api'),
|
||||
},
|
||||
{ value: 'custom', text: variables.getMessage(`${QUOTE_SECTION}.custom`) },
|
||||
]}
|
||||
const handleCustomQuote = (e, text, index, type) => {
|
||||
const result = text === true ? e.target.value : e.target.result;
|
||||
|
||||
const updatedCustomQuote = [...customQuote];
|
||||
updatedCustomQuote[index][type] = result;
|
||||
setCustomQuote(updatedCustomQuote);
|
||||
|
||||
localStorage.setItem('customQuote', JSON.stringify(updatedCustomQuote));
|
||||
document.querySelector('.reminder-info').style.display = 'flex';
|
||||
localStorage.setItem('showReminder', true);
|
||||
};
|
||||
|
||||
const modifyCustomQuote = (type, index) => {
|
||||
const updatedCustomQuote = [...customQuote];
|
||||
if (type === 'add') {
|
||||
updatedCustomQuote.push({ quote: '', author: '' });
|
||||
} else {
|
||||
updatedCustomQuote.splice(index, 1);
|
||||
}
|
||||
|
||||
setCustomQuote(updatedCustomQuote);
|
||||
|
||||
localStorage.setItem('customQuote', JSON.stringify(updatedCustomQuote));
|
||||
};
|
||||
|
||||
const QUOTE_SECTION = 'modals.main.settings.sections.quote';
|
||||
|
||||
const ButtonOptions = () => {
|
||||
return (
|
||||
<Row>
|
||||
<Content
|
||||
title={variables.getMessage(`${QUOTE_SECTION}.buttons.title`)}
|
||||
subtitle={variables.getMessage('modals.main.settings.sections.quote.buttons.subtitle')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
<Action>
|
||||
<Checkbox
|
||||
name="copyButton"
|
||||
text={variables.getMessage(`${QUOTE_SECTION}.buttons.copy`)}
|
||||
category="quote"
|
||||
/>
|
||||
<Checkbox
|
||||
name="quoteShareButton"
|
||||
text={variables.getMessage('widgets.quote.share')}
|
||||
category="quote"
|
||||
/>
|
||||
<Checkbox
|
||||
name="favouriteQuoteEnabled"
|
||||
text={variables.getMessage(`${QUOTE_SECTION}.buttons.favourite`)}
|
||||
category="quote"
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
const AdditionalOptions = () => {
|
||||
return (
|
||||
const SourceDropdown = () => {
|
||||
return (
|
||||
<Dropdown
|
||||
name="quoteType"
|
||||
label={variables.getMessage('modals.main.settings.sections.background.type.title')}
|
||||
onChange={(value) => setQuoteType(value)}
|
||||
category="quote"
|
||||
items={[
|
||||
localStorage.getItem('quote_packs') && {
|
||||
value: 'quote_pack',
|
||||
text: variables.getMessage('modals.main.navbar.marketplace'),
|
||||
},
|
||||
{
|
||||
value: 'api',
|
||||
text: variables.getMessage('modals.main.settings.sections.background.type.api'),
|
||||
},
|
||||
{ value: 'custom', text: variables.getMessage(`${QUOTE_SECTION}.custom`) },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const AdditionalOptions = () => {
|
||||
return (
|
||||
<Row final={true}>
|
||||
<Content
|
||||
title={variables.getMessage('modals.main.settings.additional_settings')}
|
||||
subtitle={variables.getMessage(`${QUOTE_SECTION}.additional`)}
|
||||
/>
|
||||
<Action>
|
||||
<Checkbox
|
||||
name="authorDetails"
|
||||
text={variables.getMessage(`${QUOTE_SECTION}.author_details`)}
|
||||
element=".other"
|
||||
/>
|
||||
<Checkbox
|
||||
name="authorLink"
|
||||
text={variables.getMessage(`${QUOTE_SECTION}.author_link`)}
|
||||
element=".other"
|
||||
disabled={localStorage.getItem('authorDetails') === 'false'}
|
||||
/>
|
||||
<Checkbox
|
||||
name="authorImg"
|
||||
text={variables.getMessage(`${QUOTE_SECTION}.author_img`)}
|
||||
element=".other"
|
||||
disabled={localStorage.getItem('authorDetails') === 'false'}
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
let customSettings;
|
||||
if (quoteType === 'custom' && sourceSection === true) {
|
||||
customSettings = (
|
||||
<>
|
||||
<Row final={true}>
|
||||
<Content
|
||||
title={variables.getMessage('modals.main.settings.additional_settings')}
|
||||
subtitle={variables.getMessage(`${QUOTE_SECTION}.additional`)}
|
||||
title={variables.getMessage(`${QUOTE_SECTION}.custom`)}
|
||||
subtitle={variables.getMessage(`${QUOTE_SECTION}.custom_subtitle`)}
|
||||
/>
|
||||
<Action>
|
||||
<Checkbox
|
||||
name="authorDetails"
|
||||
text={variables.getMessage(`${QUOTE_SECTION}.author_details`)}
|
||||
element=".other"
|
||||
/>
|
||||
<Checkbox
|
||||
name="authorLink"
|
||||
text={variables.getMessage(`${QUOTE_SECTION}.author_link`)}
|
||||
element=".other"
|
||||
disabled={localStorage.getItem('authorDetails') === 'false'}
|
||||
/>
|
||||
<Checkbox
|
||||
name="authorImg"
|
||||
text={variables.getMessage(`${QUOTE_SECTION}.author_img`)}
|
||||
element=".other"
|
||||
disabled={localStorage.getItem('authorDetails') === 'false'}
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => modifyCustomQuote('add')}
|
||||
icon={<MdAdd />}
|
||||
label={variables.getMessage(`${QUOTE_SECTION}.add`)}
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
let customSettings;
|
||||
if (this.state.quoteType === 'custom' && this.state.sourceSection === true) {
|
||||
customSettings = (
|
||||
<>
|
||||
<Row final={true}>
|
||||
<Content
|
||||
title={variables.getMessage(`${QUOTE_SECTION}.custom`)}
|
||||
subtitle={variables.getMessage(`${QUOTE_SECTION}.custom_subtitle`)}
|
||||
/>
|
||||
<Action>
|
||||
{customQuote.length !== 0 ? (
|
||||
<div className="messagesContainer">
|
||||
{customQuote.map((_url, index) => (
|
||||
<div className="messageMap" key={index}>
|
||||
<div className="icon">
|
||||
<MdOutlineFormatQuote />
|
||||
</div>
|
||||
<div className="messageText">
|
||||
<TextareaAutosize
|
||||
value={customQuote[index].quote}
|
||||
placeholder={variables.getMessage(
|
||||
'modals.main.settings.sections.quote.title',
|
||||
)}
|
||||
onChange={(e) => handleCustomQuote(e, true, index, 'quote')}
|
||||
varient="outlined"
|
||||
style={{ fontSize: '22px', fontWeight: 'bold' }}
|
||||
/>
|
||||
<TextareaAutosize
|
||||
value={customQuote[index].author}
|
||||
placeholder={variables.getMessage(
|
||||
'modals.main.settings.sections.quote.author',
|
||||
)}
|
||||
className="subtitle"
|
||||
onChange={(e) => handleCustomQuote(e, true, index, 'author')}
|
||||
varient="outlined"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="messageAction">
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => modifyCustomQuote('remove', index)}
|
||||
icon={<MdCancel />}
|
||||
label={variables.getMessage(
|
||||
'modals.main.marketplace.product.buttons.remove',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="photosEmpty">
|
||||
<div className="emptyNewMessage">
|
||||
<MdOutlineFormatQuote />
|
||||
<span className="title">{variables.getMessage(`${QUOTE_SECTION}.no_quotes`)}</span>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage('modals.main.settings.sections.message.add_some')}
|
||||
</span>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.modifyCustomQuote('add')}
|
||||
onClick={() => modifyCustomQuote('add')}
|
||||
icon={<MdAdd />}
|
||||
label={variables.getMessage(`${QUOTE_SECTION}.add`)}
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
|
||||
{this.state.customQuote.length !== 0 ? (
|
||||
<div className="messagesContainer">
|
||||
{this.state.customQuote.map((_url, index) => (
|
||||
<div className="messageMap" key={index}>
|
||||
<div className="icon">
|
||||
<MdOutlineFormatQuote />
|
||||
</div>
|
||||
<div className="messageText">
|
||||
<TextareaAutosize
|
||||
value={this.state.customQuote[index].quote}
|
||||
placeholder={variables.getMessage(
|
||||
'modals.main.settings.sections.quote.title',
|
||||
)}
|
||||
onChange={(e) => this.customQuote(e, true, index, 'quote')}
|
||||
varient="outlined"
|
||||
style={{ fontSize: '22px', fontWeight: 'bold' }}
|
||||
/>
|
||||
<TextareaAutosize
|
||||
value={this.state.customQuote[index].author}
|
||||
placeholder={variables.getMessage(
|
||||
'modals.main.settings.sections.quote.author',
|
||||
)}
|
||||
className="subtitle"
|
||||
onChange={(e) => this.customQuote(e, true, index, 'author')}
|
||||
varient="outlined"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="messageAction">
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.modifyCustomQuote('remove', index)}
|
||||
icon={<MdCancel />}
|
||||
label={variables.getMessage(
|
||||
'modals.main.marketplace.product.buttons.remove',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="photosEmpty">
|
||||
<div className="emptyNewMessage">
|
||||
<MdOutlineFormatQuote />
|
||||
<span className="title">{variables.getMessage(`${QUOTE_SECTION}.no_quotes`)}</span>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage('modals.main.settings.sections.message.add_some')}
|
||||
</span>
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={() => this.modifyCustomQuote('add')}
|
||||
icon={<MdAdd />}
|
||||
label={variables.getMessage(`${QUOTE_SECTION}.add`)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
// api
|
||||
customSettings = <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.state.sourceSection ? (
|
||||
<Header
|
||||
title={variables.getMessage(`${QUOTE_SECTION}.title`)}
|
||||
secondaryTitle={variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.title',
|
||||
)}
|
||||
goBack={() => this.setState({ sourceSection: false })}
|
||||
report={false}
|
||||
/>
|
||||
) : (
|
||||
<Header
|
||||
title={variables.getMessage(`${QUOTE_SECTION}.title`)}
|
||||
setting="quote"
|
||||
category="quote"
|
||||
element=".quotediv"
|
||||
zoomSetting="zoomQuote"
|
||||
visibilityToggle={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{this.state.sourceSection && (
|
||||
<Row final={true}>
|
||||
<Content
|
||||
title={variables.getMessage('modals.main.settings.sections.background.source.title')}
|
||||
subtitle={variables.getMessage(`${QUOTE_SECTION}.source_subtitle`)}
|
||||
/>
|
||||
<Action>
|
||||
<SourceDropdown />
|
||||
</Action>
|
||||
</Row>
|
||||
)}
|
||||
{!this.state.sourceSection && (
|
||||
<PreferencesWrapper
|
||||
setting="quote"
|
||||
zoomSetting="zoomQuote"
|
||||
category="quote"
|
||||
visibilityToggle={true}
|
||||
>
|
||||
<Section
|
||||
icon={<MdSource />}
|
||||
title={variables.getMessage('modals.main.settings.sections.background.source.title')}
|
||||
subtitle={variables.getMessage(`${QUOTE_SECTION}.source_subtitle`)}
|
||||
onClick={() => this.setState({ sourceSection: true })}
|
||||
>
|
||||
<SourceDropdown />
|
||||
</Section>
|
||||
<ButtonOptions />
|
||||
<AdditionalOptions />
|
||||
</PreferencesWrapper>
|
||||
)}
|
||||
{customSettings}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
// api
|
||||
customSettings = <></>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{sourceSection ? (
|
||||
<Header
|
||||
title={variables.getMessage(`${QUOTE_SECTION}.title`)}
|
||||
secondaryTitle={variables.getMessage(
|
||||
'modals.main.settings.sections.background.source.title',
|
||||
)}
|
||||
goBack={() => setSourceSection(false)}
|
||||
report={false}
|
||||
/>
|
||||
) : (
|
||||
<Header
|
||||
title={variables.getMessage(`${QUOTE_SECTION}.title`)}
|
||||
setting="quote"
|
||||
category="quote"
|
||||
element=".quotediv"
|
||||
zoomSetting="zoomQuote"
|
||||
visibilityToggle={true}
|
||||
/>
|
||||
)}
|
||||
{sourceSection && (
|
||||
<Row final={true}>
|
||||
<Content
|
||||
title={variables.getMessage('modals.main.settings.sections.background.source.title')}
|
||||
subtitle={variables.getMessage(`${QUOTE_SECTION}.source_subtitle`)}
|
||||
/>
|
||||
<Action>
|
||||
<SourceDropdown />
|
||||
</Action>
|
||||
</Row>
|
||||
)}
|
||||
{!sourceSection && (
|
||||
<PreferencesWrapper
|
||||
setting="quote"
|
||||
zoomSetting="zoomQuote"
|
||||
category="quote"
|
||||
visibilityToggle={true}
|
||||
>
|
||||
<Section
|
||||
icon={<MdSource />}
|
||||
title={variables.getMessage('modals.main.settings.sections.background.source.title')}
|
||||
subtitle={variables.getMessage(`${QUOTE_SECTION}.source_subtitle`)}
|
||||
onClick={() => setSourceSection(true)}
|
||||
>
|
||||
<SourceDropdown />
|
||||
</Section>
|
||||
<ButtonOptions />
|
||||
<AdditionalOptions />
|
||||
</PreferencesWrapper>
|
||||
)}
|
||||
{customSettings}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { QuoteOptions as default, QuoteOptions };
|
||||
|
||||
@@ -1,43 +1,37 @@
|
||||
import { PureComponent } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import EventBus from 'utils/eventbus';
|
||||
|
||||
import './autocomplete.scss';
|
||||
|
||||
class Autocomplete extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
filtered: [],
|
||||
input: '',
|
||||
autocompleteDisabled: localStorage.getItem('autocomplete') !== 'true',
|
||||
};
|
||||
}
|
||||
const Autocomplete = ({ suggestions, onChange: onChangeCallback, onClick: onClickCallback, placeholder, id }) => {
|
||||
const [filtered, setFiltered] = useState([]);
|
||||
const [input, setInput] = useState('');
|
||||
const [autocompleteDisabled, setAutocompleteDisabled] = useState(
|
||||
localStorage.getItem('autocomplete') !== 'true',
|
||||
);
|
||||
|
||||
onChange = (e) => {
|
||||
if (this.state.autocompleteDisabled) {
|
||||
return this.setState({
|
||||
input: e.target.value,
|
||||
});
|
||||
const onChange = (e) => {
|
||||
if (autocompleteDisabled) {
|
||||
setInput(e.target.value);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
filtered: this.props.suggestions.filter(
|
||||
setFiltered(
|
||||
suggestions.filter(
|
||||
(suggestion) => suggestion.toLowerCase().indexOf(e.target.value.toLowerCase()) > -1,
|
||||
),
|
||||
input: e.target.value,
|
||||
});
|
||||
);
|
||||
setInput(e.target.value);
|
||||
|
||||
this.props.onChange(e.target.value);
|
||||
onChangeCallback(e.target.value);
|
||||
};
|
||||
|
||||
onClick = (e) => {
|
||||
this.setState({
|
||||
filtered: [],
|
||||
input: e.target.innerText,
|
||||
});
|
||||
const onClick = (e) => {
|
||||
setFiltered([]);
|
||||
setInput(e.target.innerText);
|
||||
|
||||
this.props.onClick({
|
||||
onClickCallback({
|
||||
preventDefault: () => e.preventDefault(),
|
||||
target: {
|
||||
value: e.target.innerText,
|
||||
@@ -45,51 +39,48 @@ class Autocomplete extends PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
useEffect(() => {
|
||||
const handleRefresh = (data) => {
|
||||
if (data === 'search') {
|
||||
this.setState({
|
||||
autocompleteDisabled: localStorage.getItem('autocomplete') !== 'true',
|
||||
});
|
||||
setAutocompleteDisabled(localStorage.getItem('autocomplete') !== 'true');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
}
|
||||
EventBus.on('refresh', handleRefresh);
|
||||
return () => {
|
||||
EventBus.off('refresh');
|
||||
};
|
||||
}, []);
|
||||
|
||||
render() {
|
||||
let autocomplete = null;
|
||||
let autocomplete = null;
|
||||
|
||||
// length will only be > 0 if enabled
|
||||
if (this.state.filtered.length > 0 && this.state.input.length > 0) {
|
||||
autocomplete = (
|
||||
<div className="suggestions">
|
||||
{this.state.filtered.map((suggestion) => (
|
||||
<div key={suggestion} onClick={this.onClick}>
|
||||
{suggestion}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexFlow: 'column' }}>
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.onChange}
|
||||
value={this.state.input}
|
||||
placeholder={this.props.placeholder || ''}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
id={this.props.id || ''}
|
||||
/>
|
||||
{autocomplete}
|
||||
// length will only be > 0 if enabled
|
||||
if (filtered.length > 0 && input.length > 0) {
|
||||
autocomplete = (
|
||||
<div className="suggestions">
|
||||
{filtered.map((suggestion) => (
|
||||
<div key={suggestion} onClick={onClick}>
|
||||
{suggestion}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexFlow: 'column' }}>
|
||||
<input
|
||||
type="text"
|
||||
onChange={onChange}
|
||||
value={input}
|
||||
placeholder={placeholder || ''}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
id={id || ''}
|
||||
/>
|
||||
{autocomplete}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Autocomplete as default, Autocomplete };
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } from 'react';
|
||||
import { MdOutlineWarning } from 'react-icons/md';
|
||||
|
||||
import { Header, Row, Content, Action, PreferencesWrapper } from 'components/Layout/Settings';
|
||||
@@ -7,65 +6,62 @@ import { Checkbox } from 'components/Form/Settings';
|
||||
|
||||
import EventBus from 'utils/eventbus';
|
||||
|
||||
class SearchOptions extends PureComponent {
|
||||
|
||||
render() {
|
||||
const SEARCH_SECTION = 'modals.main.settings.sections.search';
|
||||
|
||||
const ChromePolicyWarning = () => {
|
||||
return (
|
||||
<div className="itemWarning" style={{ marginBottom: '20px' }}>
|
||||
<MdOutlineWarning />
|
||||
<div className="text">
|
||||
<span className="header">Search Engine Selection Removed</span>
|
||||
<span>{variables.getMessage(`${SEARCH_SECTION}.chrome_policy_warning`)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AdditionalOptions = () => {
|
||||
return (
|
||||
<Row final={true}>
|
||||
<Content
|
||||
title={variables.getMessage('modals.main.settings.additional_settings')}
|
||||
subtitle={variables.getMessage(`${SEARCH_SECTION}.additional`)}
|
||||
/>
|
||||
<Action>
|
||||
{/* not supported on firefox */}
|
||||
{navigator.userAgent.includes('Chrome') && typeof InstallTrigger === 'undefined' ? (
|
||||
<Checkbox
|
||||
name="voiceSearch"
|
||||
text={variables.getMessage(`${SEARCH_SECTION}.voice_search`)}
|
||||
category="search"
|
||||
/>
|
||||
) : null}
|
||||
<Checkbox
|
||||
name="searchFocus"
|
||||
text={variables.getMessage(`${SEARCH_SECTION}.focus`)}
|
||||
category="search"
|
||||
element=".other"
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
const SearchOptions = () => {
|
||||
const SEARCH_SECTION = 'modals.main.settings.sections.search';
|
||||
|
||||
const ChromePolicyWarning = () => {
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
title={variables.getMessage(`${SEARCH_SECTION}.title`)}
|
||||
setting="searchBar"
|
||||
category="widgets"
|
||||
visibilityToggle={true}
|
||||
/>
|
||||
<PreferencesWrapper setting="searchBar" category="widgets" visibilityToggle={true}>
|
||||
<ChromePolicyWarning />
|
||||
<AdditionalOptions />
|
||||
</PreferencesWrapper>
|
||||
</>
|
||||
<div className="itemWarning" style={{ marginBottom: '20px' }}>
|
||||
<MdOutlineWarning />
|
||||
<div className="text">
|
||||
<span className="header">Search Engine Selection Removed</span>
|
||||
<span>{variables.getMessage(`${SEARCH_SECTION}.chrome_policy_warning`)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const AdditionalOptions = () => {
|
||||
return (
|
||||
<Row final={true}>
|
||||
<Content
|
||||
title={variables.getMessage('modals.main.settings.additional_settings')}
|
||||
subtitle={variables.getMessage(`${SEARCH_SECTION}.additional`)}
|
||||
/>
|
||||
<Action>
|
||||
{/* not supported on firefox */}
|
||||
{navigator.userAgent.includes('Chrome') && typeof InstallTrigger === 'undefined' ? (
|
||||
<Checkbox
|
||||
name="voiceSearch"
|
||||
text={variables.getMessage(`${SEARCH_SECTION}.voice_search`)}
|
||||
category="search"
|
||||
/>
|
||||
) : null}
|
||||
<Checkbox
|
||||
name="searchFocus"
|
||||
text={variables.getMessage(`${SEARCH_SECTION}.focus`)}
|
||||
category="search"
|
||||
element=".other"
|
||||
/>
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
title={variables.getMessage(`${SEARCH_SECTION}.title`)}
|
||||
setting="searchBar"
|
||||
category="widgets"
|
||||
visibilityToggle={true}
|
||||
/>
|
||||
<PreferencesWrapper setting="searchBar" category="widgets" visibilityToggle={true}>
|
||||
<ChromePolicyWarning />
|
||||
<AdditionalOptions />
|
||||
</PreferencesWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { SearchOptions as default, SearchOptions };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PureComponent } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
import { convertTimezone } from 'utils/date';
|
||||
import { AnalogClock } from './components/AnalogClock';
|
||||
@@ -6,29 +6,25 @@ import { VerticalClock } from './components/VerticalClock';
|
||||
import EventBus from 'utils/eventbus';
|
||||
|
||||
import './clock.scss';
|
||||
export default class Clock extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.timer = undefined;
|
||||
this.state = {
|
||||
timeType: localStorage.getItem('timeType'),
|
||||
time: '',
|
||||
finalHour: '',
|
||||
finalMinute: '',
|
||||
finalSeconds: '',
|
||||
ampm: '',
|
||||
nowGlobal: new Date(),
|
||||
};
|
||||
}
|
||||
const Clock = () => {
|
||||
const [timeType] = useState(localStorage.getItem('timeType'));
|
||||
const [time, setTime] = useState('');
|
||||
const [finalHour, setFinalHour] = useState('');
|
||||
const [finalMinute, setFinalMinute] = useState('');
|
||||
const [finalSeconds, setFinalSeconds] = useState('');
|
||||
const [ampm, setAmpm] = useState('');
|
||||
const [display, setDisplay] = useState('block');
|
||||
const [fontSize, setFontSize] = useState('4em');
|
||||
const timerRef = useRef(undefined);
|
||||
|
||||
startTime(
|
||||
const startTime = (
|
||||
time = localStorage.getItem('seconds') === 'true' ||
|
||||
localStorage.getItem('timeType') === 'analogue'
|
||||
? 1000 - (Date.now() % 1000)
|
||||
: 60000 - (Date.now() % 60000),
|
||||
) {
|
||||
this.timer = setTimeout(() => {
|
||||
) => {
|
||||
timerRef.current = setTimeout(() => {
|
||||
let now = new Date();
|
||||
const timezone = localStorage.getItem('timezone');
|
||||
if (timezone && timezone !== 'auto') {
|
||||
@@ -37,16 +33,14 @@ export default class Clock extends PureComponent {
|
||||
|
||||
switch (localStorage.getItem('timeType')) {
|
||||
case 'percentageComplete':
|
||||
this.setState({
|
||||
time: (now.getHours() / 24).toFixed(2).replace('0.', '') + '%',
|
||||
ampm: '',
|
||||
});
|
||||
setTime((now.getHours() / 24).toFixed(2).replace('0.', '') + '%');
|
||||
setAmpm('');
|
||||
break;
|
||||
case 'analogue':
|
||||
// load analog clock css
|
||||
import('react-clock/dist/Clock.css');
|
||||
|
||||
this.setState({ time: now });
|
||||
setTime(now);
|
||||
break;
|
||||
default: {
|
||||
// Default clock
|
||||
@@ -56,27 +50,24 @@ export default class Clock extends PureComponent {
|
||||
|
||||
if (localStorage.getItem('seconds') === 'true') {
|
||||
sec = `:${('00' + now.getSeconds()).slice(-2)}`;
|
||||
this.setState({ finalSeconds: `${('00' + now.getSeconds()).slice(-2)}` });
|
||||
setFinalSeconds(`${('00' + now.getSeconds()).slice(-2)}`);
|
||||
}
|
||||
|
||||
if (localStorage.getItem('timeformat') === 'twentyfourhour') {
|
||||
if (zero === 'false') {
|
||||
time = `${now.getHours()}:${('00' + now.getMinutes()).slice(-2)}:${sec}`;
|
||||
this.setState({
|
||||
finalHour: `${now.getHours()}`,
|
||||
finalMinute: `${('00' + now.getMinutes()).slice(-2)}`,
|
||||
});
|
||||
setFinalHour(`${now.getHours()}`);
|
||||
setFinalMinute(`${('00' + now.getMinutes()).slice(-2)}`);
|
||||
} else {
|
||||
time = `${('00' + now.getHours()).slice(-2)}:${('00' + now.getMinutes()).slice(
|
||||
-2,
|
||||
)}${sec}`;
|
||||
this.setState({
|
||||
finalHour: `${('00' + now.getHours()).slice(-2)}`,
|
||||
finalMinute: `${('00' + now.getMinutes()).slice(-2)}`,
|
||||
});
|
||||
setFinalHour(`${('00' + now.getHours()).slice(-2)}`);
|
||||
setFinalMinute(`${('00' + now.getMinutes()).slice(-2)}`);
|
||||
}
|
||||
|
||||
this.setState({ time, ampm: '' });
|
||||
setTime(time);
|
||||
setAmpm('');
|
||||
} else {
|
||||
// 12 hour
|
||||
let hours = now.getHours();
|
||||
@@ -89,80 +80,76 @@ export default class Clock extends PureComponent {
|
||||
|
||||
if (zero === 'false') {
|
||||
time = `${hours}:${('00' + now.getMinutes()).slice(-2)}${sec}`;
|
||||
this.setState({
|
||||
finalHour: `${hours}`,
|
||||
finalMinute: `${('00' + now.getMinutes()).slice(-2)}`,
|
||||
});
|
||||
setFinalHour(`${hours}`);
|
||||
setFinalMinute(`${('00' + now.getMinutes()).slice(-2)}`);
|
||||
} else {
|
||||
time = `${('00' + hours).slice(-2)}:${('00' + now.getMinutes()).slice(-2)}${sec}`;
|
||||
this.setState({
|
||||
finalHour: `${('00' + hours).slice(-2)}`,
|
||||
finalMinute: `${('00' + now.getMinutes()).slice(-2)}`,
|
||||
});
|
||||
setFinalHour(`${('00' + hours).slice(-2)}`);
|
||||
setFinalMinute(`${('00' + now.getMinutes()).slice(-2)}`);
|
||||
}
|
||||
|
||||
this.setState({ time, ampm: now.getHours() > 11 ? 'PM' : 'AM' });
|
||||
setTime(time);
|
||||
setAmpm(now.getHours() > 11 ? 'PM' : 'AM');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.startTime();
|
||||
startTime();
|
||||
}, time);
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
useEffect(() => {
|
||||
const handleRefresh = (data) => {
|
||||
if (data === 'clock' || data === 'timezone') {
|
||||
const element = document.querySelector('.clock-container');
|
||||
|
||||
if (localStorage.getItem('time') === 'false') {
|
||||
return (element.style.display = 'none');
|
||||
setDisplay('none');
|
||||
return;
|
||||
}
|
||||
|
||||
this.timer = null;
|
||||
this.startTime(0);
|
||||
timerRef.current = null;
|
||||
startTime(0);
|
||||
|
||||
element.style.display = 'block';
|
||||
element.style.fontSize = `${
|
||||
4 * Number((localStorage.getItem('zoomClock') || 100) / 100)
|
||||
}em`;
|
||||
setDisplay('block');
|
||||
setFontSize(`${4 * Number((localStorage.getItem('zoomClock') || 100) / 100)}em`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (localStorage.getItem('timeType') !== 'analogue') {
|
||||
document.querySelector('.clock-container').style.fontSize = `${
|
||||
4 * Number((localStorage.getItem('zoomClock') || 100) / 100)
|
||||
}em`;
|
||||
setFontSize(`${4 * Number((localStorage.getItem('zoomClock') || 100) / 100)}em`);
|
||||
}
|
||||
|
||||
this.startTime(0);
|
||||
startTime(0);
|
||||
|
||||
EventBus.on('refresh', handleRefresh);
|
||||
return () => {
|
||||
EventBus.off('refresh');
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (localStorage.getItem('timeType') === 'analogue') {
|
||||
return <AnalogClock time={time} />;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
}
|
||||
|
||||
render() {
|
||||
if (localStorage.getItem('timeType') === 'analogue') {
|
||||
return <AnalogClock time={this.state.time} />;
|
||||
}
|
||||
|
||||
if (localStorage.getItem('timeType') === 'verticalClock') {
|
||||
return (
|
||||
<VerticalClock
|
||||
finalHour={this.state.finalHour}
|
||||
finalMinute={this.state.finalMinute}
|
||||
finalSeconds={this.state.finalSeconds}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (localStorage.getItem('timeType') === 'verticalClock') {
|
||||
return (
|
||||
<span className="clock clock-container">
|
||||
{this.state.time}
|
||||
<span className="ampm">{this.state.ampm}</span>
|
||||
</span>
|
||||
<VerticalClock
|
||||
finalHour={finalHour}
|
||||
finalMinute={finalMinute}
|
||||
finalSeconds={finalSeconds}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="clock clock-container" style={{ display, fontSize }}>
|
||||
{time}
|
||||
<span className="ampm">{ampm}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export { Clock as default, Clock };
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent, createRef } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
import { nth, convertTimezone } from 'utils/date';
|
||||
import EventBus from 'utils/eventbus';
|
||||
|
||||
import './date.scss';
|
||||
|
||||
export default class DateWidget extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
date: '',
|
||||
weekNumber: null,
|
||||
};
|
||||
this.date = createRef();
|
||||
}
|
||||
const DateWidget = () => {
|
||||
const [date, setDate] = useState('');
|
||||
const [weekNumber, setWeekNumber] = useState(null);
|
||||
const [display, setDisplay] = useState('block');
|
||||
const [fontSize, setFontSize] = useState('1em');
|
||||
const dateRef = useRef();
|
||||
|
||||
/**
|
||||
* Get the week number of the year for the given date.
|
||||
* @param {Date} date
|
||||
*/
|
||||
getWeekNumber(date) {
|
||||
const getWeekNumber = (date) => {
|
||||
const dateToday = new Date(date.valueOf());
|
||||
const dayNumber = (dateToday.getDay() + 6) % 7;
|
||||
|
||||
@@ -32,39 +29,37 @@ export default class DateWidget extends PureComponent {
|
||||
dateToday.setMonth(0, 1 + ((4 - dateToday.getDay() + 7) % 7));
|
||||
}
|
||||
|
||||
this.setState({
|
||||
weekNumber: `${variables.getMessage('widgets.date.week')} ${
|
||||
setWeekNumber(
|
||||
`${variables.getMessage('widgets.date.week')} ${
|
||||
1 + Math.ceil((firstThursday - dateToday) / 604800000)
|
||||
}`,
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
getDate() {
|
||||
const getDate = () => {
|
||||
let date = new Date();
|
||||
const timezone = localStorage.getItem('timezone');
|
||||
if (timezone && timezone !== 'auto') {
|
||||
date = convertTimezone(date, timezone);
|
||||
}
|
||||
|
||||
|
||||
if (localStorage.getItem('weeknumber') === 'true') {
|
||||
this.getWeekNumber(date);
|
||||
} else if (this.state.weekNumber !== null) {
|
||||
this.setState({
|
||||
weekNumber: null,
|
||||
});
|
||||
getWeekNumber(date);
|
||||
} else if (weekNumber !== null) {
|
||||
setWeekNumber(null);
|
||||
}
|
||||
|
||||
|
||||
if (localStorage.getItem('dateType') === 'short') {
|
||||
const dateDay = date.getDate();
|
||||
const dateMonth = date.getMonth() + 1;
|
||||
const dateYear = date.getFullYear();
|
||||
|
||||
|
||||
const zero = localStorage.getItem('datezero') === 'true';
|
||||
|
||||
|
||||
let day = zero ? ('00' + dateDay).slice(-2) : dateDay;
|
||||
let month = zero ? ('00' + dateMonth).slice(-2) : dateMonth;
|
||||
let year = dateYear;
|
||||
|
||||
|
||||
switch (localStorage.getItem('dateFormat')) {
|
||||
case 'MDY':
|
||||
day = dateMonth;
|
||||
@@ -78,7 +73,7 @@ export default class DateWidget extends PureComponent {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
let format;
|
||||
switch (localStorage.getItem('shortFormat')) {
|
||||
case 'dots':
|
||||
@@ -96,22 +91,22 @@ export default class DateWidget extends PureComponent {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
date: format,
|
||||
});
|
||||
|
||||
setDate(format);
|
||||
} else {
|
||||
// Long date
|
||||
const lang = variables.languagecode.split('_')[0];
|
||||
const datenth = localStorage.getItem('datenth') === 'true' ? nth(date.getDate()) : date.getDate();
|
||||
const dateDay = localStorage.getItem('dayofweek') === 'true'
|
||||
? date.toLocaleDateString(lang, { weekday: 'long' })
|
||||
: '';
|
||||
const datenth =
|
||||
localStorage.getItem('datenth') === 'true' ? nth(date.getDate()) : date.getDate();
|
||||
const dateDay =
|
||||
localStorage.getItem('dayofweek') === 'true'
|
||||
? date.toLocaleDateString(lang, { weekday: 'long' })
|
||||
: '';
|
||||
const dateMonth = date.toLocaleDateString(lang, { month: 'long' });
|
||||
const dateYear = date.getFullYear();
|
||||
|
||||
|
||||
let formattedDate;
|
||||
|
||||
|
||||
switch (localStorage.getItem('longFormat')) {
|
||||
case 'MDY':
|
||||
formattedDate = `${dateMonth} ${datenth}, ${dateYear}${dateDay ? `, ${dateDay}` : ''}`;
|
||||
@@ -124,45 +119,41 @@ export default class DateWidget extends PureComponent {
|
||||
formattedDate = `${datenth} ${dateMonth} ${dateYear}${dateDay ? `, ${dateDay}` : ''}`;
|
||||
break;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
date: formattedDate,
|
||||
});
|
||||
|
||||
setDate(formattedDate);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleRefresh = (data) => {
|
||||
if (data === 'date' || data === 'timezone') {
|
||||
if (localStorage.getItem('date') === 'false') {
|
||||
return (this.date.current.style.display = 'none');
|
||||
setDisplay('none');
|
||||
return;
|
||||
}
|
||||
|
||||
this.date.current.style.display = 'block';
|
||||
this.date.current.style.fontSize = `${Number(
|
||||
(localStorage.getItem('zoomDate') || 100) / 100,
|
||||
)}em`;
|
||||
this.getDate();
|
||||
setDisplay('block');
|
||||
setFontSize(`${Number((localStorage.getItem('zoomDate') || 100) / 100)}em`);
|
||||
getDate();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.date.current.style.fontSize = `${Number(
|
||||
(localStorage.getItem('zoomDate') || 100) / 100,
|
||||
)}em`;
|
||||
this.getDate();
|
||||
}
|
||||
setFontSize(`${Number((localStorage.getItem('zoomDate') || 100) / 100)}em`);
|
||||
getDate();
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
}
|
||||
EventBus.on('refresh', handleRefresh);
|
||||
return () => {
|
||||
EventBus.off('refresh');
|
||||
};
|
||||
}, []);
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span className="date" ref={this.date}>
|
||||
{this.state.date}
|
||||
<br />
|
||||
{this.state.weekNumber}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<span className="date" ref={dateRef} style={{ display, fontSize }}>
|
||||
{date}
|
||||
<br />
|
||||
{weekNumber}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export { DateWidget as default, DateWidget };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } from 'react';
|
||||
import { memo, useState, useEffect, useCallback } from 'react';
|
||||
|
||||
import WeatherIcon from './components/WeatherIcon';
|
||||
import Expanded from './components/Expanded';
|
||||
@@ -11,85 +11,85 @@ import { getWeather } from './api/getWeather.js';
|
||||
|
||||
import './weather.scss';
|
||||
|
||||
class WeatherWidget extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
location: localStorage.getItem('location') || 'London',
|
||||
done: false,
|
||||
};
|
||||
}
|
||||
const WeatherWidget = memo(() => {
|
||||
const [location, setLocation] = useState(localStorage.getItem('location') || 'London');
|
||||
const [done, setDone] = useState(false);
|
||||
const [weatherData, setWeatherData] = useState({});
|
||||
|
||||
async componentDidMount() {
|
||||
EventBus.on('refresh', async (data) => {
|
||||
if (data === 'weather') {
|
||||
const weatherData = await getWeather(this.state.location, this.state.done);
|
||||
this.setState(weatherData);
|
||||
|
||||
const zoomWeather = `${Number((localStorage.getItem('zoomWeather') || 100) / 100)}em`;
|
||||
document.querySelector('.weather').style.fontSize = zoomWeather;
|
||||
}
|
||||
});
|
||||
|
||||
const weatherData = await getWeather(this.state.location, this.state.done);
|
||||
this.setState(weatherData);
|
||||
const updateWeather = useCallback(async () => {
|
||||
const data = await getWeather(location, done);
|
||||
setWeatherData(data);
|
||||
setDone(data.done);
|
||||
|
||||
const zoomWeather = `${Number((localStorage.getItem('zoomWeather') || 100) / 100)}em`;
|
||||
document.querySelector('.weather').style.fontSize = zoomWeather;
|
||||
const weatherElement = document.querySelector('.weather');
|
||||
if (weatherElement) {
|
||||
weatherElement.style.fontSize = zoomWeather;
|
||||
}
|
||||
}, [location, done]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleRefresh = async (data) => {
|
||||
if (data === 'weather') {
|
||||
await updateWeather();
|
||||
}
|
||||
};
|
||||
|
||||
EventBus.on('refresh', handleRefresh);
|
||||
updateWeather();
|
||||
|
||||
return () => {
|
||||
EventBus.off('refresh', handleRefresh);
|
||||
};
|
||||
}, [updateWeather]);
|
||||
|
||||
const weatherType = localStorage.getItem('weatherType') || 1;
|
||||
|
||||
if (done === false) {
|
||||
return <WeatherSkeleton weatherType={weatherType} />;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
}
|
||||
|
||||
render() {
|
||||
const weatherType = localStorage.getItem('weatherType') || 1;
|
||||
|
||||
if (this.state.done === false) {
|
||||
return <WeatherSkeleton weatherType={weatherType} />;
|
||||
}
|
||||
|
||||
if (!this.state.weather) {
|
||||
return (
|
||||
<div className="weather">
|
||||
<span className="loc">{this.state.location}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!weatherData.weather) {
|
||||
return (
|
||||
<div className="weather">
|
||||
{/*{this.state.done === false ? <h1>cheese</h1> : <h1>loading finished</h1>}*/}
|
||||
<div className="weatherCore">
|
||||
<div className="iconAndTemps">
|
||||
<div className="weathericon">
|
||||
<WeatherIcon name={this.state.icon} />
|
||||
<span>{`${this.state.weather.temp}${this.state.temp_text}`}</span>
|
||||
</div>
|
||||
{weatherType >= 2 && (
|
||||
<span className="minmax">
|
||||
<span className="subtitle">{`${this.state.weather.temp_min}${this.state.temp_text}`}</span>
|
||||
<span className="subtitle">{`${this.state.weather.temp_max}${this.state.temp_text}`}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{weatherType >= 2 && (
|
||||
<div className="extra-info">
|
||||
<span>
|
||||
{variables.getMessage('widgets.weather.feels_like', {
|
||||
amount: `${this.state.weather.feels_like}${this.state.temp_text}`,
|
||||
})}
|
||||
</span>
|
||||
<span className="loc">{this.state.location}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{weatherType >= 3 && (
|
||||
<Expanded weatherType={weatherType} state={this.state} variables={variables} />
|
||||
)}
|
||||
<span className="loc">{location}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="weather">
|
||||
<div className="weatherCore">
|
||||
<div className="iconAndTemps">
|
||||
<div className="weathericon">
|
||||
<WeatherIcon name={weatherData.icon} />
|
||||
<span>{`${weatherData.weather.temp}${weatherData.temp_text}`}</span>
|
||||
</div>
|
||||
{weatherType >= 2 && (
|
||||
<span className="minmax">
|
||||
<span className="subtitle">{`${weatherData.weather.temp_min}${weatherData.temp_text}`}</span>
|
||||
<span className="subtitle">{`${weatherData.weather.temp_max}${weatherData.temp_text}`}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{weatherType >= 2 && (
|
||||
<div className="extra-info">
|
||||
<span>
|
||||
{variables.getMessage('widgets.weather.feels_like', {
|
||||
amount: `${weatherData.weather.feels_like}${weatherData.temp_text}`,
|
||||
})}
|
||||
</span>
|
||||
<span className="loc">{location}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{weatherType >= 3 && (
|
||||
<Expanded weatherType={weatherType} state={weatherData} variables={variables} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
WeatherWidget.displayName = 'WeatherWidget';
|
||||
|
||||
export { WeatherWidget as default, WeatherWidget };
|
||||
|
||||
Reference in New Issue
Block a user