refactor(modules): second part of moving files around, changing layout etc

This commit is contained in:
David Ralph
2024-02-19 09:42:59 +00:00
parent 618b5fe466
commit 294b3830bf
87 changed files with 760 additions and 728 deletions

View File

@@ -0,0 +1,35 @@
import offlineImages from 'utils/data/offline_images.json';
/**
* It gets a random photographer from the offlineImages.json file, then gets a random image from that
* photographer, and returns an object with the image's URL, type, and photoInfo.
* </code>
* @param type - 'background' or 'thumbnail'
* @returns An object with the following properties:
* url: A string that is the path to the image.
* type: A string that is the type of image.
* photoInfo: An object with the following properties:
* offline: A boolean that is true.
* credit: A string that is the name of the photographer.
*/
export function offlineBackground(type) {
const photographers = Object.keys(offlineImages);
const photographer = photographers[Math.floor(Math.random() * photographers.length)];
const randomImage =
offlineImages[photographer].photo[
Math.floor(Math.random() * offlineImages[photographer].photo.length)
];
const object = {
url: `./offline-images/${randomImage}.webp`,
type,
photoInfo: {
offline: true,
credit: photographer,
},
};
localStorage.setItem('currentBackground', JSON.stringify(object));
return object;
}

View File

@@ -0,0 +1,46 @@
/**
* It takes a gradient object and returns a style object
* @returns An object with two properties: type and style.
*/
export function gradientStyleBuilder({ type, angle, gradient }) {
// Note: Append the gradient for additional browser support.
const steps = gradient?.map((v) => `${v.colour} ${v.stop}%`);
const grad = `background: ${type}-gradient(${type === 'linear' ? `${angle}deg,` : ''}${steps})`;
return {
type: 'colour',
style: `background:${gradient[0]?.colour};${grad}`,
};
}
/**
* It gets the gradient settings from localStorage, parses it, and returns the gradient style.
* @returns A string.
*/
export function getGradient() {
const customBackgroundColour = localStorage.getItem('customBackgroundColour') || {
angle: '180',
gradient: [{ colour: '#ffb032', stop: 0 }],
type: 'linear',
};
let gradientSettings = '';
try {
gradientSettings = JSON.parse(customBackgroundColour);
} catch (e) {
const hexColorRegex = /#[0-9a-fA-F]{6}/s;
if (hexColorRegex.exec(customBackgroundColour)) {
// Colour used to be simply a hex colour or a NULL value before it was a JSON object. This automatically upgrades the hex colour value to the new standard. (NULL would not trigger an exception)
gradientSettings = {
type: 'linear',
angle: '180',
gradient: [{ colour: customBackgroundColour, stop: 0 }],
};
localStorage.setItem('customBackgroundColour', JSON.stringify(gradientSettings));
}
}
if (typeof gradientSettings === 'object' && gradientSettings !== null) {
return gradientStyleBuilder(gradientSettings);
}
}

View File

