diff --git a/package-lock.json b/package-lock.json index 3979690..81fd6a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "quasar-items-editor", - "version": "0.5.0", + "version": "0.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quasar-items-editor", - "version": "0.5.0", + "version": "0.5.1", "dependencies": { "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2.6.0", diff --git a/src/App.jsx b/src/App.jsx index 9283dc4..212a88e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -184,6 +184,29 @@ function parseVec3String(value) { }; } +function normalizeCompareValue(value) { + if (value === null || value === undefined) return ""; + return String(value).trim().toLowerCase(); +} + +function getDuplicateFieldValue(item, field) { + if (!item) return ""; + if (field === "image") { + return normalizeCompareValue(item.image || item.img || item.icon || ""); + } + if (field === "label") return normalizeCompareValue(item.label || ""); + if (field === "name") return normalizeCompareValue(item.name || ""); + return normalizeCompareValue(item.id || ""); +} + +function getSortFieldValue(item, field) { + if (!item || field === "none") return ""; + if (field === "image") return normalizeCompareValue(item.image || item.img || item.icon || ""); + if (field === "label") return normalizeCompareValue(item.label || ""); + if (field === "name") return normalizeCompareValue(item.name || ""); + return normalizeCompareValue(item.id || ""); +} + export default function App() { // dark default document.documentElement.classList.add("dark"); @@ -206,6 +229,10 @@ export default function App() { const [newItemTemplate, setNewItemTemplate] = useState(itemTemplates[0]?.key || "basic"); const [toastMessage, setToastMessage] = useState(""); const [languageMenuOpen, setLanguageMenuOpen] = useState(false); + const [showDuplicatesOnly, setShowDuplicatesOnly] = useState(false); + const [duplicateField, setDuplicateField] = useState("id"); + const [sortField, setSortField] = useState("none"); + const [sortDirection, setSortDirection] = useState("asc"); const toastTimerRef = useRef(null); const languageMenuRef = useRef(null); @@ -249,24 +276,37 @@ export default function App() { () => categories.filter((c) => c !== ALL_CATEGORY), [categories] ); - const duplicateIdCounts = useMemo(() => { - const counts = {}; - for (const item of items) { - const id = item?.id; - if (!id) continue; - counts[id] = (counts[id] || 0) + 1; + const duplicateCountsByField = useMemo(() => { + const fields = ["id", "label", "name", "image"]; + const result = {}; + + for (const field of fields) { + const counts = {}; + for (const item of items) { + const value = getDuplicateFieldValue(item, field); + if (!value) continue; + counts[value] = (counts[value] || 0) + 1; + } + + const duplicates = {}; + for (const [value, count] of Object.entries(counts)) { + if (count > 1) duplicates[value] = count; + } + result[field] = duplicates; } - const duplicates = {}; - for (const [id, count] of Object.entries(counts)) { - if (count > 1) duplicates[id] = count; - } - return duplicates; + + return result; }, [items]); + const duplicateIdCounts = duplicateCountsByField.id || {}; const duplicateIds = useMemo( () => Object.keys(duplicateIdCounts).sort((a, b) => a.localeCompare(b)), [duplicateIdCounts] ); const duplicatePreview = useMemo(() => duplicateIds.slice(0, 6), [duplicateIds]); + const activeDuplicateCounts = useMemo( + () => duplicateCountsByField[duplicateField] || {}, + [duplicateCountsByField, duplicateField] + ); const filteredItems = useMemo(() => { let list = items; @@ -284,8 +324,29 @@ export default function App() { }); } + if (showDuplicatesOnly) { + list = list.filter((i) => { + const value = getDuplicateFieldValue(i, duplicateField); + return !!value && !!activeDuplicateCounts[value]; + }); + } + return list; - }, [items, selectedCategory, search]); + }, [items, selectedCategory, search, showDuplicatesOnly, duplicateField, activeDuplicateCounts]); + + const sortedItems = useMemo(() => { + if (sortField === "none") return filteredItems; + + const direction = sortDirection === "desc" ? -1 : 1; + return [...filteredItems].sort((a, b) => { + const av = getSortFieldValue(a, sortField); + const bv = getSortFieldValue(b, sortField); + if (!av && !bv) return 0; + if (!av) return 1; + if (!bv) return -1; + return av.localeCompare(bv, undefined, { numeric: true, sensitivity: "base" }) * direction; + }); + }, [filteredItems, sortField, sortDirection]); const selectedItem = useMemo(() => { return items.find((i) => i.id === selectedItemId) || null; @@ -951,16 +1012,66 @@ export default function App() {