feat: dark mode complet + préférences utilisateur #7

Merged
colgora merged 1 commits from feature/dark-mode-user-preferences into main 2026-03-12 20:45:19 +01:00
36 changed files with 2832 additions and 317 deletions

View File

@@ -1,11 +1,11 @@
<template> <template>
<tr <tr
class="border-b border-gray-200 hover:bg-gray-50 transition duration-150 ease-in-out" class="border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition duration-150 ease-in-out"
:class="{ :class="{
'bg-yellow-50': job.status === 'pending', 'bg-yellow-50 dark:bg-yellow-900/20': job.status === 'pending',
'bg-blue-50': job.status === 'in_progress', 'bg-blue-50 dark:bg-blue-900/20': job.status === 'in_progress',
'bg-green-50': job.status === 'completed', 'bg-green-50 dark:bg-green-900/20': job.status === 'completed',
'bg-red-50': job.status === 'failed' 'bg-red-50 dark:bg-red-900/20': job.status === 'failed'
}"> }">
<td class="py-4 px-4 text-center"> <td class="py-4 px-4 text-center">
<input type="checkbox" class="form-checkbox h-5 w-5 text-green-600" /> <input type="checkbox" class="form-checkbox h-5 w-5 text-green-600" />
@@ -20,37 +20,37 @@
<span <span
class="px-2 py-1 text-xs rounded-full" class="px-2 py-1 text-xs rounded-full"
:class="{ :class="{
'bg-yellow-100 text-yellow-800': job.status === 'pending', 'bg-yellow-100 dark:bg-yellow-900/40 text-yellow-800 dark:text-yellow-300': job.status === 'pending',
'bg-blue-100 text-blue-800': job.status === 'in_progress', 'bg-blue-100 dark:bg-blue-900/40 text-blue-800 dark:text-blue-300': job.status === 'in_progress',
'bg-green-100 text-green-800': job.status === 'completed', 'bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-300': job.status === 'completed',
'bg-red-100 text-red-800': job.status === 'failed' 'bg-red-100 dark:bg-red-900/40 text-red-800 dark:text-red-300': job.status === 'failed'
}"> }">
{{ job.status }} {{ job.status }}
</span> </span>
</td> </td>
<td class="py-4 px-4"> <td class="py-4 px-4">
<div v-if="job.error" class="text-sm text-red-600"> <div v-if="job.error" class="text-sm text-red-600 dark:text-red-400">
{{ job.error }} {{ job.error }}
</div> </div>
<div v-else-if="job.context?.mangaTitle || job.context?.chapterNumber !== undefined || job.context?.sourceId" <div v-else-if="job.context?.mangaTitle || job.context?.chapterNumber !== undefined || job.context?.sourceId"
class="text-sm text-gray-700 space-y-0.5"> class="text-sm text-gray-700 dark:text-gray-300 space-y-0.5">
<div v-if="job.context.mangaTitle" class="font-medium"> <div v-if="job.context.mangaTitle" class="font-medium">
{{ job.context.mangaTitle }} {{ job.context.mangaTitle }}
</div> </div>
<div v-if="job.context.chapterNumber !== undefined" class="text-gray-500"> <div v-if="job.context.chapterNumber !== undefined" class="text-gray-500 dark:text-gray-400">
Chapitre {{ job.context.chapterNumber }} Chapitre {{ job.context.chapterNumber }}
</div> </div>
<div v-if="job.context.sourceId" class="text-xs text-gray-400"> <div v-if="job.context.sourceId" class="text-xs text-gray-400 dark:text-gray-500">
Source : {{ job.context.sourceId }} Source : {{ job.context.sourceId }}
</div> </div>
</div> </div>
<div v-else class="text-sm text-gray-600"> <div v-else class="text-sm text-gray-600 dark:text-gray-400">
{{ formatDate(job.createdAt) }} {{ formatDate(job.createdAt) }}
</div> </div>
</td> </td>
<td class="py-4 px-4"> <td class="py-4 px-4">
<div v-if="job.status === 'in_progress'" class="mt-2"> <div v-if="job.status === 'in_progress'" class="mt-2">
<div class="relative bg-gray-200 rounded-full h-6 overflow-hidden"> <div class="relative bg-gray-200 dark:bg-gray-700 rounded-full h-6 overflow-hidden">
<div <div
class="absolute top-0 left-0 h-full bg-green-400 transition-all duration-300 ease-out" class="absolute top-0 left-0 h-full bg-green-400 transition-all duration-300 ease-out"
:style="{ width: `${job.progress}%` }"></div> :style="{ width: `${job.progress}%` }"></div>
@@ -59,7 +59,7 @@
</div> </div>
</div> </div>
</div> </div>
<div v-else-if="job.status === 'completed'" class="relative bg-gray-200 rounded-full h-6 overflow-hidden"> <div v-else-if="job.status === 'completed'" class="relative bg-gray-200 dark:bg-gray-700 rounded-full h-6 overflow-hidden">
<div <div
class="absolute top-0 left-0 h-full bg-green-400 transition-all duration-300 ease-out" class="absolute top-0 left-0 h-full bg-green-400 transition-all duration-300 ease-out"
style="width: 100%"></div> style="width: 100%"></div>
@@ -67,7 +67,7 @@
100% 100%
</div> </div>
</div> </div>
<div v-else-if="job.status === 'failed'" class="relative bg-gray-200 rounded-full h-6 overflow-hidden"> <div v-else-if="job.status === 'failed'" class="relative bg-gray-200 dark:bg-gray-700 rounded-full h-6 overflow-hidden">
<div <div
class="absolute top-0 left-0 h-full bg-red-400 transition-all duration-300 ease-out" class="absolute top-0 left-0 h-full bg-red-400 transition-all duration-300 ease-out"
style="width: 100%"></div> style="width: 100%"></div>
@@ -75,17 +75,17 @@
Erreur Erreur
</div> </div>
</div> </div>
<div v-else class="relative bg-gray-200 rounded-full h-6 overflow-hidden"> <div v-else class="relative bg-gray-200 dark:bg-gray-700 rounded-full h-6 overflow-hidden">
<div <div
class="absolute top-0 left-0 h-full bg-yellow-400 transition-all duration-300 ease-out" class="absolute top-0 left-0 h-full bg-yellow-400 transition-all duration-300 ease-out"
style="width: 0%"></div> style="width: 0%"></div>
<div class="absolute inset-0 flex items-center justify-center text-xs font-semibold text-gray-600"> <div class="absolute inset-0 flex items-center justify-center text-xs font-semibold text-gray-600 dark:text-gray-300">
En attente En attente
</div> </div>
</div> </div>
<div v-if="job.maxAttempts > 1 || job.attempts > 0" <div v-if="job.maxAttempts > 1 || job.attempts > 0"
class="text-xs text-gray-400 mt-1 text-center"> class="text-xs text-gray-400 dark:text-gray-500 mt-1 text-center">
{{ job.attempts }} / {{ job.maxAttempts }} tentative{{ job.maxAttempts > 1 ? 's' : '' }} {{ job.attempts }} / {{ job.maxAttempts }} tentative{{ job.maxAttempts > 1 ? 's' : '' }}
</div> </div>
</td> </td>

View File

@@ -6,16 +6,16 @@
<div class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-indigo-500"></div> <div class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-indigo-500"></div>
</div> </div>
<div v-else-if="activityStore.error" class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6"> <div v-else-if="activityStore.error" class="bg-red-100 dark:bg-red-900/20 border-l-4 border-red-500 text-red-700 dark:text-red-400 p-4 mb-6">
<p>{{ activityStore.error }}</p> <p>{{ activityStore.error }}</p>
</div> </div>
<div v-else class="container mx-auto p-2"> <div v-else class="container mx-auto p-2">
<div class="bg-white overflow-hidden shadow rounded-lg"> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full bg-white"> <table class="min-w-full bg-white dark:bg-gray-800">
<thead> <thead>
<tr class="bg-gray-200 text-gray-800"> <tr class="bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200">
<th class="w-1/12 py-3 px-4 text-left"> <th class="w-1/12 py-3 px-4 text-left">
<input <input
type="checkbox" type="checkbox"
@@ -29,14 +29,14 @@
<th class="w-1/12 py-3 px-4 text-left">Actions</th> <th class="w-1/12 py-3 px-4 text-left">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody class="text-gray-700"> <tbody class="text-gray-700 dark:text-gray-300">
<template v-if="activityStore.jobs.length === 0"> <template v-if="activityStore.jobs.length === 0">
<tr> <tr>
<td colspan="6" class="py-8 px-4 text-center text-gray-500"> <td colspan="6" class="py-8 px-4 text-center text-gray-500">
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<ClockIcon class="h-12 w-12 text-gray-300 mb-4" /> <ClockIcon class="h-12 w-12 text-gray-300 dark:text-gray-600 mb-4" />
<p class="text-lg font-medium">Aucune activité trouvée</p> <p class="text-lg font-medium dark:text-gray-300">Aucune activité trouvée</p>
<p class="text-sm">Aucune activité ne correspond aux filtres actuels.</p> <p class="text-sm dark:text-gray-400">Aucune activité ne correspond aux filtres actuels.</p>
</div> </div>
</td> </td>
</tr> </tr>

View File

@@ -24,10 +24,10 @@
<!-- Message de statut --> <!-- Message de statut -->
<div class="flex-1"> <div class="flex-1">
<p class="text-sm font-medium text-gray-900"> <p class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ statusMessage }} {{ statusMessage }}
</p> </p>
<p v-if="fileName" class="text-xs text-gray-500"> <p v-if="fileName" class="text-xs text-gray-500 dark:text-gray-400">
{{ fileName }} {{ fileName }}
</p> </p>
</div> </div>
@@ -35,11 +35,11 @@
<!-- Barre de progression --> <!-- Barre de progression -->
<div v-if="showProgress" class="space-y-2"> <div v-if="showProgress" class="space-y-2">
<div class="flex justify-between text-xs text-gray-600"> <div class="flex justify-between text-xs text-gray-600 dark:text-gray-400">
<span>Progression</span> <span>Progression</span>
<span>{{ Math.round(progress) }}%</span> <span>{{ Math.round(progress) }}%</span>
</div> </div>
<div class="w-full bg-gray-200 rounded-full h-2"> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div <div
class="bg-blue-500 h-2 rounded-full transition-all duration-300 ease-out" class="bg-blue-500 h-2 rounded-full transition-all duration-300 ease-out"
:style="{ width: `${progress}%` }" :style="{ width: `${progress}%` }"
@@ -48,7 +48,7 @@
</div> </div>
<!-- Détails de la conversion --> <!-- Détails de la conversion -->
<div v-if="showDetails && (originalSize || convertedSize)" class="text-xs text-gray-500 space-y-1"> <div v-if="showDetails && (originalSize || convertedSize)" class="text-xs text-gray-500 dark:text-gray-400 space-y-1">
<div v-if="originalSize" class="flex justify-between"> <div v-if="originalSize" class="flex justify-between">
<span>Taille originale:</span> <span>Taille originale:</span>
<span>{{ formatFileSize(originalSize) }}</span> <span>{{ formatFileSize(originalSize) }}</span>
@@ -77,7 +77,7 @@
<button <button
v-if="canReset" v-if="canReset"
@click="$emit('reset')" @click="$emit('reset')"
class="flex items-center space-x-2 px-4 py-2 border border-gray-300 text-gray-700 text-sm font-medium rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors" class="flex items-center space-x-2 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 text-sm font-medium rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
> >
<ArrowPathIcon class="w-4 h-4" /> <ArrowPathIcon class="w-4 h-4" />
<span>Convertir un autre fichier</span> <span>Convertir un autre fichier</span>
@@ -85,14 +85,14 @@
</div> </div>
<!-- Message d'erreur détaillé --> <!-- Message d'erreur détaillé -->
<div v-if="hasError && errorMessage" class="p-3 bg-red-50 border border-red-200 rounded-md"> <div v-if="hasError && errorMessage" class="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md">
<div class="flex"> <div class="flex">
<ExclamationTriangleIcon class="w-5 h-5 text-red-400 flex-shrink-0" /> <ExclamationTriangleIcon class="w-5 h-5 text-red-400 flex-shrink-0" />
<div class="ml-3"> <div class="ml-3">
<h3 class="text-sm font-medium text-red-800"> <h3 class="text-sm font-medium text-red-800 dark:text-red-300">
Erreur de conversion Erreur de conversion
</h3> </h3>
<p class="mt-1 text-sm text-red-700"> <p class="mt-1 text-sm text-red-700 dark:text-red-400">
{{ errorMessage }} {{ errorMessage }}
</p> </p>
</div> </div>

View File

@@ -10,8 +10,8 @@
:class="[ :class="[
'border-2 border-dashed rounded-lg p-8 text-center transition-all duration-200', 'border-2 border-dashed rounded-lg p-8 text-center transition-all duration-200',
isDragOver isDragOver
? 'border-green-400 bg-green-50' ? 'border-green-400 bg-green-50 dark:bg-green-900/20'
: 'border-gray-300 hover:border-gray-400' : 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'
]" ]"
> >
<!-- Zone d'upload --> <!-- Zone d'upload -->
@@ -28,13 +28,13 @@
<!-- Message principal --> <!-- Message principal -->
<div class="space-y-2"> <div class="space-y-2">
<h3 class="text-lg font-medium text-gray-900"> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ isDragOver ? 'Déposez votre fichier ici' : 'Sélectionnez un fichier CBR ou CBZ' }} {{ isDragOver ? 'Déposez votre fichier ici' : 'Sélectionnez un fichier CBR ou CBZ' }}
</h3> </h3>
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500 dark:text-gray-400">
Glissez-déposez votre fichier ou cliquez pour le sélectionner Glissez-déposez votre fichier ou cliquez pour le sélectionner
</p> </p>
<p class="text-xs text-gray-400"> <p class="text-xs text-gray-400 dark:text-gray-500">
Fichiers supportés: .cbr, .cbz (max. 150MB) Fichiers supportés: .cbr, .cbz (max. 150MB)
</p> </p>
</div> </div>
@@ -63,20 +63,20 @@
</div> </div>
<!-- Informations du fichier sélectionné --> <!-- Informations du fichier sélectionné -->
<div v-if="selectedFile" class="mt-6 p-4 bg-gray-50 rounded-lg"> <div v-if="selectedFile" class="mt-6 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<DocumentIcon class="w-8 h-8 text-gray-600" /> <DocumentIcon class="w-8 h-8 text-gray-600 dark:text-gray-400" />
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900 truncate"> <p class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{{ selectedFile.name }} {{ selectedFile.name }}
</p> </p>
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500 dark:text-gray-400">
{{ formatFileSize(selectedFile.size) }} {{ formatFileSize(selectedFile.size) }}
</p> </p>
</div> </div>
<button <button
@click="clearFile" @click="clearFile"
class="p-1 text-gray-400 hover:text-gray-600 transition-colors" class="p-1 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
title="Supprimer le fichier" title="Supprimer le fichier"
> >
<XMarkIcon class="w-5 h-5" /> <XMarkIcon class="w-5 h-5" />

