fix: le driver de Transmission n'est plus basé sur android
fix: les torrent inconnue en états seront mis dans la catégorie terminée feat: ajout d'un retour visuel si on a un path par defaut sur / fix: utilisation de nfo_filename au lieu de original_filename
This commit is contained in:
1 parent
bc960ba585
commit
f3a36af629
4 files changed
+91
-45
No files matched your search
@@ -159,7 +159,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right shrink-0">
|
||||
<p class="text-sm font-semibold text-primary">{{ t.progress }}%</p>
|
||||
<p class="text-sm font-semibold text-primary">{{ clampedProgress(t) }}%</p>
|
||||
<p class="text-xs text-muted mt-0.5">{{ formatSize(t.downloaded) }} / {{ formatSize(t.size) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -168,10 +168,10 @@
|
||||
<div class="h-1 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full rounded-full transition-all duration-500"
|
||||
:class="t.state === 'seeding' ? 'bg-green-500' :
|
||||
:class="t.state === 'seeding' || t.state === 'unknown' ? 'bg-green-500' :
|
||||
t.state === 'error' ? 'bg-red-500' :
|
||||
t.state === 'paused' ? 'bg-muted' : 'bg-accent'"
|
||||
:style="{ width: `${t.progress}%` }"
|
||||
:style="{ width: `${clampedProgress(t)}%` }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -181,9 +181,10 @@
|
||||
<span v-if="t.state === 'downloading'">↓ {{ formatSpeed(t.speed) }}</span>
|
||||
<span v-if="t.state === 'downloading' && t.eta > 0">ETA {{ formatEta(t.eta) }}</span>
|
||||
<span v-if="t.state === 'seeding'" class="text-green-500">Complété</span>
|
||||
<span v-if="t.state === 'unknown'" class="text-yellow-500">État non remonté par le client</span>
|
||||
</div>
|
||||
|
||||
<div v-if="t.state === 'seeding'" class="flex items-center gap-2">
|
||||
<div v-if="t.state === 'seeding' || t.state === 'unknown'" class="flex items-center gap-2">
|
||||
|
||||
<!-- Erreurs fichiers -->
|
||||
<div v-if="t.errorFiles?.length > 0" class="relative group/err">
|
||||
@@ -275,12 +276,13 @@ function setSort(val: typeof activeSort.value) {
|
||||
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
// FIX : unknown retiré de activeTorrents, ajouté dans doneTorrents
|
||||
const activeTorrents = computed(() =>
|
||||
torrents.value.filter(t => ['downloading', 'paused', 'checking', 'error', 'unknown'].includes(t.state))
|
||||
torrents.value.filter(t => ['downloading', 'paused', 'checking', 'error'].includes(t.state))
|
||||
)
|
||||
|
||||
const doneTorrents = computed(() =>
|
||||
torrents.value.filter(t => t.state === 'seeding')
|
||||
torrents.value.filter(t => t.state === 'seeding' || t.state === 'unknown')
|
||||
)
|
||||
|
||||
const visibleTorrents = computed(() => {
|
||||
@@ -320,6 +322,11 @@ const stats = computed(() => [
|
||||
{ label: 'Erreurs', value: torrents.value.filter(t => t.state === 'error').length },
|
||||
])
|
||||
|
||||
// FIX : progress cappé à 100 pour éviter "278.2 GB / 278.1 GB" → 100%
|
||||
function clampedProgress(t: any): number {
|
||||
return Math.min(100, Math.max(0, t.progress ?? 0))
|
||||
}
|
||||
|
||||
async function fetchTorrents() {
|
||||
try {
|
||||
const res = await fetch('/api/downloads', { credentials: 'include' })
|
||||
@@ -406,7 +413,8 @@ function stateBadge(state: string) {
|
||||
paused : { label: 'En pause', class: 'border-border text-muted' },
|
||||
checking : { label: 'Vérification', class: 'border-yellow-500/40 text-yellow-500' },
|
||||
error : { label: 'Erreur', class: 'border-red-500/40 text-red-400' },
|
||||
unknown : { label: 'Inconnu', class: 'border-border text-muted' },
|
||||
// FIX : badge jaune pour signaler état dégradé sans alarmer
|
||||
unknown : { label: 'État inconnu', class: 'border-yellow-500/40 text-yellow-500' },
|
||||
}
|
||||
return map[state] ?? map.unknown
|
||||
}
|
||||
|
||||
@@ -15,10 +15,13 @@
|
||||
<span v-if="isDocker" class="text-[10px] text-accent border border-accent/40 px-1.5 py-0.5 rounded">Docker</span>
|
||||
</div>
|
||||
<button @click="openPicker('completePath')" class="settings-input text-left font-mono truncate w-full"
|
||||
:class="form.completePath ? 'text-primary' : 'text-muted'">
|
||||
:class="isRootPath(form.completePath) ? 'text-yellow-500 border-yellow-500/40' : form.completePath ? 'text-primary' : 'text-muted'">
|
||||
{{ form.completePath || '/' }}
|
||||
</button>
|
||||
<p class="text-xs text-muted mt-1.5">Dossier où sont déposés les fichiers téléchargés</p>
|
||||
<p v-if="isRootPath(form.completePath)" class="text-xs text-yellow-500 mt-1.5 flex items-center gap-1">
|
||||
<span>⚠</span> Chemin non configuré — cliquez pour sélectionner un dossier
|
||||
</p>
|
||||
<p v-else class="text-xs text-muted mt-1.5">Dossier où sont déposés les fichiers téléchargés</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -27,14 +30,29 @@
|
||||
<span v-if="isDocker" class="text-[10px] text-accent border border-accent/40 px-1.5 py-0.5 rounded">Docker</span>
|
||||
</div>
|
||||
<button @click="openPicker('mediaPath')" class="settings-input text-left font-mono truncate w-full"
|
||||
:class="form.mediaPath ? 'text-primary' : 'text-muted'">
|
||||
:class="isRootPath(form.mediaPath) ? 'text-yellow-500 border-yellow-500/40' : form.mediaPath ? 'text-primary' : 'text-muted'">
|
||||
{{ form.mediaPath || '/' }}
|
||||
</button>
|
||||
<p class="text-xs text-muted mt-1.5">Dossier racine de votre médiathèque (Jellyfin, Kodi, Plex…)</p>
|
||||
<p v-if="isRootPath(form.mediaPath)" class="text-xs text-yellow-500 mt-1.5 flex items-center gap-1">
|
||||
<span>⚠</span> Chemin non configuré — cliquez pour sélectionner un dossier
|
||||
</p>
|
||||
<p v-else class="text-xs text-muted mt-1.5">Dossier racine de votre médiathèque (Jellyfin, Kodi, Plex…)</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Alerte globale si un des deux paths est sur / -->
|
||||
<div v-if="hasUnconfiguredPaths" class="flex items-start gap-3 px-4 py-3 rounded-lg border border-yellow-500/40 bg-yellow-500/5">
|
||||
<span class="text-yellow-500 text-sm mt-0.5">⚠</span>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<p class="text-sm text-yellow-500 font-medium">Chemins non configurés</p>
|
||||
<p class="text-xs text-muted">
|
||||
L'import automatique est désactivé tant que les dossiers ne sont pas configurés.
|
||||
Configurez-les ci-dessus avant de sauvegarder.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode import -->
|
||||
<div class="settings-card flex flex-col gap-2">
|
||||
<label class="settings-label">Mode d'import</label>
|
||||
@@ -109,7 +127,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import FolderPicker from '@/components/FolderPicker.vue'
|
||||
import SettingsToggle from '@/components/settings/SettingsToggle.vue'
|
||||
@@ -135,6 +153,15 @@ const picker = ref<{ open: boolean; field: 'completePath' | 'mediaPath'; current
|
||||
currentPath: '/',
|
||||
})
|
||||
|
||||
// FIX : détection path non configuré (vide ou racine /)
|
||||
function isRootPath(p: string): boolean {
|
||||
return !p || p === '/'
|
||||
}
|
||||
|
||||
const hasUnconfiguredPaths = computed(() =>
|
||||
isRootPath(form.value.completePath) || isRootPath(form.value.mediaPath)
|
||||
)
|
||||
|
||||
function openPicker(field: 'completePath' | 'mediaPath') {
|
||||
picker.value = { open: true, field, currentPath: form.value[field] || picker.value.currentPath || '/' }
|
||||
}
|
||||
@@ -153,7 +180,6 @@ onMounted(async () => {
|
||||
if (settingsRes.ok) Object.assign(form.value, await settingsRes.json())
|
||||
if (systemRes.ok) isDocker.value = (await systemRes.json()).isDocker
|
||||
if (infoRes.ok) picker.value.currentPath = (await infoRes.json()).defaultPath ?? '/'
|
||||
|
||||
})
|
||||
|
||||
async function save() {
|
||||
|
||||
Reference in new issue
Block a user