feat: complete UI redesign with DaisyUI + Tailwind CSS v4

Design system overhaul using DaisyUI v5 on Tailwind CSS v4:

- Custom 'ohmstream' dark theme with orange primary (#FF9F1C),
  magenta secondary, gold accent matching existing palette
- Tailwind CSS-first config (input.css source, style.css built output)
- DaisyUI components: navbar, drawer, cards, badges, alerts, tables,
  progress bars, tabs, toggles, stats, form controls, tooltips
- Mobile-first responsive layout with drawer navigation
- Eliminated ~500+ lines of embedded CSS across 15+ template files
- Removed all inline style spam from admin_panel and settings_section
- Preserved all HTMX triggers, Alpine.js state, and Jinja2 logic
- Updated auth-ui.js for DaisyUI tab-active class compatibility

Build: npm run build:css (minified) / npm run watch:css (dev)
This commit is contained in:
root
2026-04-11 19:46:52 +00:00
parent 87f245d3fc
commit 4101d98a41
28 changed files with 2534 additions and 2808 deletions
+1017
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -4,12 +4,17 @@
"description": "Ohm Stream Downloader - Frontend JavaScript Tests", "description": "Ohm Stream Downloader - Frontend JavaScript Tests",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build:css": "npx @tailwindcss/cli -i static/css/input.css -o static/css/style.css --minify",
"watch:css": "npx @tailwindcss/cli -i static/css/input.css -o static/css/style.css",
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest" "test:watch": "vitest"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.58.2", "@playwright/test": "^1.58.2",
"@tailwindcss/cli": "^4.2.2",
"daisyui": "^5.5.19",
"jsdom": "^29.0.0", "jsdom": "^29.0.0",
"tailwindcss": "^4.2.2",
"vitest": "^1.0.0" "vitest": "^1.0.0"
} }
} }
+34
View File
@@ -0,0 +1,34 @@
@import "tailwindcss";
@plugin "daisyui";
@plugin "daisyui/theme" {
name: "ohmstream";
default: true;
prefersdark: false;
color-scheme: dark;
--color-base-100: oklch(0.15 0.01 260); /* #1a1c20 - main bg */
--color-base-200: oklch(0.18 0.01 260); /* #202327 - card bg */
--color-base-300: oklch(0.22 0.01 260); /* #2a2d32 - elevated */
--color-base-content: oklch(0.93 0.01 80); /* #eae8e4 - text */
--color-primary: oklch(0.72 0.16 65); /* #FF9F1C - orange */
--color-primary-content: oklch(0.18 0.02 65); /* #1a1400 */
--color-secondary: oklch(0.65 0.12 310); /* #e05faa - magenta */
--color-secondary-content: oklch(0.95 0 0);
--color-accent: oklch(0.78 0.14 75); /* #FFBF69 - gold */
--color-accent-content: oklch(0.18 0.02 75);
--color-neutral: oklch(0.25 0.01 260); /* #292b30 */
--color-neutral-content: oklch(0.9 0.01 80);
--color-info: oklch(0.65 0.15 250); /* #3b7ddd */
--color-success: oklch(0.65 0.14 155); /* #2d936c */
--color-warning: oklch(0.75 0.16 75); /* #f0a500 */
--color-error: oklch(0.6 0.2 25); /* #e63946 */
--color-error-content: oklch(0.95 0 0);
--radius-selector: 0.5rem;
--radius-field: 0.375rem;
--radius-box: 0.5rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1px;
--depth: 1;
--noise: 0;
}
+2 -1185
View File
File diff suppressed because one or more lines are too long
+12 -8
View File
@@ -104,19 +104,23 @@ function switchTab(tab) {
const tabs = document.querySelectorAll('.auth-tab'); const tabs = document.querySelectorAll('.auth-tab');
const forms = document.querySelectorAll('.auth-form'); const forms = document.querySelectorAll('.auth-form');
tabs.forEach(t => t.classList.remove('active')); // Remove active states — DaisyUI uses tab-active on tabs, hidden on forms
forms.forEach(f => f.classList.remove('active')); tabs.forEach(t => t.classList.remove('tab-active'));
forms.forEach(f => f.classList.add('hidden'));
if (tab === 'login') { if (tab === 'login') {
tabs[0].classList.add('active'); tabs[0].classList.add('tab-active');
document.getElementById('loginForm').classList.add('active'); document.getElementById('loginForm').classList.remove('hidden');
} else { } else {
tabs[1].classList.add('active'); tabs[1].classList.add('tab-active');
document.getElementById('registerForm').classList.add('active'); document.getElementById('registerForm').classList.remove('hidden');
} }
document.getElementById('authError').classList.remove('show'); // Hide alerts on tab switch
document.getElementById('authSuccess').classList.remove('show'); const authError = document.getElementById('authError');
const authSuccess = document.getElementById('authSuccess');
if (authError) authError.classList.add('hidden');
if (authSuccess) authSuccess.classList.add('hidden');
} }
window.authUi = { window.authUi = {
+260 -17
View File
@@ -1,24 +1,33 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr" data-theme="ohmstream">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ohm Stream Downloader</title> <title>Ohm Stream Downloader</title>
<!-- CSS --> <!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- CSS: Tailwind (built from input.css via DaisyUI), Font Awesome, Plyr -->
<link rel="stylesheet" href="/static/css/style.css"> <link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" /> <link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" />
<!-- External Libraries (local first, CDN fallback) --> <!-- x-cloak: hide elements until Alpine initializes -->
<script src="/static/vendor/htmx.min.js"></script>
<script src="/static/vendor/alpine.min.js" defer></script>
<script src="https://cdn.plyr.io/3.7.8/plyr.polyfilled.js"></script>
<style> <style>
[x-cloak] { display: none !important; } [x-cloak] { display: none !important; }
/* Inter as default font, system sans-serif fallback */
body {
font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
</style> </style>
<!-- HTMX (local vendor) -->
<script src="/static/vendor/htmx.min.js"></script>
<!-- Configure HTMX to include auth token in all requests --> <!-- Configure HTMX to include auth token in all requests -->
<script> <script>
document.addEventListener('htmx:configRequest', (event) => { document.addEventListener('htmx:configRequest', (event) => {
@@ -29,35 +38,267 @@
}); });
</script> </script>
<!-- Legacy JavaScript (Refactored to HTMX/Alpine) --> <!-- Alpine.js (local vendor, deferred) -->
<script src="/static/vendor/alpine.min.js" defer></script>
<!-- Plyr.io JS (CDN) -->
<script src="https://cdn.plyr.io/3.7.8/plyr.polyfilled.js"></script>
<!-- Application JS modules -->
<script src="/static/js/auth.js?v=1.10" defer></script> <script src="/static/js/auth.js?v=1.10" defer></script>
<script src="/static/js/api.js?v=1.11" defer></script> <script src="/static/js/api.js?v=1.11" defer></script>
<script src="/static/js/utils.js?v=1.11" defer></script> <script src="/static/js/utils.js?v=1.11" defer></script>
<script src="/static/js/downloads.js?v=1.11" defer></script> <script src="/static/js/downloads.js?v=1.11" defer></script>
<!-- <script src="/static/js/anime.js?v=1.11" defer></script> -->
<!-- <script src="/static/js/anime-details.js?v=1.12" defer></script> -->
<!-- <script src="/static/js/series-search.js?v=1.11" defer></script> -->
<!-- <script src="/static/js/recommendations.js?v=1.11" defer></script> -->
<script src="/static/js/watchlist.js?v=1.11" defer></script> <script src="/static/js/watchlist.js?v=1.11" defer></script>
<!-- <script src="/static/js/watchlist-ui.js?v=1.11" defer></script> -->
<script src="/static/js/main.js?v=1.11" defer></script> <script src="/static/js/main.js?v=1.11" defer></script>
<script src="/static/js/settings.js?v=1.0" defer></script> <script src="/static/js/settings.js?v=1.0" defer></script>
</head> </head>
<body x-data="globalAppState">
<body x-data="globalAppState" x-cloak class="min-h-screen bg-base-100 text-base-content">
<!-- ============================================================
Toast notification container (fixed position, top-right)
============================================================ -->
{% include "components/toast_container.html" %} {% include "components/toast_container.html" %}
<div class="container">
{% block content %}{% endblock %} <!-- ============================================================
DaisyUI Drawer: wraps the entire page layout.
The checkbox (id="ohm-drawer") toggles the mobile sidebar.
============================================================ -->
<div class="drawer">
<input id="ohm-drawer" type="checkbox" class="drawer-toggle" />
<!-- Page content area -->
<div class="drawer-content flex flex-col min-h-screen">
<!-- ====================================================
DaisyUI Navbar (top bar)
==================================================== -->
<nav class="navbar bg-base-200 border-b border-base-300 sticky top-0 z-30 px-4 lg:px-8">
<!-- Mobile menu toggle -->
<div class="flex-none lg:hidden">
<label for="ohm-drawer" class="btn btn-square btn-ghost" aria-label="Menu">
<i class="fa-solid fa-bars text-lg"></i>
</label>
</div>
<!-- Brand / Logo -->
<div class="flex-1 gap-2">
<a href="/web" class="btn btn-ghost text-xl gap-2 hover:bg-transparent">
<i class="fa-solid fa-bolt text-primary"></i>
<span class="font-bold">Ohm Stream</span>
</a>
</div>
<!-- Desktop navigation tabs (hidden on mobile, shown in drawer instead) -->
<div class="hidden lg:flex flex-none gap-1">
<button class="btn btn-sm btn-ghost gap-1.5"
:class="{ 'btn-active bg-primary/15 text-primary': activeTab === 'home' }"
@click="activeTab = 'home'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'home' } }))">
<i class="fa-solid fa-house text-xs"></i> Accueil
</button>
<button class="btn btn-sm btn-ghost gap-1.5"
:class="{ 'btn-active bg-primary/15 text-primary': activeTab === 'anime' }"
@click="activeTab = 'anime'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'anime' } }))">
<i class="fa-solid fa-film text-xs"></i> Anime
</button>
<button class="btn btn-sm btn-ghost gap-1.5"
:class="{ 'btn-active bg-primary/15 text-primary': activeTab === 'series' }"
@click="activeTab = 'series'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'series' } }))">
<i class="fa-solid fa-tv text-xs"></i> Séries
</button>
<button class="btn btn-sm btn-ghost gap-1.5"
:class="{ 'btn-active bg-primary/15 text-primary': activeTab === 'watchlist' }"
@click="activeTab = 'watchlist'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'watchlist' } }))">
<i class="fa-solid fa-clipboard-list text-xs"></i> Watchlist
</button>
<button class="btn btn-sm btn-ghost gap-1.5"
:class="{ 'btn-active bg-primary/15 text-primary': activeTab === 'downloads' }"
@click="activeTab = 'downloads'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'downloads' } }))">
<i class="fa-solid fa-download text-xs"></i> Téléchargements
</button>
<button class="btn btn-sm btn-ghost gap-1.5"
:class="{ 'btn-active bg-primary/15 text-primary': activeTab === 'settings' }"
@click="activeTab = 'settings'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'settings' } }))">
<i class="fa-solid fa-gear text-xs"></i> Paramètres
</button>
</div>
<!-- User info (desktop) -->
<div class="hidden lg:flex flex-none items-center gap-2">
<!-- Authenticated state -->
<div x-show="isAuthenticated" x-cloak class="flex items-center gap-2">
<span class="text-sm text-base-content/70">
<i class="fa-solid fa-user text-primary"></i>
<strong class="text-primary" x-text="username">-</strong>
</span>
<button class="btn btn-sm btn-ghost text-error"
onclick="removeToken(); isAuthenticated = false"
hx-post="/api/auth/logout"
hx-on::after-request="window.location.href = '/login'">
<i class="fa-solid fa-right-from-bracket"></i> Déconnexion
</button>
</div>
<!-- Unauthenticated state -->
<div x-show="!isAuthenticated" x-cloak>
<a href="/login" class="btn btn-sm btn-primary">
<i class="fa-solid fa-right-to-bracket"></i> Se connecter
</a>
</div>
</div>
<!-- Mobile: user icon trigger + settings dropdown -->
<div class="flex-none lg:hidden">
<div x-show="isAuthenticated" x-cloak>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-square btn-ghost">
<i class="fa-solid fa-circle-user text-lg text-primary"></i>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-200 rounded-box border border-base-300 z-[1] w-56 p-2 shadow-lg mt-2">
<li class="menu-title text-xs" x-text="username"></li>
<li>
<button class="text-error"
onclick="removeToken(); isAuthenticated = false"
hx-post="/api/auth/logout"
hx-on::after-request="window.location.href = '/login'">
<i class="fa-solid fa-right-from-bracket"></i> Déconnexion
</button>
</li>
</ul>
</div>
</div>
<div x-show="!isAuthenticated" x-cloak>
<a href="/login" class="btn btn-sm btn-primary">
<i class="fa-solid fa-right-to-bracket"></i>
</a>
</div>
</div>
</nav>
<!-- ====================================================
Main content block (rendered by child templates)
==================================================== -->
<main class="flex-1">
<div class="container mx-auto px-4 py-6 max-w-7xl">
{% block content %}{% endblock %}
</div>
</main>
<!-- Footer -->
<footer class="footer footer-center p-4 bg-base-200 text-base-content/50 border-t border-base-300">
<aside class="text-xs">
<p>Ohm Stream Downloader &mdash; Téléchargez vos animes et séries</p>
</aside>
</footer>
</div>
<!-- ====================================================
DaisyUI Drawer sidebar (mobile navigation)
Slides in from the left on mobile (< lg).
==================================================== -->
<div class="drawer-side z-40">
<label for="ohm-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<aside class="bg-base-200 min-h-full w-64 border-r border-base-300 flex flex-col">
<!-- Drawer header / brand -->
<div class="p-4 border-b border-base-300">
<a href="/web" class="flex items-center gap-2 text-xl font-bold" @click="document.getElementById('ohm-drawer').checked = false">
<i class="fa-solid fa-bolt text-primary"></i>
<span>Ohm Stream</span>
</a>
<p class="text-xs text-base-content/50 mt-1">Téléchargez vos vidéos, animes et séries</p>
</div>
<!-- Mobile navigation menu -->
<ul class="menu p-4 gap-1 flex-1">
<!-- User info (mobile drawer) -->
<li x-show="isAuthenticated" x-cloak class="mb-2">
<div class="flex items-center gap-2 px-2 py-1 rounded-lg bg-base-300/50">
<i class="fa-solid fa-user text-primary text-sm"></i>
<span class="text-sm truncate">
<span class="text-base-content/50">Connecté: </span>
<strong class="text-primary" x-text="username">-</strong>
</span>
</div>
</li>
<li x-show="!isAuthenticated" x-cloak class="mb-2">
<a href="/login" class="btn btn-primary btn-sm w-full justify-center">
<i class="fa-solid fa-right-to-bracket"></i> Se connecter
</a>
</li>
<li class="mt-2">
<button class="w-full text-left"
:class="{ 'active': activeTab === 'home' }"
@click="activeTab = 'home'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'home' } })); document.getElementById('ohm-drawer').checked = false">
<i class="fa-solid fa-house w-5 text-center"></i> Accueil
</button>
</li>
<li>
<button class="w-full text-left"
:class="{ 'active': activeTab === 'anime' }"
@click="activeTab = 'anime'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'anime' } })); document.getElementById('ohm-drawer').checked = false">
<i class="fa-solid fa-film w-5 text-center"></i> Anime
</button>
</li>
<li>
<button class="w-full text-left"
:class="{ 'active': activeTab === 'series' }"
@click="activeTab = 'series'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'series' } })); document.getElementById('ohm-drawer').checked = false">
<i class="fa-solid fa-tv w-5 text-center"></i> Séries
</button>
</li>
<li>
<button class="w-full text-left"
:class="{ 'active': activeTab === 'watchlist' }"
@click="activeTab = 'watchlist'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'watchlist' } })); document.getElementById('ohm-drawer').checked = false">
<i class="fa-solid fa-clipboard-list w-5 text-center"></i> Watchlist
</button>
</li>
<li>
<button class="w-full text-left"
:class="{ 'active': activeTab === 'downloads' }"
@click="activeTab = 'downloads'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'downloads' } })); document.getElementById('ohm-drawer').checked = false">
<i class="fa-solid fa-download w-5 text-center"></i> Téléchargements
</button>
</li>
<li>
<button class="w-full text-left"
:class="{ 'active': activeTab === 'settings' }"
@click="activeTab = 'settings'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'settings' } })); document.getElementById('ohm-drawer').checked = false">
<i class="fa-solid fa-gear w-5 text-center"></i> Paramètres
</button>
</li>
<!-- Mobile logout -->
<li x-show="isAuthenticated" x-cloak class="mt-auto border-t border-base-300 pt-2">
<button class="text-error"
onclick="removeToken(); isAuthenticated = false"
hx-post="/api/auth/logout"
hx-on::after-request="window.location.href = '/login'">
<i class="fa-solid fa-right-from-bracket w-5 text-center"></i> Déconnexion
</button>
</li>
</ul>
</aside>
</div>
</div> </div>
<!-- ============================================================
Alpine.js global state initialization
============================================================ -->
<script> <script>
// Global State initialized when Alpine is ready
document.addEventListener('alpine:init', () => { document.addEventListener('alpine:init', () => {
console.log('Alpine.js initializing...'); console.log('Alpine.js initializing...');
Alpine.data('globalAppState', () => ({ Alpine.data('globalAppState', () => ({
activeTab: 'home', activeTab: 'home',
isAuthenticated: true, isAuthenticated: true,
username: '', username: '',
init() { init() {
// Auth state listeners
window.addEventListener('auth-success', (e) => { window.addEventListener('auth-success', (e) => {
this.isAuthenticated = true; this.isAuthenticated = true;
this.username = e.detail.username; this.username = e.detail.username;
@@ -66,6 +307,8 @@
this.isAuthenticated = false; this.isAuthenticated = false;
this.username = ''; this.username = '';
}); });
// Tab switching via custom events (SPA hash routing support)
window.addEventListener('set-tab', (e) => { window.addEventListener('set-tab', (e) => {
this.activeTab = e.detail.tab; this.activeTab = e.detail.tab;
}); });
+51 -47
View File
@@ -1,85 +1,89 @@
<div class="settings-container section-container"> <div class="mb-10">
<div class="section-header"> <div class="flex justify-between items-center mb-4">
<h2>Administration</h2> <h2 class="text-xl font-bold">Administration</h2>
</div> </div>
<!-- Stats Cards --> <!-- Stats Cards -->
<div id="admin-stats" class="admin-stats-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; margin-bottom: 30px;"> <div class="stats stats-vertical md:stats-horizontal shadow w-full mb-6">
<div class="admin-stat-card" style="padding: 20px; background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid #2a2d32;text-align: center;"> <div class="stat bg-base-200 border border-base-300 rounded-box">
<div style="font-size: 2rem; font-weight: 800; color: var(--primary);" id="stat-total-users">{{ users|length }}</div> <div class="stat-title">Utilisateurs</div>
<div style="color: var(--text-dim); font-size: 0.85rem;">Utilisateurs</div> <div class="stat-value text-primary">{{ users|length }}</div>
</div> </div>
<div class="admin-stat-card" style="padding: 20px; background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid #2a2d32;text-align: center;"> <div class="stat bg-base-200 border border-base-300 rounded-box">
<div style="font-size: 2rem; font-weight: 800; color: var(--accent);" id="stat-active-users">{{ users|selectattr('is_active')|list|length }}</div> <div class="stat-title">Actifs</div>
<div style="color: var(--text-dim); font-size: 0.85rem;">Actifs</div> <div class="stat-value text-secondary">{{ users|selectattr('is_active')|list|length }}</div>
</div> </div>
<div class="admin-stat-card" style="padding: 20px; background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid #2a2d32;text-align: center;"> <div class="stat bg-base-200 border border-base-300 rounded-box">
<div style="font-size: 2rem; font-weight: 800; color: #f0a500;" id="stat-admin-users">{{ users|selectattr('is_admin')|list|length }}</div> <div class="stat-title">Admins</div>
<div style="color: var(--text-dim); font-size: 0.85rem;">Admins</div> <div class="stat-value text-warning">{{ users|selectattr('is_admin')|list|length }}</div>
</div> </div>
</div> </div>
<!-- Users Table --> <!-- Users Table -->
<div style="background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid #2a2d32;overflow: hidden;"> <div class="bg-base-200 border border-base-300 rounded-box overflow-hidden">
<div style="padding: 20px 25px; border-bottom: 1px solid #2a2d32;"> <div class="px-6 py-5 border-b border-base-300">
<h3 style="margin: 0; color: var(--primary);">Gestion des utilisateurs</h3> <h3 class="font-bold text-primary m-0">Gestion des utilisateurs</h3>
</div> </div>
{% if users %} {% if users %}
<div style="overflow-x: auto;"> <div class="overflow-x-auto">
<table style="width: 100%; border-collapse: collapse;"> <table class="table table-sm">
<thead> <thead>
<tr style="border-bottom: 1px solid #2a2d32;"> <tr>
<th style="padding: 12px 20px; text-align: left; color: var(--text-dim); font-size: 0.8rem; text-transform: uppercase;">Utilisateur</th> <th>Utilisateur</th>
<th style="padding: 12px 15px; text-align: left; color: var(--text-dim); font-size: 0.8rem; text-transform: uppercase;">Email</th> <th>Email</th>
<th style="padding: 12px 15px; text-align: center; color: var(--text-dim); font-size: 0.8rem; text-transform: uppercase;">Statut</th> <th class="text-center">Statut</th>
<th style="padding: 12px 15px; text-align: center; color: var(--text-dim); font-size: 0.8rem; text-transform: uppercase;">Role</th> <th class="text-center">Role</th>
<th style="padding: 12px 15px; text-align: left; color: var(--text-dim); font-size: 0.8rem; text-transform: uppercase;">Derniere connexion</th> <th>Derniere connexion</th>
<th style="padding: 12px 15px; text-align: left; color: var(--text-dim); font-size: 0.8rem; text-transform: uppercase;">Inscription</th> <th>Inscription</th>
<th style="padding: 12px 20px; text-align: center; color: var(--text-dim); font-size: 0.8rem; text-transform: uppercase;">Actions</th> <th class="text-center">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for user in users %} {% for user in users %}
<tr style="border-bottom: 1px solid #2a2d32; {% if not user.is_active %}opacity: 0.5;{% endif %}"> <tr class="{% if not user.is_active %}opacity-50{% endif %}">
<td style="padding: 12px 20px;"> <td>
<div style="font-weight: 600;">{{ user.username }}</div> <div class="font-semibold">{{ user.username }}</div>
{% if user.full_name %} {% if user.full_name %}
<div style="font-size: 0.8rem; color: var(--text-dim);">{{ user.full_name }}</div> <div class="text-xs text-base-content/50">{{ user.full_name }}</div>
{% endif %} {% endif %}
</td> </td>
<td style="padding: 12px 15px; color: var(--text-dim); font-size: 0.9rem;">{{ user.email or '-' }}</td> <td class="text-base-content/60 text-sm">{{ user.email or '-' }}</td>
<td style="padding: 12px 15px; text-align: center;"> <td class="text-center">
<span style="display: inline-block; padding: 3px 10px; border-radius: 4px; font-size: 0.75rem; font-weight: 600; background: {% if user.is_active %}rgba(45,147,108,0.1); color: #2d936c{% else %}rgba(230,57,70,0.1); color: #e63946{% endif %};"> {% if user.is_active %}
{% if user.is_active %}Actif{% else %}Inactif{% endif %} <span class="badge badge-success badge-sm">Actif</span>
</span> {% else %}
<span class="badge badge-error badge-sm">Inactif</span>
{% endif %}
</td> </td>
<td style="padding: 12px 15px; text-align: center;"> <td class="text-center">
<span style="display: inline-block; padding: 3px 10px; border-radius: 4px; font-size: 0.75rem; font-weight: 600; background: {% if user.is_admin %}rgba(244,162,97,0.15); color: #f4a261{% else %}var(--bg-elevated); color: var(--text-dim){% endif %};"> {% if user.is_admin %}
{% if user.is_admin %}Admin{% else %}User{% endif %} <span class="badge badge-primary badge-sm">Admin</span>
</span> {% else %}
<span class="badge badge-ghost badge-sm">User</span>
{% endif %}
</td> </td>
<td style="padding: 12px 15px; color: var(--text-dim); font-size: 0.85rem;"> <td class="text-base-content/50 text-sm">
{{ user.last_login.strftime('%d/%m/%Y %H:%M') if user.last_login else '-' }} {{ user.last_login.strftime('%d/%m/%Y %H:%M') if user.last_login else '-' }}
</td> </td>
<td style="padding: 12px 15px; color: var(--text-dim); font-size: 0.85rem;"> <td class="text-base-content/50 text-sm">
{{ user.created_at.strftime('%d/%m/%Y') if user.created_at else '-' }} {{ user.created_at.strftime('%d/%m/%Y') if user.created_at else '-' }}
</td> </td>
<td style="padding: 12px 20px; text-align: center; white-space: nowrap;"> <td class="text-center whitespace-nowrap">
{% if user.id != current_user.id %} {% if user.id != current_user.id %}
<button class="btn btn-sm {% if user.is_active %}btn-secondary{% else %}btn-accent{% endif %}" <button class="btn btn-xs {% if user.is_active %}btn-ghost{% else %}btn-success{% endif %}"
hx-put="/api/admin/users/{{ user.id }}/toggle-active" hx-swap="none" hx-put="/api/admin/users/{{ user.id }}/toggle-active" hx-swap="none"
hx-on::after-request="htmx.ajax('GET', '/api/admin/ui', {target: '#admin-panel-content'})" hx-on::after-request="htmx.ajax('GET', '/api/admin/ui', {target: '#admin-panel-content'})"
title="{% if user.is_active %}Desactiver{% else %}Activer{% endif %}"> title="{% if user.is_active %}Desactiver{% else %}Activer{% endif %}">
{% if user.is_active %}Desactiver{% else %}Activer{% endif %} {% if user.is_active %}Desactiver{% else %}Activer{% endif %}
</button> </button>
<button class="btn btn-sm {% if user.is_admin %}btn-secondary{% else %}btn-accent{% endif %}" <button class="btn btn-xs {% if user.is_admin %}btn-ghost{% else %}btn-success{% endif %}"
hx-put="/api/admin/users/{{ user.id }}/toggle-admin" hx-swap="none" hx-put="/api/admin/users/{{ user.id }}/toggle-admin" hx-swap="none"
hx-on::after-request="htmx.ajax('GET', '/api/admin/ui', {target: '#admin-panel-content'})" hx-on::after-request="htmx.ajax('GET', '/api/admin/ui', {target: '#admin-panel-content'})"
title="{% if user.is_admin %}Retrograder{% else %}Promouvoir{% endif %}"> title="{% if user.is_admin %}Retrograder{% else %}Promouvoir{% endif %}">
{% if user.is_admin %}Retrograder{% else %}Admin{% endif %} {% if user.is_admin %}Retrograder{% else %}Admin{% endif %}
</button> </button>
<button class="btn btn-sm btn-danger" <button class="btn btn-xs btn-error"
hx-delete="/api/admin/users/{{ user.id }}" hx-swap="none" hx-delete="/api/admin/users/{{ user.id }}" hx-swap="none"
hx-confirm="Supprimer {{ user.username }} ?" hx-confirm="Supprimer {{ user.username }} ?"
hx-on::after-request="htmx.ajax('GET', '/api/admin/ui', {target: '#admin-panel-content'})" hx-on::after-request="htmx.ajax('GET', '/api/admin/ui', {target: '#admin-panel-content'})"
@@ -87,7 +91,7 @@
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
{% else %} {% else %}
<span style="color: var(--text-dim); font-size: 0.8rem;">Vous</span> <span class="text-base-content/40 text-xs">Vous</span>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
@@ -96,7 +100,7 @@
</table> </table>
</div> </div>
{% else %} {% else %}
<div style="padding: 40px; text-align: center; color: var(--text-dim);">Aucun utilisateur</div> <div class="p-10 text-center text-base-content/40">Aucun utilisateur</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
+16 -8
View File
@@ -1,18 +1,26 @@
{% macro anime_card(anime, in_watchlist=False, lang='vostfr') %} {% macro anime_card(anime, in_watchlist=False, lang='vostfr') %}
<div class="hc" id="anime-{{ anime.url | hash }}" <div class="card card-compact bg-base-200 shadow-lg hover:shadow-xl transition-all cursor-pointer w-40 shrink-0 group"
id="anime-{{ anime.url | hash }}"
@click="activeTab = 'anime'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'anime' } })); $nextTick(() => { const input = document.getElementById('animeSearchInput'); if (input) { input.value = '{{ anime.title | e }}'; htmx.trigger(input, 'keyup'); } });"> @click="activeTab = 'anime'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'anime' } })); $nextTick(() => { const input = document.getElementById('animeSearchInput'); if (input) { input.value = '{{ anime.title | e }}'; htmx.trigger(input, 'keyup'); } });">
<div class="hc-poster"> <figure class="relative overflow-hidden rounded-t-lg aspect-[2/3]">
{% set poster = anime.cover_image or (anime.metadata.poster_image if anime.metadata else None) or 'https://placehold.co/400x600/e6e8e6/f15025?text=No+Image' %} {% set poster = anime.cover_image or (anime.metadata.poster_image if anime.metadata else None) or 'https://placehold.co/400x600/e6e8e6/f15025?text=No+Image' %}
<img src="{{ poster }}" alt="{{ anime.title }}" loading="lazy" referrerpolicy="no-referrer" <img src="{{ poster }}" alt="{{ anime.title }}" loading="lazy" referrerpolicy="no-referrer"
class="w-full h-full object-cover"
onerror="this.src='https://placehold.co/400x600/e6e8e6/f15025?text=Error'; this.onerror=null;"> onerror="this.src='https://placehold.co/400x600/e6e8e6/f15025?text=Error'; this.onerror=null;">
{% if anime.metadata and anime.metadata.rating %} {% if anime.metadata and anime.metadata.rating %}
<span class="hc-rating"><i class="fas fa-star"></i> {{ anime.metadata.rating }}</span> <span class="badge badge-warning badge-sm absolute top-2 left-2 gap-1">
<i class="fa-solid fa-star text-[10px]"></i> {{ anime.metadata.rating }}
</span>
{% endif %} {% endif %}
<span class="hc-play"><i class="fas fa-search"></i></span> <div class="absolute inset-0 bg-primary/20 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
</div> <div class="btn btn-circle btn-sm bg-primary/80 border-primary text-primary-content">
<div class="hc-info"> <i class="fa-solid fa-magnifying-glass"></i>
<span class="hc-src">{{ anime.provider_id or 'Anime' }}</span> </div>
<span class="hc-title">{{ anime.title }}</span> </div>
</figure>
<div class="card-body p-3">
<span class="badge badge-outline badge-xs">{{ anime.provider_id or 'Anime' }}</span>
<p class="text-xs font-semibold truncate mt-1">{{ anime.title }}</p>
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}
+77 -106
View File
@@ -1,4 +1,3 @@
{% set accent = "#FF9F1C" %}
{% set default_lang = settings.default_lang if settings else 'vostfr' %} {% set default_lang = settings.default_lang if settings else 'vostfr' %}
{% set _groups = namespace(items={}) %} {% set _groups = namespace(items={}) %}
@@ -30,128 +29,100 @@
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
<div class="sr-list" x-data="{ openDropdown: null }"> <div class="flex flex-col gap-4" x-data="{ openDropdown: null }">
{% if _groups.items.values() | list | length > 0 %} {% if _groups.items.values() | list | length > 0 %}
{% for group in _groups.items.values() | list %} {% for group in _groups.items.values() | list %}
{% set first_url = group.providers[0].url %} {% set first_url = group.providers[0].url %}
<div class="sr-card" style="--sr-accent: {{ accent }};"> <div class="card bg-base-200 border border-base-300 hover:border-primary transition-colors">
<a class="sr-poster-link" href="{{ first_url }}" target="_blank" rel="noopener"> <div class="card-body p-5 flex-row gap-5">
<img class="sr-poster-img" src="{{ group.cover or 'https://placehold.co/240x360/29274c/7e52a0?text=No+Image' }}" <figure class="w-28 shrink-0">
alt="{{ group.title }}" loading="lazy" referrerpolicy="no-referrer" <a href="{{ first_url }}" target="_blank" rel="noopener">
onerror="this.src='https://placehold.co/240x360/29274c/7e52a0?text=Error'; this.onerror=null;"> <img src="{{ group.cover or 'https://placehold.co/240x360/29274c/7e52a0?text=No+Image' }}"
</a> alt="{{ group.title }}" loading="lazy" referrerpolicy="no-referrer"
<div class="sr-body"> class="rounded-lg w-full aspect-[2/3] object-cover"
<div class="sr-top"> onerror="this.src='https://placehold.co/240x360/29274c/7e52a0?text=Error'; this.onerror=null;">
<h3 class="sr-title">{{ group.title }}</h3> </a>
{% if group.rating %} </figure>
<span class="sr-rating"><i class="fas fa-star"></i> {{ group.rating }}</span> <div class="flex-1 min-w-0 flex flex-col gap-2">
<div class="flex items-baseline gap-3">
<h3 class="card-title text-base truncate">{{ group.title }}</h3>
{% if group.rating %}
<span class="badge badge-warning badge-sm shrink-0"><i class="fas fa-star"></i> {{ group.rating }}</span>
{% endif %}
</div>
{% if group.synopsis %}
<p class="text-sm text-base-content/60 line-clamp-3">{{ group.synopsis }}</p>
{% endif %} {% endif %}
</div>
{% if group.synopsis %} {% if group.genres %}
<p class="sr-synopsis">{{ group.synopsis }}</p> <div class="flex flex-wrap gap-1">
{% endif %} {% for g in group.genres[:5] %}
<span class="badge badge-ghost badge-sm">{{ g }}</span>
{% endfor %}
</div>
{% endif %}
{% if group.genres %} <div class="flex flex-wrap gap-1.5">
<div class="sr-tags"> {% for p in group.providers %}
{% for g in group.genres[:5] %} <a href="{{ p.url }}" target="_blank" rel="noopener"
<span class="sr-tag">{{ g }}</span> class="badge badge-primary badge-outline text-xs uppercase tracking-wider hover:bg-primary hover:text-primary-content hover:border-primary transition-colors cursor-pointer">
{{ p.id | upper }}
</a>
{% endfor %} {% endfor %}
</div> </div>
{% endif %}
<div class="sr-providers"> <div class="flex flex-wrap gap-2 mt-1">
{% for p in group.providers %} <a href="{{ first_url }}" target="_blank" rel="noopener"
<a class="sr-provider-badge" href="{{ p.url }}" target="_blank" rel="noopener">{{ p.id | upper }}</a> class="btn btn-sm btn-primary">
{% endfor %} <i class="fas fa-play"></i> Regarder
</div> </a>
<div class="dropdown" @click.outside="openDropdown = null">
<div class="sr-actions"> <div tabindex="0" role="button"
<a class="sr-btn sr-btn-watch" href="{{ first_url }}" target="_blank" rel="noopener"> @click.stop="openDropdown = (openDropdown === '{{ first_url | urlencode }}') ? null : '{{ first_url | urlencode }}'">
<i class="fas fa-play"></i> Regarder <span class="btn btn-sm btn-secondary">
</a> <i class="fas fa-download"></i> Telecharger <i class="fas fa-chevron-down text-[0.6rem] ml-1"></i>
<div class="sr-dropdown" @click.outside="openDropdown = null"> </span>
<button class="sr-btn sr-btn-dl" @click.stop="openDropdown = (openDropdown === '{{ first_url | urlencode }}') ? null : '{{ first_url | urlencode }}'"> </div>
<i class="fas fa-download"></i> Telecharger <i class="fas fa-chevron-down" style="font-size:0.6rem;margin-left:4px;"></i> <ul tabindex="0"
</button> class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52 border border-base-300"
<div class="sr-dropdown-menu" x-show="openDropdown === '{{ first_url | urlencode }}'" x-transition> x-show="openDropdown === '{{ first_url | urlencode }}'"
<button class="sr-dropdown-item" x-transition>
hx-post="/api/anime/download-season?url={{ first_url | urlencode }}&lang={{ default_lang }}" <li>
hx-swap="none" <button class="flex items-center gap-2 text-sm"
hx-on::after-request="openDropdown = null"> hx-post="/api/anime/download-season?url={{ first_url | urlencode }}&lang={{ default_lang }}"
<i class="fas fa-layer-group"></i> Saison complete hx-swap="none"
</button> hx-on::after-request="openDropdown = null">
<button class="sr-dropdown-item" <i class="fas fa-layer-group"></i> Saison complete
hx-get="/api/anime/episodes?url={{ first_url | urlencode }}&lang={{ default_lang }}&html=1" </button>
hx-target="#player-container" </li>
hx-swap="innerHTML" <li>
hx-on::after-request="openDropdown = null; document.getElementById('player-container').scrollIntoView({behavior: 'smooth'})"> <button class="flex items-center gap-2 text-sm"
<i class="fas fa-list-ol"></i> Choisir des episodes hx-get="/api/anime/episodes?url={{ first_url | urlencode }}&lang={{ default_lang }}&html=1"
</button> hx-target="#player-container"
hx-swap="innerHTML"
hx-on::after-request="openDropdown = null; document.getElementById('player-container').scrollIntoView({behavior: 'smooth'})">
<i class="fas fa-list-ol"></i> Choisir des episodes
</button>
</li>
</ul>
</div> </div>
<button class="btn btn-sm btn-accent btn-outline"
hx-post="/api/watchlist"
hx-vals='{"anime_url": "{{ first_url }}", "anime_title": "{{ group.title }}", "provider_id": "{{ group.providers[0].id }}", "lang": "{{ default_lang }}", "poster_image": "{{ group.cover }}"}'
hx-swap="none"
hx-on::after-request="if(event.detail.successful){this.innerHTML='<i class=\'fas fa-check\'></i> Suivi';this.disabled=true;this.classList.remove('btn-accent','btn-outline');this.classList.add('btn-accent','btn-ghost','pointer-events-none')}">
<i class="fas fa-plus"></i> Suivre
</button>
</div> </div>
<button class="sr-btn sr-btn-follow"
hx-post="/api/watchlist"
hx-vals='{"anime_url": "{{ first_url }}", "anime_title": "{{ group.title }}", "provider_id": "{{ group.providers[0].id }}", "lang": "{{ default_lang }}", "poster_image": "{{ group.cover }}"}'
hx-swap="none"
hx-on::after-request="if(event.detail.successful){this.innerHTML='<i class=\'fas fa-check\'></i> Suivi';this.disabled=true;this.classList.add('sr-btn-followed')}">
<i class="fas fa-plus"></i> Suivre
</button>
</div> </div>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
{% else %} {% else %}
<div class="sr-empty"> <div class="text-center py-20 text-base-content/40">
<i class="fas fa-search"></i> <i class="fas fa-search text-6xl mb-5 block opacity-20"></i>
<p>Aucun anime trouve pour votre recherche.</p> <p>Aucun anime trouve pour votre recherche.</p>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<style>
.sr-list { display: flex; flex-direction: column; gap: 16px; }
.sr-card {
display: flex; gap: 20px;
background: var(--bg-card); border-radius: var(--card-radius);
padding: 20px; border: 1px solid #2a2d32;"
transition: var(--transition);
}
.sr-card:hover { border-color: var(--sr-accent); }
.sr-poster-link { flex-shrink: 0; display: block; width: 120px; aspect-ratio: 2/3; border-radius: 4px; overflow: hidden; background: #000; }
.sr-poster-img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sr-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
.sr-top { display: flex; align-items: baseline; gap: 12px; }
.sr-title { font-size: 1.1rem; font-weight: 700; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sr-rating { flex-shrink: 0; font-size: 0.8rem; font-weight: 700; color: #ffcc00; }
.sr-synopsis { font-size: 0.85rem; color: var(--text-dim); margin: 0; line-height: 1.5; }
.sr-tags { display: flex; flex-wrap: wrap; gap: 4px; margin: 0; }
.sr-tag { font-size: 0.65rem; font-weight: 600; padding: 2px 8px; border-radius: 4px; background: var(--bg-elevated); color: var(--text-dim); }
.sr-providers { display: flex; flex-wrap: wrap; gap: 6px; }
.sr-provider-badge { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; padding: 4px 12px; border-radius: 20px; border: 1px solid var(--sr-accent); color: var(--sr-accent); background: transparent; cursor: pointer; transition: var(--transition); letter-spacing: 0.5px; text-decoration: none; }
.sr-provider-badge:hover { background: var(--sr-accent); color: #fff; }
.sr-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
.sr-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 16px; border-radius: 4px; font-size: 0.8rem; font-weight: 600; border: 1px solid #2a2d32; cursor: pointer; transition: var(--transition); text-decoration: none; background: transparent; color: var(--text-main); min-height: 34px; }
.sr-btn:hover { border-color: var(--text-main); background: var(--bg-card); }
.sr-btn-dl { border-color: var(--secondary); color: var(--secondary); }
.sr-btn-dl:hover { background: var(--secondary); color: #ffffff; }
.sr-btn-watch { border-color: var(--sr-accent); color: var(--sr-accent); }
.sr-btn-watch:hover { background: var(--sr-accent); color: #fff; }
.sr-btn-follow { border-color: var(--accent); color: var(--accent); }
.sr-btn-follow:hover { background: var(--accent); color: #fff; }
.sr-btn-followed { border-color: var(--accent); color: var(--accent); background: rgba(255,191,105,0.1); pointer-events: none; }
.sr-dropdown { position: relative; }
.sr-dropdown-menu { position: absolute; top: calc(100% + 6px); left: 0; min-width: 200px; background: var(--bg-card); border: 1px solid #2a2d32; border-radius: 4px; padding: 4px; z-index: 100; }
.sr-dropdown-item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 10px 12px; border: none; background: transparent; color: var(--text-main); font-size: 0.8rem; cursor: pointer; border-radius: 4px; transition: var(--transition); text-align: left; }
.sr-dropdown-item:hover { background: var(--bg-elevated); }
.sr-empty { text-align: center; padding: 100px 20px; color: var(--text-dim); }
.sr-empty i { font-size: 4rem; margin-bottom: 20px; display: block; opacity: 0.2; }
@media (max-width: 768px) {
.sr-card { flex-direction: column; align-items: center; text-align: center; gap: 12px; padding: 16px; }
.sr-poster-link { width: 160px; }
.sr-top { justify-content: center; }
.sr-tags { justify-content: center; }
.sr-providers { justify-content: center; }
.sr-actions { justify-content: center; }
}
</style>
+33 -24
View File
@@ -1,52 +1,61 @@
{% if tasks %} {% if tasks %}
<div class="downloads-grid"> <div class="flex flex-col gap-3">
{% for task in tasks %} {% for task in tasks %}
<div class="download-item status-{{ task.status }}"> <div class="card bg-base-200 border border-base-300 p-4">
<div class="download-info"> <!-- Top row: filename + status badge -->
<span class="download-name" title="{{ task.filename }}">{{ task.filename }}</span> <div class="flex justify-between items-center mb-3">
<span class="badge badge-{{ task.status }}">{{ task.status | upper }}</span> <span class="font-medium truncate mr-2" title="{{ task.filename }}">{{ task.filename }}</span>
<span class="badge
{% if task.status == 'downloading' %}badge-info
{% elif task.status == 'completed' %}badge-success
{% elif task.status == 'failed' %}badge-error
{% elif task.status == 'paused' %}badge-warning
{% else %}badge-ghost{% endif %}">
{{ task.status | upper }}
</span>
</div> </div>
<div class="progress-container"> <!-- Progress bar -->
<div class="progress-bar" style="width: {{ task.progress }}%"></div> <progress class="progress progress-primary w-full mb-3" value="{{ task.progress }}" max="100"></progress>
</div>
<!-- Meta row: speed, percentage, ETA -->
<div class="download-meta"> <div class="flex gap-4 text-xs text-base-content/50 mb-3">
<span>{{ task.progress | round(1) }}%</span> <span>{{ task.progress | round(1) }}%</span>
<span>{{ task.speed or '0' }} KB/s</span> <span>{{ task.speed or '0' }} KB/s</span>
<span>{{ task.eta or '' }}</span> <span>{{ task.eta or '' }}</span>
</div> </div>
<div class="download-actions"> <!-- Action buttons -->
<div class="flex gap-1 justify-end">
{% if task.status == 'downloading' or task.status == 'pending' %} {% if task.status == 'downloading' or task.status == 'pending' %}
<button class="btn-icon" hx-post="/api/downloads/{{ task.id }}/pause" hx-swap="none" <button class="btn btn-circle btn-sm btn-ghost" hx-post="/api/downloads/{{ task.id }}/pause" hx-swap="none"
hx-on::after-request="htmx.trigger('#downloads-container-inner', 'refresh')" title="Pause"> hx-on::after-request="htmx.trigger('#downloads-container-inner', 'refresh')" title="Pause">
<i class="fas fa-pause"></i> <i class="fas fa-pause"></i>
</button> </button>
{% elif task.status == 'paused' %} {% elif task.status == 'paused' %}
<button class="btn-icon success" hx-post="/api/downloads/{{ task.id }}/resume" hx-swap="none" <button class="btn btn-circle btn-sm btn-success" hx-post="/api/downloads/{{ task.id }}/resume" hx-swap="none"
hx-on::after-request="htmx.trigger('#downloads-container-inner', 'refresh')" title="Reprendre"> hx-on::after-request="htmx.trigger('#downloads-container-inner', 'refresh')" title="Reprendre">
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
</button> </button>
{% endif %} {% endif %}
{% if task.status == 'failed' or task.status == 'cancelled' %} {% if task.status == 'failed' or task.status == 'cancelled' %}
<button class="btn-icon warning" hx-post="/api/downloads/{{ task.id }}/retry" hx-swap="none" <button class="btn btn-circle btn-sm btn-warning" hx-post="/api/downloads/{{ task.id }}/retry" hx-swap="none"
hx-on::after-request="htmx.trigger('#downloads-container-inner', 'refresh')" title="Relancer"> hx-on::after-request="htmx.trigger('#downloads-container-inner', 'refresh')" title="Relancer">
<i class="fas fa-redo"></i> <i class="fas fa-redo"></i>
</button> </button>
{% endif %} {% endif %}
{% if task.status == 'completed' %} {% if task.status == 'completed' %}
<a href="/api/downloads/video/{{ task.id }}" class="btn-icon success" title="Streamer"> <a href="/api/downloads/video/{{ task.id }}" class="btn btn-circle btn-sm btn-success" title="Streamer">
<i class="fas fa-play-circle"></i> <i class="fas fa-play-circle"></i>
</a> </a>
<a href="/downloads/{{ task.filename }}" class="btn-icon" download title="Telecharger"> <a href="/downloads/{{ task.filename }}" class="btn btn-circle btn-sm btn-ghost" download title="Telecharger">
<i class="fas fa-file-download"></i> <i class="fas fa-file-download"></i>
</a> </a>
{% endif %} {% endif %}
<button class="btn-icon danger" <button class="btn btn-circle btn-sm btn-error"
hx-delete="/api/downloads/{{ task.id }}" hx-delete="/api/downloads/{{ task.id }}"
hx-confirm="Supprimer ce telechargement ?" hx-confirm="Supprimer ce telechargement ?"
hx-swap="none" hx-swap="none"
@@ -59,8 +68,8 @@
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<div class="empty-state" style="text-align: center; padding: 60px 20px; color: var(--text-dim);"> <div class="text-center py-16 text-base-content/30">
<i class="fas fa-cloud-download-alt" style="font-size: 3rem; margin-bottom: 20px; opacity: 0.1; display: block;"></i> <i class="fas fa-cloud-download-alt text-5xl mb-5 block"></i>
<p>Aucun telechargement en cours</p> <p>Aucun telechargement en cours</p>
</div> </div>
{% endif %} {% endif %}
+13 -23
View File
@@ -1,15 +1,18 @@
<div class="section-container"> <div class="mb-10">
<div class="section-header"> <div class="flex justify-between items-center mb-4">
<h2>Telechargements <span id="activeDownloadsCount" class="active-downloads-counter" style="display:none;">(0 actif)</span></h2> <h2 class="text-xl font-bold flex items-center gap-2">
<div class="header-actions"> Téléchargements
<button class="btn btn-sm btn-secondary" <span id="activeDownloadsCount" class="badge badge-primary badge-sm" style="display:none;">0 actif</span>
</h2>
<div class="flex gap-2">
<button class="btn btn-sm btn-ghost"
hx-post="/api/downloads/cleanup" hx-post="/api/downloads/cleanup"
hx-swap="none" hx-swap="none"
hx-confirm="Nettoyer tous les telechargements termines ?" hx-confirm="Nettoyer tous les telechargements termines ?"
hx-on::after-request="htmx.trigger('#downloads-container-inner', 'refresh')"> hx-on::after-request="htmx.trigger('#downloads-container-inner', 'refresh')">
<i class="fas fa-broom"></i> Nettoyer termines <i class="fas fa-broom"></i> Nettoyer termines
</button> </button>
<button class="btn btn-sm btn-danger" <button class="btn btn-sm btn-error"
hx-post="/api/downloads/cancel-all" hx-post="/api/downloads/cancel-all"
hx-swap="none" hx-swap="none"
hx-confirm="Annuler tous les telechargements actifs ?" hx-confirm="Annuler tous les telechargements actifs ?"
@@ -23,22 +26,9 @@
<div id="downloads-container-inner" <div id="downloads-container-inner"
hx-get="/api/downloads?html=1" hx-get="/api/downloads?html=1"
hx-trigger="load, refresh, every 3s" hx-trigger="load, refresh, every 3s"
hx-swap="innerHTML"> hx-swap="innerHTML"
<div class="loading-placeholder"> class="flex justify-center py-8 text-base-content/50">
<div class="spinner"></div> Chargement des telechargements... <span class="loading loading-spinner loading-lg"></span>
</div> <span class="ml-2">Chargement des telechargements...</span>
</div> </div>
</div> </div>
<style>
.section-container { margin-bottom: 40px; }
.active-downloads-counter {
font-size: 0.85rem;
font-weight: 600;
color: var(--primary);
background: rgba(241, 80, 37, 0.1);
padding: 2px 10px;
border-radius: 12px;
margin-left: 10px;
}
</style>
+76 -119
View File
@@ -1,130 +1,87 @@
<div class="episode-list-container section-container" x-data="{ view: 'grid' }"> <div class="card bg-base-200 border border-primary/30 mt-8" x-data="{ view: 'grid' }">
<div class="section-header"> <!-- Header -->
<div> <div class="card-body p-6">
<h2 style="border: none; padding: 0; margin-bottom: 5px;">{{ anime_title }}</h2> <div class="flex justify-between items-center">
<span class="badge">{{ episodes|length }} épisodes disponibles</span> <div class="flex items-center gap-3">
<h2 class="text-xl font-bold border-none p-0 m-0">{{ anime_title }}</h2>
<span class="badge badge-outline">{{ episodes|length }} épisodes disponibles</span>
</div>
<div class="flex gap-2">
<button class="btn btn-circle btn-sm btn-ghost" @click="view = 'grid'" :class="{ 'btn-primary': view === 'grid' }">
<i class="fas fa-th"></i>
</button>
<button class="btn btn-circle btn-sm btn-ghost" @click="view = 'list'" :class="{ 'btn-primary': view === 'list' }">
<i class="fas fa-list"></i>
</button>
<button class="btn btn-circle btn-sm btn-error" onclick="document.getElementById('player-container').innerHTML = ''">
<i class="fas fa-times"></i>
</button>
</div>
</div> </div>
<div class="header-actions" style="display: flex; gap: 10px;">
<button class="btn btn-icon" @click="view = 'grid'" :class="{ 'btn-primary': view === 'grid' }">
<i class="fas fa-th"></i>
</button>
<button class="btn btn-icon" @click="view = 'list'" :class="{ 'btn-primary': view === 'list' }">
<i class="fas fa-list"></i>
</button>
<button class="btn btn-icon danger" onclick="document.getElementById('player-container').innerHTML = ''">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<!-- Zone d'affichage du player vidéo (Placé en haut pour une meilleure visibilité lors de la sélection) --> <!-- Video player display area -->
<div id="video-player-display"></div> <div id="video-player-display" x-ref="playerArea"></div>
<div class="episodes-content" :class="'view-' + view" style="margin-top: 25px;"> <!-- Episodes content -->
{% if episodes %} {% if episodes %}
{% for ep in episodes %} <!-- Grid View -->
<div class="episode-item"> <div x-show="view === 'grid'" x-transition class="mt-6">
<div class="ep-number">EP {{ ep.episode_number or loop.index }}</div> <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 lg:grid-cols-7 gap-3">
<div class="ep-title" title="{{ ep.title or 'Épisode ' ~ (ep.episode_number or loop.index) }}"> {% for ep in episodes %}
{{ ep.title or 'Épisode ' ~ (ep.episode_number or loop.index) }} <div class="bg-base-300 rounded-lg p-4 text-center hover:bg-base-100 transition-colors border border-transparent hover:border-primary flex flex-col gap-2">
</div> <div class="text-primary font-bold text-xl">EP {{ ep.episode_number or loop.index }}</div>
<div class="ep-actions"> <button class="btn btn-xs btn-primary w-full"
<button class="btn btn-primary btn-small" hx-get="/api/player/embed?url={{ ep.url | urlencode }}"
hx-get="/api/player/embed?url={{ ep.url | urlencode }}" hx-target="#video-player-display"
hx-target="#video-player-display" hx-swap="innerHTML"
hx-swap="innerHTML" onclick="document.getElementById('video-player-display').scrollIntoView({behavior: 'smooth'})">
onclick="document.getElementById('video-player-display').scrollIntoView({behavior: 'smooth'})"> <i class="fas fa-play"></i> Regarder
<i class="fas fa-play"></i> Regarder </button>
</button> <button class="btn btn-xs btn-ghost w-full"
<button class="btn btn-secondary btn-icon btn-small" hx-post="/api/anime/download?url={{ ep.url | urlencode }}"
hx-post="/api/anime/download?url={{ ep.url | urlencode }}" hx-swap="none"
hx-swap="none" title="Télécharger cet épisode">
title="Télécharger cet épisode"> <i class="fas fa-download"></i> Télécharger
<i class="fas fa-download"></i> </button>
</button> </div>
</div> {% endfor %}
</div> </div>
{% endfor %} </div>
<!-- List View -->
<div x-show="view === 'list'" x-transition class="mt-6">
<div class="flex flex-col gap-2">
{% for ep in episodes %}
<div class="flex items-center gap-4 bg-base-300 rounded-lg px-4 py-3 hover:bg-base-100 transition-colors">
<span class="font-bold text-primary w-12 shrink-0">EP {{ ep.episode_number or loop.index }}</span>
<span class="flex-1 truncate text-base-content/80 font-medium"
title="{{ ep.title or 'Épisode ' ~ (ep.episode_number or loop.index) }}">
{{ ep.title or 'Épisode ' ~ (ep.episode_number or loop.index) }}
</span>
<div class="flex gap-2 shrink-0">
<button class="btn btn-xs btn-primary"
hx-get="/api/player/embed?url={{ ep.url | urlencode }}"
hx-target="#video-player-display"
hx-swap="innerHTML"
onclick="document.getElementById('video-player-display').scrollIntoView({behavior: 'smooth'})">
<i class="fas fa-play"></i> Regarder
</button>
<button class="btn btn-xs btn-ghost"
hx-post="/api/anime/download?url={{ ep.url | urlencode }}"
hx-swap="none"
title="Télécharger cet épisode">
<i class="fas fa-download"></i>
</button>
</div>
</div>
{% endfor %}
</div>
</div>
{% else %} {% else %}
<div class="no-results"> <div class="text-center py-12 text-base-content/40">
<i class="fas fa-exclamation-circle"></i> <i class="fas fa-exclamation-circle text-3xl mb-3 block"></i>
<p>Aucun épisode trouvé pour cette source.</p> <p>Aucun épisode trouvé pour cette source.</p>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<style>
.episode-list-container {
margin-top: 30px;
background: var(--bg-card);
border-radius: var(--card-radius);
padding: 30px;
border: 1px solid var(--secondary);
animation: fadeIn 0.3s ease-out;
}
.episodes-content.view-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 15px;
}
.view-grid .episode-item {
background: var(--bg-elevated);
padding: 20px 15px;
border-radius: 4px;
text-align: center;
transition: var(--transition);
border: 1px solid var(--secondary);
display: flex;
flex-direction: column;
gap: 12px;
}
.view-grid .episode-item:hover {
background: var(--text-dim);
border-color: var(--primary);
}
.view-grid .ep-title { display: none; }
.view-grid .ep-number { font-weight: 800; font-size: 1.2rem; color: var(--primary); }
.view-grid .ep-actions { display: flex; flex-direction: column; gap: 8px; }
.view-grid .ep-actions .btn { width: 100%; }
.episodes-content.view-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.view-list .episode-item {
display: flex;
align-items: center;
gap: 20px;
background: var(--bg-elevated);
padding: 12px 20px;
border-radius: 4px;
border: 1px solid var(--secondary);
transition: var(--transition);
}
.view-list .episode-item:hover {
background: var(--text-dim);
border-color: var(--primary);
}
.view-list .ep-number { font-weight: 800; width: 60px; color: var(--primary); }
.view-list .ep-title { flex: 1; color: var(--text-main); font-weight: 500; }
.view-list .ep-actions { display: flex; gap: 10px; }
#video-player-display:not(:empty) {
margin: 20px 0 30px 0;
padding: 25px;
background: #000;
border-radius: 4px;
border: 1px solid var(--primary);
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
</style>
+36 -23
View File
@@ -1,36 +1,49 @@
<div id="tab-home" class="tab-content" x-show="activeTab === 'home'"> <!-- Home Tab -->
<div id="tab-home" x-show="activeTab === 'home'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
<div class="section-container"> <!-- Recommendations Section -->
<div class="section-header"> <div class="mb-8">
<h2><i class="fa-solid fa-bullseye"></i> Recommandé pour vous</h2> <div class="flex justify-between items-center mb-4">
<button class="btn btn-secondary btn-small" <h2 class="text-xl font-bold">
hx-get="/api/recommendations" <i class="fa-solid fa-bullseye text-primary"></i> Recommandé pour vous
</h2>
<button class="btn btn-sm btn-ghost gap-1.5"
hx-get="/api/recommendations"
hx-target="#recommendationsList"> hx-target="#recommendationsList">
<i class="fas fa-sync-alt"></i> Actualiser <i class="fa-solid fa-arrows-rotate text-xs"></i> Actualiser
</button> </button>
</div> </div>
<div id="recommendationsList" <div id="recommendationsList"
hx-get="/api/recommendations" hx-get="/api/recommendations"
hx-trigger="load delay:100ms" hx-trigger="load delay:100ms">
class="home-row"> <div class="flex gap-4 overflow-x-auto pb-4">
<div class="loading-placeholder"><div class="spinner"></div></div> <div class="flex items-center justify-center py-8 w-full">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
</div>
</div> </div>
</div> </div>
<div class="section-container"> <!-- Latest Releases Section -->
<div class="section-header"> <div>
<h2><i class="fa-solid fa-fire"></i> Dernières sorties</h2> <div class="flex justify-between items-center mb-4">
<button class="btn btn-secondary btn-small" <h2 class="text-xl font-bold">
hx-get="/api/releases/latest" <i class="fa-solid fa-fire text-error"></i> Dernières sorties
</h2>
<button class="btn btn-sm btn-ghost gap-1.5"
hx-get="/api/releases/latest"
hx-target="#releasesList"> hx-target="#releasesList">
<i class="fas fa-sync-alt"></i> Actualiser <i class="fa-solid fa-arrows-rotate text-xs"></i> Actualiser
</button> </button>
</div> </div>
<div id="releasesList" <div id="releasesList"
hx-get="/api/releases/latest" hx-get="/api/releases/latest"
hx-trigger="load delay:300ms" hx-trigger="load delay:300ms">
class="home-row"> <div class="flex gap-4 overflow-x-auto pb-4">
<div class="loading-placeholder"><div class="spinner"></div></div> <div class="flex items-center justify-center py-8 w-full">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
+6 -3
View File
@@ -1,4 +1,7 @@
<div class="login-prompt" style="text-align: center; padding: 40px 20px;"> <div class="flex flex-col items-center justify-center py-16 text-base-content/50">
<i class="fa-solid fa-lock" style="font-size: 2rem; color: #FF9F1C; margin-bottom: 15px;"></i> <i class="fa-solid fa-lock text-4xl text-primary mb-4"></i>
<p style="color: #8a8f98; font-size: 0.95rem;">Connectez-vous pour accéder à cette section.</p> <p class="text-base">Connectez-vous pour accéder à cette section.</p>
<a href="/login" class="btn btn-sm btn-primary mt-4 gap-2">
<i class="fa-solid fa-right-to-bracket"></i> Se connecter
</a>
</div> </div>
+9 -48
View File
@@ -1,4 +1,4 @@
<div class="player-embed-box" <div class="bg-black rounded-lg border border-base-300 overflow-hidden my-5 p-4"
x-data="{ x-data="{
initPlayer() { initPlayer() {
if (!this.$refs.player) return; if (!this.$refs.player) return;
@@ -12,66 +12,27 @@
x-init="initPlayer()"> x-init="initPlayer()">
{% if is_iframe %} {% if is_iframe %}
<div class="iframe-container"> <div class="relative w-full" style="padding-bottom: 56.25%; height: 0; overflow: hidden;">
<iframe src="{{ video_url }}" <iframe src="{{ video_url }}"
allowfullscreen allowfullscreen
webkitallowfullscreen webkitallowfullscreen
mozallowfullscreen></iframe> mozallowfullscreen
class="absolute top-0 left-0 w-full h-full border-0 rounded-t-lg"></iframe>
</div> </div>
<div class="player-info-hint"> <div class="text-xs text-base-content/40 mt-3 text-center">
<i class="fa-solid fa-lightbulb"></i> Lecteur externe utilisé. Les contrôles dépendent de l'hébergeur. <i class="fa-solid fa-lightbulb"></i> Lecteur externe utilisé. Les contrôles dépendent de l'hébergeur.
</div> </div>
{% else %} {% else %}
<div class="video-wrapper"> <div class="w-full rounded-lg overflow-hidden">
<video x-ref="player" playsinline controls preload="metadata"> <video x-ref="player" playsinline controls preload="metadata" class="w-full rounded-lg">
<source src="{{ video_url }}" type="video/mp4"> <source src="{{ video_url }}" type="video/mp4">
</video> </video>
</div> </div>
{% endif %} {% endif %}
<div class="player-footer-actions"> <div class="flex justify-center mt-4">
<a href="{{ video_url }}" class="btn btn-sm btn-secondary" target="_blank"> <a href="{{ video_url }}" class="btn btn-sm btn-ghost" target="_blank">
<i class="fas fa-external-link-alt"></i> Ouvrir sur l'hébergeur <i class="fas fa-external-link-alt"></i> Ouvrir sur l'hébergeur
</a> </a>
</div> </div>
</div> </div>
<style>
.player-embed-box {
margin: 20px 0;
padding: 15px;
background: #000;
border-radius: 4px;
border: 1px solid #2a2d32;
}
.iframe-container {
position: relative;
padding-bottom: 56.25%; /* 16:9 Aspect Ratio */
height: 0;
overflow: hidden;
}
.iframe-container iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
}
.video-wrapper {
max-width: 100%;
border-radius: 8px;
overflow: hidden;
}
.player-info-hint {
font-size: 0.8rem;
color: var(--text-dim);
margin-top: 10px;
text-align: center;
}
.player-footer-actions {
margin-top: 15px;
display: flex;
justify-content: center;
}
</style>
+13 -5
View File
@@ -1,11 +1,19 @@
{% from "components/anime_card.html" import anime_card %} {% from "components/anime_card.html" import anime_card %}
{% from "components/series_card.html" import series_card %}
{% if recommendations %} {% if recommendations %}
{% for anime in recommendations %} <div class="flex gap-4 overflow-x-auto pb-4">
{{ anime_card(anime) }} {% for item in recommendations %}
{% if item.get('content_type') == 'series' %}
{{ series_card(item) }}
{% else %}
{{ anime_card(item) }}
{% endif %}
{% endfor %} {% endfor %}
</div>
{% else %} {% else %}
<div class="empty-state"> <div class="flex flex-col items-center justify-center py-12 text-base-content/40">
<p>Aucune recommandation pour le moment.</p> <i class="fa-regular fa-face-meh text-3xl mb-2"></i>
</div> <p class="text-sm">Aucune recommandation pour le moment.</p>
</div>
{% endif %} {% endif %}
+13 -5
View File
@@ -1,11 +1,19 @@
{% from "components/anime_card.html" import anime_card %} {% from "components/anime_card.html" import anime_card %}
{% from "components/series_card.html" import series_card %}
{% if releases %} {% if releases %}
{% for anime in releases %} <div class="flex gap-4 overflow-x-auto pb-4">
{{ anime_card(anime) }} {% for item in releases %}
{% if item.get('content_type') == 'series' %}
{{ series_card(item) }}
{% else %}
{{ anime_card(item) }}
{% endif %}
{% endfor %} {% endfor %}
</div>
{% else %} {% else %}
<div class="empty-state"> <div class="flex flex-col items-center justify-center py-12 text-base-content/40">
<p>Aucune sortie récente trouvée.</p> <i class="fa-regular fa-face-meh text-3xl mb-2"></i>
</div> <p class="text-sm">Aucune sortie récente trouvée.</p>
</div>
{% endif %} {% endif %}
+13 -8
View File
@@ -1,17 +1,22 @@
{% macro series_card(series) %} {% macro series_card(series) %}
<div class="hc" <div class="card card-compact bg-base-200 shadow-lg hover:shadow-xl transition-all cursor-pointer w-40 shrink-0 group"
@click="activeTab = 'series'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'series' } })); $nextTick(() => { const input = document.getElementById('seriesSearchInput'); if (input) { input.value = '{{ series.title | e }}'; htmx.trigger(input, 'keyup'); } });"> @click="activeTab = 'series'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'series' } })); $nextTick(() => { const input = document.getElementById('seriesSearchInput'); if (input) { input.value = '{{ series.title | e }}'; htmx.trigger(input, 'keyup'); } });">
<div class="hc-poster"> <figure class="relative overflow-hidden rounded-t-lg aspect-[2/3]">
<img src="{{ series.cover_image or 'https://placehold.co/400x600/202327/FF9F1C?text=No+Image' }}" alt="{{ series.title }}" loading="lazy" referrerpolicy="no-referrer" <img src="{{ series.cover_image or 'https://placehold.co/400x600/202327/FF9F1C?text=No+Image' }}" alt="{{ series.title }}" loading="lazy" referrerpolicy="no-referrer"
class="w-full h-full object-cover"
onerror="this.src='https://placehold.co/400x600/202327/FF9F1C?text=Error'; this.onerror=null;"> onerror="this.src='https://placehold.co/400x600/202327/FF9F1C?text=Error'; this.onerror=null;">
{% if series.lang %} {% if series.lang %}
<span class="hc-rating" style="text-transform: uppercase;">{{ series.lang }}</span> <span class="badge badge-secondary badge-sm absolute top-2 left-2" style="text-transform: uppercase;">{{ series.lang }}</span>
{% endif %} {% endif %}
<span class="hc-play"><i class="fas fa-search"></i></span> <div class="absolute inset-0 bg-secondary/20 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
</div> <div class="btn btn-circle btn-sm bg-secondary/80 border-secondary text-secondary-content">
<div class="hc-info"> <i class="fa-solid fa-magnifying-glass"></i>
<span class="hc-src">{{ series.provider_id or 'FS7' }}</span> </div>
<span class="hc-title">{{ series.title }}</span> </div>
</figure>
<div class="card-body p-3">
<span class="badge badge-outline badge-xs">{{ series.provider_id or 'FS7' }}</span>
<p class="text-xs font-semibold truncate mt-1">{{ series.title }}</p>
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}
@@ -1,11 +1,14 @@
{% from "components/series_card.html" import series_card %} {% from "components/series_card.html" import series_card %}
{% if releases %} {% if releases %}
{% for series in releases %} <div class="flex gap-4 overflow-x-auto pb-4">
{{ series_card(series) }} {% for item in releases %}
{{ series_card(item) }}
{% endfor %} {% endfor %}
</div>
{% else %} {% else %}
<div class="empty-state"> <div class="flex flex-col items-center justify-center py-12 text-base-content/40">
<p>Aucune sortie recente trouvee.</p> <i class="fa-regular fa-face-meh text-3xl mb-2"></i>
</div> <p class="text-sm">Aucune série récente trouvée.</p>
</div>
{% endif %} {% endif %}
+72 -94
View File
@@ -1,4 +1,3 @@
{% set accent = "#FF9F1C" %}
{% set default_lang = settings.default_lang if settings else 'vf' %} {% set default_lang = settings.default_lang if settings else 'vf' %}
{% set _groups = namespace(items={}) %} {% set _groups = namespace(items={}) %}
@@ -22,110 +21,89 @@
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
<div class="sr-list" x-data="{ openDropdown: null }"> <div class="flex flex-col gap-4" x-data="{ openDropdown: null }">
{% if _groups.items.values() | list | length > 0 %} {% if _groups.items.values() | list | length > 0 %}
{% for group in _groups.items.values() | list %} {% for group in _groups.items.values() | list %}
{% set first_url = group.providers[0].url %} {% set first_url = group.providers[0].url %}
<div class="sr-card" style="--sr-accent: {{ accent }};"> <div class="card bg-base-200 border border-base-300 hover:border-primary transition-colors">
<a class="sr-poster-link" href="{{ first_url }}" target="_blank" rel="noopener"> <div class="card-body p-5 flex-row gap-5">
<img class="sr-poster-img" src="{{ group.cover or 'https://placehold.co/240x360/29274c/7e52a0?text=No+Image' }}" <figure class="w-28 shrink-0">
alt="{{ group.title }}" loading="lazy" referrerpolicy="no-referrer" <a href="{{ first_url }}" target="_blank" rel="noopener">
onerror="this.src='https://placehold.co/240x360/29274c/7e52a0?text=Error'; this.onerror=null;"> <img src="{{ group.cover or 'https://placehold.co/240x360/29274c/7e52a0?text=No+Image' }}"
</a> alt="{{ group.title }}" loading="lazy" referrerpolicy="no-referrer"
<div class="sr-body"> class="rounded-lg w-full aspect-[2/3] object-cover"
<h3 class="sr-title">{{ group.title }}</h3> onerror="this.src='https://placehold.co/240x360/29274c/7e52a0?text=Error'; this.onerror=null;">
{% if group.synopsis %}
<p class="sr-synopsis">{{ group.synopsis }}</p>
{% endif %}
<div class="sr-providers">
{% for p in group.providers %}
<a class="sr-provider-badge" href="{{ p.url }}" target="_blank" rel="noopener">{{ p.id | upper }}</a>
{% endfor %}
</div>
<div class="sr-actions">
<a class="sr-btn sr-btn-watch" href="{{ first_url }}" target="_blank" rel="noopener">
<i class="fas fa-play"></i> Regarder
</a> </a>
<div class="sr-dropdown" @click.outside="openDropdown = null"> </figure>
<button class="sr-btn sr-btn-dl" @click.stop="openDropdown = (openDropdown === '{{ first_url | urlencode }}') ? null : '{{ first_url | urlencode }}'"> <div class="flex-1 min-w-0 flex flex-col gap-2">
<i class="fas fa-download"></i> Telecharger <i class="fas fa-chevron-down" style="font-size:0.6rem;margin-left:4px;"></i> <div class="flex items-baseline gap-3">
</button> <h3 class="card-title text-base truncate">{{ group.title }}</h3>
<div class="sr-dropdown-menu" x-show="openDropdown === '{{ first_url | urlencode }}'" x-transition> </div>
<button class="sr-dropdown-item"
hx-post="/api/anime/download-season?url={{ first_url | urlencode }}&lang={{ default_lang }}" {% if group.synopsis %}
hx-swap="none" <p class="text-sm text-base-content/60 line-clamp-3">{{ group.synopsis }}</p>
hx-on::after-request="openDropdown = null"> {% endif %}
<i class="fas fa-layer-group"></i> Saison complete
</button> <div class="flex flex-wrap gap-1.5">
<button class="sr-dropdown-item" {% for p in group.providers %}
hx-get="/api/anime/episodes?url={{ first_url | urlencode }}&lang={{ default_lang }}&html=1" <a href="{{ p.url }}" target="_blank" rel="noopener"
hx-target="#player-container" class="badge badge-primary badge-outline text-xs uppercase tracking-wider hover:bg-primary hover:text-primary-content hover:border-primary transition-colors cursor-pointer">
hx-swap="innerHTML" {{ p.id | upper }}
hx-on::after-request="openDropdown = null; document.getElementById('player-container').scrollIntoView({behavior: 'smooth'})"> </a>
<i class="fas fa-list-ol"></i> Choisir des episodes {% endfor %}
</button> </div>
</div>
<div class="flex flex-wrap gap-2 mt-1">
<a href="{{ first_url }}" target="_blank" rel="noopener"
class="btn btn-sm btn-primary">
<i class="fas fa-play"></i> Regarder
</a>
<div class="dropdown" @click.outside="openDropdown = null">
<div tabindex="0" role="button"
@click.stop="openDropdown = (openDropdown === '{{ first_url | urlencode }}') ? null : '{{ first_url | urlencode }}'">
<span class="btn btn-sm btn-secondary">
<i class="fas fa-download"></i> Telecharger <i class="fas fa-chevron-down text-[0.6rem] ml-1"></i>
</span>
</div>
<ul tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52 border border-base-300"
x-show="openDropdown === '{{ first_url | urlencode }}'"
x-transition>
<li>
<button class="flex items-center gap-2 text-sm"
hx-post="/api/anime/download-season?url={{ first_url | urlencode }}&lang={{ default_lang }}"
hx-swap="none"
hx-on::after-request="openDropdown = null">
<i class="fas fa-layer-group"></i> Saison complete
</button>
</li>
<li>
<button class="flex items-center gap-2 text-sm"
hx-get="/api/anime/episodes?url={{ first_url | urlencode }}&lang={{ default_lang }}&html=1"
hx-target="#player-container"
hx-swap="innerHTML"
hx-on::after-request="openDropdown = null; document.getElementById('player-container').scrollIntoView({behavior: 'smooth'})">
<i class="fas fa-list-ol"></i> Choisir des episodes
</button>
</li>
</ul>
</div>
<button class="btn btn-sm btn-accent btn-outline"
hx-post="/api/watchlist"
hx-vals='{"anime_url": "{{ first_url }}", "anime_title": "{{ group.title }}", "provider_id": "{{ group.providers[0].id }}", "lang": "{{ default_lang }}", "poster_image": "{{ group.cover }}"}'
hx-swap="none"
hx-on::after-request="if(event.detail.successful){this.innerHTML='<i class=\'fas fa-check\'></i> Suivi';this.disabled=true;this.classList.remove('btn-accent','btn-outline');this.classList.add('btn-accent','btn-ghost','pointer-events-none')}">
<i class="fas fa-plus"></i> Suivre
</button>
</div> </div>
<button class="sr-btn sr-btn-follow"
hx-post="/api/watchlist"
hx-vals='{"anime_url": "{{ first_url }}", "anime_title": "{{ group.title }}", "provider_id": "{{ group.providers[0].id }}", "lang": "{{ default_lang }}", "poster_image": "{{ group.cover }}"}'
hx-swap="none"
hx-on::after-request="if(event.detail.successful){this.innerHTML='<i class=\'fas fa-check\'></i> Suivi';this.disabled=true;this.classList.add('sr-btn-followed')}">
<i class="fas fa-plus"></i> Suivre
</button>
</div> </div>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
{% else %} {% else %}
<div class="sr-empty"> <div class="text-center py-20 text-base-content/40">
<i class="fas fa-search"></i> <i class="fas fa-search text-6xl mb-5 block opacity-20"></i>
<p>Aucune serie TV trouvee pour votre recherche.</p> <p>Aucune serie TV trouvee pour votre recherche.</p>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<style>
.sr-list { display: flex; flex-direction: column; gap: 16px; }
.sr-card {
display: flex; gap: 20px;
background: var(--bg-card); border-radius: var(--card-radius);
padding: 20px; border: 1px solid #2a2d32;"
transition: var(--transition);
}
.sr-card:hover { border-color: var(--sr-accent); }
.sr-poster-link { flex-shrink: 0; display: block; width: 120px; aspect-ratio: 2/3; border-radius: 4px; overflow: hidden; background: #000; }
.sr-poster-img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sr-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
.sr-title { font-size: 1.1rem; font-weight: 700; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sr-synopsis { font-size: 0.85rem; color: var(--text-dim); margin: 0; line-height: 1.5; }
.sr-providers { display: flex; flex-wrap: wrap; gap: 6px; }
.sr-provider-badge { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; padding: 4px 12px; border-radius: 20px; border: 1px solid var(--sr-accent); color: var(--sr-accent); background: transparent; cursor: pointer; transition: var(--transition); letter-spacing: 0.5px; text-decoration: none; }
.sr-provider-badge:hover { background: var(--sr-accent); color: #fff; }
.sr-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
.sr-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 16px; border-radius: 4px; font-size: 0.8rem; font-weight: 600; border: 1px solid #2a2d32; cursor: pointer; transition: var(--transition); text-decoration: none; background: transparent; color: var(--text-main); min-height: 34px; }
.sr-btn:hover { border-color: var(--text-main); background: var(--bg-card); }
.sr-btn-dl { border-color: var(--secondary); color: var(--secondary); }
.sr-btn-dl:hover { background: var(--secondary); color: #ffffff; }
.sr-btn-watch { border-color: var(--sr-accent); color: var(--sr-accent); }
.sr-btn-watch:hover { background: var(--sr-accent); color: #fff; }
.sr-btn-follow { border-color: var(--accent); color: var(--accent); }
.sr-btn-follow:hover { background: var(--accent); color: #fff; }
.sr-btn-followed { border-color: var(--accent); color: var(--accent); background: rgba(255,191,105,0.1); pointer-events: none; }
.sr-dropdown { position: relative; }
.sr-dropdown-menu { position: absolute; top: calc(100% + 6px); left: 0; min-width: 200px; background: var(--bg-card); border: 1px solid #2a2d32; border-radius: 4px; padding: 4px; z-index: 100; }
.sr-dropdown-item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 10px 12px; border: none; background: transparent; color: var(--text-main); font-size: 0.8rem; cursor: pointer; border-radius: 4px; transition: var(--transition); text-align: left; }
.sr-dropdown-item:hover { background: var(--bg-elevated); }
.sr-empty { text-align: center; padding: 100px 20px; color: var(--text-dim); }
.sr-empty i { font-size: 4rem; margin-bottom: 20px; display: block; opacity: 0.2; }
@media (max-width: 768px) {
.sr-card { flex-direction: column; align-items: center; text-align: center; gap: 12px; padding: 16px; }
.sr-poster-link { width: 160px; }
.sr-title { white-space: normal; text-overflow: initial; }
.sr-providers { justify-content: center; }
.sr-actions { justify-content: center; }
}
</style>
+242 -161
View File
@@ -1,202 +1,283 @@
<div class="settings-container section-container"> <div class="space-y-6">
<div class="section-header"> <!-- Section Title -->
<h2>Parametres</h2> <div>
<h2 class="text-2xl font-bold">Paramètres</h2>
</div> </div>
<!-- General Preferences --> <!-- General Preferences -->
<div class="settings-card card" style="margin-bottom: 30px; padding: 25px; background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid #2a2d32;"> <div class="card bg-base-200 border border-base-300">
<h3 style="margin-bottom: 20px; color: var(--primary);">General</h3> <div class="card-body">
<h3 class="card-title text-lg text-primary">
<form id="settings-form" class="settings-form"> <i class="fa-solid fa-sliders"></i> Général
<div class="form-group"> </h3>
<label for="default_lang">Langue par defaut</label>
<select name="default_lang" id="default_lang" class="btn btn-secondary btn-block" style="text-align: left; padding: 12px 15px;">
<option value="vostfr" {% if settings.default_lang == 'vostfr' %}selected{% endif %}>VOSTFR</option>
<option value="vf" {% if settings.default_lang == 'vf' %}selected{% endif %}>VF</option>
</select>
</div>
<div class="form-group" style="margin-top: 20px;"> <form id="settings-form" class="space-y-4">
<label for="theme">Theme</label> <!-- Language -->
<select name="theme" id="theme" class="btn btn-secondary btn-block" style="text-align: left; padding: 12px 15px;"> <div class="form-control w-full max-w-xs">
<option value="dark" {% if settings.theme == 'dark' %}selected{% endif %}>Sombre (Premium Dark)</option> <label class="label" for="default_lang">
<option value="light" {% if settings.theme == 'light' %}selected{% endif %}>Clair (Soon)</option> <span class="label-text font-semibold">Langue par défaut</span>
<option value="oled" {% if settings.theme == 'oled' %}selected{% endif %}>OLED (Noir absolu)</option> </label>
</select> <select name="default_lang" id="default_lang" class="select select-bordered w-full">
</div> <option value="vostfr" {% if settings.default_lang == 'vostfr' %}selected{% endif %}>VOSTFR</option>
<option value="vf" {% if settings.default_lang == 'vf' %}selected{% endif %}>VF</option>
<div class="form-group" style="margin-top: 20px;"> </select>
<label for="download_dir">Repertoire de telechargement</label>
<div style="display: flex; gap: 8px;">
<input type="text" name="download_dir" id="download_dir" value="{{ settings.download_dir }}"
class="btn btn-secondary" style="flex: 1; text-align: left; padding: 12px 15px;">
</div> </div>
<small style="color: var(--text-dim); font-size: 12px; margin-top: 5px; display: block;">
Repertoire ou les fichiers seront telecharges (defaut: downloads/)
</small>
</div>
<button type="submit" class="btn btn-primary" style="margin-top: 20px; width: 100%;" onclick="event.preventDefault(); saveSettings();"> <!-- Theme -->
<i class="fas fa-save"></i> Enregistrer les preferences <div class="form-control w-full max-w-xs">
</button> <label class="label" for="theme">
</form> <span class="label-text font-semibold">Thème</span>
</label>
<select name="theme" id="theme" class="select select-bordered w-full">
<option value="dark" {% if settings.theme == 'dark' %}selected{% endif %}>Sombre (Premium Dark)</option>
<option value="light" {% if settings.theme == 'light' %}selected{% endif %}>Clair (Soon)</option>
<option value="oled" {% if settings.theme == 'oled' %}selected{% endif %}>OLED (Noir absolu)</option>
</select>
</div>
<!-- Download Directory -->
<div class="form-control w-full">
<label class="label" for="download_dir">
<span class="label-text font-semibold">Répertoire de téléchargement</span>
</label>
<input
type="text"
name="download_dir"
id="download_dir"
value="{{ settings.download_dir }}"
class="input input-bordered w-full"
>
<label class="label">
<span class="label-text-alt text-base-content/50">Répertoire où les fichiers seront téléchargés (défaut: downloads/)</span>
</label>
</div>
<!-- Save Button -->
<button type="submit" class="btn btn-primary w-full" onclick="event.preventDefault(); saveSettings();">
<i class="fa-solid fa-save"></i> Enregistrer les préférences
</button>
</form>
</div>
</div> </div>
<!-- Content Filters --> <!-- Content Filters -->
<div class="settings-card card" style="margin-bottom: 30px; padding: 25px; background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid #2a2d32;"> <div class="card bg-base-200 border border-base-300">
<h3 style="margin-bottom: 20px; color: var(--primary);">Filtres de contenu</h3> <div class="card-body">
<h3 class="card-title text-lg text-primary">
<div class="form-group"> <i class="fa-solid fa-filter"></i> Filtres de contenu
<label for="recommendations_filter">Recommande pour vous : afficher</label> </h3>
<select name="recommendations_filter" id="recommendations_filter" class="btn btn-secondary btn-block" style="text-align: left; padding: 12px 15px;" onchange="saveFilter('recommendations_filter', this.value)">
<option value="all" {% if settings.recommendations_filter == 'all' %}selected{% endif %}>Les deux (Animes + Series)</option>
<option value="anime" {% if settings.recommendations_filter == 'anime' %}selected{% endif %}>Animes uniquement</option>
<option value="series" {% if settings.recommendations_filter == 'series' %}selected{% endif %}>Series uniquement</option>
</select>
</div>
<div class="form-group" style="margin-top: 15px;"> <div class="space-y-4">
<label for="releases_filter">Dernieres sorties : afficher</label> <!-- Recommendations Filter -->
<select name="releases_filter" id="releases_filter" class="btn btn-secondary btn-block" style="text-align: left; padding: 12px 15px;" onchange="saveFilter('releases_filter', this.value)"> <div class="form-control w-full max-w-xs">
<option value="all" {% if settings.releases_filter == 'all' %}selected{% endif %}>Les deux (Animes + Series)</option> <label class="label" for="recommendations_filter">
<option value="anime" {% if settings.releases_filter == 'anime' %}selected{% endif %}>Animes uniquement</option> <span class="label-text font-semibold">Recommandé pour vous : afficher</span>
<option value="series" {% if settings.releases_filter == 'series' %}selected{% endif %}>Series uniquement</option> </label>
</select> <select name="recommendations_filter" id="recommendations_filter" class="select select-bordered w-full" onchange="saveFilter('recommendations_filter', this.value)">
<option value="all" {% if settings.recommendations_filter == 'all' %}selected{% endif %}>Les deux (Animes + Séries)</option>
<option value="anime" {% if settings.recommendations_filter == 'anime' %}selected{% endif %}>Animes uniquement</option>
<option value="series" {% if settings.recommendations_filter == 'series' %}selected{% endif %}>Séries uniquement</option>
</select>
</div>
<!-- Releases Filter -->
<div class="form-control w-full max-w-xs">
<label class="label" for="releases_filter">
<span class="label-text font-semibold">Dernières sorties : afficher</span>
</label>
<select name="releases_filter" id="releases_filter" class="select select-bordered w-full" onchange="saveFilter('releases_filter', this.value)">
<option value="all" {% if settings.releases_filter == 'all' %}selected{% endif %}>Les deux (Animes + Séries)</option>
<option value="anime" {% if settings.releases_filter == 'anime' %}selected{% endif %}>Animes uniquement</option>
<option value="series" {% if settings.releases_filter == 'series' %}selected{% endif %}>Séries uniquement</option>
</select>
</div>
</div>
</div> </div>
</div> </div>
<!-- Categories --> <!-- Categories -->
<div class="settings-card card" style="margin-bottom: 30px; padding: 25px; background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid #2a2d32;"> <div class="card bg-base-200 border border-base-300">
<h3 style="margin-bottom: 20px; color: var(--primary);">Categories</h3> <div class="card-body">
<p style="color: var(--text-dim); font-size: 13px; margin-bottom: 15px;">Activez ou desactivez les categories. Au moins une doit rester active.</p> <h3 class="card-title text-lg text-primary">
<i class="fa-solid fa-layer-group"></i> Catégories
<div style="display: flex; gap: 15px; flex-wrap: wrap;"> </h3>
<label class="toggle-card" style="flex: 1; min-width: 200px; padding: 15px; background: var(--bg-elevated); border-radius: 4px; border: 1px solid var(--secondary); display: flex; align-items: center; justify-content: space-between; cursor: pointer;"> <p class="text-sm text-base-content/60 mb-4">Activez ou désactivez les catégories. Au moins une doit rester active.</p>
<div>
<div style="font-weight: 600; font-size: 1.1rem;">Animes</div> <div class="flex gap-4 flex-wrap">
<div style="font-size: 0.8rem; color: var(--text-dim);">Films et series anime</div> <!-- Anime Toggle -->
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4 bg-base-300 rounded-lg p-4 border border-base-content/10 flex-1 min-w-[200px]">
<div class="flex-1">
<span class="font-semibold text-base">Animes</span>
<p class="text-xs text-base-content/60">Films et séries animées</p>
</div>
<input
type="checkbox"
id="anime_enabled"
class="toggle toggle-primary"
{% if settings.anime_enabled %}checked{% endif %}
onchange="toggleCategory('anime_enabled', this.checked)"
>
</label>
</div> </div>
<input type="checkbox" id="anime_enabled" {% if settings.anime_enabled %}checked{% endif %} onchange="toggleCategory('anime_enabled', this.checked)" style="width: 20px; height: 20px; cursor: pointer; accent-color: var(--primary);">
</label> <!-- Series Toggle -->
<div class="form-control">
<label class="toggle-card" style="flex: 1; min-width: 200px; padding: 15px; background: var(--bg-elevated); border-radius: 4px; border: 1px solid var(--secondary); display: flex; align-items: center; justify-content: space-between; cursor: pointer;"> <label class="label cursor-pointer justify-start gap-4 bg-base-300 rounded-lg p-4 border border-base-content/10 flex-1 min-w-[200px]">
<div> <div class="flex-1">
<div style="font-weight: 600; font-size: 1.1rem;">Series TV</div> <span class="font-semibold text-base">Séries TV</span>
<div style="font-size: 0.8rem; color: var(--text-dim);">Series americaines et europeennes</div> <p class="text-xs text-base-content/60">Séries américaines et européennes</p>
</div>
<input
type="checkbox"
id="series_enabled"
class="toggle toggle-primary"
{% if settings.series_enabled %}checked{% endif %}
onchange="toggleCategory('series_enabled', this.checked)"
>
</label>
</div> </div>
<input type="checkbox" id="series_enabled" {% if settings.series_enabled %}checked{% endif %} onchange="toggleCategory('series_enabled', this.checked)" style="width: 20px; height: 20px; cursor: pointer; accent-color: var(--primary);"> </div>
</label>
</div> </div>
</div> </div>
<!-- Content Weight --> <!-- Content Weight -->
<div class="settings-card card" style="margin-bottom: 30px; padding: 25px; background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid #2a2d32;"> <div class="card bg-base-200 border border-base-300">
<h3 style="margin-bottom: 5px; color: var(--primary);">Equilibre du fil d'actualite</h3> <div class="card-body">
<p style="color: var(--text-dim); font-size: 13px; margin-bottom: 20px;"> <h3 class="card-title text-lg text-primary">
Definissez la proportion d'animes et de series affiches dans les recommandations et dernieres sorties. <i class="fa-solid fa-scale-balanced"></i> Équilibre du fil d'actualité
</p> </h3>
<p class="text-sm text-base-content/60 mb-4">
Définissez la proportion d'animes et de séries affichés dans les recommandations et dernières sorties.
</p>
<div class="form-group"> <!-- Weight Mode -->
<label for="content_weight_mode" style="font-weight: 600; margin-bottom: 10px; display: block;">Mode</label> <div class="form-control w-full max-w-xs mb-4">
<select name="content_weight_mode" id="content_weight_mode" class="btn btn-secondary btn-block" style="text-align: left; padding: 12px 15px;" onchange="onWeightModeChange(this.value)"> <label class="label" for="content_weight_mode">
<option value="auto" {% if settings.content_weight_mode == 'auto' %}selected{% endif %}>Automatique (basé sur vos telechargements)</option> <span class="label-text font-semibold">Mode</span>
<option value="manual" {% if settings.content_weight_mode == 'manual' %}selected{% endif %}>Manuel</option> </label>
</select> <select name="content_weight_mode" id="content_weight_mode" class="select select-bordered w-full" onchange="onWeightModeChange(this.value)">
</div> <option value="auto" {% if settings.content_weight_mode == 'auto' %}selected{% endif %}>Automatique (basé sur vos téléchargements)</option>
<option value="manual" {% if settings.content_weight_mode == 'manual' %}selected{% endif %}>Manuel</option>
<!-- Auto mode info --> </select>
<div id="weight-auto-info" style="margin-top: 15px; padding: 15px; background: var(--bg-elevated); border-radius: 4px; border: 1px solid var(--secondary); display: {% if settings.content_weight_mode == 'auto' %}block{% else %}none{% endif %};">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
<i class="fas fa-chart-pie" style="color: var(--primary);"></i>
<span style="font-weight: 600;">Analyse de vos telechargements</span>
</div> </div>
<div id="weight-auto-details" style="font-size: 14px; color: var(--text-dim);">
Chargement...
</div>
</div>
<!-- Manual mode controls --> <!-- Auto mode info -->
<div id="weight-manual-controls" style="margin-top: 15px; display: {% if settings.content_weight_mode == 'manual' %}block{% else %}none{% endif %};"> <div id="weight-auto-info" class="bg-base-300 rounded-lg p-4 border border-base-content/10 mb-4" {% if settings.content_weight_mode != 'auto' %}style="display:none;"{% endif %}>
<div style="display: flex; gap: 15px; align-items: center;"> <div class="flex items-center gap-2 mb-2">
<div style="flex: 1;"> <i class="fa-solid fa-chart-pie text-primary"></i>
<label for="content_weight_anime" style="font-weight: 600; font-size: 0.9rem; display: block; margin-bottom: 8px;"> <span class="font-semibold">Analyse de vos téléchargements</span>
<i class="fas fa-dragon" style="color: var(--primary);"></i> Poids Animes
</label>
<input type="range" id="content_weight_anime_range" min="0" max="5" step="1" value="{{ settings.content_weight_anime }}"
style="width: 100%; accent-color: var(--primary);"
oninput="document.getElementById('content_weight_anime').value = this.value; updateWeightPreview();">
<div style="display: flex; justify-content: space-between; font-size: 11px; color: var(--text-dim); margin-top: 2px;">
<span>0</span><span>1</span><span>2</span><span>3</span><span>4</span><span>5</span>
</div>
</div> </div>
<div style="flex: 1;"> <div id="weight-auto-details" class="text-sm text-base-content/60">
<label for="content_weight_series" style="font-weight: 600; font-size: 0.9rem; display: block; margin-bottom: 8px;"> Chargement...
<i class="fas fa-tv" style="color: #6CB4EE;"></i> Poids Series
</label>
<input type="range" id="content_weight_series_range" min="0" max="5" step="1" value="{{ settings.content_weight_series }}"
style="width: 100%; accent-color: #6CB4EE;"
oninput="document.getElementById('content_weight_series').value = this.value; updateWeightPreview();">
<div style="display: flex; justify-content: space-between; font-size: 11px; color: var(--text-dim); margin-top: 2px;">
<span>0</span><span>1</span><span>2</span><span>3</span><span>4</span><span>5</span>
</div>
</div> </div>
</div> </div>
<input type="hidden" id="content_weight_anime" value="{{ settings.content_weight_anime }}">
<input type="hidden" id="content_weight_series" value="{{ settings.content_weight_series }}"> <!-- Manual mode controls -->
<div id="weight-preview" style="margin-top: 15px; padding: 12px; background: var(--bg-elevated); border-radius: 4px; text-align: center; font-size: 14px;"> <div id="weight-manual-controls" {% if settings.content_weight_mode != 'manual' %}style="display:none;"{% endif %}>
<div class="flex gap-6 items-start flex-wrap">
<!-- Anime Weight -->
<div class="flex-1 min-w-[200px]">
<label class="label">
<span class="label-text font-semibold">
<i class="fa-solid fa-dragon text-primary"></i> Poids Animes
</span>
</label>
<input
type="range"
id="content_weight_anime_range"
min="0"
max="5"
step="1"
value="{{ settings.content_weight_anime }}"
class="range range-primary range-sm"
oninput="document.getElementById('content_weight_anime').value = this.value; updateWeightPreview();"
>
<div class="flex justify-between text-xs text-base-content/50 px-1 mt-1">
<span>0</span><span>1</span><span>2</span><span>3</span><span>4</span><span>5</span>
</div>
</div>
<!-- Series Weight -->
<div class="flex-1 min-w-[200px]">
<label class="label">
<span class="label-text font-semibold">
<i class="fa-solid fa-tv text-secondary"></i> Poids Séries
</span>
</label>
<input
type="range"
id="content_weight_series_range"
min="0"
max="5"
step="1"
value="{{ settings.content_weight_series }}"
class="range range-secondary range-sm"
oninput="document.getElementById('content_weight_series').value = this.value; updateWeightPreview();"
>
<div class="flex justify-between text-xs text-base-content/50 px-1 mt-1">
<span>0</span><span>1</span><span>2</span><span>3</span><span>4</span><span>5</span>
</div>
</div>
</div>
<input type="hidden" id="content_weight_anime" value="{{ settings.content_weight_anime }}">
<input type="hidden" id="content_weight_series" value="{{ settings.content_weight_series }}">
<!-- Weight Preview -->
<div id="weight-preview" class="bg-base-300 rounded-lg p-3 text-center text-sm mt-4"></div>
<button class="btn btn-primary w-full mt-4" onclick="saveManualWeights()">
<i class="fa-solid fa-scale-balanced"></i> Appliquer
</button>
</div> </div>
<button class="btn btn-primary" style="margin-top: 15px; width: 100%;" onclick="saveManualWeights()">
<i class="fas fa-balance-scale"></i> Appliquer
</button>
</div> </div>
</div> </div>
<!-- Providers Management --> <!-- Providers Management -->
<div class="settings-card card" style="padding: 25px; background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid #2a2d32;"> <div class="card bg-base-200 border border-base-300">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;"> <div class="card-body">
<h3 style="margin: 0; color: var(--primary);">Disponibilite des Fournisseurs</h3> <div class="flex justify-between items-center mb-4">
<button class="btn btn-secondary btn-small" hx-post="/api/providers/health/check" hx-swap="none"> <h3 class="card-title text-lg text-primary mb-0">
<i class="fas fa-sync-alt"></i> Forcer verification <i class="fa-solid fa-server"></i> Disponibilité des Fournisseurs
</button> </h3>
</div> <button class="btn btn-sm btn-ghost" hx-post="/api/providers/health/check" hx-swap="none">
<i class="fa-solid fa-arrows-rotate"></i> Forcer vérification
</button>
</div>
<div class="providers-settings-list" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px;"> <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
{% for provider in providers %} {% for provider in providers %}
<div class="provider-status-card" style="padding: 15px; background: var(--bg-elevated); border-radius: 4px; border: 1px solid var(--secondary); display: flex; align-items: center; justify-content: space-between;"> <div class="flex items-center justify-between bg-base-300 rounded-lg p-3 border border-base-content/10">
<div style="display: flex; align-items: center; gap: 12px;"> <div class="flex items-center gap-3">
<span style="font-size: 1.5rem;">{{ provider.icon }}</span> <span class="text-2xl">{{ provider.icon }}</span>
<div> <div>
<div style="font-weight: 600;">{{ provider.name }}</div> <div class="font-semibold text-sm">{{ provider.name }}</div>
<div style="font-size: 0.75rem; display: flex; align-items: center; gap: 5px;"> <div class="flex items-center gap-1.5">
<span class="status-dot" style="width: 8px; height: 8px; border-radius: 50%; background: {% if provider.status == 'up' %}var(--accent){% elif provider.status == 'down' %}var(--danger){% else %}var(--text-muted){% endif %};"></span> {% if provider.status == 'up' %}
<span style="color: {% if provider.status == 'up' %}var(--accent){% elif provider.status == 'down' %}var(--danger){% else %}var(--text-dim){% endif %}; text-transform: uppercase; font-weight: 800;"> <span class="badge badge-success badge-xs"></span>
{{ provider.status | upper }} <span class="text-xs font-bold text-success">UP</span>
</span> {% elif provider.status == 'down' %}
<span class="badge badge-error badge-xs"></span>
<span class="text-xs font-bold text-error">DOWN</span>
{% else %}
<span class="badge badge-ghost badge-xs"></span>
<span class="text-xs font-bold text-base-content/40">{{ provider.status | upper }}</span>
{% endif %}
</div>
</div> </div>
</div> </div>
</div> <button
class="btn btn-sm {% if provider.enabled %}btn-ghost{% else %}btn-primary{% endif %}"
<button class="btn {% if provider.enabled %}btn-secondary{% else %}btn-accent{% endif %} btn-sm"
hx-post="/api/settings/providers/{{ provider.id }}/toggle" hx-post="/api/settings/providers/{{ provider.id }}/toggle"
hx-swap="none" hx-swap="none"
hx-on::after-request="htmx.trigger('#tab-settings > div', 'refresh-settings')" hx-on::after-request="htmx.trigger('#tab-settings > div', 'refresh-settings')"
style="min-width: 100px;"> >
{% if provider.enabled %}Desactiver{% else %}Activer{% endif %} {% if provider.enabled %}Désactiver{% else %}Activer{% endif %}
</button> </button>
</div>
{% endfor %}
</div> </div>
{% endfor %}
</div> </div>
</div> </div>
</div> </div>
<style>
.settings-form label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: var(--text-dim);
}
.status-dot {
display: inline-block;
}
</style>
+30 -44
View File
@@ -1,59 +1,45 @@
<div id="toast-container" <!-- Toast notification container -->
class="toast-container" <div id="toast-container"
class="fixed top-4 right-4 z-[9999] flex flex-col gap-2 max-h-[80vh] overflow-hidden"
style="pointer-events: none;" style="pointer-events: none;"
x-data="{ toasts: [] }" x-data="{ toasts: [] }"
@show-toast.window="toasts.push({ id: Date.now(), message: $event.detail.message, type: $event.detail.type || 'info' }); setTimeout(() => { toasts = toasts.filter(t => t.id !== toasts[0].id) }, 5000)"> @show-toast.window="toasts.push({ id: Date.now(), message: $event.detail.message, type: $event.detail.type || 'info' }); setTimeout(() => { toasts = toasts.filter(t => t.id !== toasts[0].id) }, 5000)">
<template x-for="toast in toasts" :key="toast.id"> <template x-for="toast in toasts" :key="toast.id">
<div class="toast" <div class="alert shadow-lg max-w-sm animate-slide-in"
style="pointer-events: auto;" style="pointer-events: auto;"
:class="'toast-' + toast.type" :class="{
'alert-success': toast.type === 'success',
'alert-error': toast.type === 'error',
'alert-info': toast.type === 'info'
}"
x-show="true" x-show="true"
x-transition:enter="toast-enter" x-transition:enter="transition ease-out duration-300"
x-transition:leave="toast-leave"> x-transition:enter-start="opacity-0 translate-x-8"
<div class="toast-content"> x-transition:enter-end="opacity-100 translate-x-0"
<i class="fas" :class="{ x-transition:leave="transition ease-in duration-200"
'fa-check-circle': toast.type === 'success', x-transition:leave-start="opacity-100 translate-x-0"
'fa-exclamation-circle': toast.type === 'error', x-transition:leave-end="opacity-0 translate-x-8">
'fa-info-circle': toast.type === 'info' <i class="fa-solid"
}"></i> :class="{
<span x-text="toast.message"></span> 'fa-circle-check': toast.type === 'success',
</div> 'fa-circle-exclamation': toast.type === 'error',
<button class="toast-close" @click="toasts = toasts.filter(t => t.id !== toast.id)"> 'fa-circle-info': toast.type === 'info'
<i class="fas fa-times"></i> }"></i>
<span class="text-sm" x-text="toast.message"></span>
<button class="btn btn-ghost btn-xs" @click="toasts = toasts.filter(t => t.id !== toast.id)">
<i class="fa-solid fa-xmark"></i>
</button> </button>
</div> </div>
</template> </template>
</div> </div>
<style> <style>
.toast-container { @keyframes slide-in {
position: fixed; from { opacity: 0; transform: translateX(100%); }
top: 20px; to { opacity: 1; transform: translateX(0); }
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 10px;
pointer-events: none;
max-height: 80vh;
overflow: hidden;
} }
.toast { .animate-slide-in {
min-width: 250px; animation: slide-in 0.3s ease-out;
padding: 12px 16px;
border-radius: 4px;
background: var(--bg-card);
color: var(--text-main);
border: 1px solid var(--secondary);
display: flex;
justify-content: space-between;
align-items: center;
border-left: 4px solid var(--secondary);
} }
.toast-success { border-left-color: #2d936c; }
.toast-error { border-left-color: #e63946; }
.toast-info { border-left-color: #FFBF69; }
.toast-content { display: flex; align-items: center; gap: 10px; }
.toast-close { background: none; border: none; color: #aaa; cursor: pointer; }
</style> </style>
+119 -446
View File
@@ -1,489 +1,162 @@
{% set status_filter = request.query_params.get('status', 'all') %} {% set status_filter = request.query_params.get('status', 'all') %}
<div class="watchlist-container" x-data="{ currentFilter: '{{ status_filter }}' }"> <div class="flex flex-col gap-5" x-data="{ currentFilter: '{{ status_filter }}' }">
<!-- Filter Tabs --> <!-- Filter Tabs -->
<div class="filter-tabs"> <div class="tabs tabs-boxed bg-base-200 p-1">
<button class="filter-tab {{ 'active' if status_filter == 'all' or not status_filter else '' }}" <button class="tab {% if status_filter == 'all' or not status_filter %}tab-active{% endif %}"
hx-get="/api/watchlist?status=all" hx-get="/api/watchlist?status=all"
hx-target="#watchlist-items-container" hx-target="#watchlist-items-container"
hx-swap="outerHTML" hx-swap="outerHTML">
@click="$el.closest('.watchlist-container').querySelector('.filter-tab').forEach(t => t.classList.remove('active')); $el.classList.add('active');">
<i class="fas fa-list"></i> Tous <i class="fas fa-list"></i> Tous
</button> </button>
<button class="filter-tab {{ 'active' if status_filter == 'active' else '' }}" <button class="tab {% if status_filter == 'active' %}tab-active{% endif %}"
hx-get="/api/watchlist?status=active" hx-get="/api/watchlist?status=active"
hx-target="#watchlist-items-container" hx-target="#watchlist-items-container"
hx-swap="outerHTML" hx-swap="outerHTML">
@click="$el.closest('.watchlist-container').querySelector('.filter-tab').forEach(t => t.classList.remove('active')); $el.classList.add('active');">
<i class="fas fa-play"></i> Actifs <i class="fas fa-play"></i> Actifs
</button> </button>
<button class="filter-tab {{ 'active' if status_filter == 'paused' else '' }}" <button class="tab {% if status_filter == 'paused' %}tab-active{% endif %}"
hx-get="/api/watchlist?status=paused" hx-get="/api/watchlist?status=paused"
hx-target="#watchlist-items-container" hx-target="#watchlist-items-container"
hx-swap="outerHTML" hx-swap="outerHTML">
@click="$el.closest('.watchlist-container').querySelector('.filter-tab').forEach(t => t.classList.remove('active')); $el.classList.add('active');">
<i class="fas fa-pause"></i> En pause <i class="fas fa-pause"></i> En pause
</button> </button>
<button class="filter-tab {{ 'active' if status_filter == 'completed' else '' }}" <button class="tab {% if status_filter == 'completed' %}tab-active{% endif %}"
hx-get="/api/watchlist?status=completed" hx-get="/api/watchlist?status=completed"
hx-target="#watchlist-items-container" hx-target="#watchlist-items-container"
hx-swap="outerHTML" hx-swap="outerHTML">
@click="$el.closest('.watchlist-container').querySelector('.filter-tab').forEach(t => t.classList.remove('active')); $el.classList.add('active');">
<i class="fas fa-check"></i> Terminés <i class="fas fa-check"></i> Terminés
</button> </button>
</div> </div>
<!-- Watchlist Items Grid --> <!-- Watchlist Items Grid -->
{% if items and items | length > 0 %} {% if items and items | length > 0 %}
<div class="watchlist-grid"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for item in items %} {% for item in items %}
<div class="watchlist-card" id="watchlist-{{ item.id }}" data-item-id="{{ item.id }}"> <div class="card bg-base-200 border border-base-300 hover:border-primary transition-colors" id="watchlist-{{ item.id }}" data-item-id="{{ item.id }}">
<!-- Poster --> <div class="card-body p-4 flex-row gap-4">
<div class="watchlist-poster"> <!-- Poster -->
<img src="{{ item.poster_image or item.cover_image or '/static/img/no-poster.png' }}" <figure class="w-24 shrink-0 relative">
alt="{{ item.anime_title }}" <img src="{{ item.poster_image or item.cover_image or '/static/img/no-poster.png' }}"
onerror="this.src='/static/img/no-poster.png'"> alt="{{ item.anime_title }}"
<div class="poster-badge {{ item.status }}"> class="rounded-lg aspect-[2/3] object-cover w-full"
{% if item.status == 'active' %} onerror="this.src='/static/img/no-poster.png'">
<i class="fas fa-play"></i> Actif <!-- Status badge -->
{% elif item.status == 'paused' %} <span class="badge badge-sm absolute top-2 left-2
<i class="fas fa-pause"></i> En pause {% if item.status == 'active' %}badge-success
{% elif item.status == 'completed' %} {% elif item.status == 'paused' %}badge-warning
<i class="fas fa-check"></i> Terminé {% elif item.status == 'completed' %}badge-primary
{% else %} {% else %}badge-ghost{% endif %}">
<i class="fas fa-archive"></i> Archivé {% if item.status == 'active' %}
{% endif %} <i class="fas fa-play"></i> Actif
</div> {% elif item.status == 'paused' %}
{% if item.auto_download %} <i class="fas fa-pause"></i> Pause
<div class="auto-download-badge"> {% elif item.status == 'completed' %}
<i class="fas fa-magic"></i> Auto <i class="fas fa-check"></i> Terminé
</div> {% else %}
{% endif %} <i class="fas fa-archive"></i> Archivé
</div>
<!-- Content -->
<div class="watchlist-content">
<h3 class="watchlist-title">{{ item.anime_title }}</h3>
<div class="watchlist-meta">
<span class="meta-provider">
<i class="fas fa-tv"></i> {{ item.provider_id | upper }}
</span>
<span class="meta-lang">{{ item.lang | upper }}</span>
{% if item.quality_preference and item.quality_preference != 'auto' %}
<span class="meta-quality">{{ item.quality_preference }}</span>
{% endif %}
</div>
{% if item.synopsis %}
<p class="watchlist-synopsis">{{ item.synopsis | truncate(150) }}</p>
{% endif %}
<div class="watchlist-stats">
<span class="stat">
<i class="fas fa-download"></i>
Ép. {{ item.last_episode_downloaded }}
{% if item.total_episodes %}
/ {{ item.total_episodes }}
{% endif %} {% endif %}
</span> </span>
{% if item.added_at %} <!-- Auto-download badge -->
<span class="stat" title="Ajouté le {{ item.added_at.strftime('%d/%m/%Y') }}"> {% if item.auto_download %}
<i class="fas fa-calendar"></i> <span class="badge badge-primary badge-sm absolute bottom-2 left-2">
{{ item.added_at.strftime('%d/%m/%Y') }} <i class="fas fa-magic"></i> Auto
</span> </span>
{% endif %} {% endif %}
</div> </figure>
<!-- Actions --> <!-- Content -->
<div class="watchlist-actions"> <div class="flex-1 min-w-0 flex flex-col gap-1.5">
<!-- Pause/Resume Toggle --> <h3 class="font-bold text-sm truncate m-0">{{ item.anime_title }}</h3>
{% if item.status == 'active' %}
<button class="action-btn btn-pause" <!-- Meta badges -->
hx-put="/api/watchlist/{{ item.id }}" <div class="flex flex-wrap gap-1.5 text-[0.7rem]">
hx-vals='{"status": "paused"}' <span class="badge badge-outline badge-sm">
hx-swap="none" <i class="fas fa-tv"></i> {{ item.provider_id | upper }}
hx-on::after-request="htmx.trigger('#watchlist-items-container', 'refresh');" </span>
title="Mettre en pause"> <span class="badge badge-outline badge-sm badge-ghost">{{ item.lang | upper }}</span>
<i class="fas fa-pause"></i> {% if item.quality_preference and item.quality_preference != 'auto' %}
</button> <span class="badge badge-outline badge-sm badge-success">{{ item.quality_preference }}</span>
{% elif item.status == 'paused' %} {% endif %}
<button class="action-btn btn-resume" </div>
hx-put="/api/watchlist/{{ item.id }}"
hx-vals='{"status": "active"}' <!-- Synopsis -->
hx-swap="none" {% if item.synopsis %}
hx-on::after-request="htmx.trigger('#watchlist-items-container', 'refresh');" <p class="text-xs text-base-content/50 m-0 line-clamp-3">{{ item.synopsis | truncate(150) }}</p>
title="Reprendre">
<i class="fas fa-play"></i>
</button>
{% endif %} {% endif %}
<!-- Mark as completed --> <!-- Stats -->
{% if item.status not in ['completed', 'archived'] %} <div class="flex flex-wrap gap-3 text-[0.7rem] text-base-content/50">
<button class="action-btn btn-complete" <span class="flex items-center gap-1">
hx-put="/api/watchlist/{{ item.id }}" <i class="fas fa-download"></i>
hx-vals='{"status": "completed"}' Ép. {{ item.last_episode_downloaded }}
hx-swap="none" {% if item.total_episodes %}/ {{ item.total_episodes }}{% endif %}
hx-on::after-request="htmx.trigger('#watchlist-items-container', 'refresh');" </span>
title="Marquer comme terminé"> {% if item.added_at %}
<i class="fas fa-check"></i> <span class="flex items-center gap-1" title="Ajouté le {{ item.added_at.strftime('%d/%m/%Y') }}">
</button> <i class="fas fa-calendar"></i>
{% endif %} {{ item.added_at.strftime('%d/%m/%Y') }}
</span>
{% endif %}
</div>
<!-- Delete --> <!-- Actions -->
<button class="action-btn btn-delete" <div class="flex gap-1 mt-auto pt-2 border-t border-base-300">
hx-delete="/api/watchlist/{{ item.id }}" <!-- Pause/Resume Toggle -->
hx-target="#watchlist-{{ item.id }}" {% if item.status == 'active' %}
hx-swap="outerHTML" <button class="btn btn-circle btn-sm btn-warning"
hx-confirm="Supprimer '{{ item.anime_title }}' de votre watchlist ?" hx-put="/api/watchlist/{{ item.id }}"
title="Supprimer"> hx-vals='{"status": "paused"}'
<i class="fas fa-trash"></i> hx-swap="none"
</button> hx-on::after-request="htmx.trigger('#watchlist-items-container', 'refresh');"
title="Mettre en pause">
<i class="fas fa-pause"></i>
</button>
{% elif item.status == 'paused' %}
<button class="btn btn-circle btn-sm btn-success"
hx-put="/api/watchlist/{{ item.id }}"
hx-vals='{"status": "active"}'
hx-swap="none"
hx-on::after-request="htmx.trigger('#watchlist-items-container', 'refresh');"
title="Reprendre">
<i class="fas fa-play"></i>
</button>
{% endif %}
<!-- Mark as completed -->
{% if item.status not in ['completed', 'archived'] %}
<button class="btn btn-circle btn-sm btn-ghost"
hx-put="/api/watchlist/{{ item.id }}"
hx-vals='{"status": "completed"}'
hx-swap="none"
hx-on::after-request="htmx.trigger('#watchlist-items-container', 'refresh');"
title="Marquer comme terminé">
<i class="fas fa-check"></i>
</button>
{% endif %}
<!-- Delete -->
<button class="btn btn-circle btn-sm btn-error"
hx-delete="/api/watchlist/{{ item.id }}"
hx-target="#watchlist-{{ item.id }}"
hx-swap="outerHTML"
hx-confirm="Supprimer '{{ item.anime_title }}' de votre watchlist ?"
title="Supprimer">
<i class="fas fa-trash"></i>
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<div class="watchlist-empty"> <div class="text-center py-16 bg-base-200 rounded-box border border-dashed border-base-300">
<i class="fas fa-inbox"></i> <i class="fas fa-inbox text-6xl text-base-content/20 mb-5 block"></i>
<h3>Votre watchlist est vide</h3> <h3 class="text-lg font-bold mb-2 text-base-content">Votre watchlist est vide</h3>
<p>Ajoutez des animes ou séries depuis l'onglet Recherche pour commencer à suivre leurs sorties.</p> <p class="text-base-content/50 mb-6">Ajoutez des animes ou séries depuis l'onglet Recherche pour commencer à suivre leurs sorties.</p>
<button class="btn btn-primary" @click="$dispatch('set-tab', {tab: 'anime'})"> <button class="btn btn-primary" @click="$dispatch('set-tab', {tab: 'anime'})">
<i class="fas fa-search"></i> Rechercher des animes <i class="fas fa-search"></i> Rechercher des animes
</button> </button>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<style>
/* Container */
.watchlist-container {
display: flex;
flex-direction: column;
gap: 20px;
}
/* Filter Tabs */
.filter-tabs {
display: flex;
gap: 8px;
border-bottom: 1px solid #2a2d32;
padding-bottom: 12px;
margin-bottom: 8px;
}
.filter-tab {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
border: none;
background: transparent;
color: var(--text-dim);
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
border-radius: var(--input-radius);
transition: var(--transition);
}
.filter-tab:hover {
background: var(--bg-elevated);
color: var(--text-main);
}
.filter-tab.active {
background: var(--primary);
color: var(--bg-dark);
}
/* Grid */
.watchlist-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
}
/* Card */
.watchlist-card {
display: flex;
gap: 16px;
background: var(--bg-card);
border-radius: var(--card-radius);
padding: 16px;
border: 1px solid #2a2d32;
transition: var(--transition);
}
.watchlist-card:hover {
border-color: var(--primary);
}
/* Poster */
.watchlist-poster {
position: relative;
flex-shrink: 0;
width: 100px;
aspect-ratio: 2/3;
border-radius: 8px;
overflow: hidden;
background: var(--bg-dark);
}
.watchlist-poster img {
width: 100%;
height: 100%;
object-fit: cover;
}
.poster-badge {
position: absolute;
top: 8px;
left: 8px;
padding: 4px 10px;
border-radius: 20px;
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 4px;
}
.poster-badge.active {
background: #2d936c;
color: #fff;
}
.poster-badge.paused {
background: #f4a261;
color: #15171A;
}
.poster-badge.completed {
background: #FF9F1C;
color: #15171A;
}
.poster-badge.archived {
background: rgba(206, 208, 206, 0.2);
color: var(--text-dim);
}
.auto-download-badge {
position: absolute;
bottom: 8px;
left: 8px;
padding: 4px 10px;
border-radius: 20px;
background: #FF9F1C;
color: var(--bg-dark);
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 4px;
}
/* Content */
.watchlist-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.watchlist-title {
font-size: 1rem;
font-weight: 700;
margin: 0;
color: var(--text-main);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.watchlist-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
font-size: 0.75rem;
}
.meta-provider,
.meta-lang,
.meta-quality {
padding: 3px 10px;
border-radius: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.meta-provider {
background: rgba(255, 191, 105, 0.1);
color: var(--primary);
border: 1px solid rgba(255, 191, 105, 0.3);
}
.meta-lang {
background: rgba(206, 208, 206, 0.3);
color: var(--text-dim);
border: 1px solid var(--text-dim);
}
.meta-quality {
background: rgba(45, 147, 108, 0.1);
color: var(--success);
border: 1px solid rgba(45, 147, 108, 0.3);
}
.watchlist-synopsis {
font-size: 0.8rem;
color: var(--text-dim);
margin: 0;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.watchlist-stats {
display: flex;
flex-wrap: wrap;
gap: 12px;
font-size: 0.75rem;
color: var(--text-dim);
}
.stat {
display: flex;
align-items: center;
gap: 4px;
}
/* Actions */
.watchlist-actions {
display: flex;
gap: 6px;
margin-top: auto;
padding-top: 8px;
border-top: 1px solid #2a2d32;
}
.action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 6px;
font-size: 0.85rem;
cursor: pointer;
transition: var(--transition);
background: var(--bg-elevated);
color: var(--text-dim);
}
.action-btn:hover {
background: var(--secondary);
color: var(--text-main);
}
.btn-pause {
color: #ffc107;
}
.btn-pause:hover {
background: rgba(255, 193, 7, 0.15);
}
.btn-resume {
color: var(--accent);
}
.btn-resume:hover {
background: rgba(0, 255, 136, 0.15);
}
.btn-complete {
color: #FFBF69;
}
.btn-complete:hover {
background: rgba(255, 191, 105, 0.15);
}
.btn-delete {
color: #f44336;
}
.btn-delete:hover {
background: rgba(244, 67, 54, 0.15);
}
/* Empty State */
.watchlist-empty {
text-align: center;
padding: 80px 40px;
background: var(--bg-card);
border-radius: var(--card-radius);
border: 1px dashed #2a2d32;
}
.watchlist-empty i {
font-size: 4rem;
color: var(--text-dim);
opacity: 0.3;
margin-bottom: 20px;
display: block;
}
.watchlist-empty h3 {
font-size: 1.3rem;
margin-bottom: 8px;
color: var(--text-main);
}
.watchlist-empty p {
color: var(--text-dim);
margin-bottom: 24px;
}
/* Responsive */
@media (max-width: 768px) {
.watchlist-grid {
grid-template-columns: 1fr;
}
.filter-tabs {
overflow-x: auto;
flex-wrap: nowrap;
}
.filter-tab {
white-space: nowrap;
}
.watchlist-card {
flex-direction: column;
align-items: center;
text-align: center;
}
.watchlist-poster {
width: 140px;
}
.watchlist-meta,
.watchlist-stats {
justify-content: center;
}
}
</style>
+11 -34
View File
@@ -1,11 +1,13 @@
<div class="section-container"> <div class="mb-10">
<div class="section-header"> <div class="flex justify-between items-center mb-4">
<h2><i class="fa-solid fa-clipboard-list"></i> Ma Watchlist</h2> <h2 class="text-xl font-bold flex items-center gap-2">
<div class="header-actions"> <i class="fa-solid fa-clipboard-list"></i> Ma Watchlist
</h2>
<div class="flex gap-2">
<button class="btn btn-sm btn-primary" hx-post="/api/watchlist/check" hx-swap="none"> <button class="btn btn-sm btn-primary" hx-post="/api/watchlist/check" hx-swap="none">
<i class="fas fa-sync"></i> Vérifier épisodes <i class="fas fa-sync"></i> Vérifier épisodes
</button> </button>
<button class="btn btn-sm btn-secondary" <button class="btn btn-sm btn-ghost"
hx-get="/api/watchlist" hx-get="/api/watchlist"
hx-target="#watchlist-items-container"> hx-target="#watchlist-items-container">
<i class="fas fa-redo"></i> Actualiser <i class="fas fa-redo"></i> Actualiser
@@ -16,34 +18,9 @@
<!-- Watchlist items container loaded via HTMX on page load or manual refresh --> <!-- Watchlist items container loaded via HTMX on page load or manual refresh -->
<div id="watchlist-items-container" <div id="watchlist-items-container"
hx-get="/api/watchlist" hx-get="/api/watchlist"
hx-trigger="load" hx-trigger="load"
class="watchlist-content"> class="flex justify-center py-8 text-base-content/50">
<div class="loading-placeholder"> <span class="loading loading-spinner loading-lg"></span>
<div class="spinner"></div> Chargement de votre watchlist... <span class="ml-2">Chargement de votre watchlist...</span>
</div>
</div> </div>
</div> </div>
<style>
.watchlist-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.watchlist-item {
display: flex;
gap: 15px;
padding: 15px;
background: var(--bg-card);
border-radius: 4px;
border: 1px solid var(--secondary);
transition: border-color 0.2s;
}
.watchlist-item:hover { border-color: #FFBF69; }
.item-poster img { width: 80px; height: 120px; border-radius: 4px; object-fit: cover; }
.item-info { flex: 1; display: flex; flex-direction: column; justify-content: space-between; }
.item-info h3 { font-size: 1rem; margin-bottom: 5px; color: #F2F2F2; }
.item-meta { display: flex; gap: 8px; margin-bottom: 8px; }
.item-actions { display: flex; gap: 10px; margin-top: 10px; }
</style>
+93 -88
View File
@@ -1,139 +1,144 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
{% include "components/header.html" %}
<!-- Main content - Managed by Alpine state --> <!-- Main content - Managed by Alpine state -->
<div id="main-content"> <div id="main-content">
{% include "components/home_section.html" %} {% include "components/home_section.html" %}
<!-- Nouveaux onglets --> <!-- Anime Tab -->
<div id="tab-anime" class="tab-content" x-show="activeTab === 'anime'"> <div id="tab-anime" x-show="activeTab === 'anime'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
<!-- Anime Search Section --> <!-- Anime Search Section -->
<div class="section-header"> <div class="flex justify-between items-center mb-4">
<h2>Rechercher un Anime</h2> <h2 class="text-xl font-bold">
<i class="fa-solid fa-film text-primary"></i> Rechercher un Anime
</h2>
</div> </div>
<div class="url-form"> <form hx-get="/api/anime/search"
<form hx-get="/api/anime/search" hx-target="#animeSearchResults"
hx-target="#animeSearchResults" hx-indicator="#search-loading"
hx-indicator="#search-loading" hx-trigger="submit, keyup changed delay:500ms from:#animeSearchInput"
hx-trigger="submit, keyup changed delay:500ms from:#animeSearchInput" class="join w-full mb-4">
class="input-group"> <input type="hidden" name="html" value="1">
<input type="hidden" name="html" value="1"> <input
<input type="text"
type="text" name="q"
name="q" id="animeSearchInput"
id="animeSearchInput" placeholder="Rechercher un anime (ex: Frieren, One Piece, Naruto...)"
placeholder="Rechercher un anime (ex: Frieren, One Piece, Naruto...)" required
required class="input input-bordered join-item flex-1"
> >
<button type="submit" class="btn btn-primary btn-search"> <button type="submit" class="btn btn-primary join-item">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:18px;height:18px;"> <svg fill="none" stroke="currentColor" viewBox="0 0 24 24" class="w-[18px] h-[18px]">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg> </svg>
Rechercher Rechercher
</button> </button>
</form> </form>
<div id="search-loading" class="htmx-indicator" style="margin-top: 15px; color: var(--primary);"> <div id="search-loading" class="htmx-indicator flex items-center gap-2 text-primary py-2">
<div class="spinner"></div> Recherche en cours... <span class="loading loading-spinner loading-sm"></span> Recherche en cours...
</div>
</div> </div>
<!-- Anime search results --> <!-- Anime search results -->
<div id="animeSearchResults" style="margin-bottom: 40px;"></div> <div id="animeSearchResults" class="mb-10"></div>
<!-- Player container for HTMX injections --> <!-- Player container for HTMX injections -->
<div id="player-container"></div> <div id="player-container"></div>
<hr style="border: none; border-top: 1px solid #2a2d32; margin: 40px 0;"> <div class="divider"></div>
<!-- Latest Releases Section - Anime only --> <!-- Latest Releases Section - Anime only -->
<div class="section-header"> <div class="flex justify-between items-center mb-4">
<h2>Dernieres sorties Anime</h2> <h2 class="text-xl font-bold">
<button class="btn btn-secondary btn-small" <i class="fa-solid fa-fire text-error"></i> Dernières sorties Anime
hx-get="/api/releases/latest?content_type=anime&html=1" </h2>
<button class="btn btn-sm btn-ghost gap-1.5"
hx-get="/api/releases/latest?content_type=anime&html=1"
hx-target="#animeReleasesList"> hx-target="#animeReleasesList">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:14px;height:14px;"> <i class="fa-solid fa-arrows-rotate text-xs"></i> Actualiser
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
Actualiser
</button> </button>
</div> </div>
<div id="animeReleasesList" class="recommendations-carousel" hx-get="/api/releases/latest?content_type=anime&html=1" hx-trigger="load delay:500ms"></div> <div id="animeReleasesList" hx-get="/api/releases/latest?content_type=anime&html=1" hx-trigger="load delay:500ms"></div>
</div> </div>
<div id="tab-series" class="tab-content" x-show="activeTab === 'series'"> <!-- Series Tab -->
<div id="tab-series" x-show="activeTab === 'series'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
<!-- Series Search Section --> <!-- Series Search Section -->
<div class="section-header"> <div class="flex justify-between items-center mb-4">
<h2>Rechercher une Serie TV</h2> <h2 class="text-xl font-bold">
<i class="fa-solid fa-tv text-secondary"></i> Rechercher une Série TV
</h2>
</div> </div>
<div class="url-form"> <form hx-get="/api/series/search"
<form hx-get="/api/series/search" hx-target="#seriesSearchResults"
hx-target="#seriesSearchResults" hx-indicator="#series-search-loading"
hx-indicator="#series-search-loading" hx-trigger="submit, keyup changed delay:500ms from:#seriesSearchInput"
hx-trigger="submit, keyup changed delay:500ms from:#seriesSearchInput" class="join w-full mb-4">
class="input-group"> <input type="hidden" name="html" value="1">
<input type="hidden" name="html" value="1"> <input
<input type="text"
type="text" name="q"
name="q" id="seriesSearchInput"
id="seriesSearchInput" placeholder="Rechercher une série (ex: Breaking Bad, Game of Thrones...)"
placeholder="Rechercher une serie (ex: Breaking Bad, Game of Thrones...)" required
required class="input input-bordered join-item flex-1"
> >
<button type="submit" class="btn btn-primary btn-search"> <button type="submit" class="btn btn-primary join-item">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:18px;height:18px;"> <svg fill="none" stroke="currentColor" viewBox="0 0 24 24" class="w-[18px] h-[18px]">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg> </svg>
Rechercher Rechercher
</button> </button>
</form> </form>
<div id="series-search-loading" class="htmx-indicator" style="margin-top: 15px; color: var(--secondary);"> <div id="series-search-loading" class="htmx-indicator flex items-center gap-2 text-secondary py-2">
<div class="spinner"></div> Recherche en cours... <span class="loading loading-spinner loading-sm"></span> Recherche en cours...
</div>
</div> </div>
<!-- Series search results --> <!-- Series search results -->
<div id="seriesSearchResults" style="margin-bottom: 40px;"></div> <div id="seriesSearchResults" class="mb-10"></div>
<hr style="border: none; border-top: 1px solid #2a2d32; margin: 40px 0;"> <div class="divider"></div>
<!-- Latest Releases Section - Series only --> <!-- Latest Releases Section - Series only -->
<div class="section-header"> <div class="flex justify-between items-center mb-4">
<h2>Dernieres sorties Series TV</h2> <h2 class="text-xl font-bold">
<button class="btn btn-secondary btn-small" <i class="fa-solid fa-fire text-error"></i> Dernières sorties Séries TV
hx-get="/api/series/latest?html=1" </h2>
<button class="btn btn-sm btn-ghost gap-1.5"
hx-get="/api/series/latest?html=1"
hx-target="#seriesReleasesList"> hx-target="#seriesReleasesList">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:14px;height:14px;"> <i class="fa-solid fa-arrows-rotate text-xs"></i> Actualiser
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
Actualiser
</button> </button>
</div> </div>
<div id="seriesReleasesList" class="releases-carousel" hx-get="/api/series/latest?html=1" hx-trigger="load delay:700ms"></div> <div id="seriesReleasesList" hx-get="/api/series/latest?html=1" hx-trigger="load delay:700ms"></div>
</div> </div>
<div id="tab-watchlist" class="tab-content" x-show="activeTab === 'watchlist'"> <!-- Watchlist Tab -->
<div id="tab-watchlist" x-show="activeTab === 'watchlist'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
{% include "components/watchlist_section.html" %} {% include "components/watchlist_section.html" %}
</div> </div>
<div id="tab-downloads" class="tab-content" x-show="activeTab === 'downloads'"> <!-- Downloads Tab -->
<div id="tab-downloads" x-show="activeTab === 'downloads'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
{% include "components/downloads_section.html" %} {% include "components/downloads_section.html" %}
</div> </div>
<div id="tab-settings" class="tab-content" x-show="activeTab === 'settings'"> <!-- Settings Tab -->
<div id="tab-settings" x-show="activeTab === 'settings'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
<div hx-get="/api/settings/ui" hx-trigger="set-tab[detail.tab === 'settings'] from:window, refresh-settings" hx-swap="innerHTML"> <div hx-get="/api/settings/ui" hx-trigger="set-tab[detail.tab === 'settings'] from:window, refresh-settings" hx-swap="innerHTML">
<div class="loading-placeholder"> <div class="flex items-center justify-center py-16">
<div class="spinner"></div> Chargement des parametres... <span class="loading loading-spinner loading-lg text-primary"></span>
<span class="ml-3 text-base-content/50">Chargement des paramètres...</span>
</div> </div>
</div> </div>
</div> </div>
<div id="tab-admin" class="tab-content" x-show="activeTab === 'admin'"> <!-- Admin Tab -->
<div id="tab-admin" x-show="activeTab === 'admin'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
<div id="admin-panel-content" hx-get="/api/admin/ui" hx-trigger="set-tab[detail.tab === 'admin'] from:window" hx-swap="innerHTML"> <div id="admin-panel-content" hx-get="/api/admin/ui" hx-trigger="set-tab[detail.tab === 'admin'] from:window" hx-swap="innerHTML">
<div class="loading-placeholder"> <div class="flex items-center justify-center py-16">
<div class="spinner"></div> Chargement du panel admin... <span class="loading loading-spinner loading-lg text-primary"></span>
<span class="ml-3 text-base-content/50">Chargement du panel admin...</span>
</div> </div>
</div> </div>
</div> </div>
+152 -90
View File
@@ -1,106 +1,148 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr" data-theme="ohmstream">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Connexion - Ohm Stream Downloader</title> <title>Connexion - Ohm Stream Downloader</title>
<link rel="stylesheet" href="/static/css/style.css"> <link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
</head> </head>
<body> <body>
<div class="auth-container"> <div class="min-h-screen flex items-center justify-center bg-base-100">
<h1 class="auth-title"><i class="fa-solid fa-film"></i> Ohm Stream</h1> <div class="card w-96 bg-base-200 shadow-2xl">
<div class="card-body">
<!-- Title -->
<h1 class="text-2xl font-bold text-center text-primary">
<i class="fa-solid fa-film"></i> Ohm Stream
</h1>
<div class="auth-tabs"> <!-- Tab Toggle -->
<div class="auth-tab active" data-tab="login">Connexion</div> <div class="tabs tabs-boxed bg-base-300 mb-4" role="tablist">
<div class="auth-tab" data-tab="register">Inscription</div> <button class="tab tab-active auth-tab" role="tab" data-tab="login">Connexion</button>
</div> <button class="tab auth-tab" role="tab" data-tab="register">Inscription</button>
</div>
<div class="auth-error" id="authError" aria-live="polite"></div> <!-- Error / Success Alerts -->
<div class="auth-success" id="authSuccess" aria-live="polite"></div> <div id="authError" class="auth-error alert alert-error hidden mb-2" role="alert" aria-live="polite">
<i class="fa-solid fa-circle-exclamation"></i>
<span></span>
</div>
<div id="authSuccess" class="auth-success alert alert-success hidden mb-2" role="status" aria-live="polite">
<i class="fa-solid fa-circle-check"></i>
<span></span>
</div>
<!-- Login Form --> <!-- Login Form -->
<form class="auth-form active" id="loginForm"> <form class="auth-form active" id="loginForm">
<div class="form-group"> <div class="form-control mb-3">
<label for="loginUsername">Nom d'utilisateur</label> <label class="label" for="loginUsername">
<input <span class="label-text">Nom d'utilisateur</span>
type="text" </label>
id="loginUsername" <input
placeholder="Entrez votre nom d'utilisateur" type="text"
required id="loginUsername"
aria-required="true" placeholder="Entrez votre nom d'utilisateur"
aria-describedby="loginUsernameHelp" class="input input-bordered w-full"
> required
<span id="loginUsernameHelp" style="display: none;">Champ obligatoire</span> aria-required="true"
</div> aria-describedby="loginUsernameHelp"
<div class="form-group"> >
<label for="loginPassword">Mot de passe</label> <label class="label hidden" id="loginUsernameHelp">
<input <span class="label-text-alt text-error">Champ obligatoire</span>
type="password" </label>
id="loginPassword" </div>
placeholder="Entrez votre mot de passe" <div class="form-control mb-3">
required <label class="label" for="loginPassword">
aria-required="true" <span class="label-text">Mot de passe</span>
> </label>
</div> <input
<button type="submit" id="loginSubmit" class="btn btn-primary btn-block">Se connecter</button> type="password"
</form> id="loginPassword"
placeholder="Entrez votre mot de passe"
class="input input-bordered w-full"
required
aria-required="true"
>
</div>
<button type="submit" id="loginSubmit" class="btn btn-primary w-full">Se connecter</button>
</form>
<!-- Register Form --> <!-- Register Form -->
<form class="auth-form" id="registerForm"> <form class="auth-form hidden" id="registerForm">
<div class="form-group"> <div class="form-control mb-3">
<label for="registerUsername">Nom d'utilisateur</label> <label class="label" for="registerUsername">
<input <span class="label-text">Nom d'utilisateur</span>
type="text" </label>
id="registerUsername" <input
placeholder="Choisissez un nom d'utilisateur" type="text"
minlength="3" id="registerUsername"
required placeholder="Choisissez un nom d'utilisateur"
aria-required="true" class="input input-bordered w-full"
> minlength="3"
</div> required
<div class="form-group"> aria-required="true"
<label for="registerEmail">Email (optionnel)</label> >
<input </div>
type="email" <div class="form-control mb-3">
id="registerEmail" <label class="label" for="registerEmail">
placeholder="votre@email.com" <span class="label-text">Email (optionnel)</span>
> </label>
</div> <input
<div class="form-group"> type="email"
<label for="registerFullName">Nom complet (optionnel)</label> id="registerEmail"
<input placeholder="votre@email.com"
type="text" class="input input-bordered w-full"
id="registerFullName" >
placeholder="Votre nom complet" </div>
> <div class="form-control mb-3">
</div> <label class="label" for="registerFullName">
<div class="form-group"> <span class="label-text">Nom complet (optionnel)</span>
<label for="registerPassword">Mot de passe</label> </label>
<input <input
type="password" type="text"
id="registerPassword" id="registerFullName"
placeholder="Au moins 6 caractères" placeholder="Votre nom complet"
minlength="6" class="input input-bordered w-full"
required >
aria-required="true" </div>
> <div class="form-control mb-3">
</div> <label class="label" for="registerPassword">
<div class="form-group"> <span class="label-text">Mot de passe</span>
<label for="registerPasswordConfirm">Confirmer le mot de passe</label> </label>
<input <input
type="password" type="password"
id="registerPasswordConfirm" id="registerPassword"
placeholder="Confirmez votre mot de passe" placeholder="Au moins 6 caractères"
minlength="6" class="input input-bordered w-full"
required minlength="6"
aria-required="true" required
> aria-required="true"
</div> >
<button type="submit" id="registerSubmit" class="btn btn-primary btn-block">S'inscrire</button> </div>
</form> <div class="form-control mb-3">
<label class="label" for="registerPasswordConfirm">
<span class="label-text">Confirmer le mot de passe</span>
</label>
<input
type="password"
id="registerPasswordConfirm"
placeholder="Confirmez votre mot de passe"
class="input input-bordered w-full"
minlength="6"
required
aria-required="true"
>
</div>
<button type="submit" id="registerSubmit" class="btn btn-primary w-full">S'inscrire</button>
</form>
<div style="text-align: center; margin-top: 25px;"> <!-- Back Link -->
<a href="/web" class="btn btn-secondary btn-small">← Retour à l'accueil</a> <div class="text-center mt-5">
<a href="/web" class="btn btn-ghost btn-sm">
<i class="fa-solid fa-arrow-left"></i> Retour à l'accueil
</a>
</div>
</div>
</div> </div>
</div> </div>
@@ -109,6 +151,26 @@
<script src="/static/js/auth-api.js"></script> <script src="/static/js/auth-api.js"></script>
<script src="/static/js/auth-ui.js"></script> <script src="/static/js/auth-ui.js"></script>
<script> <script>
// Patch displayError / displaySuccess to work with DaisyUI alerts
(function () {
const origDisplayError = window.displayError;
const origDisplaySuccess = window.displaySuccess;
window.displayError = function (id, message) {
const el = document.getElementById(id);
if (!el) return;
el.classList.remove('hidden');
el.querySelector('span').textContent = message || '';
};
window.displaySuccess = function (id, message) {
const el = document.getElementById(id);
if (!el) return;
el.classList.remove('hidden');
el.querySelector('span').textContent = message || '';
};
})();
// Expose setToken from auth.js if available // Expose setToken from auth.js if available
if (typeof window.setToken === 'undefined') { if (typeof window.setToken === 'undefined') {
window.setToken = function(token) { window.setToken = function(token) {
+42 -149
View File
@@ -1,157 +1,45 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr" data-theme="ohmstream">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ filename }} - Ohm Stream Player</title> <title>{{ filename }} - Ohm Stream Player</title>
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" /> <link rel="stylesheet" href="/static/css/style.css">
<style> <link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css">
* { <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: #15171A;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
color: #F2F2F2;
}
.container {
max-width: 1200px;
width: 100%;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
font-size: 1.5rem;
margin-bottom: 10px;
color: #FFBF69;
}
.video-info {
background: #202327;
padding: 15px 20px;
border-radius: 4px;
border: 1px solid #2a2d32;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.video-info .filename {
font-size: 1.1rem;
font-weight: 500;
}
.video-info .filesize {
color: #8a8f98;
font-size: 0.9rem;
}
.video-wrapper {
background: #000;
border-radius: 4px;
overflow: hidden;
}
.plyr {
border-radius: 4px;
}
.controls {
margin-top: 20px;
display: flex;
gap: 10px;
justify-content: center;
flex-wrap: wrap;
}
.btn {
padding: 12px 24px;
background: #202327;
border: 1px solid #2a2d32;
color: #F2F2F2;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn:hover {
background: #2a2d32;
border-color: #FFBF69;
}
.btn-primary {
background: #FF9F1C;
border: 1px solid #FF9F1C;
color: #fff;
font-weight: 600;
}
.btn-primary:hover {
background: #e08a15;
border-color: #e08a15;
}
.error-message {
background: rgba(230, 57, 70, 0.1);
border: 1px solid #e63946;
color: #e63946;
padding: 20px;
border-radius: 4px;
text-align: center;
margin-top: 20px;
}
@media (max-width: 768px) {
.header h1 {
font-size: 1.2rem;
}
.video-info { flex-direction: column; align-items: flex-start; }
.controls { flex-direction: column; }
.btn { width: 100%; justify-content: center; }
}
</style>
</head> </head>
<body> <body>
<div class="container"> <div class="min-h-screen bg-base-100 p-4 md:p-8">
<div class="header"> <div class="max-w-5xl mx-auto">
<h1><i class="fa-solid fa-film"></i> Ohm Stream Player</h1> <!-- Header -->
</div> <div class="text-center mb-6">
<h1 class="text-2xl md:text-3xl font-bold text-primary">
<i class="fa-solid fa-film"></i> Ohm Stream Player
</h1>
</div>
<div class="video-info"> <!-- Video Info Bar -->
<span class="filename">{{ filename }}</span> <div class="flex justify-between items-center bg-base-200 rounded-box border border-base-300 p-4 mb-4 flex-wrap gap-2">
<span class="filesize">{{ "%.2f"|format(file_size / 1024 / 1024) }} MB</span> <span class="font-medium text-base-content">{{ filename }}</span>
</div> <span class="badge badge-ghost">{{ "%.2f"|format(file_size / 1024 / 1024) }} MB</span>
</div>
<div class="video-wrapper"> <!-- Video Wrapper -->
<video id="player" playsinline controls preload="metadata"> <div class="bg-black rounded-box overflow-hidden">
<source src="/stream/{{ filename }}" type="video/mp4"> <video id="player" playsinline controls preload="metadata">
</video> <source src="/stream/{{ filename }}" type="video/mp4">
</div> </video>
</div>
<div class="controls"> <!-- Controls -->
<a href="/web" class="btn">← Retour à l'accueil</a> <div class="flex justify-center gap-3 mt-4 flex-wrap">
<a href="/stream/{{ filename }}" class="btn btn-primary" download><i class="fa-solid fa-download"></i> Télécharger</a> <a href="/web" class="btn btn-ghost">
<i class="fa-solid fa-arrow-left"></i> Retour
</a>
<a href="/stream/{{ filename }}" class="btn btn-primary" download>
<i class="fa-solid fa-download"></i> Télécharger
</a>
</div>
</div> </div>
</div> </div>
@@ -165,12 +53,17 @@
// Error handling // Error handling
player.on('error', (error) => { player.on('error', (error) => {
console.error('Plyr error:', error); console.error('Plyr error:', error);
const wrapper = document.querySelector('.video-wrapper'); const wrapper = document.querySelector('.bg-black');
wrapper.innerHTML = ` wrapper.innerHTML = `
<div class="error-message"> <div class="alert alert-error m-4">
Erreur lors de la lecture du flux vidéo.<br> <i class="fa-solid fa-circle-exclamation"></i>
<a href="/video/{{ task_id }}" style="color: #FF9F1C; text-decoration: underline;">Réessayer</a> ou <div>
<a href="/stream/{{ filename }}" style="color: #FF9F1C; text-decoration: underline;" download>Télécharger</a> <p>Erreur lors de la lecture du flux vidéo.</p>
<div class="flex gap-2 mt-2">
<a href="/video/{{ task_id }}" class="btn btn-sm btn-primary">Réessayer</a>
<a href="/stream/{{ filename }}" download class="btn btn-sm btn-ghost">Télécharger</a>
</div>
</div>
</div> </div>
`; `;
}); });
+79 -68
View File
@@ -1,79 +1,90 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr" data-theme="ohmstream">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Watchlist - Ohm Stream Downloader</title> <title>Watchlist - Ohm Stream Downloader</title>
<link rel="stylesheet" href="/static/css/style.css"> <link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
</head> </head>
<body class="watchlist-body"> <body class="min-h-screen bg-base-100">
<!-- Main Header --> <!-- Navbar -->
<div style="text-align: center; margin-bottom: 20px;"> <div class="navbar bg-base-200 border-b border-base-300 px-4">
<h1 style="color: #FF9F1C; font-size: 32px; margin: 0;"><i class="fa-solid fa-bolt"></i> Ohm Stream Downloader</h1> <div class="flex-1">
<p style="color: #8a8f98; font-size: 14px; margin: 5px 0 0;">Téléchargez vos vidéos, animes et séries</p> <a href="/web" class="text-xl font-bold text-primary gap-2">
<i class="fa-solid fa-bolt"></i> Ohm Stream
</a>
</div>
<div class="flex-none">
<ul class="menu menu-horizontal px-1 gap-1">
<li><a href="/web"><i class="fa-solid fa-house"></i> Accueil</a></li>
<li><a href="/web#anime"><i class="fa-solid fa-film"></i> Anime</a></li>
<li><a href="/web#series"><i class="fa-solid fa-tv"></i> Série</a></li>
<li><a href="/web#providers"><i class="fa-solid fa-box"></i> Fournisseurs</a></li>
<li><a href="/watchlist" class="active"><i class="fa-solid fa-clipboard-list"></i> Watchlist</a></li>
</ul>
</div>
</div> </div>
<!-- User Info --> <!-- Main Content -->
<div id="userInfo" style="display: none; max-width: 1200px; margin: 0 auto 15px; padding: 10px; background: rgba(255,191,105,0.1); border: 1px solid #FF9F1C; border-radius: 4px;"> <div class="max-w-6xl mx-auto px-4 py-6">
<span style="color: #FF9F1C;"><i class="fa-solid fa-user"></i> Connecté</span> <!-- Page Header -->
<button class="btn-secondary btn-small" onclick="handleLogout()"><i class="fa-solid fa-right-from-bracket"></i> Déconnexion</button> <div class="flex justify-between items-start flex-wrap gap-4 mb-6">
</div> <div>
<h1 class="text-2xl font-bold">
<!-- Tabs --> <i class="fa-solid fa-clipboard-list text-primary"></i> Ma Watchlist
<div class="tabs" style="max-width: 1200px; margin: 0 auto 20px; display: flex; gap: 5px; border-bottom: 1px solid #2a2d32; padding-bottom: 10px;"> </h1>
<button class="tab" onclick="window.location.href='/web'"><i class="fa-solid fa-house"></i> Accueil</button> <p class="text-sm text-base-content/60 mt-1">
<button class="tab" onclick="window.location.href='/web#anime'"><i class="fa-solid fa-film"></i> Anime</button> Suivez vos animes préférés et téléchargez automatiquement les nouveaux épisodes
<button class="tab" onclick="window.location.href='/web#series'"><i class="fa-solid fa-tv"></i> Série</button> </p>
<button class="tab" onclick="window.location.href='/web#providers'"><i class="fa-solid fa-box"></i> Fournisseurs</button> </div>
<button class="tab active" onclick="window.location.href='/watchlist'"><i class="fa-solid fa-clipboard-list"></i> Watchlist</button> <a href="/web" class="btn btn-ghost btn-sm">
</div> <i class="fa-solid fa-arrow-left"></i> Retour à l'accueil
</a>
<div class="watchlist-container">
<!-- Header -->
<div class="watchlist-header">
<h1><i class="fa-solid fa-clipboard-list"></i> Ma Watchlist</h1>
<p>Suivez vos animes préférés et téléchargez automatiquement les nouveaux épisodes</p>
<button type="button" class="btn-secondary watchlist-header-back-btn" onclick="window.location.href = '/web'">
← Retour à l'accueil
</button>
</div> </div>
<!-- Scheduler Status --> <!-- Scheduler Status -->
<div class="scheduler-status" id="schedulerStatus"> <div class="alert bg-base-200 border border-base-300 mb-4" id="schedulerStatus">
<div class="scheduler-status-header"> <div class="flex-1">
<div> <div class="flex justify-between items-start flex-wrap gap-3">
<h3><i class="fa-solid fa-clock"></i> Planificateur Automatique</h3> <div>
<div id="nextRunInfo" class="next-run-info">Chargement...</div> <h3 class="font-semibold text-base-content">
</div> <i class="fa-solid fa-clock text-primary"></i> Planificateur Automatique
<div class="scheduler-controls"> </h3>
<button id="startSchedulerBtn" class="btn-primary btn-small watchlist-btn-small" onclick="handleStartScheduler()" style="display:none;"> <div id="nextRunInfo" class="text-sm text-base-content/60 mt-1">Chargement...</div>
<i class="fa-solid fa-play"></i> Démarrer </div>
</button> <div class="flex gap-2 flex-wrap">
<button id="stopSchedulerBtn" class="btn-secondary btn-small watchlist-btn-small" onclick="handleStopScheduler()" style="display:none;"> <button id="startSchedulerBtn" class="btn btn-primary btn-sm hidden" onclick="handleStartScheduler()">
<i class="fa-solid fa-pause"></i> Arrêter <i class="fa-solid fa-play"></i> Démarrer
</button> </button>
<button class="btn-secondary btn-small watchlist-btn-small" onclick="handleCheckAll()"> <button id="stopSchedulerBtn" class="btn btn-ghost btn-sm hidden" onclick="handleStopScheduler()">
<i class="fa-solid fa-magnifying-glass"></i> Vérifier tout <i class="fa-solid fa-pause"></i> Arrêter
</button> </button>
<button class="btn-secondary btn-small watchlist-btn-small" onclick="handleOpenSettings()"> <button class="btn btn-ghost btn-sm" onclick="handleCheckAll()">
<i class="fa-solid fa-gear"></i> Paramètres <i class="fa-solid fa-magnifying-glass"></i> Vérifier tout
</button> </button>
<button class="btn btn-ghost btn-sm" onclick="handleOpenSettings()">
<i class="fa-solid fa-gear"></i> Paramètres
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Filter Tabs --> <!-- Filter Tabs -->
<div class="filter-tabs"> <div class="tabs tabs-boxed bg-base-200 p-1 mb-4">
<button class="filter-tab active" onclick="filterWatchlist('all', this)">Tous</button> <button class="tab filter-tab tab-active" onclick="filterWatchlist('all', this)">Tous</button>
<button class="filter-tab" onclick="filterWatchlist('active', this)">Actifs</button> <button class="tab filter-tab" onclick="filterWatchlist('active', this)">Actifs</button>
<button class="filter-tab" onclick="filterWatchlist('paused', this)">En pause</button> <button class="tab filter-tab" onclick="filterWatchlist('paused', this)">En pause</button>
<button class="filter-tab" onclick="filterWatchlist('completed', this)">Terminés</button> <button class="tab filter-tab" onclick="filterWatchlist('completed', this)">Terminés</button>
</div> </div>
<!-- Watchlist Items --> <!-- Watchlist Items -->
<div id="watchlistContainer"> <div id="watchlistContainer" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="watchlist-loading">Chargement de la watchlist...</div> <div class="col-span-full text-center py-12">
<span class="loading loading-spinner loading-lg text-primary"></span>
<p class="text-base-content/60 mt-3">Chargement de la watchlist...</p>
</div>
</div> </div>
</div> </div>
@@ -156,22 +167,22 @@
if (status.running) { if (status.running) {
// Update buttons if they exist // Update buttons if they exist
if (startBtn) startBtn.style.display = 'none'; if (startBtn) startBtn.classList.add('hidden');
if (stopBtn) stopBtn.style.display = 'inline-block'; if (stopBtn) stopBtn.classList.remove('hidden');
if (status.next_run) { if (status.next_run) {
const nextRun = new Date(status.next_run); const nextRun = new Date(status.next_run);
nextRunInfo.innerHTML = `<i class="fa-solid fa-check"></i> En cours<br>Prochaine vérification: ${nextRun.toLocaleString('fr-FR')}`; nextRunInfo.innerHTML = `<i class="fa-solid fa-check text-success"></i> En cours<br>Prochaine vérification: ${nextRun.toLocaleString('fr-FR')}`;
} else { } else {
// Scheduler running but no next_run yet (just started) // Scheduler running but no next_run yet (just started)
const interval = status.settings?.check_interval_hours || 6; const interval = status.settings?.check_interval_hours || 6;
nextRunInfo.innerHTML = `<i class="fa-solid fa-check"></i> En cours<br>Vérification toutes les ${interval}h`; nextRunInfo.innerHTML = `<i class="fa-solid fa-check text-success"></i> En cours<br>Vérification toutes les ${interval}h`;
} }
} else { } else {
// Update buttons if they exist // Update buttons if they exist
if (startBtn) startBtn.style.display = 'inline-block'; if (startBtn) startBtn.classList.remove('hidden');
if (stopBtn) stopBtn.style.display = 'none'; if (stopBtn) stopBtn.classList.add('hidden');
nextRunInfo.innerHTML = '<i class="fa-solid fa-pause"></i> Arrêté'; nextRunInfo.innerHTML = '<i class="fa-solid fa-pause text-base-content/40"></i> Arrêté';
} }
} }
@@ -181,11 +192,11 @@
async function filterWatchlist(status, tabElement) { async function filterWatchlist(status, tabElement) {
currentFilter = status; currentFilter = status;
// Update tab styles // Update tab styles — DaisyUI uses tab-active
document.querySelectorAll('.filter-tab').forEach(tab => { document.querySelectorAll('.filter-tab').forEach(tab => {
tab.classList.remove('active'); tab.classList.remove('tab-active');
}); });
tabElement.classList.add('active'); tabElement.classList.add('tab-active');
// Reload with filter // Reload with filter
await displayWatchlist(status === 'all' ? null : status); await displayWatchlist(status === 'all' ? null : status);
@@ -254,4 +265,4 @@
setInterval(loadSchedulerStatus, 30000); setInterval(loadSchedulerStatus, 30000);
</script> </script>
</body> </body>
</html> </html>