View File

@@ -4,17 +4,17 @@
<div class="mb-8"> <div class="mb-8">
<div class="flex items-center space-x-3 mb-4"> <div class="flex items-center space-x-3 mb-4">
<ArrowPathIcon class="w-8 h-8 text-green-600" /> <ArrowPathIcon class="w-8 h-8 text-green-600" />
<h1 class="text-3xl font-bold text-gray-900"> <h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">
Convertir CBR en CBZ Convertir CBR en CBZ
</h1> </h1>
</div> </div>
<p class="text-lg text-gray-600"> <p class="text-lg text-gray-600 dark:text-gray-400">
Convertissez vos fichiers CBR (Comic Book RAR) en CBZ (Comic Book ZIP) pour une meilleure compatibilité. Convertissez vos fichiers CBR (Comic Book RAR) en CBZ (Comic Book ZIP) pour une meilleure compatibilité.
</p> </p>
</div> </div>
<!-- Zone principale --> <!-- Zone principale -->
<div class="bg-white shadow-lg rounded-lg overflow-hidden"> <div class="bg-white dark:bg-gray-800 shadow-lg rounded-lg overflow-hidden">
<!-- En-tête de la carte --> <!-- En-tête de la carte -->
<div class="bg-gray-800 text-white p-6"> <div class="bg-gray-800 text-white p-6">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
@@ -75,14 +75,14 @@
/> />
<!-- Message d'information --> <!-- Message d'information -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4"> <div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<div class="flex"> <div class="flex">
<InformationCircleIcon class="w-5 h-5 text-blue-500 flex-shrink-0" /> <InformationCircleIcon class="w-5 h-5 text-blue-500 flex-shrink-0" />
<div class="ml-3"> <div class="ml-3">
<h3 class="text-sm font-medium text-blue-800"> <h3 class="text-sm font-medium text-blue-800 dark:text-blue-300">
À propos de la conversion À propos de la conversion
</h3> </h3>
<div class="mt-2 text-sm text-blue-700 space-y-1"> <div class="mt-2 text-sm text-blue-700 dark:text-blue-400 space-y-1">
<p> Les fichiers CBZ sont plus largement supportés par les lecteurs de bandes dessinées</p> <p> Les fichiers CBZ sont plus largement supportés par les lecteurs de bandes dessinées</p>
<p> La compression ZIP permet généralement une meilleure accessibilité</p> <p> La compression ZIP permet généralement une meilleure accessibilité</p>
<p> Aucune perte de qualité lors de la conversion</p> <p> Aucune perte de qualité lors de la conversion</p>
@@ -95,34 +95,34 @@
<!-- Historique des conversions --> <!-- Historique des conversions -->
<div v-if="conversionStore.conversionCount > 0" class="space-y-4"> <div v-if="conversionStore.conversionCount > 0" class="space-y-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900"> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
Historique des conversions Historique des conversions
</h3> </h3>
<button <button
@click="handleClearHistory" @click="handleClearHistory"
class="text-sm text-gray-500 hover:text-gray-700 transition-colors" class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors"
> >
Effacer l'historique Effacer l'historique
</button> </button>
</div> </div>
<div class="bg-gray-50 rounded-lg p-4"> <div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
<div class="space-y-3"> <div class="space-y-3">
<div <div
v-for="(conversion, index) in conversionStore.conversionHistory" v-for="(conversion, index) in conversionStore.conversionHistory"
:key="index" :key="index"
class="flex items-center justify-between py-2 border-b border-gray-200 last:border-b-0" class="flex items-center justify-between py-2 border-b border-gray-200 dark:border-gray-600 last:border-b-0"
> >
<div class="flex-1"> <div class="flex-1">
<p class="text-sm font-medium text-gray-900"> <p class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ conversion.originalName }} {{ conversion.originalName }}
</p> </p>
<p class="text-xs text-gray-500"> <p class="text-xs text-gray-500 dark:text-gray-400">
{{ formatDate(conversion.timestamp) }} {{ formatDate(conversion.timestamp) }}
</p> </p>
</div> </div>
<div class="text-right"> <div class="text-right">
<p class="text-sm text-gray-600"> <p class="text-sm text-gray-600 dark:text-gray-300">
{{ formatFileSize(conversion.originalSize) }} → {{ formatFileSize(conversion.convertedSize) }} {{ formatFileSize(conversion.originalSize) }} → {{ formatFileSize(conversion.convertedSize) }}
</p> </p>
<p class="text-xs text-green-600"> <p class="text-xs text-green-600">

View File

@@ -1,10 +1,10 @@
<template> <template>
<div class="bg-white rounded-lg shadow-sm border p-6"> <div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border dark:border-gray-700 p-6">
<div class="flex items-start space-x-4"> <div class="flex items-start space-x-4">
<!-- File Icon and Info --> <!-- File Icon and Info -->
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<div class="w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center"> <div class="w-12 h-12 bg-gray-100 dark:bg-gray-700 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg> </svg>
</div> </div>
@@ -13,7 +13,7 @@
<!-- File Details --> <!-- File Details -->
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900 truncate"> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 truncate">
{{ file.filename }} {{ file.filename }}
</h3> </h3>
@@ -23,29 +23,29 @@
</div> </div>
</div> </div>
<p class="text-sm text-gray-500 mt-1"> <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
{{ file.getFormattedSize() }} {{ file.getFileExtension().toUpperCase() }} {{ file.getFormattedSize() }} {{ file.getFileExtension().toUpperCase() }}
</p> </p>
<!-- Extracted Info --> <!-- Extracted Info -->
<div v-if="file.isAnalyzed()" class="mt-2 flex gap-3 text-sm"> <div v-if="file.isAnalyzed()" class="mt-2 flex gap-3 text-sm">
<span v-if="file.getExtractedChapterNumber()" class="inline-flex items-center px-2 py-1 rounded-md bg-blue-50 text-blue-700"> <span v-if="file.getExtractedChapterNumber()" class="inline-flex items-center px-2 py-1 rounded-md bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300">
Chapitre {{ file.getExtractedChapterNumber() }} Chapitre {{ file.getExtractedChapterNumber() }}
</span> </span>
<span v-if="file.getExtractedVolumeNumber()" class="inline-flex items-center px-2 py-1 rounded-md bg-purple-50 text-purple-700"> <span v-if="file.getExtractedVolumeNumber()" class="inline-flex items-center px-2 py-1 rounded-md bg-purple-50 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300">
Volume {{ file.getExtractedVolumeNumber() }} Volume {{ file.getExtractedVolumeNumber() }}
</span> </span>
</div> </div>
<!-- Error Display --> <!-- Error Display -->
<div v-if="file.hasError()" class="mt-3 p-3 bg-red-50 border border-red-200 rounded-md"> <div v-if="file.hasError()" class="mt-3 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md">
<div class="flex"> <div class="flex">
<svg class="flex-shrink-0 h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20"> <svg class="flex-shrink-0 h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg> </svg>
<div class="ml-3"> <div class="ml-3">
<h3 class="text-sm font-medium text-red-800">Erreur</h3> <h3 class="text-sm font-medium text-red-800 dark:text-red-300">Erreur</h3>
<div class="mt-2 text-sm text-red-700">{{ file.errorMessage }}</div> <div class="mt-2 text-sm text-red-700 dark:text-red-400">{{ file.errorMessage }}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -53,7 +53,7 @@
<!-- Manga Selection --> <!-- Manga Selection -->
<div v-if="file.isAnalyzed() && file.hasMatches()" class="mt-4 space-y-3"> <div v-if="file.isAnalyzed() && file.hasMatches()" class="mt-4 space-y-3">
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-3"> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Sélectionner un manga ({{ file.getMatches().length }} correspondance(s) trouvée(s)) Sélectionner un manga ({{ file.getMatches().length }} correspondance(s) trouvée(s))
</label> </label>
@@ -70,7 +70,7 @@
</div> </div>
<!-- Selected Manga Preview --> <!-- Selected Manga Preview -->
<div v-if="file.selectedManga" class="flex items-center gap-3 p-3 bg-blue-50 border border-blue-200 rounded-md"> <div v-if="file.selectedManga" class="flex items-center gap-3 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md">
<img <img
v-if="file.selectedManga.thumbnailUrl" v-if="file.selectedManga.thumbnailUrl"
:src="file.selectedManga.thumbnailUrl" :src="file.selectedManga.thumbnailUrl"
@@ -78,9 +78,9 @@
class="w-12 h-16 object-cover rounded" class="w-12 h-16 object-cover rounded"
/> />
<div class="flex-1"> <div class="flex-1">
<p class="font-medium text-gray-900">{{ file.selectedManga.title }}</p> <p class="font-medium text-gray-900 dark:text-gray-100">{{ file.selectedManga.title }}</p>
<p class="text-sm text-gray-500">{{ file.selectedManga.slug }}</p> <p class="text-sm text-gray-500 dark:text-gray-400">{{ file.selectedManga.slug }}</p>
<p class="text-xs text-blue-600 mt-1">Score: {{ file.selectedManga.matchScore }}%</p> <p class="text-xs text-blue-600 dark:text-blue-400 mt-1">Score: {{ file.selectedManga.matchScore }}%</p>
</div> </div>
</div> </div>
@@ -88,7 +88,7 @@
<div v-if="file.selectedManga" class="grid grid-cols-2 gap-3"> <div v-if="file.selectedManga" class="grid grid-cols-2 gap-3">
<!-- Chapter Number --> <!-- Chapter Number -->
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2"> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Numéro de chapitre Numéro de chapitre
</label> </label>
<input <input
@@ -97,14 +97,14 @@
:value="file.selectedChapterNumber ?? ''" :value="file.selectedChapterNumber ?? ''"
@input="handleChapterNumberInput" @input="handleChapterNumberInput"
:disabled="file.selectedVolumeNumber !== null" :disabled="file.selectedVolumeNumber !== null"
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100" class="w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-600"
placeholder="Ex: 1, 1.5, 2..." placeholder="Ex: 1, 1.5, 2..."
/> />
</div> </div>
<!-- Volume Number --> <!-- Volume Number -->
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2"> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Numéro de volume Numéro de volume
</label> </label>
<input <input
@@ -113,7 +113,7 @@
:value="file.selectedVolumeNumber ?? ''" :value="file.selectedVolumeNumber ?? ''"
@input="handleVolumeNumberInput" @input="handleVolumeNumberInput"
:disabled="file.selectedChapterNumber !== null" :disabled="file.selectedChapterNumber !== null"
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100" class="w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-600"
placeholder="Ex: 1, 1.5, 2..." placeholder="Ex: 1, 1.5, 2..."
/> />
</div> </div>
@@ -121,14 +121,14 @@
</div> </div>
<!-- No Matches Message --> <!-- No Matches Message -->
<div v-if="file.isAnalyzed() && !file.hasMatches()" class="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-md"> <div v-if="file.isAnalyzed() && !file.hasMatches()" class="mt-4 p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-md">
<div class="flex"> <div class="flex">
<svg class="flex-shrink-0 h-5 w-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20"> <svg class="flex-shrink-0 h-5 w-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg> </svg>
<div class="ml-3"> <div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800">Aucun manga trouvé</h3> <h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-300">Aucun manga trouvé</h3>
<div class="mt-2 text-sm text-yellow-700"> <div class="mt-2 text-sm text-yellow-700 dark:text-yellow-400">
Aucun manga ne correspond à ce fichier. Vérifiez le nom du fichier. Aucun manga ne correspond à ce fichier. Vérifiez le nom du fichier.
</div> </div>
</div> </div>
@@ -138,7 +138,7 @@
</div> </div>
<!-- Actions --> <!-- Actions -->
<div class="mt-6 flex justify-between items-center"> <div class="mt-6 flex justify-between items-center border-t dark:border-gray-700 pt-4">
<div class="flex space-x-3"> <div class="flex space-x-3">
<!-- Import Button --> <!-- Import Button -->
<button <button

View File

@@ -1,13 +1,13 @@
<template> <template>
<div class="bg-white rounded-lg shadow-sm border p-6"> <div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border dark:border-gray-700 p-6">
<div class="text-center mb-6"> <div class="text-center mb-6">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 mb-4"> <div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100 dark:bg-green-900/40 mb-4">
<svg class="h-6 w-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-6 w-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
</div> </div>
<h3 class="text-lg font-medium text-gray-900 mb-2">Import terminé</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">Import terminé</h3>
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500 dark:text-gray-400">
Voici le résumé de votre session d'import Voici le résumé de votre session d'import
</p> </p>
</div> </div>
@@ -16,7 +16,7 @@
<div class="grid grid-cols-3 gap-4 mb-6"> <div class="grid grid-cols-3 gap-4 mb-6">
<div class="text-center"> <div class="text-center">
<div class="text-2xl font-bold text-green-600">{{ importedCount }}</div> <div class="text-2xl font-bold text-green-600">{{ importedCount }}</div>
<div class="text-sm text-gray-500">Importés</div> <div class="text-sm text-gray-500 dark:text-gray-400">Importés</div>
</div> </div>
<div class="text-center"> <div class="text-center">
<div class="text-2xl font-bold text-red-600">{{ errorCount }}</div> <div class="text-2xl font-bold text-red-600">{{ errorCount }}</div>
@@ -30,7 +30,7 @@
<!-- Success Files List --> <!-- Success Files List -->
<div v-if="importedFiles.length > 0" class="mb-6"> <div v-if="importedFiles.length > 0" class="mb-6">
<h4 class="text-sm font-medium text-gray-900 mb-3"> <h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
Fichiers importés avec succès ({{ importedFiles.length }}) Fichiers importés avec succès ({{ importedFiles.length }})
</h4> </h4>
<ul class="space-y-2"> <ul class="space-y-2">
@@ -42,8 +42,8 @@
<svg class="flex-shrink-0 h-4 w-4 text-green-400 mr-2" fill="currentColor" viewBox="0 0 20 20"> <svg class="flex-shrink-0 h-4 w-4 text-green-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg> </svg>
<span class="text-gray-900">{{ file.filename }}</span> <span class="text-gray-900 dark:text-gray-100">{{ file.filename }}</span>
<span v-if="file.selectedManga" class="ml-2 text-gray-500"> <span v-if="file.selectedManga" class="ml-2 text-gray-500 dark:text-gray-400">
→ {{ file.selectedManga.title }} → {{ file.selectedManga.title }}
</span> </span>
</li> </li>
@@ -52,7 +52,7 @@
<!-- Error Files List --> <!-- Error Files List -->
<div v-if="errorFiles.length > 0" class="mb-6"> <div v-if="errorFiles.length > 0" class="mb-6">
<h4 class="text-sm font-medium text-gray-900 mb-3"> <h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
Fichiers en erreur ({{ errorFiles.length }}) Fichiers en erreur ({{ errorFiles.length }})
</h4> </h4>
<ul class="space-y-2"> <ul class="space-y-2">
@@ -65,15 +65,15 @@
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg> </svg>
<div> <div>
<div class="text-gray-900">{{ file.filename }}</div> <div class="text-gray-900 dark:text-gray-100">{{ file.filename }}</div>
<div class="text-red-600 text-xs mt-1">{{ file.errorMessage }}</div> <div class="text-red-600 dark:text-red-400 text-xs mt-1">{{ file.errorMessage }}</div>
</div> </div>
</li> </li>
</ul> </ul>
</div> </div>
<!-- Actions --> <!-- Actions -->
<div class="flex justify-center space-x-4 pt-6 border-t"> <div class="flex justify-center space-x-4 pt-6 border-t dark:border-gray-700">
<button <button
@click="startNewImport" @click="startNewImport"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium"

