From 0d77508f4ce020ffa834202d2eb42550212b9a07 Mon Sep 17 00:00:00 2001 From: Shashank Date: Fri, 2 Feb 2024 01:48:46 +0530 Subject: [PATCH] [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 --- src/components/modals/Modals.jsx | 27 ++++ src/components/modals/apps/Apps.jsx | 42 +++++ src/components/modals/apps/scss/index.scss | 77 +++++++++ src/components/modals/main/scss/index.scss | 6 - .../modals/main/settings/sections/Navbar.jsx | 153 +++++++++++++++++- .../main/settings/sections/QuickLinks.jsx | 124 +++----------- .../sections/quicklinks/QuickLink.jsx | 68 ++++++++ .../main/settings/sections/utils/utils.js | 28 ++++ .../widgets/background/PhotoInformation.jsx | 1 - src/components/widgets/navbar/Navbar.jsx | 18 ++- src/components/widgets/navbar/scss/index.scss | 10 ++ src/modules/default_settings.json | 8 + src/modules/helpers/settings/index.js | 9 ++ src/translations/en_GB.json | 7 +- src/translations/en_US.json | 7 +- 15 files changed, 470 insertions(+), 115 deletions(-) create mode 100644 src/components/modals/apps/Apps.jsx create mode 100644 src/components/modals/apps/scss/index.scss create mode 100644 src/components/modals/main/settings/sections/quicklinks/QuickLink.jsx create mode 100644 src/components/modals/main/settings/sections/utils/utils.js diff --git a/src/components/modals/Modals.jsx b/src/components/modals/Modals.jsx index 61c7ff89..2a241011 100644 --- a/src/components/modals/Modals.jsx +++ b/src/components/modals/Modals.jsx @@ -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 { > this.closeWelcome()} modalSkip={() => this.previewWelcome()} /> + + 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', + }, + }} + > + + + {this.state.preview && window.location.reload()} />} ); diff --git a/src/components/modals/apps/Apps.jsx b/src/components/modals/apps/Apps.jsx new file mode 100644 index 00000000..574417e0 --- /dev/null +++ b/src/components/modals/apps/Apps.jsx @@ -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 ( +
+ {appsInfo.length > 0 ? ( + appsInfo.map((info, i) => ( + + + Google + {info.name} + + + )) + ) : ( +
+

+ No app links found + +

+
+ )} +
+ ); +}; + +export default Apps; diff --git a/src/components/modals/apps/scss/index.scss b/src/components/modals/apps/scss/index.scss new file mode 100644 index 00000000..4774cc1f --- /dev/null +++ b/src/components/modals/apps/scss/index.scss @@ -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); + } + } +} diff --git a/src/components/modals/main/scss/index.scss b/src/components/modals/main/scss/index.scss index 28eb8413..690b7f82 100644 --- a/src/components/modals/main/scss/index.scss +++ b/src/components/modals/main/scss/index.scss @@ -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 { diff --git a/src/components/modals/main/settings/sections/Navbar.jsx b/src/components/modals/main/settings/sections/Navbar.jsx index 566b1b57..3810f6a0 100644 --- a/src/components/modals/main/settings/sections/Navbar.jsx +++ b/src/components/modals/main/settings/sections/Navbar.jsx @@ -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} > + + {showRefreshOptions && ( )} + + + + + +
+ {appsModalInfo.items.map((item, i) => ( + startEditLink(item)} + deleteLink={(key, e) => deleteLink(key, e)} + /> + ))} +
+ + + setAppsModalInfo((oldState) => ({ ...oldState, newLink: false, edit: false })) + } + isOpen={appsModalInfo.edit || appsModalInfo.newLink} + className="Modal resetmodal mainModal" + overlayClassName="Overlay resetoverlay" + ariaHideApp={false} + > + 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 })) + } + /> + ); } diff --git a/src/components/modals/main/settings/sections/QuickLinks.jsx b/src/components/modals/main/settings/sections/QuickLinks.jsx index 694152bf..08c170a0 100644 --- a/src/components/modals/main/settings/sections/QuickLinks.jsx +++ b/src/components/modals/main/settings/sections/QuickLinks.jsx @@ -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 ( - this.deleteLink(item.key, e)} - href={item.url} - target={target} - rel={rel} - draggable={false} - > - {item.name} - - ); - } - - const img = - item.icon || - 'https://icon.horse/icon/ ' + item.url.replace('https://', '').replace('http://', ''); - - const link = ( -
-
- {item.name} -
-
-
{item.name}
- -
-
-
- - -
-
-
- ); - - return link; - }; - return ( <>
- {this.state.items.map((item) => quickLink(item))} + {this.state.items.map((item, i) => ( + this.startEditLink(item)} + deleteLink={(key, e) => this.deleteLink(key, e)} + /> + ))} 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 }) + } /> diff --git a/src/components/modals/main/settings/sections/quicklinks/QuickLink.jsx b/src/components/modals/main/settings/sections/quicklinks/QuickLink.jsx new file mode 100644 index 00000000..ad8a1f2c --- /dev/null +++ b/src/components/modals/main/settings/sections/quicklinks/QuickLink.jsx @@ -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 ( + deleteLink(item.key, e)} + href={item.url} + target={target} + rel={rel} + draggable={false} + > + {item.name} + + ); + } + + const img = + item.icon || + 'https://icon.horse/icon/ ' + item.url.replace('https://', '').replace('http://', ''); + + return ( +
+
+ {item.name} +
+
+
{item.name}
+ +
+
+
+ + +
+
+
+ ); +}; + +export default QuickLink; diff --git a/src/components/modals/main/settings/sections/utils/utils.js b/src/components/modals/main/settings/sections/utils/utils.js new file mode 100644 index 00000000..6efcfb86 --- /dev/null +++ b/src/components/modals/main/settings/sections/utils/utils.js @@ -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 }; diff --git a/src/components/widgets/background/PhotoInformation.jsx b/src/components/widgets/background/PhotoInformation.jsx index 5efae880..4af6d57e 100644 --- a/src/components/widgets/background/PhotoInformation.jsx +++ b/src/components/widgets/background/PhotoInformation.jsx @@ -206,7 +206,6 @@ function PhotoInformation({ info, url, api }) { >
{useMapIcon || photoMap() === null ? : ''} -

{photoMap}

{photoMap()}
{showingPhotoMap && ( diff --git a/src/components/widgets/navbar/Navbar.jsx b/src/components/widgets/navbar/Navbar.jsx index e101f49c..c0d6e0b8 100644 --- a/src/components/widgets/navbar/Navbar.jsx +++ b/src/components/widgets/navbar/Navbar.jsx @@ -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 { )} + + {localStorage.getItem('appsEnabled') === 'true' && ( + <> + + + + + )} +