feat: flat design Sunset Glitch palette + Font Awesome icons
CI / Test (Python 3.11) (pull_request) Has been cancelled
CI / Test (Python 3.12) (pull_request) Has been cancelled
CI / Lint (pull_request) Has been cancelled
CI / Type Check (pull_request) Has been cancelled
CI / Summary (pull_request) Has been cancelled

This commit is contained in:
root
2026-04-04 07:59:46 +00:00
parent 0179ddbdf4
commit 9e53579b36
24 changed files with 437 additions and 260 deletions
+174
View File
@@ -0,0 +1,174 @@
import { chromium } from 'playwright';
const BASE = 'http://127.0.0.1:3000';
const opts = { waitUntil: 'domcontentloaded', timeout: 15000 };
(async () => {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] });
// Obtenir un token via API
const apiCtx = await browser.newContext();
const apiPage = await apiCtx.newPage();
await apiPage.goto(BASE + '/api/auth/login', opts);
const token = await apiPage.evaluate(async () => {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'roman', password: 'roman123' })
});
const data = await res.json();
return data.access_token || null;
});
await apiCtx.close();
console.log(`Token obtained: ${token ? token.substring(0, 20) + '...' : 'FAILED'}`);
if (!token) {
console.error('Cannot get token, aborting');
process.exit(1);
}
// ========== NON AUTHENTIFIE ==========
console.log('\n=== NON AUTHENTIFIE ===');
const anonCtx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const anon = await anonCtx.newPage();
const snap = async (p, name, url, wait = 3000) => {
try {
await p.goto(url, opts);
await p.waitForTimeout(wait);
await p.screenshot({ path: `/tmp/screenshots/${name}.png`, fullPage: false });
console.log(`OK: ${name}`);
} catch(e) {
console.log(`FAIL: ${name} - ${e.message}`);
}
};
await snap(anon, 'anon_01_home', `${BASE}/`);
await snap(anon, 'anon_02_watchlist', `${BASE}/watchlist`);
await snap(anon, 'anon_03_favorites', `${BASE}/favorites`);
await snap(anon, 'anon_04_downloads', `${BASE}/downloads`);
await snap(anon, 'anon_05_settings', `${BASE}/settings`);
await snap(anon, 'anon_06_recommendations', `${BASE}/recommendations`);
// ========== AUTHENTIFIE (cookie + localStorage) ==========
console.log('\n=== AUTHENTIFIE ===');
const authCtx = await browser.newContext({
viewport: { width: 1440, height: 900 },
});
// Injecter le token comme cookie AVANT toute navigation
await authCtx.addCookies([{
name: 'auth_token',
value: token,
domain: '127.0.0.1',
path: '/',
sameSite: 'Strict',
httpOnly: false,
}]);
const auth = await authCtx.newPage();
// Injecter dans localStorage au premier chargement
await auth.goto(BASE + '/', opts);
await auth.evaluate((t) => {
localStorage.setItem('auth_token', t);
}, token);
await auth.waitForTimeout(3000);
await auth.screenshot({ path: '/tmp/screenshots/auth_01_home.png', fullPage: false });
console.log('OK: auth_01_home');
await snap(auth, 'auth_02_watchlist', `${BASE}/watchlist`);
await snap(auth, 'auth_03_favorites', `${BASE}/favorites`);
await snap(auth, 'auth_04_downloads', `${BASE}/downloads`);
await snap(auth, 'auth_05_settings', `${BASE}/settings`);
await snap(auth, 'auth_06_recommendations', `${BASE}/recommendations`);
// ========== TESTS FONCTIONNELS ==========
console.log('\n=== TESTS FONCTIONNELS ===');
// Test API: toggle favori
const favResult = await auth.evaluate(async (t) => {
try {
const res = await fetch('/api/favorites/toggle', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${t}`
},
body: JSON.stringify({ content_type: 'anime', anime_id: 'test-screenshot-1', title: 'Test Screenshot Anime' })
});
const data = await res.json();
return { status: res.status, is_favorite: data.is_favorite };
} catch(e) {
return { error: e.message };
}
}, token);
console.log(`Favorite toggle: ${JSON.stringify(favResult)}`);
// Voir les favoris
await snap(auth, 'auth_07_favorites_after_add', `${BASE}/favorites`);
// Test API: ajouter watchlist item
const wlResult = await auth.evaluate(async (t) => {
try {
const res = await fetch('/api/watchlist', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${t}`
},
body: JSON.stringify({
anime_title: 'Test Screenshot Anime',
anime_url: 'https://example.com/anime/1',
episode_count: 12,
current_episode: 0,
status: 'watching'
})
});
const data = await res.json();
return { status: res.status, id: data.id, title: data.anime_title };
} catch(e) {
return { error: e.message };
}
}, token);
console.log(`Watchlist add: ${JSON.stringify(wlResult)}`);
// Voir la watchlist
await snap(auth, 'auth_08_watchlist_with_item', `${BASE}/watchlist`);
// Scroller sur la home
await auth.goto(`${BASE}/`, opts);
await auth.waitForTimeout(2000);
await auth.evaluate(() => window.scrollTo(0, 600));
await auth.waitForTimeout(1000);
await auth.screenshot({ path: '/tmp/screenshots/auth_09_home_scrolled.png', fullPage: false });
console.log('OK: auth_09_home_scrolled');
// ========== NETTOYAGE ==========
console.log('\n=== Nettoyage ===');
// Retirer le favori de test
await auth.evaluate(async (t) => {
await fetch('/api/favorites/toggle', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${t}`
},
body: JSON.stringify({ content_type: 'anime', anime_id: 'test-screenshot-1', title: 'Test Screenshot Anime' })
});
});
// Retirer le watchlist item de test
if (wlResult.id) {
await auth.evaluate(async ({t, id}) => {
await fetch(`/api/watchlist/${id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${t}` }
});
}, { t: token, id: wlResult.id });
console.log('Test watchlist item deleted');
}
console.log('Test favorite removed');
await browser.close();
console.log('\n=== ALL DONE ===');
})();
+48 -40
View File
@@ -1,14 +1,22 @@
/* Ohm Streaming - Flat Design Theme */
:root {
/* ========== FLAT DESIGN VARIABLES ========== */
--primary: #f15025;
--primary-hover: #d94420;
--bg-dark: #ffffff;
--bg-card: #e6e8e6;
--text-main: #191919;
--text-dim: #ced0ce;
--secondary: #ced0ce;
--accent: #f15025;
/* ========== FLAT DESIGN VARIABLES - SUNSET GLITCH PALETTE ========== */
--primary: #FF9F1C;
--primary-hover: #e08a15;
--bg-dark: #15171A;
--bg-card: #202327;
--bg-elevated: #2a2d32;
--text-main: #F2F2F2;
--text-dim: #8a8f98;
--text-muted: #5a5f68;
--secondary: #FF9F1C;
--border: #2a2d32;
--border-hover: #FFBF69;
--accent: #FF9F1C;
--hover: rgba(255, 191, 105, 0.15);
--lilac: #8a8f98;
--pastel-petal: #F2F2F2;
--surface-hover: rgba(255, 191, 105, 0.08);
--danger: #e63946;
--success: #2d936c;
--warning: #f4a261;
@@ -39,7 +47,7 @@ body {
}
::-webkit-scrollbar-track {
background: #e6e8e6;
background: #202327;
border-radius: 4px;
}
@@ -50,13 +58,13 @@ body {
}
::-webkit-scrollbar-thumb:hover {
background: var(--primary);
background: var(--text-main);
}
/* Firefox scrollbar */
* {
scrollbar-width: thin;
scrollbar-color: var(--text-dim) #e6e8e6;
scrollbar-color: var(--text-dim) #202327;
}
/* ========== CONTAINER ========== */
@@ -97,7 +105,7 @@ h1 {
.section-header h2 {
font-size: 1.5rem;
font-weight: 700;
border-left: 4px solid var(--primary);
border-left: 4px solid #FFBF69;
padding-left: 15px;
color: var(--text-main);
}
@@ -188,8 +196,8 @@ h1 {
}
.btn-outlined:hover:not(:disabled) {
border-color: var(--primary);
color: var(--primary);
border-color: var(--text-dim);
color: var(--text-dim);
}
/* Text Button */
@@ -268,7 +276,7 @@ h1 {
}
.card:hover, .hc:hover, .download-item:hover {
border-color: var(--primary);
border-color: #FFBF69;
}
/* ========== HORIZONTAL SCROLL ROW ========== */
@@ -300,7 +308,7 @@ h1 {
.streaming-row::-webkit-scrollbar-thumb:hover,
.recommendations-carousel::-webkit-scrollbar-thumb:hover,
.releases-carousel::-webkit-scrollbar-thumb:hover {
background: var(--primary);
background: var(--text-main);
}
/* ========== HOME CARD ========== */
@@ -317,7 +325,7 @@ h1 {
}
.hc:hover {
border-color: var(--primary);
border-color: #FFBF69;
}
.hc-poster {
@@ -432,7 +440,7 @@ h1 {
}
.tab.active {
color: var(--primary);
color: #FFBF69;
}
.tab.active::after {
@@ -442,7 +450,7 @@ h1 {
left: 0;
width: 100%;
height: 3px;
background: var(--primary);
background: #FF9F1C;
border-radius: 3px 3px 0 0;
}
@@ -457,7 +465,7 @@ h1 {
}
.input-group:focus-within {
border-color: var(--primary);
border-color: #FFBF69;
}
.input-group input {
@@ -511,14 +519,14 @@ h1 {
.form-group input:focus {
outline: none;
border-bottom-color: var(--primary);
border-bottom-color: #FFBF69;
}
.form-group input:focus + label,
.form-group input:not(:placeholder-shown) + label {
transform: translateY(-24px);
font-size: 0.75rem;
color: var(--primary);
color: var(--text-dim);
}
.form-group input::placeholder {
@@ -530,7 +538,7 @@ h1 {
margin-bottom: 25px;
padding: 16px 20px;
background: var(--bg-card);
border: 1px solid var(--primary);
border: 1px solid var(--text-dim);
border-radius: var(--card-radius);
display: flex;
justify-content: space-between;
@@ -543,13 +551,13 @@ h1 {
padding: 40px;
background: var(--bg-card);
border-radius: 6px;
border: 1px solid var(--text-dim);
border: 1px solid rgba(255, 191, 105, 0.3);
}
.auth-title {
text-align: center;
margin-bottom: 30px;
color: var(--primary);
color: var(--text-dim);
}
.auth-tabs {
@@ -570,7 +578,7 @@ h1 {
}
.auth-tab.active {
color: var(--primary);
color: #FFBF69;
}
.auth-tab.active::after {
@@ -580,7 +588,7 @@ h1 {
left: 0;
width: 100%;
height: 3px;
background: var(--primary);
background: #FF9F1C;
}
.auth-form {
@@ -606,7 +614,7 @@ h1 {
}
.auth-success {
background: rgba(45, 147, 108, 0.1);
background: rgba(255, 191, 105, 0.1);
border: 1px solid var(--success);
color: var(--success);
}
@@ -618,7 +626,7 @@ h1 {
/* ========== FLAT PROGRESS BARS ========== */
.progress-container {
height: 6px;
background: var(--text-dim);
background: rgba(255, 191, 105, 0.15);
border-radius: 3px;
margin: 12px 0;
overflow: hidden;
@@ -626,7 +634,7 @@ h1 {
.progress-bar {
height: 100%;
background: var(--primary);
background: #FF9F1C;
transition: width 0.3s ease;
border-radius: 3px;
}
@@ -690,8 +698,8 @@ h1 {
}
.download-item:hover {
border-color: var(--primary);
border-left-color: var(--primary);
border-color: #FFBF69;
border-left-color: #FFBF69;
}
.download-info {
@@ -769,13 +777,13 @@ h1 {
/* Progress bar for downloading */
.download-item.status-downloading .progress-bar {
background: var(--primary);
background: var(--text-dim);
}
/* ========== BADGE SYSTEM ========== */
.badge-completed {
color: var(--success);
background: rgba(45, 147, 108, 0.1);
background: rgba(255, 191, 105, 0.1);
padding: 4px 8px;
border-radius: var(--input-radius);
}
@@ -789,7 +797,7 @@ h1 {
.badge-downloading {
color: var(--primary);
background: rgba(241, 80, 37, 0.1);
background: rgba(255, 191, 105, 0.15);
padding: 4px 8px;
border-radius: var(--input-radius);
}
@@ -817,7 +825,7 @@ h1 {
width: 40px;
height: 40px;
border: 3px solid var(--text-dim);
border-top-color: var(--primary);
border-top-color: var(--text-main);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@@ -830,7 +838,7 @@ h1 {
/* Skeleton Loading */
.skeleton {
background: var(--bg-card);
background: rgba(255, 191, 105, 0.1);
animation: skeleton-loading 1.5s ease-in-out infinite;
border-radius: var(--input-radius);
}
@@ -904,7 +912,7 @@ h1 {
.toast {
padding: 16px 24px;
background: var(--bg-card);
border-left: 4px solid var(--primary);
border-left: 4px solid var(--text-dim);
border-radius: var(--card-radius);
border: 1px solid var(--text-dim);
display: flex;
@@ -929,7 +937,7 @@ h1 {
}
.toast.info {
border-left-color: var(--primary);
border-left-color: #FFBF69;
}
@keyframes slide-up {
+24 -24
View File
@@ -82,7 +82,7 @@ async function searchAnimeDetails(query, malId = null) {
if (hasResults) {
streamingParts.unshift(
`<div class="streaming-results-header">
<h3>🎬 Résultats de streaming</h3>
<h3><i class="fa-solid fa-film"></i> Résultats de streaming</h3>
</div>
<div class="search-results" style="margin-top: 20px;">`
);
@@ -110,7 +110,7 @@ async function searchAnimeDetails(query, malId = null) {
if (streamingHtml) {
resultsContainer.innerHTML = `
<div class="no-results" style="margin-bottom: 20px;">
<p> Aucune fiche trouvée sur MyAnimeList pour "${escapeHtml(query)}"</p>
<p><i class="fa-solid fa-circle-info"></i> Aucune fiche trouvée sur MyAnimeList pour "${escapeHtml(query)}"</p>
<p style="font-size: 12px; margin-top: 10px; color: #888;">
Essayez le nom en anglais ou japonais (ex: "Frieren: Beyond Journey's End")
</p>
@@ -125,7 +125,7 @@ async function searchAnimeDetails(query, malId = null) {
} else {
resultsContainer.innerHTML = `
<div class="no-results">
<p> Aucun résultat trouvé pour "${escapeHtml(query)}"</p>
<p><i class="fa-solid fa-xmark"></i> Aucun résultat trouvé pour "${escapeHtml(query)}"</p>
<p style="font-size: 12px; margin-top: 10px; color: #888;">
Essayez le nom en anglais ou japonais (ex: "Frieren: Beyond Journey's End", "One Piece")
</p>
@@ -138,7 +138,7 @@ async function searchAnimeDetails(query, malId = null) {
console.error('Error searching anime details:', error);
resultsContainer.innerHTML = `
<div class="no-results">
<p> Erreur lors de la recherche.</p>
<p><i class="fa-solid fa-xmark"></i> Erreur lors de la recherche.</p>
<p style="font-size: 12px; margin-top: 10px; color: #ff6b6b;">${error.message}</p>
</div>
`;
@@ -177,7 +177,7 @@ async function getProviderSearchResults(query) {
if (hasResults) {
htmlParts.unshift(
`<div class="streaming-results-header">
<h3>🎬 Résultats de streaming</h3>
<h3><i class="fa-solid fa-film"></i> Résultats de streaming</h3>
</div>
<div class="search-results" style="margin-top: 20px;">`
);
@@ -249,16 +249,16 @@ function renderAnimeDetails(anime) {
` : ''}
<div class="anime-details-meta">
${score > 0 ? `<div class="anime-details-rating"> ${score.toFixed(2)}</div>` : ''}
${score > 0 ? `<div class="anime-details-rating"><i class="fa-solid fa-star"></i> ${score.toFixed(2)}</div>` : ''}
${rank > 0 ? `<div class="anime-details-rank">#${rank}</div>` : ''}
${popularity > 0 ? `<div class="anime-details-popularity">Popularity #${popularity}</div>` : ''}
</div>
<div class="anime-details-stats">
${anime.episodes ? `<span>📺 ${anime.episodes} épisodes</span>` : ''}
${anime.status ? `<span>📡 ${translateStatus(anime.status)}</span>` : ''}
${anime.duration ? `<span>⏱️ ${escapeHtml(anime.duration)}</span>` : ''}
${anime.year ? `<span>📅 ${anime.year}</span>` : ''}
${anime.episodes ? `<span><i class="fa-solid fa-tv"></i> ${anime.episodes} épisodes</span>` : ''}
${anime.status ? `<span><i class="fa-solid fa-tower-broadcast"></i> ${translateStatus(anime.status)}</span>` : ''}
${anime.duration ? `<span><i class="fa-solid fa-clock"></i> ${escapeHtml(anime.duration)}</span>` : ''}
${anime.year ? `<span><i class="fa-solid fa-calendar"></i> ${anime.year}</span>` : ''}
</div>
${studios.length > 0 ? `
@@ -269,10 +269,10 @@ function renderAnimeDetails(anime) {
<div class="anime-details-actions">
<a href="${escapeHtml(anime.url)}" target="_blank" class="btn btn-secondary btn-small">
🔗 Voir sur MAL
<i class="fa-solid fa-link"></i> Voir sur MAL
</a>
<button onclick="searchAnimeOnProviders('${escapeHtml(anime.title)}')" class="btn btn-primary btn-small">
📥 Télécharger
<i class="fa-solid fa-download"></i> Télécharger
</button>
</div>
</div>
@@ -290,9 +290,9 @@ function renderAnimeDetails(anime) {
${synopsis ? `
<div class="anime-details-section">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h3 style="margin: 0;">📖 Synopsis</h3>
<h3 style="margin: 0;"><i class="fa-solid fa-book"></i> Synopsis</h3>
<button onclick="translateSynopsis('${synopsisId}', this)" class="btn btn-secondary btn-small" style="font-size: 12px;">
🌐 Traduire en français
<i class="fa-solid fa-globe"></i> Traduire en français
</button>
</div>
<p id="${synopsisId}" class="anime-details-synopsis">${escapeHtml(synopsis)}</p>
@@ -302,7 +302,7 @@ function renderAnimeDetails(anime) {
<!-- Seasons (Sequel/Prequel) -->
${seasons.length > 0 ? `
<div class="anime-details-section">
<h3>📺 Saisons</h3>
<h3><i class="fa-solid fa-tv"></i> Saisons</h3>
<div class="anime-related-list">
${seasons.map(season => `
<div class="anime-related-group">
@@ -310,7 +310,7 @@ function renderAnimeDetails(anime) {
<div class="anime-related-items">
${season.entries.map(entry => `
<div class="anime-related-item" onclick="searchAnimeDetails('${escapeHtml(entry.title)}', ${entry.mal_id})" style="cursor: pointer;">
${entry.type ? `<span style="color: #f15025; font-size: 11px; margin-right: 8px;">${escapeHtml(entry.type)}</span>` : ''}
${entry.type ? `<span style="color: #FFBF69; font-size: 11px; margin-right: 8px;">${escapeHtml(entry.type)}</span>` : ''}
${escapeHtml(entry.title)}
${entry.url ? `<a href="${escapeHtml(entry.url)}" target="_blank" style="margin-left: auto; color: #888; font-size: 18px; text-decoration: none;" title="Voir sur MyAnimeList">↗</a>` : ''}
</div>
@@ -358,7 +358,7 @@ async function loadStreamingResults(query) {
if (successfulResults.length === 0) {
container.innerHTML = `
<div class="no-results">
<p>⚠️ Aucun résultat de streaming trouvé pour "${escapeHtml(query)}"</p>
<p><i class="fa-solid fa-triangle-exclamation"></i> Aucun résultat de streaming trouvé pour "${escapeHtml(query)}"</p>
</div>
`;
return;
@@ -367,7 +367,7 @@ async function loadStreamingResults(query) {
// Display results
container.innerHTML = `
<div class="streaming-results-header">
<h3>🎬 Disponible sur</h3>
<h3><i class="fa-solid fa-film"></i> Disponible sur</h3>
</div>
<div class="streaming-results-grid">
${successfulResults.map(result => renderStreamingResult(result, query)).join('')}
@@ -378,7 +378,7 @@ async function loadStreamingResults(query) {
console.error('Error loading streaming results:', error);
container.innerHTML = `
<div class="no-results">
<p> Erreur lors de la recherche des sources de streaming.</p>
<p><i class="fa-solid fa-xmark"></i> Erreur lors de la recherche des sources de streaming.</p>
</div>
`;
}
@@ -406,7 +406,7 @@ function renderStreamingResult(result, query) {
</select>
<button class="btn btn-primary btn-small streaming-download-btn" onclick="downloadSelectedEpisode(this)">
📥 Télécharger
<i class="fa-solid fa-download"></i> Télécharger
</button>
</div>
@@ -475,7 +475,7 @@ async function translateSynopsis(synopsisId, button) {
// Revert to original
synopsisElement.textContent = originalText;
synopsisElement.dataset.translated = 'false';
button.innerHTML = '🌐 Traduire en français';
button.innerHTML = '<i class="fa-solid fa-globe"></i> Traduire en français';
return;
}
@@ -484,7 +484,7 @@ async function translateSynopsis(synopsisId, button) {
// Show loading state
button.disabled = true;
button.innerHTML = ' Traduction...';
button.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Traduction...';
synopsisElement.style.opacity = '0.5';
try {
@@ -509,7 +509,7 @@ async function translateSynopsis(synopsisId, button) {
synopsisElement.textContent = data.translatedText;
synopsisElement.dataset.translated = 'true';
button.innerHTML = '🔄 Voir l\'original';
button.innerHTML = '<i class="fa-solid fa-rotate"></i> Voir l\'original';
} else {
const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }));
console.error('Translation API error:', errorData);
@@ -523,7 +523,7 @@ async function translateSynopsis(synopsisId, button) {
const errorMessage = document.createElement('div');
errorMessage.style.cssText = 'margin-top: 10px; padding: 10px; background: rgba(255, 107, 107, 0.2); border-radius: 8px; font-size: 12px; color: #ff6b6b;';
errorMessage.innerHTML = `
⚠️ Service de traduction temporairement indisponible.<br>
<i class="fa-solid fa-triangle-exclamation"></i> Service de traduction temporairement indisponible.<br>
<small>Essayez à nouveau dans quelques instants.</small>
`;
+25 -25
View File
@@ -22,12 +22,12 @@ async function loadRecommendations() {
} else {
container.innerHTML = `
<div class="no-results">
<p>⚠️ Aucune recommandation disponible pour le moment.</p>
<p><i class="fa-solid fa-triangle-exclamation"></i> Aucune recommandation disponible pour le moment.</p>
<p style="font-size: 12px; margin-top: 10px; color: #888;">
Soit l'API MyAnimeList est inaccessible, soit vous n'avez pas encore de téléchargements.
</p>
<button class="btn btn-secondary btn-small" onclick="loadRecommendations()" style="margin-top: 10px;">
🔄 Réessayer
<i class="fa-solid fa-rotate"></i> Réessayer
</button>
</div>
`;
@@ -38,10 +38,10 @@ async function loadRecommendations() {
console.error('Error loading recommendations:', error);
container.innerHTML = `
<div class="no-results">
<p> Erreur lors du chargement des recommandations.</p>
<p><i class="fa-solid fa-xmark"></i> Erreur lors du chargement des recommandations.</p>
<p style="font-size: 12px; margin-top: 10px; color: #ff6b6b;">${error.message}</p>
<button class="btn btn-secondary btn-small" onclick="loadRecommendations()" style="margin-top: 10px;">
🔄 Réessayer
<i class="fa-solid fa-rotate"></i> Réessayer
</button>
</div>
`;
@@ -71,12 +71,12 @@ async function loadLatestReleases() {
} else {
container.innerHTML = `
<div class="no-results">
<p>⚠️ Aucune sortie disponible pour le moment.</p>
<p><i class="fa-solid fa-triangle-exclamation"></i> Aucune sortie disponible pour le moment.</p>
<p style="font-size: 12px; margin-top: 10px; color: #888;">
L'API MyAnimeList pourrait être temporairement inaccessible.
</p>
<button class="btn btn-secondary btn-small" onclick="loadLatestReleases()" style="margin-top: 10px;">
🔄 Réessayer
<i class="fa-solid fa-rotate"></i> Réessayer
</button>
</div>
`;
@@ -87,10 +87,10 @@ async function loadLatestReleases() {
console.error('Error loading releases:', error);
container.innerHTML = `
<div class="no-results">
<p> Erreur lors du chargement des sorties.</p>
<p><i class="fa-solid fa-xmark"></i> Erreur lors du chargement des sorties.</p>
<p style="font-size: 12px; margin-top: 10px; color: #ff6b6b;">${error.message}</p>
<button class="btn btn-secondary btn-small" onclick="loadLatestReleases()" style="margin-top: 10px;">
🔄 Réessayer
<i class="fa-solid fa-rotate"></i> Réessayer
</button>
</div>
`;
@@ -100,7 +100,7 @@ async function loadLatestReleases() {
// Load all home content
async function loadHomeContent() {
console.log('🏠 loadHomeContent() called');
console.log('loadHomeContent() called');
const loading = document.getElementById('homeLoading');
const recommendationsSection = document.getElementById('recommendationsSection');
@@ -123,13 +123,13 @@ async function loadHomeContent() {
loadRecommendations(),
loadLatestReleases()
]);
console.log('Home content loaded successfully');
console.log('Home content loaded successfully');
// Show sections if they have content
if (recommendationsSection) recommendationsSection.style.display = 'block';
if (releasesSection) releasesSection.style.display = 'block';
} catch (error) {
console.error('Error loading home content:', error);
console.error('Error loading home content:', error);
if (loading) {
loading.innerHTML = 'Erreur lors du chargement. Consultez la console pour plus de détails.';
}
@@ -149,11 +149,11 @@ function renderRecommendationCard(anime) {
return `
<div class="anime-card-horizontal recommendation-card">
${reason ? `<div class="recommendation-badge">💡 ${escapeHtml(reason)}</div>` : ''}
${reason ? `<div class="recommendation-badge"><i class="fa-solid fa-lightbulb"></i> ${escapeHtml(reason)}</div>` : ''}
<div class="anime-card-header">
<div class="anime-card-title">${escapeHtml(anime.title)}</div>
${score > 0 ? `<div class="anime-card-rating"> ${score.toFixed(1)}</div>` : ''}
${score > 0 ? `<div class="anime-card-rating"><i class="fa-solid fa-star"></i> ${score.toFixed(1)}</div>` : ''}
</div>
<div class="anime-card-content">
@@ -165,7 +165,7 @@ function renderRecommendationCard(anime) {
</div>
<div class="anime-card-meta">
${anime.episodes ? `📺 ${anime.episodes} ep` : ''}
${anime.episodes ? `<i class="fa-solid fa-tv"></i> ${anime.episodes} ep` : ''}
${anime.episodes && anime.status ? ' • ' : ''}
${anime.status ? translateStatus(anime.status) : ''}
</div>
@@ -174,17 +174,17 @@ function renderRecommendationCard(anime) {
${anime.synopsis ? `
<details class="anime-synopsis">
<summary>📖 Synopsis</summary>
<summary><i class="fa-solid fa-book"></i> Synopsis</summary>
<p>${escapeHtml(anime.synopsis.substring(0, 150))}${anime.synopsis.length > 150 ? '...' : ''}</p>
</details>
` : ''}
<div class="anime-card-actions">
<button class="btn btn-secondary btn-small" onclick="window.open('${escapeHtml(anime.url)}', '_blank')">
🔗 MAL
<i class="fa-solid fa-link"></i> MAL
</button>
<button class="btn btn-primary btn-small" onclick="searchAnimeOnProviders('${escapeHtml(anime.title)}')">
📥 Télécharger
<i class="fa-solid fa-download"></i> Télécharger
</button>
</div>
</div>
@@ -202,11 +202,11 @@ function renderReleaseCard(anime) {
return `
<div class="anime-card-horizontal release-card">
<div class="release-badge">🔥 ${escapeHtml(releaseType)}</div>
<div class="release-badge"><i class="fa-solid fa-fire"></i> ${escapeHtml(releaseType)}</div>
<div class="anime-card-header">
<div class="anime-card-title">${escapeHtml(anime.title)}</div>
${score > 0 ? `<div class="anime-card-rating"> ${score.toFixed(1)}</div>` : ''}
${score > 0 ? `<div class="anime-card-rating"><i class="fa-solid fa-star"></i> ${score.toFixed(1)}</div>` : ''}
</div>
<div class="anime-card-content">
@@ -218,7 +218,7 @@ function renderReleaseCard(anime) {
</div>
<div class="anime-card-meta">
${anime.episodes ? `📺 ${anime.episodes} ep` : ''}
${anime.episodes ? `<i class="fa-solid fa-tv"></i> ${anime.episodes} ep` : ''}
${anime.episodes && anime.status ? ' • ' : ''}
${anime.status ? translateStatus(anime.status) : ''}
</div>
@@ -227,17 +227,17 @@ function renderReleaseCard(anime) {
${anime.synopsis ? `
<details class="anime-synopsis">
<summary>📖 Synopsis</summary>
<summary><i class="fa-solid fa-book"></i> Synopsis</summary>
<p>${escapeHtml(anime.synopsis.substring(0, 150))}${anime.synopsis.length > 150 ? '...' : ''}</p>
</details>
` : ''}
<div class="anime-card-actions">
<button class="btn btn-secondary btn-small" onclick="window.open('${escapeHtml(anime.url)}', '_blank')">
🔗 MAL
<i class="fa-solid fa-link"></i> MAL
</button>
<button class="btn btn-primary btn-small" onclick="searchAnimeOnProviders('${escapeHtml(anime.title)}')">
📥 Télécharger
<i class="fa-solid fa-download"></i> Télécharger
</button>
</div>
</div>
@@ -248,9 +248,9 @@ function renderReleaseCard(anime) {
function getRatingColor(score) {
if (score >= 9) return '#ffd700';
if (score >= 8) return '#2d936c';
if (score >= 7) return '#f15025';
if (score >= 7) return '#FF9F1C';
if (score >= 6) return '#f4a261';
return '#ced0ce';
return '#888888';
}
// Search anime on providers (redirects to anime tab)
+12 -12
View File
@@ -26,7 +26,7 @@ async function handleSeriesSearch() {
const series = data.results['fs7'];
let html = `
<div class="streaming-results-header">
<h3>📺 Résultats pour "${escapeHtml(query)}"</h3>
<h3><i class="fa-solid fa-tv"></i> Résultats pour "${escapeHtml(query)}"</h3>
</div>
<div class="search-results" style="margin-top: 20px;">
`;
@@ -46,7 +46,7 @@ async function handleSeriesSearch() {
<div class="anime-card" id="series-fs7-${encodeURIComponent(s.url)}">
<div class="anime-card-header">
<div class="anime-card-title">${escapeHtml(s.title)}</div>
<div class="anime-card-provider">📺 French Stream</div>
<div class="anime-card-provider"><i class="fa-solid fa-tv"></i> French Stream</div>
</div>
${coverImage ? `
<div style="text-align: center; margin: 10px 0;">
@@ -55,10 +55,10 @@ async function handleSeriesSearch() {
` : ''}
<div class="anime-card-actions">
<button class="btn btn-secondary btn-small" onclick="window.open('${escapeHtml(s.url)}', '_blank')">
🔗 Voir sur FS7
<i class="fa-solid fa-link"></i> Voir sur FS7
</button>
<button class="btn btn-primary btn-small" onclick="loadSeriesEpisodesDirect('${escapeHtml(s.url)}', '${escapeHtml(s.title)}')">
📥 Voir les épisodes
<i class="fa-solid fa-download"></i> Voir les épisodes
</button>
</div>
<div id="episodes-fs7-${encodeURIComponent(s.url)}" style="margin-top: 10px;"></div>
@@ -71,7 +71,7 @@ async function handleSeriesSearch() {
} else {
resultsContainer.innerHTML = `
<div class="no-results">
<p> Aucune série trouvée pour "${escapeHtml(query)}"</p>
<p><i class="fa-solid fa-xmark"></i> Aucune série trouvée pour "${escapeHtml(query)}"</p>
<p style="font-size: 12px; margin-top: 10px; opacity: 0.7;">
Essayez avec un autre titre ou vérifiez l'orthographe
</p>
@@ -81,7 +81,7 @@ async function handleSeriesSearch() {
console.error('Error searching series:', error);
resultsContainer.innerHTML = `
<div class="no-results">
<p> Erreur lors de la recherche</p>
<p><i class="fa-solid fa-xmark"></i> Erreur lors de la recherche</p>
<p style="font-size: 12px; margin-top: 10px; color: #ff6b6b;">${error.message}</p>
</div>`;
}
@@ -102,10 +102,10 @@ async function loadSeriesEpisodesDirect(url, title) {
if (data.episodes && data.episodes.length > 0) {
let html = `
<div style="margin-top: 15px;">
<label style="font-size: 12px; color: #f15025; margin-bottom: 5px; display: block;">
📺 Sélectionner un épisode:
<label style="font-size: 12px; color: #FF9F1C; margin-bottom: 5px; display: block;">
<i class="fa-solid fa-tv"></i> Sélectionner un épisode:
</label>
<select id="select-episodes-${encodeURIComponent(url)}" style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ced0ce; background: #ffffff; color: #191919;">
<select id="select-episodes-${encodeURIComponent(url)}" style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #2a2d32; background: #202327; color: #F2F2F2;">
<option value="">Sélectionner un épisode</option>
${data.episodes.map(ep => `
<option value="${escapeHtml(ep.url)}">Épisode ${escapeHtml(ep.episode)}</option>
@@ -145,7 +145,7 @@ async function downloadSeriesEpisode(url, title) {
});
if (response.ok) {
alert(`Téléchargement démarré pour "${title}"`);
alert(`Téléchargement démarré pour "${title}"`);
// Refresh downloads
if (typeof loadDownloads === 'function') {
loadDownloads();
@@ -155,11 +155,11 @@ async function downloadSeriesEpisode(url, title) {
const errorMessage = error.detail
? (typeof error.detail === 'string' ? error.detail : JSON.stringify(error.detail))
: 'Impossible de démarrer le téléchargement';
alert(`Erreur: ${errorMessage}`);
alert(`Erreur : ${errorMessage}`);
}
} catch (error) {
console.error('Download error:', error);
alert(`Erreur lors du téléchargement: ${error.message}`);
alert(`Erreur lors du téléchargement : ${error.message}`);
}
}
+17 -17
View File
@@ -19,7 +19,7 @@ function renderSeriesRecommendationCard(series) {
return `
<div class="anime-card-horizontal recommendation-card">
<div class="recommendation-badge">🎺 Série TV populaire</div>
<div class="recommendation-badge"><i class="fa-solid fa-music"></i> Série TV populaire</div>
<div class="anime-card-header">
<div class="anime-card-title">${escapeHtml(series.title)}</div>
@@ -30,17 +30,17 @@ function renderSeriesRecommendationCard(series) {
<div class="anime-card-info">
<div class="anime-card-meta">
📺 Série TV
<i class="fa-solid fa-tv"></i> Série TV
</div>
</div>
</div>
<div class="anime-card-actions">
<button class="btn btn-secondary btn-small" onclick="window.open('${escapeHtml(series.url)}', '_blank')">
🔗 Voir sur FS7
<i class="fa-solid fa-link"></i> Voir sur FS7
</button>
<button class="btn btn-primary btn-small" onclick="loadSeriesEpisodes('${escapeHtml(series.url)}', '${escapeHtml(series.title)}')">
📥 Voir les épisodes
<i class="fa-solid fa-download"></i> Voir les épisodes
</button>
</div>
</div>
@@ -92,17 +92,17 @@ function renderSeriesReleaseCard(series) {
<div class="anime-card-info">
<div class="anime-card-meta">
📺 Série TV • Nouveau
<i class="fa-solid fa-tv"></i> Série TV • Nouveau
</div>
</div>
</div>
<div class="anime-card-actions">
<button class="btn btn-secondary btn-small" onclick="window.open('${escapeHtml(series.url)}', '_blank')">
🔗 Voir sur FS7
<i class="fa-solid fa-link"></i> Voir sur FS7
</button>
<button class="btn btn-primary btn-small" onclick="loadSeriesEpisodes('${escapeHtml(series.url)}', '${escapeHtml(series.title)}')">
📥 Voir les épisodes
<i class="fa-solid fa-download"></i> Voir les épisodes
</button>
</div>
</div>
@@ -236,10 +236,10 @@ async function loadSeriesReleases() {
if (container) {
container.innerHTML = `
<div class="no-results">
<p> Erreur lors du chargement des séries</p>
<p><i class="fa-solid fa-xmark"></i> Erreur lors du chargement des séries</p>
<p style="font-size: 12px; margin-top: 10px; color: #ff6b6b;">${error.message}</p>
<button class="btn btn-secondary btn-small" onclick="loadSeriesReleases()" style="margin-top: 10px;">
🔄 Réessayer
<i class="fa-solid fa-rotate"></i> Réessayer
</button>
</div>`;
}
@@ -260,7 +260,7 @@ async function loadProvidersGrid() {
let html = '';
// Section Anime providers
html += '<div class="section-header"><h3 style="margin-top: 20px;">🎬 Sites Anime</h3></div>';
html += '<div class="section-header"><h3 style="margin-top: 20px;"><i class="fa-solid fa-film"></i> Sites Anime</h3></div>';
html += '<div class="search-results">';
const animeProviders = Object.entries(data.anime_providers || {});
@@ -281,11 +281,11 @@ async function loadProvidersGrid() {
<div class="anime-card-actions">
${domains.length > 0 ? `
<button class="btn btn-primary btn-small" onclick="window.open('https://${domains[0]}', '_blank')">
🔗 Visiter le site
<i class="fa-solid fa-link"></i> Visiter le site
</button>
` : ''}
<button class="btn btn-secondary btn-small" onclick="showProviderSearch('${id}')">
🔍 Rechercher
<i class="fa-solid fa-magnifying-glass"></i> Rechercher
</button>
</div>
</div>
@@ -298,7 +298,7 @@ async function loadProvidersGrid() {
html += '</div>';
// Section File hosts
html += '<div class="section-header" style="margin-top: 40px;"><h3>💾 Hébergeurs de fichiers</h3></div>';
html += '<div class="section-header" style="margin-top: 40px;"><h3><i class="fa-solid fa-floppy-disk"></i> Hébergeurs de fichiers</h3></div>';
html += '<div class="search-results">';
const fileHosts = Object.entries(data.file_hosts || {});
@@ -311,7 +311,7 @@ async function loadProvidersGrid() {
</div>
<div class="anime-card-actions">
<button class="btn btn-secondary btn-small" onclick="showDownloadInfo()">
📥 Télécharger un fichier
<i class="fa-solid fa-download"></i> Télécharger un fichier
</button>
</div>
</div>
@@ -330,10 +330,10 @@ async function loadProvidersGrid() {
if (container) {
container.innerHTML = `
<div class="no-results">
<p> Erreur lors du chargement des fournisseurs</p>
<p><i class="fa-solid fa-xmark"></i> Erreur lors du chargement des fournisseurs</p>
<p style="font-size: 12px; margin-top: 10px; color: #ff6b6b;">${error.message}</p>
<button class="btn btn-secondary btn-small" onclick="loadProvidersGrid()" style="margin-top: 10px;">
🔄 Réessayer
<i class="fa-solid fa-rotate"></i> Réessayer
</button>
</div>
`;
@@ -349,7 +349,7 @@ function showProviderSearch(providerId) {
// Show download info (explains how to download)
function showDownloadInfo() {
alert('💡 Pour télécharger un fichier:\n\n1. Utilisez l\'onglet "Recherche"\n2. Entrez le nom de l\'anime/série\n3. Cliquez sur "Télécharger" sur un épisode\n\nOu bien:\n- Copiez directement un lien de téléchargement dans la barre d\'adresse de votre navigateur');
alert('Pour télécharger un fichier:\n\n1. Utilisez l\'onglet "Recherche"\n2. Entrez le nom de l\'anime/série\n3. Cliquez sur "Télécharger" sur un épisode\n\nOu bien:\n- Copiez directement un lien de téléchargement dans la barre d\'adresse de votre navigateur');
}
// Make additional functions available globally
+9 -9
View File
@@ -346,10 +346,10 @@ async function handleStartScheduler() {
try {
await startScheduler();
await loadSchedulerStatus();
alert('Planificateur démarré!');
alert('Planificateur démarré !');
} catch (error) {
console.error('Error starting scheduler:', error);
alert(`Erreur: ${error.message}`);
alert(`Erreur : ${error.message}`);
}
}
@@ -360,10 +360,10 @@ async function handleStopScheduler() {
try {
await stopScheduler();
await loadSchedulerStatus();
alert('Planificateur arrêté!');
alert('Planificateur arrêté !');
} catch (error) {
console.error('Error stopping scheduler:', error);
alert(`Erreur: ${error.message}`);
alert(`Erreur : ${error.message}`);
}
}
@@ -376,7 +376,7 @@ async function handleCheckAll() {
await loadSchedulerStatus();
} catch (error) {
console.error('Error checking all:', error);
alert(`Erreur: ${error.message}`);
alert(`Erreur : ${error.message}`);
}
}
@@ -394,7 +394,7 @@ async function handleOpenSettings() {
document.body.appendChild(modalContainer);
} catch (error) {
console.error('Error loading settings:', error);
alert(`Erreur: ${error.message}`);
alert(`Erreur : ${error.message}`);
}
}
@@ -438,17 +438,17 @@ function updateSchedulerUI(status) {
if (status.next_run) {
const nextRun = new Date(status.next_run);
nextRunInfo.innerHTML = ` En cours<br>Prochaine vérification: ${nextRun.toLocaleString('fr-FR')}`;
nextRunInfo.innerHTML = `<i class="fa-solid fa-check"></i> En cours<br>Prochaine vérification: ${nextRun.toLocaleString('fr-FR')}`;
} else {
// Scheduler running but no next_run yet (just started)
const interval = status.settings?.check_interval_hours || 6;
nextRunInfo.innerHTML = ` En cours<br>Vérification toutes les ${interval}h`;
nextRunInfo.innerHTML = `<i class="fa-solid fa-check"></i> En cours<br>Vérification toutes les ${interval}h`;
}
} else {
// Update buttons if they exist
if (startBtn) startBtn.style.display = 'inline-block';
if (stopBtn) stopBtn.style.display = 'none';
nextRunInfo.innerHTML = '⏸️ Arrêté';
nextRunInfo.innerHTML = '<i class="fa-solid fa-pause"></i> Arrêté';
}
}
+1
View File
@@ -7,6 +7,7 @@
<!-- 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://cdn.plyr.io/3.7.8/plyr.css" />
<!-- External Libraries -->
+8 -8
View File
@@ -5,23 +5,23 @@
<!-- 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="admin-stat-card" style="padding: 20px; background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid #ced0ce; text-align: center;">
<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 style="font-size: 2rem; font-weight: 800; color: var(--primary);" id="stat-total-users">{{ users|length }}</div>
<div style="color: var(--text-dim); font-size: 0.85rem;">Utilisateurs</div>
</div>
<div class="admin-stat-card" style="padding: 20px; background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid #ced0ce; text-align: center;">
<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 style="font-size: 2rem; font-weight: 800; color: var(--accent);" id="stat-active-users">{{ users|selectattr('is_active')|list|length }}</div>
<div style="color: var(--text-dim); font-size: 0.85rem;">Actifs</div>
</div>
<div class="admin-stat-card" style="padding: 20px; background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid #ced0ce; text-align: center;">
<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 style="font-size: 2rem; font-weight: 800; color: #f0a500;" id="stat-admin-users">{{ users|selectattr('is_admin')|list|length }}</div>
<div style="color: var(--text-dim); font-size: 0.85rem;">Admins</div>
</div>
</div>
<!-- Users Table -->
<div style="background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid #ced0ce; overflow: hidden;">
<div style="padding: 20px 25px; border-bottom: 1px solid #ced0ce;">
<div style="background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid #2a2d32;overflow: hidden;">
<div style="padding: 20px 25px; border-bottom: 1px solid #2a2d32;">
<h3 style="margin: 0; color: var(--primary);">Gestion des utilisateurs</h3>
</div>
@@ -29,7 +29,7 @@
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="border-bottom: 1px solid #ced0ce;">
<tr style="border-bottom: 1px solid #2a2d32;">
<th style="padding: 12px 20px; text-align: left; color: var(--text-dim); font-size: 0.8rem; text-transform: uppercase;">Utilisateur</th>
<th style="padding: 12px 15px; text-align: left; color: var(--text-dim); font-size: 0.8rem; text-transform: uppercase;">Email</th>
<th style="padding: 12px 15px; text-align: center; color: var(--text-dim); font-size: 0.8rem; text-transform: uppercase;">Statut</th>
@@ -41,7 +41,7 @@
</thead>
<tbody>
{% for user in users %}
<tr style="border-bottom: 1px solid #ced0ce; {% if not user.is_active %}opacity: 0.5;{% endif %}">
<tr style="border-bottom: 1px solid #2a2d32; {% if not user.is_active %}opacity: 0.5;{% endif %}">
<td style="padding: 12px 20px;">
<div style="font-weight: 600;">{{ user.username }}</div>
{% if user.full_name %}
@@ -55,7 +55,7 @@
</span>
</td>
<td style="padding: 12px 15px; text-align: 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 %}#e6e8e6; color: var(--text-dim){% endif %};">
<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 %}Admin{% else %}User{% endif %}
</span>
</td>
+10 -10
View File
@@ -1,4 +1,4 @@
{% set accent = "#f15025" %}
{% set accent = "#FF9F1C" %}
{% set default_lang = settings.default_lang if settings else 'vostfr' %}
{% set _groups = namespace(items={}) %}
@@ -36,9 +36,9 @@
{% set first_url = group.providers[0].url %}
<div class="sr-card" style="--sr-accent: {{ accent }};">
<a class="sr-poster-link" href="{{ first_url }}" target="_blank" rel="noopener">
<img class="sr-poster-img" src="{{ group.cover or 'https://placehold.co/240x360/e6e8e6/f15025?text=No+Image' }}"
<img class="sr-poster-img" src="{{ group.cover or 'https://placehold.co/240x360/29274c/7e52a0?text=No+Image' }}"
alt="{{ group.title }}" loading="lazy" referrerpolicy="no-referrer"
onerror="this.src='https://placehold.co/240x360/e6e8e6/f15025?text=Error'; this.onerror=null;">
onerror="this.src='https://placehold.co/240x360/29274c/7e52a0?text=Error'; this.onerror=null;">
</a>
<div class="sr-body">
<div class="sr-top">
@@ -114,7 +114,7 @@
.sr-card {
display: flex; gap: 20px;
background: var(--bg-card); border-radius: var(--card-radius);
padding: 20px; border: 1px solid #ced0ce;
padding: 20px; border: 1px solid #2a2d32;"
transition: var(--transition);
}
.sr-card:hover { border-color: var(--sr-accent); }
@@ -126,24 +126,24 @@
.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: #e6e8e6; color: var(--text-dim); }
.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 #ced0ce; cursor: pointer; transition: var(--transition); text-decoration: none; background: transparent; color: var(--text-main); min-height: 34px; }
.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: #191919; }
.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(241,80,37,0.1); pointer-events: none; }
.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 #ced0ce; border-radius: 4px; padding: 4px; z-index: 100; }
.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: #ced0ce; }
.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) {
+7 -7
View File
@@ -60,7 +60,7 @@
background: var(--bg-card);
border-radius: var(--card-radius);
padding: 30px;
border: 1px solid #ced0ce;
border: 1px solid var(--secondary);
animation: fadeIn 0.3s ease-out;
}
@@ -71,19 +71,19 @@
}
.view-grid .episode-item {
background: #ffffff;
background: var(--bg-elevated);
padding: 20px 15px;
border-radius: 4px;
text-align: center;
transition: var(--transition);
border: 1px solid #ced0ce;
border: 1px solid var(--secondary);
display: flex;
flex-direction: column;
gap: 12px;
}
.view-grid .episode-item:hover {
background: #e6e8e6;
background: var(--text-dim);
border-color: var(--primary);
}
@@ -102,15 +102,15 @@
display: flex;
align-items: center;
gap: 20px;
background: #ffffff;
background: var(--bg-elevated);
padding: 12px 20px;
border-radius: 4px;
border: 1px solid #ced0ce;
border: 1px solid var(--secondary);
transition: var(--transition);
}
.view-list .episode-item:hover {
background: #e6e8e6;
background: var(--text-dim);
border-color: var(--primary);
}
+4 -4
View File
@@ -1,25 +1,25 @@
<header>
<h1> Ohm Stream Downloader</h1>
<h1><i class="fa-solid fa-bolt"></i> Ohm Stream Downloader</h1>
<p class="subtitle">Téléchargez vos vidéos, animes et séries depuis vos hébergeurs préférés</p>
<!-- User info and logout button -->
<div id="userInfo" x-show="isAuthenticated" class="auth-panel" x-cloak>
<div style="display: flex; align-items: center; gap: 10px;">
<span style="color: var(--primary); font-size: 1.2rem;">👤</span>
<span style="color: var(--primary); font-size: 1.2rem;"><i class="fa-solid fa-user"></i></span>
<span style="color: var(--text-main); font-size: 14px;">Connecté en tant que <strong x-text="username" style="color: var(--primary);">-</strong></span>
</div>
<button class="btn btn-secondary btn-small"
onclick="removeToken(); isAuthenticated = false"
hx-post="/api/auth/logout"
hx-on::after-request="window.location.href = '/login'">
🚪 Déconnexion
<i class="fa-solid fa-right-from-bracket"></i> Déconnexion
</button>
</div>
<!-- Login prompt (shown when not logged in) -->
<div id="loginPrompt" x-show="!isAuthenticated" class="auth-panel" style="justify-content: center;" x-cloak>
<p style="color: var(--primary); margin: 0;">
👋 Bienvenue! <a href="/login" class="btn btn-secondary btn-small" style="margin-left: 10px;">Se connecter</a> pour accéder à toutes les fonctionnalités.
<i class="fa-solid fa-hand"></i> Bienvenue! <a href="/login" class="btn btn-secondary btn-small" style="margin-left: 10px;">Se connecter</a> pour accéder à toutes les fonctionnalités.
</p>
</div>
+2 -2
View File
@@ -2,7 +2,7 @@
<div class="section-container">
<div class="section-header">
<h2>🎯 Recommandé pour vous</h2>
<h2><i class="fa-solid fa-bullseye"></i> Recommandé pour vous</h2>
<button class="btn btn-secondary btn-small"
hx-get="/api/recommendations"
hx-target="#recommendationsList">
@@ -19,7 +19,7 @@
<div class="section-container">
<div class="section-header">
<h2>🔥 Dernières sorties</h2>
<h2><i class="fa-solid fa-fire"></i> Dernières sorties</h2>
<button class="btn btn-secondary btn-small"
hx-get="/api/releases/latest"
hx-target="#releasesList">
+2 -2
View File
@@ -1,4 +1,4 @@
<div class="login-prompt" style="text-align: center; padding: 40px 20px;">
<i class="fas fa-lock" style="font-size: 2rem; color: #f15025; margin-bottom: 15px;"></i>
<p style="color: #ced0ce; font-size: 0.95rem;">Connectez-vous pour accéder à cette section.</p>
<i class="fa-solid fa-lock" style="font-size: 2rem; color: #FF9F1C; margin-bottom: 15px;"></i>
<p style="color: #8a8f98; font-size: 0.95rem;">Connectez-vous pour accéder à cette section.</p>
</div>
+3 -3
View File
@@ -19,7 +19,7 @@
mozallowfullscreen></iframe>
</div>
<div class="player-info-hint">
💡 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>
{% else %}
<div class="video-wrapper">
@@ -42,7 +42,7 @@
padding: 15px;
background: #000;
border-radius: 4px;
border: 1px solid #ced0ce;
border: 1px solid #2a2d32;
}
.iframe-container {
position: relative;
@@ -65,7 +65,7 @@
}
.player-info-hint {
font-size: 0.8rem;
color: #888;
color: var(--text-dim);
margin-top: 10px;
text-align: center;
}
@@ -1,4 +1,4 @@
{% set accent = "#f15025" %}
{% set accent = "#FF9F1C" %}
{% set default_lang = settings.default_lang if settings else 'vf' %}
{% set _groups = namespace(items={}) %}
@@ -28,9 +28,9 @@
{% set first_url = group.providers[0].url %}
<div class="sr-card" style="--sr-accent: {{ accent }};">
<a class="sr-poster-link" href="{{ first_url }}" target="_blank" rel="noopener">
<img class="sr-poster-img" src="{{ group.cover or 'https://placehold.co/240x360/e6e8e6/f15025?text=No+Image' }}"
<img class="sr-poster-img" src="{{ group.cover or 'https://placehold.co/240x360/29274c/7e52a0?text=No+Image' }}"
alt="{{ group.title }}" loading="lazy" referrerpolicy="no-referrer"
onerror="this.src='https://placehold.co/240x360/e6e8e6/f15025?text=Error'; this.onerror=null;">
onerror="this.src='https://placehold.co/240x360/29274c/7e52a0?text=Error'; this.onerror=null;">
</a>
<div class="sr-body">
<h3 class="sr-title">{{ group.title }}</h3>
@@ -93,7 +93,7 @@
.sr-card {
display: flex; gap: 20px;
background: var(--bg-card); border-radius: var(--card-radius);
padding: 20px; border: 1px solid #ced0ce;
padding: 20px; border: 1px solid #2a2d32;"
transition: var(--transition);
}
.sr-card:hover { border-color: var(--sr-accent); }
@@ -106,19 +106,19 @@
.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 #ced0ce; cursor: pointer; transition: var(--transition); text-decoration: none; background: transparent; color: var(--text-main); min-height: 34px; }
.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: #191919; }
.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(241,80,37,0.1); pointer-events: none; }
.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 #ced0ce; border-radius: 4px; padding: 4px; z-index: 100; }
.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: #ced0ce; }
.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) {
+8 -8
View File
@@ -4,7 +4,7 @@
</div>
<!-- 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 #ced0ce;">
<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: 20px; color: var(--primary);">General</h3>
<form id="settings-form" class="settings-form">
@@ -43,7 +43,7 @@
</div>
<!-- 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 #ced0ce;">
<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: 20px; color: var(--primary);">Filtres de contenu</h3>
<div class="form-group">
@@ -66,12 +66,12 @@
</div>
<!-- Categories -->
<div class="settings-card card" style="margin-bottom: 30px; padding: 25px; background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid #ced0ce;">
<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: 20px; color: var(--primary);">Categories</h3>
<p style="color: var(--text-dim); font-size: 13px; margin-bottom: 15px;">Activez ou desactivez les categories. Au moins une doit rester active.</p>
<div style="display: flex; gap: 15px; flex-wrap: wrap;">
<label class="toggle-card" style="flex: 1; min-width: 200px; padding: 15px; background: #ffffff; border-radius: 4px; border: 1px solid #ced0ce; display: flex; align-items: center; justify-content: space-between; cursor: pointer;">
<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;">
<div>
<div style="font-weight: 600; font-size: 1.1rem;">Animes</div>
<div style="font-size: 0.8rem; color: var(--text-dim);">Films et series anime</div>
@@ -79,7 +79,7 @@
<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>
<label class="toggle-card" style="flex: 1; min-width: 200px; padding: 15px; background: #ffffff; border-radius: 4px; border: 1px solid #ced0ce; display: flex; align-items: center; justify-content: space-between; cursor: pointer;">
<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;">
<div>
<div style="font-weight: 600; font-size: 1.1rem;">Series TV</div>
<div style="font-size: 0.8rem; color: var(--text-dim);">Series americaines et europeennes</div>
@@ -90,7 +90,7 @@
</div>
<!-- Providers Management -->
<div class="settings-card card" style="padding: 25px; background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid #ced0ce;">
<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;">
<h3 style="margin: 0; color: var(--primary);">Disponibilite des Fournisseurs</h3>
<button class="btn btn-secondary btn-small" hx-post="/api/providers/health/check" hx-swap="none">
@@ -100,13 +100,13 @@
<div class="providers-settings-list" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px;">
{% for provider in providers %}
<div class="provider-status-card" style="padding: 15px; background: #ffffff; border-radius: 4px; border: 1px solid #ced0ce; display: flex; align-items: center; justify-content: space-between;">
<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 style="display: flex; align-items: center; gap: 12px;">
<span style="font-size: 1.5rem;">{{ provider.icon }}</span>
<div>
<div style="font-weight: 600;">{{ provider.name }}</div>
<div style="font-size: 0.75rem; display: flex; align-items: center; gap: 5px;">
<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 %}#aaa{% endif %};"></span>
<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>
<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;">
{{ provider.status | upper }}
</span>
+5 -5
View File
@@ -38,17 +38,17 @@
min-width: 250px;
padding: 12px 16px;
border-radius: 4px;
background: #e6e8e6;
color: #191919;
border: 1px solid #ced0ce;
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 #ced0ce;
border-left: 4px solid var(--secondary);
}
.toast-success { border-left-color: #2d936c; }
.toast-error { border-left-color: #e63946; }
.toast-info { border-left-color: #f15025; }
.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>
+16 -22
View File
@@ -167,7 +167,7 @@
.filter-tabs {
display: flex;
gap: 8px;
border-bottom: 1px solid #ced0ce;
border-bottom: 1px solid #2a2d32;
padding-bottom: 12px;
margin-bottom: 8px;
}
@@ -188,7 +188,7 @@
}
.filter-tab:hover {
background: #e6e8e6;
background: var(--bg-elevated);
color: var(--text-main);
}
@@ -211,7 +211,7 @@
background: var(--bg-card);
border-radius: var(--card-radius);
padding: 16px;
border: 1px solid #ced0ce;
border: 1px solid #2a2d32;
transition: var(--transition);
}
@@ -258,16 +258,16 @@
.poster-badge.paused {
background: #f4a261;
color: #191919;
color: #15171A;
}
.poster-badge.completed {
background: #9c27b0;
color: #fff;
background: #FF9F1C;
color: #15171A;
}
.poster-badge.archived {
background: #ced0ce;
background: rgba(206, 208, 206, 0.2);
color: var(--text-dim);
}
@@ -277,7 +277,7 @@
left: 8px;
padding: 4px 10px;
border-radius: 20px;
background: #f15025;
background: #FF9F1C;
color: var(--bg-dark);
font-size: 0.65rem;
font-weight: 700;
@@ -325,9 +325,9 @@
}
.meta-provider {
background: rgba(241, 80, 37, 0.1);
background: rgba(255, 191, 105, 0.1);
color: var(--primary);
border: 1px solid rgba(241, 80, 37, 0.3);
border: 1px solid rgba(255, 191, 105, 0.3);
}
.meta-lang {
@@ -373,7 +373,7 @@
gap: 6px;
margin-top: auto;
padding-top: 8px;
border-top: 1px solid #ced0ce;
border-top: 1px solid #2a2d32;
}
.action-btn {
@@ -387,18 +387,12 @@
font-size: 0.85rem;
cursor: pointer;
transition: var(--transition);
background: #e6e8e6;
border: none;
border-radius: 6px;
font-size: 0.85rem;
cursor: pointer;
transition: var(--transition);
background: #e6e8e6;
background: var(--bg-elevated);
color: var(--text-dim);
}
.action-btn:hover {
background: #ced0ce;
background: var(--secondary);
color: var(--text-main);
}
@@ -419,11 +413,11 @@
}
.btn-complete {
color: #9c27b0;
color: #FFBF69;
}
.btn-complete:hover {
background: rgba(156, 39, 176, 0.15);
background: rgba(255, 191, 105, 0.15);
}
.btn-delete {
@@ -440,7 +434,7 @@
padding: 80px 40px;
background: var(--bg-card);
border-radius: var(--card-radius);
border: 1px dashed #ced0ce;
border: 1px dashed #2a2d32;
}
.watchlist-empty i {
+5 -5
View File
@@ -1,6 +1,6 @@
<div class="section-container">
<div class="section-header">
<h2>📋 Ma Watchlist</h2>
<h2><i class="fa-solid fa-clipboard-list"></i> Ma Watchlist</h2>
<div class="header-actions">
<button class="btn btn-sm btn-primary" hx-post="/api/watchlist/check" hx-swap="none">
<i class="fas fa-sync"></i> Vérifier épisodes
@@ -35,15 +35,15 @@
display: flex;
gap: 15px;
padding: 15px;
background: #e6e8e6;
background: var(--bg-card);
border-radius: 4px;
border: 1px solid #ced0ce;
border: 1px solid var(--secondary);
transition: border-color 0.2s;
}
.watchlist-item:hover { border-color: #f15025; }
.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: #191919; }
.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>
+2 -2
View File
@@ -46,7 +46,7 @@
<!-- Player container for HTMX injections -->
<div id="player-container"></div>
<hr style="border: none; border-top: 1px solid #ced0ce; margin: 40px 0;">
<hr style="border: none; border-top: 1px solid #2a2d32; margin: 40px 0;">
<!-- Latest Releases Section - Anime only -->
<div class="section-header">
@@ -97,7 +97,7 @@
<!-- Series search results -->
<div id="seriesSearchResults" style="margin-bottom: 40px;"></div>
<hr style="border: none; border-top: 1px solid #ced0ce; margin: 40px 0;">
<hr style="border: none; border-top: 1px solid #2a2d32; margin: 40px 0;">
<!-- Recommendations Section - Series only -->
<div class="section-header">
+1 -1
View File
@@ -8,7 +8,7 @@
</head>
<body>
<div class="auth-container">
<h1 class="auth-title">🎬 Ohm Stream</h1>
<h1 class="auth-title"><i class="fa-solid fa-film"></i> Ohm Stream</h1>
<div class="auth-tabs">
<div class="auth-tab active" data-tab="login">Connexion</div>
+19 -19
View File
@@ -14,13 +14,13 @@
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: #ffffff;
background: #15171A;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
color: #191919;
color: #F2F2F2;
}
.container {
@@ -36,14 +36,14 @@
.header h1 {
font-size: 1.5rem;
margin-bottom: 10px;
color: #f15025;
color: #FFBF69;
}
.video-info {
background: #e6e8e6;
background: #202327;
padding: 15px 20px;
border-radius: 4px;
border: 1px solid #ced0ce;
border: 1px solid #2a2d32;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
@@ -58,7 +58,7 @@
}
.video-info .filesize {
color: #ced0ce;
color: #8a8f98;
font-size: 0.9rem;
}
@@ -82,9 +82,9 @@
.btn {
padding: 12px 24px;
background: #e6e8e6;
border: 1px solid #ced0ce;
color: #191919;
background: #202327;
border: 1px solid #2a2d32;
color: #F2F2F2;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
@@ -96,20 +96,20 @@
}
.btn:hover {
background: #ced0ce;
border-color: #191919;
background: #2a2d32;
border-color: #FFBF69;
}
.btn-primary {
background: #f15025;
border: 1px solid #f15025;
background: #FF9F1C;
border: 1px solid #FF9F1C;
color: #fff;
font-weight: 600;
}
.btn-primary:hover {
background: #d94420;
border-color: #d94420;
background: #e08a15;
border-color: #e08a15;
}
.error-message {
@@ -135,7 +135,7 @@
<body>
<div class="container">
<div class="header">
<h1>🎬 Ohm Stream Player</h1>
<h1><i class="fa-solid fa-film"></i> Ohm Stream Player</h1>
</div>
<div class="video-info">
@@ -151,7 +151,7 @@
<div class="controls">
<a href="/web" class="btn">← Retour à l'accueil</a>
<a href="/stream/{{ filename }}" class="btn btn-primary" download>⬇️ Télécharger</a>
<a href="/stream/{{ filename }}" class="btn btn-primary" download><i class="fa-solid fa-download"></i> Télécharger</a>
</div>
</div>
@@ -169,8 +169,8 @@
wrapper.innerHTML = `
<div class="error-message">
Erreur lors de la lecture du flux vidéo.<br>
<a href="/video/{{ task_id }}" style="color: #f15025; text-decoration: underline;">Réessayer</a> ou
<a href="/stream/{{ filename }}" style="color: #f15025; text-decoration: underline;" download>Télécharger</a>
<a href="/video/{{ task_id }}" style="color: #FF9F1C; text-decoration: underline;">Réessayer</a> ou
<a href="/stream/{{ filename }}" style="color: #FF9F1C; text-decoration: underline;" download>Télécharger</a>
</div>
`;
});
+26 -26
View File
@@ -10,29 +10,29 @@
<body class="watchlist-body">
<!-- Main Header -->
<div style="text-align: center; margin-bottom: 20px;">
<h1 style="color: #f15025; font-size: 32px; margin: 0;"> Ohm Stream Downloader</h1>
<p style="color: #ced0ce; font-size: 14px; margin: 5px 0 0;">Téléchargez vos vidéos, animes et séries</p>
<h1 style="color: #FF9F1C; font-size: 32px; margin: 0;"><i class="fa-solid fa-bolt"></i> Ohm Stream Downloader</h1>
<p style="color: #8a8f98; font-size: 14px; margin: 5px 0 0;">Téléchargez vos vidéos, animes et séries</p>
</div>
<!-- User Info -->
<div id="userInfo" style="display: none; max-width: 1200px; margin: 0 auto 15px; padding: 10px; background: rgba(241,80,37,0.1); border: 1px solid #f15025; border-radius: 4px;">
<span style="color: #f15025;">👤 Connecté</span>
<button class="btn-secondary btn-small" onclick="handleLogout()">🚪 Déconnexion</button>
<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;">
<span style="color: #FF9F1C;"><i class="fa-solid fa-user"></i> Connecté</span>
<button class="btn-secondary btn-small" onclick="handleLogout()"><i class="fa-solid fa-right-from-bracket"></i> Déconnexion</button>
</div>
<!-- Tabs -->
<div class="tabs" style="max-width: 1200px; margin: 0 auto 20px; display: flex; gap: 5px; border-bottom: 1px solid #ced0ce; padding-bottom: 10px;">
<button class="tab" onclick="window.location.href='/web'">🏠 Accueil</button>
<button class="tab" onclick="window.location.href='/web#anime'">🎬 Anime</button>
<button class="tab" onclick="window.location.href='/web#series'">📺 Série</button>
<button class="tab" onclick="window.location.href='/web#providers'">📦 Fournisseurs</button>
<button class="tab active" onclick="window.location.href='/watchlist'">📋 Watchlist</button>
<div class="tabs" style="max-width: 1200px; margin: 0 auto 20px; display: flex; gap: 5px; border-bottom: 1px solid #2a2d32; padding-bottom: 10px;">
<button class="tab" onclick="window.location.href='/web'"><i class="fa-solid fa-house"></i> Accueil</button>
<button class="tab" onclick="window.location.href='/web#anime'"><i class="fa-solid fa-film"></i> Anime</button>
<button class="tab" onclick="window.location.href='/web#series'"><i class="fa-solid fa-tv"></i> Série</button>
<button class="tab" onclick="window.location.href='/web#providers'"><i class="fa-solid fa-box"></i> Fournisseurs</button>
<button class="tab active" onclick="window.location.href='/watchlist'"><i class="fa-solid fa-clipboard-list"></i> Watchlist</button>
</div>
<div class="watchlist-container">
<!-- Header -->
<div class="watchlist-header">
<h1>📋 Ma Watchlist</h1>
<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
@@ -43,21 +43,21 @@
<div class="scheduler-status" id="schedulerStatus">
<div class="scheduler-status-header">
<div>
<h3> Planificateur Automatique</h3>
<h3><i class="fa-solid fa-clock"></i> Planificateur Automatique</h3>
<div id="nextRunInfo" class="next-run-info">Chargement...</div>
</div>
<div class="scheduler-controls">
<button id="startSchedulerBtn" class="btn-primary btn-small watchlist-btn-small" onclick="handleStartScheduler()" style="display:none;">
▶️ Démarrer
<i class="fa-solid fa-play"></i> Démarrer
</button>
<button id="stopSchedulerBtn" class="btn-secondary btn-small watchlist-btn-small" onclick="handleStopScheduler()" style="display:none;">
⏸️ Arrêter
<i class="fa-solid fa-pause"></i> Arrêter
</button>
<button class="btn-secondary btn-small watchlist-btn-small" onclick="handleCheckAll()">
🔍 Vérifier tout
<i class="fa-solid fa-magnifying-glass"></i> Vérifier tout
</button>
<button class="btn-secondary btn-small watchlist-btn-small" onclick="handleOpenSettings()">
⚙️ Paramètres
<i class="fa-solid fa-gear"></i> Paramètres
</button>
</div>
</div>
@@ -161,17 +161,17 @@
if (status.next_run) {
const nextRun = new Date(status.next_run);
nextRunInfo.innerHTML = ` En cours<br>Prochaine vérification: ${nextRun.toLocaleString('fr-FR')}`;
nextRunInfo.innerHTML = `<i class="fa-solid fa-check"></i> En cours<br>Prochaine vérification: ${nextRun.toLocaleString('fr-FR')}`;
} else {
// Scheduler running but no next_run yet (just started)
const interval = status.settings?.check_interval_hours || 6;
nextRunInfo.innerHTML = ` En cours<br>Vérification toutes les ${interval}h`;
nextRunInfo.innerHTML = `<i class="fa-solid fa-check"></i> En cours<br>Vérification toutes les ${interval}h`;
}
} else {
// Update buttons if they exist
if (startBtn) startBtn.style.display = 'inline-block';
if (stopBtn) stopBtn.style.display = 'none';
nextRunInfo.innerHTML = '⏸️ Arrêté';
nextRunInfo.innerHTML = '<i class="fa-solid fa-pause"></i> Arrêté';
}
}
@@ -198,10 +198,10 @@
try {
await startScheduler();
await loadSchedulerStatus();
alert('Planificateur démarré!');
alert('Planificateur démarré !');
} catch (error) {
console.error('Error starting scheduler:', error);
alert(`Erreur: ${error.message}`);
alert(`Erreur : ${error.message}`);
}
}
@@ -212,10 +212,10 @@
try {
await stopScheduler();
await loadSchedulerStatus();
alert('Planificateur arrêté!');
alert('Planificateur arrêté !');
} catch (error) {
console.error('Error stopping scheduler:', error);
alert(`Erreur: ${error.message}`);
alert(`Erreur : ${error.message}`);
}
}
@@ -228,7 +228,7 @@
await loadSchedulerStatus();
} catch (error) {
console.error('Error checking all:', error);
alert(`Erreur: ${error.message}`);
alert(`Erreur : ${error.message}`);
}
}
@@ -246,7 +246,7 @@
document.body.appendChild(modalContainer);
} catch (error) {
console.error('Error loading settings:', error);
alert(`Erreur: ${error.message}`);
alert(`Erreur : ${error.message}`);
}
}