added duplicate detection

This commit is contained in:
2026-02-16 06:51:41 +01:00
parent b0dac1ec58
commit 8625838e37
3 changed files with 157 additions and 18 deletions
+2 -2
View File
@@ -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",
+123 -12
View File
@@ -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 duplicateCountsByField = useMemo(() => {
const fields = ["id", "label", "name", "image"];
const result = {};
for (const field of fields) {
const counts = {};
for (const item of items) {
const id = item?.id;
if (!id) continue;
counts[id] = (counts[id] || 0) + 1;
const value = getDuplicateFieldValue(item, field);
if (!value) continue;
counts[value] = (counts[value] || 0) + 1;
}
const duplicates = {};
for (const [id, count] of Object.entries(counts)) {
if (count > 1) duplicates[id] = count;
for (const [value, count] of Object.entries(counts)) {
if (count > 1) duplicates[value] = count;
}
return duplicates;
result[field] = 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() {
<div className="flex items-center justify-between gap-2 mb-2 pb-2 border-b border-slate-800">
<h2 className="text-accent font-semibold">{displayCategoryName(selectedCategory)}</h2>
<span className="text-xs text-slate-400 bg-slate-800/60 px-2 py-0.5 rounded-full">
{filteredItems.length}
{sortedItems.length}
</span>
</div>
<div className="mb-3 grid grid-cols-1 gap-2">
<div className="flex items-center justify-between gap-2">
<label className="text-xs text-slate-400">{t("duplicateField")}</label>
<select
value={duplicateField}
onChange={(e) => setDuplicateField(e.target.value)}
className="bg-panel2 border border-slate-700 rounded px-2 py-1 text-xs"
>
<option value="id">{t("duplicateById")}</option>
<option value="label">{t("duplicateByLabel")}</option>
<option value="name">{t("duplicateByName")}</option>
<option value="image">{t("duplicateByImage")}</option>
</select>
</div>
<label className="flex items-center gap-2 text-xs text-slate-300">
<input
type="checkbox"
checked={showDuplicatesOnly}
onChange={(e) => setShowDuplicatesOnly(e.target.checked)}
className="accent-cyan-400"
/>
{t("duplicatesOnly")}
</label>
<div className="flex items-center justify-between gap-2">
<label className="text-xs text-slate-400">{t("sortBy")}</label>
<div className="flex items-center gap-2">
<select
value={sortField}
onChange={(e) => setSortField(e.target.value)}
className="bg-panel2 border border-slate-700 rounded px-2 py-1 text-xs"
>
<option value="none">{t("sortNone")}</option>
<option value="id">{t("sortId")}</option>
<option value="label">{t("sortLabel")}</option>
<option value="name">{t("sortName")}</option>
<option value="image">{t("sortImage")}</option>
</select>
<select
value={sortDirection}
onChange={(e) => setSortDirection(e.target.value)}
disabled={sortField === "none"}
className="bg-panel2 border border-slate-700 rounded px-2 py-1 text-xs disabled:opacity-50"
>
<option value="asc">{t("sortAsc")}</option>
<option value="desc">{t("sortDesc")}</option>
</select>
</div>
</div>
</div>
<ul className="space-y-1">
{filteredItems.map((it, idx) => {
{sortedItems.map((it, idx) => {
const imageSrc = getImageSrc(it);
const duplicateCount = duplicateIdCounts[it.id] || 0;
const duplicateCount = showDuplicatesOnly
? activeDuplicateCounts[getDuplicateFieldValue(it, duplicateField)] || 0
: duplicateIdCounts[it.id] || 0;
return (
<li
key={`${it.id}-${idx}`}
+28
View File
@@ -56,6 +56,20 @@ export const translations = {
imageAdded: "Bild hinzugef\u00fcgt",
duplicatesFound: ({ count }) => `Doppelte Item-IDs gefunden: ${count}`,
duplicateSelectedHint: ({ id, count }) => `Achtung: ID '${id}' ist ${count}x vorhanden.`,
duplicatesOnly: "Nur Duplikate",
duplicateField: "Duplikat nach",
duplicateById: "ID",
duplicateByLabel: "Label",
duplicateByName: "Name",
duplicateByImage: "Bild",
sortBy: "Sortieren nach",
sortNone: "Original",
sortId: "ID",
sortLabel: "Label",
sortName: "Name",
sortImage: "Bild",
sortAsc: "A-Z",
sortDesc: "Z-A",
},
en: {
allItems: "All Items",
@@ -102,6 +116,20 @@ export const translations = {
imageAdded: "Image added",
duplicatesFound: ({ count }) => `Duplicate item IDs found: ${count}`,
duplicateSelectedHint: ({ id, count }) => `Warning: ID '${id}' exists ${count} times.`,
duplicatesOnly: "Duplicates only",
duplicateField: "Duplicate by",
duplicateById: "ID",
duplicateByLabel: "Label",
duplicateByName: "Name",
duplicateByImage: "Image",
sortBy: "Sort by",
sortNone: "Original",
sortId: "ID",
sortLabel: "Label",
sortName: "Name",
sortImage: "Image",
sortAsc: "A-Z",
sortDesc: "Z-A",
},
es: {
allItems: "Todos los items",