Skip to content

Commit

Permalink
fix duplicate problem
Browse files Browse the repository at this point in the history
  • Loading branch information
ildyria committed Jan 10, 2025
1 parent 792cc79 commit 77816e1
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 31 deletions.
27 changes: 19 additions & 8 deletions app/Actions/Photo/DuplicateFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Models\Photo;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

/**
* Look for duplicates in the database.
Expand Down Expand Up @@ -46,6 +47,8 @@ public function search(
bool $with_title_constraint,
): Collection {
/** @var Collection<int,object{album_id:string,album_title:string,photo_id:string,photo_title:string,checksum:string,short_path:string|null,storage_disk:string|null}> */
// dd($this->query($with_album_constraint, $with_checksum_constraint, $with_title_constraint)
// ->toSql());
return $this->query($with_album_constraint, $with_checksum_constraint, $with_title_constraint)
->get();
}
Expand All @@ -71,18 +74,11 @@ private function query(

return Photo::query()
->join('base_albums', 'base_albums.id', '=', 'photos.album_id')
->join(
'photos as p2',
fn ($join) => $join->on('photos.id', '<>', 'p2.id')
->when($with_title_constraint, fn ($q) => $q->on('photos.title', '=', 'p2.title'))
->when($with_checksum_constraint, fn ($q) => $q->on('photos.checksum', '=', 'p2.checksum'))
->when($with_album_constraint, fn ($q) => $q->on('photos.album_id', '=', 'p2.album_id'))
)
->join(
'size_variants', 'size_variants.photo_id', '=', 'photos.id', 'left'
)
->whereIn('photos.id', $this->getDuplicatesIdsQuery($with_album_constraint, $with_checksum_constraint, $with_title_constraint))
->where('size_variants.type', '=', 4)
->where('photos.id', '<>', 'p2.id')
->select([
'base_albums.id as album_id',
'base_albums.title as album_title',
Expand All @@ -97,4 +93,19 @@ private function query(
->when(!$with_checksum_constraint, fn ($q) => $q->orderBy('photos.title', 'asc'))
->toBase();
}

private function getDuplicatesIdsQuery(
bool $with_album_constraint,
bool $with_checksum_constraint,
bool $with_title_constraint,
): Builder {
return DB::table('photos', 'p1')->select('p1.id')
->join(
'photos as p2',
fn ($join) => $join->on('p1.id', '<>', 'p2.id')
->when($with_title_constraint, fn ($q) => $q->on('p1.title', '=', 'p2.title'))
->when($with_checksum_constraint, fn ($q) => $q->on('p1.checksum', '=', 'p2.checksum'))
->when($with_album_constraint, fn ($q) => $q->on('p1.album_id', '=', 'p2.album_id'))
);
}
}
36 changes: 30 additions & 6 deletions resources/js/components/maintenance/DuplicateLine.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,48 @@
<div class="hover:bg-primary-emphasis/5">
<template v-for="duplicate in props.duplicates.data">
<div
class="flex justify-between hover:text-color-emphasis gap-8 items-center"
:class="{
'flex justify-between items-center hover:text-color-emphasis': true,
'bg-red-700/10': selectedIds.includes(duplicate.photo_id),
}"
@mouseover="hover(duplicate.url ?? '', duplicate.photo_title)"
>
<div class="w-1/4 flex items-center gap-2 group">
<div class="flex-shrink">
<i
class="pi pi-trash mr-2"
:class="{
'text-red-700': selectedIds.includes(duplicate.photo_id),
'text-transparent': !selectedIds.includes(duplicate.photo_id),
}"
/>
</div>
<div class="w-1/3 flex-none flex items-center gap-2 group">
<router-link :to="{ name: 'album', params: { albumid: duplicate.album_id } }" target="_blank" class="">
<i class="pi pi-link text-primary-emphasis hover:text-primary-emphasis-alt"></i>
</router-link>
<span class="w-full inline-block whitespace-nowrap text-ellipsis overflow-hidden">
<span
class="w-full inline-block whitespace-nowrap text-ellipsis overflow-hidden cursor-pointer"
@click="click(duplicate.photo_id)"
>
{{ duplicate.album_title }}
</span>
</div>
<div class="w-1/4 flex gap-2 group">
<div class="w-1/3 flex-none flex gap-2 group">
<router-link
:to="{ name: 'photo', params: { albumid: duplicate.album_id, photoid: duplicate.photo_id } }"
target="_blank"
class=""
>
<i class="pi pi-link text-primary-emphasis hover:text-primary-emphasis-alt"></i>
</router-link>
<span class="w-full inline-block whitespace-nowrap text-ellipsis overflow-hidden">
<span
class="w-full inline-block whitespace-nowrap text-ellipsis overflow-hidden cursor-pointer"
@click="click(duplicate.photo_id)"
>
{{ duplicate.photo_title }}
</span>
</div>
<div class="w-1/4 font-mono text-xs">{{ duplicate.checksum.slice(0, 12) }}</div>
<div class="w-1/4 font-mono text-xs cursor-pointer" @click="click(duplicate.photo_id)">{{ duplicate.checksum.slice(0, 12) }}</div>
</div>
</template>
</div>
Expand All @@ -35,13 +53,19 @@ import { type SplitData } from "@/composables/album/splitter";
const props = defineProps<{
duplicates: SplitData<App.Http.Resources.Models.Duplicates.Duplicate>;
selectedIds: string[];
}>();
const emits = defineEmits<{
hover: [src: string, title: string];
click: [id: string];
}>();
function hover(url: string, title: string) {
emits("hover", url, title);
}
function click(id: string) {
emits("click", id);
}
</script>
61 changes: 44 additions & 17 deletions resources/js/views/DuplicatesFinder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,42 +63,45 @@
</li>
</ul>
</div>
<div class="flex justify-between md:max-w-3xl lg:max-w-5xl xl:max-w-7xl mx-auto gap-4 xl:gap-8">
<div class="w-1/4 flex-none"></div>
<div class="pb-2 w-full flex justify-between items-center font-bold text-lg text-color-emphasis border-b border-b-white/50">
<div class=""><i class="pi pi-trash text-transparent mr-2" /></div>
<div class="w-1/3">Album</div>
<div class="w-1/3">Photo</div>
<div class="w-1/4">Checksum</div>
</div>
</div>
<div class="flex justify-between md:max-w-3xl lg:max-w-5xl xl:max-w-7xl mx-auto gap-4 xl:gap-8">
<div class="w-1/4 flex flex-col flex-none">
<Button
severity="danger"
class="w-full font-bold border-none mb-4"
@click="isDeleteVisible = true"
:disabled="selectedIds.length === 0"
>{{ $t("Delete selected") }}</Button
>