View File

@@ -2,8 +2,8 @@
<div <div
class="border rounded-lg p-4 cursor-pointer transition-all duration-200 hover:shadow-md" class="border rounded-lg p-4 cursor-pointer transition-all duration-200 hover:shadow-md"
:class="{ :class="{
'border-blue-500 bg-blue-50': isSelected, 'border-blue-500 bg-blue-50 dark:bg-blue-900/20': isSelected,
'border-gray-200 hover:border-gray-300': !isSelected 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-500': !isSelected
}" }"
@click="$emit('select-match', match)" @click="$emit('select-match', match)"
> >
@@ -17,7 +17,7 @@
'bg-gray-300': !isSelected 'bg-gray-300': !isSelected
}" }"
></div> ></div>
<span class="text-sm font-medium text-gray-700">Score: {{ match.matchScore }}</span> <span class="text-sm font-medium text-gray-700 dark:text-gray-300">Score: {{ match.matchScore }}</span>
</div> </div>
<div v-if="isSelected" class="text-blue-600"> <div v-if="isSelected" class="text-blue-600">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
@@ -37,9 +37,9 @@
/> />
<div <div
v-else v-else
class="w-16 h-20 bg-gray-200 rounded border flex items-center justify-center" class="w-16 h-20 bg-gray-200 dark:bg-gray-700 rounded border dark:border-gray-600 flex items-center justify-center"
> >
<svg class="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-8 h-8 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg> </svg>
</div> </div>
@@ -47,27 +47,27 @@
<!-- Manga Info --> <!-- Manga Info -->
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<h4 class="text-sm font-medium text-gray-900 truncate" :title="match.title"> <h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate" :title="match.title">
{{ match.title }} {{ match.title }}
</h4> </h4>
<p class="text-xs text-gray-500 mt-1 truncate" :title="match.slug"> <p class="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate" :title="match.slug">
{{ match.slug }} {{ match.slug }}
</p> </p>
<!-- Alternative Slugs --> <!-- Alternative Slugs -->
<div v-if="match.alternativeSlugs && match.alternativeSlugs.length > 0" class="mt-2"> <div v-if="match.alternativeSlugs && match.alternativeSlugs.length > 0" class="mt-2">
<p class="text-xs text-gray-400">Autres titres:</p> <p class="text-xs text-gray-400 dark:text-gray-500">Autres titres:</p>
<div class="flex flex-wrap gap-1 mt-1"> <div class="flex flex-wrap gap-1 mt-1">
<span <span
v-for="altSlug in match.alternativeSlugs.slice(0, 2)" v-for="altSlug in match.alternativeSlugs.slice(0, 2)"
:key="altSlug" :key="altSlug"
class="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded" class="text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 px-2 py-1 rounded"
> >
{{ altSlug }} {{ altSlug }}
</span> </span>
<span <span
v-if="match.alternativeSlugs.length > 2" v-if="match.alternativeSlugs.length > 2"
class="text-xs text-gray-400" class="text-xs text-gray-400 dark:text-gray-500"
> >
+{{ match.alternativeSlugs.length - 2 }} autres +{{ match.alternativeSlugs.length - 2 }} autres
</span> </span>
@@ -78,11 +78,11 @@
<!-- Score Bar --> <!-- Score Bar -->
<div class="mt-3"> <div class="mt-3">
<div class="flex items-center justify-between text-xs text-gray-500 mb-1"> <div class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400 mb-1">
<span>Correspondance</span> <span>Correspondance</span>
<span>{{ match.matchScore }}%</span> <span>{{ match.matchScore }}%</span>
</div> </div>
<div class="w-full bg-gray-200 rounded-full h-2"> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div <div
class="h-2 rounded-full transition-all duration-300" class="h-2 rounded-full transition-all duration-300"
:class="{ :class="{

View File

@@ -49,22 +49,22 @@ const badgeClasses = computed(() => {
const baseClasses = 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium'; const baseClasses = 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium';
if (props.isImporting || props.isAnalyzing) { if (props.isImporting || props.isAnalyzing) {
return `${baseClasses} bg-blue-100 text-blue-800`; return `${baseClasses} bg-blue-100 dark:bg-blue-900/40 text-blue-800 dark:text-blue-300`;
} }
switch (props.status) { switch (props.status) {
case 'pending': case 'pending':
return `${baseClasses} bg-gray-100 text-gray-800`; return `${baseClasses} bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300`;
case 'analyzed': case 'analyzed':
return `${baseClasses} bg-yellow-100 text-yellow-800`; return `${baseClasses} bg-yellow-100 dark:bg-yellow-900/40 text-yellow-800 dark:text-yellow-300`;
case 'importing': case 'importing':
return `${baseClasses} bg-blue-100 text-blue-800`; return `${baseClasses} bg-blue-100 dark:bg-blue-900/40 text-blue-800 dark:text-blue-300`;
case 'imported': case 'imported':
return `${baseClasses} bg-green-100 text-green-800`; return `${baseClasses} bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-300`;
case 'error': case 'error':
return `${baseClasses} bg-red-100 text-red-800`; return `${baseClasses} bg-red-100 dark:bg-red-900/40 text-red-800 dark:text-red-300`;
default: default:
return `${baseClasses} bg-gray-100 text-gray-800`; return `${baseClasses} bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300`;
} }
}); });
</script> </script>

View File

@@ -2,26 +2,26 @@
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<!-- Header --> <!-- Header -->
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 mb-2">Import de Bibliothèque</h1> <h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">Import de Bibliothèque</h1>
<p class="text-gray-600"> <p class="text-gray-600 dark:text-gray-400">
Importez vos fichiers CBZ/CBR dans votre bibliothèque Mangarr Importez vos fichiers CBZ/CBR dans votre bibliothèque Mangarr
</p> </p>
</div> </div>
<!-- Progress Bar (if files are being processed) --> <!-- Progress Bar (if files are being processed) -->
<div v-if="store.hasFiles && !store.allFilesProcessed" class="mb-8"> <div v-if="store.hasFiles && !store.allFilesProcessed" class="mb-8">
<div class="bg-white rounded-lg shadow-sm p-6"> <div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-700">Progression</span> <span class="text-sm font-medium text-gray-700 dark:text-gray-300">Progression</span>
<span class="text-sm text-gray-500">{{ store.progressPercentage }}%</span> <span class="text-sm text-gray-500 dark:text-gray-400">{{ store.progressPercentage }}%</span>
</div> </div>
<div class="w-full bg-gray-200 rounded-full h-2"> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div <div
class="bg-blue-600 h-2 rounded-full transition-all duration-300" class="bg-blue-600 h-2 rounded-full transition-all duration-300"
:style="{ width: store.progressPercentage + '%' }" :style="{ width: store.progressPercentage + '%' }"
></div> ></div>
</div> </div>
<div class="flex justify-between text-xs text-gray-500 mt-2"> <div class="flex justify-between text-xs text-gray-500 dark:text-gray-400 mt-2">
<span>{{ store.importedCount }} importés</span> <span>{{ store.importedCount }} importés</span>
<span>{{ store.errorCount }} erreurs</span> <span>{{ store.errorCount }} erreurs</span>
<span>{{ store.totalFiles }} total</span> <span>{{ store.totalFiles }} total</span>

View File

