refactor: split quicklinks, move features to functional components

This commit is contained in:
David Ralph
2025-10-28 23:04:19 +00:00
parent 2eed0f7307
commit 293cc93500
39 changed files with 2847 additions and 3021 deletions

View File

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

View File

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

View File

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
import { MdOutlineDragIndicator } from 'react-icons/md';
export const DragHandle = () => (
<div className="quicklink-drag-handle" aria-hidden="true">
<MdOutlineDragIndicator />
</div>
);

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

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

View File

@@ -0,0 +1,3 @@
export * from './DragHandle';
export * from './SortableItem';
export * from './SortableList';

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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