<img :src="hoverImgSrc" class="w-full" />
<div class="text-center mt-2 font-bold text-muted-color-emphasis">
<span class="inline-block w-full text-ellipsis text-nowrap whitespace-nowrap overflow-hidden">{{ hoverTitle }}</span>
</div>
</div>
<VirtualScroller :items="groupedDuplicates" :itemSize="10" class="h-screen w-full">
<VirtualScroller :items="groupedDuplicates" :itemSize="50" class="h-screen w-full">
<template v-slot:item="{ item, options }">
<template v-if="options.first">
<div
class="pb-2 flex justify-between md:max-w-3xl lg:max-w-5xl xl:max-w-7xl mx-auto hover:bg-primary-emphasis/5 gap-8 items-center font-bold text-lg text-color-emphasis border-b border-b-white/50"
>
<div class="w-1/4">Album</div>
<div class="w-1/4">Photo</div>
<div class="w-1/4">Checksum</div>
<!-- <div class="flex w-1/4 justify-between">
<div class="w-full">Id</div>
<div class="w-full text-right">Parent Id</div>
</div> -->
</div>
</template>
<DuplicateLine :duplicates="item" @hover="onHover" />
<DuplicateLine :duplicates="item" :selected-ids="selectedIds" @hover="onHover" @click="onClick" />
</template>
</VirtualScroller>
</div>
</div>
<ScrollTop />
<DeleteDialog v-model:visible="isDeleteVisible" :parent-id="undefined" :photo-ids="selectedIds" @deleted="onDeleted" />
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import MaintenanceService from "@/services/maintenance-service";
import Toolbar from "primevue/toolbar";
import ProgressBar from "primevue/progressbar";
import Button from "primevue/button";
import ScrollTop from "primevue/scrolltop";
import VirtualScroller from "primevue/virtualscroller";
import { useToast } from "primevue/usetoast";
import Checkbox from "primevue/checkbox";
Expand All @@ -108,9 +111,12 @@ import { useLycheeStateStore } from "@/stores/LycheeState";
import { storeToRefs } from "pinia";
import SETag from "@/components/icons/SETag.vue";
import { type SplitData, useSplitter } from "@/composables/album/splitter";
import DeleteDialog from "@/components/forms/gallery-dialogs/DeleteDialog.vue";
const duplicates = ref<App.Http.Resources.Models.Duplicates.Duplicate[] | undefined>(undefined);
const groupedDuplicates = ref<SplitData<App.Http.Resources.Models.Duplicates.Duplicate>[] | undefined>(undefined);
const isDeleteVisible = ref(false);
const selectedIds = ref<string[]>([]);
const toast = useToast();
const lycheeStore = useLycheeStateStore();
Expand All @@ -130,13 +136,34 @@ function onHover(src: string, title: string) {
hoverTitle.value = title;
}
function onClick(id: string) {
const index = selectedIds.value.indexOf(id);
if (index === -1) {
selectedIds.value.push(id);
} else {
selectedIds.value.splice(index, 1);
}
}
function onDeleted() {
// Remove the deleted photos (no need to fetch data again).
duplicates.value = duplicates.value?.filter((duplicate) => !selectedIds.value.includes(duplicate.photo_id));
groupData();
// Remove groups with only one element => no duplicates
groupedDuplicates.value = groupedDuplicates.value?.filter((group) => group.data.length > 1);
}
function fetch() {
if (!isValid.value) {
return;
}
duplicates.value = undefined;
groupedDuplicates.value = undefined;
selectedIds.value = [];
hoverImgSrc.value = "";
hoverTitle.value = "";
MaintenanceService.getDuplicates(withAlbumConstraint.value, withChecksumConstraint.value, withTitleConstraint.value).then((response) => {
duplicates.value = response.data;
Expand Down

0 comments on commit 77816e1

Please sign in to comment.