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", "name": "quasar-items-editor",
"version": "0.5.0", "version": "0.5.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "quasar-items-editor", "name": "quasar-items-editor",
"version": "0.5.0", "version": "0.5.1",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2.6.0", "@tauri-apps/plugin-dialog": "^2.6.0",
+127 -16
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() { export default function App() {
// dark default // dark default
document.documentElement.classList.add("dark"); document.documentElement.classList.add("dark");
@@ -206,6 +229,10 @@ export default function App() {
const [newItemTemplate, setNewItemTemplate] = useState(itemTemplates[0]?.key || "basic"); const [newItemTemplate, setNewItemTemplate] = useState(itemTemplates[0]?.key || "basic");
const [toastMessage, setToastMessage] = useState(""); const [toastMessage, setToastMessage] = useState("");
const [languageMenuOpen, setLanguageMenuOpen] = useState(false); 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 toastTimerRef = useRef(null);
const languageMenuRef = useRef(null); const languageMenuRef = useRef(null);
@@ -249,24 +276,37 @@ export default function App() {
() => categories.filter((c) => c !== ALL_CATEGORY), () => categories.filter((c) => c !== ALL_CATEGORY),
[categories] [categories]
); );
const duplicateIdCounts = useMemo(() => { const duplicateCountsByField = useMemo(() => {
const counts = {}; const fields = ["id", "label", "name", "image"];
for (const item of items) { const result = {};
const id = item?.id;
if (!id) continue; for (const field of fields) {
counts[id] = (counts[id] || 0) + 1; 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)) { return result;
if (count > 1) duplicates[id] = count;
}
return duplicates;
}, [items]); }, [items]);
const duplicateIdCounts = duplicateCountsByField.id || {};
const duplicateIds = useMemo( const duplicateIds = useMemo(
() => Object.keys(duplicateIdCounts).sort((a, b) => a.localeCompare(b)), () => Object.keys(duplicateIdCounts).sort((a, b) => a.localeCompare(b)),
[duplicateIdCounts] [duplicateIdCounts]
); );
const duplicatePreview = useMemo(() => duplicateIds.slice(0, 6), [duplicateIds]); const duplicatePreview = useMemo(() => duplicateIds.slice(0, 6), [duplicateIds]);
const activeDuplicateCounts = useMemo(
() => duplicateCountsByField[duplicateField] || {},
[duplicateCountsByField, duplicateField]
);
const filteredItems = useMemo(() => { const filteredItems = useMemo(() => {
let list = items; 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; 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(() => { const selectedItem = useMemo(() => {
return items.find((i) => i.id === selectedItemId) || null; 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"> <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> <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"> <span className="text-xs text-slate-400 bg-slate-800/60 px-2 py-0.5 rounded-full">
{filteredItems.length} {sortedItems.length}
</span> </span>
</div> </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"> <ul className="space-y-1">
{filteredItems.map((it, idx) => { {sortedItems.map((it, idx) => {
const imageSrc = getImageSrc(it); const imageSrc = getImageSrc(it);
const duplicateCount = duplicateIdCounts[it.id] || 0; const duplicateCount = showDuplicatesOnly
? activeDuplicateCounts[getDuplicateFieldValue(it, duplicateField)] || 0
: duplicateIdCounts[it.id] || 0;
return ( return (
<li <li
key={`${it.id}-${idx}`} key={`${it.id}-${idx}`}
+28
View File
@@ -56,6 +56,20 @@ export const translations = {
imageAdded: "Bild hinzugef\u00fcgt", imageAdded: "Bild hinzugef\u00fcgt",
duplicatesFound: ({ count }) => `Doppelte Item-IDs gefunden: ${count}`, duplicatesFound: ({ count }) => `Doppelte Item-IDs gefunden: ${count}`,
duplicateSelectedHint: ({ id, count }) => `Achtung: ID '${id}' ist ${count}x vorhanden.`, 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: { en: {
allItems: "All Items", allItems: "All Items",
@@ -102,6 +116,20 @@ export const translations = {
imageAdded: "Image added", imageAdded: "Image added",
duplicatesFound: ({ count }) => `Duplicate item IDs found: ${count}`, duplicatesFound: ({ count }) => `Duplicate item IDs found: ${count}`,
duplicateSelectedHint: ({ id, count }) => `Warning: ID '${id}' exists ${count} times.`, 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: { es: {
allItems: "Todos los items", allItems: "Todos los items",