BREAKING CHANGE: nouvelle architecture frontend avec layout sidebar, système de thèmes et routes restructurées ### Frontend - nouveau layout global `AppLayout.vue` avec sidebar de navigation inspirée Radarr/Sonarr - sidebar configurable via un objet `NavItem[]` avec support parents/enfants cliquables - 3 thèmes CSS (Indigo Électrique, Crimson Night, Teal Froid) persistés via localStorage - switcher de thème en bas de sidebar avec pastilles colorées - logo FanKarr cliquable, bouton déconnexion discret dans le header sidebar - badge animé sur "Activité" affichant le nombre de téléchargements en cours - migration complète vers Tailwind v4 avec variables CSS `@theme` - classes utilitaires globales : `btn-primary`, `btn-secondary`, `settings-card`, `settings-input` ### Vues - `SeriesView` : barre colorée en bas des affiches (vert/bleu/orange), dropdown filtres, boutons taille S/M/L/XL persistés - `SerieView` : bouton état unique par épisode (↓/⟳/✓/✗), badges simplifiés, toast via `useToast` global - `ActivityView` : onglets pill, stats, toggle "masquer importés", polling silencieux - `DashboardView` : stats catalogue/importées/en cours/erreurs, torrents actifs, derniers imports - `SettingsView` découpée en 3 vues indépendantes : `DownloadClientView`, `MediaManagementView`, `CatalogueView` - `LogsView` déplacée dans `src/views/settings/` - `AuthView` : migré au système de thème ### Composants - `SidebarNavItem` : support badge, chevron masqué si badge présent - `SettingsToggle` : composant réutilisable `v-model` - `FolderPicker` : migré au thème, `Teleport` intégré - `ToastContainer` : déplacé en bas droite, groupement des toasts similaires avec badge count ### Router - `AppLayout` comme layout racine pour toutes les routes protégées - `/auth` reste hors layout - redirections `/` → `/series`, `/downloads` → `/activity` - `SettingsLayout` avec titre dynamique par route - scroll géré sur `<main id="main-scroll">` dans `AppLayout` ### Stores & composables - nouveau store `downloads` partagé pour le badge sidebar - `usePosterSize` : taille des affiches persistée en localStorage - `useToast` : groupement des toasts du même type, timer reset au lieu d'empilement ### Serveur - logs structurés via `logger` dans `auth.ts`, `organize.ts`, `organize-worker.ts`, `torrent-clients/index.ts`, `qbittorrent.ts`, `index.ts` - suppression des `console.log/error/warn` en faveur de `logger.info/warn/error/debug` - messages worker routés vers `logger` via `organize.ts` - logs `debug` pour les événements verbeux (healthcheck, skip, worker terminé)
102 lines
3.0 KiB
Vue
102 lines
3.0 KiB
Vue
<template>
|
|
<div>
|
|
<component
|
|
:is="item.to ? RouterLink : 'button'"
|
|
:to="item.to ?? undefined"
|
|
class="w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg cursor-pointer transition-colors duration-100 text-left group"
|
|
:class="isActive
|
|
? 'bg-active text-primary'
|
|
: 'text-secondary hover:bg-hover hover:text-primary'"
|
|
@click="item.children && !item.to && toggleOpen()"
|
|
>
|
|
<component
|
|
:is="iconComponent"
|
|
:size="17"
|
|
class="shrink-0 transition-colors"
|
|
:class="isActive ? 'text-accent' : 'text-muted group-hover:text-secondary'"
|
|
/>
|
|
<span class="flex-1 text-sm truncate">{{ item.label }}</span>
|
|
|
|
<!-- Badge animé (ex: downloads actifs) -->
|
|
<span
|
|
v-if="item.badge"
|
|
class="min-w-[18px] h-[18px] px-1 rounded-full bg-accent text-white text-[10px] font-medium flex items-center justify-center"
|
|
>
|
|
{{ item.badge }}
|
|
</span>
|
|
|
|
<!-- Chevron (seulement si pas de badge) -->
|
|
<ChevronRight
|
|
v-else-if="item.children"
|
|
:size="13"
|
|
class="shrink-0 text-muted transition-transform duration-200"
|
|
:class="isOpen ? 'rotate-90' : ''"
|
|
/>
|
|
</component>
|
|
|
|
<Transition name="slide">
|
|
<div v-if="item.children && isOpen" class="ml-[34px] mt-0.5 flex flex-col gap-0.5">
|
|
<RouterLink
|
|
v-for="child in item.children"
|
|
:key="child.to"
|
|
:to="child.to"
|
|
class="flex items-center gap-2 px-2.5 py-1.5 rounded-md transition-colors duration-100"
|
|
:class="route.path === child.to
|
|
? 'text-accent'
|
|
: 'text-muted hover:text-secondary'"
|
|
>
|
|
<span
|
|
class="w-1 h-1 rounded-full shrink-0 transition-colors"
|
|
:class="route.path === child.to ? 'bg-accent' : 'bg-current opacity-40'"
|
|
/>
|
|
<span class="text-xs">{{ child.label }}</span>
|
|
</RouterLink>
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, watch } from 'vue'
|
|
import { RouterLink, useRoute } from 'vue-router'
|
|
import { ChevronRight } from 'lucide-vue-next'
|
|
import * as Icons from 'lucide-vue-next'
|
|
import type { NavItem } from '@/types/nav'
|
|
|
|
const props = defineProps<{ item: NavItem }>()
|
|
|
|
const route = useRoute()
|
|
|
|
const iconComponent = computed(() => {
|
|
if (!props.item.icon) return Icons.Circle
|
|
return (Icons as Record<string, unknown>)[props.item.icon] ?? Icons.Circle
|
|
})
|
|
|
|
const isActive = computed(() =>
|
|
props.item.to
|
|
? route.path.startsWith(props.item.to)
|
|
: props.item.children?.some(c => route.path.startsWith(c.to))
|
|
)
|
|
|
|
const isOpen = ref(isActive.value)
|
|
|
|
watch(isActive, (val) => { if (val) isOpen.value = true })
|
|
|
|
function toggleOpen() {
|
|
isOpen.value = !isOpen.value
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.slide-enter-active,
|
|
.slide-leave-active {
|
|
transition: max-height 0.2s ease, opacity 0.15s ease;
|
|
max-height: 300px;
|
|
overflow: hidden;
|
|
}
|
|
.slide-enter-from,
|
|
.slide-leave-to {
|
|
max-height: 0;
|
|
opacity: 0;
|
|
}
|
|
</style> |