- Filtre recommandations (all/anime/series) - Filtre dernieres sorties (all/anime/series) - Toggle categories anime/series (min 1 active) - Repertoire de telechargement personnalisable - Migration automatique des nouvelles colonnes SQLite - Template settings avec tous les nouveaux controles - Validation cote backend (400 si les deux categories desactivees) Closes #9, Closes #10, Closes #11, Closes #12
This commit is contained in:
@@ -25,6 +25,34 @@ def create_db_and_tables():
|
||||
from app.models.settings import AppSettingsTable
|
||||
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
# Add new columns to existing tables if they don't exist (SQLite workaround)
|
||||
_ensure_columns(engine)
|
||||
|
||||
|
||||
def _ensure_columns(engine):
|
||||
"""Add new columns to app_settings table if they don't exist"""
|
||||
from sqlalchemy import inspect, text
|
||||
|
||||
inspector = inspect(engine)
|
||||
if 'app_settings' not in inspector.get_table_names():
|
||||
return
|
||||
|
||||
existing = {col['name'] for col in inspector.get_columns('app_settings')}
|
||||
|
||||
new_columns = {
|
||||
'recommendations_filter': 'TEXT DEFAULT "all"',
|
||||
'releases_filter': 'TEXT DEFAULT "all"',
|
||||
'anime_enabled': 'BOOLEAN DEFAULT 1',
|
||||
'series_enabled': 'BOOLEAN DEFAULT 1',
|
||||
'download_dir': 'TEXT DEFAULT "downloads"',
|
||||
}
|
||||
|
||||
with engine.connect() as conn:
|
||||
for col_name, col_def in new_columns.items():
|
||||
if col_name not in existing:
|
||||
conn.execute(text(f'ALTER TABLE app_settings ADD COLUMN {col_name} {col_def}'))
|
||||
conn.commit()
|
||||
|
||||
|
||||
def get_session() -> Generator[Session, None, None]:
|
||||
|
||||
@@ -14,6 +14,19 @@ class AppSettingsBase(SQLModel):
|
||||
|
||||
# Store list of disabled providers as a JSON string
|
||||
disabled_providers_json: str = Field(default="[]", sa_column=Column(String))
|
||||
|
||||
# #9: Filter for recommendations section ("all", "anime", "series")
|
||||
recommendations_filter: str = Field(default="all", sa_column=Column(String))
|
||||
|
||||
# #10: Filter for latest releases section ("all", "anime", "series")
|
||||
releases_filter: str = Field(default="all", sa_column=Column(String))
|
||||
|
||||
# #11: Enable/disable categories
|
||||
anime_enabled: bool = Field(default=True)
|
||||
series_enabled: bool = Field(default=True)
|
||||
|
||||
# #12: Custom download directory
|
||||
download_dir: str = Field(default="downloads")
|
||||
|
||||
@property
|
||||
def disabled_providers(self) -> List[str]:
|
||||
@@ -46,6 +59,11 @@ class AppSettings(BaseModel):
|
||||
default_lang: str = "vostfr"
|
||||
theme: str = "dark"
|
||||
disabled_providers: List[str] = []
|
||||
recommendations_filter: str = "all"
|
||||
releases_filter: str = "all"
|
||||
anime_enabled: bool = True
|
||||
series_enabled: bool = True
|
||||
download_dir: str = "downloads"
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -56,3 +74,8 @@ class AppSettingsUpdate(BaseModel):
|
||||
default_lang: Optional[str] = None
|
||||
theme: Optional[str] = None
|
||||
disabled_providers: Optional[List[str]] = None
|
||||
recommendations_filter: Optional[str] = None
|
||||
releases_filter: Optional[str] = None
|
||||
anime_enabled: Optional[bool] = None
|
||||
series_enabled: Optional[bool] = None
|
||||
download_dir: Optional[str] = None
|
||||
|
||||
@@ -39,6 +39,11 @@ async def get_settings(
|
||||
default_lang=settings_obj.default_lang,
|
||||
theme=settings_obj.theme,
|
||||
disabled_providers=settings_obj.disabled_providers,
|
||||
recommendations_filter=getattr(settings_obj, 'recommendations_filter', 'all'),
|
||||
releases_filter=getattr(settings_obj, 'releases_filter', 'all'),
|
||||
anime_enabled=getattr(settings_obj, 'anime_enabled', True),
|
||||
series_enabled=getattr(settings_obj, 'series_enabled', True),
|
||||
download_dir=getattr(settings_obj, 'download_dir', 'downloads'),
|
||||
)
|
||||
|
||||
|
||||
@@ -65,6 +70,22 @@ async def update_settings(
|
||||
settings_obj.theme = update_data.theme
|
||||
if update_data.disabled_providers is not None:
|
||||
settings_obj.disabled_providers = update_data.disabled_providers
|
||||
if update_data.recommendations_filter is not None:
|
||||
settings_obj.recommendations_filter = update_data.recommendations_filter
|
||||
if update_data.releases_filter is not None:
|
||||
settings_obj.releases_filter = update_data.releases_filter
|
||||
if update_data.anime_enabled is not None:
|
||||
# Prevent disabling both categories
|
||||
if not update_data.anime_enabled and not getattr(settings_obj, 'series_enabled', True):
|
||||
raise HTTPException(status_code=400, detail="Au moins une categorie doit rester active")
|
||||
settings_obj.anime_enabled = update_data.anime_enabled
|
||||
if update_data.series_enabled is not None:
|
||||
# Prevent disabling both categories
|
||||
if not update_data.series_enabled and not getattr(settings_obj, 'anime_enabled', True):
|
||||
raise HTTPException(status_code=400, detail="Au moins une categorie doit rester active")
|
||||
settings_obj.series_enabled = update_data.series_enabled
|
||||
if update_data.download_dir is not None:
|
||||
settings_obj.download_dir = update_data.download_dir
|
||||
|
||||
session.add(settings_obj)
|
||||
session.commit()
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<div class="settings-container section-container">
|
||||
<div class="section-header">
|
||||
<h2>⚙️ Paramètres</h2>
|
||||
<h2>Parametres</h2>
|
||||
</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 rgba(255,255,255,0.05);">
|
||||
<h3 style="margin-bottom: 20px; color: var(--primary);">Général</h3>
|
||||
<h3 style="margin-bottom: 20px; color: var(--primary);">General</h3>
|
||||
|
||||
<form hx-patch="/api/settings" hx-swap="none" hx-ext="json-enc" class="settings-form">
|
||||
<form id="settings-form" class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="default_lang">Langue par défaut</label>
|
||||
<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 (Version Originale Sous-Titrée Français)</option>
|
||||
<option value="vf" {% if settings.default_lang == 'vf' %}selected{% endif %}>VF (Version Française)</option>
|
||||
<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;">
|
||||
<label for="theme">Thème</label>
|
||||
<label for="theme">Theme</label>
|
||||
<select name="theme" id="theme" class="btn btn-secondary btn-block" style="text-align: left; padding: 12px 15px;">
|
||||
<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>
|
||||
@@ -25,18 +25,76 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" style="margin-top: 20px; width: 100%;">
|
||||
<i class="fas fa-save"></i> Enregistrer les préférences
|
||||
<div class="form-group" style="margin-top: 20px;">
|
||||
<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>
|
||||
<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();">
|
||||
<i class="fas fa-save"></i> Enregistrer les preferences
|
||||
</button>
|
||||
</form>
|
||||
</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 rgba(255,255,255,0.05);">
|
||||
<h3 style="margin-bottom: 20px; color: var(--primary);">Filtres de contenu</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="recommendations_filter">Recommande pour vous : afficher</label>
|
||||
<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;">
|
||||
<label for="releases_filter">Dernieres sorties : afficher</label>
|
||||
<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)">
|
||||
<option value="all" {% if settings.releases_filter == 'all' %}selected{% endif %}>Les deux (Animes + Series)</option>
|
||||
<option value="anime" {% if settings.releases_filter == 'anime' %}selected{% endif %}>Animes uniquement</option>
|
||||
<option value="series" {% if settings.releases_filter == 'series' %}selected{% endif %}>Series uniquement</option>
|
||||
</select>
|
||||
</div>
|
||||
</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 rgba(255,255,255,0.05);">
|
||||
<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: rgba(255,255,255,0.02); border-radius: 10px; border: 1px solid rgba(255,255,255,0.05); 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>
|
||||
</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>
|
||||
|
||||
<label class="toggle-card" style="flex: 1; min-width: 200px; padding: 15px; background: rgba(255,255,255,0.02); border-radius: 10px; border: 1px solid rgba(255,255,255,0.05); 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>
|
||||
</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);">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Providers Management -->
|
||||
<div class="settings-card card" style="padding: 25px; background: var(--bg-card); border-radius: var(--card-radius); border: 1px solid rgba(255,255,255,0.05);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h3 style="margin: 0; color: var(--primary);">Disponibilité des Fournisseurs</h3>
|
||||
<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">
|
||||
<i class="fas fa-sync-alt"></i> Forcer vérification
|
||||
<i class="fas fa-sync-alt"></i> Forcer verification
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +119,7 @@
|
||||
hx-swap="none"
|
||||
hx-on::after-request="htmx.trigger('#tab-settings > div', 'refresh-settings')"
|
||||
style="min-width: 100px;">
|
||||
{% if provider.enabled %}Désactiver{% else %}Activer{% endif %}
|
||||
{% if provider.enabled %}Desactiver{% else %}Activer{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -69,6 +127,93 @@
|
||||
</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;
|
||||
|
||||
Reference in New Issue
Block a user