mirror of https://github.com/mue/mue.git
[New Feature] Created a "Apps" section to add more frequently visited website links like in Chrome. (#622)
* added apps links section to the navbar with settings to add and edit links in under navbar settings * translated to english US
This commit is contained in:
parent
59e721d663
commit
0d77508f4c
|
@ -10,6 +10,8 @@ import EventBus from 'modules/helpers/eventbus';
|
||||||
|
|
||||||
import Welcome from './welcome/Welcome';
|
import Welcome from './welcome/Welcome';
|
||||||
|
|
||||||
|
import Apps from './apps/Apps';
|
||||||
|
|
||||||
export default class Modals extends PureComponent {
|
export default class Modals extends PureComponent {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
@ -17,6 +19,7 @@ export default class Modals extends PureComponent {
|
||||||
mainModal: false,
|
mainModal: false,
|
||||||
updateModal: false,
|
updateModal: false,
|
||||||
welcomeModal: false,
|
welcomeModal: false,
|
||||||
|
appsModal: false,
|
||||||
preview: false,
|
preview: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -75,6 +78,9 @@ export default class Modals extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const navZoom = localStorage.getItem('zoomNavbar');
|
||||||
|
const appsInfo = JSON.parse(localStorage.getItem('applinks'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{this.state.welcomeModal === false && (
|
{this.state.welcomeModal === false && (
|
||||||
|
@ -102,6 +108,27 @@ export default class Modals extends PureComponent {
|
||||||
>
|
>
|
||||||
<Welcome modalClose={() => this.closeWelcome()} modalSkip={() => this.previewWelcome()} />
|
<Welcome modalClose={() => this.closeWelcome()} modalSkip={() => this.previewWelcome()} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
closeTimeoutMS={300}
|
||||||
|
onRequestClose={() => this.toggleModal('appsModal', false)}
|
||||||
|
isOpen={this.state.appsModal}
|
||||||
|
className="Modal appsmodal"
|
||||||
|
overlayClassName="Overlay"
|
||||||
|
shouldCloseOnOverlayClick={true}
|
||||||
|
ariaHideApp={false}
|
||||||
|
style={{
|
||||||
|
content: {
|
||||||
|
position: 'absolute',
|
||||||
|
right: '1rem',
|
||||||
|
top: `calc(1rem + ${55 + Math.ceil((navZoom / 20) * (navZoom * 0.01))}px)`,
|
||||||
|
overflow: 'visible',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Apps appsInfo={appsInfo} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{this.state.preview && <Preview setup={() => window.location.reload()} />}
|
{this.state.preview && <Preview setup={() => window.location.reload()} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import Tooltip from 'components/helpers/tooltip/Tooltip';
|
||||||
|
import './scss/index.scss';
|
||||||
|
import { MdLinkOff } from 'react-icons/md';
|
||||||
|
|
||||||
|
const Apps = ({ appsInfo }) => {
|
||||||
|
return (
|
||||||
|
<div className="appsShortcutContainer">
|
||||||
|
{appsInfo.length > 0 ? (
|
||||||
|
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 className="noAppsContainer">
|
||||||
|
<h3>
|
||||||
|
No app links found
|
||||||
|
<MdLinkOff />
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Apps;
|
|
@ -0,0 +1,77 @@
|
||||||
|
@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 {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -170,12 +170,6 @@ h5 {
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
.link {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
gap: 15px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.marketplaceCondition {
|
.marketplaceCondition {
|
||||||
|
|
|
@ -2,16 +2,113 @@ import variables from 'modules/variables';
|
||||||
|
|
||||||
import { useState, memo } from 'react';
|
import { useState, memo } from 'react';
|
||||||
|
|
||||||
|
import Modal from 'react-modal';
|
||||||
|
import { MdAddLink } from 'react-icons/md';
|
||||||
|
|
||||||
|
import AddModal from './quicklinks/AddModal';
|
||||||
|
|
||||||
import Checkbox from '../Checkbox';
|
import Checkbox from '../Checkbox';
|
||||||
import Dropdown from '../Dropdown';
|
import Dropdown from '../Dropdown';
|
||||||
|
|
||||||
import SettingsItem from '../SettingsItem';
|
import SettingsItem from '../SettingsItem';
|
||||||
import Header from '../Header';
|
import Header from '../Header';
|
||||||
|
import { getTitleFromUrl, isValidUrl } from './utils/utils';
|
||||||
|
import QuickLink from './quicklinks/QuickLink';
|
||||||
|
|
||||||
function Navbar() {
|
function Navbar() {
|
||||||
const [showRefreshOptions, setShowRefreshOptions] = useState(
|
const [showRefreshOptions, setShowRefreshOptions] = useState(
|
||||||
localStorage.getItem('refresh') === 'true',
|
localStorage.getItem('refresh') === 'true',
|
||||||
);
|
);
|
||||||
|
const [appsModalInfo, setAppsModalInfo] = useState({
|
||||||
|
newLink: false,
|
||||||
|
edit: false,
|
||||||
|
items: JSON.parse(localStorage.getItem('applinks')),
|
||||||
|
urlError: '',
|
||||||
|
iconError: '',
|
||||||
|
editData: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const addLink = async (name, url, icon) => {
|
||||||
|
const data = JSON.parse(localStorage.getItem('applinks'));
|
||||||
|
|
||||||
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||||
|
url = 'http://' + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.length <= 0 || isValidUrl(url) === false) {
|
||||||
|
return setAppsModalInfo((oldState) => ({
|
||||||
|
...oldState,
|
||||||
|
urlError: variables.getMessage('widgets.quicklinks.url_error'),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (icon.length > 0 && isValidUrl(icon) === false) {
|
||||||
|
return this.setState((oldState) => ({
|
||||||
|
...oldState,
|
||||||
|
iconError: variables.getMessage('widgets.quicklinks.url_error'),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
data.push({
|
||||||
|
name: name || (await getTitleFromUrl(url)),
|
||||||
|
url,
|
||||||
|
icon: icon || '',
|
||||||
|
key: Math.random().toString(36).substring(7) + 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.setItem('applinks', JSON.stringify(data));
|
||||||
|
|
||||||
|
setAppsModalInfo({
|
||||||
|
newLink: false,
|
||||||
|
edit: false,
|
||||||
|
items: data,
|
||||||
|
urlError: '',
|
||||||
|
iconError: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
variables.stats.postEvent('feature', 'App link add');
|
||||||
|
};
|
||||||
|
|
||||||
|
const startEditLink = (data) => {
|
||||||
|
setAppsModalInfo((oldState) => ({
|
||||||
|
...oldState,
|
||||||
|
edit: true,
|
||||||
|
editData: data,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const editLink = async (og, name, url, icon) => {
|
||||||
|
const data = JSON.parse(localStorage.getItem('applinks'));
|
||||||
|
const dataobj = data.find((i) => i.key === og.key);
|
||||||
|
dataobj.name = name || (await getTitleFromUrl(url));
|
||||||
|
dataobj.url = url;
|
||||||
|
dataobj.icon = icon || '';
|
||||||
|
|
||||||
|
localStorage.setItem('applinks', JSON.stringify(data));
|
||||||
|
|
||||||
|
setAppsModalInfo((oldState) => ({
|
||||||
|
...oldState,
|
||||||
|
items: data,
|
||||||
|
edit: false,
|
||||||
|
newLink: false,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteLink = (key, event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// remove link from array
|
||||||
|
const data = JSON.parse(localStorage.getItem('applinks')).filter((i) => i.key !== key);
|
||||||
|
|
||||||
|
localStorage.setItem('applinks', JSON.stringify(data));
|
||||||
|
|
||||||
|
setAppsModalInfo((oldState) => ({
|
||||||
|
...oldState,
|
||||||
|
items: data,
|
||||||
|
}));
|
||||||
|
|
||||||
|
variables.stats.postEvent('feature', 'App link delete');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -27,7 +124,7 @@ function Navbar() {
|
||||||
subtitle={variables.getMessage(
|
subtitle={variables.getMessage(
|
||||||
'modals.main.settings.sections.appearance.navbar.additional',
|
'modals.main.settings.sections.appearance.navbar.additional',
|
||||||
)}
|
)}
|
||||||
final={!showRefreshOptions}
|
final={false}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name="navbarHover"
|
name="navbarHover"
|
||||||
|
@ -55,6 +152,12 @@ function Navbar() {
|
||||||
text={variables.getMessage('widgets.navbar.todo.title')}
|
text={variables.getMessage('widgets.navbar.todo.title')}
|
||||||
category="navbar"
|
category="navbar"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
name="appsEnabled"
|
||||||
|
text={variables.getMessage('widgets.navbar.apps.title')}
|
||||||
|
category="navbar"
|
||||||
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
{showRefreshOptions && (
|
{showRefreshOptions && (
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
|
@ -62,7 +165,7 @@ function Navbar() {
|
||||||
subtitle={variables.getMessage(
|
subtitle={variables.getMessage(
|
||||||
'modals.main.settings.sections.appearance.navbar.refresh_subtitle',
|
'modals.main.settings.sections.appearance.navbar.refresh_subtitle',
|
||||||
)}
|
)}
|
||||||
final={true}
|
final={false}
|
||||||
>
|
>
|
||||||
<Dropdown name="refreshOption" category="navbar">
|
<Dropdown name="refreshOption" category="navbar">
|
||||||
<option value="page">
|
<option value="page">
|
||||||
|
@ -83,6 +186,52 @@ function Navbar() {
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SettingsItem
|
||||||
|
title={variables.getMessage('widgets.navbar.apps.title')}
|
||||||
|
subtitle={variables.getMessage(
|
||||||
|
'modals.main.settings.sections.appearance.navbar.apps_subtitle',
|
||||||
|
)}
|
||||||
|
final={true}
|
||||||
|
>
|
||||||
|
<button onClick={() => setAppsModalInfo((oldState) => ({ ...oldState, newLink: true }))}>
|
||||||
|
{variables.getMessage('modals.main.settings.sections.quicklinks.add_link')}
|
||||||
|
<MdAddLink />
|
||||||
|
</button>
|
||||||
|
</SettingsItem>
|
||||||
|
|
||||||
|
<div className="messagesContainer">
|
||||||
|
{appsModalInfo.items.map((item, i) => (
|
||||||
|
<QuickLink
|
||||||
|
key={i}
|
||||||
|
item={item}
|
||||||
|
startEditLink={() => startEditLink(item)}
|
||||||
|
deleteLink={(key, e) => deleteLink(key, e)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
closeTimeoutMS={100}
|
||||||
|
onRequestClose={() =>
|
||||||
|
setAppsModalInfo((oldState) => ({ ...oldState, newLink: false, edit: false }))
|
||||||
|
}
|
||||||
|
isOpen={appsModalInfo.edit || appsModalInfo.newLink}
|
||||||
|
className="Modal resetmodal mainModal"
|
||||||
|
overlayClassName="Overlay resetoverlay"
|
||||||
|
ariaHideApp={false}
|
||||||
|
>
|
||||||
|
<AddModal
|
||||||
|
urlError={appsModalInfo.urlError}
|
||||||
|
addLink={(name, url, icon) => addLink(name, url, icon)}
|
||||||
|
editLink={(og, name, url, icon) => editLink(og, name, url, icon)}
|
||||||
|
edit={appsModalInfo.edit}
|
||||||
|
editData={appsModalInfo.editData}
|
||||||
|
closeModal={() =>
|
||||||
|
setAppsModalInfo((oldState) => ({ ...oldState, newLink: false, edit: false }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@ import SettingsItem from '../SettingsItem';
|
||||||
import AddModal from './quicklinks/AddModal';
|
import AddModal from './quicklinks/AddModal';
|
||||||
|
|
||||||
import EventBus from 'modules/helpers/eventbus';
|
import EventBus from 'modules/helpers/eventbus';
|
||||||
|
import QuickLink from './quicklinks/QuickLink';
|
||||||
|
import { getTitleFromUrl, isValidUrl } from './utils/utils';
|
||||||
|
|
||||||
export default class QuickLinks extends PureComponent {
|
export default class QuickLinks extends PureComponent {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -42,28 +44,24 @@ export default class QuickLinks extends PureComponent {
|
||||||
async addLink(name, url, icon) {
|
async addLink(name, url, icon) {
|
||||||
const data = JSON.parse(localStorage.getItem('quicklinks'));
|
const data = JSON.parse(localStorage.getItem('quicklinks'));
|
||||||
|
|
||||||
// regex: https://ihateregex.io/expr/url/
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||||
// eslint-disable-next-line no-useless-escape
|
url = 'http://' + url;
|
||||||
const urlRegex =
|
}
|
||||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_.~#?&=]*)/;
|
|
||||||
if (url.length <= 0 || urlRegex.test(url) === false) {
|
if (url.length <= 0 || isValidUrl(url) === false) {
|
||||||
return this.setState({
|
return this.setState({
|
||||||
urlError: variables.getMessage('widgets.quicklinks.url_error'),
|
urlError: variables.getMessage('widgets.quicklinks.url_error'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (icon.length > 0 && urlRegex.test(icon) === false) {
|
if (icon.length > 0 && isValidUrl(icon) === false) {
|
||||||
return this.setState({
|
return this.setState({
|
||||||
iconError: variables.getMessage('widgets.quicklinks.url_error'),
|
iconError: variables.getMessage('widgets.quicklinks.url_error'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
||||||
url = 'http://' + url;
|
|
||||||
}
|
|
||||||
|
|
||||||
data.push({
|
data.push({
|
||||||
name: name || (await this.getTitle(url)),
|
name: name || (await getTitleFromUrl(url)),
|
||||||
url,
|
url,
|
||||||
icon: icon || '',
|
icon: icon || '',
|
||||||
key: Math.random().toString(36).substring(7) + 1,
|
key: Math.random().toString(36).substring(7) + 1,
|
||||||
|
@ -92,7 +90,7 @@ export default class QuickLinks extends PureComponent {
|
||||||
async editLink(og, name, url, icon) {
|
async editLink(og, name, url, icon) {
|
||||||
const data = JSON.parse(localStorage.getItem('quicklinks'));
|
const data = JSON.parse(localStorage.getItem('quicklinks'));
|
||||||
const dataobj = data.find((i) => i.key === og.key);
|
const dataobj = data.find((i) => i.key === og.key);
|
||||||
dataobj.name = name || (await this.getTitle(url));
|
dataobj.name = name || (await getTitleFromUrl(url));
|
||||||
dataobj.url = url;
|
dataobj.url = url;
|
||||||
dataobj.icon = icon || '';
|
dataobj.icon = icon || '';
|
||||||
|
|
||||||
|
@ -105,24 +103,6 @@ export default class QuickLinks extends PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTitle(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;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
EventBus.on('refresh', (data) => {
|
EventBus.on('refresh', (data) => {
|
||||||
if (data === 'quicklinks') {
|
if (data === 'quicklinks') {
|
||||||
|
@ -144,77 +124,6 @@ export default class QuickLinks extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let target,
|
|
||||||
rel = null;
|
|
||||||
if (localStorage.getItem('quicklinksnewtab') === 'true') {
|
|
||||||
target = '_blank';
|
|
||||||
rel = 'noopener noreferrer';
|
|
||||||
}
|
|
||||||
|
|
||||||
const useText = localStorage.getItem('quicklinksText') === 'true';
|
|
||||||
|
|
||||||
const quickLink = (item) => {
|
|
||||||
if (useText) {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
className="quicklinkstext"
|
|
||||||
key={item.key}
|
|
||||||
onContextMenu={(e) => this.deleteLink(item.key, e)}
|
|
||||||
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://', '');
|
|
||||||
|
|
||||||
const link = (
|
|
||||||
<div className="messageMap" key={item.key}>
|
|
||||||
<div className="icon">
|
|
||||||
<img
|
|
||||||
src={img}
|
|
||||||
alt={item.name}
|
|
||||||
draggable={false}
|
|
||||||
style={{ height: '30px', width: '30px' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="messageText">
|
|
||||||
<div className="title">{item.name}</div>
|
|
||||||
<div className="subtitle">
|
|
||||||
<a
|
|
||||||
className="quicklinknostyle"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
href={item.url}
|
|
||||||
>
|
|
||||||
{item.url}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="messageAction">
|
|
||||||
<button className="deleteButton" onClick={() => this.startEditLink(item)}>
|
|
||||||
{variables.getMessage('modals.main.settings.sections.quicklinks.edit')}
|
|
||||||
<MdEdit />
|
|
||||||
</button>
|
|
||||||
<button className="deleteButton" onClick={(e) => this.deleteLink(item.key, e)}>
|
|
||||||
{variables.getMessage('modals.main.marketplace.product.buttons.remove')}
|
|
||||||
<MdCancel />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return link;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header
|
<Header
|
||||||
|
@ -292,7 +201,14 @@ export default class QuickLinks extends PureComponent {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="messagesContainer" ref={this.quicklinksContainer}>
|
<div className="messagesContainer" ref={this.quicklinksContainer}>
|
||||||
{this.state.items.map((item) => quickLink(item))}
|
{this.state.items.map((item, i) => (
|
||||||
|
<QuickLink
|
||||||
|
key={i}
|
||||||
|
item={item}
|
||||||
|
startEditLink={() => this.startEditLink(item)}
|
||||||
|
deleteLink={(key, e) => this.deleteLink(key, e)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Modal
|
<Modal
|
||||||
closeTimeoutMS={100}
|
closeTimeoutMS={100}
|
||||||
|
@ -308,7 +224,9 @@ export default class QuickLinks extends PureComponent {
|
||||||
editLink={(og, name, url, icon) => this.editLink(og, name, url, icon)}
|
editLink={(og, name, url, icon) => this.editLink(og, name, url, icon)}
|
||||||
edit={this.state.edit}
|
edit={this.state.edit}
|
||||||
editData={this.state.editData}
|
editData={this.state.editData}
|
||||||
closeModal={() => this.setState({ showAddModal: false, urlError: '', iconError: '' })}
|
closeModal={() =>
|
||||||
|
this.setState({ showAddModal: false, urlError: '', iconError: '', edit: false })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
import variables from 'modules/variables';
|
||||||
|
|
||||||
|
import { MdEdit, MdCancel } from 'react-icons/md';
|
||||||
|
|
||||||
|
const QuickLink = ({ item, deleteLink, startEditLink }) => {
|
||||||
|
let target,
|
||||||
|
rel = null;
|
||||||
|
if (localStorage.getItem('quicklinksnewtab') === 'true') {
|
||||||
|
target = '_blank';
|
||||||
|
rel = 'noopener noreferrer';
|
||||||
|
}
|
||||||
|
|
||||||
|
const useText = localStorage.getItem('quicklinksText') === 'true';
|
||||||
|
|
||||||
|
if (useText) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className="quicklinkstext"
|
||||||
|
onContextMenu={(e) => deleteLink(item.key, e)}
|
||||||
|
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://', '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="messageMap">
|
||||||
|
<div className="icon">
|
||||||
|
<img
|
||||||
|
src={img}
|
||||||
|
alt={item.name}
|
||||||
|
draggable={false}
|
||||||
|
style={{ height: '30px', width: '30px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="messageText">
|
||||||
|
<div className="title">{item.name}</div>
|
||||||
|
<div className="subtitle">
|
||||||
|
<a className="quicklinknostyle" target="_blank" rel="noopener noreferrer" href={item.url}>
|
||||||
|
{item.url}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="messageAction">
|
||||||
|
<button className="deleteButton" onClick={() => startEditLink(item)}>
|
||||||
|
{variables.getMessage('modals.main.settings.sections.quicklinks.edit')}
|
||||||
|
<MdEdit />
|
||||||
|
</button>
|
||||||
|
<button className="deleteButton" onClick={(e) => deleteLink(item.key, e)}>
|
||||||
|
{variables.getMessage('modals.main.marketplace.product.buttons.remove')}
|
||||||
|
<MdCancel />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QuickLink;
|
|
@ -0,0 +1,28 @@
|
||||||
|
const getTitleFromUrl = async (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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const 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,6}\b([-a-zA-Z0-9()!@:%_.~#?&=]*)/;
|
||||||
|
|
||||||
|
return urlRegex.test(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getTitleFromUrl, isValidUrl };
|
|
@ -206,7 +206,6 @@ function PhotoInformation({ info, url, api }) {
|
||||||
>
|
>
|
||||||
<div className={photoMapClassList}>
|
<div className={photoMapClassList}>
|
||||||
{useMapIcon || photoMap() === null ? <MdLocationOn /> : ''}
|
{useMapIcon || photoMap() === null ? <MdLocationOn /> : ''}
|
||||||
<h1>{photoMap}</h1>
|
|
||||||
{photoMap()}
|
{photoMap()}
|
||||||
</div>
|
</div>
|
||||||
{showingPhotoMap && (
|
{showingPhotoMap && (
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import variables from 'modules/variables';
|
import variables from 'modules/variables';
|
||||||
import { PureComponent, createRef } from 'react';
|
import { PureComponent, createRef } from 'react';
|
||||||
|
|
||||||
import { MdRefresh, MdSettings } from 'react-icons/md';
|
import { MdRefresh, MdSettings, MdOutlineApps } from 'react-icons/md';
|
||||||
|
|
||||||
import Notes from './Notes';
|
import Notes from './Notes';
|
||||||
import Todo from './Todo';
|
import Todo from './Todo';
|
||||||
|
@ -21,6 +21,7 @@ class Navbar extends PureComponent {
|
||||||
refreshText: '',
|
refreshText: '',
|
||||||
refreshEnabled: localStorage.getItem('refresh'),
|
refreshEnabled: localStorage.getItem('refresh'),
|
||||||
refreshOption: localStorage.getItem('refreshOption') || '',
|
refreshOption: localStorage.getItem('refreshOption') || '',
|
||||||
|
appsOpen: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,6 +124,21 @@ class Navbar extends PureComponent {
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{localStorage.getItem('appsEnabled') === 'true' && (
|
||||||
|
<>
|
||||||
|
<Tooltip title={variables.getMessage('widgets.navbar.apps.title')}>
|
||||||
|
<button
|
||||||
|
style={{ fontSize: this.state.zoomFontSize }}
|
||||||
|
onClick={() => this.props.openModal('appsModal')}
|
||||||
|
id="appsShortcutBtn"
|
||||||
|
>
|
||||||
|
<MdOutlineApps className="topicons" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={variables.getMessage('modals.main.navbar.settings', {
|
title={variables.getMessage('modals.main.navbar.settings', {
|
||||||
type: variables.getMessage(
|
type: variables.getMessage(
|
||||||
|
|
|
@ -81,3 +81,13 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.appsmodal {
|
||||||
|
width: fit-content;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 1em;
|
||||||
|
|
||||||
|
@include themed {
|
||||||
|
background: t($modal-sidebarActive);
|
||||||
|
}
|
||||||
|
}
|
|
@ -262,5 +262,13 @@
|
||||||
{
|
{
|
||||||
"name": "photoMap",
|
"name": "photoMap",
|
||||||
"value": true
|
"value": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "appsEnabled",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "applinks",
|
||||||
|
"value": "[]"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -136,6 +136,15 @@ export function loadSettings(hotreload) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// everything below this shouldn't run on a hot reload event
|
||||||
if (hotreload === true) {
|
if (hotreload === true) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -67,6 +67,10 @@
|
||||||
"pin": "Pin",
|
"pin": "Pin",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"no_todos": "No Todos"
|
"no_todos": "No Todos"
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"title": "Apps",
|
||||||
|
"no_apps": "No app links found"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -361,7 +365,8 @@
|
||||||
"refresh_options": {
|
"refresh_options": {
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"page": "Page"
|
"page": "Page"
|
||||||
}
|
},
|
||||||
|
"apps_subtitle": "Create a shortcut of your other commonly used websites."
|
||||||
},
|
},
|
||||||
"font": {
|
"font": {
|
||||||
"title": "Font",
|
"title": "Font",
|
||||||
|
|
|
@ -67,6 +67,10 @@
|
||||||
"pin": "Pin",
|
"pin": "Pin",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"no_todos": "No Todos"
|
"no_todos": "No Todos"
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"title": "Apps",
|
||||||
|
"no_apps": "No app links found"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -361,7 +365,8 @@
|
||||||
"refresh_options": {
|
"refresh_options": {
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"page": "Page"
|
"page": "Page"
|
||||||
}
|
},
|
||||||
|
"apps_subtitle": "Create a shortcut of your other commonly used websites."
|
||||||
},
|
},
|
||||||
"font": {
|
"font": {
|
||||||
"title": "Font",
|
"title": "Font",
|
||||||
|
|
Loading…
Reference in New Issue