mirror of
https://github.com/mue/mue.git
synced 2026-07-15 13:03:55 +02:00
7.x Structural Changes (#637)
* refactor(files): Initial commit on experimental file structure * refactor(structure): New components system * refactor(structure): Tidy settings' components * Refactor(structure): Component exports and imports * refactor(settings): Use new component imports * feat: unified background.js script * fix(build): Partially, distributions still not ready * feat: critical error on noscript, light theme support for it * fix(background): Critical issue of code making every background #000 * refactor(welcome): Partition into different files + shared components - This took too long and destroyed my sanity --------- Co-authored-by: alexsparkes <turbomarshmello@gmail.com> Co-authored-by: alexsparkes <alexsparkes@gmail.com>
This commit is contained in:
90
src/features/widgets/Widgets.jsx
Normal file
90
src/features/widgets/Widgets.jsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { PureComponent, Fragment, Suspense, lazy } from 'react';
|
||||
|
||||
import Clock from './time/Clock';
|
||||
import Greeting from './greeting/Greeting';
|
||||
import Quote from './quote/Quote';
|
||||
import Search from './search/Search';
|
||||
import QuickLinks from './quicklinks/QuickLinks';
|
||||
import Date from './time/Date';
|
||||
import Message from './message/Message';
|
||||
import { WidgetsLayout } from 'components/Layout';
|
||||
|
||||
import EventBus from 'modules/helpers/eventbus';
|
||||
|
||||
// weather is lazy loaded due to the size of the weather icons module
|
||||
// since we're using react-icons this might not be accurate,
|
||||
// however, when we used the original module https://bundlephobia.com/package/weather-icons-react@1.2.0
|
||||
// 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 />,
|
||||
};
|
||||
}
|
||||
|
||||
enabled(key) {
|
||||
return localStorage.getItem(key) === 'true';
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
switch (data) {
|
||||
case 'widgets':
|
||||
return this.setState({
|
||||
order: JSON.parse(localStorage.getItem('order')),
|
||||
});
|
||||
case 'widgetsWelcome':
|
||||
this.setState({
|
||||
welcome: localStorage.getItem('showWelcome'),
|
||||
});
|
||||
localStorage.setItem('showWelcome', true);
|
||||
window.onbeforeunload = () => {
|
||||
localStorage.clear();
|
||||
};
|
||||
break;
|
||||
case 'widgetsWelcomeDone':
|
||||
this.setState({
|
||||
welcome: localStorage.getItem('showWelcome'),
|
||||
});
|
||||
return (window.onbeforeunload = null);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
582
src/features/widgets/background/Background.jsx
Normal file
582
src/features/widgets/background/Background.jsx
Normal file
@@ -0,0 +1,582 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
// todo: rewrite this mess
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import PhotoInformation from './PhotoInformation';
|
||||
|
||||
import EventBus from 'modules/helpers/eventbus';
|
||||
import {
|
||||
videoCheck,
|
||||
offlineBackground,
|
||||
getGradient,
|
||||
randomColourStyleBuilder,
|
||||
} from 'modules/helpers/background/widget';
|
||||
|
||||
import './scss/index.scss';
|
||||
import { decodeBlurHash } from 'fast-blurhash';
|
||||
import { supportsAVIF } from 'modules/helpers/background/avif';
|
||||
|
||||
export default class Background extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
blob: null,
|
||||
style: '',
|
||||
url: '',
|
||||
currentAPI: '',
|
||||
firstTime: false,
|
||||
photoInfo: {
|
||||
hidden: false,
|
||||
offline: false,
|
||||
photographerURL: '',
|
||||
photoURL: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async setBackground() {
|
||||
// clean up the previous image to prevent a memory leak
|
||||
if (this.blob) {
|
||||
URL.revokeObjectURL(this.blob);
|
||||
}
|
||||
|
||||
const backgroundImage = document.getElementById('backgroundImage');
|
||||
|
||||
if (this.state.url !== '') {
|
||||
let url = this.state.url;
|
||||
if (
|
||||
localStorage.getItem('ddgProxy') === 'true' &&
|
||||
this.state.photoInfo.offline !== true &&
|
||||
!this.state.url.startsWith('data:')
|
||||
) {
|
||||
url = variables.constants.DDG_IMAGE_PROXY + this.state.url;
|
||||
}
|
||||
|
||||
const photoInformation = document.querySelector('.photoInformation');
|
||||
|
||||
// just set the background
|
||||
if (localStorage.getItem('bgtransition') === 'false') {
|
||||
photoInformation?.[(photoInformation.style.display = 'flex')];
|
||||
return (backgroundImage.style.background = `url(${url})`);
|
||||
}
|
||||
|
||||
backgroundImage.style.background = null;
|
||||
|
||||
if (this.state.photoInfo.blur_hash) {
|
||||
backgroundImage.style.backgroundColor = this.state.photoInfo.colour;
|
||||
backgroundImage.classList.add('fade-in');
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 32;
|
||||
canvas.height = 32;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imageData = ctx.createImageData(32, 32);
|
||||
imageData.data.set(decodeBlurHash(this.state.photoInfo.blur_hash, 32, 32));
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
backgroundImage.style.backgroundImage = `url(${canvas.toDataURL()})`;
|
||||
}
|
||||
|
||||
this.blob = URL.createObjectURL(await (await fetch(url)).blob());
|
||||
backgroundImage.classList.add('backgroundTransform');
|
||||
backgroundImage.style.backgroundImage = `url(${this.blob})`;
|
||||
} else {
|
||||
// custom colour
|
||||
backgroundImage.setAttribute('style', this.state.style);
|
||||
}
|
||||
}
|
||||
|
||||
async getAPIImageData(currentPun) {
|
||||
let apiCategories;
|
||||
|
||||
try {
|
||||
apiCategories = JSON.parse(localStorage.getItem('apiCategories'));
|
||||
} catch (error) {
|
||||
apiCategories = localStorage.getItem('apiCategories');
|
||||
}
|
||||
|
||||
const backgroundAPI = localStorage.getItem('backgroundAPI');
|
||||
const apiQuality = localStorage.getItem('apiQuality');
|
||||
let backgroundExclude = JSON.parse(localStorage.getItem('backgroundExclude'));
|
||||
if (!Array.isArray(backgroundExclude)) {
|
||||
backgroundExclude = [];
|
||||
}
|
||||
if (currentPun) {
|
||||
backgroundExclude.push(currentPun);
|
||||
}
|
||||
|
||||
let requestURL, data;
|
||||
|
||||
switch (backgroundAPI) {
|
||||
case 'unsplash':
|
||||
case 'pexels':
|
||||
const collection = localStorage.getItem('unsplashCollections');
|
||||
if (collection) {
|
||||
requestURL = `${variables.constants.API_URL}/images/unsplash?collections=${collection}&quality=${apiQuality}`;
|
||||
} else {
|
||||
requestURL = `${variables.constants.API_URL}/images/unsplash?categories=${apiCategories}&quality=${apiQuality}`;
|
||||
}
|
||||
break;
|
||||
// Defaults to Mue
|
||||
default:
|
||||
requestURL = `${variables.constants.API_URL}/images/random?categories=${apiCategories}&quality=${apiQuality}&excludes=${backgroundExclude}`;
|
||||
break;
|
||||
}
|
||||
|
||||
const accept = 'application/json, ' + ((await supportsAVIF()) ? 'image/avif' : 'image/webp');
|
||||
try {
|
||||
data = await (await fetch(requestURL, { headers: { accept } })).json();
|
||||
} catch (e) {
|
||||
// if requesting to the API fails, we get an offline image
|
||||
this.setState(offlineBackground('api'));
|
||||
return null;
|
||||
}
|
||||
|
||||
let photoURL, photographerURL;
|
||||
|
||||
if (backgroundAPI === 'unsplash') {
|
||||
photoURL = data.photo_page;
|
||||
photographerURL = data.photographer_page;
|
||||
}
|
||||
|
||||
return {
|
||||
url: data.file,
|
||||
type: 'api',
|
||||
currentAPI: backgroundAPI,
|
||||
photoInfo: {
|
||||
hidden: false,
|
||||
category: data.category,
|
||||
credit: data.photographer,
|
||||
location: data.location.name,
|
||||
camera: data.camera,
|
||||
url: data.file,
|
||||
photographerURL,
|
||||
photoURL,
|
||||
latitude: data.location.latitude || null,
|
||||
longitude: data.location.longitude || null,
|
||||
views: data.views || null,
|
||||
downloads: data.downloads || null,
|
||||
likes: data.likes || null,
|
||||
description: data.description || null,
|
||||
colour: data.colour,
|
||||
blur_hash: data.blur_hash,
|
||||
pun: data.pun || null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Main background getting function
|
||||
async getBackground() {
|
||||
let offline = localStorage.getItem('offlineMode') === 'true';
|
||||
if (localStorage.getItem('showWelcome') === 'true') {
|
||||
offline = true;
|
||||
}
|
||||
|
||||
const setFavourited = ({ type, url, credit, location, camera, pun, offline }) => {
|
||||
if (type === 'random_colour' || type === 'random_gradient') {
|
||||
return this.setState({
|
||||
type: 'colour',
|
||||
style: `background:${url}`,
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
url,
|
||||
photoInfo: {
|
||||
credit,
|
||||
location,
|
||||
camera,
|
||||
pun,
|
||||
offline,
|
||||
url,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const favourited = JSON.parse(localStorage.getItem('favourite'));
|
||||
if (favourited) {
|
||||
return setFavourited(favourited);
|
||||
}
|
||||
|
||||
const type = localStorage.getItem('backgroundType');
|
||||
switch (type) {
|
||||
case 'api':
|
||||
if (offline) {
|
||||
return this.setState(offlineBackground('api'));
|
||||
}
|
||||
|
||||
// API background
|
||||
let data = JSON.parse(localStorage.getItem('nextImage')) || (await this.getAPIImageData());
|
||||
localStorage.setItem('nextImage', null);
|
||||
if (data) {
|
||||
this.setState(data);
|
||||
localStorage.setItem('currentBackground', JSON.stringify(data));
|
||||
localStorage.setItem(
|
||||
'nextImage',
|
||||
JSON.stringify(await this.getAPIImageData(data.photoInfo.pun)),
|
||||
); // pre-fetch data about the next image
|
||||
}
|
||||
break;
|
||||
|
||||
case 'colour':
|
||||
const gradient = getGradient();
|
||||
if (gradient) {
|
||||
this.setState(gradient);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'random_colour':
|
||||
case 'random_gradient':
|
||||
this.setState(randomColourStyleBuilder(type));
|
||||
break;
|
||||
case 'custom':
|
||||
let customBackground = [];
|
||||
const customSaved = localStorage.getItem('customBackground');
|
||||
try {
|
||||
customBackground = JSON.parse(customSaved);
|
||||
} catch (e) {
|
||||
if (customSaved !== '') {
|
||||
// move to new format
|
||||
customBackground = [customSaved];
|
||||
}
|
||||
localStorage.setItem('customBackground', JSON.stringify(customBackground));
|
||||
}
|
||||
|
||||
// pick random
|
||||
customBackground = customBackground[Math.floor(Math.random() * customBackground.length)];
|
||||
|
||||
// allow users to use offline images
|
||||
if (offline && !customBackground.startsWith('data:')) {
|
||||
return this.setState(offlineBackground('custom'));
|
||||
}
|
||||
|
||||
if (
|
||||
customBackground !== '' &&
|
||||
customBackground !== 'undefined' &&
|
||||
customBackground !== undefined
|
||||
) {
|
||||
const object = {
|
||||
url: customBackground,
|
||||
type: 'custom',
|
||||
video: videoCheck(customBackground),
|
||||
photoInfo: {
|
||||
hidden: true,
|
||||
},
|
||||
};
|
||||
|
||||
this.setState(object);
|
||||
|
||||
localStorage.setItem('currentBackground', JSON.stringify(object));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'photo_pack':
|
||||
if (offline) {
|
||||
return this.setState(offlineBackground('photo_pack'));
|
||||
}
|
||||
|
||||
const photofavourited = JSON.parse(localStorage.getItem('favourite'));
|
||||
if (photofavourited) {
|
||||
return setFavourited(photofavourited);
|
||||
}
|
||||
|
||||
const photoPack = [];
|
||||
const installed = JSON.parse(localStorage.getItem('installed'));
|
||||
installed.forEach((item) => {
|
||||
if (item.type === 'photos') {
|
||||
photoPack.push(...item.photos);
|
||||
}
|
||||
});
|
||||
if (photoPack) {
|
||||
const randomNumber = Math.floor(Math.random() * photoPack.length);
|
||||
const randomPhoto = photoPack[randomNumber];
|
||||
if (
|
||||
(localStorage.getItem('backgroundchange') === 'refresh' &&
|
||||
this.state.firstTime === true) ||
|
||||
(localStorage.getItem('backgroundchange') === null && this.state.firstTime === true)
|
||||
) {
|
||||
if (this.state.firstTime !== true) {
|
||||
localStorage.setItem('marketplaceNumber', randomNumber);
|
||||
this.setState({
|
||||
firstTime: false,
|
||||
url: randomPhoto.url.default,
|
||||
type: 'photo_pack',
|
||||
photoInfo: {
|
||||
hidden: false,
|
||||
credit: randomPhoto.photographer,
|
||||
location: randomPhoto.location,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
Number(
|
||||
Number(localStorage.getItem('backgroundStartTime')) +
|
||||
Number(localStorage.getItem('backgroundchange')) >=
|
||||
Number(Date.now()),
|
||||
)
|
||||
) {
|
||||
const randomPhoto = photoPack[localStorage.getItem('marketplaceNumber')];
|
||||
if (this.state.firstTime !== true) {
|
||||
this.setState({
|
||||
url: randomPhoto.url.default,
|
||||
type: 'photo_pack',
|
||||
photoInfo: {
|
||||
hidden: false,
|
||||
credit: randomPhoto.photographer,
|
||||
location: randomPhoto.location,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.setState({ firstTime: true });
|
||||
}
|
||||
this.setState({ firstTime: true });
|
||||
} else {
|
||||
localStorage.setItem('marketplaceNumber', randomNumber);
|
||||
return this.setState({
|
||||
url: randomPhoto.url.default,
|
||||
type: 'photo_pack',
|
||||
photoInfo: {
|
||||
hidden: false,
|
||||
credit: randomPhoto.photographer,
|
||||
location: randomPhoto.location,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const element = document.getElementById('backgroundImage');
|
||||
|
||||
// this resets it so the fade in and getting background all works properly
|
||||
const refresh = () => {
|
||||
element.classList.remove('fade-in');
|
||||
this.setState({
|
||||
url: '',
|
||||
style: '',
|
||||
type: '',
|
||||
video: false,
|
||||
photoInfo: {
|
||||
hidden: true,
|
||||
},
|
||||
});
|
||||
this.getBackground();
|
||||
};
|
||||
|
||||
EventBus.on('refresh', (data) => {
|
||||
if (data === 'welcomeLanguage') {
|
||||
localStorage.setItem('welcomeImage', JSON.stringify(this.state));
|
||||
}
|
||||
|
||||
if (data === 'background') {
|
||||
if (localStorage.getItem('background') === 'false') {
|
||||
// user is using custom colour or image
|
||||
if (this.state.photoInfo.hidden === false) {
|
||||
document.querySelector('.photoInformation').style.display = 'none';
|
||||
}
|
||||
|
||||
// video backgrounds
|
||||
if (this.state.video === true) {
|
||||
return (document.getElementById('backgroundVideo').style.display = 'none');
|
||||
} else {
|
||||
return (element.style.display = 'none');
|
||||
}
|
||||
}
|
||||
|
||||
// video backgrounds
|
||||
if (this.state.video === true) {
|
||||
document.getElementById('backgroundVideo').style.display = 'block';
|
||||
} else {
|
||||
if (this.state.photoInfo.hidden === false) {
|
||||
try {
|
||||
document.querySelector('.photoInformation').style.display = 'flex';
|
||||
} catch (e) {
|
||||
// Disregard exception
|
||||
}
|
||||
}
|
||||
|
||||
element.style.display = 'block';
|
||||
}
|
||||
|
||||
const backgroundType = localStorage.getItem('backgroundType');
|
||||
|
||||
if (this.state.photoInfo.offline !== true) {
|
||||
// basically check to make sure something has changed before we try getting another background
|
||||
if (
|
||||
backgroundType !== this.state.type ||
|
||||
(this.state.type === 'api' &&
|
||||
localStorage.getItem('backgroundAPI') !== this.state.currentAPI) ||
|
||||
(this.state.type === 'custom' &&
|
||||
localStorage.getItem('customBackground') !== this.state.url) ||
|
||||
JSON.parse(localStorage.getItem('backgroundExclude')).includes(this.state.photoInfo.pun)
|
||||
) {
|
||||
return refresh();
|
||||
}
|
||||
} else if (backgroundType !== this.state.type) {
|
||||
return refresh();
|
||||
}
|
||||
|
||||
// background effects so we don't get another image again
|
||||
const backgroundFilterSetting = localStorage.getItem('backgroundFilter');
|
||||
const backgroundFilter = backgroundFilterSetting && backgroundFilterSetting !== 'none';
|
||||
|
||||
if (this.state.video === true) {
|
||||
document.getElementById('backgroundVideo').style.webkitFilter =
|
||||
`blur(${localStorage.getItem('blur')}px) brightness(${localStorage.getItem(
|
||||
'brightness',
|
||||
)}%) ${
|
||||
backgroundFilter
|
||||
? backgroundFilterSetting +
|
||||
'(' +
|
||||
localStorage.getItem('backgroundFilterAmount') +
|
||||
'%)'
|
||||
: ''
|
||||
}`;
|
||||
} else {
|
||||
element.style.webkitFilter = `blur(${localStorage.getItem(
|
||||
'blur',
|
||||
)}px) brightness(${localStorage.getItem('brightness')}%) ${
|
||||
backgroundFilter
|
||||
? backgroundFilterSetting +
|
||||
'(' +
|
||||
localStorage.getItem('backgroundFilterAmount') +
|
||||
'%)'
|
||||
: ''
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
// uninstall photo pack reverts your background to what you had previously
|
||||
if (
|
||||
data === 'marketplacebackgrounduninstall' ||
|
||||
data === 'backgroundwelcome' ||
|
||||
data === 'backgroundrefresh'
|
||||
) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
if (localStorage.getItem('welcomeTab')) {
|
||||
return this.setState(JSON.parse(localStorage.getItem('welcomeImage')));
|
||||
}
|
||||
|
||||
if (
|
||||
localStorage.getItem('backgroundchange') === 'refresh' ||
|
||||
localStorage.getItem('backgroundchange') === null
|
||||
) {
|
||||
try {
|
||||
document.getElementById('backgroundImage').classList.remove('fade-in');
|
||||
document.getElementsByClassName('photoInformation')[0].classList.remove('fade-in');
|
||||
} catch (e) {
|
||||
// Disregard exception
|
||||
}
|
||||
this.getBackground();
|
||||
localStorage.setItem('backgroundStartTime', Date.now());
|
||||
}
|
||||
|
||||
const test = localStorage.getItem('backgroundchange');
|
||||
|
||||
this.interval = setInterval(() => {
|
||||
const targetTime = Number(Number(localStorage.getItem('backgroundStartTime')) + Number(test));
|
||||
const currentTime = Number(Date.now());
|
||||
const type = localStorage.getItem('backgroundType');
|
||||
|
||||
if (test !== null && test !== 'refresh') {
|
||||
if (currentTime >= targetTime) {
|
||||
element.classList.remove('fade-in');
|
||||
this.getBackground();
|
||||
localStorage.setItem('backgroundStartTime', Date.now());
|
||||
} else {
|
||||
try {
|
||||
const current = JSON.parse(localStorage.getItem('currentBackground'));
|
||||
if (current.type !== type) {
|
||||
this.getBackground();
|
||||
}
|
||||
const offline = localStorage.getItem('offlineMode');
|
||||
if (current.url.startsWith('http') && offline === 'false') {
|
||||
if (this.state.firstTime !== true) {
|
||||
this.setState(current);
|
||||
}
|
||||
} else if (current.url.startsWith('http')) {
|
||||
this.setState(offlineBackground());
|
||||
}
|
||||
if (this.state.firstTime !== true) {
|
||||
this.setState(current);
|
||||
}
|
||||
this.setState({ firstTime: true });
|
||||
} catch (e) {
|
||||
this.setBackground();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// only set once we've got the info
|
||||
componentDidUpdate() {
|
||||
if (this.state.video === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setBackground();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.video === true) {
|
||||
const enabled = (setting) => {
|
||||
return localStorage.getItem(setting) === 'true';
|
||||
};
|
||||
|
||||
return (
|
||||
<video
|
||||
autoPlay
|
||||
muted={enabled('backgroundVideoMute')}
|
||||
loop={enabled('backgroundVideoLoop')}
|
||||
style={{
|
||||
WebkitFilter: `blur(${localStorage.getItem(
|
||||
'blur',
|
||||
)}px) brightness(${localStorage.getItem('brightness')}%)`,
|
||||
}}
|
||||
id="backgroundVideo"
|
||||
>
|
||||
<source src={this.state.url} />
|
||||
</video>
|
||||
);
|
||||
}
|
||||
|
||||
const backgroundFilter = localStorage.getItem('backgroundFilter');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
WebkitFilter: `blur(${localStorage.getItem(
|
||||
'blur',
|
||||
)}px) brightness(${localStorage.getItem('brightness')}%) ${
|
||||
backgroundFilter && backgroundFilter !== 'none'
|
||||
? backgroundFilter + '(' + localStorage.getItem('backgroundFilterAmount') + '%)'
|
||||
: ''
|
||||
}`,
|
||||
}}
|
||||
id="backgroundImage"
|
||||
/>
|
||||
{this.state.photoInfo.credit !== '' && (
|
||||
<PhotoInformation
|
||||
info={this.state.photoInfo}
|
||||
api={this.state.currentAPI}
|
||||
url={this.state.url}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
49
src/features/widgets/background/ExcludeModal.jsx
Normal file
49
src/features/widgets/background/ExcludeModal.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import variables from 'config/variables';
|
||||
import { memo } from 'react';
|
||||
import EventBus from 'modules/helpers/eventbus';
|
||||
import { Tooltip } from 'components/Elements';
|
||||
|
||||
import { MdClose, MdDone } from 'react-icons/md';
|
||||
|
||||
function ExcludeModal({ modalClose, info }) {
|
||||
const excludeImage = async () => {
|
||||
let backgroundExclude = JSON.parse(localStorage.getItem('backgroundExclude'));
|
||||
backgroundExclude.push(info.pun);
|
||||
backgroundExclude = JSON.stringify(backgroundExclude);
|
||||
localStorage.setItem('backgroundExclude', backgroundExclude);
|
||||
EventBus.emit('refresh', 'background');
|
||||
modalClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="smallModal">
|
||||
<div className="shareHeader">
|
||||
<span className="title">
|
||||
{variables.getMessage('modals.main.settings.sections.advanced.reset_modal.title')}
|
||||
</span>
|
||||
<Tooltip
|
||||
title={variables.getMessage('modals.main.settings.sections.advanced.reset_modal.cancel')}
|
||||
>
|
||||
<div className="close" onClick={modalClose}>
|
||||
<MdClose />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage('widgets.background.exclude_confirm', { category: info.category })}
|
||||
</span>
|
||||
<div className="resetFooter">
|
||||
<button className="textButton" onClick={modalClose}>
|
||||
<MdClose />
|
||||
{variables.getMessage('modals.main.settings.sections.advanced.reset_modal.cancel')}
|
||||
</button>
|
||||
<button onClick={() => excludeImage()}>
|
||||
<MdDone />
|
||||
{variables.getMessage('widgets.background.confirm')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(ExcludeModal);
|
||||
107
src/features/widgets/background/Favourite.jsx
Normal file
107
src/features/widgets/background/Favourite.jsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } 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" />,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
favourited: localStorage.getItem('favourite')
|
||||
? this.buttons.favourited
|
||||
: this.buttons.unfavourited,
|
||||
};
|
||||
}
|
||||
|
||||
async favourite() {
|
||||
if (localStorage.getItem('favourite')) {
|
||||
localStorage.removeItem('favourite');
|
||||
this.setState({
|
||||
favourited: this.buttons.unfavourited,
|
||||
});
|
||||
variables.stats.postEvent('feature', 'Background favourite');
|
||||
} else {
|
||||
const type = localStorage.getItem('backgroundType');
|
||||
|
||||
switch (type) {
|
||||
case 'colour':
|
||||
return;
|
||||
case 'random_colour':
|
||||
case 'random_gradient':
|
||||
localStorage.setItem(
|
||||
'favourite',
|
||||
JSON.stringify({
|
||||
type: localStorage.getItem('backgroundType'),
|
||||
url: document.getElementById('backgroundImage').style.background,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
let url = document
|
||||
.getElementById('backgroundImage')
|
||||
.style.backgroundImage.replace('url("', '')
|
||||
.replace('")', '')
|
||||
.replace(variables.constants.DDG_IMAGE_PROXY, '');
|
||||
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.startsWith('blob:')) {
|
||||
const reader = new FileReader();
|
||||
url = await new Promise(async (resolve) => {
|
||||
reader.onloadend = () => resolve(reader.result);
|
||||
reader.readAsDataURL(await (await fetch(url)).blob());
|
||||
});
|
||||
}
|
||||
|
||||
if (type === 'custom') {
|
||||
localStorage.setItem(
|
||||
'favourite',
|
||||
JSON.stringify({
|
||||
type,
|
||||
url,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// photo information now hides information if it isn't sent, unless if photoinformation hover is hidden
|
||||
const location = document.getElementById('infoLocation');
|
||||
const camera = document.getElementById('infoCamera');
|
||||
|
||||
localStorage.setItem(
|
||||
'favourite',
|
||||
JSON.stringify({
|
||||
type,
|
||||
url,
|
||||
credit: this.props.credit || '',
|
||||
location: location?.innerText,
|
||||
camera: camera?.innerText,
|
||||
resolution: document.getElementById('infoResolution').textContent || '',
|
||||
offline: this.props.offline,
|
||||
pun: this.props.pun,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
favourited: this.buttons.favourited,
|
||||
});
|
||||
variables.stats.postEvent('feature', 'Background unfavourite');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (localStorage.getItem('backgroundType') === 'colour') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.state.favourited;
|
||||
}
|
||||
}
|
||||
|
||||
export default Favourite;
|
||||
89
src/features/widgets/background/Maximise.jsx
Normal file
89
src/features/widgets/background/Maximise.jsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } 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) {
|
||||
// don't attempt to modify the background if it isn't an image
|
||||
const backgroundType = localStorage.getItem('backgroundType');
|
||||
if (
|
||||
backgroundType === 'colour' ||
|
||||
backgroundType === 'random_colour' ||
|
||||
backgroundType === 'random_gradient'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = document.getElementById('backgroundImage');
|
||||
|
||||
let backgroundFilter;
|
||||
if (filter === true) {
|
||||
const filterData = localStorage.getItem('backgroundFilter');
|
||||
if (filterData !== 'none') {
|
||||
backgroundFilter = filterData;
|
||||
}
|
||||
}
|
||||
|
||||
element.setAttribute(
|
||||
'style',
|
||||
`background-image: url(${element.style.backgroundImage
|
||||
.replace('url("', '')
|
||||
.replace('")', '')}); -webkit-filter: blur(${blur}px) brightness(${brightness}%) ${
|
||||
backgroundFilter
|
||||
? backgroundFilter + '(' + localStorage.getItem('backgroundFilterAmount') + '%)'
|
||||
: ''
|
||||
};`,
|
||||
);
|
||||
}
|
||||
|
||||
maximise = () => {
|
||||
// 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);
|
||||
variables.stats.postEvent('feature', 'Background maximise');
|
||||
} else {
|
||||
this.setState({
|
||||
hidden: false,
|
||||
});
|
||||
|
||||
this.setAttribute(localStorage.getItem('blur'), localStorage.getItem('brightness'), true);
|
||||
variables.stats.postEvent('feature', 'Background unmaximise');
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Tooltip
|
||||
title={variables.getMessage('modals.main.settings.sections.background.buttons.view')}
|
||||
>
|
||||
<button
|
||||
style={{ fontSize: this.props.fontSize }}
|
||||
onClick={this.maximise}
|
||||
aria-label={variables.getMessage('modals.main.settings.sections.background.buttons.view')}
|
||||
>
|
||||
<MdCropFree className="topicons" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Maximise;
|
||||
393
src/features/widgets/background/PhotoInformation.jsx
Normal file
393
src/features/widgets/background/PhotoInformation.jsx
Normal file
@@ -0,0 +1,393 @@
|
||||
import variables from 'config/variables';
|
||||
import { useState, memo } from 'react';
|
||||
import Favourite from './Favourite';
|
||||
import {
|
||||
MdInfo,
|
||||
MdLocationOn,
|
||||
MdPhotoCamera,
|
||||
MdCrop as Resolution,
|
||||
MdGetApp as Download,
|
||||
MdVisibility as Views,
|
||||
MdIosShare as Share,
|
||||
MdSource as Source,
|
||||
MdFavorite as MdFavourite,
|
||||
MdCategory as Category,
|
||||
MdVisibilityOff as VisibilityOff,
|
||||
} from 'react-icons/md';
|
||||
import { Tooltip } from 'components/Elements';
|
||||
|
||||
import Modal from 'react-modal';
|
||||
import { ShareModal } from 'components/Elements';
|
||||
import ExcludeModal from './ExcludeModal';
|
||||
|
||||
/**
|
||||
* It takes a URL, fetches the resource, and returns a URL to the resource.
|
||||
* @param {string} url The URL to fetch.
|
||||
* @returns A promise that resolves to a blob URL.
|
||||
*/
|
||||
const toDataURL = async (url) => {
|
||||
const res = await fetch(url);
|
||||
return URL.createObjectURL(await res.blob());
|
||||
};
|
||||
|
||||
/**
|
||||
* It takes a string, makes it lowercase, removes commas, and replaces spaces with dashes.
|
||||
* @param {string} text The string to format.
|
||||
* @returns A function that takes a string and returns a string.
|
||||
*/
|
||||
const formatText = (text) => {
|
||||
return text.toLowerCase().replaceAll(',', '').replaceAll(' ', '-');
|
||||
};
|
||||
|
||||
/**
|
||||
* It downloads an image from a URL and saves it to the user's computer.
|
||||
* @param {object} info The photo information.
|
||||
*/
|
||||
const downloadImage = async (info) => {
|
||||
const link = document.createElement('a');
|
||||
link.href = await toDataURL(info.url);
|
||||
link.download = `mue-${formatText(info.credit)}-${formatText(info.location)}.jpg`; // image is more likely to be webp or avif btw
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
variables.stats.postEvent('feature', 'Background download');
|
||||
};
|
||||
|
||||
function PhotoInformation({ info, url, api }) {
|
||||
const [width, setWidth] = useState(0);
|
||||
const [height, setHeight] = useState(0);
|
||||
const [usePhotoMap, setPhotoMap] = useState(false);
|
||||
const [useMapIcon, setMapIcon] = useState(true);
|
||||
const [showExtraInfo, setshowExtraInfo] = useState(false);
|
||||
//const [showOld, setShowOld] = useState(true);
|
||||
const [other, setOther] = useState(false);
|
||||
const [shareModal, openShareModal] = useState(false);
|
||||
const [excludeModal, openExcludeModal] = useState(false);
|
||||
|
||||
if (info.hidden === true || !info.credit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// remove unsplash text
|
||||
const unsplash = variables.getMessage('widgets.background.unsplash');
|
||||
|
||||
let credit = info.credit;
|
||||
let photo = variables.getMessage('widgets.background.credit');
|
||||
|
||||
// unsplash credit
|
||||
if (info.photographerURL && info.photographerURL !== '' && !info.offline && api) {
|
||||
photo = (
|
||||
<a href={info.photoURL + '?utm_source=mue'} target="_blank" rel="noopener noreferrer">
|
||||
{photo}
|
||||
</a>
|
||||
);
|
||||
credit = (
|
||||
<>
|
||||
<a href={info.photographerURL} target="_blank" rel="noopener noreferrer">
|
||||
{info.credit}
|
||||
</a>{' '}
|
||||
<a href="https://unsplash.com?utm_source=mue" target="_blank" rel="noopener noreferrer">
|
||||
{unsplash}
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const ddgProxy = localStorage.getItem('ddgProxy') === 'true';
|
||||
|
||||
// get resolution
|
||||
const img = new Image();
|
||||
img.onload = (event) => {
|
||||
setWidth(event.target.width);
|
||||
setHeight(event.target.height);
|
||||
};
|
||||
img.src =
|
||||
ddgProxy && !info.offline && !url.startsWith('data:')
|
||||
? variables.constants.DDG_IMAGE_PROXY + url
|
||||
: url;
|
||||
|
||||
// info is still there because we want the favourite button to work
|
||||
if (localStorage.getItem('photoInformation') === 'false') {
|
||||
return (
|
||||
<div style={{ display: 'none' }}>
|
||||
<span id="credit">{credit}</span>
|
||||
<span id="infoLocation">{info.location}</span>
|
||||
<span id="infoCamera">{info.camera}</span>
|
||||
<span id="infoResolution">
|
||||
{width}x{height}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let showingPhotoMap = false;
|
||||
const photoMap = () => {
|
||||
if (
|
||||
localStorage.getItem('photoMap') !== 'true' ||
|
||||
!info.latitude ||
|
||||
!info.longitude ||
|
||||
usePhotoMap === false
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tile =
|
||||
variables.constants.API_URL + `/map?latitude=${info.latitude}&longitude=${info.longitude}`;
|
||||
showingPhotoMap = true;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`${variables.constants.OPENSTREETMAP_URL}/?mlat=${info.latitude}&mlon=${info.longitude}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img className="locationMap" src={tile} alt="location" draggable={false} />
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
const InformationItems = () => {
|
||||
return (
|
||||
<div className="extra-content">
|
||||
{info.location && info.location !== 'N/A' ? (
|
||||
<div className="row" title={variables.getMessage('widgets.background.location')}>
|
||||
<MdLocationOn />
|
||||
<span id="infoLocation">{info.location}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{info.camera && info.camera !== 'N/A' ? (
|
||||
<div className="row" title={variables.getMessage('widgets.background.camera')}>
|
||||
<MdPhotoCamera />
|
||||
<span id="infoCamera">{info.camera}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="row" title={variables.getMessage('widgets.background.resolution')}>
|
||||
<Resolution />
|
||||
<span id="infoResolution">
|
||||
{width}x{height}
|
||||
</span>
|
||||
</div>
|
||||
{info.category && (
|
||||
<div className="row" title={variables.getMessage('widgets.background.category')}>
|
||||
<Category />
|
||||
<span id="infoCategory">{info.category[0].toUpperCase() + info.category.slice(1)}</span>
|
||||
</div>
|
||||
)}
|
||||
{api && (
|
||||
<div className="row" title={variables.getMessage('widgets.background.source')}>
|
||||
<Source />
|
||||
<span id="infoSource">
|
||||
{info.photoURL ? (
|
||||
<a
|
||||
href={info.photoURL + '?utm_source=mue'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="link"
|
||||
>
|
||||
{api.charAt(0).toUpperCase() + api.slice(1)}
|
||||
</a>
|
||||
) : (
|
||||
<a href={info.url} target="_blank" rel="noopener noreferrer" className="link">
|
||||
{api.charAt(0).toUpperCase() + api.slice(1)}
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ActionButtons = () => {
|
||||
return (
|
||||
<div className="buttons">
|
||||
{!info.offline && (
|
||||
<Tooltip title={variables.getMessage('widgets.quote.share')} key="share" placement="top">
|
||||
<Share onClick={() => openShareModal(true)} />
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip
|
||||
title={variables.getMessage('widgets.quote.favourite')}
|
||||
key="favourite"
|
||||
placement="top"
|
||||
>
|
||||
<Favourite
|
||||
pun={info.pun}
|
||||
offline={info.offline}
|
||||
credit={info.credit}
|
||||
photoURL={info.url}
|
||||
/>
|
||||
</Tooltip>
|
||||
{!info.offline && (
|
||||
<Tooltip
|
||||
title={variables.getMessage('widgets.background.download')}
|
||||
key="download"
|
||||
placement="top"
|
||||
>
|
||||
<Download onClick={() => downloadImage(info)} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{info.pun && (
|
||||
<Tooltip
|
||||
title={variables.getMessage('widgets.background.exclude')}
|
||||
key="exclude"
|
||||
placement="top"
|
||||
>
|
||||
<VisibilityOff onClick={() => openExcludeModal(true)} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const UnsplashStats = () => {
|
||||
return (
|
||||
<div className="unsplashStats">
|
||||
<div title={variables.getMessage('widgets.background.views')}>
|
||||
<Views />
|
||||
<span>{info.views.toLocaleString()}</span>
|
||||
</div>
|
||||
<div title={variables.getMessage('widgets.background.downloads')}>
|
||||
<Download />
|
||||
<span>{info.downloads.toLocaleString()}</span>
|
||||
</div>
|
||||
{!!info.likes ? (
|
||||
<div title={variables.getMessage('widgets.background.likes')}>
|
||||
<MdFavourite />
|
||||
<span>{info.likes.toLocaleString()}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
let photoMapClassList = 'map-concept';
|
||||
if (photoMap() !== null) {
|
||||
photoMapClassList += ' photoMap';
|
||||
}
|
||||
|
||||
// only request map image if the user looks at the photo information
|
||||
// this is to reduce requests to the api
|
||||
try {
|
||||
document.getElementsByClassName('photoInformation')[0].onmouseover = () => {
|
||||
try {
|
||||
setPhotoMap(true);
|
||||
setMapIcon(false);
|
||||
} catch (e) {}
|
||||
};
|
||||
} catch (e) {}
|
||||
|
||||
const widgetStyle = localStorage.getItem('widgetStyle');
|
||||
|
||||
return (
|
||||
<div
|
||||
className="photoInformationHolder"
|
||||
onMouseEnter={() => setOther(true)}
|
||||
onMouseLeave={() => setOther(false)}
|
||||
>
|
||||
<Modal
|
||||
closeTimeoutMS={300}
|
||||
isOpen={shareModal}
|
||||
className="Modal mainModal"
|
||||
overlayClassName="Overlay"
|
||||
ariaHideApp={false}
|
||||
onRequestClose={() => openShareModal(false)}
|
||||
>
|
||||
<ShareModal data={info.photoURL || info.url} modalClose={() => openShareModal(false)} />
|
||||
</Modal>
|
||||
<Modal
|
||||
closeTimeoutMS={300}
|
||||
isOpen={excludeModal}
|
||||
className="Modal mainModal"
|
||||
overlayClassName="Overlay"
|
||||
ariaHideApp={false}
|
||||
onRequestClose={() => openExcludeModal(false)}
|
||||
>
|
||||
<ExcludeModal info={info} modalClose={() => openExcludeModal(false)} />
|
||||
</Modal>
|
||||
{widgetStyle === 'legacy' && (
|
||||
<div className="photoInformation-legacy">
|
||||
<MdInfo />
|
||||
<span className="title">
|
||||
{photo} <span id="credit">{credit}</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{widgetStyle !== 'legacy' || other ? (
|
||||
<div
|
||||
className="photoInformation orHover"
|
||||
style={{ padding: widgetStyle === 'legacy' && '20px' }}
|
||||
onMouseEnter={() => setshowExtraInfo(true)}
|
||||
onMouseLeave={() => setshowExtraInfo(false)}
|
||||
>
|
||||
<div className={photoMapClassList}>
|
||||
{useMapIcon || photoMap() === null ? <MdLocationOn /> : ''}
|
||||
{photoMap()}
|
||||
</div>
|
||||
{showingPhotoMap && (
|
||||
<div className="copyright">
|
||||
<a
|
||||
href="https://www.mapbox.com/about/maps/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{' '}
|
||||
© Mapbox{' '}
|
||||
</a>{' '}
|
||||
•{' '}
|
||||
<a
|
||||
href="https://www.openstreetmap.org/about/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{' '}
|
||||
© OpenStreetMap{' '}
|
||||
</a>{' '}
|
||||
•{' '}
|
||||
<a
|
||||
href="https://www.mapbox.com/map-feedback/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{' '}
|
||||
Improve this map{' '}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<div className="photoInformation-content">
|
||||
<div className="photoInformation-text">
|
||||
<span
|
||||
className="title"
|
||||
title={
|
||||
(showExtraInfo || other) && info.description ? info.description : info.location
|
||||
}
|
||||
>
|
||||
{(showExtraInfo || other) && info.description
|
||||
? info.description.length > 40
|
||||
? info.description.substring(0, 40) + '...'
|
||||
: info.description
|
||||
: info.location?.split(',').slice(-2).join(', ').trim()}
|
||||
</span>
|
||||
<span className="subtitle" id="credit">
|
||||
{photo} {credit}
|
||||
</span>
|
||||
</div>
|
||||
{info.views && info.downloads !== null ? <UnsplashStats /> : null}
|
||||
</div>
|
||||
|
||||
{showExtraInfo || other ? (
|
||||
<>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage('widgets.background.information')}
|
||||
</span>
|
||||
<InformationItems />
|
||||
<ActionButtons />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(PhotoInformation);
|
||||
475
src/features/widgets/background/scss/_photoinformation.scss
Normal file
475
src/features/widgets/background/scss/_photoinformation.scss
Normal file
@@ -0,0 +1,475 @@
|
||||
@import 'scss/variables';
|
||||
|
||||
.photoInformationHolder {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
left: 1rem;
|
||||
flex-flow: column-reverse;
|
||||
display: flex;
|
||||
|
||||
&:hover {
|
||||
.photoInformation {
|
||||
height: auto;
|
||||
align-items: flex-start;
|
||||
flex-flow: column;
|
||||
|
||||
.buttons {
|
||||
padding: 30px 0 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.extra-content {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
gap: 10px;
|
||||
transition: 0.8s;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
padding-top: 5px;
|
||||
gap: 20px;
|
||||
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
flex-direction: row;
|
||||
gap: 5px;
|
||||
|
||||
svg {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copyright {
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
gap: 10px;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.photoInformation-content {
|
||||
padding: 20px 20px 20px 0;
|
||||
}
|
||||
|
||||
.map-concept {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mapOn {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
flex-shrink: 0;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
svg {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.photoInformation-legacy {
|
||||
font-size: 1.36em;
|
||||
text-shadow: 0 0 10px rgb(0 0 0 / 30%);
|
||||
color: #fff;
|
||||
z-index: 99;
|
||||
user-select: none;
|
||||
cursor: initial;
|
||||
font-weight: bolder;
|
||||
|
||||
svg {
|
||||
font-size: 1.36em;
|
||||
}
|
||||
|
||||
&:hover .infoCard {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
|
||||
svg:hover + .infoCard {
|
||||
display: block;
|
||||
}
|
||||
|
||||
a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
+ .photoInformation {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.infoCard {
|
||||
display: none;
|
||||
background: var(--background);
|
||||
box-shadow: 0 0 10px rgb(0 0 0 / 30%);
|
||||
color: var(--modal-text);
|
||||
position: fixed;
|
||||
bottom: 2.9rem;
|
||||
left: 0.7em;
|
||||
padding: 1rem;
|
||||
border-radius: 24px 24px 24px 0;
|
||||
width: 300px !important;
|
||||
text-align: left;
|
||||
text-shadow: none;
|
||||
|
||||
svg {
|
||||
margin-right: 0.5rem;
|
||||
vertical-align: middle;
|
||||
display: inline-flex;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.locationMap {
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.mapboxLogo {
|
||||
height: 20px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
span,
|
||||
svg {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
span {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
h1,
|
||||
.MuiSvgIcon-root {
|
||||
user-select: none;
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
h1,
|
||||
.infoIcon {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
.infoIcon {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 3px;
|
||||
background: var(--photo-info);
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
span {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.download {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.mapCopyright {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
.photoInformation {
|
||||
@extend %basic;
|
||||
|
||||
animation: fadeIn 2s;
|
||||
min-width: 300px;
|
||||
font-size: 0.8em;
|
||||
font-weight: 300;
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
transition: 0.8s cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
|
||||
// width: 300px;
|
||||
padding: 10px;
|
||||
|
||||
.link {
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
@include themed {
|
||||
color: t($link);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
padding: 20px;
|
||||
height: auto;
|
||||
align-items: flex-start;
|
||||
flex-flow: column;
|
||||
|
||||
.buttons {
|
||||
padding: 30px 0 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.extra-content {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 10px;
|
||||
transition: 0.8s;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
|
||||
svg {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copyright {
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
gap: 10px;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.photoInformation-content {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.map-concept {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.map-concept {
|
||||
@extend %basic;
|
||||
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none !important;
|
||||
|
||||
svg {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.location {
|
||||
font-size: 1.9em;
|
||||
}
|
||||
|
||||
.credit {
|
||||
font-size: 1.4em;
|
||||
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
|
||||
a {
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
color: var(--modal-link);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.photoInformation-content {
|
||||
line-height: 2em;
|
||||
margin-right: 3px;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding-left: 20px;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
|
||||
a {
|
||||
@include themed {
|
||||
color: t($link);
|
||||
}
|
||||
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.extra-content {
|
||||
display: none;
|
||||
flex-flow: column;
|
||||
width: 100%;
|
||||
transition: 1s;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
|
||||
svg {
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copyright {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 20px 20px 0 0;
|
||||
display: none;
|
||||
|
||||
svg {
|
||||
@include basicIconButton(11px, 1.3rem, ui);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.photoMap {
|
||||
img {
|
||||
height: 50px !important;
|
||||
width: 50px !important;
|
||||
}
|
||||
|
||||
a {
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.photoInformation:hover,
|
||||
.photoInformation-legacy + .photoInformation {
|
||||
.photoMap {
|
||||
height: auto !important;
|
||||
|
||||
img {
|
||||
height: auto !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
a {
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unsplashStats {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: space-between;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.photoInformation-text {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
53
src/features/widgets/background/scss/index.scss
Normal file
53
src/features/widgets/background/scss/index.scss
Normal file
@@ -0,0 +1,53 @@
|
||||
@import 'photoinformation';
|
||||
@import 'scss/mixins';
|
||||
|
||||
#backgroundImage {
|
||||
height: 100vh;
|
||||
background-size: cover !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position: center !important;
|
||||
background-attachment: fixed !important;
|
||||
zoom: 100% !important;
|
||||
}
|
||||
|
||||
.backgroundTransform {
|
||||
@include animation('fadein2 2s');
|
||||
|
||||
transition: background 2s;
|
||||
}
|
||||
|
||||
@include keyframes(fadein2) {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.backgroundPreload {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#backgroundVideo {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
@include animation('fadein 1s');
|
||||
}
|
||||
|
||||
@include keyframes(fadein) {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
164
src/features/widgets/greeting/Greeting.jsx
Normal file
164
src/features/widgets/greeting/Greeting.jsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent, createRef } from 'react';
|
||||
|
||||
import { nth, convertTimezone } from 'modules/helpers/date';
|
||||
import EventBus from 'modules/helpers/eventbus';
|
||||
|
||||
import './greeting.scss';
|
||||
|
||||
export default class Greeting extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
greeting: '',
|
||||
};
|
||||
this.timer = undefined;
|
||||
this.greeting = createRef();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the greeting message for the events of Christmas, New Year and Halloween.
|
||||
* If the events setting is disabled, then the greeting message will not be changed.
|
||||
* @param {Date} time The current time.
|
||||
* @param {String} message The current greeting message.
|
||||
* @returns The message variable is being returned.
|
||||
*/
|
||||
doEvents(time, message) {
|
||||
if (localStorage.getItem('events') === 'false') {
|
||||
return message;
|
||||
}
|
||||
|
||||
// Get current month & day
|
||||
const month = time.getMonth();
|
||||
const date = time.getDate();
|
||||
|
||||
// If it's December 25th, set the greeting string to "Merry Christmas"
|
||||
if (month === 11 && date === 25) {
|
||||
message = variables.getMessage('widgets.greeting.christmas');
|
||||
// If the date is January 1st, set the greeting string to "Happy new year"
|
||||
} else if (month === 0 && date === 1) {
|
||||
message = variables.getMessage('widgets.greeting.newyear');
|
||||
// If it's October 31st, set the greeting string to "Happy Halloween"
|
||||
} else if (month === 9 && date === 31) {
|
||||
message = variables.getMessage('widgets.greeting.halloween');
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* It takes a date object and returns the age of the person in years.
|
||||
* @param {Date} date The date of birth.
|
||||
* @returns The age of the person.
|
||||
*/
|
||||
calculateAge(date) {
|
||||
const diff = Date.now() - date.getTime();
|
||||
const birthday = new Date(diff);
|
||||
return Math.abs(birthday.getUTCFullYear() - 1970);
|
||||
}
|
||||
|
||||
getGreeting(time = 60000 - (Date.now() % 60000)) {
|
||||
this.timer = setTimeout(() => {
|
||||
let now = new Date();
|
||||
const timezone = localStorage.getItem('timezone');
|
||||
if (timezone && timezone !== 'auto') {
|
||||
now = convertTimezone(now, timezone);
|
||||
}
|
||||
|
||||
const hour = now.getHours();
|
||||
|
||||
// Set the default greeting string to "Good evening"
|
||||
let message = variables.getMessage('widgets.greeting.evening');
|
||||
// If it's before 12am, set the greeting string to "Good morning"
|
||||
if (hour < 12) {
|
||||
message = variables.getMessage('widgets.greeting.morning');
|
||||
// If it's before 6pm, set the greeting string to "Good afternoon"
|
||||
} else if (hour < 18) {
|
||||
message = variables.getMessage('widgets.greeting.afternoon');
|
||||
}
|
||||
|
||||
// Events and custom
|
||||
const custom = localStorage.getItem('defaultGreetingMessage');
|
||||
if (custom === 'false') {
|
||||
message = '';
|
||||
} else {
|
||||
message = this.doEvents(now, message);
|
||||
}
|
||||
|
||||
// Name
|
||||
let name = '';
|
||||
const data = localStorage.getItem('greetingName');
|
||||
|
||||
if (typeof data === 'string') {
|
||||
if (data.replace(/\s/g, '').length > 0) {
|
||||
name = `, ${data.trim()}`;
|
||||
}
|
||||
}
|
||||
|
||||
const birthday = localStorage.getItem('birthdayenabled');
|
||||
|
||||
if (custom === 'false' && birthday !== 'true') {
|
||||
name = name.replace(',', '');
|
||||
}
|
||||
|
||||
// Birthday
|
||||
if (birthday === 'true') {
|
||||
const birth = new Date(localStorage.getItem('birthday'));
|
||||
|
||||
if (birth.getDate() === now.getDate() && birth.getMonth() === now.getMonth()) {
|
||||
if (localStorage.getItem('birthdayage') === 'true' && this.calculateAge(birth) !== 0) {
|
||||
const text = variables.getMessage('widgets.greeting.birthday').split(' ');
|
||||
message = `${text[0]} ${nth(this.calculateAge(birth))} ${text[1]}`;
|
||||
} else {
|
||||
message = variables.getMessage('widgets.greeting.birthday');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the state to the greeting string
|
||||
this.setState({
|
||||
greeting: `${message}${name}`,
|
||||
});
|
||||
|
||||
this.getGreeting();
|
||||
}, time);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
if (data === 'greeting' || data === 'timezone') {
|
||||
if (localStorage.getItem('greeting') === 'false') {
|
||||
return (this.greeting.current.style.display = 'none');
|
||||
}
|
||||
|
||||
this.timer = null;
|
||||
this.getGreeting(0);
|
||||
|
||||
this.greeting.current.style.display = 'block';
|
||||
this.greeting.current.style.fontSize = `${
|
||||
1.6 * Number((localStorage.getItem('zoomGreeting') || 100) / 100)
|
||||
}em`;
|
||||
}
|
||||
});
|
||||
|
||||
// this comment can apply to all widget zoom features apart from the general one in the Accessibility section
|
||||
// in a nutshell: 1.6 is the current font size, and we do "localstorage || 100" so we don't have to try that 4.0 -> 5.0 thing again
|
||||
this.greeting.current.style.fontSize = `${
|
||||
1.6 * Number((localStorage.getItem('zoomGreeting') || 100) / 100)
|
||||
}em`;
|
||||
|
||||
this.getGreeting(0);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span className="greeting" ref={this.greeting}>
|
||||
{this.state.greeting}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
9
src/features/widgets/greeting/greeting.scss
Normal file
9
src/features/widgets/greeting/greeting.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
.greeting {
|
||||
margin: 0;
|
||||
font-size: 1.6em;
|
||||
cursor: initial;
|
||||
user-select: none;
|
||||
font-weight: 600;
|
||||
|
||||
--shadow-shift: 0.2rem;
|
||||
}
|
||||
45
src/features/widgets/message/Message.jsx
Normal file
45
src/features/widgets/message/Message.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import EventBus from 'modules/helpers/eventbus';
|
||||
import './message.scss';
|
||||
|
||||
const Message = () => {
|
||||
const [messageText, setMessageText] = useState('');
|
||||
const [display, setDisplay] = useState('none');
|
||||
const [fontSize, setFontSize] = useState('1em');
|
||||
const message = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
const handleRefresh = (data) => {
|
||||
if (data === 'message') {
|
||||
const messageSetting = localStorage.getItem('message');
|
||||
const zoomMessage = localStorage.getItem('zoomMessage');
|
||||
setDisplay(messageSetting === 'false' ? 'none' : 'block');
|
||||
setFontSize(`${1 * Number((zoomMessage || 100) / 100)}em`);
|
||||
}
|
||||
};
|
||||
|
||||
const messages = JSON.parse(localStorage.getItem('messages')) || [];
|
||||
if (messages.length > 0) {
|
||||
setMessageText(messages[Math.floor(Math.random() * messages.length)]);
|
||||
setDisplay('block');
|
||||
}
|
||||
|
||||
EventBus.on('refresh', handleRefresh);
|
||||
return () => {
|
||||
EventBus.off('refresh');
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<h2 className="message" ref={message} style={{ display, fontSize }}>
|
||||
{messageText.split('\\n').map((item) => (
|
||||
<span key={item}>
|
||||
{item}
|
||||
<br />
|
||||
</span>
|
||||
))}
|
||||
</h2>
|
||||
);
|
||||
};
|
||||
|
||||
export default Message;
|
||||
6
src/features/widgets/message/message.scss
Normal file
6
src/features/widgets/message/message.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
.message {
|
||||
cursor: default;
|
||||
margin: 0;
|
||||
user-select: none;
|
||||
font-size: 1em;
|
||||
}
|
||||
154
src/features/widgets/navbar/Apps.jsx
Normal file
154
src/features/widgets/navbar/Apps.jsx
Normal file
@@ -0,0 +1,154 @@
|
||||
// TODO: make it work with pins or on click or smth
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent, memo, useState } from 'react';
|
||||
|
||||
import { MdPlaylistRemove, MdOutlineApps } from 'react-icons/md';
|
||||
import { Tooltip } from 'components/Elements';
|
||||
|
||||
import { shift, useFloating } from '@floating-ui/react-dom';
|
||||
import EventBus from 'modules/helpers/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',
|
||||
};
|
||||
}
|
||||
|
||||
setZoom() {
|
||||
this.setState({
|
||||
zoomFontSize: Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem',
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
if (data === 'navbar') {
|
||||
this.forceUpdate();
|
||||
try {
|
||||
this.setZoom();
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
|
||||
this.setZoom();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
}
|
||||
|
||||
showApps() {
|
||||
this.setState({
|
||||
showApps: true,
|
||||
});
|
||||
}
|
||||
|
||||
hideApps() {
|
||||
this.setState({
|
||||
showApps: localStorage.getItem('AppsPinned') === 'true',
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const appsInfo = this.state.apps;
|
||||
|
||||
return (
|
||||
<div className="notes" onMouseLeave={() => this.hideApps()} onFocus={() => this.showApps()}>
|
||||
<button
|
||||
className="first"
|
||||
onMouseEnter={() => this.showApps()}
|
||||
onFocus={() => this.hideApps()}
|
||||
onBlur={() => this.showApps()}
|
||||
ref={this.props.appsRef}
|
||||
style={{ fontSize: this.state.zoomFontSize }}
|
||||
>
|
||||
<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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function AppsWrapper() {
|
||||
const [reference, setReference] = useState(null);
|
||||
|
||||
const { x, y, refs, strategy } = useFloating({
|
||||
placement: 'bottom',
|
||||
middleware: [shift()],
|
||||
elements: {
|
||||
reference,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Apps
|
||||
appsRef={setReference}
|
||||
floatRef={refs.setFloating}
|
||||
position={strategy}
|
||||
xPosition={x}
|
||||
yPosition={y}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(AppsWrapper);
|
||||
162
src/features/widgets/navbar/Navbar.jsx
Normal file
162
src/features/widgets/navbar/Navbar.jsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent, createRef } from 'react';
|
||||
|
||||
import { MdRefresh, MdSettings } from 'react-icons/md';
|
||||
|
||||
import Notes from './Notes';
|
||||
import Todo from './Todo';
|
||||
import Apps from './Apps';
|
||||
import Maximise from '../background/Maximise';
|
||||
import { Tooltip } from 'components/Elements';
|
||||
import EventBus from 'modules/helpers/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,
|
||||
};
|
||||
}
|
||||
|
||||
setZoom() {
|
||||
this.setState({
|
||||
zoomFontSize: Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem',
|
||||
});
|
||||
}
|
||||
|
||||
updateRefreshText() {
|
||||
let refreshText;
|
||||
switch (localStorage.getItem('refreshOption')) {
|
||||
case 'background':
|
||||
refreshText = variables.getMessage('modals.main.settings.sections.background.title');
|
||||
break;
|
||||
case 'quote':
|
||||
refreshText = variables.getMessage('modals.main.settings.sections.quote.title');
|
||||
break;
|
||||
case 'quotebackground':
|
||||
refreshText =
|
||||
variables.getMessage('modals.main.settings.sections.quote.title') +
|
||||
' ' +
|
||||
variables.getMessage('modals.main.settings.sections.background.title');
|
||||
break;
|
||||
default:
|
||||
refreshText = variables.getMessage(
|
||||
'modals.main.settings.sections.appearance.navbar.refresh_options.page',
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
refreshText,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
if (data === 'navbar' || data === 'background') {
|
||||
this.setState({
|
||||
refreshEnabled: localStorage.getItem('refresh'),
|
||||
refreshOption: localStorage.getItem('refreshOption'),
|
||||
});
|
||||
try {
|
||||
this.updateRefreshText();
|
||||
this.setZoom();
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
|
||||
this.updateRefreshText();
|
||||
this.setZoom();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
}
|
||||
|
||||
refresh() {
|
||||
switch (this.state.refreshOption) {
|
||||
case 'background':
|
||||
return EventBus.emit('refresh', 'backgroundrefresh');
|
||||
case 'quote':
|
||||
return EventBus.emit('refresh', 'quoterefresh');
|
||||
case 'quotebackground':
|
||||
EventBus.emit('refresh', 'quoterefresh');
|
||||
return EventBus.emit('refresh', 'backgroundrefresh');
|
||||
default:
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
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} />
|
||||
)}
|
||||
|
||||
{this.refreshEnabled !== 'false' && (
|
||||
<Tooltip
|
||||
title={variables.getMessage('widgets.navbar.tooltips.refresh')}
|
||||
subtitle={this.state.refreshText}
|
||||
>
|
||||
<button
|
||||
onClick={() => this.refresh()}
|
||||
style={{ fontSize: this.state.zoomFontSize }}
|
||||
aria-label={variables.getMessage('widgets.navbar.tooltips.refresh')}
|
||||
>
|
||||
<MdRefresh className="refreshicon topicons" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip
|
||||
title={variables.getMessage('modals.main.navbar.settings', {
|
||||
type: variables.getMessage(
|
||||
'modals.main.navbar.tooltips.refresh_' + this.refreshValue,
|
||||
),
|
||||
})}
|
||||
>
|
||||
<button
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
|
||||
return localStorage.getItem('navbarHover') === 'true' ? (
|
||||
<div className="navbar-hover">{navbar}</div>
|
||||
) : (
|
||||
navbar
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Navbar;
|
||||
173
src/features/widgets/navbar/Notes.jsx
Normal file
173
src/features/widgets/navbar/Notes.jsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent, memo, useState } from 'react';
|
||||
|
||||
import { MdContentCopy, MdAssignment, MdPushPin, MdDownload } from 'react-icons/md';
|
||||
import { useFloating, shift } from '@floating-ui/react-dom';
|
||||
import TextareaAutosize from '@mui/material/TextareaAutosize';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Tooltip } from 'components/Elements';
|
||||
|
||||
import { saveFile } from 'modules/helpers/settings/modals';
|
||||
import EventBus from 'modules/helpers/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,
|
||||
};
|
||||
}
|
||||
|
||||
setZoom() {
|
||||
this.setState({
|
||||
zoomFontSize: Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem',
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
if (data === 'navbar') {
|
||||
this.forceUpdate();
|
||||
try {
|
||||
this.setZoom();
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
|
||||
this.setZoom();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
}
|
||||
|
||||
setNotes = (e) => {
|
||||
localStorage.setItem('notes', e.target.value);
|
||||
this.setState({
|
||||
notes: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
showNotes() {
|
||||
this.setState({
|
||||
showNotes: true,
|
||||
});
|
||||
}
|
||||
|
||||
hideNotes() {
|
||||
this.setState({
|
||||
showNotes: localStorage.getItem('notesPinned') === 'true',
|
||||
});
|
||||
}
|
||||
|
||||
pin() {
|
||||
variables.stats.postEvent('feature', 'Notes pin');
|
||||
const notesPinned = localStorage.getItem('notesPinned') === 'true';
|
||||
localStorage.setItem('notesPinned', !notesPinned);
|
||||
this.setState({
|
||||
showNotes: !notesPinned,
|
||||
});
|
||||
}
|
||||
|
||||
copy() {
|
||||
variables.stats.postEvent('feature', 'Notes copied');
|
||||
navigator.clipboard.writeText(this.state.notes);
|
||||
toast(variables.getMessage('toasts.notes'));
|
||||
}
|
||||
|
||||
download() {
|
||||
const notes = localStorage.getItem('notes');
|
||||
if (!notes || notes === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
variables.stats.postEvent('feature', 'Notes download');
|
||||
saveFile(this.state.notes, 'mue-notes.txt', 'text/plain');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="notes" onMouseLeave={() => this.hideNotes()} onFocus={() => this.showNotes()}>
|
||||
<button
|
||||
className="first"
|
||||
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')}
|
||||
>
|
||||
<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>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function NotesWrapper() {
|
||||
const [reference, setReference] = useState(null);
|
||||
|
||||
const { x, y, refs, strategy } = useFloating({
|
||||
placement: 'bottom',
|
||||
middleware: [shift()],
|
||||
elements: {
|
||||
reference,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Notes
|
||||
notesRef={setReference}
|
||||
floatRef={refs.setFloating}
|
||||
position={strategy}
|
||||
xPosition={x}
|
||||
yPosition={y}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(NotesWrapper);
|
||||
257
src/features/widgets/navbar/Todo.jsx
Normal file
257
src/features/widgets/navbar/Todo.jsx
Normal file
@@ -0,0 +1,257 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent, memo, useState } from 'react';
|
||||
|
||||
import {
|
||||
MdChecklist,
|
||||
MdPushPin,
|
||||
MdDelete,
|
||||
MdPlaylistAdd,
|
||||
MdOutlineDragIndicator,
|
||||
MdPlaylistRemove,
|
||||
} from 'react-icons/md';
|
||||
import TextareaAutosize from '@mui/material/TextareaAutosize';
|
||||
import { Tooltip } from 'components/Elements';
|
||||
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import { shift, useFloating } from '@floating-ui/react-dom';
|
||||
import { sortableContainer, sortableElement, sortableHandle } from '@muetab/react-sortable-hoc';
|
||||
import EventBus from 'modules/helpers/eventbus';
|
||||
|
||||
const SortableItem = sortableElement(({ value }) => <div>{value}</div>);
|
||||
const SortableContainer = sortableContainer(({ children }) => <div>{children}</div>);
|
||||
const SortableHandle = sortableHandle(() => <MdOutlineDragIndicator />);
|
||||
|
||||
class Todo extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
todo: JSON.parse(localStorage.getItem('todo')) || [],
|
||||
visibility: localStorage.getItem('todoPinned') === 'true' ? 'visible' : 'hidden',
|
||||
marginLeft: localStorage.getItem('refresh') === 'false' ? '-200px' : '-130px',
|
||||
showTodo: localStorage.getItem('todoPinned') === 'true',
|
||||
};
|
||||
}
|
||||
|
||||
setZoom() {
|
||||
this.setState({
|
||||
zoomFontSize: Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem',
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
if (data === 'navbar') {
|
||||
this.forceUpdate();
|
||||
try {
|
||||
this.setZoom();
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
|
||||
this.setZoom();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
}
|
||||
|
||||
/**
|
||||
* It takes an array, removes an item from it, and then inserts it at a new index.
|
||||
* @param {Array} array The array to move the item in.
|
||||
* @param {Number} oldIndex The index of the item to move.
|
||||
* @param {Number} newIndex The index to move the item to.
|
||||
* @returns The result of the splice method.
|
||||
*/
|
||||
arrayMove(array, oldIndex, newIndex) {
|
||||
const result = Array.from(array);
|
||||
const [removed] = result.splice(oldIndex, 1);
|
||||
result.splice(newIndex, 0, removed);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
onSortEnd = ({ oldIndex, newIndex }) => {
|
||||
this.setState({
|
||||
todo: this.arrayMove(this.state.todo, oldIndex, newIndex),
|
||||
});
|
||||
};
|
||||
|
||||
showTodo() {
|
||||
this.setState({
|
||||
showTodo: true,
|
||||
});
|
||||
}
|
||||
|
||||
hideTodo() {
|
||||
this.setState({
|
||||
showTodo: localStorage.getItem('todoPinned') === 'true',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function takes in an action, an index, and data, and then updates the todo list accordingly.
|
||||
* @param {String} action The action to perform. Can be 'add', 'remove', 'set', or 'done'.
|
||||
* @param {Number} index The index of the item to perform the action on.
|
||||
* @param {Object} data The data to use for the action.
|
||||
*/
|
||||
updateTodo(action, index, data) {
|
||||
let todo = this.state.todo;
|
||||
switch (action) {
|
||||
case 'add':
|
||||
todo.push({
|
||||
value: '',
|
||||
done: false,
|
||||
});
|
||||
break;
|
||||
case 'remove':
|
||||
todo.splice(index, 1);
|
||||
break;
|
||||
case 'set':
|
||||
todo[index] = {
|
||||
value: data.target.value,
|
||||
done: todo[index].done,
|
||||
};
|
||||
break;
|
||||
case 'done':
|
||||
todo[index].done = !todo[index].done;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
localStorage.setItem('todo', JSON.stringify(todo));
|
||||
this.setState({
|
||||
todo,
|
||||
});
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
pin() {
|
||||
variables.stats.postEvent('feature', 'Todo pin');
|
||||
const todoPinned = localStorage.getItem('todoPinned') === 'true';
|
||||
localStorage.setItem('todoPinned', !todoPinned);
|
||||
this.setState({
|
||||
showTodo: !todoPinned,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="notes" onMouseLeave={() => this.hideTodo()} onFocus={() => this.showTodo()}>
|
||||
<button
|
||||
className="first"
|
||||
onMouseEnter={() => this.showTodo()}
|
||||
onFocus={() => this.hideTodo()}
|
||||
onBlur={() => this.showTodo()}
|
||||
ref={this.props.todoRef}
|
||||
style={{ fontSize: this.state.zoomFontSize }}
|
||||
>
|
||||
<MdChecklist className="topicons" />
|
||||
</button>
|
||||
{this.state.showTodo && (
|
||||
<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' }}>
|
||||
<MdChecklist />
|
||||
<span>{variables.getMessage('widgets.navbar.todo.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.navbar.todo.add')}>
|
||||
<button onClick={() => this.updateTodo('add')}>
|
||||
<MdPlaylistAdd />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className={'todoRows'}>
|
||||
{this.state.todo.length === 0 ? (
|
||||
<div className="todosEmpty">
|
||||
<div className="emptyNewMessage">
|
||||
<MdPlaylistRemove />
|
||||
<span className="title">
|
||||
{variables.getMessage('widgets.navbar.todo.no_todos')}
|
||||
</span>
|
||||
<span className="subtitle">
|
||||
{variables.getMessage('modals.main.settings.sections.message.add_some')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<SortableContainer
|
||||
onSortEnd={this.onSortEnd}
|
||||
lockAxis="y"
|
||||
lockToContainerEdges
|
||||
disableAutoscroll
|
||||
useDragHandle
|
||||
>
|
||||
{this.state.todo.map((_value, index) => (
|
||||
<SortableItem
|
||||
key={`item-${index}`}
|
||||
index={index}
|
||||
value={
|
||||
<div
|
||||
className={'todoRow' + (this.state.todo[index].done ? ' done' : '')}
|
||||
key={index}
|
||||
>
|
||||
<Checkbox
|
||||
checked={this.state.todo[index].done}
|
||||
onClick={() => this.updateTodo('done', index)}
|
||||
/>
|
||||
<TextareaAutosize
|
||||
placeholder={variables.getMessage('widgets.navbar.notes.placeholder')}
|
||||
value={this.state.todo[index].value}
|
||||
onChange={(data) => this.updateTodo('set', index, data)}
|
||||
readOnly={this.state.todo[index].done}
|
||||
/>
|
||||
<MdDelete onClick={() => this.updateTodo('remove', index)} />
|
||||
<SortableHandle />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</SortableContainer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function TodoWrapper() {
|
||||
const [reference, setReference] = useState(null);
|
||||
|
||||
const { x, y, refs, strategy } = useFloating({
|
||||
placement: 'bottom',
|
||||
middleware: [shift()],
|
||||
elements: {
|
||||
reference,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Todo
|
||||
todoRef={setReference}
|
||||
floatRef={refs.setFloating}
|
||||
position={strategy}
|
||||
xPosition={x}
|
||||
yPosition={y}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(TodoWrapper);
|
||||
80
src/features/widgets/navbar/scss/_apps.scss
Normal file
80
src/features/widgets/navbar/scss/_apps.scss
Normal file
@@ -0,0 +1,80 @@
|
||||
@import 'scss/variables';
|
||||
|
||||
$appsWidth: 21rem;
|
||||
|
||||
.appsShortcutContainer {
|
||||
max-height: 35rem;
|
||||
overflow-y: auto;
|
||||
|
||||
// scrollbar-width: thin;
|
||||
border-radius: 0.8em;
|
||||
padding: 1.2em;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, auto);
|
||||
grid-auto-rows: 100px;
|
||||
gap: 10px;
|
||||
place-items: center;
|
||||
|
||||
@include themed {
|
||||
background: t($modal-secondaryColour);
|
||||
}
|
||||
|
||||
.noAppsContainer {
|
||||
grid-column: 1/3;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
svg {
|
||||
font-size: 30px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.appsIcon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
text-decoration: none;
|
||||
padding: 10px;
|
||||
border-radius: 0.8em;
|
||||
cursor: pointer;
|
||||
width: 5rem;
|
||||
height: 4.7rem;
|
||||
transition: 0.5s;
|
||||
|
||||
img {
|
||||
border-radius: 0.6rem;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
span {
|
||||
white-space: initial;
|
||||
}
|
||||
|
||||
height: max-content;
|
||||
}
|
||||
|
||||
@include themed {
|
||||
color: t($color);
|
||||
|
||||
&:hover {
|
||||
background: t($modal-sidebarActive);
|
||||
}
|
||||
}
|
||||
}
|
||||
125
src/features/widgets/navbar/scss/_notes.scss
Normal file
125
src/features/widgets/navbar/scss/_notes.scss
Normal file
@@ -0,0 +1,125 @@
|
||||
@import 'scss/variables';
|
||||
|
||||
.notes {
|
||||
position: relative;
|
||||
|
||||
h3 {
|
||||
text-shadow: none;
|
||||
margin: 0;
|
||||
cursor: initial;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&:hover .notesContainer {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
&:active .notesContainer {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
&:focus .notesContainer {
|
||||
visibility: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
.notesContainer {
|
||||
@extend %basic;
|
||||
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
border-radius: 12px;
|
||||
position: absolute;
|
||||
font-size: 1rem !important;
|
||||
|
||||
/* top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -130px; */
|
||||
|
||||
.notes-buttons {
|
||||
button {
|
||||
@include basicIconButton(11px, 1.3rem, ui);
|
||||
|
||||
@include themed {
|
||||
background: t($btn-background) !important;
|
||||
|
||||
&:hover {
|
||||
background: t($btn-backgroundHover) !important;
|
||||
}
|
||||
}
|
||||
|
||||
flex-grow: 1;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
svg {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flexNotes {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 15px;
|
||||
|
||||
textarea {
|
||||
max-height: 65vh !important;
|
||||
overflow-y: visible !important;
|
||||
|
||||
@extend %basic;
|
||||
|
||||
border: none;
|
||||
padding: 15px;
|
||||
border-radius: 12px;
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: #636e72;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
border: none;
|
||||
resize: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.topBarNotes {
|
||||
@extend %basic;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 50px;
|
||||
border-radius: 12px;
|
||||
flex-flow: row;
|
||||
gap: 5px;
|
||||
user-select: none;
|
||||
|
||||
svg {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.notes-buttons {
|
||||
display: flex !important;
|
||||
gap: 10px;
|
||||
|
||||
button {
|
||||
&:disabled {
|
||||
@include themed {
|
||||
background: t($modal-sidebar) !important;
|
||||
}
|
||||
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
flex: 1 !important;
|
||||
}
|
||||
}
|
||||
69
src/features/widgets/navbar/scss/_todo.scss
Normal file
69
src/features/widgets/navbar/scss/_todo.scss
Normal file
@@ -0,0 +1,69 @@
|
||||
.notesContainer {
|
||||
.flexTodo {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 15px;
|
||||
width: 223px;
|
||||
|
||||
.todoRows {
|
||||
max-height: 65vh !important;
|
||||
overflow: hidden visible !important;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.todosEmpty {
|
||||
height: 200px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.todoRow {
|
||||
@include basicIconButton(1px, 16px, ui);
|
||||
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
margin: 15px 5px;
|
||||
|
||||
@include themed {
|
||||
color: t($color) !important;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 120px;
|
||||
|
||||
@include themed {
|
||||
color: t($color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
padding: 10px;
|
||||
place-items: center;
|
||||
display: grid;
|
||||
cursor: pointer;
|
||||
|
||||
@include themed {
|
||||
color: t($color) !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include themed {
|
||||
background: t($modal-sidebar);
|
||||
border-radius: t($borderRadius);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.done {
|
||||
text-decoration: line-through;
|
||||
|
||||
textarea {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
96
src/features/widgets/navbar/scss/index.scss
Normal file
96
src/features/widgets/navbar/scss/index.scss
Normal file
@@ -0,0 +1,96 @@
|
||||
@import 'notes';
|
||||
@import 'todo';
|
||||
@import 'apps';
|
||||
@import 'scss/variables';
|
||||
|
||||
.navbar {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: 10px;
|
||||
animation: fadeIn 2s;
|
||||
|
||||
button {
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
|
||||
/* filter: drop-shadow(0 0 6px rgba(0, 0, 0, 0.3)); */
|
||||
cursor: pointer;
|
||||
|
||||
// transition: 0.2s ease;
|
||||
margin-top: 0;
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* &:hover {
|
||||
color: map-get($theme-colours, 'main-text-color');
|
||||
transform: translateZ(0);
|
||||
transform: scale(1.3);
|
||||
} */
|
||||
}
|
||||
}
|
||||
|
||||
.old {
|
||||
.tooltip {
|
||||
button {
|
||||
@include basicIconButton(12px, 1.2rem, legacy);
|
||||
}
|
||||
}
|
||||
|
||||
.first {
|
||||
@include basicIconButton(12px, 1.2rem, legacy);
|
||||
}
|
||||
}
|
||||
|
||||
.new {
|
||||
button {
|
||||
@include basicIconButton(12px, 1.2rem, ui);
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-hover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 50px;
|
||||
width: 500px;
|
||||
|
||||
.navbar {
|
||||
opacity: 0;
|
||||
transition:
|
||||
visibility 0.2s linear,
|
||||
opacity 0.2s linear;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.navbar {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-container {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
.appsmodal {
|
||||
width: fit-content;
|
||||
padding: 5px;
|
||||
border-radius: 1em;
|
||||
|
||||
@include themed {
|
||||
background: t($modal-sidebarActive);
|
||||
}
|
||||
}
|
||||
121
src/features/widgets/quicklinks/QuickLinks.jsx
Normal file
121
src/features/widgets/quicklinks/QuickLinks.jsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { PureComponent, createRef } from 'react';
|
||||
import { Tooltip } from 'components/Elements';
|
||||
|
||||
import EventBus from 'modules/helpers/eventbus';
|
||||
|
||||
import './quicklinks.scss';
|
||||
|
||||
export default class QuickLinks extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
items: JSON.parse(localStorage.getItem('quicklinks')),
|
||||
};
|
||||
this.quicklinksContainer = createRef();
|
||||
}
|
||||
|
||||
// widget zoom
|
||||
setZoom(element) {
|
||||
const zoom = localStorage.getItem('zoomQuicklinks') || 100;
|
||||
for (const link of element.getElementsByTagName('span')) {
|
||||
link.style.fontSize = `${14 * Number(zoom / 100)}px`;
|
||||
}
|
||||
|
||||
if (localStorage.getItem('quickLinksStyle') !== 'text') {
|
||||
for (const img of element.getElementsByTagName('img')) {
|
||||
img.style.height = `${30 * Number(zoom / 100)}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
if (data === 'quicklinks') {
|
||||
if (localStorage.getItem('quicklinksenabled') === 'false') {
|
||||
return (this.quicklinksContainer.current.style.display = 'none');
|
||||
}
|
||||
|
||||
this.quicklinksContainer.current.style.display = 'block';
|
||||
this.setZoom(this.quicklinksContainer.current);
|
||||
|
||||
this.setState({
|
||||
items: JSON.parse(localStorage.getItem('quicklinks')),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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) => {
|
||||
if (localStorage.getItem('quickLinksStyle') === 'text') {
|
||||
return (
|
||||
<a
|
||||
className="quicklinkstext"
|
||||
key={item.key}
|
||||
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={item.key}
|
||||
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={item.key} 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={item.key}>
|
||||
{link}
|
||||
</Tooltip>
|
||||
) : (
|
||||
link
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="quicklinkscontainer" ref={this.quicklinksContainer}>
|
||||
{this.state.items.map((item) => quickLink(item))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
258
src/features/widgets/quicklinks/quicklinks.scss
Normal file
258
src/features/widgets/quicklinks/quicklinks.scss
Normal file
@@ -0,0 +1,258 @@
|
||||
@import 'scss/variables';
|
||||
|
||||
.quicklinks {
|
||||
@include basicIconButton(10px, 14px, ui);
|
||||
|
||||
outline: none;
|
||||
border: none;
|
||||
box-shadow: 0 0 0 1px #484848;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
font-size: 0.5em;
|
||||
padding: 10px 40px;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.quicklinkscontainer {
|
||||
border-radius: 12px;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
place-content: center center;
|
||||
gap: 12px;
|
||||
|
||||
textarea {
|
||||
@extend %basic;
|
||||
|
||||
border: none;
|
||||
padding: 15px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: #636e72;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.topbarquicklinks {
|
||||
svg {
|
||||
font-size: 46px;
|
||||
padding: 9px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
cursor: initial;
|
||||
user-select: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
cursor: initial;
|
||||
user-select: none;
|
||||
color: rgb(255 71 87 / 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.quicklinks-container > a,
|
||||
.quicklinks-container > .quicklinks > button {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.quicklinks-container {
|
||||
img {
|
||||
height: 32px;
|
||||
width: auto;
|
||||
transition: transform 0.2s;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.quicklinkstext {
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
font-size: 0.8em;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
/* background-color: var(--background);
|
||||
color: var(--modal-text); */
|
||||
|
||||
.quicklinksdropdown {
|
||||
@extend %basic;
|
||||
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
display: none;
|
||||
flex-flow: column;
|
||||
padding: 15px;
|
||||
box-shadow: 0 0 0 1px #484848;
|
||||
gap: 5px;
|
||||
|
||||
&:hover {
|
||||
.quicklinksdropdown {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-title {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.dropdown-subtitle {
|
||||
font-size: 0.4em;
|
||||
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
@include basicIconButton(10px, 0.9rem, ui);
|
||||
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-error {
|
||||
font-size: 0.4em;
|
||||
}
|
||||
|
||||
button.quicklinks {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.outOfScreen {
|
||||
bottom: 515px;
|
||||
}
|
||||
|
||||
.addLinkModal {
|
||||
@extend %tabText;
|
||||
|
||||
padding: 15px;
|
||||
|
||||
@include themed {
|
||||
background: t($modal-secondaryColour);
|
||||
}
|
||||
|
||||
button {
|
||||
@include modal-button(standard);
|
||||
|
||||
padding: 10px 30px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.addFooter {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dropdown-error {
|
||||
font-size: 16px;
|
||||
padding-left: 5px;
|
||||
color: #e74c3c;
|
||||
}
|
||||
}
|
||||
|
||||
.quicklinkModalTextbox {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr; /* Two rows: first auto-sized, second filling remaining space */
|
||||
grid-template-columns: repeat(2, 1fr); /* Two equal-width columns for the first row */
|
||||
grid-gap: 10px; /* Optional gap between items */
|
||||
padding: 15px 0;
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dropdown-error {
|
||||
font-size: 30px;
|
||||
padding-left: 5px;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
@include themed {
|
||||
textarea {
|
||||
background: t($modal-sidebar);
|
||||
border: 1px solid t($modal-sidebarActive);
|
||||
color: t($color);
|
||||
border-radius: t($borderRadius);
|
||||
padding: 10px 20px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px t($color);
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 0 0 1px t($color);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1px t($color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quickLinksMetro {
|
||||
@extend %basic;
|
||||
|
||||
text-decoration: none;
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
min-width: 100px;
|
||||
background-image: linear-gradient(to left, rgb(0 0 0), transparent, rgb(0 0 0)),
|
||||
url('https://media.cntraveller.com/photos/615ee85…/16:9/w_2580,c_limit/Best%20Cities%20in%20the%20World%20-%20Grid.jpg');
|
||||
transition: 0.8s;
|
||||
text-align: left;
|
||||
padding: 20px 40px;
|
||||
|
||||
@include themed {
|
||||
&:hover {
|
||||
background: t($btn-backgroundHover);
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: auto;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.quicklinknostyle {
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
}
|
||||
516
src/features/widgets/quote/Quote.jsx
Normal file
516
src/features/widgets/quote/Quote.jsx
Normal file
@@ -0,0 +1,516 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent, createRef } from 'react';
|
||||
import {
|
||||
MdContentCopy,
|
||||
MdStarBorder,
|
||||
MdStar,
|
||||
MdPerson,
|
||||
MdOpenInNew,
|
||||
MdIosShare,
|
||||
} from 'react-icons/md';
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { Tooltip } from 'components/Elements';
|
||||
|
||||
import Modal from 'react-modal';
|
||||
import { ShareModal } from 'components/Elements';
|
||||
|
||||
import offline_quotes from './offline_quotes.json';
|
||||
|
||||
import EventBus from 'modules/helpers/eventbus';
|
||||
|
||||
import './quote.scss';
|
||||
|
||||
export default class Quote extends PureComponent {
|
||||
buttons = {
|
||||
share: (
|
||||
<Tooltip title={variables.getMessage('widgets.quote.share')}>
|
||||
<button
|
||||
onClick={() => this.setState({ shareModal: true })}
|
||||
aria-label={variables.getMessage('widgets.quote.share')}
|
||||
>
|
||||
<MdIosShare className="copyButton" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
),
|
||||
copy: (
|
||||
<Tooltip title={variables.getMessage('widgets.quote.copy')}>
|
||||
<button
|
||||
onClick={() => this.copyQuote()}
|
||||
aria-label={variables.getMessage('widgets.quote.copy')}
|
||||
>
|
||||
<MdContentCopy className="copyButton" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
),
|
||||
unfavourited: (
|
||||
<Tooltip title={variables.getMessage('widgets.quote.favourite')}>
|
||||
<button
|
||||
onClick={() => this.favourite()}
|
||||
aria-label={variables.getMessage('widgets.quote.favourite')}
|
||||
>
|
||||
<MdStarBorder className="copyButton" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
),
|
||||
favourited: (
|
||||
<Tooltip title={variables.getMessage('widgets.quote.unfavourite')}>
|
||||
<button
|
||||
onClick={() => this.favourite()}
|
||||
aria-label={variables.getMessage('widgets.quote.unfavourite')}
|
||||
>
|
||||
<MdStar className="copyButton" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
),
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
quote: '',
|
||||
author: '',
|
||||
authorOccupation: '',
|
||||
favourited: this.useFavourite(),
|
||||
share: localStorage.getItem('quoteShareButton') === 'false' ? null : this.buttons.share,
|
||||
copy: localStorage.getItem('copyButton') === 'false' ? null : this.buttons.copy,
|
||||
quoteLanguage: '',
|
||||
type: localStorage.getItem('quoteType') || 'api',
|
||||
shareModal: false,
|
||||
};
|
||||
this.quote = createRef();
|
||||
this.quotediv = createRef();
|
||||
this.quoteauthor = createRef();
|
||||
}
|
||||
|
||||
useFavourite() {
|
||||
if (localStorage.getItem('favouriteQuoteEnabled') === 'true') {
|
||||
return localStorage.getItem('favouriteQuote')
|
||||
? this.buttons.favourited
|
||||
: this.buttons.unfavourited;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
doOffline() {
|
||||
// Get a random quote from our local JSON
|
||||
const quote = offline_quotes[Math.floor(Math.random() * offline_quotes.length)];
|
||||
|
||||
this.setState({
|
||||
quote: '"' + quote.quote + '"',
|
||||
author: quote.author,
|
||||
authorlink: this.getAuthorLink(quote.author),
|
||||
authorimg: '',
|
||||
});
|
||||
}
|
||||
|
||||
getAuthorLink(author) {
|
||||
return localStorage.getItem('authorLink') === 'false' || author === 'Unknown'
|
||||
? null
|
||||
: `https://${variables.languagecode.split('_')[0]}.wikipedia.org/wiki/${author
|
||||
.split(' ')
|
||||
.join('_')}`;
|
||||
}
|
||||
|
||||
stripHTML(html) {
|
||||
const tmpdoc = new DOMParser().parseFromString(html, 'text/html');
|
||||
return tmpdoc.body.textContent || '';
|
||||
}
|
||||
|
||||
async getAuthorImg(author) {
|
||||
if (localStorage.getItem('authorImg') === 'false') {
|
||||
return {
|
||||
authorimg: null,
|
||||
authorimglicense: null,
|
||||
};
|
||||
}
|
||||
|
||||
const authorimgdata = await (
|
||||
await fetch(
|
||||
`https://${
|
||||
variables.languagecode.split('_')[0]
|
||||
}.wikipedia.org/w/api.php?action=query&titles=${author}&origin=*&prop=pageimages&format=json&pithumbsize=100`,
|
||||
)
|
||||
).json();
|
||||
|
||||
let authorimg, authorimglicense;
|
||||
try {
|
||||
authorimg =
|
||||
authorimgdata.query.pages[Object.keys(authorimgdata.query.pages)[0]].thumbnail.source;
|
||||
|
||||
const authorimglicensedata = await (
|
||||
await fetch(
|
||||
`https://${
|
||||
variables.languagecode.split('_')[0]
|
||||
}.wikipedia.org/w/api.php?action=query&prop=imageinfo&iiprop=extmetadata&titles=File:${
|
||||
authorimgdata.query.pages[Object.keys(authorimgdata.query.pages)[0]].pageimage
|
||||
}&origin=*&format=json`,
|
||||
)
|
||||
).json();
|
||||
|
||||
const metadata =
|
||||
authorimglicensedata.query.pages[Object.keys(authorimglicensedata.query.pages)[0]]
|
||||
.imageinfo[0].extmetadata;
|
||||
const license = metadata.LicenseShortName;
|
||||
const photographer =
|
||||
metadata.Attribution.value ||
|
||||
this.stripHTML(metadata.Artist?.value || '').replace(/ \(talk\)/, '') || // talk page link (if applicable) is only removed for English
|
||||
'Unknown';
|
||||
authorimglicense = `© ${photographer}. ${license.value}`;
|
||||
authorimglicense = authorimglicense.replace(/copyright\s/i, '').replace(/©\s©\s/, '© ');
|
||||
|
||||
if (license.value === 'Public domain') {
|
||||
authorimglicense = null;
|
||||
} else if (photographer.value === 'Unknown' || !photographer) {
|
||||
authorimglicense = null;
|
||||
authorimg = null;
|
||||
}
|
||||
} catch (e) {
|
||||
authorimg = null;
|
||||
authorimglicense = null;
|
||||
}
|
||||
|
||||
if (author === 'Unknown') {
|
||||
authorimg = null;
|
||||
authorimglicense = null;
|
||||
}
|
||||
|
||||
return {
|
||||
authorimg,
|
||||
authorimglicense,
|
||||
};
|
||||
}
|
||||
|
||||
async getQuote() {
|
||||
const offline = localStorage.getItem('offlineMode') === 'true';
|
||||
|
||||
const favouriteQuote = localStorage.getItem('favouriteQuote');
|
||||
if (favouriteQuote) {
|
||||
let author = favouriteQuote.split(' - ')[1];
|
||||
const authorimgdata = await this.getAuthorImg(author);
|
||||
return this.setState({
|
||||
quote: favouriteQuote.split(' - ')[0],
|
||||
author,
|
||||
authorlink: this.getAuthorLink(author),
|
||||
authorimg: authorimgdata.authorimg,
|
||||
authorimglicense: authorimgdata.authorimglicense,
|
||||
});
|
||||
}
|
||||
|
||||
switch (this.state.type) {
|
||||
case 'custom':
|
||||
let customQuote;
|
||||
try {
|
||||
customQuote = JSON.parse(localStorage.getItem('customQuote'));
|
||||
} catch (e) {
|
||||
// move to new format
|
||||
customQuote = [
|
||||
{
|
||||
quote: localStorage.getItem('customQuote'),
|
||||
author: localStorage.getItem('customQuoteAuthor'),
|
||||
},
|
||||
];
|
||||
localStorage.setItem('customQuote', JSON.stringify(customQuote));
|
||||
}
|
||||
|
||||
// pick random
|
||||
customQuote = customQuote
|
||||
? customQuote[Math.floor(Math.random() * customQuote.length)]
|
||||
: null;
|
||||
|
||||
if (customQuote !== undefined) {
|
||||
return this.setState({
|
||||
quote: '"' + customQuote.quote + '"',
|
||||
author: customQuote.author,
|
||||
authorlink: this.getAuthorLink(customQuote.author),
|
||||
authorimg: await this.getAuthorImg(customQuote.author),
|
||||
noQuote: false,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
noQuote: true,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'quote_pack':
|
||||
if (offline) {
|
||||
return this.doOffline();
|
||||
}
|
||||
|
||||
const quotePack = [];
|
||||
const installed = JSON.parse(localStorage.getItem('installed'));
|
||||
installed.forEach((item) => {
|
||||
if (item.type === 'quotes') {
|
||||
const quotes = item.quotes.map((quote) => ({
|
||||
...quote,
|
||||
fallbackauthorimg: item.icon_url,
|
||||
}));
|
||||
quotePack.push(...quotes);
|
||||
}
|
||||
});
|
||||
|
||||
if (quotePack) {
|
||||
const data = quotePack[Math.floor(Math.random() * quotePack.length)];
|
||||
return this.setState({
|
||||
quote: '"' + data.quote + '"',
|
||||
author: data.author,
|
||||
authorlink: this.getAuthorLink(data.author),
|
||||
authorimg: data.fallbackauthorimg,
|
||||
});
|
||||
} else {
|
||||
return this.doOffline();
|
||||
}
|
||||
case 'api':
|
||||
if (offline) {
|
||||
return this.doOffline();
|
||||
}
|
||||
|
||||
const getAPIQuoteData = async () => {
|
||||
const quoteLanguage = localStorage.getItem('quoteLanguage');
|
||||
const data = await (
|
||||
await fetch(variables.constants.API_URL + '/quotes/random?language=' + quoteLanguage)
|
||||
).json();
|
||||
// If we hit the ratelimit, we fall back to local quotes
|
||||
if (data.statusCode === 429) {
|
||||
return null;
|
||||
}
|
||||
const authorimgdata = await this.getAuthorImg(data.author);
|
||||
return {
|
||||
quote: '"' + data.quote.replace(/\s+$/g, '') + '"',
|
||||
author: data.author,
|
||||
authorlink: this.getAuthorLink(data.author),
|
||||
authorimg: authorimgdata.authorimg,
|
||||
authorimglicense: authorimgdata.authorimglicense,
|
||||
quoteLanguage: quoteLanguage,
|
||||
authorOccupation: data.author_occupation,
|
||||
};
|
||||
};
|
||||
|
||||
// First we try and get a quote from the API...
|
||||
try {
|
||||
let data = JSON.parse(localStorage.getItem('nextQuote')) || (await getAPIQuoteData());
|
||||
localStorage.setItem('nextQuote', null);
|
||||
if (data) {
|
||||
this.setState(data);
|
||||
localStorage.setItem('currentQuote', JSON.stringify(data));
|
||||
localStorage.setItem('nextQuote', JSON.stringify(await getAPIQuoteData())); // pre-fetch data about the next quote
|
||||
} else {
|
||||
this.doOffline();
|
||||
}
|
||||
} catch (e) {
|
||||
// ...and if that fails we load one locally
|
||||
this.doOffline();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
copyQuote() {
|
||||
variables.stats.postEvent('feature', 'Quote copied');
|
||||
navigator.clipboard.writeText(`${this.state.quote} - ${this.state.author}`);
|
||||
toast(variables.getMessage('toasts.quote'));
|
||||
}
|
||||
|
||||
favourite() {
|
||||
if (localStorage.getItem('favouriteQuote')) {
|
||||
localStorage.removeItem('favouriteQuote');
|
||||
this.setState({
|
||||
favourited: this.buttons.unfavourited,
|
||||
});
|
||||
} else {
|
||||
localStorage.setItem('favouriteQuote', this.state.quote + ' - ' + this.state.author);
|
||||
this.setState({
|
||||
favourited: this.buttons.favourited,
|
||||
});
|
||||
}
|
||||
|
||||
variables.stats.postEvent('feature', 'Quote favourite');
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setZoom();
|
||||
|
||||
const quoteType = localStorage.getItem('quoteType');
|
||||
|
||||
if (
|
||||
this.state.type !== quoteType ||
|
||||
localStorage.getItem('quoteLanguage') !== this.state.quoteLanguage ||
|
||||
(quoteType === 'custom' && this.state.quote !== localStorage.getItem('customQuote')) ||
|
||||
(quoteType === 'custom' && this.state.author !== localStorage.getItem('customQuoteAuthor'))
|
||||
) {
|
||||
this.getQuote();
|
||||
}
|
||||
}
|
||||
|
||||
setZoom() {
|
||||
const zoomQuote = Number((localStorage.getItem('zoomQuote') || 100) / 100);
|
||||
this.quote.current.style.fontSize = `${0.8 * zoomQuote}em`;
|
||||
this.quoteauthor.current.style.fontSize = `${0.9 * zoomQuote}em`;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// const test = localStorage.getItem('quotechange');
|
||||
|
||||
// this.interval = setInterval(() => {
|
||||
// if (test !== null) {
|
||||
// const targetTime = Number(
|
||||
// Number(localStorage.getItem('quoteStartTime')) +
|
||||
// Number(localStorage.getItem('quotechange')),
|
||||
// );
|
||||
// const currentTime = Number(Date.now());
|
||||
// if (currentTime >= targetTime) {
|
||||
// this.setZoom();
|
||||
// this.getQuote();
|
||||
// localStorage.setItem('quoteStartTime', Date.now());
|
||||
// } else {
|
||||
// try {
|
||||
// this.setState(JSON.parse(localStorage.getItem('currentQuote')));
|
||||
// } catch (e) {
|
||||
// this.setZoom();
|
||||
// this.getQuote();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
this.setZoom();
|
||||
|
||||
EventBus.on('refresh', (data) => {
|
||||
if (data === 'quote') {
|
||||
if (localStorage.getItem('quote') === 'false') {
|
||||
return (this.quotediv.current.style.display = 'none');
|
||||
}
|
||||
|
||||
this.quotediv.current.style.display = 'block';
|
||||
this.init();
|
||||
|
||||
// buttons hot reload
|
||||
this.setState({
|
||||
favourited: this.useFavourite(),
|
||||
share: localStorage.getItem('quoteShareButton') === 'false' ? null : this.buttons.share,
|
||||
copy: localStorage.getItem('copyButton') === 'false' ? null : this.buttons.copy,
|
||||
});
|
||||
}
|
||||
|
||||
// uninstall quote pack reverts the quote to what you had previously
|
||||
if (data === 'marketplacequoteuninstall') {
|
||||
this.init();
|
||||
}
|
||||
|
||||
if (data === 'quoterefresh') {
|
||||
this.getQuote();
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
localStorage.getItem('quotechange') === 'refresh' ||
|
||||
localStorage.getItem('quotechange') === null
|
||||
) {
|
||||
this.setZoom();
|
||||
this.getQuote();
|
||||
localStorage.setItem('quoteStartTime', Date.now());
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.noQuote === true) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="quotediv" ref={this.quotediv}>
|
||||
<Modal
|
||||
closeTimeoutMS={300}
|
||||
isOpen={this.state.shareModal}
|
||||
className="Modal mainModal"
|
||||
overlayClassName="Overlay"
|
||||
ariaHideApp={false}
|
||||
onRequestClose={() => this.setState({ shareModal: false })}
|
||||
>
|
||||
<ShareModal
|
||||
data={`${this.state.quote} - ${this.state.author}`}
|
||||
modalClose={() => this.setState({ shareModal: false })}
|
||||
/>
|
||||
</Modal>
|
||||
<span className="quote" ref={this.quote}>
|
||||
{this.state.quote}
|
||||
</span>
|
||||
|
||||
{localStorage.getItem('widgetStyle') === 'legacy' ? (
|
||||
<>
|
||||
<div>
|
||||
<h1 className="quoteauthor" ref={this.quoteauthor}>
|
||||
<a
|
||||
href={this.state.authorlink}
|
||||
className="quoteAuthorLink"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Learn about the author of the quote."
|
||||
>
|
||||
{this.state.author}
|
||||
</a>
|
||||
</h1>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: '20px' }}>
|
||||
{this.state.copy} {this.state.share} {this.state.favourited}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="author-holder">
|
||||
<div className="author">
|
||||
<div
|
||||
className="author-img"
|
||||
style={{ backgroundImage: `url(${this.state.authorimg})` }}
|
||||
>
|
||||
{this.state.authorimg === undefined || this.state.authorimg ? '' : <MdPerson />}
|
||||
</div>
|
||||
{this.state.author !== '' ? (
|
||||
<div className="author-content" ref={this.quoteauthor}>
|
||||
<span className="title">{this.state.author}</span>
|
||||
{this.state.authorOccupation !== 'Unknown' && (
|
||||
<span className="subtitle">{this.state.authorOccupation}</span>
|
||||
)}
|
||||
<span className="author-license">
|
||||
{this.state.authorimglicense &&
|
||||
this.state.authorimglicense.replace(' undefined. ', ' ')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="author-content whileLoading" ref={this.quoteauthor}>
|
||||
{/* these are placeholders for skeleton and as such don't need translating */}
|
||||
<span className="title">loading</span>
|
||||
<span className="subtitle">loading</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="quote-buttons">
|
||||
{this.state.authorOccupation !== 'Unknown' && this.state.authorlink !== '' ? (
|
||||
<Tooltip title={variables.getMessage('widgets.quote.link_tooltip')}>
|
||||
<a
|
||||
href={this.state.authorlink}
|
||||
className="quoteAuthorLink"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Learn about the author of the quote."
|
||||
>
|
||||
<MdOpenInNew />
|
||||
</a>{' '}
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{this.state.copy} {this.state.share} {this.state.favourited}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
102
src/features/widgets/quote/offline_quotes.json
Normal file
102
src/features/widgets/quote/offline_quotes.json
Normal file
@@ -0,0 +1,102 @@
|
||||
[
|
||||
{
|
||||
"author": "Robert De Niro",
|
||||
"quote": "Time goes on. So whatever you’re going to do, do it. Do it now. Don’t wait."
|
||||
},
|
||||
{
|
||||
"author": "Walt Disney",
|
||||
"quote": "All our dreams can come true, if we have the courage to pursue them."
|
||||
},
|
||||
{
|
||||
"author": "Confucius",
|
||||
"quote": "It does not matter how slowly you go as long as you do not stop."
|
||||
},
|
||||
{
|
||||
"author": "Roy T. Bennett",
|
||||
"quote": "Believe in yourself. You are braver than you think, more talented than you know, and capable of more than you imagine."
|
||||
},
|
||||
{
|
||||
"author": "Wayne Dyer",
|
||||
"quote": "If you believe it will work out, you’ll see opportunities. If you believe it won’t, you will see obstacles."
|
||||
},
|
||||
{
|
||||
"author": "George Addair",
|
||||
"quote": "Everything you’ve ever wanted is on the other side of fear."
|
||||
},
|
||||
{
|
||||
"author": "Winston Churchill",
|
||||
"quote": "Success is not final, failure is not fatal: it is the courage to continue that counts."
|
||||
},
|
||||
{
|
||||
"author": "Paulo Coelho",
|
||||
"quote": "There is only one thing that makes a dream impossible to achieve: the fear of failure"
|
||||
},
|
||||
{
|
||||
"author": "Brian Tracy",
|
||||
"quote": "Your true success in life begins only when you make the commitment to become excellent at what you do."
|
||||
},
|
||||
{
|
||||
"author": "Chantal Sutherland",
|
||||
"quote": "Believe in yourself, take on your challenges, dig deep within yourself to conquer fears. Never let anyone bring you down. You got to keep going."
|
||||
},
|
||||
{
|
||||
"author": "Les Brown",
|
||||
"quote": "Too many of us are not living our dreams because we are living our fears."
|
||||
},
|
||||
{
|
||||
"author": "Bob Riley",
|
||||
"quote": "Hard times don’t create heroes. It is during the hard times when the ‘hero’ within us is revealed."
|
||||
},
|
||||
{
|
||||
"author": "Jack Canfield",
|
||||
"quote": "If you can tune into your purpose and really align with it, setting goals so that your vision is an expression of that purpose, then life flows much more easily."
|
||||
},
|
||||
{
|
||||
"author": "Napolean Hill",
|
||||
"quote": "Whatever the mind can conceive and believe, it can achieve."
|
||||
},
|
||||
{
|
||||
"author": "Jim Rohn",
|
||||
"quote": "Don’t wish it were easier. Wish you were better."
|
||||
},
|
||||
{
|
||||
"author": "Serena Williams",
|
||||
"quote": "A champion is defined not by their wins but by how they can recover when they fall."
|
||||
},
|
||||
{
|
||||
"author": "Sheryl Sandberg",
|
||||
"quote": "Motivation comes from working on things we care about."
|
||||
},
|
||||
{
|
||||
"author": "Reese Witherspoon",
|
||||
"quote": "With the right kind of coaching and determination you can accomplish anything."
|
||||
},
|
||||
{
|
||||
"author": "Hazrat Inayat Khan",
|
||||
"quote": "Some people look for a beautiful place. Others make a place beautiful."
|
||||
},
|
||||
{
|
||||
"author": "Albert Einstein",
|
||||
"quote": "Life is like riding a bicycle. To keep your balance, you must keep moving."
|
||||
},
|
||||
{
|
||||
"author": "Walt Disney",
|
||||
"quote": "The way to get started is to quit talking and begin doing."
|
||||
},
|
||||
{
|
||||
"author": "Winston Churchill",
|
||||
"quote": "A pessimist sees the difficulty in every opportunity; an optimist sees the opportunity in every difficulty."
|
||||
},
|
||||
{
|
||||
"author": "Will Rogers",
|
||||
"quote": "Don't let yesterday take up too much of today."
|
||||
},
|
||||
{
|
||||
"author": "Vince Lombardi",
|
||||
"quote": "It's not whether you get knocked down, it's whether you get up."
|
||||
},
|
||||
{
|
||||
"author": "Steve Jobs",
|
||||
"quote": "If you are working on something that you really care about, you don’t have to be pushed. The vision pulls you."
|
||||
}
|
||||
]
|
||||
168
src/features/widgets/quote/quote.scss
Normal file
168
src/features/widgets/quote/quote.scss
Normal file
@@ -0,0 +1,168 @@
|
||||
@import 'scss/variables';
|
||||
|
||||
.quote {
|
||||
font-size: 0.8em;
|
||||
text-shadow: 0 0 10px rgb(0 0 0 / 30%);
|
||||
cursor: initial;
|
||||
user-select: none;
|
||||
|
||||
--shadow-shift: 0.125rem;
|
||||
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
width: 40vw;
|
||||
}
|
||||
|
||||
.quoteauthor {
|
||||
font-size: 0.9em;
|
||||
letter-spacing: 0.5px;
|
||||
user-select: none;
|
||||
|
||||
--shadow-shift: 0.125rem;
|
||||
|
||||
svg {
|
||||
margin-left: 10px;
|
||||
filter: drop-shadow(0 0 6px rgb(0 0 0 / 30%));
|
||||
}
|
||||
}
|
||||
|
||||
i.material-icons,
|
||||
h1.quoteauthor {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.quoteAuthorLink {
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.author {
|
||||
@extend %basic;
|
||||
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
height: 70px;
|
||||
font-weight: 300;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
font-size: clamp(15px, 2.5vw, 0.6em);
|
||||
}
|
||||
|
||||
.author-knownfor {
|
||||
font-size: clamp(13px, 2.5vw, 0.4em);
|
||||
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
}
|
||||
|
||||
.author-license {
|
||||
font-size: clamp(8px, 2.5vw, 0.1em);
|
||||
|
||||
@include themed {
|
||||
color: t($subColor);
|
||||
}
|
||||
}
|
||||
|
||||
.author img {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
border-radius: 12px 0 0 12px;
|
||||
}
|
||||
|
||||
.author-img {
|
||||
@extend %basic;
|
||||
|
||||
height: 100%;
|
||||
width: 70px;
|
||||
border-radius: 12px 0 0 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-size: cover !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position: initial;
|
||||
}
|
||||
|
||||
.author-content {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.author-holder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-flow: column;
|
||||
animation: fadeIn 1s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.quote-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 20px 20px 20px 0;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
a {
|
||||
@include basicIconButton(11px, 1.3rem, ui);
|
||||
}
|
||||
}
|
||||
|
||||
.quotediv {
|
||||
animation: fadeIn 1s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
@include basicIconButton(11px, 1.3rem, ui);
|
||||
}
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
height: auto !important;
|
||||
|
||||
@include basicIconButton(11px, 1.3rem, modal);
|
||||
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.author-content.whileLoading {
|
||||
@include themed {
|
||||
gap: 5px;
|
||||
|
||||
.title {
|
||||
color: transparent;
|
||||
width: 100px;
|
||||
background: t($modal-sidebar);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: transparent;
|
||||
width: 50px;
|
||||
background: t($modal-sidebarActive);
|
||||
}
|
||||
}
|
||||
}
|
||||
282
src/features/widgets/search/Search.jsx
Normal file
282
src/features/widgets/search/Search.jsx
Normal file
@@ -0,0 +1,282 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent, createRef } from 'react';
|
||||
import { MdSearch, MdMic, MdScreenSearchDesktop } from 'react-icons/md';
|
||||
import { BsGoogle } from 'react-icons/bs';
|
||||
import { SiDuckduckgo, SiMicrosoftbing, SiYahoo, SiBaidu } from 'react-icons/si';
|
||||
import { FaYandex } from 'react-icons/fa';
|
||||
import { Tooltip } from 'components/Elements';
|
||||
import AutocompleteInput from 'features/helpers/autocomplete/Autocomplete';
|
||||
|
||||
import EventBus from 'modules/helpers/eventbus';
|
||||
|
||||
import './search.scss';
|
||||
|
||||
import searchEngines from 'features/widgets/search/search_engines.json';
|
||||
|
||||
export default class Search extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
url: '',
|
||||
query: '',
|
||||
microphone: null,
|
||||
suggestions: [],
|
||||
searchDropdown: false,
|
||||
classList:
|
||||
localStorage.getItem('widgetStyle') === 'legacy' ? 'searchIcons old' : 'searchIcons',
|
||||
};
|
||||
this.micIcon = createRef();
|
||||
}
|
||||
|
||||
startSpeechRecognition = () => {
|
||||
const voiceSearch = new window.webkitSpeechRecognition();
|
||||
voiceSearch.start();
|
||||
|
||||
this.micIcon.current.classList.add('micActive');
|
||||
|
||||
const searchText = document.getElementById('searchtext');
|
||||
|
||||
voiceSearch.onresult = (event) => {
|
||||
searchText.value = event.results[0][0].transcript;
|
||||
};
|
||||
|
||||
voiceSearch.onend = () => {
|
||||
this.micIcon.current.classList.remove('micActive');
|
||||
if (searchText.value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
variables.stats.postEvent('feature', 'Voice search');
|
||||
window.location.href = this.state.url + `?${this.state.query}=` + searchText.value;
|
||||
}, 1000);
|
||||
};
|
||||
};
|
||||
|
||||
searchButton = (e) => {
|
||||
e.preventDefault();
|
||||
const value = e.target.value || document.getElementById('searchtext').value || 'mue fast';
|
||||
variables.stats.postEvent('feature', 'Search');
|
||||
window.location.href = this.state.url + `?${this.state.query}=` + value;
|
||||
};
|
||||
|
||||
async getSuggestions(input) {
|
||||
window.setResults = (results) => {
|
||||
window.searchResults = results;
|
||||
};
|
||||
|
||||
const results = await (await fetch(`https://ac.ecosia.org/?q=${input}`)).json();
|
||||
|
||||
try {
|
||||
this.setState({
|
||||
suggestions: results.suggestions.splice(0, 3),
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore error if empty
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
let url, microphone;
|
||||
let query = 'q';
|
||||
|
||||
const setting = localStorage.getItem('searchEngine');
|
||||
const info = searchEngines.find((i) => i.settingsName === setting);
|
||||
|
||||
if (info !== undefined) {
|
||||
url = info.url;
|
||||
if (info.query) {
|
||||
query = info.query;
|
||||
}
|
||||
}
|
||||
|
||||
if (setting === 'custom') {
|
||||
const custom = localStorage.getItem('customSearchEngine');
|
||||
if (custom !== null) {
|
||||
url = custom;
|
||||
}
|
||||
}
|
||||
|
||||
if (localStorage.getItem('voiceSearch') === 'true') {
|
||||
microphone = (
|
||||
<button
|
||||
onClick={this.startSpeechRecognition}
|
||||
ref={this.micIcon}
|
||||
aria-label="Microphone Search"
|
||||
>
|
||||
<MdMic className="micIcon" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
url,
|
||||
query,
|
||||
microphone,
|
||||
currentSearch: info ? info.name : 'Custom',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user selects a search engine from the dropdown menu, the function will set the state of the
|
||||
* search engine to the selected search engine.
|
||||
* @param {string} name - The name of the search engine
|
||||
* @param {boolean} custom - If the search engine is custom
|
||||
*/
|
||||
setSearch(name, custom) {
|
||||
let url;
|
||||
let query = 'q';
|
||||
const info = searchEngines.find((i) => i.name === name);
|
||||
|
||||
if (info !== undefined) {
|
||||
url = info.url;
|
||||
if (info.query) {
|
||||
query = info.query;
|
||||
}
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
const customSetting = localStorage.getItem('customSearchEngine');
|
||||
if (customSetting !== null) {
|
||||
url = customSetting;
|
||||
} else {
|
||||
url = this.state.url;
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem('searchEngine', info.settingsName);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
url,
|
||||
query,
|
||||
currentSearch: name,
|
||||
searchDropdown: false,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
if (data === 'search') {
|
||||
this.init();
|
||||
}
|
||||
});
|
||||
|
||||
this.init();
|
||||
|
||||
if (localStorage.getItem('searchFocus') === 'true') {
|
||||
const element = document.getElementById('searchtext');
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the icon for the search engine dropdown.
|
||||
* @param {string} name - The name of the search engine.
|
||||
* @returns A React component.
|
||||
*/
|
||||
getSearchDropdownicon(name) {
|
||||
switch (name) {
|
||||
case 'Google':
|
||||
return <BsGoogle />;
|
||||
case 'DuckDuckGo':
|
||||
return <SiDuckduckgo />;
|
||||
case 'Bing':
|
||||
return <SiMicrosoftbing />;
|
||||
case 'Yahoo':
|
||||
case 'Yahoo! JAPAN':
|
||||
return <SiYahoo />;
|
||||
case 'Яндекс':
|
||||
return <FaYandex />;
|
||||
case '百度':
|
||||
return <SiBaidu />;
|
||||
default:
|
||||
return <MdScreenSearchDesktop />;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const customText = variables
|
||||
.getMessage('modals.main.settings.sections.search.custom')
|
||||
.split(' ')[0];
|
||||
|
||||
return (
|
||||
<div className="searchComponents">
|
||||
<div className="searchMain">
|
||||
<div className={this.state.classList}>
|
||||
{localStorage.getItem('searchDropdown') === 'true' ? (
|
||||
<Tooltip
|
||||
title={variables.getMessage('modals.main.settings.sections.search.search_engine')}
|
||||
>
|
||||
<button
|
||||
aria-label="Search Engine"
|
||||
onClick={() => this.setState({ searchDropdown: !this.state.searchDropdown })}
|
||||
>
|
||||
{this.getSearchDropdownicon(this.state.currentSearch)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<Tooltip
|
||||
title={variables.getMessage('modals.main.settings.sections.search.voice_search')}
|
||||
>
|
||||
{this.state.microphone}
|
||||
</Tooltip>
|
||||
</div>
|
||||
<form onSubmit={this.searchButton} className="searchBar">
|
||||
<div className={this.state.classList}>
|
||||
<Tooltip title={variables.getMessage('widgets.search')}>
|
||||
<button onClick={this.searchButton} aria-label="Search">
|
||||
<MdSearch />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<AutocompleteInput
|
||||
placeholder={variables.getMessage('widgets.search')}
|
||||
id="searchtext"
|
||||
suggestions={this.state.suggestions}
|
||||
onChange={(e) => this.getSuggestions(e)}
|
||||
onClick={this.searchButton}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
{localStorage.getItem('searchDropdown') === 'true' &&
|
||||
this.state.searchDropdown === true && (
|
||||
<div className="searchDropdown">
|
||||
{searchEngines.map(({ name }, key) => {
|
||||
return (
|
||||
<span
|
||||
className={
|
||||
'searchDropdownList' +
|
||||
(this.state.currentSearch === name ? ' searchDropdownListActive' : '')
|
||||
}
|
||||
onClick={() => this.setSearch(name)}
|
||||
key={key}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
<span
|
||||
className={
|
||||
'searchDropdownList' +
|
||||
(this.state.currentSearch === customText ? ' searchDropdownListActive' : '')
|
||||
}
|
||||
onClick={() => this.setSearch(customText, 'custom')}
|
||||
>
|
||||
{customText}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
93
src/features/widgets/search/search.scss
Normal file
93
src/features/widgets/search/search.scss
Normal file
@@ -0,0 +1,93 @@
|
||||
@import 'scss/variables';
|
||||
|
||||
.searchBar {
|
||||
display: flex;
|
||||
border-radius: 12px;
|
||||
justify-content: flex-start;
|
||||
flex-direction: row;
|
||||
|
||||
input[type='text'] {
|
||||
@extend %basic;
|
||||
|
||||
outline: none;
|
||||
border: none;
|
||||
font-size: 1.2rem;
|
||||
padding: 10px 0 10px 20px;
|
||||
|
||||
&::placeholder {
|
||||
@include themed {
|
||||
color: t($color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.searchIcons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 1px;
|
||||
margin-right: 10px;
|
||||
|
||||
.tooltip {
|
||||
max-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.searchComponents {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
||||
button {
|
||||
@include basicIconButton(12px, 1.2rem, ui);
|
||||
}
|
||||
}
|
||||
|
||||
.searchDropdown {
|
||||
@extend %basic;
|
||||
|
||||
margin-top: 5px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-content: flex-start;
|
||||
text-align: left;
|
||||
font-size: 0.6em;
|
||||
width: 200px;
|
||||
transition: 0.5s;
|
||||
|
||||
span {
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
border-radius: 12px;
|
||||
|
||||
&:hover {
|
||||
@include themed {
|
||||
background: t($btn-backgroundHover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.searchDropdownListActive {
|
||||
@include themed {
|
||||
background: t($btn-backgroundHover);
|
||||
}
|
||||
}
|
||||
|
||||
.searchMain {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
animation: fadeIn 2s;
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/features/widgets/search/search_engines.json
Normal file
60
src/features/widgets/search/search_engines.json
Normal file
@@ -0,0 +1,60 @@
|
||||
[
|
||||
{
|
||||
"name": "DuckDuckGo",
|
||||
"settingsName": "duckduckgo",
|
||||
"url": "https://duckduckgo.com"
|
||||
},
|
||||
{
|
||||
"name": "Google",
|
||||
"settingsName": "google",
|
||||
"url": "https://google.com/search"
|
||||
},
|
||||
{
|
||||
"name": "Bing",
|
||||
"settingsName": "bing",
|
||||
"url": "https://bing.com/search"
|
||||
},
|
||||
{
|
||||
"name": "Ecosia",
|
||||
"settingsName": "ecosia",
|
||||
"url": "https://ecosia.org/search"
|
||||
},
|
||||
{
|
||||
"name": "Ask",
|
||||
"settingsName": "ask",
|
||||
"url": "https://ask.com/web"
|
||||
},
|
||||
{
|
||||
"name": "Yahoo",
|
||||
"settingsName": "yahoo",
|
||||
"url": "https://search.yahoo.com/search"
|
||||
},
|
||||
{
|
||||
"name": "Qwant",
|
||||
"settingsName": "qwant",
|
||||
"url": "https://www.qwant.com/"
|
||||
},
|
||||
{
|
||||
"name": "Startpage",
|
||||
"settingsName": "startpage",
|
||||
"url": "https://www.startpage.com/sp/search"
|
||||
},
|
||||
{
|
||||
"name": "Yahoo! JAPAN",
|
||||
"settingsName": "yahoojp",
|
||||
"url": "https://search.yahoo.co.jp/search",
|
||||
"query": "p"
|
||||
},
|
||||
{
|
||||
"name": "Яндекс",
|
||||
"settingsName": "yandex",
|
||||
"url": "https://yandex.ru/search",
|
||||
"query": "text"
|
||||
},
|
||||
{
|
||||
"name": "百度",
|
||||
"settingsName": "baidu",
|
||||
"url": "https://www.baidu.com/s",
|
||||
"query": "wd"
|
||||
}
|
||||
]
|
||||
201
src/features/widgets/time/Clock.jsx
Normal file
201
src/features/widgets/time/Clock.jsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import { PureComponent, Suspense, lazy } from 'react';
|
||||
|
||||
import { convertTimezone } from 'modules/helpers/date';
|
||||
import EventBus from 'modules/helpers/eventbus';
|
||||
|
||||
import './clock.scss';
|
||||
|
||||
const Analog = lazy(() => import('react-clock'));
|
||||
|
||||
export default class Clock extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.timer = undefined;
|
||||
this.state = {
|
||||
time: '',
|
||||
finalHour: '',
|
||||
finalMinute: '',
|
||||
finalSeconds: '',
|
||||
ampm: '',
|
||||
nowGlobal: new Date(),
|
||||
minuteColour: localStorage.getItem('minuteColour'),
|
||||
hourColour: localStorage.getItem('hourColour'),
|
||||
};
|
||||
}
|
||||
|
||||
startTime(
|
||||
time = localStorage.getItem('seconds') === 'true' ||
|
||||
localStorage.getItem('timeType') === 'analogue'
|
||||
? 1000 - (Date.now() % 1000)
|
||||
: 60000 - (Date.now() % 60000),
|
||||
) {
|
||||
this.timer = setTimeout(() => {
|
||||
let now = new Date();
|
||||
const timezone = localStorage.getItem('timezone');
|
||||
if (timezone && timezone !== 'auto') {
|
||||
now = convertTimezone(now, timezone);
|
||||
}
|
||||
|
||||
switch (localStorage.getItem('timeType')) {
|
||||
case 'percentageComplete':
|
||||
this.setState({
|
||||
time: (now.getHours() / 24).toFixed(2).replace('0.', '') + '%',
|
||||
ampm: '',
|
||||
});
|
||||
break;
|
||||
case 'analogue':
|
||||
// load analog clock css
|
||||
import('react-clock/dist/Clock.css');
|
||||
|
||||
this.setState({
|
||||
time: now,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
// Default clock
|
||||
let time,
|
||||
sec = '';
|
||||
const zero = localStorage.getItem('zero');
|
||||
|
||||
if (localStorage.getItem('seconds') === 'true') {
|
||||
sec = `:${('00' + now.getSeconds()).slice(-2)}`;
|
||||
this.setState({ finalSeconds: `:${('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)}`,
|
||||
});
|
||||
} 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)}`,
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
time,
|
||||
ampm: '',
|
||||
});
|
||||
} else {
|
||||
// 12 hour
|
||||
let hours = now.getHours();
|
||||
|
||||
if (hours > 12) {
|
||||
hours -= 12;
|
||||
} else if (hours === 0) {
|
||||
hours = 12;
|
||||
}
|
||||
|
||||
if (zero === 'false') {
|
||||
time = `${hours}:${('00' + now.getMinutes()).slice(-2)}${sec}`;
|
||||
this.setState({
|
||||
finalHour: `${hours}`,
|
||||
finalMinute: `${('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)}`,
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
time,
|
||||
ampm: now.getHours() > 11 ? 'PM' : 'AM',
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this.startTime();
|
||||
}, time);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
if (data === 'clock' || data === 'timezone') {
|
||||
const element = document.querySelector('.clock-container');
|
||||
|
||||
if (localStorage.getItem('time') === 'false') {
|
||||
return (element.style.display = 'none');
|
||||
}
|
||||
|
||||
this.timer = null;
|
||||
this.startTime(0);
|
||||
|
||||
element.style.display = 'block';
|
||||
element.style.fontSize = `${
|
||||
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`;
|
||||
}
|
||||
|
||||
this.startTime(0);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
}
|
||||
|
||||
render() {
|
||||
const enabled = (setting) => {
|
||||
return localStorage.getItem(setting) === 'true';
|
||||
};
|
||||
|
||||
if (localStorage.getItem('timeType') === 'analogue') {
|
||||
return (
|
||||
<Suspense fallback={<></>}>
|
||||
<div className={`clockBackground ${enabled('roundClock') ? 'round' : ''}`}>
|
||||
<Analog
|
||||
className="analogclock clock-container"
|
||||
value={this.state.time}
|
||||
size={1.5 * Number(localStorage.getItem('zoomClock') || 100)}
|
||||
renderMinuteMarks={enabled('minuteMarks')}
|
||||
renderHourMarks={enabled('hourMarks')}
|
||||
renderSecondHand={enabled('secondHand')}
|
||||
renderMinuteHand={enabled('minuteHand')}
|
||||
renderHourHand={enabled('hourHand')}
|
||||
/>
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
if (localStorage.getItem('timeType') === 'verticalClock') {
|
||||
return (
|
||||
<span className="new-clock clock-container">
|
||||
{' '}
|
||||
<div className="hour" style={{ color: this.state.hourColour }}>
|
||||
{this.state.finalHour}
|
||||
</div>{' '}
|
||||
<div className="minute" style={{ color: this.state.minuteColour }}>
|
||||
{this.state.finalMinute}
|
||||
</div>{' '}
|
||||
<div className="seconds">{this.state.finalSeconds}</div>{' '}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="clock clock-container">
|
||||
{this.state.time}
|
||||
<span className="ampm">{this.state.ampm}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
174
src/features/widgets/time/Date.jsx
Normal file
174
src/features/widgets/time/Date.jsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent, createRef } from 'react';
|
||||
|
||||
import { nth, convertTimezone } from '../../../modules/helpers/date';
|
||||
import EventBus from 'modules/helpers/eventbus';
|
||||
|
||||
import './date.scss';
|
||||
|
||||
export default class DateWidget extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
date: '',
|
||||
weekNumber: null,
|
||||
};
|
||||
this.date = createRef();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the week number of the year for the given date.
|
||||
* @param {Date} date
|
||||
*/
|
||||
getWeekNumber(date) {
|
||||
const dateToday = new Date(date.valueOf());
|
||||
const dayNumber = (dateToday.getDay() + 6) % 7;
|
||||
|
||||
dateToday.setDate(dateToday.getDate() - dayNumber + 3);
|
||||
const firstThursday = dateToday.valueOf();
|
||||
dateToday.setMonth(0, 1);
|
||||
|
||||
if (dateToday.getDay() !== 4) {
|
||||
dateToday.setMonth(0, 1 + ((4 - dateToday.getDay() + 7) % 7));
|
||||
}
|
||||
|
||||
this.setState({
|
||||
weekNumber: `${variables.getMessage('widgets.date.week')} ${
|
||||
1 + Math.ceil((firstThursday - dateToday) / 604800000)
|
||||
}`,
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
month = dateDay;
|
||||
break;
|
||||
case 'YMD':
|
||||
day = dateYear;
|
||||
year = dateDay;
|
||||
break;
|
||||
// DMY
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
let format;
|
||||
switch (localStorage.getItem('shortFormat')) {
|
||||
case 'dots':
|
||||
format = `${day}.${month}.${year}`;
|
||||
break;
|
||||
case 'dash':
|
||||
format = `${day}-${month}-${year}`;
|
||||
break;
|
||||
case 'gaps':
|
||||
format = `${day} - ${month} - ${year}`;
|
||||
break;
|
||||
case 'slashes':
|
||||
format = `${day}/${month}/${year}`;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
date: 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 dateMonth = date.toLocaleDateString(lang, { month: 'long' });
|
||||
const dateYear = date.getFullYear();
|
||||
|
||||
let day = dateDay + ' ' + datenth;
|
||||
let month = dateMonth;
|
||||
let year = dateYear;
|
||||
switch (localStorage.getItem('longFormat')) {
|
||||
case 'MDY':
|
||||
day = dateMonth;
|
||||
month = dateDay + ' ' + datenth;
|
||||
break;
|
||||
case 'YMD':
|
||||
day = dateYear;
|
||||
year = dateDay + ' ' + datenth;
|
||||
break;
|
||||
// DMY
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
date: `${day} ${month} ${year}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
if (data === 'date' || data === 'timezone') {
|
||||
if (localStorage.getItem('date') === 'false') {
|
||||
return (this.date.current.style.display = 'none');
|
||||
}
|
||||
|
||||
this.date.current.style.display = 'block';
|
||||
this.date.current.style.fontSize = `${Number(
|
||||
(localStorage.getItem('zoomDate') || 100) / 100,
|
||||
)}em`;
|
||||
this.getDate();
|
||||
}
|
||||
});
|
||||
|
||||
this.date.current.style.fontSize = `${Number(
|
||||
(localStorage.getItem('zoomDate') || 100) / 100,
|
||||
)}em`;
|
||||
this.getDate();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span className="date" ref={this.date}>
|
||||
{this.state.date}
|
||||
<br />
|
||||
{this.state.weekNumber}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
59
src/features/widgets/time/clock.scss
Normal file
59
src/features/widgets/time/clock.scss
Normal file
@@ -0,0 +1,59 @@
|
||||
@import 'scss/variables';
|
||||
|
||||
.clock {
|
||||
font-size: 4em;
|
||||
margin: 0;
|
||||
cursor: initial;
|
||||
user-select: none;
|
||||
font-weight: 600;
|
||||
|
||||
--shadow-shift: 0.4rem;
|
||||
}
|
||||
|
||||
.ampm {
|
||||
font-size: 0.5em;
|
||||
}
|
||||
|
||||
.analogclock,
|
||||
.react-clock__face {
|
||||
margin: 0 auto;
|
||||
border-radius: 100%;
|
||||
|
||||
/* box-shadow: inset 0 0 100px rgba(0, 0, 0, 0.3); */
|
||||
border: none !important;
|
||||
|
||||
@include themed {
|
||||
border: 1px solid t($color) !important;
|
||||
}
|
||||
|
||||
cursor: initial;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.react-clock__hand__body,
|
||||
.react-clock__mark__body {
|
||||
@include themed {
|
||||
background: t($color) !important;
|
||||
}
|
||||
|
||||
/* box-shadow: 0 0 25px rgba(0, 0, 0, 0.3); */
|
||||
}
|
||||
|
||||
.clockBackground {
|
||||
@extend %basic;
|
||||
|
||||
padding: 1rem;
|
||||
|
||||
&.round {
|
||||
border-radius: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.new-clock {
|
||||
line-height: 100%;
|
||||
|
||||
.seconds {
|
||||
font-size: 0.2em;
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
8
src/features/widgets/time/date.scss
Normal file
8
src/features/widgets/time/date.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
.date {
|
||||
cursor: initial;
|
||||
user-select: none;
|
||||
text-transform: capitalize;
|
||||
font-weight: bold;
|
||||
|
||||
--shadow-shift: 0.125rem;
|
||||
}
|
||||
350
src/features/widgets/time/timezones.json
Normal file
350
src/features/widgets/time/timezones.json
Normal file
@@ -0,0 +1,350 @@
|
||||
[
|
||||
"Africa/Abidjan",
|
||||
"Africa/Accra",
|
||||
"Africa/Algiers",
|
||||
"Africa/Bissau",
|
||||
"Africa/Cairo",
|
||||
"Africa/Casablanca",
|
||||
"Africa/Ceuta",
|
||||
"Africa/El_Aaiun",
|
||||
"Africa/Johannesburg",
|
||||
"Africa/Juba",
|
||||
"Africa/Khartoum",
|
||||
"Africa/Lagos",
|
||||
"Africa/Maputo",
|
||||
"Africa/Monrovia",
|
||||
"Africa/Nairobi",
|
||||
"Africa/Ndjamena",
|
||||
"Africa/Sao_Tome",
|
||||
"Africa/Tripoli",
|
||||
"Africa/Tunis",
|
||||
"Africa/Windhoek",
|
||||
"America/Adak",
|
||||
"America/Anchorage",
|
||||
"America/Araguaina",
|
||||
"America/Argentina/Buenos_Aires",
|
||||
"America/Argentina/Catamarca",
|
||||
"America/Argentina/Cordoba",
|
||||
"America/Argentina/Jujuy",
|
||||
"America/Argentina/La_Rioja",
|
||||
"America/Argentina/Mendoza",
|
||||
"America/Argentina/Rio_Gallegos",
|
||||
"America/Argentina/Salta",
|
||||
"America/Argentina/San_Juan",
|
||||
"America/Argentina/San_Luis",
|
||||
"America/Argentina/Tucuman",
|
||||
"America/Argentina/Ushuaia",
|
||||
"America/Asuncion",
|
||||
"America/Atikokan",
|
||||
"America/Bahia",
|
||||
"America/Bahia_Banderas",
|
||||
"America/Barbados",
|
||||
"America/Belem",
|
||||
"America/Belize",
|
||||
"America/Blanc-Sablon",
|
||||
"America/Boa_Vista",
|
||||
"America/Bogota",
|
||||
"America/Boise",
|
||||
"America/Cambridge_Bay",
|
||||
"America/Campo_Grande",
|
||||
"America/Cancun",
|
||||
"America/Caracas",
|
||||
"America/Cayenne",
|
||||
"America/Chicago",
|
||||
"America/Chihuahua",
|
||||
"America/Costa_Rica",
|
||||
"America/Creston",
|
||||
"America/Cuiaba",
|
||||
"America/Curacao",
|
||||
"America/Danmarkshavn",
|
||||
"America/Dawson",
|
||||
"America/Dawson_Creek",
|
||||
"America/Denver",
|
||||
"America/Detroit",
|
||||
"America/Edmonton",
|
||||
"America/Eirunepe",
|
||||
"America/El_Salvador",
|
||||
"America/Fort_Nelson",
|
||||
"America/Fortaleza",
|
||||
"America/Glace_Bay",
|
||||
"America/Godthab",
|
||||
"America/Goose_Bay",
|
||||
"America/Grand_Turk",
|
||||
"America/Guatemala",
|
||||
"America/Guayaquil",
|
||||
"America/Guyana",
|
||||
"America/Halifax",
|
||||
"America/Havana",
|
||||
"America/Hermosillo",
|
||||
"America/Indiana/Indianapolis",
|
||||
"America/Indiana/Knox",
|
||||
"America/Indiana/Marengo",
|
||||
"America/Indiana/Petersburg",
|
||||
"America/Indiana/Tell_City",
|
||||
"America/Indiana/Vevay",
|
||||
"America/Indiana/Vincennes",
|
||||
"America/Indiana/Winamac",
|
||||
"America/Inuvik",
|
||||
"America/Iqaluit",
|
||||
"America/Jamaica",
|
||||
"America/Juneau",
|
||||
"America/Kentucky/Louisville",
|
||||
"America/Kentucky/Monticello",
|
||||
"America/La_Paz",
|
||||
"America/Lima",
|
||||
"America/Los_Angeles",
|
||||
"America/Maceio",
|
||||
"America/Managua",
|
||||
"America/Manaus",
|
||||
"America/Martinique",
|
||||
"America/Matamoros",
|
||||
"America/Mazatlan",
|
||||
"America/Menominee",
|
||||
"America/Merida",
|
||||
"America/Metlakatla",
|
||||
"America/Mexico_City",
|
||||
"America/Miquelon",
|
||||
"America/Moncton",
|
||||
"America/Monterrey",
|
||||
"America/Montevideo",
|
||||
"America/Nassau",
|
||||
"America/New_York",
|
||||
"America/Nipigon",
|
||||
"America/Nome",
|
||||
"America/Noronha",
|
||||
"America/North_Dakota/Beulah",
|
||||
"America/North_Dakota/Center",
|
||||
"America/North_Dakota/New_Salem",
|
||||
"America/Ojinaga",
|
||||
"America/Panama",
|
||||
"America/Pangnirtung",
|
||||
"America/Paramaribo",
|
||||
"America/Phoenix",
|
||||
"America/Port-au-Prince",
|
||||
"America/Port_of_Spain",
|
||||
"America/Porto_Velho",
|
||||
"America/Puerto_Rico",
|
||||
"America/Punta_Arenas",
|
||||
"America/Rainy_River",
|
||||
"America/Rankin_Inlet",
|
||||
"America/Recife",
|
||||
"America/Regina",
|
||||
"America/Resolute",
|
||||
"America/Rio_Branco",
|
||||
"America/Santarem",
|
||||
"America/Santiago",
|
||||
"America/Santo_Domingo",
|
||||
"America/Sao_Paulo",
|
||||
"America/Scoresbysund",
|
||||
"America/Sitka",
|
||||
"America/St_Johns",
|
||||
"America/Swift_Current",
|
||||
"America/Tegucigalpa",
|
||||
"America/Thule",
|
||||
"America/Thunder_Bay",
|
||||
"America/Tijuana",
|
||||
"America/Toronto",
|
||||
"America/Vancouver",
|
||||
"America/Whitehorse",
|
||||
"America/Winnipeg",
|
||||
"America/Yakutat",
|
||||
"America/Yellowknife",
|
||||
"Antarctica/Casey",
|
||||
"Antarctica/Davis",
|
||||
"Antarctica/DumontDUrville",
|
||||
"Antarctica/Macquarie",
|
||||
"Antarctica/Mawson",
|
||||
"Antarctica/Palmer",
|
||||
"Antarctica/Rothera",
|
||||
"Antarctica/Syowa",
|
||||
"Antarctica/Troll",
|
||||
"Antarctica/Vostok",
|
||||
"Asia/Almaty",
|
||||
"Asia/Amman",
|
||||
"Asia/Anadyr",
|
||||
"Asia/Aqtau",
|
||||
"Asia/Aqtobe",
|
||||
"Asia/Ashgabat",
|
||||
"Asia/Atyrau",
|
||||
"Asia/Baghdad",
|
||||
"Asia/Baku",
|
||||
"Asia/Bangkok",
|
||||
"Asia/Barnaul",
|
||||
"Asia/Beirut",
|
||||
"Asia/Bishkek",
|
||||
"Asia/Brunei",
|
||||
"Asia/Chita",
|
||||
"Asia/Choibalsan",
|
||||
"Asia/Colombo",
|
||||
"Asia/Damascus",
|
||||
"Asia/Dhaka",
|
||||
"Asia/Dili",
|
||||
"Asia/Dubai",
|
||||
"Asia/Dushanbe",
|
||||
"Asia/Famagusta",
|
||||
"Asia/Gaza",
|
||||
"Asia/Hebron",
|
||||
"Asia/Ho_Chi_Minh",
|
||||
"Asia/Hong_Kong",
|
||||
"Asia/Hovd",
|
||||
"Asia/Irkutsk",
|
||||
"Asia/Jakarta",
|
||||
"Asia/Jayapura",
|
||||
"Asia/Jerusalem",
|
||||
"Asia/Kabul",
|
||||
"Asia/Kamchatka",
|
||||
"Asia/Karachi",
|
||||
"Asia/Kathmandu",
|
||||
"Asia/Khandyga",
|
||||
"Asia/Kolkata",
|
||||
"Asia/Krasnoyarsk",
|
||||
"Asia/Kuala_Lumpur",
|
||||
"Asia/Kuching",
|
||||
"Asia/Macau",
|
||||
"Asia/Magadan",
|
||||
"Asia/Makassar",
|
||||
"Asia/Manila",
|
||||
"Asia/Nicosia",
|
||||
"Asia/Novokuznetsk",
|
||||
"Asia/Novosibirsk",
|
||||
"Asia/Omsk",
|
||||
"Asia/Oral",
|
||||
"Asia/Pontianak",
|
||||
"Asia/Pyongyang",
|
||||
"Asia/Qatar",
|
||||
"Asia/Qostanay",
|
||||
"Asia/Qyzylorda",
|
||||
"Asia/Riyadh",
|
||||
"Asia/Sakhalin",
|
||||
"Asia/Samarkand",
|
||||
"Asia/Seoul",
|
||||
"Asia/Shanghai",
|
||||
"Asia/Singapore",
|
||||
"Asia/Srednekolymsk",
|
||||
"Asia/Taipei",
|
||||
"Asia/Tashkent",
|
||||
"Asia/Tbilisi",
|
||||
"Asia/Tehran",
|
||||
"Asia/Thimphu",
|
||||
"Asia/Tokyo",
|
||||
"Asia/Tomsk",
|
||||
"Asia/Ulaanbaatar",
|
||||
"Asia/Urumqi",
|
||||
"Asia/Ust-Nera",
|
||||
"Asia/Vladivostok",
|
||||
"Asia/Yakutsk",
|
||||
"Asia/Yangon",
|
||||
"Asia/Yekaterinburg",
|
||||
"Asia/Yerevan",
|
||||
"Atlantic/Azores",
|
||||
"Atlantic/Bermuda",
|
||||
"Atlantic/Canary",
|
||||
"Atlantic/Cape_Verde",
|
||||
"Atlantic/Faroe",
|
||||
"Atlantic/Madeira",
|
||||
"Atlantic/Reykjavik",
|
||||
"Atlantic/South_Georgia",
|
||||
"Atlantic/Stanley",
|
||||
"Australia/Adelaide",
|
||||
"Australia/Brisbane",
|
||||
"Australia/Broken_Hill",
|
||||
"Australia/Currie",
|
||||
"Australia/Darwin",
|
||||
"Australia/Eucla",
|
||||
"Australia/Hobart",
|
||||
"Australia/Lindeman",
|
||||
"Australia/Lord_Howe",
|
||||
"Australia/Melbourne",
|
||||
"Australia/Perth",
|
||||
"Australia/Sydney",
|
||||
"Europe/Amsterdam",
|
||||
"Europe/Andorra",
|
||||
"Europe/Astrakhan",
|
||||
"Europe/Athens",
|
||||
"Europe/Belgrade",
|
||||
"Europe/Berlin",
|
||||
"Europe/Brussels",
|
||||
"Europe/Bucharest",
|
||||
"Europe/Budapest",
|
||||
"Europe/Chisinau",
|
||||
"Europe/Copenhagen",
|
||||
"Europe/Dublin",
|
||||
"Europe/Gibraltar",
|
||||
"Europe/Helsinki",
|
||||
"Europe/Istanbul",
|
||||
"Europe/Kaliningrad",
|
||||
"Europe/Kiev",
|
||||
"Europe/Kirov",
|
||||
"Europe/Lisbon",
|
||||
"Europe/London",
|
||||
"Europe/Luxembourg",
|
||||
"Europe/Madrid",
|
||||
"Europe/Malta",
|
||||
"Europe/Minsk",
|
||||
"Europe/Monaco",
|
||||
"Europe/Moscow",
|
||||
"Europe/Oslo",
|
||||
"Europe/Paris",
|
||||
"Europe/Prague",
|
||||
"Europe/Riga",
|
||||
"Europe/Rome",
|
||||
"Europe/Samara",
|
||||
"Europe/Saratov",
|
||||
"Europe/Simferopol",
|
||||
"Europe/Sofia",
|
||||
"Europe/Stockholm",
|
||||
"Europe/Tallinn",
|
||||
"Europe/Tirane",
|
||||
"Europe/Ulyanovsk",
|
||||
"Europe/Uzhgorod",
|
||||
"Europe/Vienna",
|
||||
"Europe/Vilnius",
|
||||
"Europe/Volgograd",
|
||||
"Europe/Warsaw",
|
||||
"Europe/Zaporozhye",
|
||||
"Europe/Zurich",
|
||||
"Indian/Chagos",
|
||||
"Indian/Christmas",
|
||||
"Indian/Cocos",
|
||||
"Indian/Kerguelen",
|
||||
"Indian/Mahe",
|
||||
"Indian/Maldives",
|
||||
"Indian/Mauritius",
|
||||
"Indian/Reunion",
|
||||
"Pacific/Apia",
|
||||
"Pacific/Auckland",
|
||||
"Pacific/Bougainville",
|
||||
"Pacific/Chatham",
|
||||
"Pacific/Chuuk",
|
||||
"Pacific/Easter",
|
||||
"Pacific/Efate",
|
||||
"Pacific/Enderbury",
|
||||
"Pacific/Fakaofo",
|
||||
"Pacific/Fiji",
|
||||
"Pacific/Funafuti",
|
||||
"Pacific/Galapagos",
|
||||
"Pacific/Gambier",
|
||||
"Pacific/Guadalcanal",
|
||||
"Pacific/Guam",
|
||||
"Pacific/Honolulu",
|
||||
"Pacific/Kiritimati",
|
||||
"Pacific/Kosrae",
|
||||
"Pacific/Kwajalein",
|
||||
"Pacific/Majuro",
|
||||
"Pacific/Marquesas",
|
||||
"Pacific/Nauru",
|
||||
"Pacific/Niue",
|
||||
"Pacific/Norfolk",
|
||||
"Pacific/Noumea",
|
||||
"Pacific/Pago_Pago",
|
||||
"Pacific/Palau",
|
||||
"Pacific/Pitcairn",
|
||||
"Pacific/Pohnpei",
|
||||
"Pacific/Port_Moresby",
|
||||
"Pacific/Rarotonga",
|
||||
"Pacific/Tahiti",
|
||||
"Pacific/Tarawa",
|
||||
"Pacific/Tongatapu",
|
||||
"Pacific/Wake",
|
||||
"Pacific/Wallis"
|
||||
]
|
||||
119
src/features/widgets/weather/Expanded.jsx
Normal file
119
src/features/widgets/weather/Expanded.jsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
import { WiHumidity, WiWindy, WiBarometer, WiCloud } from 'react-icons/wi';
|
||||
import { MdDisabledVisible } from 'react-icons/md';
|
||||
|
||||
import WeatherIcon from './WeatherIcon';
|
||||
import WindDirectionIcon from './WindDirectionIcon';
|
||||
|
||||
import { Tooltip } from 'components/Elements';
|
||||
function Expanded({ state: { weather, icon }, weatherType, variables }) {
|
||||
/**
|
||||
* If the localStorage item is true and the weatherType is greater than or equal to 3, or if the
|
||||
* weatherType is equal to 3, then return true.
|
||||
* @param {string} setting - The localStorage item to check.
|
||||
* @returns a boolean value.
|
||||
*/
|
||||
const enabled = useMemo(() => {
|
||||
return (setting) => {
|
||||
return (localStorage.getItem(setting) === 'true' && weatherType >= 3) || weatherType === '3';
|
||||
};
|
||||
}, [weatherType]);
|
||||
|
||||
return (
|
||||
<div className="expanded-info">
|
||||
{weatherType >= 3 && (
|
||||
<span className="subtitle">
|
||||
{variables.getMessage('widgets.weather.extra_information')}
|
||||
</span>
|
||||
)}
|
||||
{enabled('cloudiness') && (
|
||||
<Tooltip
|
||||
title={variables.getMessage(
|
||||
'modals.main.settings.sections.weather.extra_info.cloudiness',
|
||||
)}
|
||||
placement="left"
|
||||
>
|
||||
<span>
|
||||
<WiCloud className="weatherIcon" />
|
||||
{weather.cloudiness}%
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{enabled('windspeed') && (
|
||||
<Tooltip
|
||||
title={variables.getMessage(
|
||||
'modals.main.settings.sections.weather.extra_info.wind_speed',
|
||||
)}
|
||||
placement="left"
|
||||
>
|
||||
<span>
|
||||
<WiWindy className="weatherIcon" />
|
||||
{weather.wind_speed}
|
||||
<span className="minmax">m/s</span>{' '}
|
||||
{enabled('windDirection') && (
|
||||
<div style={{ fontSize: '25px', display: 'grid' }}>
|
||||
<WindDirectionIcon className="weatherIcon" degrees={weather.wind_degrees} />
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{enabled('atmosphericpressure') && (
|
||||
<Tooltip
|
||||
title={variables.getMessage(
|
||||
'modals.main.settings.sections.weather.extra_info.atmospheric_pressure',
|
||||
)}
|
||||
placement="left"
|
||||
>
|
||||
<span>
|
||||
<WiBarometer className="weatherIcon" />
|
||||
{weather.pressure}
|
||||
<span className="minmax">hPa</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{enabled('weatherdescription') && (
|
||||
<Tooltip
|
||||
title={variables.getMessage(
|
||||
'modals.main.settings.sections.weather.extra_info.weather_description',
|
||||
)}
|
||||
placement="left"
|
||||
>
|
||||
<span>
|
||||
<WeatherIcon className="weatherIcon" name={icon} />
|
||||
{weather.description}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{enabled('visibility') && (
|
||||
<Tooltip
|
||||
title={variables.getMessage(
|
||||
'modals.main.settings.sections.weather.extra_info.visibility',
|
||||
)}
|
||||
placement="left"
|
||||
>
|
||||
<span>
|
||||
<MdDisabledVisible className="materialWeatherIcon" />
|
||||
{variables.getMessage('widgets.weather.meters', {
|
||||
amount: weather.visibility,
|
||||
})}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{enabled('humidity') && (
|
||||
<Tooltip
|
||||
title={variables.getMessage('modals.main.settings.sections.weather.extra_info.humidity')}
|
||||
placement="left"
|
||||
>
|
||||
<span>
|
||||
<WiHumidity className="materialWeatherIcon" />
|
||||
{weather.humidity}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Expanded);
|
||||
155
src/features/widgets/weather/Weather.jsx
Normal file
155
src/features/widgets/weather/Weather.jsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import variables from 'config/variables';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import WeatherIcon from './WeatherIcon';
|
||||
import Expanded from './Expanded';
|
||||
|
||||
import EventBus from 'modules/helpers/eventbus';
|
||||
|
||||
import './weather.scss';
|
||||
|
||||
const convertTemperature = (temp, format) => {
|
||||
if (format === 'celsius') {
|
||||
return Math.round(temp - 273.15);
|
||||
} else if (format === 'fahrenheit') {
|
||||
return Math.round((temp - 273.15) * 1.8 + 32);
|
||||
}
|
||||
return Math.round(temp);
|
||||
};
|
||||
|
||||
export default class WeatherSettings extends PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
location: localStorage.getItem('location') || 'London',
|
||||
done: false,
|
||||
};
|
||||
}
|
||||
|
||||
async getWeather() {
|
||||
if (this.state.done === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const zoomWeather = `${Number((localStorage.getItem('zoomWeather') || 100) / 100)}em`;
|
||||
document.querySelector('.weather').style.fontSize = zoomWeather;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
variables.constants.API_URL +
|
||||
`/weather?city=${this.state.location}&language=${variables.languagecode}`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
this.setState({
|
||||
location: variables.getMessage('modals.main.error_boundary.title'),
|
||||
done: true,
|
||||
});
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 404) {
|
||||
return this.setState({
|
||||
location: variables.getMessage('widgets.weather.not_found'),
|
||||
done: true,
|
||||
});
|
||||
}
|
||||
|
||||
const { temp, temp_min, temp_max, feels_like } = data.main;
|
||||
const tempFormat = localStorage.getItem('tempformat');
|
||||
|
||||
const tempSymbols = {
|
||||
celsius: '°C',
|
||||
kelvin: 'K',
|
||||
fahrenheit: '°F',
|
||||
};
|
||||
|
||||
this.setState({
|
||||
icon: data.weather[0].icon,
|
||||
temp_text: tempSymbols[tempFormat] || 'K',
|
||||
weather: {
|
||||
temp: convertTemperature(temp, tempFormat),
|
||||
description: data.weather[0].description,
|
||||
temp_min: convertTemperature(temp_min, tempFormat),
|
||||
temp_max: convertTemperature(temp_max, tempFormat),
|
||||
feels_like: convertTemperature(feels_like, tempFormat),
|
||||
humidity: data.main.humidity,
|
||||
wind_speed: data.wind.speed,
|
||||
wind_degrees: data.wind.deg,
|
||||
cloudiness: data.clouds.all,
|
||||
visibility: data.visibility,
|
||||
pressure: data.main.pressure,
|
||||
},
|
||||
done: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fetch Error: ', error);
|
||||
}
|
||||
|
||||
document.querySelector('.top-weather svg').style.fontSize = zoomWeather;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventBus.on('refresh', (data) => {
|
||||
if (data === 'weather') {
|
||||
this.getWeather();
|
||||
}
|
||||
});
|
||||
|
||||
this.getWeather();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventBus.off('refresh');
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.done === false) {
|
||||
return <div className="weather"></div>;
|
||||
}
|
||||
|
||||
const weatherType = localStorage.getItem('weatherType') || 1;
|
||||
|
||||
if (!this.state.weather) {
|
||||
return (
|
||||
<div className="weather">
|
||||
<span className="loc">{this.state.location}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="weather">
|
||||
<div className="top-weather">
|
||||
{weatherType >= 1 && (
|
||||
<div>
|
||||
<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>
|
||||
)}
|
||||
{weatherType >= 3 && (
|
||||
<Expanded weatherType={weatherType} state={this.state} variables={variables} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
44
src/features/widgets/weather/WeatherIcon.jsx
Normal file
44
src/features/widgets/weather/WeatherIcon.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import {
|
||||
WiDaySunny,
|
||||
WiNightClear,
|
||||
WiDayCloudy,
|
||||
WiNightCloudy,
|
||||
WiCloud,
|
||||
WiCloudy,
|
||||
WiDayShowers,
|
||||
WiNightShowers,
|
||||
WiRain,
|
||||
WiThunderstorm,
|
||||
WiSnow,
|
||||
WiFog,
|
||||
} from 'react-icons/wi';
|
||||
|
||||
const iconMap = {
|
||||
'01d': WiDaySunny,
|
||||
'01n': WiNightClear,
|
||||
'02d': WiDayCloudy,
|
||||
'02n': WiNightCloudy,
|
||||
'03d': WiCloud,
|
||||
'03n': WiCloud,
|
||||
'04d': WiCloudy,
|
||||
'04n': WiCloudy,
|
||||
'09d': WiDayShowers,
|
||||
'09n': WiNightShowers,
|
||||
'10d': WiRain,
|
||||
'10n': WiRain,
|
||||
'11d': WiThunderstorm,
|
||||
'11n': WiThunderstorm,
|
||||
'13d': WiSnow,
|
||||
'13n': WiSnow,
|
||||
'50d': WiFog,
|
||||
'50n': WiFog,
|
||||
};
|
||||
|
||||
function WeatherIcon({ name }) {
|
||||
const IconComponent = iconMap[name];
|
||||
return IconComponent ? <IconComponent className="weatherIcon" /> : null;
|
||||
}
|
||||
|
||||
export default memo(WeatherIcon);
|
||||
57
src/features/widgets/weather/WindDirectionIcon.jsx
Normal file
57
src/features/widgets/weather/WindDirectionIcon.jsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import {
|
||||
WiDirectionDownLeft,
|
||||
WiDirectionDownRight,
|
||||
WiDirectionDown,
|
||||
WiDirectionLeft,
|
||||
WiDirectionRight,
|
||||
WiDirectionUpLeft,
|
||||
WiDirectionUpRight,
|
||||
WiDirectionUp,
|
||||
} from 'react-icons/wi';
|
||||
|
||||
// degrees are imported because of a potential bug, IDK what causes it, but now it is fixed
|
||||
function WindDirectionIcon({ degrees }) {
|
||||
// convert the number OpenWeatherMap gives us to the closest direction or something
|
||||
const directions = [
|
||||
{
|
||||
name: 'North',
|
||||
icon: <WiDirectionUp />,
|
||||
},
|
||||
{
|
||||
name: 'North-West',
|
||||
icon: <WiDirectionUpLeft />,
|
||||
},
|
||||
{
|
||||
name: 'West',
|
||||
icon: <WiDirectionLeft />,
|
||||
},
|
||||
{
|
||||
name: 'South-West',
|
||||
icon: <WiDirectionDownLeft />,
|
||||
},
|
||||
{
|
||||
name: 'South',
|
||||
icon: <WiDirectionDown />,
|
||||
},
|
||||
{
|
||||
name: 'South-East',
|
||||
icon: <WiDirectionDownRight />,
|
||||
},
|
||||
{
|
||||
name: 'East',
|
||||
icon: <WiDirectionRight />,
|
||||
},
|
||||
{
|
||||
name: 'North-East',
|
||||
icon: <WiDirectionUpRight />,
|
||||
},
|
||||
];
|
||||
const direction =
|
||||
directions[Math.round(((degrees %= 360) < 0 ? degrees + 360 : degrees) / 45) % 8];
|
||||
|
||||
return direction && direction.icon;
|
||||
}
|
||||
|
||||
export default memo(WindDirectionIcon);
|
||||
120
src/features/widgets/weather/weather.scss
Normal file
120
src/features/widgets/weather/weather.scss
Normal file
@@ -0,0 +1,120 @@
|
||||
//noinspection CssUnknownTarget
|
||||
@import 'scss/variables';
|
||||
|
||||
.weather {
|
||||
@extend %basic;
|
||||
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
cursor: initial;
|
||||
user-select: none;
|
||||
transition: 0.8s cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
width: auto;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
justify-items: start;
|
||||
padding: 25px;
|
||||
|
||||
&:hover {
|
||||
height: auto;
|
||||
transition: 0.8s cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
|
||||
.expanded-info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.tooltip {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tooltipTitle {
|
||||
max-height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.extra-info {
|
||||
width: 100%;
|
||||
font-size: 18px;
|
||||
gap: 40px;
|
||||
|
||||
@include themed {
|
||||
color: t($weather);
|
||||
}
|
||||
}
|
||||
|
||||
.visibility {
|
||||
text-transform: none !important;
|
||||
}
|
||||
|
||||
.extra-info {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: space-evenly;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.top-weather {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 25px;
|
||||
|
||||
div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
svg {
|
||||
font-size: 2em !important;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.minmax {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
}
|
||||
|
||||
.expanded-info {
|
||||
display: none;
|
||||
font-size: 18px;
|
||||
text-transform: capitalize;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@include themed {
|
||||
svg {
|
||||
color: t($subColor);
|
||||
}
|
||||
|
||||
.weatherIcon {
|
||||
font-size: 21px !important;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.materialWeatherIcon {
|
||||
font-size: 18px !important;
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user