@@ -5,32 +5,32 @@
<div class="fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity" @click="handleClose"></div> <div class="fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity" @click="handleClose"></div>
<!-- Modal avec style Material Design --> <!-- Modal avec style Material Design -->
<div class="inline-block align-bottom bg-white rounded-2xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-5xl sm:w-full border border-gray-100"> <div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-2xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-5xl sm:w-full border border-gray-100 dark:border-gray-700">
<!-- Header Material Design --> <!-- Header Material Design -->
<div class="bg-gradient-to-r from-green-50 to-emerald-50 px-6 pt-6 pb-4 sm:px-8 sm:pb-6 border-b border-gray-100"> <div class="bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 px-6 pt-6 pb-4 sm:px-8 sm:pb-6 border-b border-gray-100 dark:border-gray-700">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center"> <div class="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
<FolderIcon class="h-5 w-5 text-green-600" /> <FolderIcon class="h-5 w-5 text-green-600" />
</div> </div>
<div> <div>
<h3 class="text-xl font-medium text-gray-900 leading-6"> <h3 class="text-xl font-medium text-gray-900 dark:text-gray-100 leading-6">
Gérer les chapitres Gérer les chapitres
</h3> </h3>
<p class="text-sm text-gray-600 mt-1">{{ manga?.title }}</p> <p class="text-sm text-gray-600 dark:text-gray-400 mt-1">{{ manga?.title }}</p>
</div> </div>
</div> </div>
<button <button
@click="handleClose" @click="handleClose"
class="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-colors duration-200" class="w-8 h-8 rounded-full bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center transition-colors duration-200"
> >
<XMarkIcon class="h-5 w-5 text-gray-600" /> <XMarkIcon class="h-5 w-5 text-gray-600 dark:text-gray-300" />
</button> </button>
</div> </div>
</div> </div>
<!-- Content avec style Material Design --> <!-- Content avec style Material Design -->
<div class="bg-white px-6 py-6 sm:px-8 sm:py-8"> <div class="bg-white dark:bg-gray-800 px-6 py-6 sm:px-8 sm:py-8">
<div v-if="isLoading" class="flex justify-center items-center h-32"> <div v-if="isLoading" class="flex justify-center items-center h-32">
<div class="relative"> <div class="relative">
<div class="w-8 h-8 border-4 border-green-200 rounded-full"></div> <div class="w-8 h-8 border-4 border-green-200 rounded-full"></div>
@@ -38,7 +38,7 @@
</div> </div>
</div> </div>
<div v-else-if="error" class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-xl mb-6 flex items-center space-x-2"> <div v-else-if="error" class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700 text-red-700 dark:text-red-400 px-4 py-3 rounded-xl mb-6 flex items-center space-x-2">
<div class="w-5 h-5 bg-red-100 rounded-full flex items-center justify-center"> <div class="w-5 h-5 bg-red-100 rounded-full flex items-center justify-center">
<XMarkIcon class="h-3 w-3 text-red-600" /> <XMarkIcon class="h-3 w-3 text-red-600" />
</div> </div>
@@ -47,7 +47,7 @@
<div v-else class="space-y-6"> <div v-else class="space-y-6">
<!-- Actions avec style Material Design --> <!-- Actions avec style Material Design -->
<div class="flex items-center justify-between bg-gray-50 rounded-xl p-4"> <div class="flex items-center justify-between bg-gray-50 dark:bg-gray-700/50 rounded-xl p-4">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<button <button
@click="showCreateVolumeModal = true" @click="showCreateVolumeModal = true"
@@ -58,7 +58,7 @@
</button> </button>
<button <button
@click="showUnassignedChapters = !showUnassignedChapters" @click="showUnassignedChapters = !showUnassignedChapters"
class="text-gray-600 hover:text-gray-800 text-sm font-medium hover:bg-gray-100 px-3 py-2 rounded-lg transition-colors duration-200" class="text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-100 text-sm font-medium hover:bg-gray-100 dark:hover:bg-gray-700 px-3 py-2 rounded-lg transition-colors duration-200"
> >
{{ showUnassignedChapters ? 'Masquer' : 'Afficher' }} les chapitres non assignés {{ showUnassignedChapters ? 'Masquer' : 'Afficher' }} les chapitres non assignés
</button> </button>
@@ -88,17 +88,17 @@
</button> </button>
</div> </div>
</div> </div>
<div class="text-sm text-gray-500 bg-white px-3 py-1.5 rounded-lg border border-gray-200"> <div class="text-sm text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-700 px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-600">
{{ totalChapters }} chapitres, {{ volumes.length }} volumes {{ totalChapters }} chapitres, {{ volumes.length }} volumes
</div> </div>
</div> </div>
<!-- Arborescence avec style Material Design --> <!-- Arborescence avec style Material Design -->
<div class="bg-white border border-gray-200 rounded-xl overflow-hidden shadow-sm"> <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl overflow-hidden shadow-sm">
<!-- Chapitres non assignés --> <!-- Chapitres non assignés -->
<div v-if="showUnassignedChapters && unassignedChapters.length > 0" class="bg-gradient-to-r from-gray-50 to-gray-100 border-b border-gray-200"> <div v-if="showUnassignedChapters && unassignedChapters.length > 0" class="bg-gradient-to-r from-gray-50 to-gray-100 dark:from-gray-700/50 dark:to-gray-700/30 border-b border-gray-200 dark:border-gray-600">
<div class="px-6 py-4"> <div class="px-6 py-4">
<h4 class="text-sm font-semibold text-gray-700 mb-3 flex items-center space-x-2"> <h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3 flex items-center space-x-2">
<DocumentIcon class="h-4 w-4 text-gray-500" /> <DocumentIcon class="h-4 w-4 text-gray-500" />
<span>Chapitres non assignés ({{ unassignedChapters.length }})</span> <span>Chapitres non assignés ({{ unassignedChapters.length }})</span>
</h4> </h4>
@@ -119,11 +119,11 @@
/> />
</div> </div>
<DocumentIcon class="h-5 w-5 text-gray-400" /> <DocumentIcon class="h-5 w-5 text-gray-400" />
<span class="text-sm font-medium text-gray-700 w-12 bg-gray-100 px-2 py-1 rounded text-center">{{ chapter.number }}</span> <span class="text-sm font-medium text-gray-700 dark:text-gray-300 w-12 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded text-center">{{ chapter.number }}</span>
<div class="flex-1"> <div class="flex-1">
<div v-if="!chapter.isEditing" class="flex items-center"> <div v-if="!chapter.isEditing" class="flex items-center">
<span <span
class="text-sm text-gray-900 cursor-pointer hover:text-green-600 transition-colors duration-200" class="text-sm text-gray-900 dark:text-gray-100 cursor-pointer hover:text-green-600 dark:hover:text-green-400 transition-colors duration-200"
@click="startEditingTitle(chapter)" @click="startEditingTitle(chapter)"
> >
{{ chapter.title || 'Sans titre' }} {{ chapter.title || 'Sans titre' }}
@@ -173,22 +173,22 @@
</div> </div>
<!-- Volumes avec style Material Design --> <!-- Volumes avec style Material Design -->
<div class="divide-y divide-gray-100"> <div class="divide-y divide-gray-100 dark:divide-gray-700">
<div <div
v-for="volume in volumes" v-for="volume in volumes"
:key="volume.number" :key="volume.number"
class="bg-white" class="bg-white dark:bg-gray-800"
> >
<!-- En-tête du volume Material Design --> <!-- En-tête du volume Material Design -->
<div class="px-6 py-4 bg-gradient-to-r from-green-50 to-emerald-50 border-b border-green-100"> <div class="px-6 py-4 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 border-b border-green-100 dark:border-green-900/30">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center"> <div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
<FolderIcon class="h-4 w-4 text-green-600" /> <FolderIcon class="h-4 w-4 text-green-600" />
</div> </div>
<div> <div>
<span class="text-sm font-semibold text-green-900">Volume {{ volume.number }}</span> <span class="text-sm font-semibold text-green-900 dark:text-green-300">Volume {{ volume.number }}</span>
<span class="text-xs text-green-600 ml-2">({{ volume.chapters.length }} chapitres)</span> <span class="text-xs text-green-600 dark:text-green-400 ml-2">({{ volume.chapters.length }} chapitres)</span>
</div> </div>
</div> </div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
@@ -211,10 +211,10 @@
<!-- Chapitres du volume --> <!-- Chapitres du volume -->
<div v-if="volume.isExpanded" class="px-6 py-4"> <div v-if="volume.isExpanded" class="px-6 py-4">
<div v-if="volume.chapters.length === 0" class="text-center py-8 text-gray-500"> <div v-if="volume.chapters.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
<DocumentIcon class="h-12 w-12 text-gray-300 mx-auto mb-3" /> <DocumentIcon class="h-12 w-12 text-gray-300 dark:text-gray-600 mx-auto mb-3" />
<p class="text-sm">Aucun chapitre assigné à ce volume.</p> <p class="text-sm">Aucun chapitre assigné à ce volume.</p>
<p class="text-xs text-gray-400 mt-1">Utilisez le bouton "Assigner" sur les chapitres non assignés pour les ajouter.</p> <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Utilisez le bouton "Assigner" sur les chapitres non assignés pour les ajouter.</p>
</div> </div>
<div v-else class="space-y-2"> <div v-else class="space-y-2">
<div <div
@@ -233,11 +233,11 @@
/> />
</div> </div>
<DocumentIcon class="h-5 w-5 text-gray-400" /> <DocumentIcon class="h-5 w-5 text-gray-400" />
<span class="text-sm font-medium text-gray-700 w-12 bg-gray-100 px-2 py-1 rounded text-center">{{ chapter.number }}</span> <span class="text-sm font-medium text-gray-700 dark:text-gray-300 w-12 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded text-center">{{ chapter.number }}</span>
<div class="flex-1"> <div class="flex-1">
<div v-if="!chapter.isEditing" class="flex items-center"> <div v-if="!chapter.isEditing" class="flex items-center">
<span <span
class="text-sm text-gray-900 cursor-pointer hover:text-green-600 transition-colors duration-200" class="text-sm text-gray-900 dark:text-gray-100 cursor-pointer hover:text-green-600 dark:hover:text-green-400 transition-colors duration-200"
@click="startEditingTitle(chapter)" @click="startEditingTitle(chapter)"
> >
{{ chapter.title || 'Sans titre' }} {{ chapter.title || 'Sans titre' }}
@@ -291,12 +291,12 @@
</div> </div>
<!-- Footer Material Design --> <!-- Footer Material Design -->
<div class="bg-gray-50 px-6 py-4 sm:px-8 sm:py-6 border-t border-gray-200"> <div class="bg-gray-50 dark:bg-gray-700/50 px-6 py-4 sm:px-8 sm:py-6 border-t border-gray-200 dark:border-gray-700">
<div class="flex flex-col sm:flex-row sm:justify-end sm:space-x-3 space-y-3 sm:space-y-0"> <div class="flex flex-col sm:flex-row sm:justify-end sm:space-x-3 space-y-3 sm:space-y-0">
<button <button
@click="handleClose" @click="handleClose"
:disabled="isSaving" :disabled="isSaving"
class="w-full sm:w-auto inline-flex justify-center items-center rounded-lg border border-gray-300 bg-white px-6 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 transition-all duration-200 shadow-sm hover:shadow-md" class="w-full sm:w-auto inline-flex justify-center items-center rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-6 py-2.5 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 transition-all duration-200 shadow-sm hover:shadow-md"
> >
Annuler Annuler
</button> </button>
@@ -320,24 +320,24 @@
<div v-if="showCreateVolumeModal" class="fixed inset-0 z-60 overflow-y-auto"> <div v-if="showCreateVolumeModal" class="fixed inset-0 z-60 overflow-y-auto">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> <div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity" @click="showCreateVolumeModal = false"></div> <div class="fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity" @click="showCreateVolumeModal = false"></div>
<div class="inline-block align-bottom bg-white rounded-2xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full border border-gray-100"> <div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-2xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full border border-gray-100 dark:border-gray-700">
<div class="bg-gradient-to-r from-green-50 to-emerald-50 px-6 pt-6 pb-4 sm:px-8 sm:pb-6 border-b border-gray-100"> <div class="bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 px-6 pt-6 pb-4 sm:px-8 sm:pb-6 border-b border-gray-100 dark:border-gray-700">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center"> <div class="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
<PlusIcon class="h-5 w-5 text-green-600" /> <PlusIcon class="h-5 w-5 text-green-600" />
</div> </div>
<h3 class="text-lg font-medium text-gray-900">Créer un nouveau volume</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Créer un nouveau volume</h3>
</div> </div>
</div> </div>
<div class="bg-white px-6 py-6 sm:px-8 sm:py-6"> <div class="bg-white dark:bg-gray-800 px-6 py-6 sm:px-8 sm:py-6">
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2">Numéro du volume</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Numéro du volume</label>
<input <input
v-model="newVolumeNumber" v-model="newVolumeNumber"
type="number" type="number"
min="1" min="1"
class="block w-full border border-gray-300 rounded-lg px-4 py-3 text-sm focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors duration-200" class="block w-full border border-gray-300 dark:border-gray-600 rounded-lg px-4 py-3 text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-colors duration-200"
placeholder="Ex: 1" placeholder="Ex: 1"
/> />
</div> </div>
@@ -351,7 +351,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-6 py-4 sm:px-8 sm:py-6 border-t border-gray-200"> <div class="bg-gray-50 dark:bg-gray-700/50 px-6 py-4 sm:px-8 sm:py-6 border-t border-gray-200 dark:border-gray-700">
<div class="flex flex-col sm:flex-row sm:justify-end sm:space-x-3 space-y-3 sm:space-y-0"> <div class="flex flex-col sm:flex-row sm:justify-end sm:space-x-3 space-y-3 sm:space-y-0">
<button <button
@click="showCreateVolumeModal = false" @click="showCreateVolumeModal = false"
@@ -376,8 +376,8 @@
<div v-if="showAssignModal" class="fixed inset-0 z-60 overflow-y-auto"> <div v-if="showAssignModal" class="fixed inset-0 z-60 overflow-y-auto">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> <div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity" @click="showAssignModal = false"></div> <div class="fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity" @click="showAssignModal = false"></div>
<div class="inline-block align-bottom bg-white rounded-2xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full border border-gray-100"> <div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-2xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full border border-gray-100 dark:border-gray-700">
<div class="bg-gradient-to-r from-green-50 to-emerald-50 px-6 pt-6 pb-4 sm:px-8 sm:pb-6 border-b border-gray-100"> <div class="bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 px-6 pt-6 pb-4 sm:px-8 sm:pb-6 border-b border-gray-100 dark:border-gray-700">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center"> <div class="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
<DocumentIcon class="h-5 w-5 text-green-600" /> <DocumentIcon class="h-5 w-5 text-green-600" />
@@ -385,7 +385,7 @@
<h3 class="text-lg font-medium text-gray-900">Assigner le chapitre {{ selectedChapter?.number }}</h3> <h3 class="text-lg font-medium text-gray-900">Assigner le chapitre {{ selectedChapter?.number }}</h3>
</div> </div>
</div> </div>
<div class="bg-white px-6 py-6 sm:px-8 sm:py-6"> <div class="bg-white dark:bg-gray-800 px-6 py-6 sm:px-8 sm:py-6">
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2">Volume</label> <label class="block text-sm font-medium text-gray-700 mb-2">Volume</label>
@@ -401,7 +401,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-6 py-4 sm:px-8 sm:py-6 border-t border-gray-200"> <div class="bg-gray-50 dark:bg-gray-700/50 px-6 py-4 sm:px-8 sm:py-6 border-t border-gray-200 dark:border-gray-700">
<div class="flex flex-col sm:flex-row sm:justify-end sm:space-x-3 space-y-3 sm:space-y-0"> <div class="flex flex-col sm:flex-row sm:justify-end sm:space-x-3 space-y-3 sm:space-y-0">
<button <button
@click="showAssignModal = false" @click="showAssignModal = false"
@@ -426,8 +426,8 @@
<div v-if="showMoveToVolumeModal" class="fixed inset-0 z-60 overflow-y-auto"> <div v-if="showMoveToVolumeModal" class="fixed inset-0 z-60 overflow-y-auto">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> <div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity" @click="showMoveToVolumeModal = false"></div> <div class="fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity" @click="showMoveToVolumeModal = false"></div>
<div class="inline-block align-bottom bg-white rounded-2xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full border border-gray-100"> <div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-2xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full border border-gray-100 dark:border-gray-700">
<div class="bg-gradient-to-r from-green-50 to-emerald-50 px-6 pt-6 pb-4 sm:px-8 sm:pb-6 border-b border-gray-100"> <div class="bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 px-6 pt-6 pb-4 sm:px-8 sm:pb-6 border-b border-gray-100 dark:border-gray-700">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center"> <div class="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
<ArrowPathIcon class="h-5 w-5 text-green-600" /> <ArrowPathIcon class="h-5 w-5 text-green-600" />
@@ -435,7 +435,7 @@
<h3 class="text-lg font-medium text-gray-900">Déplacer {{ selectedChapters.length }} chapitre(s)</h3> <h3 class="text-lg font-medium text-gray-900">Déplacer {{ selectedChapters.length }} chapitre(s)</h3>
</div> </div>
</div> </div>
<div class="bg-white px-6 py-6 sm:px-8 sm:py-6"> <div class="bg-white dark:bg-gray-800 px-6 py-6 sm:px-8 sm:py-6">
<div class="space-y-4"> <div class="space-y-4">
<div class="bg-green-50 p-4 rounded-lg border border-green-200"> <div class="bg-green-50 p-4 rounded-lg border border-green-200">
<p class="text-sm text-green-800 font-medium"> <p class="text-sm text-green-800 font-medium">
@@ -457,7 +457,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-6 py-4 sm:px-8 sm:py-6 border-t border-gray-200"> <div class="bg-gray-50 dark:bg-gray-700/50 px-6 py-4 sm:px-8 sm:py-6 border-t border-gray-200 dark:border-gray-700">
<div class="flex flex-col sm:flex-row sm:justify-end sm:space-x-3 space-y-3 sm:space-y-0"> <div class="flex flex-col sm:flex-row sm:justify-end sm:space-x-3 space-y-3 sm:space-y-0">
<button <button
@click="showMoveToVolumeModal = false" @click="showMoveToVolumeModal = false"
@@ -491,7 +491,7 @@
<h3 class="text-lg font-medium text-gray-900">Séparer le volume 00</h3> <h3 class="text-lg font-medium text-gray-900">Séparer le volume 00</h3>
</div> </div>
</div> </div>
<div class="bg-white px-6 py-6 sm:px-8 sm:py-6"> <div class="bg-white dark:bg-gray-800 px-6 py-6 sm:px-8 sm:py-6">
<div class="space-y-4"> <div class="space-y-4">
<div class="bg-green-50 p-4 rounded-lg border border-green-200"> <div class="bg-green-50 p-4 rounded-lg border border-green-200">
<p class="text-sm text-green-800 font-medium"> <p class="text-sm text-green-800 font-medium">
@@ -517,7 +517,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-6 py-4 sm:px-8 sm:py-6 border-t border-gray-200"> <div class="bg-gray-50 dark:bg-gray-700/50 px-6 py-4 sm:px-8 sm:py-6 border-t border-gray-200 dark:border-gray-700">
<div class="flex flex-col sm:flex-row sm:justify-end sm:space-x-3 space-y-3 sm:space-y-0"> <div class="flex flex-col sm:flex-row sm:justify-end sm:space-x-3 space-y-3 sm:space-y-0">
<button <button
@click="showSplitVolumeZeroModal = false" @click="showSplitVolumeZeroModal = false"

View File

@@ -1,7 +1,7 @@
<template> <template>
<RouterLink <RouterLink
:to="{ name: 'manga-details', params: { id: manga.id } }" :to="{ name: 'manga-details', params: { id: manga.id } }"
class="bg-white rounded-lg shadow-md overflow-hidden cursor-pointer transition-transform hover:scale-105 block"> class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden cursor-pointer transition-transform hover:scale-105 block">
<div class="relative pb-[150%]"> <div class="relative pb-[150%]">
<img <img
:src="manga.thumbnailUrl || 'https://via.placeholder.com/300x400'" :src="manga.thumbnailUrl || 'https://via.placeholder.com/300x400'"
@@ -9,11 +9,11 @@
class="absolute inset-0 w-full h-full object-cover bg-gray-100" /> class="absolute inset-0 w-full h-full object-cover bg-gray-100" />
</div> </div>
<div class="p-2"> <div class="p-2">
<h3 class="text-lg font-semibold text-gray-800 mb-1">{{ manga.title }}</h3> <h3 class="text-lg font-semibold text-gray-800 dark:text-gray-100 mb-1">{{ manga.title }}</h3>
<div class="flex items-center"> <div class="flex items-center">
<span class="text-sm text-gray-500">{{ manga.publicationYear }}</span> <span class="text-sm text-gray-500 dark:text-gray-400">{{ manga.publicationYear }}</span>
</div> </div>
<div class="mt-1 text-sm text-gray-500"> Added: {{ formatDate(manga.createdAt) }} </div> <div class="mt-1 text-sm text-gray-500 dark:text-gray-400"> Added: {{ formatDate(manga.createdAt) }} </div>
</div> </div>
</RouterLink> </RouterLink>
</template> </template>

View File

