Nouvelle version réécrite de zéro : recherche multi-sources (Vostfree, French-Manga), extraction 2 niveaux, proxy vidéo intégré, streaming/téléchargement HLS, métadonnées Kitsu, bibliothèque locale, comptes JWT + administration, découverte fusionnée, indexeur Torznab (Sonarr/Prowlarr).
63 lines
2.3 KiB
HTML
63 lines
2.3 KiB
HTML
{% extends "base.html" %}
|
|
{% set active = 'favorites' %}
|
|
{% block title %}Favoris — Ohm Stream{% endblock %}
|
|
|
|
{% block content %}
|
|
<h1 class="page-title">Favoris</h1>
|
|
<p class="page-sub" x-data x-text="$store.fav?.total != null ? $store.fav.total + ' titre(s) suivi(s)' : ''"></p>
|
|
|
|
<div x-data="favoritesPage()" x-init="load()">
|
|
<div class="spinner" x-show="loading"></div>
|
|
|
|
<template x-if="!loading && items.length === 0">
|
|
<div class="empty-state"><div class="big">⭐</div>Aucun favori — ajoute-en depuis une fiche de titre.</div>
|
|
</template>
|
|
|
|
<div class="grid">
|
|
<template x-for="f in items" :key="f.id">
|
|
<div class="card">
|
|
<a :href="`/title/${f.source}/${encodeURIComponent(f.source_id)}`">
|
|
<img class="card-poster" :src="f.image_url || '/static/img/placeholder.svg'" loading="lazy"
|
|
onerror="this.src='/static/img/placeholder.svg'">
|
|
</a>
|
|
<span class="card-chip" x-text="f.source"></span>
|
|
<button class="btn btn-sm btn-danger" style="position:absolute;top:0.5rem;right:0.5rem"
|
|
@click.prevent="remove(f)">✖</button>
|
|
<div class="card-overlay">
|
|
<div class="card-title" x-text="f.title"></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div style="display:flex;gap:0.6rem;justify-content:center;margin-top:1.6rem" x-show="total > limit">
|
|
<button class="btn btn-ghost btn-sm" :disabled="offset === 0" @click="page(-1)">← Précédent</button>
|
|
<button class="btn btn-ghost btn-sm" :disabled="offset + limit >= total" @click="page(1)">Suivant →</button>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
function favoritesPage() {
|
|
return {
|
|
items: [], total: 0, offset: 0, limit: 24, loading: true,
|
|
async load() {
|
|
this.loading = true;
|
|
const res = await fetch(`/api/favorites?offset=${this.offset}&limit=${this.limit}`);
|
|
const data = await res.json();
|
|
this.items = data.items; this.total = data.total;
|
|
Alpine.store('fav', { total: data.total });
|
|
this.loading = false;
|
|
},
|
|
async page(dir) { this.offset = Math.max(0, this.offset + dir * this.limit); await this.load(); },
|
|
async remove(f) {
|
|
await fetch('/api/favorites/' + f.id, { method: 'DELETE' });
|
|
await this.load();
|
|
toast('Favori retiré');
|
|
},
|
|
};
|
|
}
|
|
</script>
|
|
{% endblock %}
|