mirror of
https://github.com/koichixD/Quasar_Item_Editor.git
synced 2026-08-28 21:01:08 +00:00
added duplicate detection
This commit is contained in:
Generated
+2
-2
@@ -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",
|
||||
|
||||
+127
-16
@@ -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() {
|
||||
<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}`}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user