fix: bouton global de serie
This commit is contained in:
1 parent
e6ca1d2a36
commit
ed66c2c0a2
3 files changed
+73
-97
No files matched your search
@@ -78,99 +78,27 @@
|
|||||||
{{ serie.plot }}
|
{{ serie.plot }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Boutons intégrale / tout télécharger -->
|
|
||||||
<div class="flex flex-wrap gap-2 mt-1">
|
|
||||||
<button
|
|
||||||
v-if="hasDownloadableTorrents"
|
|
||||||
@click="emit('downloadAll')"
|
|
||||||
:disabled="downloadingAll"
|
|
||||||
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg bg-accent text-white hover:bg-accent-hover transition"
|
|
||||||
:class="downloadingAll ? 'opacity-50 cursor-not-allowed' : ''"
|
|
||||||
>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" fill="none" :class="downloadingAll ? 'animate-spin' : ''">
|
|
||||||
<path v-if="!downloadingAll" d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline v-if="!downloadingAll" points="7 10 12 15 17 10"/><line v-if="!downloadingAll" x1="12" y1="15" x2="12" y2="3"/>
|
|
||||||
<path v-else d="M21 12a9 9 0 0 1-9 9 9 9 0 0 1-6.36-2.64M3 12a9 9 0 0 1 9-9 9 9 0 0 1 6.36 2.64"/>
|
|
||||||
</svg>
|
|
||||||
{{ downloadingAll ? 'Envoi…' : 'Tout télécharger' }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
v-for="(t, i) in torrentsIntegrale"
|
|
||||||
:key="i"
|
|
||||||
:title="t.raw"
|
|
||||||
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition"
|
|
||||||
:class="[
|
|
||||||
t.label === 'Intégrale'
|
|
||||||
? 'bg-accent text-white hover:bg-accent-hover'
|
|
||||||
: 'bg-accent-muted text-accent border border-accent/20 hover:bg-accent/20',
|
|
||||||
(isAlreadyQueued(t) || isDownloaded(`integrale-${i}`))
|
|
||||||
? 'opacity-50 cursor-not-allowed' : ''
|
|
||||||
]"
|
|
||||||
:disabled="isAlreadyQueued(t) || isDownloaded(`integrale-${i}`)"
|
|
||||||
@click="emit('download', `integrale-${i}`, t.torrent_url, t.magnet)"
|
|
||||||
>
|
|
||||||
<component :is="btnIcon(`integrale-${i}`, t)" />
|
|
||||||
{{ isAlreadyQueued(t) ? 'Déjà ajouté' : isDownloaded(`integrale-${i}`) ? 'Envoyé ✓' : t.label }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Progression intégrale -->
|
|
||||||
<template v-for="(t, i) in torrentsIntegrale" :key="`prog-${i}`">
|
|
||||||
<div v-if="torrentProgress(extractHash(t)) && torrentProgress(extractHash(t))!.progress < 100" class="h-0.5 bg-border rounded-full overflow-hidden">
|
|
||||||
<div class="h-full bg-accent rounded-full transition-all duration-500" :style="{ width: `${torrentProgress(extractHash(t))!.progress}%` }" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, h } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink } from 'vue-router'
|
||||||
import { ArrowLeft, Tv, Download, Loader, Check } from 'lucide-vue-next'
|
import { ArrowLeft, Tv } from 'lucide-vue-next'
|
||||||
|
|
||||||
interface ActiveTorrent { hash: string; progress: number; state: string }
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
serie : any
|
serie : any
|
||||||
torrentsIntegrale : any[]
|
organizedByEpisode : Record<string, any>
|
||||||
organizedByEpisode : Record<string, any>
|
|
||||||
hasDownloadableTorrents: boolean
|
|
||||||
downloadingAll : boolean
|
|
||||||
activeTorrents : ActiveTorrent[]
|
|
||||||
downloading : string[]
|
|
||||||
downloaded : string[]
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
openManualImport: []
|
openManualImport: []
|
||||||
unimport : [deleteFile: boolean]
|
unimport : [deleteFile: boolean]
|
||||||
downloadAll : []
|
|
||||||
download : [key: string, url: string | null, magnet: string | null, file_index?: number | null, file_path?: string | null]
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const unimportMenuOpen = ref(false)
|
const unimportMenuOpen = ref(false)
|
||||||
|
|
||||||
function isDownloaded(key: string) { return props.downloaded.includes(key) }
|
|
||||||
function isAlreadyQueued(torrent: any): boolean {
|
|
||||||
const hash = extractHash(torrent)
|
|
||||||
if (!hash) return false
|
|
||||||
return props.activeTorrents.some(t => t.hash.toLowerCase() === hash.toLowerCase())
|
|
||||||
}
|
|
||||||
function extractHash(torrent: any): string | null {
|
|
||||||
if (!torrent?.magnet) return null
|
|
||||||
const m = torrent.magnet.match(/xt=urn:btih:([a-fA-F0-9]{40})/i)
|
|
||||||
return m ? m[1].toLowerCase() : null
|
|
||||||
}
|
|
||||||
function torrentProgress(hash: string | null | undefined): ActiveTorrent | null {
|
|
||||||
if (!hash) return null
|
|
||||||
return props.activeTorrents.find(t => t.hash.toLowerCase() === hash.toLowerCase()) ?? null
|
|
||||||
}
|
|
||||||
function btnIcon(key: string, torrent: any) {
|
|
||||||
if (isDownloaded(key) || isAlreadyQueued(torrent)) return h(Check, { size: 14 })
|
|
||||||
return h(Download, { size: 14 })
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({ closeUnimportMenu: () => { unimportMenuOpen.value = false } })
|
defineExpose({ closeUnimportMenu: () => { unimportMenuOpen.value = false } })
|
||||||
</script>
|
</script>
|
||||||
@@ -232,10 +232,12 @@ const seasonBtnIcon = computed(() => {
|
|||||||
|
|
||||||
// ── État épisode ───────────────────────────────────────────────
|
// ── État épisode ───────────────────────────────────────────────
|
||||||
function epProgress(ep: any): ActiveTorrent | null {
|
function epProgress(ep: any): ActiveTorrent | null {
|
||||||
// Pour les épisodes d'un pack (file_index défini), le hash du pack est partagé par tous les
|
if (ep.torrent?.file_index != null) {
|
||||||
// épisodes : consulter la progression du pack donnerait le même état à tous.
|
// Épisode dans un pack : le hash est partagé par tous les épisodes du pack.
|
||||||
// On retourne null pour ignorer la progression globale du pack.
|
// On affiche la progression du pack UNIQUEMENT pour cet épisode s'il a été
|
||||||
if (ep.torrent?.file_index != null) return null
|
// explicitement téléchargé dans cette session (isDownloaded).
|
||||||
|
return isDownloaded(`ep-${ep.id}`) ? torrentProgress(extractHash(ep.torrent)) : null
|
||||||
|
}
|
||||||
return torrentProgress(extractHash(ep.torrent))
|
return torrentProgress(extractHash(ep.torrent))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+63
-17
@@ -17,21 +17,60 @@
|
|||||||
<!-- Hero -->
|
<!-- Hero -->
|
||||||
<SerieHero
|
<SerieHero
|
||||||
:serie="data.serie"
|
:serie="data.serie"
|
||||||
:torrents-integrale="data.torrents_integrale"
|
|
||||||
:organized-by-episode="organizedByEpisode"
|
:organized-by-episode="organizedByEpisode"
|
||||||
:has-downloadable-torrents="hasNonIntegraleDownloadables"
|
|
||||||
:downloading-all="downloadingAll"
|
|
||||||
:active-torrents="activeTorrents"
|
|
||||||
:downloading="downloading"
|
|
||||||
:downloaded="downloaded"
|
|
||||||
@open-manual-import="openManualImport"
|
@open-manual-import="openManualImport"
|
||||||
@unimport="(del) => unimportSerie(del)"
|
@unimport="(del) => unimportSerie(del)"
|
||||||
@download-all="downloadAll"
|
|
||||||
@download="(key, url, magnet, fi, fp) => download(key, url, magnet, fi, fp)"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Zone d'actions téléchargement -->
|
||||||
|
<div
|
||||||
|
v-if="data.torrents_integrale.length > 0 || hasNonIntegraleDownloadables"
|
||||||
|
class="px-4 md:px-8 py-4 flex items-center justify-between gap-4 border-b border-border"
|
||||||
|
>
|
||||||
|
<h2 class="text-sm font-semibold text-primary shrink-0">Saisons</h2>
|
||||||
|
<div class="flex flex-wrap gap-2 justify-end">
|
||||||
|
<!-- Tout télécharger (saisons + épisodes individuels) -->
|
||||||
|
<button
|
||||||
|
v-if="hasNonIntegraleDownloadables"
|
||||||
|
@click="downloadAll"
|
||||||
|
:disabled="downloadingAll"
|
||||||
|
class="inline-flex items-center gap-2 px-3 py-1.5 text-xs font-medium rounded-lg border border-border text-muted hover:text-primary hover:border-secondary transition"
|
||||||
|
:class="downloadingAll ? 'opacity-50 cursor-not-allowed' : ''"
|
||||||
|
>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" fill="none" :class="downloadingAll ? 'animate-spin' : ''">
|
||||||
|
<path v-if="!downloadingAll" d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline v-if="!downloadingAll" points="7 10 12 15 17 10"/><line v-if="!downloadingAll" x1="12" y1="15" x2="12" y2="3"/>
|
||||||
|
<path v-else d="M21 12a9 9 0 0 1-9 9 9 9 0 0 1-6.36-2.64M3 12a9 9 0 0 1 9-9 9 9 0 0 1 6.36 2.64"/>
|
||||||
|
</svg>
|
||||||
|
{{ downloadingAll ? 'Envoi…' : 'Tout télécharger' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Boutons intégrale -->
|
||||||
|
<template v-for="(t, i) in data.torrents_integrale" :key="i">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<button
|
||||||
|
:title="t.raw"
|
||||||
|
class="inline-flex items-center gap-2 px-3 py-1.5 text-xs font-medium rounded-lg transition"
|
||||||
|
:class="[
|
||||||
|
'bg-accent text-white hover:bg-accent-hover',
|
||||||
|
(isAlreadyQueued(t) || isDownloaded(`integrale-${i}`)) ? 'opacity-50 cursor-not-allowed' : ''
|
||||||
|
]"
|
||||||
|
:disabled="isAlreadyQueued(t) || isDownloaded(`integrale-${i}`)"
|
||||||
|
@click="download(`integrale-${i}`, t.torrent_url, t.magnet)"
|
||||||
|
>
|
||||||
|
<component :is="integraleIcon(`integrale-${i}`, t)" />
|
||||||
|
{{ isAlreadyQueued(t) ? 'Déjà ajouté' : isDownloaded(`integrale-${i}`) ? 'Envoyé ✓' : t.label }}
|
||||||
|
</button>
|
||||||
|
<!-- Progression -->
|
||||||
|
<div v-if="isDownloaded(`integrale-${i}`) && integraleProgress(t) && integraleProgress(t)!.progress < 100" class="h-0.5 bg-border rounded-full overflow-hidden">
|
||||||
|
<div class="h-full bg-accent rounded-full transition-all duration-500" :style="{ width: `${integraleProgress(t)!.progress}%` }" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Saisons -->
|
<!-- Saisons -->
|
||||||
<div class="px-4 md:px-8 pb-16 flex flex-col gap-4">
|
<div class="px-4 md:px-8 pb-16 flex flex-col gap-4 pt-4">
|
||||||
<SerieSeasonCard
|
<SerieSeasonCard
|
||||||
v-for="season in data.seasons"
|
v-for="season in data.seasons"
|
||||||
:key="season.id"
|
:key="season.id"
|
||||||
@@ -69,10 +108,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, h } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useSeriesStore } from '@/stores/series'
|
import { useSeriesStore } from '@/stores/series'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
|
import { Download, Check } from 'lucide-vue-next'
|
||||||
import ManualImportModal from '@/components/ManualImportModal.vue'
|
import ManualImportModal from '@/components/ManualImportModal.vue'
|
||||||
import SerieHero from '@/components/serie/SerieHero.vue'
|
import SerieHero from '@/components/serie/SerieHero.vue'
|
||||||
import SerieSeasonCard from '@/components/serie/SerieSeasonCard.vue'
|
import SerieSeasonCard from '@/components/serie/SerieSeasonCard.vue'
|
||||||
@@ -90,7 +130,6 @@ const organizedByEpisode = ref<Record<string, any>>({})
|
|||||||
const mediaPath = ref('/')
|
const mediaPath = ref('/')
|
||||||
const nfoSupport = ref(false)
|
const nfoSupport = ref(false)
|
||||||
const epActionLoading = ref<Record<number, boolean>>({})
|
const epActionLoading = ref<Record<number, boolean>>({})
|
||||||
const unimportMenuOpen = ref(false)
|
|
||||||
const downloadingAll = ref(false)
|
const downloadingAll = ref(false)
|
||||||
const downloadingSeason = ref<Record<number, boolean>>({})
|
const downloadingSeason = ref<Record<number, boolean>>({})
|
||||||
|
|
||||||
@@ -100,6 +139,17 @@ let pollTimer: ReturnType<typeof setInterval> | null = null
|
|||||||
|
|
||||||
const data = computed(() => store.currentSerie)
|
const data = computed(() => store.currentSerie)
|
||||||
|
|
||||||
|
// ── Helpers intégrale ─────────────────────────────────────────
|
||||||
|
function integraleIcon(key: string, torrent: any) {
|
||||||
|
if (isDownloaded(key) || isAlreadyQueued(torrent)) return h(Check, { size: 13 })
|
||||||
|
return h(Download, { size: 13 })
|
||||||
|
}
|
||||||
|
function integraleProgress(torrent: any) {
|
||||||
|
const hash = extractHash(torrent)
|
||||||
|
if (!hash) return null
|
||||||
|
return activeTorrents.value.find(t => t.hash.toLowerCase() === hash.toLowerCase()) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
// ── Helpers d'état ────────────────────────────────────────────
|
// ── Helpers d'état ────────────────────────────────────────────
|
||||||
function isDownloading(key: string) { return downloading.value.includes(key) }
|
function isDownloading(key: string) { return downloading.value.includes(key) }
|
||||||
function isDownloaded(key: string) { return downloaded.value.includes(key) }
|
function isDownloaded(key: string) { return downloaded.value.includes(key) }
|
||||||
@@ -182,10 +232,8 @@ function collectDownloadables() {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasDownloadableTorrents = computed(() => collectDownloadables().length > 0)
|
// S'affiche uniquement s'il y a des torrents saison/épisode à lancer
|
||||||
|
// (les intégrales ont leurs propres boutons dédiés dans la zone d'actions)
|
||||||
// "Tout télécharger" ne s'affiche que s'il y a des torrents saison/épisode à lancer
|
|
||||||
// (les intégrales ont déjà leurs propres boutons dédiés dans le hero)
|
|
||||||
const hasNonIntegraleDownloadables = computed(() => {
|
const hasNonIntegraleDownloadables = computed(() => {
|
||||||
if (!data.value) return false
|
if (!data.value) return false
|
||||||
const covered = new Set<number>()
|
const covered = new Set<number>()
|
||||||
@@ -280,10 +328,8 @@ async function unimportEpisode(ep: any, _season: any, deleteFile: boolean) {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
load(); fetchSettings(); fetchOrganized(); fetchActiveDownloads()
|
load(); fetchSettings(); fetchOrganized(); fetchActiveDownloads()
|
||||||
pollTimer = setInterval(fetchActiveDownloads, 5000)
|
pollTimer = setInterval(fetchActiveDownloads, 5000)
|
||||||
document.addEventListener('click', () => { unimportMenuOpen.value = false })
|
|
||||||
})
|
})
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (pollTimer) clearInterval(pollTimer)
|
if (pollTimer) clearInterval(pollTimer)
|
||||||
document.removeEventListener('click', () => { unimportMenuOpen.value = false })
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
Reference in new issue
Block a user