onDragStart && onDragStart(e, item.key)}
+ onDragOver={(e) => onDragOver && onDragOver(e, item.key)}
+ onDrop={(e) => onDrop && onDrop(e, item.key)}
+ onDragEnd={(e) => onDragEnd && onDragEnd(e)}
+ >
+

{
+ try {
+ const raw = localStorage.getItem('quicklinks');
+ if (!raw) return [];
+ const data = JSON.parse(raw);
+ return Array.isArray(data) ? data : [];
+ } catch (e) {
+ console.warn('Failed to parse quicklinks from localStorage. Resetting to []', e);
+ return [];
+ }
+};
+
+const DragHandle = () => (
+
+
+
+);
+
+const SortableItem = sortableElement(({ value, enabled, startEditLink, deleteLink }) => {
+ const getIconUrl = (item) => {
+ return item.icon || 'https://icon.horse/icon/' + item.url.replace('https://', '').replace('http://', '');
+ };
+
+ return (
+
+
+
+
})
+
+
+
{value.name}
+
{value.url}
+
+
+
+
+
+
+ );
+});
+
+const SortableContainer = sortableContainer(({ children }) => (
+
{children}
+));
+
class QuickLinksOptions extends PureComponent {
constructor() {
super();
this.state = {
- items: JSON.parse(localStorage.getItem('quicklinks')),
+ items: readQuicklinks(),
showAddModal: false,
urlError: '',
iconError: '',
edit: false,
editData: '',
+ enabled: localStorage.getItem('quicklinksenabled') !== 'false',
};
this.quicklinksContainer = createRef();
+ this.silenceEvent = false;
+ this.handleRefresh = null;
}
+ setContainerDisplay(enabled) {
+ if (!this.quicklinksContainer || !this.quicklinksContainer.current) return;
+ const el = this.quicklinksContainer.current;
+ el.classList.toggle('disabled', !enabled);
+ if (!enabled) {
+ el.setAttribute('aria-hidden', 'true');
+ } else {
+ el.removeAttribute('aria-hidden');
+ }
+}
deleteLink(key, event) {
- event.preventDefault();
-
- // remove link from array
- const data = JSON.parse(localStorage.getItem('quicklinks')).filter((i) => i.key !== key);
-
- localStorage.setItem('quicklinks', JSON.stringify(data));
- this.setState({
- items: data,
- });
+ event.preventDefault();
+ const stored = readQuicklinks();
+ const data = stored.filter((i) => i.key !== key);
+ this.silenceEvent = true;
+ localStorage.setItem('quicklinks', JSON.stringify(data));
+ this.setState({ items: data }, () => {
variables.stats.postEvent('feature', 'Quicklink delete');
- }
+ EventBus.emit('refresh', 'quicklinks');
+ setTimeout(() => { this.silenceEvent = false; }, 0);
+ });
+}
+
async addLink(name, url, icon) {
- const data = JSON.parse(localStorage.getItem('quicklinks'));
+ const data = readQuicklinks();
- if (!url.startsWith('http://') && !url.startsWith('https://')) {
- url = 'https://' + url;
- }
-
- if (url.length <= 0 || isValidUrl(url) === false) {
- return this.setState({
- urlError: variables.getMessage('widgets.quicklinks.url_error'),
- });
- }
-
- if (icon.length > 0 && isValidUrl(icon) === false) {
- return this.setState({
- 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('quicklinks', JSON.stringify(data));
-
- this.setState({
- items: data,
- showAddModal: false,
- urlError: '',
- iconError: '',
- });
-
- variables.stats.postEvent('feature', 'Quicklink add');
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
+ url = 'https://' + url;
}
+ if (url.length <= 0 || isValidUrl(url) === false) {
+ return this.setState({ urlError: variables.getMessage('widgets.quicklinks.url_error') });
+ }
+
+ if (icon.length > 0 && isValidUrl(icon) === false) {
+ return this.setState({ iconError: variables.getMessage('widgets.quicklinks.url_error') });
+ }
+
+ const newItem = {
+ name: name || (await getTitleFromUrl(url)),
+ url,
+ icon: icon || '',
+ key: Date.now().toString() + Math.random().toString(36).substring(2),
+ };
+
+ data.push(newItem);
+
+ this.silenceEvent = true;
+ localStorage.setItem('quicklinks', JSON.stringify(data));
+ this.setState({ items: data, showAddModal: false, urlError: '', iconError: '' }, () => {
+ variables.stats.postEvent('feature', 'Quicklink add');
+ EventBus.emit('refresh', 'quicklinks');
+ setTimeout(() => { this.silenceEvent = false; }, 0);
+ });
+}
+
+
startEditLink(data) {
- this.setState({
- edit: true,
- editData: data,
- showAddModal: true,
- });
+ this.setState({ edit: true, editData: data, showAddModal: true });
}
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 getTitleFromUrl(url));
- dataobj.url = url;
- dataobj.icon = icon || '';
+ const data = readQuicklinks();
+ const dataobj = data.find((i) => i.key === og.key);
+ if (!dataobj) return;
- localStorage.setItem('quicklinks', JSON.stringify(data));
+ dataobj.name = name || (await getTitleFromUrl(url));
+ dataobj.url = url;
+ dataobj.icon = icon || '';
- this.setState({
- items: data,
- showAddModal: false,
- edit: false,
- });
+ this.silenceEvent = true;
+ localStorage.setItem('quicklinks', JSON.stringify(data));
+ this.setState({ items: data, showAddModal: false, edit: false }, () => {
+ EventBus.emit('refresh', 'quicklinks');
+ setTimeout(() => { this.silenceEvent = false; }, 0);
+ });
+}
+
+
+ arrayMove = (array, oldIndex, newIndex) => {
+ const result = Array.from(array);
+ const [removed] = result.splice(oldIndex, 1);
+ result.splice(newIndex, 0, removed);
+ return result;
+ };
+
+onSortStart = () => {
+ if (this.quicklinksContainer && this.quicklinksContainer.current) {
+ this.quicklinksContainer.current.classList.add('dragging');
}
+};
+
+onSortEnd = ({ oldIndex, newIndex }) => {
+ if (!this.state.enabled) return;
+ if (oldIndex === newIndex) return;
+ const newItems = this.arrayMove(this.state.items, oldIndex, newIndex);
+
+ this.silenceEvent = true;
+ this.setState({ items: newItems }, () => {
+ localStorage.setItem('quicklinks', JSON.stringify(newItems));
+ EventBus.emit('refresh', 'quicklinks');
+ setTimeout(() => { this.silenceEvent = false; }, 0);
+ });
+};
+
componentDidMount() {
- EventBus.on('refresh', (data) => {
- if (data === 'quicklinks') {
- if (localStorage.getItem('quicklinksenabled') === 'false') {
- return (this.quicklinksContainer.current.style.display = 'none');
- }
+ this.setContainerDisplay(this.state.enabled);
- this.quicklinksContainer.current.style.display = 'block';
+ this.handleRefresh = (data) => {
+ if (data !== 'quicklinks') return;
+ if (this.silenceEvent) return;
- this.setState({
- items: JSON.parse(localStorage.getItem('quicklinks')),
- });
- }
- });
+ const enabled = localStorage.getItem('quicklinksenabled') !== 'false';
+ const newItems = readQuicklinks();
+ const oldItems = this.state.items || [];
+ const oldKeys = new Set(oldItems.map(i => i.key));
+ const newKeys = new Set(newItems.map(i => i.key));
+
+ const keysEqual =
+ oldItems.length === newItems.length &&
+ oldItems.every(i => newKeys.has(i.key));
+
+ if (enabled !== this.state.enabled || !keysEqual) {
+ this.setContainerDisplay(enabled);
+ this.setState({ items: newItems, enabled });
+ }
+ };
+
+ EventBus.on('refresh', this.handleRefresh);
+}
+
+
+ componentDidUpdate(prevProps, prevState) {
+ if (prevState.enabled !== this.state.enabled) {
+ this.setContainerDisplay(this.state.enabled);
}
+}
- componentWillUnmount() {
- EventBus.off('refresh');
+ componentWillUnmount() {
+ if (this.handleRefresh) {
+ EventBus.off('refresh', this.handleRefresh);
+ } else {
+ try { EventBus.off('refresh'); } catch (e) {}
}
-
+}
render() {
const QUICKLINKS_SECTION = 'modals.main.settings.sections.quicklinks';
+ const { enabled } = this.state;
- const AdditionalSettings = () => {
- return (
-
- (
+
+
+
+
+
+
+
+ );
+
+ const StylingOptions = () => (
+
+
+
+
-
-
-
-
-
- );
- };
+
+
+ );
- const StylingOptions = () => {
- return (
-
- (
+
+
+
+
- );
- };
-
- const AddLink = () => {
- return (
-
-
-
-
-
- );
- };
+
+
+ );
return (
<>
@@ -208,6 +311,7 @@ class QuickLinksOptions extends PureComponent {
zoomSetting="zoomQuicklinks"
visibilityToggle={true}
/>
+
-
- {variables.getMessage(`${QUICKLINKS_SECTION}.no_quicklinks`)}
-
-
- {variables.getMessage('modals.main.settings.sections.message.add_some')}
-
-