feat: flat design Sunset Glitch + download manager + settings + recommendations overhaul
- Sunset Glitch color palette applied to all templates - Font Awesome icons throughout UI - Download manager with parallel queue and progress tracking - Settings page with dynamic configuration - Recommendations router enhanced with scoring - Local vendor libs (Alpine.js, HTMX) for offline support - Auto test suite with screenshots - Series releases list component - New download model
This commit is contained in:
+4
-3
@@ -10,9 +10,9 @@
|
||||
<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" />
|
||||
|
||||
<!-- External Libraries -->
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||||
<!-- External Libraries (local first, CDN fallback) -->
|
||||
<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>
|
||||
@@ -41,6 +41,7 @@
|
||||
<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/settings.js?v=1.0" defer></script>
|
||||
</head>
|
||||
<body x-data="globalAppState">
|
||||
{% include "components/toast_container.html" %}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
{% macro series_card(series, in_watchlist=False, lang='vf') %}
|
||||
<div class="ac" id="series-{{ series.url | hash }}">
|
||||
<div class="ac-poster">
|
||||
<img src="{{ series.cover_image or 'https://placehold.co/400x600/e6e8e6/f15025?text=No+Image' }}"
|
||||
alt="{{ series.title }}" loading="lazy" referrerpolicy="no-referrer"
|
||||
onerror="this.src='https://placehold.co/400x600/e6e8e6/f15025?text=Error'; this.onerror=null;">
|
||||
<button class="ac-play"
|
||||
hx-get="/api/anime/episodes?url={{ series.url | urlencode }}&lang={{ lang }}"
|
||||
hx-target="#player-container" hx-swap="innerHTML">
|
||||
<i class="fas fa-play"></i>
|
||||
</button>
|
||||
{% macro series_card(series) %}
|
||||
<div class="hc"
|
||||
@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">
|
||||
<img src="{{ series.cover_image or 'https://placehold.co/400x600/202327/FF9F1C?text=No+Image' }}" alt="{{ series.title }}" loading="lazy" referrerpolicy="no-referrer"
|
||||
onerror="this.src='https://placehold.co/400x600/202327/FF9F1C?text=Error'; this.onerror=null;">
|
||||
{% if series.lang %}
|
||||
<span class="hc-rating" style="text-transform: uppercase;">{{ series.lang }}</span>
|
||||
{% endif %}
|
||||
<span class="hc-play"><i class="fas fa-search"></i></span>
|
||||
</div>
|
||||
<div class="ac-info">
|
||||
<span class="ac-provider" style="--ac-color: var(--secondary)">{{ series.provider_id | upper if series.provider_id else 'FS7' }}</span>
|
||||
<h3 class="ac-title" title="{{ series.title }}">{{ series.title }}</h3>
|
||||
<div class="hc-info">
|
||||
<span class="hc-src">{{ series.provider_id or 'FS7' }}</span>
|
||||
<span class="hc-title">{{ series.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{% from "components/series_card.html" import series_card %}
|
||||
|
||||
{% if releases %}
|
||||
{% for series in releases %}
|
||||
{{ series_card(series) }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<p>Aucune sortie recente trouvee.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -89,6 +89,68 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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;">
|
||||
<h3 style="margin-bottom: 5px; color: var(--primary);">Equilibre du fil d'actualite</h3>
|
||||
<p style="color: var(--text-dim); font-size: 13px; margin-bottom: 20px;">
|
||||
Definissez la proportion d'animes et de series affiches dans les recommandations et dernieres sorties.
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="content_weight_mode" style="font-weight: 600; margin-bottom: 10px; display: block;">Mode</label>
|
||||
<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)">
|
||||
<option value="auto" {% if settings.content_weight_mode == 'auto' %}selected{% endif %}>Automatique (basé sur vos telechargements)</option>
|
||||
<option value="manual" {% if settings.content_weight_mode == 'manual' %}selected{% endif %}>Manuel</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Auto mode info -->
|
||||
<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 id="weight-auto-details" style="font-size: 14px; color: var(--text-dim);">
|
||||
Chargement...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual mode controls -->
|
||||
<div id="weight-manual-controls" style="margin-top: 15px; display: {% if settings.content_weight_mode == 'manual' %}block{% else %}none{% endif %};">
|
||||
<div style="display: flex; gap: 15px; align-items: center;">
|
||||
<div style="flex: 1;">
|
||||
<label for="content_weight_anime" style="font-weight: 600; font-size: 0.9rem; display: block; margin-bottom: 8px;">
|
||||
<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 style="flex: 1;">
|
||||
<label for="content_weight_series" style="font-weight: 600; font-size: 0.9rem; display: block; margin-bottom: 8px;">
|
||||
<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>
|
||||
<input type="hidden" id="content_weight_anime" value="{{ settings.content_weight_anime }}">
|
||||
<input type="hidden" id="content_weight_series" value="{{ settings.content_weight_series }}">
|
||||
<div id="weight-preview" style="margin-top: 15px; padding: 12px; background: var(--bg-elevated); border-radius: 4px; text-align: center; font-size: 14px;">
|
||||
</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>
|
||||
|
||||
<!-- Providers Management -->
|
||||
<div class="settings-card card" style="padding: 25px; background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid #2a2d32;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
@@ -127,93 +189,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function getToken() {
|
||||
return localStorage.getItem('auth_token') || null;
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
const token = getToken();
|
||||
if (!token) return;
|
||||
|
||||
const data = {
|
||||
default_lang: document.getElementById('default_lang').value,
|
||||
theme: document.getElementById('theme').value,
|
||||
download_dir: document.getElementById('download_dir').value,
|
||||
};
|
||||
|
||||
try {
|
||||
const r = await fetch('/api/settings', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (r.ok) {
|
||||
showToast('Preferences enregistrees', 'success');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('Erreur: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveFilter(field, value) {
|
||||
const token = getToken();
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const r = await fetch('/api/settings', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ [field]: value })
|
||||
});
|
||||
if (r.ok) {
|
||||
showToast('Filtre mis a jour', 'success');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('Erreur: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleCategory(field, value) {
|
||||
const token = getToken();
|
||||
if (!token) return;
|
||||
|
||||
// Prevent disabling both
|
||||
if (!value) {
|
||||
const otherField = field === 'anime_enabled' ? 'series_enabled' : 'anime_enabled';
|
||||
const otherCheckbox = document.getElementById(otherField);
|
||||
if (otherCheckbox && !otherCheckbox.checked) {
|
||||
showToast('Au moins une categorie doit rester active', 'error');
|
||||
document.getElementById(field).checked = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const r = await fetch('/api/settings', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ [field]: value })
|
||||
});
|
||||
if (!r.ok) {
|
||||
const err = await r.json().catch(() => ({}));
|
||||
showToast(err.detail || 'Erreur', 'error');
|
||||
document.getElementById(field).checked = !value;
|
||||
} else {
|
||||
showToast('Categorie ' + (value ? 'activee' : 'desactivee'), 'success');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('Erreur: ' + e.message, 'error');
|
||||
document.getElementById(field).checked = !value;
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message, type) {
|
||||
const event = new CustomEvent('show-toast', { detail: { message, type } });
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.settings-form label {
|
||||
display: block;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<div id="toast-container"
|
||||
class="toast-container"
|
||||
style="pointer-events: none;"
|
||||
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)">
|
||||
|
||||
<template x-for="toast in toasts" :key="toast.id">
|
||||
<div class="toast"
|
||||
style="pointer-events: auto;"
|
||||
:class="'toast-' + toast.type"
|
||||
x-show="true"
|
||||
x-transition:enter="toast-enter"
|
||||
@@ -33,6 +35,9 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
pointer-events: none;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
.toast {
|
||||
min-width: 250px;
|
||||
|
||||
+3
-17
@@ -99,25 +99,11 @@
|
||||
|
||||
<hr style="border: none; border-top: 1px solid #2a2d32; margin: 40px 0;">
|
||||
|
||||
<!-- Recommendations Section - Series only -->
|
||||
<div class="section-header">
|
||||
<h2>Recommande pour vous</h2>
|
||||
<button class="btn btn-secondary btn-small"
|
||||
hx-get="/api/recommendations?content_type=series&html=1"
|
||||
hx-target="#seriesRecommendationsList">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:14px;height:14px;">
|
||||
<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>
|
||||
</div>
|
||||
<div id="seriesRecommendationsList" class="recommendations-carousel" style="margin-bottom: 40px;" hx-get="/api/recommendations?content_type=series&html=1" hx-trigger="load delay:600ms"></div>
|
||||
|
||||
<!-- Latest Releases Section - Series only -->
|
||||
<div class="section-header" style="margin-top: 40px;">
|
||||
<div class="section-header">
|
||||
<h2>Dernieres sorties Series TV</h2>
|
||||
<button class="btn btn-secondary btn-small"
|
||||
hx-get="/api/releases/latest?content_type=series&html=1"
|
||||
hx-get="/api/series/latest?html=1"
|
||||
hx-target="#seriesReleasesList">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:14px;height:14px;">
|
||||
<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>
|
||||
@@ -125,7 +111,7 @@
|
||||
Actualiser
|
||||
</button>
|
||||
</div>
|
||||
<div id="seriesReleasesList" class="releases-carousel" hx-get="/api/releases/latest?content_type=series&html=1" hx-trigger="load delay:700ms"></div>
|
||||
<div id="seriesReleasesList" class="releases-carousel" hx-get="/api/series/latest?html=1" hx-trigger="load delay:700ms"></div>
|
||||
</div>
|
||||
|
||||
<div id="tab-watchlist" class="tab-content" x-show="activeTab === 'watchlist'">
|
||||
|
||||
Reference in New Issue
Block a user