@@ -1,5 +1,5 @@
import rgbToHsv from './rgbToHsv';
import setRgba from './setRgba';
import { setRGBA } from './setRgba';
const hexRegexp = /(^#{0,1}[0-9A-F]{6}$)|(^#{0,1}[0-9A-F]{3}$)|(^#{0,1}[0-9A-F]{8}$)/i;
const regexp = /([0-9A-F])([0-9A-F])([0-9A-F])/i;
@@ -26,7 +26,7 @@ export default function hexToRgb(value) {
const blue = parseInt(value.substr(4, 2), 16);
const alpha = parseInt(value.substr(6, 2), 16) / 255;
const color = setRgba(red, green, blue, alpha);
const color = setRGBA(red, green, blue, alpha);
const hsv = rgbToHsv({ ...color });
return {

View File

@@ -0,0 +1,17 @@
import { getGradient, gradientStyleBuilder } from './getGradient';
import hexToRgb from './hexToRgb';
import rgbToHex from './rgbToHex';
import rgbToHsv from './rgbToHsv';
import { setRGBA, isValidRGBValue } from './setRgba';
export {
getGradient,
gradientStyleBuilder,
hexToRgb,
rgbToHex,
rgbToHsv,
setRGBA,
isValidRGBValue,
};

View File

@@ -3,7 +3,7 @@
* @param value - The value to check.
* @returns A function that takes a value and returns a boolean.
*/
const isValidRGBValue = (value) => {
export function isValidRGBValue(value) {
return typeof value === 'number' && Number.isNaN(value) === false && value >= 0 && value <= 255;
};
@@ -18,7 +18,7 @@ const isValidRGBValue = (value) => {
* @param alpha - The alpha value of the color.
* @returns An object with the properties red, green, blue, and alpha.
*/
export default function setRGBA(red, green, blue, alpha) {
export function setRGBA(red, green, blue, alpha) {
if (isValidRGBValue(red) && isValidRGBValue(green) && isValidRGBValue(blue)) {
const color = {
red: red | 0,

View File

@@ -0,0 +1,6 @@
import { supportsAVIF } from './avif';
import { offlineBackground } from './getOfflineImage';
import { randomColourStyleBuilder } from './randomColour';
import videoCheck from './videoCheck';
export { supportsAVIF, offlineBackground, randomColourStyleBuilder, videoCheck };

View File

@@ -0,0 +1,34 @@
/**
* It returns a random colour or random gradient as a style object
* @param type - The type of the style. This is used to determine which style builder to use.
* @returns An object with two properties: type and style.
*/
export function randomColourStyleBuilder(type) {
// randomColour based on https://stackoverflow.com/a/5092872
const randomColour = () =>
'#000000'.replace(/0/g, () => {
return (~~(Math.random() * 16)).toString(16);
});
let style = `background:${randomColour()};`;
if (type === 'random_gradient') {
const directions = [
'to right',
'to left',
'to bottom',
'to top',
'to bottom right',
'to bottom left',
'to top right',
'to top left',
];
style = `background:linear-gradient(${
directions[Math.floor(Math.random() * directions.length)]
}, ${randomColour()}, ${randomColour()});`;
}
return {
type: 'colour',
style,
};
}

View File

@@ -0,0 +1,13 @@
/**
* If the URL starts with `data:video/` or ends with `.mp4`, `.webm`, or `.ogg`, then it's a video.
* @param url - The URL of the file to be checked.
* @returns A function that takes a url and returns a boolean.
*/
export default function videoCheck(url) {
return (
url.startsWith('data:video/') ||
url.endsWith('.mp4') ||
url.endsWith('.webm') ||
url.endsWith('.ogg')
);
}

View File

@@ -0,0 +1,36 @@
{
"zoom": [
{ "value": 10, "label": "0.1x" },
{ "value": 100, "label": "1x" },
{ "value": 200, "label": "2x" },
{ "value": 400, "label": "4x" }
],
"toast": [
{ "value": 500, "label": "0.5s" },
{ "value": 1000, "label": "1s" },
{ "value": 1500, "label": "1.5s" },
{ "value": 2000, "label": "2s" },
{ "value": 2500, "label": "2.5s" },
{ "value": 3000, "label": "3s" },
{ "value": 4000, "label": "4s" },
{ "value": 5000, "label": "5s" }
],
"background": [
{ "value": 0, "label": "0%" },
{ "value": 25, "label": "25%" },
{ "value": 50, "label": "50%" },
{ "value": 75, "label": "75%" },
{ "value": 100, "label": "100%" }
],
"experimental": [
{ "value": 0, "label": "0s" },
{ "value": 500, "label": "0.5s" },
{ "value": 1000, "label": "1s" },
{ "value": 1500, "label": "1.5s" },
{ "value": 2000, "label": "2s" },
{ "value": 2500, "label": "2.5s" },
{ "value": 3000, "label": "3s" },
{ "value": 4000, "label": "4s" },
{ "value": 5000, "label": "5s" }
]
}

View File

@@ -1,3 +1,4 @@
// todo: maybe move stuff
/**
* If the number is between 3 and 20, return the number with the suffix "th". Otherwise, return the
* number with the suffix "st", "nd", "rd", or "th" depending on the last digit of the number

View File

@@ -1,132 +0,0 @@
// since there is so much code in the component, we have moved it to a separate file
import offlineImages from 'utils/data/offline_images.json';
/**
* If the URL starts with `data:video/` or ends with `.mp4`, `.webm`, or `.ogg`, then it's a video.
* @param url - The URL of the file to be checked.
* @returns A function that takes a url and returns a boolean.
*/
export function videoCheck(url) {
return (
url.startsWith('data:video/') ||
url.endsWith('.mp4') ||
url.endsWith('.webm') ||
url.endsWith('.ogg')
);
}
/**
* It gets a random photographer from the offlineImages.json file, then gets a random image from that
* photographer, and returns an object with the image's URL, type, and photoInfo.
* </code>
* @param type - 'background' or 'thumbnail'
* @returns An object with the following properties:
* url: A string that is the path to the image.
* type: A string that is the type of image.
* photoInfo: An object with the following properties:
* offline: A boolean that is true.
* credit: A string that is the name of the photographer.
*/
export function offlineBackground(type) {
const photographers = Object.keys(offlineImages);
const photographer = photographers[Math.floor(Math.random() * photographers.length)];
const randomImage =
offlineImages[photographer].photo[
Math.floor(Math.random() * offlineImages[photographer].photo.length)
];
const object = {
url: `./offline-images/${randomImage}.webp`,
type,
photoInfo: {
offline: true,
credit: photographer,
},
};
localStorage.setItem('currentBackground', JSON.stringify(object));
return object;
}
/**
* It takes a gradient object and returns a style object
* @returns An object with two properties: type and style.
*/
function gradientStyleBuilder({ type, angle, gradient }) {
// Note: Append the gradient for additional browser support.
const steps = gradient?.map((v) => `${v.colour} ${v.stop}%`);
const grad = `background: ${type}-gradient(${type === 'linear' ? `${angle}deg,` : ''}${steps})`;
return {
type: 'colour',
style: `background:${gradient[0]?.colour};${grad}`,
};
}
/**
* It gets the gradient settings from localStorage, parses it, and returns the gradient style.
* @returns A string.
*/
export function getGradient() {
const customBackgroundColour = localStorage.getItem('customBackgroundColour') || {
angle: '180',
gradient: [{ colour: '#ffb032', stop: 0 }],
type: 'linear',
};
let gradientSettings = '';
try {
gradientSettings = JSON.parse(customBackgroundColour);
} catch (e) {
const hexColorRegex = /#[0-9a-fA-F]{6}/s;
if (hexColorRegex.exec(customBackgroundColour)) {
// Colour used to be simply a hex colour or a NULL value before it was a JSON object. This automatically upgrades the hex colour value to the new standard. (NULL would not trigger an exception)
gradientSettings = {
type: 'linear',
angle: '180',
gradient: [{ colour: customBackgroundColour, stop: 0 }],
};
localStorage.setItem('customBackgroundColour', JSON.stringify(gradientSettings));
}
}
if (typeof gradientSettings === 'object' && gradientSettings !== null) {
return gradientStyleBuilder(gradientSettings);
}
}
/**
* It returns a random colour or random gradient as a style object
* @param type - The type of the style. This is used to determine which style builder to use.
* @returns An object with two properties: type and style.
*/
export function randomColourStyleBuilder(type) {
// randomColour based on https://stackoverflow.com/a/5092872
const randomColour = () =>
'#000000'.replace(/0/g, () => {
return (~~(Math.random() * 16)).toString(16);
});
let style = `background:${randomColour()};`;
if (type === 'random_gradient') {
const directions = [
'to right',
'to left',
'to bottom',
'to top',
'to bottom right',
'to bottom left',
'to top right',
'to top left',
];
style = `background:linear-gradient(${
directions[Math.floor(Math.random() * directions.length)]
}, ${randomColour()}, ${randomColour()});`;
}
return {
type: 'colour',
style,
};
}

View File

@@ -1,168 +0,0 @@
import EventBus from './eventbus';
function showReminder() {
document.querySelector('.reminder-info').style.display = 'flex';
localStorage.setItem('showReminder', true);
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// based on https://stackoverflow.com/questions/37684/how-to-replace-plain-urls-with-links
export function urlParser(input) {
const urlPattern =
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,63}\b([-a-zA-Z0-9()!@:%_+.~#?&//=]*)/g;
const emailPattern = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g;
const replaceUrl = (url) => `<br/><a class="link" href="${url}" target="_blank">${url}</a>`;
const replaceEmail = (email) => `<a class="link" href="mailto:${email}">${email}</a>`;
const replacedUrls = input.replace(urlPattern, replaceUrl);
const replacedEmails = replacedUrls.replace(emailPattern, replaceEmail);
return replacedEmails;
}
export function install(type, input, sideload) {
switch (type) {
case 'settings':
localStorage.removeItem('backup_settings');
let oldSettings = [];
Object.keys(localStorage).forEach((key) => {
oldSettings.push({
name: key,
value: localStorage.getItem(key),
});
});
localStorage.setItem('backup_settings', JSON.stringify(oldSettings));
Object.keys(input.settings).forEach((key) => {
localStorage.setItem(key, input.settings[key]);
});
showReminder();
break;
case 'photos':
const currentPhotos = JSON.parse(localStorage.getItem('photo_packs')) || [];
input.photos.forEach((photo) => {
currentPhotos.push(photo);
});
localStorage.setItem('photo_packs', JSON.stringify(currentPhotos));
if (localStorage.getItem('backgroundType') !== 'photo_pack') {
localStorage.setItem('oldBackgroundType', localStorage.getItem('backgroundType'));
}
localStorage.setItem('backgroundType', 'photo_pack');
localStorage.removeItem('backgroundchange');
EventBus.emit('refresh', 'background');
// TODO: make this legitimately good and work without a reload - currently we just refresh
sleep(4000);
window.location.reload();
break;
case 'quotes':
const currentQuotes = JSON.parse(localStorage.getItem('quote_packs')) || [];
input.quotes.forEach((quote) => {
currentQuotes.push(quote);
});
localStorage.setItem('quote_packs', JSON.stringify(currentQuotes));
if (localStorage.getItem('quoteType') !== 'quote_pack') {
localStorage.setItem('oldQuoteType', localStorage.getItem('quoteType'));
}
localStorage.setItem('quoteType', 'quote_pack');
localStorage.removeItem('quotechange');
EventBus.emit('refresh', 'quote');
break;
default:
break;
}
const installed = JSON.parse(localStorage.getItem('installed'));
if (sideload) {
installed.push({
content: {
updated: 'Unpublished',
data: input,
},
});
} else {
installed.push(input);
}
localStorage.setItem('installed', JSON.stringify(installed));
}
export function uninstall(type, name) {
let installedContents, packContents;
switch (type) {
case 'settings':
const oldSettings = JSON.parse(localStorage.getItem('backup_settings'));
localStorage.clear();
oldSettings.forEach((item) => {
localStorage.setItem(item.name, item.value);
});
showReminder();
break;
case 'quotes':
installedContents = JSON.parse(localStorage.getItem('quote_packs'));
packContents = JSON.parse(localStorage.getItem('installed')).find(
(content) => content.name === name,
);
installedContents.forEach((item, index) => {
const exists = packContents.quotes.find(
(content) => content.quote === item.quote || content.author === item.author,
);
if (exists !== undefined) {
installedContents.splice(index, 1);
}
});
localStorage.setItem('quote_packs', JSON.stringify(installedContents));
if (installedContents.length === 0) {
localStorage.setItem('quoteType', localStorage.getItem('oldQuoteType') || 'api');
localStorage.removeItem('oldQuoteType');
localStorage.removeItem('quote_packs');
}
localStorage.removeItem('quotechange');
EventBus.emit('refresh', 'marketplacequoteuninstall');
break;
case 'photos':
installedContents = JSON.parse(localStorage.getItem('photo_packs'));
packContents = JSON.parse(localStorage.getItem('installed')).find(
(content) => content.name === name,
);
installedContents.forEach((item, index) => {
const exists = packContents.photos.find((content) => content.photo === item.photo);
if (exists !== undefined) {
installedContents.splice(index, 1);
}
});
localStorage.setItem('photo_packs', JSON.stringify(installedContents));
if (installedContents.length === 0) {
localStorage.setItem('backgroundType', localStorage.getItem('oldBackgroundType') || 'api');
localStorage.removeItem('oldBackgroundType');
localStorage.removeItem('photo_packs');
}
localStorage.removeItem('backgroundchange');
EventBus.emit('refresh', 'marketplacebackgrounduninstall');
break;
default:
break;
}
let installed = JSON.parse(localStorage.getItem('installed'));
for (let i = 0; i < installed.length; i++) {
if (installed[i].name === name) {
installed.splice(i, 1);
break;
}
}
localStorage.setItem('installed', JSON.stringify(installed));
}

View File

@@ -1,199 +0,0 @@
import variables from 'config/variables';
import experimentalInit from '../experimental';
import defaultSettings from 'utils/data/default_settings.json';
import languages from '@/i18n/languages.json';
/**
* It sets the default settings for the extension
* @param reset - boolean
*/
export function setDefaultSettings(reset) {
localStorage.clear();
defaultSettings.forEach((element) => localStorage.setItem(element.name, element.value));
// Languages
const languageCodes = languages.map(({ value }) => value);
const browserLanguage =
(navigator.languages &&
navigator.languages.find((lang) => lang.replace('-', '_') && languageCodes.includes(lang))) ||
navigator.language.replace('-', '_');
if (languageCodes.includes(browserLanguage)) {
localStorage.setItem('language', browserLanguage);
} else {
localStorage.setItem('language', 'en_GB');
}
localStorage.setItem('tabName', variables.getMessage('tabname'));
if (reset) {
localStorage.setItem('showWelcome', false);
}
// finally we set this to true so it doesn't run the function on every load
localStorage.setItem('firstRun', true);
}
/**
* It loads the settings from localStorage and applies them to the page.
* @param hotreload - boolean
*/
export function loadSettings(hotreload) {
switch (localStorage.getItem('theme')) {
case 'dark':
document.body.classList.add('dark');
document.body.classList.remove('light');
break;
case 'auto':
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark');
} else {
document.body.classList.remove('dark');
document.body.classList.add('light');
}
break;
default:
document.body.classList.add('light');
document.body.classList.remove('dark');
}
document.title = localStorage.getItem('tabName') || variables.getMessage('tabname');
if (hotreload === true) {
// remove old custom stuff and add new
const custom = ['customcss', 'customfont'];
custom.forEach((element) => {
try {
document.head.removeChild(document.getElementById(element));
} catch (e) {
// Disregard exception if custom stuff doesn't exist
}
});
}
if (localStorage.getItem('animations') === 'false') {
document.body.classList.add('no-animations');
} else {
document.body.classList.remove('no-animations');
}
// technically, this is text SHADOW, and not BORDER
// however it's a mess and we'll just leave it at this for now
const textBorder = localStorage.getItem('textBorder');
// enable/disable old text border from before redesign
if (textBorder === 'true') {
const elements = ['greeting', 'clock', 'quote', 'quoteauthor', 'date'];
elements.forEach((element) => {
try {
document.querySelector('.' + element).classList.add('textBorder');
} catch (e) {
// Disregard exception
}
});
} else {
const elements = ['greeting', 'clock', 'quote', 'quoteauthor', 'date'];
elements.forEach((element) => {
try {
document.querySelector('.' + element).classList.remove('textBorder');
} catch (e) {
// Disregard exception
}
});
}
// remove actual default shadow
if (textBorder === 'none') {
document.getElementById('center').classList.add('no-textBorder');
} else {
document.getElementById('center').classList.remove('no-textBorder');
}
const css = localStorage.getItem('customcss');
if (css) {
document.head.insertAdjacentHTML('beforeend', '<style id="customcss">' + css + '</style>');
}
const font = localStorage.getItem('font');
if (font) {
let url = '';
if (localStorage.getItem('fontGoogle') === 'true') {
url = `@import url('https://fonts.googleapis.com/css2?family=${font}&display=swap');`;
}
document.head.insertAdjacentHTML(
'beforeend',
`
<style id='customfont'>
${url}
* {
font-family: '${font}', 'Lexend Deca', 'Montserrat', sans-serif !important;
font-weight: ${localStorage.getItem('fontweight')};
font-style: ${localStorage.getItem('fontstyle')};
}
</style>
`,
);
}
// If the extension got updated and the new app links default settings
// were not set, set them
if (localStorage.getItem('applinks') === null) {
localStorage.setItem('applinks', JSON.stringify([]));
}
if (localStorage.getItem('appsEnabled') === null) {
localStorage.setItem('showWelcome', false);
}
// everything below this shouldn't run on a hot reload event
if (hotreload === true) {
return;
}
if (localStorage.getItem('experimental') === 'true') {
experimentalInit();
}
// easter egg
console.log(`
█████████████████████████████████████████████████████████████
██ ██
██ ███ ███ ██ ██ ███████ ██
██ ████ ████ ██ ██ ██ ██
██ ██ ████ ██ ██ ██ █████ ██
██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██████ ███████ ██
██ ██
██ ██
██ Copyright 2018-${new Date().getFullYear()} The Mue Authors ██
██ GitHub: https://github.com/mue/mue ██
██ ██
██ Thank you for using Mue! ██
██ Feedback: hello@muetab.com ██
█████████████████████████████████████████████████████████████
`);
}
/**
* Saves all of the current settings, resets them, sets the defaults and then overrides
* the new settings with the old saved messages where they exist.
* @returns the result of the setDefaultSettings() function.
*/
export function moveSettings() {
const currentSettings = Object.keys(localStorage);
if (currentSettings.length === 0) {
return this.setDefaultSettings();
}
const settings = {};
currentSettings.forEach((key) => {
settings[key] = localStorage.getItem(key);
});
localStorage.clear();
setDefaultSettings();
Object.keys(settings).forEach((key) => {
localStorage.setItem(key, settings[key]);
});
}

View File

@@ -1,150 +0,0 @@
import variables from 'config/variables';
import { toast } from 'react-toastify';
/**
* It creates a link to a file, and then clicks it
* @param data - the data you want to save
* @param [filename=file] - the name of the file to be saved
* @param [type=text/json] - the type of file you want to save.
*/
export function saveFile(data, filename = 'file', type = 'text/json') {
if (typeof data === 'object') {
data = JSON.stringify(data, undefined, 4);
}
const blob = new Blob([data], { type });
const event = document.createEvent('MouseEvents');
const a = document.createElement('a');
a.href = window.URL.createObjectURL(blob);
a.download = filename;
a.dataset.downloadurl = [type, a.download, a.href].join(':');
// i need to see what all this actually does, i think wessel wrote this function
event.initMouseEvent(
'click',
true,
false,
window,
0,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null,
);
a.dispatchEvent(event);
}
/**
* It takes all the settings from localStorage and saves them to a file
*/
export function exportSettings() {
const settings = {};
Object.keys(localStorage).forEach((key) => {
settings[key] = localStorage.getItem(key);
});
let date = new Date();
// Format the date as YYYY-MM-DD_HH-MM-SS
let formattedDate = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}_${date.getHours().toString().padStart(2, '0')}-${date.getMinutes().toString().padStart(2, '0')}-${date.getSeconds().toString().padStart(2, '0')}`;
let filename = `mue_settings_backup_${formattedDate}.json`;
saveFile(settings, filename);
variables.stats.postEvent('tab', 'Settings exported');
}
/**
* It takes a JSON file of Mue settings, parses it, and then sets the localStorage values to the values in the
* file.
* @param e - The JSON settings string to import
*/
export function importSettings(e) {
const content = JSON.parse(e);
Object.keys(content).forEach((key) => {
localStorage.setItem(key, content[key]);
});
toast(variables.getMessage('toasts.imported'));
variables.stats.postEvent('tab', 'Settings imported');
}
/**
* It returns an array of objects with a value and label property for the Mue sliders.
* @param type - The type of slider you want to use.
* @returns An object with keys of either zoom, toast, background or experimental.
*/
export function values(type) {
const marks = {
zoom: [
{ value: 10, label: '0.1x' },
{ value: 100, label: '1x' },
{ value: 200, label: '2x' },
{ value: 400, label: '4x' },
],
toast: [
{ value: 500, label: '0.5s' },
{ value: 1000, label: '1s' },
{ value: 1500, label: '1.5s' },
{ value: 2000, label: '2s' },
{ value: 2500, label: '2.5s' },
{ value: 3000, label: '3s' },
{ value: 4000, label: '4s' },
{ value: 5000, label: '5s' },
],
background: [
{ value: 0, label: '0%' },
{ value: 25, label: '25%' },
{ value: 50, label: '50%' },
{ value: 75, label: '75%' },
{ value: 100, label: '100%' },
],
experimental: [
{ value: 0, label: '0s' },
{ value: 500, label: '0.5s' },
{ value: 1000, label: '1s' },
{ value: 1500, label: '1.5s' },
{ value: 2000, label: '2s' },
{ value: 2500, label: '2.5s' },
{ value: 3000, label: '3s' },
{ value: 4000, label: '4s' },
{ value: 5000, label: '5s' },
],
};
return marks[type] || [];
}
export async function getTitleFromUrl(url) {
let title;
try {
let response = await fetch(url);
if (response.redirected) {
response = await fetch(response.url);
}
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
title = doc.title;
} catch (e) {
title = url;
}
return title;
}
export function isValidUrl(url) {
// regex: https://ihateregex.io/expr/url/
// eslint-disable-next-line no-useless-escape
const urlRegex =
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._~#=]{1,256}\.[a-zA-Z0-9()]{1,63}\b([-a-zA-Z0-9()!@:%_.~#?&=]*)/;
return urlRegex.test(url);
}

View File

@@ -0,0 +1,17 @@
export async function getTitleFromUrl(url) {
let title;
try {
let response = await fetch(url);
if (response.redirected) {
response = await fetch(response.url);
}
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
title = doc.title;
} catch (e) {
title = url;
}
return title;
}

4
src/utils/links/index.js Normal file
View File

@@ -0,0 +1,4 @@
import { getTitleFromUrl } from "./getTitleFromUrl";
import { isValidUrl } from "./isValidUrl";
export { getTitleFromUrl, isValidUrl };

View File

@@ -0,0 +1,8 @@
export function isValidUrl(url) {
// regex: https://ihateregex.io/expr/url/
// eslint-disable-next-line no-useless-escape
const urlRegex =
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._~#=]{1,256}\.[a-zA-Z0-9()]{1,63}\b([-a-zA-Z0-9()!@:%_.~#?&=]*)/;
return urlRegex.test(url);
}

View File

@@ -0,0 +1,5 @@
import { install } from './install';
import { uninstall } from './uninstall';
import { urlParser } from './urlParser';
export { install, uninstall, urlParser };

View File

@@ -0,0 +1,84 @@
import EventBus from 'utils/eventbus';
// todo: relocate these 2 functions
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function showReminder() {
document.querySelector('.reminder-info').style.display = 'flex';
localStorage.setItem('showReminder', true);
}
export function install(type, input, sideload) {
switch (type) {
case 'settings':
localStorage.removeItem('backup_settings');
let oldSettings = [];
Object.keys(localStorage).forEach((key) => {
oldSettings.push({
name: key,
value: localStorage.getItem(key),
});
});
localStorage.setItem('backup_settings', JSON.stringify(oldSettings));
Object.keys(input.settings).forEach((key) => {
localStorage.setItem(key, input.settings[key]);
});
showReminder();
break;
case 'photos':
const currentPhotos = JSON.parse(localStorage.getItem('photo_packs')) || [];
input.photos.forEach((photo) => {
currentPhotos.push(photo);
});
localStorage.setItem('photo_packs', JSON.stringify(currentPhotos));
if (localStorage.getItem('backgroundType') !== 'photo_pack') {
localStorage.setItem('oldBackgroundType', localStorage.getItem('backgroundType'));
}
localStorage.setItem('backgroundType', 'photo_pack');
localStorage.removeItem('backgroundchange');
EventBus.emit('refresh', 'background');
// TODO: make this legitimately good and work without a reload - currently we just refresh
sleep(4000);
window.location.reload();
break;
case 'quotes':
const currentQuotes = JSON.parse(localStorage.getItem('quote_packs')) || [];
input.quotes.forEach((quote) => {
currentQuotes.push(quote);
});
localStorage.setItem('quote_packs', JSON.stringify(currentQuotes));
if (localStorage.getItem('quoteType') !== 'quote_pack') {
localStorage.setItem('oldQuoteType', localStorage.getItem('quoteType'));
}
localStorage.setItem('quoteType', 'quote_pack');
localStorage.removeItem('quotechange');
EventBus.emit('refresh', 'quote');
break;
default:
break;
}
const installed = JSON.parse(localStorage.getItem('installed'));
if (sideload) {
installed.push({
content: {
updated: 'Unpublished',
data: input,
},
});
} else {
installed.push(input);
}
localStorage.setItem('installed', JSON.stringify(installed));
}

View File

@@ -0,0 +1,78 @@
import EventBus from 'utils/eventbus';
// todo: relocate this function
function showReminder() {
document.querySelector('.reminder-info').style.display = 'flex';
localStorage.setItem('showReminder', true);
}
export function uninstall(type, name) {
let installedContents, packContents;
switch (type) {
case 'settings':
const oldSettings = JSON.parse(localStorage.getItem('backup_settings'));
localStorage.clear();
oldSettings.forEach((item) => {
localStorage.setItem(item.name, item.value);
});
showReminder();
break;
case 'quotes':
installedContents = JSON.parse(localStorage.getItem('quote_packs'));
packContents = JSON.parse(localStorage.getItem('installed')).find(
(content) => content.name === name,
);
installedContents.forEach((item, index) => {
const exists = packContents.quotes.find(
(content) => content.quote === item.quote || content.author === item.author,
);
if (exists !== undefined) {
installedContents.splice(index, 1);
}
});
localStorage.setItem('quote_packs', JSON.stringify(installedContents));
if (installedContents.length === 0) {
localStorage.setItem('quoteType', localStorage.getItem('oldQuoteType') || 'api');
localStorage.removeItem('oldQuoteType');
localStorage.removeItem('quote_packs');
}
localStorage.removeItem('quotechange');
EventBus.emit('refresh', 'marketplacequoteuninstall');
break;
case 'photos':
installedContents = JSON.parse(localStorage.getItem('photo_packs'));
packContents = JSON.parse(localStorage.getItem('installed')).find(
(content) => content.name === name,
);
installedContents.forEach((item, index) => {
const exists = packContents.photos.find((content) => content.photo === item.photo);
if (exists !== undefined) {
installedContents.splice(index, 1);
}
});
localStorage.setItem('photo_packs', JSON.stringify(installedContents));
if (installedContents.length === 0) {
localStorage.setItem('backgroundType', localStorage.getItem('oldBackgroundType') || 'api');
localStorage.removeItem('oldBackgroundType');
localStorage.removeItem('photo_packs');
}
localStorage.removeItem('backgroundchange');
EventBus.emit('refresh', 'marketplacebackgrounduninstall');
break;
default:
break;
}
let installed = JSON.parse(localStorage.getItem('installed'));
for (let i = 0; i < installed.length; i++) {
if (installed[i].name === name) {
installed.splice(i, 1);
break;
}
}
localStorage.setItem('installed', JSON.stringify(installed));
}

View File

@@ -0,0 +1,13 @@
// based on https://stackoverflow.com/questions/37684/how-to-replace-plain-urls-with-links
export function urlParser(input) {
const urlPattern =
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,63}\b([-a-zA-Z0-9()!@:%_+.~#?&//=]*)/g;
const emailPattern = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g;
const replaceUrl = (url) => `<br/><a class="link" href="${url}" target="_blank">${url}</a>`;
const replaceEmail = (email) => `<a class="link" href="mailto:${email}">${email}</a>`;
const replacedUrls = input.replace(urlPattern, replaceUrl);
const replacedEmails = replacedUrls.replace(emailPattern, replaceEmail);
return replacedEmails;
}

40
src/utils/saveFile.js Normal file
View File

@@ -0,0 +1,40 @@
/**
* It creates a link to a file, and then clicks it
* @param data - the data you want to save
* @param [filename=file] - the name of the file to be saved
* @param [type=text/json] - the type of file you want to save.
*/
export function saveFile(data, filename = 'file', type = 'text/json') {
if (typeof data === 'object') {
data = JSON.stringify(data, undefined, 4);
}
const blob = new Blob([data], { type });
const event = document.createEvent('MouseEvents');
const a = document.createElement('a');
a.href = window.URL.createObjectURL(blob);
a.download = filename;
a.dataset.downloadurl = [type, a.download, a.href].join(':');
// i need to see what all this actually does, i think wessel wrote this function
event.initMouseEvent(
'click',
true,
false,
window,
0,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null,
);
a.dispatchEvent(event);
}

View File

@@ -0,0 +1,34 @@
import defaultSettings from 'utils/data/default_settings.json';
import languages from 'i18n/languages.json';
import variables from 'config/variables';
/**
* It sets the default settings for the extension
* @param reset - boolean
*/
export function setDefaultSettings(reset) {
localStorage.clear();
defaultSettings.forEach((element) => localStorage.setItem(element.name, element.value));
// Languages
const languageCodes = languages.map(({ value }) => value);
const browserLanguage =
(navigator.languages &&
navigator.languages.find((lang) => lang.replace('-', '_') && languageCodes.includes(lang))) ||
navigator.language.replace('-', '_');
if (languageCodes.includes(browserLanguage)) {
localStorage.setItem('language', browserLanguage);
} else {
localStorage.setItem('language', 'en_GB');
}
localStorage.setItem('tabName', variables.getMessage('tabname'));
if (reset) {
localStorage.setItem('showWelcome', false);
}
// finally we set this to true so it doesn't run the function on every load
localStorage.setItem('firstRun', true);
}

View File

@@ -0,0 +1,20 @@
import { saveFile } from 'utils/saveFile';
import variables from 'config/variables';
/**
* It takes all the settings from localStorage and saves them to a file
*/
export function exportSettings() {
const settings = {};
Object.keys(localStorage).forEach((key) => {
settings[key] = localStorage.getItem(key);
});
let date = new Date();
// Format the date as YYYY-MM-DD_HH-MM-SS
let formattedDate = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}_${date.getHours().toString().padStart(2, '0')}-${date.getMinutes().toString().padStart(2, '0')}-${date.getSeconds().toString().padStart(2, '0')}`;
let filename = `mue_settings_backup_${formattedDate}.json`;
saveFile(settings, filename);
variables.stats.postEvent('tab', 'Settings exported');
}

View File

@@ -0,0 +1,18 @@
import { toast } from 'react-toastify';
import variables from 'config/variables';
/**
* It takes a JSON file of Mue settings, parses it, and then sets the localStorage values to the values in the
* file.
* @param e - The JSON settings string to import
*/
export function importSettings(e) {
const content = JSON.parse(e);
Object.keys(content).forEach((key) => {
localStorage.setItem(key, content[key]);
});
toast(variables.getMessage('toasts.imported'));
variables.stats.postEvent('tab', 'Settings imported');
}

View File

@@ -0,0 +1,7 @@
import { setDefaultSettings } from './default';
import { exportSettings } from './export';
import { importSettings } from './import';
import { loadSettings } from './load';
import { moveSettings } from './move';
export { setDefaultSettings, exportSettings, importSettings, loadSettings, moveSettings };

141
src/utils/settings/load.js Normal file
View File

@@ -0,0 +1,141 @@
import variables from "config/variables";
import ExperimentalInit from "utils/experimental";
/**
* It loads the settings from localStorage and applies them to the page.
* @param hotreload - boolean
*/
export function loadSettings(hotreload) {
switch (localStorage.getItem('theme')) {
case 'dark':
document.body.classList.add('dark');
document.body.classList.remove('light');
break;
case 'auto':
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark');
} else {
document.body.classList.remove('dark');
document.body.classList.add('light');
}
break;
default:
document.body.classList.add('light');
document.body.classList.remove('dark');
}
document.title = localStorage.getItem('tabName') || variables.getMessage('tabname');
if (hotreload === true) {
// remove old custom stuff and add new
const custom = ['customcss', 'customfont'];
custom.forEach((element) => {
try {
document.head.removeChild(document.getElementById(element));
} catch (e) {
// Disregard exception if custom stuff doesn't exist
}
});
}
if (localStorage.getItem('animations') === 'false') {
document.body.classList.add('no-animations');
} else {
document.body.classList.remove('no-animations');
}
// technically, this is text SHADOW, and not BORDER
// however it's a mess and we'll just leave it at this for now
const textBorder = localStorage.getItem('textBorder');
// enable/disable old text border from before redesign
if (textBorder === 'true') {
const elements = ['greeting', 'clock', 'quote', 'quoteauthor', 'date'];
elements.forEach((element) => {
try {
document.querySelector('.' + element).classList.add('textBorder');
} catch (e) {
// Disregard exception
}
});
} else {
const elements = ['greeting', 'clock', 'quote', 'quoteauthor', 'date'];
elements.forEach((element) => {
try {
document.querySelector('.' + element).classList.remove('textBorder');
} catch (e) {
// Disregard exception
}
});
}
// remove actual default shadow
if (textBorder === 'none') {
document.getElementById('center').classList.add('no-textBorder');
} else {
document.getElementById('center').classList.remove('no-textBorder');
}
const css = localStorage.getItem('customcss');
if (css) {
document.head.insertAdjacentHTML('beforeend', '<style id="customcss">' + css + '</style>');
}
const font = localStorage.getItem('font');
if (font) {
let url = '';
if (localStorage.getItem('fontGoogle') === 'true') {
url = `@import url('https://fonts.googleapis.com/css2?family=${font}&display=swap');`;
}
document.head.insertAdjacentHTML(
'beforeend',
`
<style id='customfont'>
${url}
* {
font-family: '${font}', 'Lexend Deca', 'Montserrat', sans-serif !important;
font-weight: ${localStorage.getItem('fontweight')};
font-style: ${localStorage.getItem('fontstyle')};
}
</style>
`,
);
}
// If the extension got updated and the new app links default settings
// were not set, set them
if (localStorage.getItem('applinks') === null) {
localStorage.setItem('applinks', JSON.stringify([]));
}
if (localStorage.getItem('appsEnabled') === null) {
localStorage.setItem('showWelcome', false);
}
// everything below this shouldn't run on a hot reload event
if (hotreload === true) {
return;
}
if (localStorage.getItem('experimental') === 'true') {
ExperimentalInit();
}
// easter egg
console.log(`
█████████████████████████████████████████████████████████████
██ ██
██ ███ ███ ██ ██ ███████ ██
██ ████ ████ ██ ██ ██ ██
██ ██ ████ ██ ██ ██ █████ ██
██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██████ ███████ ██
██ ██
██ ██
██ Copyright 2018-${new Date().getFullYear()} The Mue Authors ██
██ GitHub: https://github.com/mue/mue ██
██ ██
██ Thank you for using Mue! ██
██ Feedback: hello@muetab.com ██
█████████████████████████████████████████████████████████████
`);
}

View File

@@ -0,0 +1,25 @@
import { setDefaultSettings } from "./default";
/**
* Saves all of the current settings, resets them, sets the defaults and then overrides
* the new settings with the old saved messages where they exist.
* @returns the result of the setDefaultSettings() function.
*/
export function moveSettings() {
const currentSettings = Object.keys(localStorage);
if (currentSettings.length === 0) {
return this.setDefaultSettings();
}
const settings = {};
currentSettings.forEach((key) => {
settings[key] = localStorage.getItem(key);
});
localStorage.clear();
setDefaultSettings();
Object.keys(settings).forEach((key) => {
localStorage.setItem(key, settings[key]);
});
}

View File

@@ -1,60 +0,0 @@
import I18n from '@eartharoid/i18n';
import * as de_DE from 'translations/de_DE.json';
import * as en_GB from 'translations/en_GB.json';
import * as en_US from 'translations/en_US.json';
import * as es from 'translations/es.json';
import * as es_419 from 'translations/es_419.json';
import * as fr from 'translations/fr.json';
import * as nl from 'translations/nl.json';
import * as no from 'translations/no.json';
import * as ru from 'translations/ru.json';
import * as zh_CN from 'translations/zh_CN.json';
import * as id_ID from 'translations/id_ID.json';
import * as tr_TR from 'translations/tr_TR.json';
import * as pt_BR from 'translations/pt_BR.json';
import * as bn from 'translations/bn.json';
/**
* Initialise the i18n object.
* The i18n object is then returned.
* @param locale - The locale to use.
* @returns The i18n object.
*/
export function initTranslations(locale) {
const i18n = new I18n(locale, {
de_DE,
en_GB,
en_US,
es,
es_419,
fr,
nl,
no,
ru,
zh_CN,
id_ID,
tr_TR,
pt_BR,
bn,
});
return i18n;
}
export const translations = {
de_DE,
en_GB,
en_US,
es,
es_419,
fr,
nl,
no,
ru,
zh_CN,
id_ID,
tr_TR,
pt_BR,
bn,
};