feat(ui): refonte de l'interface

This commit is contained in:
ElPouki committed 2026-09-15 15:30:20 +02:00
1 parent 3ede3e808c
commit 09eb1fa3aa
79 files changed
+5323 -3514

No files matched your search

@@ -1,36 +1,39 @@
<template>
<div class="settings-card flex flex-col gap-4">
<div>
<label class="settings-label mb-1.5">URL du serveur</label>
<input v-model="jellyfinUrl" type="url" class="settings-input" placeholder="http://jellyfin:8096" />
<p class="text-xs text-muted mt-1">URL accessible depuis FanKarr (réseau local ou Docker)</p>
</div>
<div>
<label class="settings-label mb-1.5">Token admin Jellyfin</label>
<input v-model="jellyfinAdminToken" type="password" class="settings-input"
:placeholder="hasToken ? '••••••••••••••••••••• (configuré)' : 'Tableau de bord → Clés API'" />
<p class="text-xs text-muted mt-1">
Dans Jellyfin : Tableau de bord → Clés API → Créer une clé
</p>
<div class="flex flex-col gap-4">
<div class="grid md:grid-cols-2 gap-4">
<div class="flex flex-col gap-[7px]">
<label for="jellyfin-url" class="field-label">Adresse du serveur</label>
<input id="jellyfin-url" v-model="jellyfinUrl" type="url" class="field" placeholder="http://jellyfin:8096" />
<p class="text-xs text-muted">Adresse joignable depuis FanKarr (réseau local ou Docker).</p>
</div>
<div class="flex flex-col gap-[7px]">
<label for="jellyfin-token" class="field-label">Clé API administrateur</label>
<input id="jellyfin-token" v-model="jellyfinAdminToken" type="password" class="field" autocomplete="off"
:placeholder="hasToken ? '•••••••••••••••••••• (configurée)' : 'Tableau de bord → Clés API'" />
<p class="text-xs text-muted">Dans Jellyfin : Tableau de bord → Clés API → Créer une clé.</p>
</div>
</div>
<div class="flex items-center gap-3 flex-wrap">
<button @click="save" :disabled="saving" class="btn-primary">
<div class="flex items-center gap-2.5 flex-wrap">
<button @click="save" :disabled="saving" class="btn-primary pointer-fine:h-[38px]">
{{ saving ? 'Enregistrement…' : 'Enregistrer' }}
</button>
<button @click="testConnection" :disabled="testing" class="btn-secondary">
<button @click="testConnection" :disabled="testing" class="btn-secondary pointer-fine:h-[38px]">
{{ testing ? 'Test…' : 'Tester la connexion' }}
</button>
<span v-if="testResult" class="text-xs" :class="testResult.ok ? 'text-green-400' : 'text-red-400'">
{{ testResult.ok ? `Connecté — Jellyfin ${testResult.version}` : testResult.error }}
<span v-if="testResult" class="pill" :class="testResult.ok ? 'pill-ok' : 'pill-err'">
<Check v-if="testResult.ok" :size="12" :stroke-width="2.5" />
<TriangleAlert v-else :size="12" :stroke-width="2.25" />
{{ testResult.ok ? `Connecté · Jellyfin ${testResult.version}` : testResult.error }}
</span>
<span v-if="saved" class="text-xs text-green-400">Enregistré.</span>
<span v-if="saved" class="text-meta text-ok flex items-center gap-1.5"><Check :size="14" /> Enregistré</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Check, TriangleAlert } from 'lucide-vue-next'
interface JellyfinTestResult { ok: boolean; version?: string; error?: string }
+44 -48
View File
@@ -1,13 +1,13 @@
<template>
<Teleport to="body">
<div class="fixed inset-0 bg-black/60 flex items-center justify-center z-50 px-4" @click.self="emit('close')">
<div class="bg-card border border-border rounded-xl w-full max-w-lg flex flex-col" style="max-height: 85vh">
<div class="modal-backdrop" @click.self="emit('close')">
<div class="bg-card border border-border rounded-card w-full max-w-lg flex flex-col max-h-[85vh] overflow-y-auto shadow-[0_24px_60px_rgb(0_0_0/0.55)]" role="dialog" aria-modal="true" aria-labelledby="plex-title">
<!-- Header -->
<div class="flex items-center justify-between px-6 py-4 border-b border-border shrink-0">
<div class="flex items-center justify-between gap-4 px-6 py-4 border-b border-hover shrink-0">
<div>
<h3 class="text-sm font-semibold text-primary">Configuration Plex</h3>
<p class="text-xs text-muted mt-0.5">
<h3 id="plex-title" class="font-display text-xl font-bold text-primary">Configuration Plex</h3>
<p class="text-meta text-muted mt-0.5">
{{ plexStep === 'auth' ? 'Connectez votre compte Plex' :
plexStep === 'server' ? 'Choisissez votre serveur Plex' :
plexStep === 'library' ? 'Créez la bibliothèque Fankai' :
@@ -15,7 +15,7 @@
'Erreur' }}
</p>
</div>
<button @click="emit('close')" class="text-muted hover:text-primary text-lg leading-none">✕</button>
<button @click="emit('close')" class="btn-icon btn-sm border-transparent" aria-label="Fermer"><X :size="16" /></button>
</div>
<!-- Étape 1 : Auth -->
@@ -23,32 +23,27 @@
<div v-if="!plexAuthMethod" class="flex flex-col gap-3">
<button
@click="startOAuth" :disabled="plexLoading"
class="flex items-center gap-3 p-4 rounded-lg border border-border hover:border-accent hover:bg-accent-muted transition-colors text-left"
class="flex items-center gap-3 p-4 rounded-field border border-border-light hover:border-accent/40 hover:bg-accent-muted transition-colors text-left"
>
<div class="w-8 h-8 rounded-lg bg-accent/10 flex items-center justify-center shrink-0">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" fill="#E5A50A" opacity=".2"/>
<path d="M12 6v6l4 2" stroke="#E5A50A" stroke-width="2" stroke-linecap="round"/>
</svg>
<div class="w-8 h-8 rounded-full bg-accent-muted text-accent flex items-center justify-center shrink-0">
<Clock3 :size="16" />
</div>
<div>
<p class="text-sm font-medium text-primary">{{ plexLoading ? 'Ouverture…' : 'Connexion via Plex' }}</p>
<p class="text-xs text-muted">Google, Apple, ou compte Plex</p>
<p class="text-meta text-muted">Google, Apple, ou compte Plex</p>
</div>
</button>
<button
@click="plexAuthMethod = 'credentials'"
class="flex items-center gap-3 p-4 rounded-lg border border-border hover:border-secondary transition-colors text-left"
class="flex items-center gap-3 p-4 rounded-field border border-border-light hover:bg-hover transition-colors text-left"
>
<div class="w-8 h-8 rounded-lg bg-hover flex items-center justify-center shrink-0">
<svg width="16" height="16" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" fill="none">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
</svg>
<div class="w-8 h-8 rounded-full bg-hover text-secondary flex items-center justify-center shrink-0">
<User :size="16" />
</div>
<div>
<p class="text-sm font-medium text-primary">Email / Mot de passe</p>
<p class="text-xs text-muted">Connexion directe avec vos identifiants</p>
<p class="text-sm font-medium text-primary">E-mail et mot de passe</p>
<p class="text-meta text-muted">Connexion directe avec vos identifiants</p>
</div>
</button>
</div>
@@ -58,26 +53,26 @@
<div class="w-10 h-10 border-2 border-border border-t-accent rounded-full animate-spin" />
<div>
<p class="text-sm text-primary font-medium">En attente de la connexion Plex…</p>
<p class="text-xs text-muted mt-1">Connectez-vous sur la page Plex qui s'est ouverte dans votre navigateur</p>
<p class="text-meta text-muted mt-1">Connectez-vous sur la page Plex qui s'est ouverte dans votre navigateur</p>
</div>
<button @click="cancelOAuth" class="text-xs text-muted hover:text-primary transition-colors">Annuler</button>
<button @click="cancelOAuth" class="btn-ghost btn-sm">Annuler</button>
</div>
<!-- Formulaire credentials -->
<template v-else-if="plexAuthMethod === 'credentials'">
<div>
<label class="settings-label mb-1 block">Adresse e-mail Plex</label>
<input v-model="plexForm.username" type="email" class="settings-input w-full" placeholder="email@example.com" />
<label class="field-label mb-1 block">Adresse e-mail Plex</label>
<input v-model="plexForm.username" type="email" class="field w-full" placeholder="email@example.com" />
</div>
<div>
<label class="settings-label mb-1 block">Mot de passe</label>
<input v-model="plexForm.password" type="password" class="settings-input w-full" placeholder="••••••••" @keyup.enter="plexConnect" />
<label class="field-label mb-1 block">Mot de passe</label>
<input v-model="plexForm.password" type="password" class="field w-full" placeholder="••••••••" @keyup.enter="plexConnect" />
</div>
<div v-if="plexNeeds2FA">
<label class="settings-label mb-1 block">Code 2FA</label>
<input v-model="plexForm.code" type="text" class="settings-input w-full" placeholder="123456" maxlength="6" @keyup.enter="plexConnect" />
<label class="field-label mb-1 block">Code 2FA</label>
<input v-model="plexForm.code" type="text" class="field w-full" placeholder="123456" maxlength="6" @keyup.enter="plexConnect" />
</div>
<p v-if="plexError" class="text-xs text-red-400">{{ plexError }}</p>
<p v-if="plexError" class="text-meta text-err">{{ plexError }}</p>
<div class="flex gap-2">
<button @click="plexConnect" :disabled="plexLoading || !plexForm.username || !plexForm.password" class="btn-primary">
{{ plexLoading ? 'Connexion…' : 'Se connecter' }}
@@ -86,26 +81,26 @@
</div>
</template>
<p v-if="plexError && !plexAuthMethod" class="text-xs text-red-400">{{ plexError }}</p>
<p v-if="plexError && !plexAuthMethod" class="text-meta text-err">{{ plexError }}</p>
<button v-if="!plexAuthMethod" @click="emit('close')" class="btn-secondary self-start">Annuler</button>
</div>
<!-- Étape 2 : Choix serveur -->
<div v-else-if="plexStep === 'server'" class="flex flex-col gap-3 px-6 py-5">
<p class="text-xs text-muted">{{ plexServers.length }} serveur{{ plexServers.length > 1 ? 's' : '' }} trouvé{{ plexServers.length > 1 ? 's' : '' }}</p>
<p class="text-meta text-muted">{{ plexServers.length }} serveur{{ plexServers.length > 1 ? 's' : '' }} trouvé{{ plexServers.length > 1 ? 's' : '' }}</p>
<div
v-for="(server, i) in plexServers" :key="i"
@click="selectServer(server)"
class="flex items-center justify-between p-3 rounded-lg border cursor-pointer transition-colors"
:class="plexSelectedServer === server ? 'border-accent bg-accent-muted' : 'border-border hover:border-secondary'"
class="flex items-center justify-between p-3 rounded-field border cursor-pointer transition-colors"
:class="plexSelectedServer === server ? 'border-accent/50 bg-accent-muted' : 'border-border-light hover:bg-hover'"
>
<div>
<p class="text-sm text-primary font-medium">{{ server.name }}</p>
<p class="text-xs text-muted">{{ server.owned ? 'Propriétaire' : 'Partagé' }} · {{ server.connections.length }} connexion{{ server.connections.length > 1 ? 's' : '' }}</p>
<p class="text-meta text-muted">{{ server.owned ? 'Propriétaire' : 'Partagé' }} · {{ server.connections.length }} connexion{{ server.connections.length > 1 ? 's' : '' }}</p>
</div>
<svg v-if="plexSelectedServer === server" width="14" height="14" viewBox="0 0 24 24" stroke="#e5a50a" stroke-width="2.5" fill="none"><polyline points="20 6 9 17 4 12"/></svg>
<Check v-if="plexSelectedServer === server" :size="15" :stroke-width="2.5" class="text-accent" />
</div>
<p v-if="plexError" class="text-xs text-red-400">{{ plexError }}</p>
<p v-if="plexError" class="text-meta text-err">{{ plexError }}</p>
<div class="flex gap-2 mt-1">
<button @click="plexConnectServer" :disabled="!plexSelectedServer || plexLoading" class="btn-primary">
{{ plexLoading ? 'Connexion…' : 'Continuer' }}
@@ -117,15 +112,15 @@
<!-- Étape 3 : Bibliothèque -->
<div v-else-if="plexStep === 'library'" class="flex flex-col gap-4 px-6 py-5">
<div>
<label class="settings-label mb-1 block">Nom de la bibliothèque</label>
<input v-model="plexForm.libraryName" type="text" class="settings-input w-full" placeholder="Fankai" />
<label class="field-label mb-1 block">Nom de la bibliothèque</label>
<input v-model="plexForm.libraryName" type="text" class="field w-full" placeholder="Fankai" />
</div>
<div>
<label class="settings-label mb-1 block">Chemin de la médiathèque</label>
<label class="field-label mb-1 block">Chemin de la médiathèque</label>
<p class="text-xs text-muted mb-1.5">Chemin tel que vu par votre serveur Plex</p>
<input v-model="plexForm.libraryPath" type="text" class="settings-input w-full font-mono" placeholder="/data/media/fankai" />
<input v-model="plexForm.libraryPath" type="text" class="field w-full" placeholder="/data/media/fankai" />
</div>
<p v-if="plexError" class="text-xs text-red-400">{{ plexError }}</p>
<p v-if="plexError" class="text-meta text-err">{{ plexError }}</p>
<div class="flex gap-2">
<button @click="plexSetup" :disabled="plexLoading || !plexForm.libraryName || !plexForm.libraryPath" class="btn-primary">
{{ plexLoading ? 'Création…' : 'Créer la bibliothèque' }}
@@ -139,16 +134,16 @@
<div class="flex flex-col gap-2">
<div v-for="s in plexSteps" :key="s.step" class="flex items-center gap-2.5">
<div class="w-5 h-5 rounded-full flex items-center justify-center shrink-0"
:class="s.ok ? 'bg-green-500/15 text-green-400' : 'bg-red-500/15 text-red-400'">
<svg v-if="s.ok" width="10" height="10" viewBox="0 0 12 12" stroke="currentColor" stroke-width="2.5" fill="none"><polyline points="2 6 5 9 10 3"/></svg>
<svg v-else width="10" height="10" viewBox="0 0 12 12" stroke="currentColor" stroke-width="2" fill="none"><line x1="3" y1="3" x2="9" y2="9"/><line x1="9" y1="3" x2="3" y2="9"/></svg>
:class="s.ok ? 'bg-ok/15 text-ok' : 'bg-err/15 text-err'">
<Check v-if="s.ok" :size="11" :stroke-width="3" />
<X v-else :size="11" :stroke-width="3" />
</div>
<p class="text-xs" :class="s.ok ? 'text-primary' : 'text-red-400'">{{ s.message }}</p>
<p class="text-meta" :class="s.ok ? 'text-primary' : 'text-err'">{{ s.message }}</p>
</div>
</div>
<div v-if="plexManualSetup" class="bg-yellow-500/10 border border-yellow-500/20 rounded-lg p-3">
<p class="text-xs text-yellow-400 font-medium mb-2">⚠ Configuration manuelle de l'agent requise (Plex &lt; 1.43)</p>
<div v-if="plexManualSetup" class="bg-accent/5 border border-accent/30 rounded-field p-3">
<p class="text-meta text-accent font-bold mb-2">Configuration manuelle de l'agent requise (Plex &lt; 1.43)</p>
<ol class="text-xs text-muted space-y-1 list-decimal list-inside">
<li>Rendez-vous sur la page <span class="text-primary">Metadata Agent</span> de Plex</li>
<li>Dans <span class="text-primary">Metadata Provider</span>, ajoutez : <code class="bg-hover px-1 rounded">https://metadata.fankai.fr/plex</code></li>
@@ -158,7 +153,7 @@
</ol>
</div>
<button @click="emit('close')" class="btn-primary">Fermer</button>
<button @click="emit('close')" class="btn-primary self-end">Fermer</button>
</div>
</div>
@@ -168,6 +163,7 @@
<script setup lang="ts">
import { ref, onUnmounted } from 'vue'
import { Check, Clock3, User, X } from 'lucide-vue-next'
const props = defineProps<{ mediaPath: string }>()
const emit = defineEmits<{ close: [] }>()
@@ -0,0 +1,20 @@
<template>
<section class="card flex flex-col gap-3.5">
<div v-if="title || $slots.actions" class="flex items-start justify-between gap-4 flex-wrap">
<div class="flex flex-col gap-0.5 min-w-0 flex-1 basis-[280px]">
<h3 class="card-title">{{ title }}</h3>
<p v-if="description || $slots.description" class="text-meta text-muted">
<slot name="description">{{ description }}</slot>
</p>
</div>
<div v-if="$slots.actions" class="flex items-center gap-2.5 flex-wrap shrink-0">
<slot name="actions" />
</div>
</div>
<slot />
</section>
</template>
<script setup lang="ts">
defineProps<{ title?: string; description?: string }>()
</script>
+11 -11
View File
@@ -1,18 +1,18 @@
<template>
<div class="flex items-start justify-between gap-4">
<div>
<p class="text-sm text-primary">{{ label }}</p>
<p class="text-xs text-muted mt-0.5">{{ description }}</p>
<div class="flex items-center justify-between gap-5">
<div class="flex-1 min-w-0 flex flex-col gap-[3px]">
<p class="text-sm font-medium text-primary">{{ label }}</p>
<p v-if="description" class="text-meta text-muted">{{ description }}</p>
</div>
<button
type="button"
role="switch"
:aria-checked="modelValue"
:aria-label="label"
@click="$emit('update:modelValue', !modelValue)"
class="relative shrink-0 w-10 h-[22px] rounded-full transition-colors duration-200 mt-0.5"
:class="modelValue ? 'bg-accent' : 'bg-border'"
class="switch"
>
<span
class="absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200"
:class="modelValue ? 'translate-x-[18px]' : 'translate-x-0'"
/>
<span class="switch-knob" />
</button>
</div>
</template>
@@ -20,4 +20,4 @@
<script setup lang="ts">
defineProps<{ modelValue: boolean; label: string; description?: string }>()
defineEmits(['update:modelValue'])
</script>
</script>
+31
View File
@@ -0,0 +1,31 @@
<template>
<div class="grid grid-cols-3 gap-3.5" role="radiogroup" aria-label="Accent">
<button
v-for="theme in themes"
:key="theme.id"
type="button"
role="radio"
:aria-checked="current === theme.id"
@click="setTheme(theme.id)"
class="flex flex-col gap-2 text-left cursor-pointer group"
>
<span
class="h-[74px] rounded-field border-2 bg-sidebar p-2.5 flex items-end gap-2 transition-colors"
:class="current === theme.id ? '' : 'border-border-light group-hover:border-border'"
:style="current === theme.id ? { borderColor: theme.color } : undefined"
>
<span class="w-[22px] h-[22px] rounded-full shrink-0" :style="{ background: theme.color }" />
<span class="flex-1 h-2 rounded" :style="{ background: `color-mix(in srgb, ${theme.color} 35%, transparent)` }" />
</span>
<span class="text-meta" :class="current === theme.id ? 'font-bold text-primary' : 'font-medium text-secondary'">
{{ theme.label }}<template v-if="current === theme.id"> · actif</template>
</span>
</button>
</div>
</template>
<script setup lang="ts">
import { useTheme } from '@/composables/useTheme'
const { themes, current, setTheme } = useTheme()
</script>
+36 -40
View File
@@ -1,16 +1,18 @@
<template>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-3">
<div>
<label class="settings-label">Nom</label>
<input v-model="name" placeholder="Mon client torrent" class="settings-input" />
</div>
<div>
<label class="settings-label">Type</label>
<select v-model="type" @change="onTypeChange" class="settings-input">
<option value="">Choisir un type...</option>
<option v-for="def in definitions" :key="def.id" :value="def.id">{{ def.label }}</option>
</select>
<div class="flex flex-col gap-3.5">
<div class="grid sm:grid-cols-2 gap-3.5">
<div class="flex flex-col gap-[7px]">
<label for="client-name" class="field-label">Nom</label>
<input id="client-name" v-model="name" placeholder="Mon client torrent" class="field" />
</div>
<div class="flex flex-col gap-[7px]">
<label for="client-type" class="field-label">Type</label>
<select id="client-type" v-model="type" @change="onTypeChange" class="field">
<option value="">Choisir un type…</option>
<option v-for="def in definitions" :key="def.id" :value="def.id">{{ def.label }}</option>
</select>
</div>
</div>
<!-- Champs principaux -->
@@ -23,18 +25,18 @@
:description="fieldTooltip(field.key)"
@update:model-value="config[field.key] = $event"
/>
<template v-else>
<label class="settings-label">
{{ field.label }}
<span v-if="field.required" class="text-red-400 ml-0.5">*</span>
<div v-else class="flex flex-col gap-[7px]">
<label :for="`client-${field.key}`" class="field-label">
{{ field.label }}<span v-if="field.required" class="text-err ml-0.5">*</span>
</label>
<input
:id="`client-${field.key}`"
v-model="config[field.key]"
:type="field.type === 'password' ? 'password' : 'text'"
:placeholder="field.placeholder ?? ''"
class="settings-input"
class="field"
/>
</template>
</div>
</div>
<!-- Paramètres avancés -->
@@ -42,17 +44,18 @@
<button
type="button"
@click="advancedOpen = !advancedOpen"
class="flex items-center gap-1.5 text-xs text-muted hover:text-primary transition-colors w-full py-1"
class="flex items-center gap-1.5 text-meta text-secondary hover:text-primary transition-colors w-full py-1"
:aria-expanded="advancedOpen"
>
<ChevronDown
:size="13"
:size="14"
class="transition-transform duration-200 shrink-0"
:class="advancedOpen ? 'rotate-180' : ''"
/>
Paramètres avancés
</button>
<div v-if="advancedOpen" class="flex flex-col gap-3 mt-2 pl-3 border-l border-border">
<div v-if="advancedOpen" class="flex flex-col gap-3.5 mt-2.5 pl-3.5 border-l border-hover">
<div v-for="field in advancedFields" :key="field.key">
<SettingsToggle
v-if="field.type === 'boolean'"
@@ -61,42 +64,35 @@
:description="fieldTooltip(field.key)"
@update:model-value="config[field.key] = $event"
/>
<template v-else>
<label class="settings-label flex items-center gap-1.5">
{{ field.label }}
<span
class="inline-flex items-center justify-center w-4 h-4 rounded-full border border-border text-[10px] text-muted cursor-default leading-none shrink-0 relative group"
tabindex="-1"
>
i
<span class="absolute bottom-full left-1/2 -translate-x-1/2 mb-1.5 w-56 text-[11px] text-secondary bg-card border border-border rounded-lg px-2.5 py-1.5 shadow-lg pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity z-10 leading-relaxed">
{{ fieldTooltip(field.key) }}
</span>
</span>
</label>
<div v-else class="flex flex-col gap-[7px]">
<label :for="`client-${field.key}`" class="field-label">{{ field.label }}</label>
<input
:id="`client-${field.key}`"
v-model="config[field.key]"
:type="field.type === 'password' ? 'password' : 'text'"
:placeholder="field.placeholder ?? ''"
class="settings-input"
class="field"
/>
</template>
<p v-if="fieldTooltip(field.key)" class="text-xs text-muted">{{ fieldTooltip(field.key) }}</p>
</div>
</div>
</div>
</div>
</template>
</div>
<p v-if="!tested && type" class="text-xs text-muted">
<p v-if="!tested && type" class="text-meta text-muted">
Testez la connexion avant d'enregistrer.
</p>
<div class="flex gap-2 pt-1">
<div class="flex items-center gap-2.5 pt-1 flex-wrap">
<button @click="save" :disabled="saving || !tested" class="btn-primary">
{{ saving ? '...' : 'Enregistrer' }}
{{ saving ? 'Enregistrement…' : 'Enregistrer' }}
</button>
<button @click="test" :disabled="testing || !type" class="btn-secondary">
{{ testing ? '...' : tested ? 'Testé ✓' : 'Tester' }}
<Loader v-if="testing" :size="14" class="animate-spin" />
<Check v-else-if="tested" :size="14" class="text-ok" />
{{ testing ? 'Test…' : tested ? 'Connexion testée' : 'Tester' }}
</button>
<button v-if="showCancel" @click="emit('cancel')" class="btn-ghost ml-auto">Annuler</button>
</div>
@@ -105,7 +101,7 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ChevronDown } from 'lucide-vue-next'
import { Check, ChevronDown, Loader } from 'lucide-vue-next'
import SettingsToggle from '@/components/settings/SettingsToggle.vue'
import { useToast } from '@/composables/useToast'
import type { TorrentClientConfig, TorrentClientDefinition, SavedTorrentClient } from '@/types/torrent-client'