@@ -1,11 +1,12 @@
<template> <template>
<tr class="border-t hover:bg-green-100"> <tr class="border-t dark:border-gray-700 hover:bg-green-100 dark:hover:bg-green-900/20">
<td class="px-4 py-2" :class="{ 'text-green-500': chapter.isAvailable }"> <td class="px-4 py-2 text-gray-900 dark:text-gray-100" :class="{ 'text-green-500 dark:text-green-400': chapter.isAvailable }">
{{ String(chapter.number).padStart(2, '0') }} {{ String(chapter.number).padStart(2, '0') }}
</td> </td>
<td class="px-4 py-2 w-full text-left"> <td class="px-4 py-2 w-full text-left text-gray-900 dark:text-gray-100">
<router-link <router-link
v-if="chapter.isAvailable" v-if="chapter.isAvailable"
class="hover:text-green-500 dark:hover:text-green-400"
:to="{ :to="{
name: 'reader', name: 'reader',
params: { params: {
@@ -14,7 +15,7 @@
}"> }">
{{ chapter.title || 'Sans titre' }} {{ chapter.title || 'Sans titre' }}
</router-link> </router-link>
<span v-else>{{ chapter.title || 'Sans titre' }}</span> <span v-else class="text-gray-500 dark:text-gray-400">{{ chapter.title || 'Sans titre' }}</span>
</td> </td>
<td class="px-4 py-2 flex justify-end gap-2"> <td class="px-4 py-2 flex justify-end gap-2">
<button v-if="!chapter.isAvailable" @click="handleSearch" :class="buttonClass"> <button v-if="!chapter.isAvailable" @click="handleSearch" :class="buttonClass">

View File

@@ -1,8 +1,8 @@
<template> <template>
<div class="p-2 border-t"> <div class="p-2 border-t dark:border-gray-700">
<table class="min-w-full table-auto"> <table class="min-w-full table-auto">
<thead> <thead>
<tr> <tr class="text-gray-700 dark:text-gray-300">
<th class="px-4 py-2 text-left">#</th> <th class="px-4 py-2 text-left">#</th>
<th class="px-4 py-2 text-left">Titre</th> <th class="px-4 py-2 text-left">Titre</th>
<th class="px-4 py-2 text-right">Actions</th> <th class="px-4 py-2 text-right">Actions</th>

View File

@@ -10,7 +10,7 @@
leave-from="opacity-100" leave-from="opacity-100"
leave-to="opacity-0" leave-to="opacity-0"
> >
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> <div class="fixed inset-0 bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-80 transition-opacity" />
</TransitionChild> </TransitionChild>
<div class="fixed inset-0 z-10 overflow-y-auto"> <div class="fixed inset-0 z-10 overflow-y-auto">
@@ -24,15 +24,15 @@
leave-from="opacity-100 translate-y-0 sm:scale-100" leave-from="opacity-100 translate-y-0 sm:scale-100"
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<DialogPanel class="relative transform overflow-hidden rounded-lg bg-white px-6 pb-6 pt-6 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg"> <DialogPanel class="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 px-6 pb-6 pt-6 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div class="mb-6"> <div class="mb-6">
<DialogTitle as="h3" class="text-lg font-semibold leading-6 text-gray-900"> <DialogTitle as="h3" class="text-lg font-semibold leading-6 text-gray-900 dark:text-gray-100">
Supprimer le manga Supprimer le manga
</DialogTitle> </DialogTitle>
</div> </div>
<!-- Error state --> <!-- Error state -->
<div v-if="error" class="mb-6 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded"> <div v-if="error" class="mb-6 bg-red-100 dark:bg-red-900/20 border border-red-400 dark:border-red-700 text-red-700 dark:text-red-400 px-4 py-3 rounded">
{{ error.message || 'Une erreur est survenue lors de la suppression.' }} {{ error.message || 'Une erreur est survenue lors de la suppression.' }}
</div> </div>
@@ -40,19 +40,19 @@
<div class="mb-6"> <div class="mb-6">
<div class="flex items-center mb-4"> <div class="flex items-center mb-4">
<ExclamationTriangleIcon class="h-6 w-6 text-red-500 mr-3" /> <ExclamationTriangleIcon class="h-6 w-6 text-red-500 mr-3" />
<span class="text-sm font-medium text-gray-900">Action irréversible</span> <span class="text-sm font-medium text-gray-900 dark:text-gray-100">Action irréversible</span>
</div> </div>
<p class="text-sm text-gray-600 mb-4"> <p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
Êtes-vous sûr de vouloir supprimer le manga <strong>"{{ manga?.title }}"</strong> ? Êtes-vous sûr de vouloir supprimer le manga <strong>"{{ manga?.title }}"</strong> ?
</p> </p>
<div class="bg-yellow-50 border border-yellow-200 rounded-md p-4"> <div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-700 rounded-md p-4">
<div class="flex"> <div class="flex">
<ExclamationTriangleIcon class="h-5 w-5 text-yellow-400" /> <ExclamationTriangleIcon class="h-5 w-5 text-yellow-400" />
<div class="ml-3"> <div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800"> <h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-300">
Attention Attention
</h3> </h3>
<div class="mt-2 text-sm text-yellow-700"> <div class="mt-2 text-sm text-yellow-700 dark:text-yellow-400">
<p>Cette action supprimera définitivement :</p> <p>Cette action supprimera définitivement :</p>
<ul class="list-disc list-inside mt-1 space-y-1"> <ul class="list-disc list-inside mt-1 space-y-1">
<li>Le manga et toutes ses métadonnées</li> <li>Le manga et toutes ses métadonnées</li>
@@ -69,7 +69,7 @@
<div class="mt-6 flex justify-end space-x-3"> <div class="mt-6 flex justify-end space-x-3">
<button <button
type="button" type="button"
class="inline-flex justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" class="inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
@click="closeModal" @click="closeModal"
:disabled="isLoading" :disabled="isLoading"
> >

View File

@@ -10,7 +10,7 @@
leave-from="opacity-100" leave-from="opacity-100"
leave-to="opacity-0" leave-to="opacity-0"
> >
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> <div class="fixed inset-0 bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-80 transition-opacity" />
</TransitionChild> </TransitionChild>
<div class="fixed inset-0 z-10 overflow-y-auto"> <div class="fixed inset-0 z-10 overflow-y-auto">
@@ -24,15 +24,15 @@
leave-from="opacity-100 translate-y-0 sm:scale-100" leave-from="opacity-100 translate-y-0 sm:scale-100"
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<DialogPanel class="relative transform overflow-hidden rounded-lg bg-white px-6 pb-6 pt-6 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-4xl"> <DialogPanel class="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 px-6 pb-6 pt-6 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-4xl">
<div class="mb-6"> <div class="mb-6">
<DialogTitle as="h3" class="text-lg font-semibold leading-6 text-gray-900"> <DialogTitle as="h3" class="text-lg font-semibold leading-6 text-gray-900 dark:text-gray-100">
Edit Manga Edit Manga
</DialogTitle> </DialogTitle>
</div> </div>
<!-- Error state --> <!-- Error state -->
<div v-if="error" class="mb-6 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded"> <div v-if="error" class="mb-6 bg-red-100 dark:bg-red-900/20 border border-red-400 dark:border-red-700 text-red-700 dark:text-red-400 px-4 py-3 rounded">
{{ error.message || 'Une erreur est survenue lors de la sauvegarde.' }} {{ error.message || 'Une erreur est survenue lors de la sauvegarde.' }}
</div> </div>
@@ -41,49 +41,49 @@
<!-- Titre et Slug --> <!-- Titre et Slug -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label for="title" class="block text-sm font-medium text-gray-700 mb-2">Titre</label> <label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Titre</label>
<input <input
id="title" id="title"
v-model="formData.title" v-model="formData.title"
type="text" type="text"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
placeholder="Titre du manga" placeholder="Titre du manga"
/> />
</div> </div>
<div> <div>
<label for="slug" class="block text-sm font-medium text-gray-700 mb-2">Slug</label> <label for="slug" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Slug</label>
<input <input
id="slug" id="slug"
:value="manga?.slug || ''" :value="manga?.slug || ''"
type="text" type="text"
disabled disabled
class="block w-full rounded-md border-gray-300 bg-gray-50 shadow-sm sm:text-sm text-gray-500" class="block w-full rounded-md border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-600 shadow-sm sm:text-sm text-gray-500 dark:text-gray-400"
/> />
</div> </div>
</div> </div>
<!-- Année de publication --> <!-- Année de publication -->
<div> <div>
<label for="publicationYear" class="block text-sm font-medium text-gray-700 mb-2">Année de publication</label> <label for="publicationYear" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Année de publication</label>
<input <input
id="publicationYear" id="publicationYear"
v-model.number="formData.publicationYear" v-model.number="formData.publicationYear"
type="number" type="number"
min="1900" min="1900"
:max="new Date().getFullYear()" :max="new Date().getFullYear()"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
placeholder="2023" placeholder="2023"
/> />
</div> </div>
<!-- Description --> <!-- Description -->
<div> <div>
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">Description</label> <label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Description</label>
<textarea <textarea
id="description" id="description"
v-model="formData.description" v-model="formData.description"
rows="4" rows="4"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
placeholder="Description du manga" placeholder="Description du manga"
/> />
</div> </div>
@@ -91,22 +91,22 @@
<!-- Auteur et Statut --> <!-- Auteur et Statut -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label for="author" class="block text-sm font-medium text-gray-700 mb-2">Auteur</label> <label for="author" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Auteur</label>
<input <input
id="author" id="author"
v-model="formData.author" v-model="formData.author"
type="text" type="text"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
placeholder="Auteur du manga" placeholder="Auteur du manga"
/> />
</div> </div>
<div> <div>
<label for="status" class="block text-sm font-medium text-gray-700 mb-2">Statut</label> <label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Statut</label>
<input <input
id="status" id="status"
v-model="formData.status" v-model="formData.status"
type="text" type="text"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
placeholder="ongoing" placeholder="ongoing"
/> />
</div> </div>
@@ -114,7 +114,7 @@
<!-- Note --> <!-- Note -->
<div> <div>
<label for="rating" class="block text-sm font-medium text-gray-700 mb-2">Note</label> <label for="rating" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Note</label>
<input <input
id="rating" id="rating"
v-model.number="formData.rating" v-model.number="formData.rating"
@@ -122,20 +122,20 @@
min="0" min="0"
max="10" max="10"
step="0.001" step="0.001"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
placeholder="9.541" placeholder="9.541"
/> />
</div> </div>
<!-- Slugs alternatifs --> <!-- Slugs alternatifs -->
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2">Slugs alternatifs</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Slugs alternatifs</label>
<div class="space-y-2"> <div class="space-y-2">
<div v-if="formData.alternativeSlugs.length > 0" class="flex flex-wrap gap-2"> <div v-if="formData.alternativeSlugs.length > 0" class="flex flex-wrap gap-2">
<span <span
v-for="(slug, index) in formData.alternativeSlugs" v-for="(slug, index) in formData.alternativeSlugs"
:key="index" :key="index"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-300"
> >
{{ slug }} {{ slug }}
<button <button
@@ -158,7 +158,7 @@
<input <input
v-model="newAlternativeSlug" v-model="newAlternativeSlug"
type="text" type="text"
class="flex-1 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" class="flex-1 rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
placeholder="Nouveau slug alternatif" placeholder="Nouveau slug alternatif"
@keyup.enter="addAlternativeSlug" @keyup.enter="addAlternativeSlug"
/> />
@@ -175,19 +175,19 @@
<!-- Genres --> <!-- Genres -->
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2">Genres</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Genres</label>
<div class="space-y-3"> <div class="space-y-3">
<div v-if="formData.genres.length > 0" class="grid grid-cols-2 md:grid-cols-4 gap-2"> <div v-if="formData.genres.length > 0" class="grid grid-cols-2 md:grid-cols-4 gap-2">
<span <span
v-for="(genre, index) in formData.genres" v-for="(genre, index) in formData.genres"
:key="index" :key="index"
class="inline-flex items-center justify-between px-3 py-1 rounded-md text-sm font-medium bg-gray-100 text-gray-800" class="inline-flex items-center justify-between px-3 py-1 rounded-md text-sm font-medium bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
> >
{{ genre }} {{ genre }}
<button <button
type="button" type="button"
@click="removeGenre(index)" @click="removeGenre(index)"
class="ml-2 inline-flex items-center justify-center w-4 h-4 text-gray-400 hover:text-gray-600" class="ml-2 inline-flex items-center justify-center w-4 h-4 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
> >
<XMarkIcon class="w-3 h-3" /> <XMarkIcon class="w-3 h-3" />
</button> </button>
@@ -204,7 +204,7 @@
<input <input
v-model="newGenre" v-model="newGenre"
type="text" type="text"
class="flex-1 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" class="flex-1 rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
placeholder="Nouveau genre" placeholder="Nouveau genre"
@keyup.enter="addGenre" @keyup.enter="addGenre"
/> />
@@ -224,7 +224,7 @@
<div class="mt-8 flex justify-end space-x-3"> <div class="mt-8 flex justify-end space-x-3">
<button <button
type="button" type="button"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600"
@click="closeModal" @click="closeModal"
:disabled="isSaving" :disabled="isSaving"
> >

View File

@@ -10,7 +10,7 @@
leave-from="opacity-100" leave-from="opacity-100"
leave-to="opacity-0" leave-to="opacity-0"
> >
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> <div class="fixed inset-0 bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-80 transition-opacity" />
</TransitionChild> </TransitionChild>
<div class="fixed inset-0 z-10 overflow-y-auto"> <div class="fixed inset-0 z-10 overflow-y-auto">
@@ -24,17 +24,17 @@
leave-from="opacity-100 translate-y-0 sm:scale-100" leave-from="opacity-100 translate-y-0 sm:scale-100"
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<DialogPanel class="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6"> <DialogPanel class="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<div> <div>
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-blue-100"> <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-blue-100">
<Cog6ToothIcon class="h-6 w-6 text-blue-600" aria-hidden="true" /> <Cog6ToothIcon class="h-6 w-6 text-blue-600" aria-hidden="true" />
</div> </div>
<div class="mt-3 text-center sm:mt-5"> <div class="mt-3 text-center sm:mt-5">
<DialogTitle as="h3" class="text-base font-semibold leading-6 text-gray-900"> <DialogTitle as="h3" class="text-base font-semibold leading-6 text-gray-900 dark:text-gray-100">
Sources préférées Sources préférées
</DialogTitle> </DialogTitle>
<div class="mt-2"> <div class="mt-2">
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500 dark:text-gray-400">
Configurez l'ordre de priorité des sources pour ce manga. Glissez-déposez les sources pour les réorganiser. Configurez l'ordre de priorité des sources pour ce manga. Glissez-déposez les sources pour les réorganiser.
</p> </p>
</div> </div>
@@ -47,13 +47,13 @@
</div> </div>
<!-- Error state --> <!-- Error state -->
<div v-else-if="error" class="mt-5 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded"> <div v-else-if="error" class="mt-5 bg-red-100 dark:bg-red-900/20 border border-red-400 dark:border-red-700 text-red-700 dark:text-red-400 px-4 py-3 rounded">
{{ error.message || 'Une erreur est survenue lors du chargement des sources.' }} {{ error.message || 'Une erreur est survenue lors du chargement des sources.' }}
</div> </div>
<!-- Sources list --> <!-- Sources list -->
<div v-else class="mt-5"> <div v-else class="mt-5">
<div v-if="localSources.length === 0" class="text-center py-8 text-gray-500"> <div v-if="localSources.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
Aucune source disponible Aucune source disponible
</div> </div>
<div v-else class="space-y-3"> <div v-else class="space-y-3">
@@ -63,10 +63,10 @@
:class="[ :class="[
'group relative flex items-center p-4 rounded-lg border-2 transition-all duration-200 cursor-grab active:cursor-grabbing select-none', 'group relative flex items-center p-4 rounded-lg border-2 transition-all duration-200 cursor-grab active:cursor-grabbing select-none',
{ {
'bg-gradient-to-r from-blue-50 to-indigo-50 border-blue-300 shadow-md': index === 0, 'bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 border-blue-300 dark:border-blue-700 shadow-md': index === 0,
'bg-gradient-to-r from-green-50 to-emerald-50 border-green-300': index === 1, 'bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 border-green-300 dark:border-green-700': index === 1,
'bg-gradient-to-r from-yellow-50 to-amber-50 border-yellow-300': index === 2, 'bg-gradient-to-r from-yellow-50 to-amber-50 dark:from-yellow-900/20 dark:to-amber-900/20 border-yellow-300 dark:border-yellow-700': index === 2,
'bg-gray-50 border-gray-200': index > 2, 'bg-gray-50 dark:bg-gray-700/50 border-gray-200 dark:border-gray-600': index > 2,
'scale-105 shadow-lg border-blue-400': draggedIndex === index, 'scale-105 shadow-lg border-blue-400': draggedIndex === index,
'opacity-50': dragOverIndex === index && draggedIndex !== index, 'opacity-50': dragOverIndex === index && draggedIndex !== index,
'scale-95 active:scale-95': isPressed === index 'scale-95 active:scale-95': isPressed === index
@@ -102,10 +102,10 @@
<div :class="[ <div :class="[
'flex items-center space-x-1 px-3 py-1 rounded-full text-xs font-semibold', 'flex items-center space-x-1 px-3 py-1 rounded-full text-xs font-semibold',
{ {
'bg-blue-100 text-blue-800': index === 0, 'bg-blue-100 dark:bg-blue-900/40 text-blue-800 dark:text-blue-300': index === 0,
'bg-green-100 text-green-800': index === 1, 'bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-300': index === 1,
'bg-yellow-100 text-yellow-800': index === 2, 'bg-yellow-100 dark:bg-yellow-900/40 text-yellow-800 dark:text-yellow-300': index === 2,
'bg-gray-100 text-gray-600': index > 2 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300': index > 2
} }
]"> ]">
<span v-if="index === 0">🥇 Priorité haute</span> <span v-if="index === 0">🥇 Priorité haute</span>
@@ -117,14 +117,14 @@
<!-- Informations de la source --> <!-- Informations de la source -->
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="font-semibold text-gray-900 truncate">{{ source.name }}</div> <div class="font-semibold text-gray-900 dark:text-gray-100 truncate">{{ source.name }}</div>
<div class="text-sm text-gray-600 truncate"> <div class="text-sm text-gray-600 dark:text-gray-400 truncate">
<a :href="source.baseUrl" target="_blank" class="hover:text-blue-600 hover:underline">{{ source.baseUrl }}</a> <a :href="source.baseUrl" target="_blank" class="hover:text-blue-600 hover:underline">{{ source.baseUrl }}</a>
</div> </div>
</div> </div>
<!-- Indicateur de drag --> <!-- Indicateur de drag -->
<div class="ml-4 text-gray-400 group-hover:text-gray-600 transition-colors duration-200"> <div class="ml-4 text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-300 transition-colors duration-200">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9h8M8 15h8" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9h8M8 15h8" />
</svg> </svg>
@@ -148,7 +148,7 @@
</button> </button>
<button <button
type="button" type="button"
class="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:col-start-1 sm:mt-0" class="mt-3 inline-flex w-full justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 sm:col-start-1 sm:mt-0"
@click="closeModal" @click="closeModal"
:disabled="isSaving" :disabled="isSaving"
> >

View File

@@ -1,13 +1,13 @@
<template> <template>
<div class="bg-white rounded-sm shadow mb-2"> <div class="bg-white dark:bg-gray-800 rounded-sm shadow mb-2">
<!-- En-tête du volume --> <!-- En-tête du volume -->
<div class="relative bg-white p-3 sm:p-4 rounded-t-sm"> <div class="relative bg-white dark:bg-gray-800 p-3 sm:p-4 rounded-t-sm">
<!-- Layout mobile/desktop --> <!-- Layout mobile/desktop -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<!-- Partie gauche --> <!-- Partie gauche -->
<div class="flex items-center space-x-1 sm:space-x-4 flex-1 min-w-0"> <div class="flex items-center space-x-1 sm:space-x-4 flex-1 min-w-0">
<BookmarkIcon class="h-6 w-6 sm:h-8 sm:w-8 text-gray-500 flex-shrink-0" /> <BookmarkIcon class="h-6 w-6 sm:h-8 sm:w-8 text-gray-500 dark:text-gray-400 flex-shrink-0" />
<h2 class="text-lg sm:text-xl font-semibold w-20 sm:w-28 flex-shrink-0">Vol {{ String(volume.number).padStart(2, '0') }}</h2> <h2 class="text-lg sm:text-xl font-semibold w-20 sm:w-28 flex-shrink-0 dark:text-gray-100">Vol {{ String(volume.number).padStart(2, '0') }}</h2>
<div class="flex items-center"> <div class="flex items-center">
<span <span
:class="[ :class="[
@@ -65,7 +65,7 @@
<MangaChapterList v-show="isOpen" :chapters="volume.chapters" :manga-slug="mangaSlug" :manga-id="mangaId" /> <MangaChapterList v-show="isOpen" :chapters="volume.chapters" :manga-slug="mangaSlug" :manga-id="mangaId" />
<!-- Chevron de fermeture --> <!-- Chevron de fermeture -->
<div v-show="isOpen" class="flex justify-center p-2 bg-white rounded-b-sm"> <div v-show="isOpen" class="flex justify-center p-2 bg-white dark:bg-gray-800 rounded-b-sm">
<button @click="toggleVolume" class="w-8 h-8 flex items-center justify-center"> <button @click="toggleVolume" class="w-8 h-8 flex items-center justify-center">
<ChevronUpIcon <ChevronUpIcon
class="h-5 w-5 sm:h-6 sm:w-6 bg-gray-400 rounded-full p-1 text-white hover:bg-green-500 cursor-pointer" class="h-5 w-5 sm:h-6 sm:w-6 bg-gray-400 rounded-full p-1 text-white hover:bg-green-500 cursor-pointer"

View File

@@ -8,7 +8,7 @@
v-model="searchQuery" v-model="searchQuery"
@keyup.enter="performSearch" @keyup.enter="performSearch"
placeholder="Rechercher un manga..." placeholder="Rechercher un manga..."
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" /> class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500" />
<button <button
@click="performSearch" @click="performSearch"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"> class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
@@ -20,27 +20,27 @@
<!-- État de chargement --> <!-- État de chargement -->
<div v-if="loading" class="text-center py-8"> <div v-if="loading" class="text-center py-8">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div> <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p class="mt-4 text-gray-600">Recherche en cours...</p> <p class="mt-4 text-gray-600 dark:text-gray-400">Recherche en cours...</p>
</div> </div>
<!-- Message d'erreur --> <!-- Message d'erreur -->
<div v-if="error" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-6"> <div v-if="error" class="bg-red-100 dark:bg-red-900/20 border border-red-400 dark:border-red-700 text-red-700 dark:text-red-400 px-4 py-3 rounded relative mb-6">
{{ error }} {{ error }}
</div> </div>
<!-- Résultats de recherche --> <!-- Résultats de recherche -->
<div class="max-w-full overflow-hidden"> <div class="max-w-full overflow-hidden">
<MangaList v-if="searchResults.length > 0" :mangas="searchResults" @manga-click="openMangaModal" /> <MangaList v-if="searchResults.length > 0" :mangas="searchResults" @manga-click="openMangaModal" />
<p v-else-if="!loading && searchQuery" class="text-center text-gray-600">Aucun résultat trouvé</p> <p v-else-if="!loading && searchQuery" class="text-center text-gray-600 dark:text-gray-400">Aucun résultat trouvé</p>
</div> </div>
<!-- Modal de confirmation --> <!-- Modal de confirmation -->
<Dialog :open="isModalOpen" @close="closeModal" class="relative z-50"> <Dialog :open="isModalOpen" @close="closeModal" class="relative z-50">
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true" /> <div class="fixed inset-0 bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-80 transition-opacity" aria-hidden="true" />
<div class="fixed inset-0 flex items-center justify-center p-4"> <div class="fixed inset-0 flex items-center justify-center p-4">
<DialogPanel class="w-full max-w-lg bg-white rounded-xl shadow-xl p-6"> <DialogPanel class="w-full max-w-lg bg-white dark:bg-gray-800 rounded-xl shadow-xl p-6">
<DialogTitle class="text-lg mb-4"> Ajouter à la bibliothèque </DialogTitle> <DialogTitle class="text-lg mb-4 text-gray-900 dark:text-gray-100"> Ajouter à la bibliothèque </DialogTitle>
<div v-if="selectedManga"> <div v-if="selectedManga">
<div class="flex gap-4"> <div class="flex gap-4">
@@ -49,8 +49,8 @@
:alt="selectedManga.title" :alt="selectedManga.title"
class="h-48 w-32 object-cover" /> class="h-48 w-32 object-cover" />
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<h4 class="text-lg">{{ selectedManga.title }}</h4> <h4 class="text-lg text-gray-900 dark:text-gray-100">{{ selectedManga.title }}</h4>
<p class="mt-2"> <p class="mt-2 text-gray-700 dark:text-gray-300">
{{ truncatedDescription }} {{ truncatedDescription }}
</p> </p>
</div> </div>
@@ -61,7 +61,7 @@
<button <button
type="button" type="button"
@click="closeModal" @click="closeModal"
class="px-4 py-2 rounded-lg border border-gray-300 hover:bg-gray-50"> class="px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 dark:bg-gray-800">
Annuler Annuler
</button> </button>
<button <button

View File

@@ -2,11 +2,20 @@
<div> <div>
<Toolbar :config="toolbarConfig" class="sticky top-16 z-10" /> <Toolbar :config="toolbarConfig" class="sticky top-16 z-10" />
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<MangaGrid v-if="viewMode === 'grid'" :mangas="collection?.items || []" /> <MangaGrid v-if="viewMode === 'grid'" :mangas="pagedItems" />
<MangaList <MangaList
v-else-if="viewMode === 'list'" v-else-if="viewMode === 'list'"
:mangas="collection?.items || []" :mangas="pagedItems"
@manga-click="handleMangaClick" /> @manga-click="handleMangaClick" />
<Pagination
v-if="totalPages > 1"
:current-page="currentPage"
:total-pages="totalPages"
:total="sortedCollection.length"
:limit="prefs.itemsPerPage"
:has-next-page="currentPage < totalPages"
:has-previous-page="currentPage > 1"
@page-change="currentPage = $event" />
<div <div
v-if="isBackgroundLoading" v-if="isBackgroundLoading"
class="fixed bottom-4 right-4 bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg"> class="fixed bottom-4 right-4 bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg">
@@ -26,8 +35,10 @@
MagnifyingGlassIcon MagnifyingGlassIcon
} from '@heroicons/vue/24/outline'; } from '@heroicons/vue/24/outline';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { onMounted, ref } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useUserPreferencesStore } from '../../../../domain/setting/application/store/userPreferencesStore';
import Pagination from '../../../../shared/components/ui/Pagination.vue';
import Toolbar from '../../../../shared/components/ui/Toolbar.vue'; import Toolbar from '../../../../shared/components/ui/Toolbar.vue';
import { useMangaStore } from '../../application/store/mangaStore'; import { useMangaStore } from '../../application/store/mangaStore';
import MangaGrid from '../components/MangaGrid.vue'; import MangaGrid from '../components/MangaGrid.vue';
@@ -35,6 +46,7 @@ import MangaList from '../components/MangaList.vue';
const router = useRouter(); const router = useRouter();
const mangaStore = useMangaStore(); const mangaStore = useMangaStore();
const prefs = useUserPreferencesStore();
const { const {
collection, collection,
@@ -43,7 +55,8 @@ import MangaList from '../components/MangaList.vue';
isBackgroundLoadingCollection: isBackgroundLoading isBackgroundLoadingCollection: isBackgroundLoading
} = storeToRefs(mangaStore); } = storeToRefs(mangaStore);
const viewMode = ref('grid'); const viewMode = ref(prefs.defaultView);
const currentPage = ref(1);
onMounted(() => { onMounted(() => {
mangaStore.loadCollection(); mangaStore.loadCollection();
@@ -53,6 +66,27 @@ import MangaList from '../components/MangaList.vue';
router.push({ name: 'manga-details', params: { id: manga.id } }); router.push({ name: 'manga-details', params: { id: manga.id } });
}; };
const sortedCollection = computed(() => {
const items = [...(collection.value?.items || [])];
if (prefs.sortBy === 'title') {
items.sort((a, b) => a.title.localeCompare(b.title));
} else if (prefs.sortBy === 'addedAt') {
items.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
}
return items;
});
const pagedItems = computed(() => {
const start = (currentPage.value - 1) * prefs.itemsPerPage;
return sortedCollection.value.slice(start, start + prefs.itemsPerPage);
});
const totalPages = computed(() => Math.ceil(sortedCollection.value.length / prefs.itemsPerPage));
watch(() => prefs.itemsPerPage, () => {
currentPage.value = 1;
});
const toolbarConfig = { const toolbarConfig = {
leftSection: [ leftSection: [
{ {
@@ -71,8 +105,8 @@ import MangaList from '../components/MangaList.vue';
type: 'dropdown', type: 'dropdown',
label: 'View', label: 'View',
items: [ items: [
{ label: 'List', onClick: () => (viewMode.value = 'list') }, { label: 'List', onClick: () => { viewMode.value = 'list'; prefs.setDefaultView('list'); } },
{ label: 'Grid', onClick: () => (viewMode.value = 'grid') } { label: 'Grid', onClick: () => { viewMode.value = 'grid'; prefs.setDefaultView('grid'); } }
] ]
}, },
{ {
@@ -80,10 +114,9 @@ import MangaList from '../components/MangaList.vue';
type: 'dropdown', type: 'dropdown',
label: 'Sort', label: 'Sort',
items: [ items: [
{ label: 'Title', onClick: () => {} }, { label: 'Title', onClick: () => prefs.setSortBy('title') },
{ label: 'Author', onClick: () => {} }, { label: "Date d'ajout", onClick: () => prefs.setSortBy('addedAt') },
{ label: 'Status', onClick: () => {} }, { label: 'Progression', onClick: () => prefs.setSortBy('progress') }
{ label: 'Year', onClick: () => {} }
] ]
}, },
{ {

View File

@@ -1,9 +1,9 @@
<template> <template>
<div class="min-h-screen bg-gray-50"> <div class="min-h-screen bg-gray-50 dark:bg-gray-900">
<!-- Notifications Toast --> <!-- Notifications Toast -->
<NotificationToast /> <NotificationToast />
<div v-if="errorDetails" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mx-4 mt-4"> <div v-if="errorDetails" class="bg-red-100 dark:bg-red-900/20 border border-red-400 dark:border-red-700 text-red-700 dark:text-red-400 px-4 py-3 rounded mx-4 mt-4">
{{ errorDetails.message || 'Une erreur est survenue lors du chargement des détails.' }} {{ errorDetails.message || 'Une erreur est survenue lors du chargement des détails.' }}
</div> </div>
@@ -13,7 +13,7 @@
<Toolbar :config="toolbarConfig" class="sticky top-16 z-10" /> <Toolbar :config="toolbarConfig" class="sticky top-16 z-10" />
<div v-if="isRefreshingDetails" class="absolute top-2 right-2 text-gray-500 z-20"> <div v-if="isRefreshingDetails" class="absolute top-2 right-2 text-gray-500 dark:text-gray-400 z-20">
<ArrowPathIcon class="h-5 w-5 animate-spin" /> <ArrowPathIcon class="h-5 w-5 animate-spin" />
</div> </div>
@@ -24,7 +24,7 @@
<div v-if="isLoadingVolumes" class="flex justify-center items-center h-32"> <div v-if="isLoadingVolumes" class="flex justify-center items-center h-32">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div> <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div> </div>
<div v-else-if="errorVolumes" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded"> <div v-else-if="errorVolumes" class="bg-red-100 dark:bg-red-900/20 border border-red-400 dark:border-red-700 text-red-700 dark:text-red-400 px-4 py-3 rounded">
{{ errorVolumes.message || 'Une erreur est survenue lors du chargement des volumes.' }} {{ errorVolumes.message || 'Une erreur est survenue lors du chargement des volumes.' }}
</div> </div>
<MangaVolumeList <MangaVolumeList
@@ -84,7 +84,7 @@
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div> <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div> </div>
<div v-else class="text-center text-gray-500 py-10 px-4"> <div v-else class="text-center text-gray-500 dark:text-gray-400 py-10 px-4">
Aucun manga sélectionné ou trouvé. Aucun manga sélectionné ou trouvé.
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { useUserPreferencesStore } from '../../../setting/application/store/userPreferencesStore';
import { Chapter } from '../../domain/entities/Chapter'; import { Chapter } from '../../domain/entities/Chapter';
import { ApiChapterRepository } from '../../infrastructure/repository/ApiChapterRepository'; import { ApiChapterRepository } from '../../infrastructure/repository/ApiChapterRepository';
@@ -163,6 +164,16 @@ export const useReaderStore = defineStore('reader', {
loadPreferences() { loadPreferences() {
try { try {
const stored = localStorage.getItem('mangarr-reader-preferences'); const stored = localStorage.getItem('mangarr-reader-preferences');
if (!stored) {
const userPrefs = useUserPreferencesStore();
this.readingDirection = userPrefs.readingDirection;
const modeMap = { scroll: 'infinite', single: 'single', double: 'single' };
this.readingMode = modeMap[userPrefs.readingMode] ?? 'single';
if (userPrefs.readingMode === 'double') {
this.doublePageSettings.autoDetect = true;
}
return;
}
if (stored) { if (stored) {
const preferences = JSON.parse(stored); const preferences = JSON.parse(stored);

View File

@@ -65,6 +65,7 @@
<script setup> <script setup>
import { onMounted, onUnmounted, ref, watch } from 'vue'; import { onMounted, onUnmounted, ref, watch } from 'vue';
import { useHeaderStore } from '../../../../shared/stores/headerStore'; import { useHeaderStore } from '../../../../shared/stores/headerStore';
import { useUserPreferencesStore } from '../../../../domain/setting/application/store/userPreferencesStore';
import { useReaderStore } from '../../application/store/readerStore'; import { useReaderStore } from '../../application/store/readerStore';
import InfiniteReader from './InfiniteReader.vue'; import InfiniteReader from './InfiniteReader.vue';
import ReaderControls from './ReaderControls.vue'; import ReaderControls from './ReaderControls.vue';
@@ -84,6 +85,7 @@ import SingleModeReader from './SingleModeReader.vue';
const store = useReaderStore(); const store = useReaderStore();
const headerStore = useHeaderStore(); const headerStore = useHeaderStore();
const prefs = useUserPreferencesStore();
// Référence vers InfiniteReader pour accéder à ses méthodes // Référence vers InfiniteReader pour accéder à ses méthodes
const infiniteReaderRef = ref(null); const infiniteReaderRef = ref(null);
@@ -97,6 +99,7 @@ import SingleModeReader from './SingleModeReader.vue';
const toggleReadingMode = () => { const toggleReadingMode = () => {
const newMode = store.readingMode === 'single' ? 'infinite' : 'single'; const newMode = store.readingMode === 'single' ? 'infinite' : 'single';
store.setReadingMode(newMode); store.setReadingMode(newMode);
prefs.setReadingMode(newMode === 'infinite' ? 'scroll' : 'single');
// Gérer la visibilité selon le mode // Gérer la visibilité selon le mode
if (newMode === 'single') { if (newMode === 'single') {
@@ -111,7 +114,9 @@ import SingleModeReader from './SingleModeReader.vue';
}; };
const toggleReadingDirection = () => { const toggleReadingDirection = () => {
store.setReadingDirection(store.readingDirection === 'ltr' ? 'rtl' : 'ltr'); const newDir = store.readingDirection === 'ltr' ? 'rtl' : 'ltr';
store.setReadingDirection(newDir);
prefs.setReadingDirection(newDir);
resetButtonsTimer(); resetButtonsTimer();
}; };
@@ -222,6 +227,16 @@ import SingleModeReader from './SingleModeReader.vue';
window.addEventListener('keydown', handleKeyPress); window.addEventListener('keydown', handleKeyPress);
// Auto-hide header si activé dans les préférences
if (prefs.autoHideHeaderReader) {
headerStore.enableAutoHide();
}
// Auto-fullscreen si activé dans les préférences
if (prefs.autoFullscreen && document.documentElement.requestFullscreen) {
document.documentElement.requestFullscreen().catch(() => {});
}
// Afficher les boutons au démarrage // Afficher les boutons au démarrage
showButtonsWithTimer(); showButtonsWithTimer();
}); });

View File

@@ -0,0 +1,142 @@
import { defineStore } from 'pinia';
const STORAGE_KEY = 'mangarr_preferences';
const defaultState = {
theme: 'system',
language: 'fr',
defaultView: 'grid',
itemsPerPage: 20,
sortBy: 'title',
readingDirection: 'ltr',
readingMode: 'scroll',
autoFullscreen: false,
autoHideHeaderReader: true,
toastDuration: 5000,
};
function loadFromStorage() {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
return { ...defaultState, ...JSON.parse(stored) };
}
} catch {
// ignore parse errors
}
return { ...defaultState };
}
let mediaQueryUnsubscribe = null;
export const useUserPreferencesStore = defineStore('userPreferences', {
state: () => loadFromStorage(),
actions: {
applyTheme() {
// Nettoyer le listener précédent
if (mediaQueryUnsubscribe) {
mediaQueryUnsubscribe();
mediaQueryUnsubscribe = null;
}
const html = document.documentElement;
if (this.theme === 'dark') {
html.classList.add('dark');
} else if (this.theme === 'light') {
html.classList.remove('dark');
} else {
// mode 'system'
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e) => {
if (e.matches) {
html.classList.add('dark');
} else {
html.classList.remove('dark');
}
};
handler(mq);
mq.addEventListener('change', handler);
mediaQueryUnsubscribe = () => mq.removeEventListener('change', handler);
}
},
setTheme(theme) {
this.theme = theme;
this.persist();
this.applyTheme();
},
setLanguage(language) {
this.language = language;
this.persist();
},
setDefaultView(view) {
this.defaultView = view;
this.persist();
},
setItemsPerPage(count) {
this.itemsPerPage = count;
this.persist();
},
setSortBy(sort) {
this.sortBy = sort;
this.persist();
},
setReadingDirection(direction) {
this.readingDirection = direction;
this.persist();
},
setReadingMode(mode) {
this.readingMode = mode;
this.persist();
},
setAutoFullscreen(value) {
this.autoFullscreen = value;
this.persist();
},
setAutoHideHeaderReader(value) {
this.autoHideHeaderReader = value;
this.persist();
},
setToastDuration(duration) {
this.toastDuration = duration;
this.persist();
},
resetToDefaults() {
Object.assign(this, defaultState);
this.persist();
this.applyTheme();
},
persist() {
try {
const data = {
theme: this.theme,
language: this.language,
defaultView: this.defaultView,
itemsPerPage: this.itemsPerPage,
sortBy: this.sortBy,
readingDirection: this.readingDirection,
readingMode: this.readingMode,
autoFullscreen: this.autoFullscreen,
autoHideHeaderReader: this.autoHideHeaderReader,
toastDuration: this.toastDuration,
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
} catch {
// ignore storage errors
}
},
},
});

View File

@@ -0,0 +1,242 @@
<template>
<div class="container mx-auto px-4 py-8 max-w-3xl">
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ t('preferences.title') }}</h1>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{ t('preferences.subtitle') }}</p>
</div>
<button
class="px-3 py-1.5 text-sm text-gray-600 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
@click="handleReset">
{{ t('preferences.reset') }}
</button>
</div>
<!-- Apparence -->
<section class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 mb-4">
<h2 class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider px-6 pt-5 pb-3">
{{ t('preferences.sections.appearance') }}
</h2>
<div class="divide-y divide-gray-100 dark:divide-gray-700">
<!-- Thème -->
<div class="flex items-center justify-between px-6 py-4">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('preferences.theme.label') }}</label>
<select
:value="store.theme"
class="text-sm border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1.5 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
@change="store.setTheme($event.target.value)">
<option value="light">{{ t('preferences.theme.light') }}</option>
<option value="dark">{{ t('preferences.theme.dark') }}</option>
<option value="system">{{ t('preferences.theme.system') }}</option>
</select>
</div>
<!-- Langue -->
<div class="flex items-center justify-between px-6 py-4">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('preferences.language.label') }}</label>
<select
:value="store.language"
class="text-sm border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1.5 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
@change="handleLanguageChange($event.target.value)">
<option value="fr">{{ t('preferences.language.fr') }}</option>
<option value="en">{{ t('preferences.language.en') }}</option>
</select>
</div>
</div>
</section>
<!-- Affichage collection -->
<section class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 mb-4">
<h2 class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider px-6 pt-5 pb-3">
{{ t('preferences.sections.collection') }}
</h2>
<div class="divide-y divide-gray-100 dark:divide-gray-700">
<!-- Vue par défaut -->
<div class="flex items-center justify-between px-6 py-4">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('preferences.defaultView.label') }}</label>
<div class="flex gap-2">
<button
:class="viewButtonClass('grid')"
@click="store.setDefaultView('grid')">
{{ t('preferences.defaultView.grid') }}
</button>
<button
:class="viewButtonClass('list')"
@click="store.setDefaultView('list')">
{{ t('preferences.defaultView.list') }}
</button>
</div>
</div>
<!-- Mangas par page -->
<div class="flex items-center justify-between px-6 py-4">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('preferences.itemsPerPage.label') }}</label>
<div class="flex gap-2">
<button
v-for="n in [12, 20, 40]"
:key="n"
:class="countButtonClass(n)"
@click="store.setItemsPerPage(n)">
{{ n }}
</button>
</div>
</div>
<!-- Tri par défaut -->
<div class="flex items-center justify-between px-6 py-4">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('preferences.sortBy.label') }}</label>
<select
:value="store.sortBy"
class="text-sm border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1.5 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
@change="store.setSortBy($event.target.value)">
<option value="title">{{ t('preferences.sortBy.title') }}</option>
<option value="addedAt">{{ t('preferences.sortBy.addedAt') }}</option>
<option value="progress">{{ t('preferences.sortBy.progress') }}</option>
</select>
</div>
</div>
</section>
<!-- Lecture -->
<section class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 mb-4">
<h2 class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider px-6 pt-5 pb-3">
{{ t('preferences.sections.reading') }}
</h2>
<div class="divide-y divide-gray-100 dark:divide-gray-700">
<!-- Direction de lecture -->
<div class="flex items-center justify-between px-6 py-4">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('preferences.readingDirection.label') }}</label>
<select
:value="store.readingDirection"
class="text-sm border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1.5 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
@change="store.setReadingDirection($event.target.value)">
<option value="ltr">{{ t('preferences.readingDirection.ltr') }}</option>
<option value="rtl">{{ t('preferences.readingDirection.rtl') }}</option>
</select>
</div>
<!-- Mode d'affichage -->
<div class="flex items-center justify-between px-6 py-4">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('preferences.readingMode.label') }}</label>
<select
:value="store.readingMode"
class="text-sm border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1.5 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
@change="store.setReadingMode($event.target.value)">
<option value="scroll">{{ t('preferences.readingMode.scroll') }}</option>
<option value="single">{{ t('preferences.readingMode.single') }}</option>
<option value="double">{{ t('preferences.readingMode.double') }}</option>
</select>
</div>
<!-- Auto plein écran -->
<div class="flex items-center justify-between px-6 py-4">
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('preferences.autoFullscreen.label') }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">{{ t('preferences.autoFullscreen.description') }}</p>
</div>
<button
:class="toggleClass(store.autoFullscreen)"
role="switch"
:aria-checked="store.autoFullscreen"
@click="store.setAutoFullscreen(!store.autoFullscreen)">
<span :class="toggleKnobClass(store.autoFullscreen)" />
</button>
</div>
<!-- Auto-hide header -->
<div class="flex items-center justify-between px-6 py-4">
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('preferences.autoHideHeaderReader.label') }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">{{ t('preferences.autoHideHeaderReader.description') }}</p>
</div>
<button
:class="toggleClass(store.autoHideHeaderReader)"
role="switch"
:aria-checked="store.autoHideHeaderReader"
@click="store.setAutoHideHeaderReader(!store.autoHideHeaderReader)">
<span :class="toggleKnobClass(store.autoHideHeaderReader)" />
</button>
</div>
</div>
</section>
<!-- Notifications -->
<section class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 mb-4">
<h2 class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider px-6 pt-5 pb-3">
{{ t('preferences.sections.notifications') }}
</h2>
<div class="divide-y divide-gray-100 dark:divide-gray-700">
<!-- Durée des toasts -->
<div class="flex items-center justify-between px-6 py-4">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('preferences.toastDuration.label') }}</label>
<div class="flex gap-2">
<button
v-for="[val, label] in toastOptions"
:key="val"
:class="countButtonClass(val, store.toastDuration)"
@click="store.setToastDuration(val)">
{{ t(label) }}
</button>
</div>
</div>
</div>
</section>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n';
import { useUserPreferencesStore } from '../../application/store/userPreferencesStore';
import { i18n } from '../../../../shared/i18n';
const { t, locale } = useI18n();
const store = useUserPreferencesStore();
const toastOptions = [
[3000, 'preferences.toastDuration.3s'],
[5000, 'preferences.toastDuration.5s'],
[10000, 'preferences.toastDuration.10s'],
];
function handleLanguageChange(lang) {
store.setLanguage(lang);
i18n.global.locale.value = lang;
locale.value = lang;
}
function handleReset() {
if (confirm(t('preferences.resetConfirm'))) {
store.resetToDefaults();
i18n.global.locale.value = store.language;
locale.value = store.language;
}
}
function viewButtonClass(view) {
const active = store.defaultView === view;
return [
'px-3 py-1.5 text-sm rounded-lg border transition-colors',
active
? 'bg-blue-600 text-white border-blue-600'
: 'border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700',
];
}
function countButtonClass(val, current = store.itemsPerPage) {
const active = current === val;
return [
'px-3 py-1.5 text-sm rounded-lg border transition-colors',
active
? 'bg-blue-600 text-white border-blue-600'
: 'border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700',
];
}
function toggleClass(active) {
return [
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
active ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-600',
];
}
function toggleKnobClass(active) {
return [
'inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform',
active ? 'translate-x-6' : 'translate-x-1',
];
}
</script>

