diff --git a/src/views/DownloadsView.vue b/src/views/DownloadsView.vue
index dd526d9..ff0288b 100644
--- a/src/views/DownloadsView.vue
+++ b/src/views/DownloadsView.vue
@@ -133,7 +133,6 @@
-
-
@@ -225,29 +223,24 @@
-
↓ {{ formatSpeed(t.speed) }}
ETA {{ formatEta(t.eta) }}
-
Complété
-
R {{ t.ratio?.toFixed(2) ?? '0.00' }}
-
- ↑ {{ formatSize(t.uploaded ?? 0) }}
-
-
- {{ formatSpeed(t.upspeed) }}
-
+ ↑ {{ formatSize(t.uploaded ?? 0) }}
+ {{ formatSpeed(t.upspeed) }}
État non remonté par le client
+
@@ -267,11 +260,11 @@
- {{ t.organizeState === 'done' ? `Importé (${t.organizeProgress?.done}/${t.organizeProgress?.total})` :
+ {{ t.organizeState === 'done' ? `Importé (${t.organizeProgress?.done}/${t.organizeProgress?.total})` :
t.organizeState === 'partial' ? `En cours (${t.organizeProgress?.done}/${t.organizeProgress?.total})` :
'Non importé' }}
@@ -286,18 +279,20 @@
-
+
-
+
-
@@ -337,24 +339,27 @@ import { useToast } from '@/composables/useToast'
const { add: toast } = useToast()
-const torrents = ref
([])
-const loading = ref(true)
-const polling = ref(true)
-const noClients = ref(false)
-const importing = ref>({})
-const importingAll = ref(false)
-const activeTab = ref<'active' | 'done'>('active')
-const hideImported = ref(false)
-const seedingOnly = ref(false)
-const search = ref('')
-const sortOpen = ref(false)
-const colOpen = ref(false)
-const activeSort = ref<'none' | 'name' | 'size' | 'progress' | 'state' | 'ratio'>('none')
-const sortDir = ref<'asc' | 'desc'>('asc')
-const sortRef = ref(null)
-const colRef = ref(null)
+// ─── State ────────────────────────────────────────────────────
+const torrents = ref([])
+const loading = ref(true)
+const polling = ref(true)
+const noClients = ref(false)
+const importing = ref>({})
+const importingAll = ref(false)
+const confirmDelete = ref(null)
+const deleting = ref>({})
+const deleteWithFiles = ref>({})
+const activeTab = ref<'active' | 'done'>('active')
+const hideImported = ref(false)
+const seedingOnly = ref(false)
+const search = ref('')
+const sortOpen = ref(false)
+const colOpen = ref(false)
+const activeSort = ref<'none' | 'name' | 'size' | 'progress' | 'state' | 'ratio'>('none')
+const sortDir = ref<'asc' | 'desc'>('asc')
+const sortRef = ref(null)
+const colRef = ref(null)
-// Colonnes visibles
const columns = ref({
client : true,
size : true,
@@ -371,29 +376,20 @@ const columnOptions = [
{ key: 'upspeed', label: 'Vitesse upload' },
]
+const sortOptions = [
+ { label: 'Nom', value: 'name' },
+ { label: 'Taille', value: 'size' },
+ { label: 'Progression', value: 'progress' },
+ { label: 'État', value: 'state' },
+ { label: 'Ratio', value: 'ratio' },
+]
+
onClickOutside(sortRef, () => { sortOpen.value = false })
onClickOutside(colRef, () => { colOpen.value = false })
-const sortOptions = [
- { label: 'Nom', value: 'name' },
- { label: 'Taille', value: 'size' },
- { label: 'Progression', value: 'progress' },
- { label: 'État', value: 'state' },
- { label: 'Ratio', value: 'ratio' },
-]
-
-function setSort(val: typeof activeSort.value) {
- if (activeSort.value === val) {
- sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc'
- } else {
- activeSort.value = val
- sortDir.value = 'asc'
- }
- sortOpen.value = false
-}
-
let pollInterval: ReturnType | null = null
+// ─── Computed ─────────────────────────────────────────────────
const activeTorrents = computed(() =>
torrents.value.filter(t => ['downloading', 'paused', 'checking', 'error'].includes(t.state))
)
@@ -403,9 +399,7 @@ const doneTorrents = computed(() =>
)
const visibleTorrents = computed(() => {
- let list = activeTab.value === 'active'
- ? activeTorrents.value
- : doneTorrents.value
+ let list = activeTab.value === 'active' ? activeTorrents.value : doneTorrents.value
if (activeTab.value === 'done') {
if (seedingOnly.value) list = list.filter(t => t.state === 'seeding')
@@ -441,113 +435,21 @@ const stats = computed(() => [
{ label: 'Erreurs', value: torrents.value.filter(t => t.state === 'error').length },
])
+// ─── Helpers ──────────────────────────────────────────────────
+function setSort(val: typeof activeSort.value) {
+ if (activeSort.value === val) {
+ sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc'
+ } else {
+ activeSort.value = val
+ sortDir.value = 'asc'
+ }
+ sortOpen.value = false
+}
+
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' })
- if (res.status === 503) { noClients.value = true; return }
- if (!res.ok) return
- torrents.value = await res.json()
- noClients.value = false
- } catch {
- } finally {
- loading.value = false
- }
-}
-
-function startPolling() {
- fetchTorrents()
- pollInterval = setInterval(fetchTorrents, 60_000)
-}
-
-function stopPolling() {
- if (pollInterval) { clearInterval(pollInterval); pollInterval = null }
-}
-
-function togglePolling() {
- polling.value = !polling.value
- polling.value ? startPolling() : stopPolling()
-}
-
-onMounted(startPolling)
-onUnmounted(stopPolling)
-
-const confirmDelete = ref(null)
-const deleting = ref>({})
-const deleteWithFiles = ref>({})
-
-async function deleteTorrent(torrent: any) {
- deleting.value[torrent.hash] = true
- try {
- const withFiles = deleteWithFiles.value[torrent.hash] ?? false
- const res = await fetch(`/api/torrent/${torrent.hash}?deleteFiles=${withFiles}`, {
- method : 'DELETE',
- credentials: 'include',
- })
- if (res.ok) {
- toast(`"${torrent.name}" supprimé ✓`, 'success')
- confirmDelete.value = null
- await fetchTorrents()
- } else {
- toast('Erreur lors de la suppression', 'error')
- }
- } catch {
- toast('Impossible de contacter le serveur', 'error')
- } finally {
- deleting.value[torrent.hash] = false
- }
-}
-importing.value[torrent.hash] = true
-try {
- const res = await fetch('/api/organize', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify({ hash: torrent.hash, save_path: torrent.save_path, name: torrent.name }),
- })
- if (res.ok) {
- toast(`${torrent.name} importé ✓`, 'success')
- await fetchTorrents()
- } else {
- const { error } = await res.json()
- toast(error ?? "Erreur lors de l'import", 'error')
- }
-} catch {
- toast('Impossible de contacter le serveur', 'error')
-} finally {
- importing.value[torrent.hash] = false
-}
-}
-
-async function importAll() {
- const toImport = doneTorrents.value.filter(t => t.organizeState !== 'done')
- if (toImport.length === 0) { toast('Tous les torrents sont déjà importés', 'success'); return }
-
- importingAll.value = true
- let done = 0, errors = 0
-
- for (const t of toImport) {
- try {
- const res = await fetch('/api/organize', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify({ hash: t.hash, save_path: t.save_path, name: t.name }),
- })
- res.ok ? done++ : errors++
- } catch { errors++ }
- }
-
- importingAll.value = false
- await fetchTorrents()
- errors === 0
- ? toast(`${done} torrent(s) importé(s) ✓`, 'success')
- : toast(`${done} OK, ${errors} erreur(s)`, 'error')
-}
-
function stateBadge(state: string) {
const map: Record = {
downloading: { label: 'Téléchargement', class: 'border-accent/40 text-accent' },
@@ -578,4 +480,106 @@ function formatEta(seconds: number): string {
if (m > 0) return `${m}m${s.toString().padStart(2, '0')}s`
return `${s}s`
}
+
+// ─── Actions ──────────────────────────────────────────────────
+async function fetchTorrents() {
+ try {
+ const res = await fetch('/api/downloads', { credentials: 'include' })
+ if (res.status === 503) { noClients.value = true; return }
+ if (!res.ok) return
+ torrents.value = await res.json()
+ noClients.value = false
+ } catch {
+ } finally {
+ loading.value = false
+ }
+}
+
+function startPolling() {
+ fetchTorrents()
+ pollInterval = setInterval(fetchTorrents, 60_000)
+}
+
+function stopPolling() {
+ if (pollInterval) { clearInterval(pollInterval); pollInterval = null }
+}
+
+function togglePolling() {
+ polling.value = !polling.value
+ polling.value ? startPolling() : stopPolling()
+}
+
+async function importTorrent(torrent: any) {
+ importing.value[torrent.hash] = true
+ try {
+ const res = await fetch('/api/organize', {
+ method : 'POST',
+ headers : { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body : JSON.stringify({ hash: torrent.hash, save_path: torrent.save_path, name: torrent.name }),
+ })
+ if (res.ok) {
+ toast(`${torrent.name} importé ✓`, 'success')
+ await fetchTorrents()
+ } else {
+ const { error } = await res.json()
+ toast(error ?? "Erreur lors de l'import", 'error')
+ }
+ } catch {
+ toast('Impossible de contacter le serveur', 'error')
+ } finally {
+ importing.value[torrent.hash] = false
+ }
+}
+
+async function importAll() {
+ const toImport = doneTorrents.value.filter(t => t.organizeState !== 'done')
+ if (toImport.length === 0) { toast('Tous les torrents sont déjà importés', 'success'); return }
+
+ importingAll.value = true
+ let done = 0, errors = 0
+
+ for (const t of toImport) {
+ try {
+ const res = await fetch('/api/organize', {
+ method : 'POST',
+ headers : { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body : JSON.stringify({ hash: t.hash, save_path: t.save_path, name: t.name }),
+ })
+ res.ok ? done++ : errors++
+ } catch { errors++ }
+ }
+
+ importingAll.value = false
+ await fetchTorrents()
+ errors === 0
+ ? toast(`${done} torrent(s) importé(s) ✓`, 'success')
+ : toast(`${done} OK, ${errors} erreur(s)`, 'error')
+}
+
+async function deleteTorrent(torrent: any) {
+ deleting.value[torrent.hash] = true
+ try {
+ const withFiles = deleteWithFiles.value[torrent.hash] ?? false
+ const res = await fetch(`/api/torrent/${torrent.hash}?deleteFiles=${withFiles}`, {
+ method : 'DELETE',
+ credentials: 'include',
+ })
+ if (res.ok) {
+ toast(`"${torrent.name}" supprimé ✓`, 'success')
+ confirmDelete.value = null
+ await fetchTorrents()
+ } else {
+ toast('Erreur lors de la suppression', 'error')
+ }
+ } catch {
+ toast('Impossible de contacter le serveur', 'error')
+ } finally {
+ deleting.value[torrent.hash] = false
+ }
+}
+
+onMounted(startPolling)
+onUnmounted(stopPolling)
\ No newline at end of file