[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:
Shashank 2024-02-02 01:48:46 +05:30 committed by GitHub
parent 59e721d663
commit 0d77508f4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 470 additions and 115 deletions

View File

@ -10,6 +10,8 @@ import EventBus from 'modules/helpers/eventbus';
import Welcome from './welcome/Welcome';
import Apps from './apps/Apps';
export default class Modals extends PureComponent {
constructor() {
super();
@ -17,6 +19,7 @@ export default class Modals extends PureComponent {
mainModal: false,
updateModal: false,
welcomeModal: false,
appsModal: false,
preview: false,
};
}
@ -75,6 +78,9 @@ export default class Modals extends PureComponent {
}
render() {
const navZoom = localStorage.getItem('zoomNavbar');
const appsInfo = JSON.parse(localStorage.getItem('applinks'));
return (
<>
{this.state.welcomeModal === false && (
@ -102,6 +108,27 @@ export default class Modals extends PureComponent {
>
<Welcome modalClose={() => this.closeWelcome()} modalSkip={() => this.previewWelcome()} />
</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()} />}
</>
);

View File

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

View File

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

View File

@ -170,12 +170,6 @@ h5 {
gap: 20px;
justify-content: center;
align-items: center;
.link {
display: flex;
flex-flow: row;
gap: 15px;
align-items: center;
}
}
.marketplaceCondition {

View File

@ -2,16 +2,113 @@ import variables from 'modules/variables';
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 Dropdown from '../Dropdown';
import SettingsItem from '../SettingsItem';
import Header from '../Header';
import { getTitleFromUrl, isValidUrl } from './utils/utils';
import QuickLink from './quicklinks/QuickLink';
function Navbar() {
const [showRefreshOptions, setShowRefreshOptions] = useState(
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 (
<>
@ -27,7 +124,7 @@ function Navbar() {
subtitle={variables.getMessage(
'modals.main.settings.sections.appearance.navbar.additional',
)}
final={!showRefreshOptions}
final={false}
>
<Checkbox
name="navbarHover"
@ -55,6 +152,12 @@ function Navbar() {
text={variables.getMessage('widgets.navbar.todo.title')}
category="navbar"
/>
<Checkbox
name="appsEnabled"
text={variables.getMessage('widgets.navbar.apps.title')}
category="navbar"
/>
</SettingsItem>
{showRefreshOptions && (
<SettingsItem
@ -62,7 +165,7 @@ function Navbar() {
subtitle={variables.getMessage(
'modals.main.settings.sections.appearance.navbar.refresh_subtitle',
)}
final={true}
final={false}
>
<Dropdown name="refreshOption" category="navbar">
<option value="page">
@ -83,6 +186,52 @@ function Navbar() {
</Dropdown>
</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>
</>
);
}

View File

@ -10,6 +10,8 @@ import SettingsItem from '../SettingsItem';
import AddModal from './quicklinks/AddModal';
import EventBus from 'modules/helpers/eventbus';
import QuickLink from './quicklinks/QuickLink';
import { getTitleFromUrl, isValidUrl } from './utils/utils';
export default class QuickLinks extends PureComponent {
constructor() {
@ -42,28 +44,24 @@ export default class QuickLinks extends PureComponent {
async addLink(name, url, icon) {
const data = JSON.parse(localStorage.getItem('quicklinks'));
// 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()!@:%_.~#?&=]*)/;
if (url.length <= 0 || urlRegex.test(url) === false) {
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'http://' + url;
}
if (url.length <= 0 || isValidUrl(url) === false) {
return this.setState({
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({
iconError: variables.getMessage('widgets.quicklinks.url_error'),
});
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'http://' + url;
}
data.push({
name: name || (await this.getTitle(url)),
name: name || (await getTitleFromUrl(url)),
url,
icon: icon || '',
key: Math.random().toString(36).substring(7) + 1,
@ -92,7 +90,7 @@ export default class QuickLinks extends PureComponent {
async editLink(og, name, url, icon) {
const data = JSON.parse(localStorage.getItem('quicklinks'));
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.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() {
EventBus.on('refresh', (data) => {
if (data === 'quicklinks') {
@ -144,77 +124,6 @@ export default class QuickLinks extends PureComponent {
}
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 (
<>
<Header
@ -292,7 +201,14 @@ export default class QuickLinks extends PureComponent {
)}
<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>
<Modal
closeTimeoutMS={100}
@ -308,7 +224,9 @@ export default class QuickLinks extends PureComponent {
editLink={(og, name, url, icon) => this.editLink(og, name, url, icon)}
edit={this.state.edit}
editData={this.state.editData}
closeModal={() => this.setState({ showAddModal: false, urlError: '', iconError: '' })}
closeModal={() =>
this.setState({ showAddModal: false, urlError: '', iconError: '', edit: false })
}
/>
</Modal>
</>

View File

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

View File

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

View File

@ -206,7 +206,6 @@ function PhotoInformation({ info, url, api }) {
>
<div className={photoMapClassList}>
{useMapIcon || photoMap() === null ? <MdLocationOn /> : ''}
<h1>{photoMap}</h1>
{photoMap()}
</div>
{showingPhotoMap && (

View File

@ -1,7 +1,7 @@
import variables from 'modules/variables';
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 Todo from './Todo';
@ -21,6 +21,7 @@ class Navbar extends PureComponent {
refreshText: '',
refreshEnabled: localStorage.getItem('refresh'),
refreshOption: localStorage.getItem('refreshOption') || '',
appsOpen: false,
};
}
@ -123,6 +124,21 @@ class Navbar extends PureComponent {
</button>
</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
title={variables.getMessage('modals.main.navbar.settings', {
type: variables.getMessage(

View File

@ -81,3 +81,13 @@
display: flex;
flex-flow: column;
}
.appsmodal {
width: fit-content;
padding: 10px;
border-radius: 1em;
@include themed {
background: t($modal-sidebarActive);
}
}

View File

@ -262,5 +262,13 @@
{
"name": "photoMap",
"value": true
},
{
"name": "appsEnabled",
"value": false
},
{
"name": "applinks",
"value": "[]"
}
]

View File

@ -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
if (hotreload === true) {
return;

View File

@ -67,6 +67,10 @@
"pin": "Pin",
"add": "Add",
"no_todos": "No Todos"
},
"apps": {
"title": "Apps",
"no_apps": "No app links found"
}
}
},
@ -361,7 +365,8 @@
"refresh_options": {
"none": "None",
"page": "Page"
}
},
"apps_subtitle": "Create a shortcut of your other commonly used websites."
},
"font": {
"title": "Font",

View File

@ -67,6 +67,10 @@
"pin": "Pin",
"add": "Add",
"no_todos": "No Todos"
},
"apps": {
"title": "Apps",
"no_apps": "No app links found"
}
}
},
@ -361,7 +365,8 @@
"refresh_options": {
"none": "None",
"page": "Page"
}
},
"apps_subtitle": "Create a shortcut of your other commonly used websites."
},
"font": {
"title": "Font",