View File

@@ -4,6 +4,9 @@ import App from './App.vue';
import { router } from './router'; import { router } from './router';
import '../../styles/app.scss'; import '../../styles/app.scss';
import { installVueQuery } from './shared/plugin/vueQuery'; import { installVueQuery } from './shared/plugin/vueQuery';
import { i18n } from './shared/i18n';
import { useUserPreferencesStore } from './domain/setting/application/store/userPreferencesStore';
// Création du store // Création du store
const pinia = createPinia(); const pinia = createPinia();
@@ -14,5 +17,12 @@ const app = createApp(App);
app.use(router); app.use(router);
app.use(pinia); app.use(pinia);
app.use(installVueQuery); app.use(installVueQuery);
app.use(i18n);
// Appliquer le thème et la langue sauvegardés
const prefs = useUserPreferencesStore();
prefs.applyTheme();
i18n.global.locale.value = prefs.language;
// Montage de l'application // Montage de l'application
app.mount('#vue-app'); app.mount('#vue-app');

View File

@@ -8,6 +8,7 @@ import MangaDetails from '../domain/manga/presentation/pages/MangaDetails.vue';
import ChapterPage from '../domain/reader/presentation/pages/ChapterPage.vue'; import ChapterPage from '../domain/reader/presentation/pages/ChapterPage.vue';
import ScrapperConfigurations from '../domain/setting/presentation/pages/ScrapperConfigurations.vue'; import ScrapperConfigurations from '../domain/setting/presentation/pages/ScrapperConfigurations.vue';
import ScrapperEdit from '../domain/setting/presentation/pages/ScrapperEdit.vue'; import ScrapperEdit from '../domain/setting/presentation/pages/ScrapperEdit.vue';
import UserPreferencesPage from '../domain/setting/presentation/pages/UserPreferencesPage.vue';
import Layout from '../shared/components/layout/Layout.vue'; import Layout from '../shared/components/layout/Layout.vue';
// Placeholder component for new routes // Placeholder component for new routes
@@ -129,8 +130,7 @@ const routes = [
{ {
path: '/settings/ui', path: '/settings/ui',
name: 'settings-ui', name: 'settings-ui',
component: PlaceholderComponent, component: UserPreferencesPage
props: { title: "Paramètres de l'interface" }
}, },
// Système // Système
{ {

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="min-h-screen bg-gray-50 flex"> <div class="min-h-screen bg-gray-50 dark:bg-gray-900 flex">
<Header <Header
:show-menu-button="isReaderMode" :show-menu-button="isReaderMode"
@menu-click="toggleSidebar" @menu-click="toggleSidebar"

View File

@@ -9,7 +9,7 @@
v-for="notification in notifications" v-for="notification in notifications"
:key="notification.id" :key="notification.id"
:class="[ :class="[
'max-w-md w-full bg-white shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden', 'max-w-md w-full bg-white dark:bg-gray-800 shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden',
getNotificationClass(notification.type) getNotificationClass(notification.type)
]" ]"
> >
@@ -18,14 +18,14 @@
<div class="flex-shrink-0 mr-3"> <div class="flex-shrink-0 mr-3">
<button <button
@click="removeNotification(notification.id)" @click="removeNotification(notification.id)"
class="bg-white rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" class="bg-white dark:bg-gray-800 rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
> >
<span class="sr-only">Close</span> <span class="sr-only">Close</span>
<XMarkIcon class="h-5 w-5" /> <XMarkIcon class="h-5 w-5" />
</button> </button>
</div> </div>
<div class="flex-1 pt-0.5 min-w-0"> <div class="flex-1 pt-0.5 min-w-0">
<p class="text-sm font-medium text-gray-900 break-words"> <p class="text-sm font-medium text-gray-900 dark:text-gray-100 break-words">
{{ notification.message }} {{ notification.message }}
</p> </p>
</div> </div>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div v-if="totalPages > 1" class="flex items-center justify-between px-4 py-3 bg-white border-t border-gray-200"> <div v-if="totalPages > 1" class="flex items-center justify-between px-4 py-3 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
<!-- Informations de pagination --> <!-- Informations de pagination -->
<div class="flex items-center text-sm text-gray-700"> <div class="flex items-center text-sm text-gray-700 dark:text-gray-300">
<span> <span>
Affichage de Affichage de
<span class="font-medium">{{ startItem }}</span> <span class="font-medium">{{ startItem }}</span>
@@ -22,8 +22,8 @@
:class="[ :class="[
'relative inline-flex items-center px-2 py-2 text-sm font-medium rounded-md', 'relative inline-flex items-center px-2 py-2 text-sm font-medium rounded-md',
hasPreviousPage hasPreviousPage
? 'text-gray-500 bg-white border border-gray-300 hover:bg-gray-50' ? 'text-gray-500 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600'
: 'text-gray-300 bg-gray-100 border border-gray-200 cursor-not-allowed' : 'text-gray-300 dark:text-gray-600 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 cursor-not-allowed'
]"> ]">
<span class="sr-only">Précédent</span> <span class="sr-only">Précédent</span>
<ChevronLeftIcon class="h-5 w-5" /> <ChevronLeftIcon class="h-5 w-5" />
@@ -38,14 +38,14 @@
:class="[ :class="[
'relative inline-flex items-center px-3 py-2 text-sm font-medium rounded-md', 'relative inline-flex items-center px-3 py-2 text-sm font-medium rounded-md',
currentPage === 1 currentPage === 1
? 'z-10 bg-indigo-50 border-indigo-500 text-indigo-600' ? 'z-10 bg-indigo-50 dark:bg-indigo-900/30 border-indigo-500 text-indigo-600 dark:text-indigo-400'
: 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50' : 'bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 text-gray-500 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600'
]"> ]">
1 1
</button> </button>
<!-- Points de suspension gauche --> <!-- Points de suspension gauche -->
<span v-if="showLeftDots" class="relative inline-flex items-center px-3 py-2 text-sm font-medium text-gray-700"> <span v-if="showLeftDots" class="relative inline-flex items-center px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300">
... ...
</span> </span>
@@ -57,14 +57,14 @@
:class="[ :class="[
'relative inline-flex items-center px-3 py-2 text-sm font-medium rounded-md', 'relative inline-flex items-center px-3 py-2 text-sm font-medium rounded-md',
currentPage === page currentPage === page
? 'z-10 bg-indigo-50 border-indigo-500 text-indigo-600' ? 'z-10 bg-indigo-50 dark:bg-indigo-900/30 border-indigo-500 text-indigo-600 dark:text-indigo-400'
: 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50' : 'bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 text-gray-500 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600'
]"> ]">
{{ page }} {{ page }}
</button> </button>
<!-- Points de suspension droite --> <!-- Points de suspension droite -->
<span v-if="showRightDots" class="relative inline-flex items-center px-3 py-2 text-sm font-medium text-gray-700"> <span v-if="showRightDots" class="relative inline-flex items-center px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300">
... ...
</span> </span>
@@ -75,8 +75,8 @@
:class="[ :class="[
'relative inline-flex items-center px-3 py-2 text-sm font-medium rounded-md', 'relative inline-flex items-center px-3 py-2 text-sm font-medium rounded-md',
currentPage === totalPages currentPage === totalPages
? 'z-10 bg-indigo-50 border-indigo-500 text-indigo-600' ? 'z-10 bg-indigo-50 dark:bg-indigo-900/30 border-indigo-500 text-indigo-600 dark:text-indigo-400'
: 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50' : 'bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 text-gray-500 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600'
]"> ]">
{{ totalPages }} {{ totalPages }}
</button> </button>
@@ -84,7 +84,7 @@
<!-- Pagination mobile --> <!-- Pagination mobile -->
<div class="md:hidden flex items-center space-x-2"> <div class="md:hidden flex items-center space-x-2">
<span class="text-sm text-gray-700"> <span class="text-sm text-gray-700 dark:text-gray-300">
{{ currentPage }} / {{ totalPages }} {{ currentPage }} / {{ totalPages }}
</span> </span>
</div> </div>
@@ -96,8 +96,8 @@
:class="[ :class="[
'relative inline-flex items-center px-2 py-2 text-sm font-medium rounded-md', 'relative inline-flex items-center px-2 py-2 text-sm font-medium rounded-md',
hasNextPage hasNextPage
? 'text-gray-500 bg-white border border-gray-300 hover:bg-gray-50' ? 'text-gray-500 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600'
: 'text-gray-300 bg-gray-100 border border-gray-200 cursor-not-allowed' : 'text-gray-300 dark:text-gray-600 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 cursor-not-allowed'
]"> ]">
<span class="sr-only">Suivant</span> <span class="sr-only">Suivant</span>
<ChevronRightIcon class="h-5 w-5" /> <ChevronRightIcon class="h-5 w-5" />

View File

@@ -1,4 +1,5 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { useUserPreferencesStore } from '../../domain/setting/application/store/userPreferencesStore';
const notifications = ref([]); const notifications = ref([]);
let nextId = 1; let nextId = 1;
@@ -36,20 +37,24 @@ export function useNotifications() {
notifications.value = []; notifications.value = [];
}; };
const showSuccess = (message, duration = 4000) => { const showSuccess = (message, duration) => {
return addNotification(message, 'success', duration); const prefs = useUserPreferencesStore();
return addNotification(message, 'success', duration ?? prefs.toastDuration);
}; };
const showError = (message, duration = 6000) => { const showError = (message, duration) => {
return addNotification(message, 'error', duration); const prefs = useUserPreferencesStore();
return addNotification(message, 'error', duration ?? prefs.toastDuration);
}; };
const showWarning = (message, duration = 5000) => { const showWarning = (message, duration) => {
return addNotification(message, 'warning', duration); const prefs = useUserPreferencesStore();
return addNotification(message, 'warning', duration ?? prefs.toastDuration);
}; };
const showInfo = (message, duration = 4000) => { const showInfo = (message, duration) => {
return addNotification(message, 'info', duration); const prefs = useUserPreferencesStore();
return addNotification(message, 'info', duration ?? prefs.toastDuration);
}; };
return { return {

View File

@@ -0,0 +1,10 @@
import { createI18n } from 'vue-i18n';
import fr from './locales/fr.json';
import en from './locales/en.json';
export const i18n = createI18n({
legacy: false,
locale: 'fr',
fallbackLocale: 'fr',
messages: { fr, en },
});

View File

@@ -0,0 +1,67 @@
{
"nav": {
"preferences": "Preferences"
},
"preferences": {
"title": "Preferences",
"subtitle": "Customize the interface to your liking",
"reset": "Reset",
"resetConfirm": "Reset to default values?",
"sections": {
"appearance": "Appearance",
"collection": "Collection display",
"reading": "Reading",
"notifications": "Notifications"
},
"theme": {
"label": "Theme",
"light": "Light",
"dark": "Dark",
"system": "System (automatic)"
},
"language": {
"label": "Language",
"fr": "Français",
"en": "English"
},
"defaultView": {
"label": "Default view",
"grid": "Grid",
"list": "List"
},
"itemsPerPage": {
"label": "Mangas per page"
},
"sortBy": {
"label": "Default sort",
"title": "Title",
"addedAt": "Date added",
"progress": "Progress"
},
"readingDirection": {
"label": "Reading direction",
"ltr": "Left → Right (western)",
"rtl": "Right → Left (manga)"
},
"readingMode": {
"label": "Display mode",
"scroll": "Vertical scroll",
"single": "Single page",
"double": "Double page"
},
"autoFullscreen": {
"label": "Auto fullscreen",
"description": "Enter fullscreen when starting the reader"
},
"autoHideHeaderReader": {
"label": "Auto-hide header",
"description": "Hide the navigation bar in reading mode"
},
"toastDuration": {
"label": "Notification duration",
"3s": "3 seconds",
"5s": "5 seconds",
"10s": "10 seconds"
}
}
}

View File

@@ -0,0 +1,67 @@
{
"nav": {
"preferences": "Préférences"
},
"preferences": {
"title": "Préférences",
"subtitle": "Personnalisez l'interface selon vos goûts",
"reset": "Réinitialiser",
"resetConfirm": "Remettre les valeurs par défaut ?",
"sections": {
"appearance": "Apparence",
"collection": "Affichage de la collection",
"reading": "Lecture",
"notifications": "Notifications"
},
"theme": {
"label": "Thème",
"light": "Clair",
"dark": "Sombre",
"system": "Système (automatique)"
},
"language": {
"label": "Langue",
"fr": "Français",
"en": "English"
},
"defaultView": {
"label": "Vue par défaut",
"grid": "Grille",
"list": "Liste"
},
"itemsPerPage": {
"label": "Mangas par page"
},
"sortBy": {
"label": "Tri par défaut",
"title": "Titre",
"addedAt": "Date d'ajout",
"progress": "Progression"
},
"readingDirection": {
"label": "Direction de lecture",
"ltr": "Gauche → Droite (occidental)",
"rtl": "Droite → Gauche (manga)"
},
"readingMode": {
"label": "Mode d'affichage",
"scroll": "Défilement vertical",
"single": "Page unique",
"double": "Double page"
},
"autoFullscreen": {
"label": "Plein écran automatique",
"description": "Passer en plein écran au démarrage du lecteur"
},
"autoHideHeaderReader": {
"label": "Masquer automatiquement l'en-tête",
"description": "Masquer la barre de navigation en mode lecture"
},
"toastDuration": {
"label": "Durée des notifications",
"3s": "3 secondes",
"5s": "5 secondes",
"10s": "10 secondes"
}
}
}

1965
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -52,6 +52,7 @@
"react-router-dom": "^7.1.5", "react-router-dom": "^7.1.5",
"sortablejs": "^1.15.2", "sortablejs": "^1.15.2",
"tailwindcss": "^3.2.7", "tailwindcss": "^3.2.7",
"vue-i18n": "^11.3.0",
"vuedraggable": "^2.24.3" "vuedraggable": "^2.24.3"
} }
} }