- Correction du dropdown toolbar : prop align (left/right) pour éviter le débordement hors écran côté droit - Filtre de collection par statut (all/completed/ongoing) persisté dans userPreferencesStore - toolbarConfig rendu réactif (computed) avec isSelected sur Filter, Sort et View - Modale Options d'affichage par vue (Grille, Overview, Table) avec toggles persistés - Composant ToggleRow réutilisable - Normalisation author → authors dans l'entité Manga (l'API renvoie author string)
243 lines
12 KiB
Vue
243 lines
12 KiB
Vue
<template>
|
|
<div>
|
|
<div class="border-t border-gray-200 dark:border-gray-700">
|
|
<table class="w-full text-sm">
|
|
<thead>
|
|
<tr class="bg-gray-50 dark:bg-gray-700/50 border-b border-gray-200 dark:border-gray-700 text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
<th v-if="options.showMonitoring" class="w-10 px-4 py-3"></th>
|
|
<th class="py-3 pr-4 text-left font-medium">Titre</th>
|
|
<th v-if="options.showAuthor" class="py-3 pr-4 text-left font-medium w-36">Auteur</th>
|
|
<th v-if="options.showYear" class="py-3 pr-4 text-left font-medium w-20">Année</th>
|
|
<th v-if="options.showStatus" class="py-3 pr-4 text-left font-medium w-28">Statut</th>
|
|
<th v-if="options.showPreferredSource" class="py-3 pr-4 text-left font-medium w-44">Source préférée</th>
|
|
<th v-if="options.showChapters" class="py-3 pr-4 text-left font-medium w-44">Chapitres</th>
|
|
<th class="py-3 px-4 text-right font-medium w-28">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
<tr
|
|
v-for="manga in mangas"
|
|
:key="manga.id"
|
|
class="hover:bg-gray-50 dark:hover:bg-gray-700/40 transition-colors">
|
|
|
|
<!-- Monitoring -->
|
|
<td v-if="options.showMonitoring" class="px-4 py-3 text-center">
|
|
<button
|
|
:title="manga.monitored ? 'Monitoring actif — cliquer pour désactiver' : 'Monitoring inactif — cliquer pour activer'"
|
|
:class="manga.monitored
|
|
? 'text-green-500 hover:text-green-600'
|
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-400 dark:hover:text-gray-500'"
|
|
class="transition-colors"
|
|
@click="doToggleMonitoring(manga)">
|
|
<component
|
|
:is="manga.monitored ? BookmarkIcon : BookmarkSlashIcon"
|
|
class="w-4 h-4" />
|
|
</button>
|
|
</td>
|
|
|
|
<!-- Titre -->
|
|
<td class="py-3 pr-4">
|
|
<RouterLink
|
|
:to="{ name: 'manga-details', params: { id: manga.id } }"
|
|
class="font-medium text-gray-900 dark:text-gray-100 hover:text-green-500 dark:hover:text-green-400 transition-colors">
|
|
{{ manga.title }}
|
|
</RouterLink>
|
|
</td>
|
|
|
|
<!-- Auteur -->
|
|
<td v-if="options.showAuthor" class="py-3 pr-4">
|
|
<span class="text-sm text-gray-600 dark:text-gray-300">{{ manga.authors?.join(', ') || '—' }}</span>
|
|
</td>
|
|
|
|
<!-- Année -->
|
|
<td v-if="options.showYear" class="py-3 pr-4">
|
|
<span class="text-sm text-gray-600 dark:text-gray-300">{{ manga.publicationYear || '—' }}</span>
|
|
</td>
|
|
|
|
<!-- Statut -->
|
|
<td v-if="options.showStatus" class="py-3 pr-4">
|
|
<span
|
|
v-if="manga.status"
|
|
class="text-xs font-medium px-2 py-0.5 rounded-full"
|
|
:class="statusClass(manga.status)">
|
|
{{ manga.status }}
|
|
</span>
|
|
<span v-else class="text-gray-400 dark:text-gray-600 text-xs">—</span>
|
|
</td>
|
|
|
|
<!-- Source préférée -->
|
|
<td v-if="options.showPreferredSource" class="py-3 pr-4">
|
|
<MangaPreferredSourceCell :manga-id="manga.id" />
|
|
</td>
|
|
|
|
<!-- Chapitres — barre de progression -->
|
|
<td v-if="options.showChapters" class="py-3 pr-4">
|
|
<div v-if="manga.chaptersTotal > 0">
|
|
<div class="flex items-center justify-between mb-1">
|
|
<span class="text-xs tabular-nums text-gray-500 dark:text-gray-400">
|
|
{{ manga.chaptersScraped }} / {{ manga.chaptersTotal }}
|
|
</span>
|
|
<span class="text-xs text-gray-400 dark:text-gray-500">
|
|
{{ progressPercent(manga) }}%
|
|
</span>
|
|
</div>
|
|
<div class="w-full bg-gray-100 dark:bg-gray-600 rounded-full h-1.5">
|
|
<div
|
|
class="h-1.5 rounded-full transition-all"
|
|
:class="progressPercent(manga) >= 100
|
|
? 'bg-green-500'
|
|
: 'bg-blue-500'"
|
|
:style="{ width: progressPercent(manga) + '%' }" />
|
|
</div>
|
|
</div>
|
|
<span v-else class="text-gray-400 dark:text-gray-600 text-xs">—</span>
|
|
</td>
|
|
|
|
<!-- Actions -->
|
|
<td class="py-3 px-4">
|
|
<div class="flex items-center justify-end gap-0.5">
|
|
<button
|
|
class="p-1.5 rounded-md text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
|
|
title="Éditer"
|
|
@click="openEdit(manga)">
|
|
<PencilIcon class="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
class="p-1.5 rounded-md text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
|
|
title="Sources préférées"
|
|
@click="openSources(manga)">
|
|
<Cog6ToothIcon class="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
class="p-1.5 rounded-md transition-colors"
|
|
:class="refreshingId === manga.id
|
|
? 'text-blue-400 cursor-not-allowed'
|
|
: 'text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600'"
|
|
title="Rafraîchir"
|
|
:disabled="refreshingId === manga.id"
|
|
@click="doRefresh(manga)">
|
|
<ArrowPathIcon
|
|
class="w-4 h-4"
|
|
:class="{ 'animate-spin': refreshingId === manga.id }" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Modales -->
|
|
<MangaEditModal
|
|
:is-open="isEditModalOpen"
|
|
:manga="selectedManga"
|
|
:is-saving="editIsLoading"
|
|
:error="editError"
|
|
@close="closeEditModal"
|
|
@save="handleSaveEdit" />
|
|
|
|
<MangaPreferredSourcesModal
|
|
:is-open="isSourcesModalOpen"
|
|
:sources="preferredSources"
|
|
:is-loading="sourcesIsLoading"
|
|
:error="sourcesError"
|
|
:is-saving="sourcesIsSaving"
|
|
@close="isSourcesModalOpen = false"
|
|
@save="handleSaveSources" />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ArrowPathIcon, BookmarkIcon, BookmarkSlashIcon, Cog6ToothIcon, PencilIcon } from '@heroicons/vue/24/outline';
|
|
import { computed, ref } from 'vue';
|
|
import { RouterLink } from 'vue-router';
|
|
import { useMangaEdit } from '../composables/useMangaEdit';
|
|
import { useMangaMonitoring } from '../composables/useMangaMonitoring';
|
|
import { useMangaPreferredSources } from '../composables/useMangaPreferredSources';
|
|
import { useMangaRefresh } from '../composables/useMangaRefresh';
|
|
import MangaEditModal from './MangaEditModal.vue';
|
|
import MangaPreferredSourceCell from './MangaPreferredSourceCell.vue';
|
|
import MangaPreferredSourcesModal from './MangaPreferredSourcesModal.vue';
|
|
|
|
const props = defineProps({
|
|
mangas: {
|
|
type: Array,
|
|
required: true
|
|
},
|
|
options: {
|
|
type: Object,
|
|
default: () => ({ showMonitoring: true, showPreferredSource: true, showChapters: true, showStatus: false, showAuthor: false, showYear: false })
|
|
}
|
|
});
|
|
|
|
function statusClass(status) {
|
|
if (status === 'ongoing') return 'text-blue-600 bg-blue-50 dark:bg-blue-900/20';
|
|
if (status === 'completed') return 'text-green-600 bg-green-50 dark:bg-green-900/20';
|
|
return 'text-gray-500 bg-gray-100 dark:bg-gray-700';
|
|
}
|
|
|
|
function progressPercent(manga) {
|
|
if (!manga.chaptersTotal) return 0;
|
|
return Math.round((manga.chaptersScraped / manga.chaptersTotal) * 100);
|
|
}
|
|
|
|
// ── Monitoring ────────────────────────────────────────────
|
|
const { toggleMonitoring } = useMangaMonitoring();
|
|
|
|
async function doToggleMonitoring(manga) {
|
|
await toggleMonitoring(manga.id, !manga.monitored);
|
|
manga.monitored = !manga.monitored;
|
|
}
|
|
|
|
// ── Selected manga ────────────────────────────────────────
|
|
const selectedManga = ref(null);
|
|
const isSourcesModalOpen = ref(false);
|
|
|
|
// ── Edit ──────────────────────────────────────────────────
|
|
const { isEditModalOpen, openEditModal, closeEditModal, editManga, isLoading: editIsLoading, error: editError } = useMangaEdit();
|
|
|
|
function openEdit(manga) {
|
|
selectedManga.value = manga;
|
|
openEditModal();
|
|
}
|
|
|
|
async function handleSaveEdit(data) {
|
|
if (!selectedManga.value) return;
|
|
await editManga(selectedManga.value.id, data);
|
|
}
|
|
|
|
// ── Sources préférées ─────────────────────────────────────
|
|
const selectedMangaId = computed(() => selectedManga.value?.id ?? null);
|
|
const {
|
|
sources: preferredSources,
|
|
isLoading: sourcesIsLoading,
|
|
error: sourcesError,
|
|
isSaving: sourcesIsSaving,
|
|
savePreferredSources
|
|
} = useMangaPreferredSources(selectedMangaId);
|
|
|
|
function openSources(manga) {
|
|
selectedManga.value = manga;
|
|
isSourcesModalOpen.value = true;
|
|
}
|
|
|
|
function handleSaveSources(sourceIds) {
|
|
savePreferredSources(sourceIds);
|
|
isSourcesModalOpen.value = false;
|
|
}
|
|
|
|
// ── Refresh ───────────────────────────────────────────────
|
|
const { refreshMetadata } = useMangaRefresh();
|
|
const refreshingId = ref(null);
|
|
|
|
async function doRefresh(manga) {
|
|
if (refreshingId.value) return;
|
|
refreshingId.value = manga.id;
|
|
try {
|
|
await refreshMetadata(manga.id);
|
|
} finally {
|
|
refreshingId.value = null;
|
|
}
|
|
}
|
|
</script>
|