- Ajout du store userPreferencesStore (thème, vue, tri, pagination, lecteur) - Page UserPreferencesPage pour configurer toutes les préférences - Câblage des prefs dans HomePage (viewMode, sortBy, itemsPerPage), readerStore (fallback prefs), ChapterReader (autoHide, autoFullscreen, sync), useNotifications (toastDuration) - Thème sombre (dark: Tailwind) sur tous les composants Vue : Layout, Pagination, NotificationToast, MangaCard, MangaVolume, MangaDetails, AddManga, HomePage, ActivityPage, JobItem, MangaDeleteModal, MangaEditModal, MangaPreferredSourcesModal, ManageChaptersModal, MangaChapterList, MangaChapter, ConversionPage, FileUploadArea, ConversionProgress, NewImportPage, FileImportCard, MangaMatchCard, StatusBadge, ImportResults - i18n partiellement initialisé Jeremy Guillot
243 lines
13 KiB
Vue
243 lines
13 KiB
Vue
<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>
|