mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-28 21:40:58 +01:00
314 lines
10 KiB
JavaScript
314 lines
10 KiB
JavaScript
import React, {useState, useEffect} from 'react';
|
|
import RegexCard from './RegexCard';
|
|
import RegexModal from './RegexModal';
|
|
import {RegexPatterns} from '@api/data';
|
|
import {Loader} from 'lucide-react';
|
|
import Alert from '@ui/Alert';
|
|
import {useMassSelection} from '@hooks/useMassSelection';
|
|
import {useKeyboardShortcut} from '@hooks/useKeyboardShortcut';
|
|
import MassActionsBar from '@ui/MassActionsBar';
|
|
import DataBar from '@ui/DataBar/DataBar';
|
|
|
|
const loadingMessages = [
|
|
'Matching all the patterns...',
|
|
'Capturing your groups...',
|
|
'Escaping special characters...',
|
|
'Untangling regular expressions...',
|
|
'Looking behind and ahead...',
|
|
'Validating the validators...'
|
|
];
|
|
|
|
const LoadingState = () => (
|
|
<div className='w-full min-h-[70vh] flex flex-col items-center justify-center'>
|
|
<Loader className='w-8 h-8 animate-spin text-blue-500 mb-4' />
|
|
<p className='text-lg font-medium text-gray-300'>
|
|
{
|
|
loadingMessages[
|
|
Math.floor(Math.random() * loadingMessages.length)
|
|
]
|
|
}
|
|
</p>
|
|
</div>
|
|
);
|
|
|
|
function RegexPage() {
|
|
const [patterns, setPatterns] = useState([]);
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [selectedPattern, setSelectedPattern] = useState(null);
|
|
const [sortBy, setSortBy] = useState('name');
|
|
const [filterType, setFilterType] = useState('none');
|
|
const [filterValue, setFilterValue] = useState('');
|
|
const [allTags, setAllTags] = useState([]);
|
|
const [isCloning, setIsCloning] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [willBeSelected, setWillBeSelected] = useState([]);
|
|
const [originalIndices, setOriginalIndices] = useState(new Map());
|
|
|
|
const {
|
|
selectedItems,
|
|
isSelectionMode,
|
|
toggleSelectionMode,
|
|
handleSelect,
|
|
clearSelection,
|
|
lastSelectedIndex
|
|
} = useMassSelection();
|
|
|
|
useKeyboardShortcut('a', toggleSelectionMode, {ctrl: true});
|
|
|
|
useEffect(() => {
|
|
loadPatterns();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const handleKeyDown = e => {
|
|
if (e.key === 'Shift' && lastSelectedIndex !== null) {
|
|
const element = document.elementFromPoint(
|
|
window.mouseX,
|
|
window.mouseY
|
|
);
|
|
if (element) {
|
|
const card = element.closest('[data-pattern-index]');
|
|
if (card) {
|
|
const index = parseInt(card.dataset.patternIndex);
|
|
handleMouseEnter(index, true);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleKeyUp = e => {
|
|
if (e.key === 'Shift') {
|
|
setWillBeSelected([]);
|
|
}
|
|
};
|
|
|
|
const handleMouseMove = e => {
|
|
window.mouseX = e.clientX;
|
|
window.mouseY = e.clientY;
|
|
};
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
window.addEventListener('keyup', handleKeyUp);
|
|
window.addEventListener('mousemove', handleMouseMove);
|
|
|
|
return () => {
|
|
window.removeEventListener('keydown', handleKeyDown);
|
|
window.removeEventListener('keyup', handleKeyUp);
|
|
window.removeEventListener('mousemove', handleMouseMove);
|
|
};
|
|
}, [lastSelectedIndex]);
|
|
|
|
const loadPatterns = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const response = await RegexPatterns.getAll();
|
|
if (Array.isArray(response)) {
|
|
const patternsData = response.map(item => ({
|
|
...item.content,
|
|
file_name: item.file_name,
|
|
modified_date: item.modified_date
|
|
}));
|
|
setPatterns(patternsData);
|
|
|
|
const tags = new Set();
|
|
patternsData.forEach(pattern => {
|
|
pattern.tags?.forEach(tag => tags.add(tag));
|
|
});
|
|
setAllTags(Array.from(tags));
|
|
} else {
|
|
Alert.error('Failed to load patterns');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading patterns:', error);
|
|
Alert.error('Failed to load patterns');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleOpenModal = (pattern = null) => {
|
|
if (isSelectionMode) return;
|
|
setSelectedPattern(pattern);
|
|
setIsModalOpen(true);
|
|
setIsCloning(false);
|
|
};
|
|
|
|
const handleCloseModal = () => {
|
|
setSelectedPattern(null);
|
|
setIsModalOpen(false);
|
|
setIsCloning(false);
|
|
};
|
|
|
|
const handleClonePattern = pattern => {
|
|
if (isSelectionMode) return;
|
|
const clonedPattern = {
|
|
...pattern,
|
|
name: `${pattern.name} [COPY]`
|
|
};
|
|
setSelectedPattern(clonedPattern);
|
|
setIsModalOpen(true);
|
|
setIsCloning(true);
|
|
};
|
|
|
|
const handleSavePattern = async () => {
|
|
await loadPatterns();
|
|
handleCloseModal();
|
|
};
|
|
|
|
const handleMassDelete = async () => {
|
|
try {
|
|
const filteredPatterns = getFilteredAndSortedPatterns();
|
|
const selectedPatterns = Array.from(selectedItems).map(
|
|
index => filteredPatterns[index]
|
|
);
|
|
|
|
for (const pattern of selectedPatterns) {
|
|
await RegexPatterns.delete(
|
|
pattern.file_name.replace('.yml', '')
|
|
);
|
|
}
|
|
Alert.success('Selected patterns deleted successfully');
|
|
loadPatterns();
|
|
toggleSelectionMode();
|
|
} catch (error) {
|
|
console.error('Error deleting patterns:', error);
|
|
Alert.error('Failed to delete selected patterns');
|
|
}
|
|
};
|
|
|
|
const handlePatternSelect = (patternName, index, e) => {
|
|
if (e.shiftKey) {
|
|
handleMouseEnter(index, true);
|
|
}
|
|
handleSelect(patternName, index, e, getFilteredAndSortedPatterns());
|
|
};
|
|
|
|
const handleMouseEnter = (index, isShiftKey) => {
|
|
if (isShiftKey && lastSelectedIndex !== null) {
|
|
const start = Math.min(lastSelectedIndex, index);
|
|
const end = Math.max(lastSelectedIndex, index);
|
|
|
|
const potentialSelection = getFilteredAndSortedPatterns()
|
|
.slice(start, end + 1)
|
|
.map((pattern, idx) => idx + start);
|
|
|
|
setWillBeSelected(potentialSelection);
|
|
}
|
|
};
|
|
|
|
const getFilteredAndSortedPatterns = () => {
|
|
let filtered = patterns.map((pattern, index) => ({
|
|
...pattern,
|
|
originalIndex: index
|
|
}));
|
|
|
|
if (searchQuery) {
|
|
filtered = filtered.filter(
|
|
pattern =>
|
|
pattern.name
|
|
.toLowerCase()
|
|
.includes(searchQuery.toLowerCase()) ||
|
|
pattern.tags?.some(tag =>
|
|
tag.toLowerCase().includes(searchQuery.toLowerCase())
|
|
)
|
|
);
|
|
}
|
|
|
|
if (filterType === 'tag' && filterValue) {
|
|
filtered = filtered.filter(pattern =>
|
|
pattern.tags?.includes(filterValue)
|
|
);
|
|
}
|
|
|
|
return filtered.sort((a, b) => {
|
|
switch (sortBy) {
|
|
case 'dateModified':
|
|
return (
|
|
new Date(b.modified_date) - new Date(a.modified_date)
|
|
);
|
|
case 'name':
|
|
default:
|
|
return a.name.localeCompare(b.name);
|
|
}
|
|
});
|
|
};
|
|
|
|
const formatDate = dateString => {
|
|
return new Date(dateString).toLocaleString();
|
|
};
|
|
|
|
if (isLoading) {
|
|
return <LoadingState />;
|
|
}
|
|
|
|
return (
|
|
<div className='w-full space-y-2'>
|
|
<DataBar
|
|
onSearch={setSearchQuery}
|
|
searchPlaceholder='Search by name or tag...'
|
|
filterType={filterType}
|
|
setFilterType={setFilterType}
|
|
filterValue={filterValue}
|
|
setFilterValue={setFilterValue}
|
|
allTags={allTags}
|
|
sortBy={sortBy}
|
|
setSortBy={setSortBy}
|
|
isSelectionMode={isSelectionMode}
|
|
toggleSelectionMode={toggleSelectionMode}
|
|
onAdd={() => handleOpenModal()}
|
|
addButtonLabel='Add New Pattern'
|
|
/>
|
|
|
|
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4'>
|
|
{getFilteredAndSortedPatterns().map((pattern, index) => (
|
|
<div
|
|
key={pattern.name}
|
|
data-pattern-index={index}
|
|
onMouseEnter={() =>
|
|
handleMouseEnter(index, window.event?.shiftKey)
|
|
}
|
|
onMouseLeave={() => setWillBeSelected([])}>
|
|
<RegexCard
|
|
pattern={pattern}
|
|
onEdit={() => handleOpenModal(pattern)}
|
|
onClone={handleClonePattern}
|
|
formatDate={formatDate}
|
|
sortBy={sortBy}
|
|
isSelectionMode={isSelectionMode}
|
|
isSelected={selectedItems.has(index)}
|
|
willBeSelected={willBeSelected.includes(index)}
|
|
onSelect={e =>
|
|
handlePatternSelect(pattern.name, index, e)
|
|
}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{isSelectionMode && (
|
|
<MassActionsBar
|
|
selectedCount={selectedItems.size}
|
|
onCancel={() => {
|
|
toggleSelectionMode();
|
|
clearSelection();
|
|
}}
|
|
onDelete={handleMassDelete}
|
|
onImport={() => {}}
|
|
showImport={false}
|
|
/>
|
|
)}
|
|
|
|
<RegexModal
|
|
pattern={selectedPattern}
|
|
isOpen={isModalOpen}
|
|
onClose={handleCloseModal}
|
|
onSave={handleSavePattern}
|
|
isCloning={isCloning}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default RegexPage;
|