Skip to content

feat(library): F10 — Duplicate detection with merge UI #3

Description

@lalithaar

Overview

Background scan on library load groups entries by identical DOI (exact match) or same normalised title + year (fuzzy match). A 'Duplicates' badge appears in the sidebar. A side-by-side merge panel lets the user pick the canonical value for each field, combining collections and tags from both entries.

Scope

Rust (src-tauri/src/library.rs)

  • Add ind_duplicates() -> Vec<Vec> command
    • Exact DOI duplicates: SELECT doi, GROUP_CONCAT(id) FROM items WHERE doi != '' GROUP BY LOWER(TRIM(doi)) HAVING COUNT(*) > 1
    • Fuzzy title+year: normalise title (lowercase, strip punctuation, collapse spaces), group by (normalised_title, year) with COUNT(*) > 1
    • Return groups as Vec<Vec> (each inner vec is a group of item IDs that are duplicates of each other)
  • Add merge_items(keep_id: i64, discard_ids: Vec, merged_item: ItemInput) -> LibraryItem command
    • Update the kept item with merged_item fields
    • Move all collection memberships and tags from discarded items to kept item (INSERT OR IGNORE)
    • Soft-delete the discarded items (insert into trash)

Frontend (src/composables/useLibrary.ts)

  • Add indDuplicates() -> number[][] and mergeItems(keepId, discardIds, mergedItem) functions

Frontend (src/components/CollectionsSidebar.vue)

  • Add a Duplicates row with badge count (hidden when 0)
  • Clicking it sets showingDuplicates flag on parent

Frontend (src/components/DuplicateMergePanel.vue) — new file

  • Receives two LibraryItem objects (one group at a time)
  • For each field: show value from item A and item B side by side, radio to pick which to keep (or edit manually)
  • 'Merge' button calls mergeItems; 'Skip' moves to next duplicate pair
  • Progress indicator: 'X of Y duplicate groups'

Frontend (src/views/LibraryView.vue)

  • When showingDuplicates: show list of duplicate groups, clicking a group opens DuplicateMergePanel
  • Run indDuplicates() after loadItems() to populate the badge count

Notes

  • Title normalisation for fuzzy match: lowercase, remove punctuation ([^a-z0-9 ]), collapse whitespace, trim. Two titles are a match if normalised strings are equal AND year matches.
  • Merge only moves data to the kept item — never silently deletes anything. Discarded items go to Trash so they are recoverable.

Verification

  • Import a .bib that has a duplicate of an existing entry (same DOI)
  • See 'Duplicates (1)' badge in sidebar
  • Open merge panel, verify both entries shown side by side
  • Merge → kept entry has combined tags/collections, discarded entry moves to Trash
  • Badge clears after all groups resolved

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions