Files
mue/src/features/quicklinks/options/QuickLinksOptions.jsx

641 lines
19 KiB
JavaScript

import variables from 'config/variables';
import { useState, useEffect, useRef } from 'react';
import {
MdAddLink,
MdLinkOff,
MdSync,
MdOutlineStyle,
MdOutlineVisibility,
MdOutlineFolderOpen,
} from 'react-icons/md';
import { arrayMove } from '@dnd-kit/sortable';
import { Header, Row, Content, Action, PreferencesWrapper, Section } from 'components/Layout/Settings';
import { Checkbox, Dropdown } from 'components/Form/Settings';
import { Button } from 'components/Elements';
import Modal from 'react-modal';
import { AddModal } from 'components/Elements/AddModal';
import { SortableList } from './components';
import { readQuicklinks } from './utils/quicklinksUtils';
import { BookmarkService } from 'utils/quicklinks';
import EventBus from 'utils/eventbus';
import { getTitleFromUrl, isValidUrl } from 'utils/links';
const QuickLinksOptions = ({ currentSubSection, onSubSectionChange, sectionName }) => {
const [items, setItems] = useState(readQuicklinks());
const [showAddModal, setShowAddModal] = useState(false);
const [urlError, setUrlError] = useState('');
const [iconError, setIconError] = useState('');
const [edit, setEdit] = useState(false);
const [editData, setEditData] = useState('');
const [enabled, setEnabled] = useState(localStorage.getItem('quicklinksenabled') !== 'false');
const quicklinksContainer = useRef();
const silenceEventRef = useRef(false);
const setContainerDisplay = (enabled) => {
if (!quicklinksContainer || !quicklinksContainer.current) return;
const el = quicklinksContainer.current;
el.classList.toggle('disabled', !enabled);
if (!enabled) {
el.setAttribute('aria-hidden', 'true');
} else {
el.removeAttribute('aria-hidden');
}
};
const deleteLink = (key, event) => {
event.preventDefault();
const stored = readQuicklinks();
const data = stored.filter((i) => i.key !== key);
silenceEventRef.current = true;
localStorage.setItem('quicklinks', JSON.stringify(data));
setItems(data);
variables.stats.postEvent('feature', 'Quicklink delete');
EventBus.emit('refresh', 'quicklinks');
setTimeout(() => {
silenceEventRef.current = false;
}, 0);
};
const addLink = async (name, url, icon, iconType = 'auto', iconData = null) => {
const data = readQuicklinks();
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
}
if (url.length <= 0 || isValidUrl(url) === false) {
setUrlError(variables.getMessage('widgets.quicklinks.url_error'));
return;
}
if (iconType === 'custom_url' && icon.length > 0 && isValidUrl(icon) === false) {
setIconError(variables.getMessage('widgets.quicklinks.url_error'));
return;
}
const newItem = {
name: name || (await getTitleFromUrl(url)),
url,
icon: icon || '',
iconType: iconType || 'auto',
iconData: iconData || null,
iconFallbacks: [],
groupId: null,
order: data.length,
bookmarkId: null,
bookmarkSource: null,
lastSynced: null,
syncEnabled: false,
customColor: null,
hideLabel: false,
key: Date.now().toString() + Math.random().toString(36).substring(2),
createdAt: Date.now(),
updatedAt: Date.now(),
accessCount: 0,
lastAccessed: null,
};
data.push(newItem);
silenceEventRef.current = true;
localStorage.setItem('quicklinks', JSON.stringify(data));
setItems(data);
setShowAddModal(false);
setUrlError('');
setIconError('');
variables.stats.postEvent('feature', 'Quicklink add');
EventBus.emit('refresh', 'quicklinks');
setTimeout(() => {
silenceEventRef.current = false;
}, 0);
};
const startEditLink = (data) => {
setEdit(true);
setEditData(data);
setShowAddModal(true);
};
const editLink = async (og, name, url, icon, iconType = 'auto', iconData = null) => {
const data = readQuicklinks();
const dataobj = data.find((i) => i.key === og.key);
if (!dataobj) return;
dataobj.name = name || (await getTitleFromUrl(url));
dataobj.url = url;
dataobj.icon = icon || '';
dataobj.iconType = iconType || 'auto';
dataobj.iconData = iconData || null;
dataobj.updatedAt = Date.now();
silenceEventRef.current = true;
localStorage.setItem('quicklinks', JSON.stringify(data));
setItems(data);
setShowAddModal(false);
setEdit(false);
EventBus.emit('refresh', 'quicklinks');
setTimeout(() => {
silenceEventRef.current = false;
}, 0);
};
const handleDragEnd = (event) => {
const { active, over } = event;
if (!over || !enabled) return;
if (active.id === over.id) return;
const oldIndex = items.findIndex((item) => item.key === active.id);
const newIndex = items.findIndex((item) => item.key === over.id);
if (oldIndex === -1 || newIndex === -1) return;
const newItems = arrayMove(items, oldIndex, newIndex);
silenceEventRef.current = true;
setItems(newItems);
localStorage.setItem('quicklinks', JSON.stringify(newItems));
EventBus.emit('refresh', 'quicklinks');
setTimeout(() => {
silenceEventRef.current = false;
}, 0);
};
useEffect(() => {
setContainerDisplay(enabled);
}, [enabled]);
useEffect(() => {
setContainerDisplay(enabled);
const handleRefresh = (data) => {
if (data !== 'quicklinks') return;
if (silenceEventRef.current) return;
const newEnabled = localStorage.getItem('quicklinksenabled') !== 'false';
const newItems = readQuicklinks();
const oldItems = items || [];
const keysEqual =
oldItems.length === newItems.length &&
oldItems.every((i) => newItems.some((n) => n.key === i.key));
if (newEnabled !== enabled || !keysEqual) {
setContainerDisplay(newEnabled);
setItems(newItems);
setEnabled(newEnabled);
}
};
EventBus.on('refresh', handleRefresh);
return () => {
EventBus.off('refresh', handleRefresh);
};
}, [enabled, items]);
const QUICKLINKS_SECTION = 'modals.main.settings.sections.quicklinks';
// Subsection: Appearance (Style + Layout)
const AppearanceSection = () => {
const handleLayoutChange = (key, value) => {
const config = JSON.parse(localStorage.getItem('quicklinks_config') || '{}');
config[key] = value;
localStorage.setItem('quicklinks_config', JSON.stringify(config));
EventBus.emit('refresh', 'quicklinks');
};
return (
<>
<Row>
<Content
title={variables.getMessage(`${QUICKLINKS_SECTION}.styling`)}
subtitle={variables.getMessage(`${QUICKLINKS_SECTION}.styling_description`)}
/>
<Action>
<Dropdown
label={variables.getMessage(`${QUICKLINKS_SECTION}.style`)}
name="quickLinksStyle"
category="quicklinks"
items={[
{ value: 'card', text: 'Card' },
{ value: 'icon', text: variables.getMessage(`${QUICKLINKS_SECTION}.options.icon`) },
{
value: 'text_only',
text: variables.getMessage(`${QUICKLINKS_SECTION}.options.text_only`),
},
{
value: 'metro',
text: variables.getMessage(`${QUICKLINKS_SECTION}.options.metro`),
},
]}
/>
</Action>
</Row>
<Row>
<Content title="Layout" subtitle="Configure grid layout and organization" />
<Action>
<Dropdown
label="Layout Mode"
name="quicklinks_layoutMode"
category="quicklinks"
onChange={(value) => handleLayoutChange('layoutMode', value)}
items={[
{ value: 'flex', text: 'Flexible' },
{ value: 'grid', text: 'Grid' },
{ value: 'compact', text: 'Compact' },
]}
/>
<Dropdown
label="Grid Columns"
name="quicklinks_gridColumns"
category="quicklinks"
onChange={(value) => handleLayoutChange('gridColumns', value)}
items={[
{ value: 'auto', text: 'Auto' },
{ value: '2', text: '2' },
{ value: '3', text: '3' },
{ value: '4', text: '4' },
{ value: '5', text: '5' },
{ value: '6', text: '6' },
]}
/>
<Dropdown
label="Grid Rows"
name="quicklinks_gridRows"
category="quicklinks"
onChange={(value) => handleLayoutChange('gridRows', value)}
items={[
{ value: 'auto', text: 'Auto' },
{ value: '2', text: '2' },
{ value: '3', text: '3' },
{ value: '4', text: '4' },
]}
/>
</Action>
</Row>
</>
);
};
// Subsection: Display
const DisplaySection = () => (
<Row>
<Content
title={variables.getMessage('modals.main.settings.additional_settings')}
subtitle={variables.getMessage(`${QUICKLINKS_SECTION}.additional`)}
/>
<Action>
<Checkbox
name="quicklinksnewtab"
text={variables.getMessage(`${QUICKLINKS_SECTION}.open_new`)}
category="quicklinks"
/>
<Checkbox
name="quicklinkstooltip"
text={variables.getMessage(`${QUICKLINKS_SECTION}.tooltip`)}
category="quicklinks"
/>
</Action>
</Row>
);
// Subsection: Organization
const OrganizationSection = () => {
const handleGroupingToggle = (value) => {
const config = JSON.parse(localStorage.getItem('quicklinks_config') || '{}');
config.groupingEnabled = value;
config.showGroupHeaders = value;
localStorage.setItem('quicklinks_config', JSON.stringify(config));
EventBus.emit('refresh', 'quicklinks');
};
return (
<Row>
<Content title="Groups" subtitle="Organize quick links into groups" />
<Action>
<Checkbox
name="quicklinks_groupingEnabled"
text="Enable Grouping"
category="quicklinks"
onChange={handleGroupingToggle}
/>
</Action>
</Row>
);
};
// Subsection: Sync
const SyncSection = () => {
const [syncStatus, setSyncStatus] = useState('');
const [syncing, setSyncing] = useState(false);
const handleSyncToggle = (value) => {
const config = JSON.parse(localStorage.getItem('quicklinks_config') || '{}');
config.bookmarkSyncEnabled = value;
localStorage.setItem('quicklinks_config', JSON.stringify(config));
};
const handleSync = async () => {
setSyncing(true);
setSyncStatus('Syncing...');
try {
// Enable sync first if not already enabled
const config = JSON.parse(localStorage.getItem('quicklinks_config') || '{}');
if (!config.bookmarkSyncEnabled) {
config.bookmarkSyncEnabled = true;
localStorage.setItem('quicklinks_config', JSON.stringify(config));
}
const hasPermission = await BookmarkService.checkPermissions();
if (!hasPermission) {
const granted = await BookmarkService.requestPermissions();
if (!granted) {
setSyncStatus('Permission denied');
setSyncing(false);
return;
}
}
const result = await BookmarkService.syncBookmarks();
setSyncStatus(
`Synced: ${result.imported} imported, ${result.updated} updated, ${result.removed} removed`,
);
EventBus.emit('refresh', 'quicklinks');
} catch (e) {
setSyncStatus(`Error: ${e.message}`);
}
setSyncing(false);
};
return (
<>
<Row>
<Content
title="Bookmark Sync"
subtitle="Automatically sync browser bookmarks to quick links"
/>
<Action>
<Checkbox
name="quicklinks_bookmarkSyncEnabled"
text="Enable Bookmark Sync"
category="quicklinks"
onChange={handleSyncToggle}
/>
</Action>
</Row>
<Row>
<Content
title="Manual Sync"
subtitle="Manually sync bookmarks now"
/>
<Action>
<Button
type="settings"
onClick={handleSync}
icon={<MdSync />}
label="Sync Bookmarks"
disabled={syncing}
/>
{syncStatus && <p style={{ marginTop: '8px', fontSize: '14px' }}>{syncStatus}</p>}
</Action>
</Row>
</>
);
};
const getHeader = () => {
if (currentSubSection === 'appearance') {
return (
<Header
title={variables.getMessage(`${QUICKLINKS_SECTION}.title`)}
secondaryTitle="Appearance"
goBack={() => onSubSectionChange(null, sectionName)}
zoomSetting="zoomQuicklinks"
/>
);
}
if (currentSubSection === 'display') {
return (
<Header
title={variables.getMessage(`${QUICKLINKS_SECTION}.title`)}
secondaryTitle="Display"
goBack={() => onSubSectionChange(null, sectionName)}
/>
);
}
if (currentSubSection === 'organization') {
return (
<Header
title={variables.getMessage(`${QUICKLINKS_SECTION}.title`)}
secondaryTitle="Organization"
goBack={() => onSubSectionChange(null, sectionName)}
/>
);
}
if (currentSubSection === 'sync') {
return (
<Header
title={variables.getMessage(`${QUICKLINKS_SECTION}.title`)}
secondaryTitle="Bookmark Sync"
goBack={() => onSubSectionChange(null, sectionName)}
/>
);
}
return (
<Header
title={variables.getMessage(`${QUICKLINKS_SECTION}.title`)}
setting="quicklinksenabled"
category="quicklinks"
element=".quicklinks-container"
visibilityToggle={true}
/>
);
};
return (
<>
{getHeader()}
{!currentSubSection && (
<>
<PreferencesWrapper
setting="quicklinksenabled"
category="quicklinks"
visibilityToggle={true}
>
<Section
title="Appearance"
subtitle="Customize style and layout of quick links"
icon={<MdOutlineStyle />}
onClick={() => onSubSectionChange('appearance', sectionName)}
/>
<Section
title="Display"
subtitle="Configure tooltips and tab behavior"
icon={<MdOutlineVisibility />}
onClick={() => onSubSectionChange('display', sectionName)}
/>
<Section
title="Organization"
subtitle="Organize links into groups"
icon={<MdOutlineFolderOpen />}
onClick={() => onSubSectionChange('organization', sectionName)}
/>
<Section
title="Bookmark Sync"
subtitle="Import and sync browser bookmarks"
icon={<MdSync />}
onClick={() => onSubSectionChange('sync', sectionName)}
/>
</PreferencesWrapper>
</>
)}
{currentSubSection === 'appearance' && (
<PreferencesWrapper
setting="quicklinksenabled"
category="quicklinks"
visibilityToggle={false}
zoomSetting="zoomQuicklinks"
>
<AppearanceSection />
</PreferencesWrapper>
)}
{currentSubSection === 'display' && (
<PreferencesWrapper
setting="quicklinksenabled"
category="quicklinks"
visibilityToggle={false}
>
<DisplaySection />
</PreferencesWrapper>
)}
{currentSubSection === 'organization' && (
<PreferencesWrapper
setting="quicklinksenabled"
category="quicklinks"
visibilityToggle={false}
>
<OrganizationSection />
</PreferencesWrapper>
)}
{currentSubSection === 'sync' && (
<PreferencesWrapper
setting="quicklinksenabled"
category="quicklinks"
visibilityToggle={false}
>
<SyncSection />
</PreferencesWrapper>
)}
{!currentSubSection && (
<>
<PreferencesWrapper
setting="quicklinksenabled"
category="quicklinks"
visibilityToggle={false}
>
<Row final={true}>
<Content title={variables.getMessage(`${QUICKLINKS_SECTION}.title`)} />
<Action>
<Button
type="settings"
onClick={() => enabled && setShowAddModal(true)}
icon={<MdAddLink />}
label={variables.getMessage(`${QUICKLINKS_SECTION}.add_link`)}
disabled={!enabled}
/>
</Action>
</Row>
{items.length === 0 && (
<div className="photosEmpty">
<div className="emptyNewMessage">
<MdLinkOff />
<span className="title">
{variables.getMessage(`${QUICKLINKS_SECTION}.no_quicklinks`)}
</span>
<span className="subtitle">
{variables.getMessage('modals.main.settings.sections.message.add_some')}
</span>
<Button
type="settings"
onClick={() => setShowAddModal(true)}
icon={<MdAddLink />}
label={variables.getMessage(`${QUICKLINKS_SECTION}.add_link`)}
/>
</div>
</div>
)}
</PreferencesWrapper>
<PreferencesWrapper
setting="quicklinksenabled"
category="quicklinks"
visibilityToggle={false}
>
<div className={`messagesContainer`}>
<SortableList
items={items}
enabled={enabled}
onDragEnd={handleDragEnd}
startEditLink={(data) => startEditLink(data)}
deleteLink={(key, e) => deleteLink(key, e)}
/>
</div>
</PreferencesWrapper>
</>
)}
<Modal
closeTimeoutMS={100}
onRequestClose={() => {
setShowAddModal(false);
setUrlError('');
setIconError('');
}}
isOpen={showAddModal}
className="Modal resetmodal mainModal"
overlayClassName="Overlay resetoverlay"
ariaHideApp={false}
>
<AddModal
urlError={urlError}
addLink={(name, url, icon, iconType, iconData) =>
addLink(name, url, icon, iconType, iconData)
}
editLink={(og, name, url, icon, iconType, iconData) =>
editLink(og, name, url, icon, iconType, iconData)
}
edit={edit}
editData={editData}
closeModal={() => {
setShowAddModal(false);
setUrlError('');
setIconError('');
setEdit(false);
}}
/>
</Modal>
</>
);
};
export { QuickLinksOptions as default, QuickLinksOptions };