feat(ui): refonte de l'interface
This commit is contained in:
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 }
|
||||
|
||||
|
||||
@@ -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 < 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 < 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
|
||||
Reference in new issue
Block a user