feat: add Settings tab with provider management and language preferences
- Implemented AppSettings model and table using SQLModel. - Created Settings router with endpoints for preferences and provider toggling. - Added Settings tab to the UI with real-time health status of providers. - Integrated language and provider filtering into anime and series search logic. - Updated templates to respect user-defined settings.
This commit is contained in:
@@ -22,6 +22,7 @@ def create_db_and_tables():
|
||||
from app.models.watchlist import WatchlistItemTable, WatchlistSettingsTable
|
||||
from app.models.favorites import FavoriteTable
|
||||
from app.models.sonarr import SonarrMappingTable, SonarrConfigTable
|
||||
from app.models.settings import AppSettingsTable
|
||||
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
|
||||
@@ -69,3 +69,4 @@ from .auth import UserTable
|
||||
from .watchlist import WatchlistItemTable, WatchlistSettingsTable
|
||||
from .favorites import FavoriteTable
|
||||
from .sonarr import SonarrMappingTable, SonarrConfigTable
|
||||
from .settings import AppSettingsTable
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Models for application settings with SQLModel support"""
|
||||
import uuid
|
||||
import json
|
||||
from pydantic import BaseModel, Field as PydanticField
|
||||
from typing import Optional, List, Dict
|
||||
from datetime import datetime
|
||||
from sqlmodel import SQLModel, Field, Column, String
|
||||
|
||||
|
||||
class AppSettingsBase(SQLModel):
|
||||
"""Base schema for application settings"""
|
||||
default_lang: str = Field(default="vostfr")
|
||||
theme: str = Field(default="dark")
|
||||
|
||||
# Store list of disabled providers as a JSON string
|
||||
disabled_providers_json: str = Field(default="[]", sa_column=Column(String))
|
||||
|
||||
@property
|
||||
def disabled_providers(self) -> List[str]:
|
||||
try:
|
||||
return json.loads(self.disabled_providers_json or "[]")
|
||||
except:
|
||||
return []
|
||||
|
||||
@disabled_providers.setter
|
||||
def disabled_providers(self, value: List[str]):
|
||||
self.disabled_providers_json = json.dumps(value or [])
|
||||
|
||||
|
||||
class AppSettingsTable(AppSettingsBase, table=True):
|
||||
"""Database table for application settings"""
|
||||
__tablename__ = "app_settings"
|
||||
|
||||
id: str = Field(
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
primary_key=True,
|
||||
index=True,
|
||||
nullable=False
|
||||
)
|
||||
user_id: str = Field(foreign_key="users.id", index=True, unique=True)
|
||||
updated_at: datetime = Field(default_factory=datetime.now)
|
||||
|
||||
|
||||
class AppSettings(BaseModel):
|
||||
"""Application settings (API Response)"""
|
||||
default_lang: str = "vostfr"
|
||||
theme: str = "dark"
|
||||
disabled_providers: List[str] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class AppSettingsUpdate(BaseModel):
|
||||
"""Model for updating application settings"""
|
||||
default_lang: Optional[str] = None
|
||||
theme: Optional[str] = None
|
||||
disabled_providers: Optional[List[str]] = None
|
||||
@@ -10,8 +10,9 @@ from app.routers.router_recommendations import router as recommendations_router
|
||||
from app.routers.router_watchlist import router as watchlist_router
|
||||
from app.routers.router_sonarr import router as sonarr_router
|
||||
from app.routers.router_player import router as player_router
|
||||
from app.routers.router_static import router as static_router
|
||||
from app.routers.router_root import router as root_router
|
||||
from .router_static import router as static_router
|
||||
from .router_root import router as root_router
|
||||
from .router_settings import router as settings_router
|
||||
|
||||
__all__ = [
|
||||
"auth_router",
|
||||
@@ -24,4 +25,6 @@ __all__ = [
|
||||
"player_router",
|
||||
"static_router",
|
||||
"root_router",
|
||||
"settings_router",
|
||||
]
|
||||
|
||||
|
||||
@@ -11,7 +11,12 @@ import hashlib
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, Request, Response
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.database import get_session
|
||||
from app.models.settings import AppSettingsTable
|
||||
from app.routers.router_auth import get_current_user_from_token
|
||||
from app.models.auth import User
|
||||
from app.download_manager import DownloadManager
|
||||
from app.downloaders import (
|
||||
AnimeSamaDownloader,
|
||||
@@ -67,6 +72,8 @@ async def search_anime_unified(
|
||||
lang: str = "vostfr",
|
||||
include_metadata: bool = False,
|
||||
html: bool = Query(False),
|
||||
current_user: User = Depends(get_current_user_from_token),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""
|
||||
Search across all anime providers.
|
||||
@@ -75,6 +82,11 @@ async def search_anime_unified(
|
||||
print(f"\n[SEARCH] Starting search for '{q}'. html={html}")
|
||||
start_time = time.time()
|
||||
|
||||
# Get user settings for disabled providers
|
||||
statement = select(AppSettingsTable).where(AppSettingsTable.user_id == current_user.id)
|
||||
settings_obj = session.exec(statement).first()
|
||||
disabled_providers = settings_obj.disabled_providers if settings_obj else []
|
||||
|
||||
results = {}
|
||||
|
||||
# 1. Prepare search tasks (Generic + Legacy)
|
||||
@@ -84,8 +96,9 @@ async def search_anime_unified(
|
||||
# Generic YAML providers
|
||||
active_generic = providers_manager.get_active_providers()
|
||||
for provider in active_generic:
|
||||
search_tasks.append(provider.search(q))
|
||||
task_metadata.append({"id": provider.id, "type": "generic"})
|
||||
if provider.id not in disabled_providers:
|
||||
search_tasks.append(provider.search(q))
|
||||
task_metadata.append({"id": provider.id, "type": "generic"})
|
||||
|
||||
# Legacy providers
|
||||
legacy_downloaders = {
|
||||
@@ -94,8 +107,9 @@ async def search_anime_unified(
|
||||
"vostfree": VostfreeDownloader(),
|
||||
}
|
||||
for pid, dl in legacy_downloaders.items():
|
||||
search_tasks.append(dl.search_anime(q, lang, include_metadata=False))
|
||||
task_metadata.append({"id": pid, "type": "legacy"})
|
||||
if pid not in disabled_providers:
|
||||
search_tasks.append(dl.search_anime(q, lang, include_metadata=False))
|
||||
task_metadata.append({"id": pid, "type": "legacy"})
|
||||
|
||||
# 2. Run searches in parallel
|
||||
all_raw_results = await asyncio.gather(*search_tasks, return_exceptions=True)
|
||||
@@ -163,7 +177,11 @@ async def search_anime_unified(
|
||||
print("[SEARCH] Returning HTML response")
|
||||
return templates.TemplateResponse(
|
||||
"components/anime_search_results.html",
|
||||
{"request": request, "results": results}
|
||||
{
|
||||
"request": request,
|
||||
"results": results,
|
||||
"settings": settings_obj
|
||||
}
|
||||
)
|
||||
|
||||
print("[SEARCH] Returning JSON response")
|
||||
@@ -181,6 +199,8 @@ async def search_series_unified(
|
||||
q: str,
|
||||
lang: str = "vf",
|
||||
html: bool = Query(False),
|
||||
current_user: User = Depends(get_current_user_from_token),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""
|
||||
Search across all TV series providers (FS7, etc.)
|
||||
@@ -191,6 +211,12 @@ async def search_series_unified(
|
||||
|
||||
print(f"\n[SERIES SEARCH] Starting search for '{q}' in {lang}. html={html}")
|
||||
start_time = time.time()
|
||||
|
||||
# Get user settings for disabled providers
|
||||
statement = select(AppSettingsTable).where(AppSettingsTable.user_id == current_user.id)
|
||||
settings_obj = session.exec(statement).first()
|
||||
disabled_providers = settings_obj.disabled_providers if settings_obj else []
|
||||
|
||||
results = {}
|
||||
series_downloaders = {
|
||||
"fs7": FS7Downloader(),
|
||||
@@ -200,7 +226,7 @@ async def search_series_unified(
|
||||
provider_ids = []
|
||||
|
||||
for provider_id, provider in get_series_providers().items():
|
||||
if provider_id in series_downloaders:
|
||||
if provider_id in series_downloaders and provider_id not in disabled_providers:
|
||||
downloader = series_downloaders[provider_id]
|
||||
search_tasks.append(downloader.search_anime(q, lang))
|
||||
provider_ids.append(provider_id)
|
||||
@@ -221,7 +247,11 @@ async def search_series_unified(
|
||||
if html or request.headers.get("HX-Request"):
|
||||
return templates.TemplateResponse(
|
||||
"components/series_search_results.html",
|
||||
{"request": request, "results": results}
|
||||
{
|
||||
"request": request,
|
||||
"results": results,
|
||||
"settings": settings_obj
|
||||
}
|
||||
)
|
||||
|
||||
return {"query": q, "lang": lang, "results": results}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
"""Application settings routes for Ohm Stream Downloader API"""
|
||||
import json
|
||||
from typing import List, Dict, Any
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.database import get_session
|
||||
from app.models.auth import User, UserTable
|
||||
from app.models.settings import AppSettings, AppSettingsTable, AppSettingsUpdate
|
||||
from app.routers.router_auth import get_current_user_from_token
|
||||
from app.providers import get_anime_providers, get_series_providers
|
||||
from app.providers_manager import providers_manager
|
||||
|
||||
router = APIRouter(prefix="/api/settings", tags=["settings"])
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
@router.get("", response_model=AppSettings)
|
||||
async def get_settings(
|
||||
current_user: User = Depends(get_current_user_from_token),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get current application settings for the user"""
|
||||
statement = select(AppSettingsTable).where(AppSettingsTable.user_id == current_user.id)
|
||||
settings_obj = session.exec(statement).first()
|
||||
|
||||
if not settings_obj:
|
||||
# Create default settings if they don't exist
|
||||
settings_obj = AppSettingsTable(user_id=current_user.id)
|
||||
session.add(settings_obj)
|
||||
session.commit()
|
||||
session.refresh(settings_obj)
|
||||
|
||||
return AppSettings(
|
||||
default_lang=settings_obj.default_lang,
|
||||
theme=settings_obj.theme,
|
||||
disabled_providers=settings_obj.disabled_providers
|
||||
)
|
||||
|
||||
|
||||
@router.patch("", response_model=AppSettings)
|
||||
async def update_settings(
|
||||
update_data: AppSettingsUpdate,
|
||||
response: Response,
|
||||
current_user: User = Depends(get_current_user_from_token),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Update application settings for the user"""
|
||||
statement = select(AppSettingsTable).where(AppSettingsTable.user_id == current_user.id)
|
||||
settings_obj = session.exec(statement).first()
|
||||
|
||||
if not settings_obj:
|
||||
settings_obj = AppSettingsTable(user_id=current_user.id)
|
||||
session.add(settings_obj)
|
||||
|
||||
if update_data.default_lang is not None:
|
||||
settings_obj.default_lang = update_data.default_lang
|
||||
if update_data.theme is not None:
|
||||
settings_obj.theme = update_data.theme
|
||||
if update_data.disabled_providers is not None:
|
||||
settings_obj.disabled_providers = update_data.disabled_providers
|
||||
|
||||
session.add(settings_obj)
|
||||
session.commit()
|
||||
session.refresh(settings_obj)
|
||||
|
||||
response.headers["HX-Trigger"] = json.dumps({"show-toast": {"message": "Paramètres enregistrés", "type": "success"}})
|
||||
|
||||
return AppSettings(
|
||||
default_lang=settings_obj.default_lang,
|
||||
theme=settings_obj.theme,
|
||||
disabled_providers=settings_obj.disabled_providers
|
||||
)
|
||||
|
||||
|
||||
@router.get("/providers/availability")
|
||||
async def get_providers_availability(
|
||||
current_user: User = Depends(get_current_user_from_token),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get list of providers with their availability and enabled status"""
|
||||
# Get user settings
|
||||
statement = select(AppSettingsTable).where(AppSettingsTable.user_id == current_user.id)
|
||||
settings_obj = session.exec(statement).first()
|
||||
disabled_providers = settings_obj.disabled_providers if settings_obj else []
|
||||
|
||||
# Get health status
|
||||
health_status = providers_manager.get_all_status()
|
||||
|
||||
# Combine anime and series providers
|
||||
all_providers = {**get_anime_providers(), **get_series_providers()}
|
||||
|
||||
result = []
|
||||
for pid, info in all_providers.items():
|
||||
status_info = health_status.get(pid, {"status": "unknown"})
|
||||
result.append({
|
||||
"id": pid,
|
||||
"name": info["name"],
|
||||
"icon": info.get("icon", "🎬"),
|
||||
"status": status_info["status"],
|
||||
"enabled": pid not in disabled_providers,
|
||||
"error": status_info.get("error")
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/providers/{provider_id}/toggle")
|
||||
async def toggle_provider(
|
||||
provider_id: str,
|
||||
response: Response,
|
||||
current_user: User = Depends(get_current_user_from_token),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Toggle a provider's enabled/disabled status"""
|
||||
statement = select(AppSettingsTable).where(AppSettingsTable.user_id == current_user.id)
|
||||
settings_obj = session.exec(statement).first()
|
||||
|
||||
if not settings_obj:
|
||||
settings_obj = AppSettingsTable(user_id=current_user.id)
|
||||
session.add(settings_obj)
|
||||
|
||||
disabled = settings_obj.disabled_providers
|
||||
if provider_id in disabled:
|
||||
disabled.remove(provider_id)
|
||||
enabled = True
|
||||
else:
|
||||
disabled.append(provider_id)
|
||||
enabled = False
|
||||
|
||||
settings_obj.disabled_providers = disabled
|
||||
session.add(settings_obj)
|
||||
session.commit()
|
||||
|
||||
status_text = "activé" if enabled else "désactivé"
|
||||
response.headers["HX-Trigger"] = json.dumps({"show-toast": {"message": f"Fournisseur {provider_id} {status_text}", "type": "success"}})
|
||||
|
||||
return {"id": provider_id, "enabled": enabled}
|
||||
|
||||
|
||||
@router.get("/ui")
|
||||
async def get_settings_ui(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_user_from_token),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Return the settings UI fragment for HTMX"""
|
||||
# Reuse existing endpoints logic
|
||||
settings = await get_settings(current_user, session)
|
||||
providers = await get_providers_availability(current_user, session)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"components/settings_section.html",
|
||||
{
|
||||
"request": request,
|
||||
"settings": settings,
|
||||
"providers": providers
|
||||
}
|
||||
)
|
||||
@@ -126,6 +126,7 @@ from app.routers import (
|
||||
player_router,
|
||||
static_router,
|
||||
root_router,
|
||||
settings_router,
|
||||
)
|
||||
|
||||
|
||||
@@ -140,6 +141,7 @@ app.include_router(watchlist_router)
|
||||
app.include_router(sonarr_router)
|
||||
app.include_router(player_router)
|
||||
app.include_router(static_router)
|
||||
app.include_router(settings_router)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+2
-2
@@ -26,7 +26,7 @@ function switchTab(tabName) {
|
||||
// Handle URL hash on page load
|
||||
if (window.location.hash) {
|
||||
const hash = window.location.hash.substring(1);
|
||||
const validTabs = ['home', 'watchlist', 'anime', 'series', 'providers', 'downloads'];
|
||||
const validTabs = ['home', 'watchlist', 'anime', 'series', 'providers', 'downloads', 'settings'];
|
||||
if (validTabs.includes(hash)) {
|
||||
// Short delay to ensure Alpine is ready
|
||||
setTimeout(() => switchTab(hash), 100);
|
||||
@@ -37,7 +37,7 @@ if (window.location.hash) {
|
||||
window.addEventListener('hashchange', function() {
|
||||
if (window.location.hash) {
|
||||
const hash = window.location.hash.substring(1);
|
||||
const validTabs = ['home', 'watchlist', 'anime', 'series', 'providers', 'downloads'];
|
||||
const validTabs = ['home', 'watchlist', 'anime', 'series', 'providers', 'downloads', 'settings'];
|
||||
if (validTabs.includes(hash)) {
|
||||
switchTab(hash);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% macro anime_card(anime, in_watchlist=False) %}
|
||||
{% macro anime_card(anime, in_watchlist=False, lang='vostfr') %}
|
||||
<div class="anime-card" id="anime-{{ anime.url | hash }}">
|
||||
<div class="anime-poster">
|
||||
{% set poster = anime.cover_image or (anime.metadata.poster_image if anime.metadata else None) or 'https://placehold.co/400x600/161625/00d9ff?text=No+Image' %}
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="anime-overlay">
|
||||
<div class="overlay-buttons">
|
||||
<button class="btn btn-primary btn-circle"
|
||||
hx-get="/api/anime/episodes?url={{ anime.url | urlencode }}"
|
||||
hx-get="/api/anime/episodes?url={{ anime.url | urlencode }}&lang={{ lang }}"
|
||||
hx-target="#player-container"
|
||||
hx-swap="innerHTML"
|
||||
title="Play">
|
||||
@@ -32,20 +32,21 @@
|
||||
|
||||
<div class="anime-meta-tags">
|
||||
<span class="badge">{{ anime.provider_id or 'Anime' }}</span>
|
||||
<span class="badge" style="color: var(--primary)">{{ lang | upper }}</span>
|
||||
{% if anime.metadata and anime.metadata.status %}
|
||||
<span class="badge" style="color: var(--primary)">{{ anime.metadata.status }}</span>
|
||||
<span class="badge" style="color: #aaa">{{ anime.metadata.status }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="anime-card-buttons">
|
||||
<button class="btn btn-primary btn-small"
|
||||
hx-get="/api/anime/episodes?url={{ anime.url | urlencode }}"
|
||||
hx-get="/api/anime/episodes?url={{ anime.url | urlencode }}&lang={{ lang }}"
|
||||
hx-target="#player-container"
|
||||
hx-swap="innerHTML">
|
||||
<i class="fas fa-eye"></i> <span>Regarder</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-small"
|
||||
hx-get="/api/anime/episodes?url={{ anime.url | urlencode }}"
|
||||
hx-get="/api/anime/episodes?url={{ anime.url | urlencode }}&lang={{ lang }}"
|
||||
hx-target="#player-container"
|
||||
hx-swap="innerHTML">
|
||||
<i class="fas fa-download"></i> <span>Télécharger</span>
|
||||
@@ -55,7 +56,7 @@
|
||||
{% if not in_watchlist %}
|
||||
<button class="btn btn-secondary btn-small btn-block"
|
||||
hx-post="/api/watchlist"
|
||||
hx-vals='{"anime_url": "{{ anime.url }}", "anime_title": "{{ anime.title }}", "provider_id": "{{ anime.provider_id }}"}'
|
||||
hx-vals='{"anime_url": "{{ anime.url }}", "anime_title": "{{ anime.title }}", "provider_id": "{{ anime.provider_id }}", "lang": "{{ lang }}"}'
|
||||
hx-swap="none"
|
||||
hx-on::after-request="this.innerHTML='<i class=\'fas fa-check\'></i> Suivi'; this.disabled=true; this.classList.add('followed')">
|
||||
<i class="fas fa-plus"></i> Watchlist
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<h3 class="provider-title">{{ provider_id | upper }}</h3>
|
||||
<div class="anime-grid">
|
||||
{% for anime in items %}
|
||||
{{ anime_card(anime) }}
|
||||
{{ anime_card(anime, lang=settings.default_lang if settings else 'vostfr') }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -65,5 +65,14 @@
|
||||
</svg>
|
||||
Téléchargements
|
||||
</button>
|
||||
<button class="tab"
|
||||
:class="{ 'active': activeTab === 'settings' }"
|
||||
@click="activeTab = 'settings'; window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: 'settings' } }))">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
Paramètres
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% macro series_card(series, in_watchlist=False) %}
|
||||
{% macro series_card(series, in_watchlist=False, lang='vf') %}
|
||||
<div class="anime-card" id="series-{{ series.url | hash }}">
|
||||
<div class="anime-poster">
|
||||
<img src="{{ series.cover_image or 'https://placehold.co/400x600/161625/ff6b6b?text=No+Image' }}"
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="anime-overlay">
|
||||
<div class="overlay-buttons">
|
||||
<button class="btn btn-primary btn-circle"
|
||||
hx-get="/api/anime/episodes?url={{ series.url | urlencode }}&lang=vf"
|
||||
hx-get="/api/anime/episodes?url={{ series.url | urlencode }}&lang={{ lang }}"
|
||||
hx-target="#player-container"
|
||||
hx-swap="innerHTML"
|
||||
title="Play">
|
||||
@@ -21,18 +21,19 @@
|
||||
<div class="anime-info">
|
||||
<h3 class="anime-title" title="{{ series.title }}">{{ series.title }}</h3>
|
||||
<div class="anime-meta-tags">
|
||||
<span class="badge">FS7</span>
|
||||
<span class="badge">{{ series.provider_id | upper if series.provider_id else 'FS7' }}</span>
|
||||
<span class="badge" style="color: var(--primary)">{{ lang | upper }}</span>
|
||||
</div>
|
||||
|
||||
<div class="anime-card-buttons">
|
||||
<button class="btn btn-primary btn-small"
|
||||
hx-get="/api/anime/episodes?url={{ series.url | urlencode }}&lang=vf"
|
||||
hx-get="/api/anime/episodes?url={{ series.url | urlencode }}&lang={{ lang }}"
|
||||
hx-target="#player-container"
|
||||
hx-swap="innerHTML">
|
||||
<i class="fas fa-eye"></i> <span>Regarder</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-small"
|
||||
hx-get="/api/anime/episodes?url={{ series.url | urlencode }}&lang=vf"
|
||||
hx-get="/api/anime/episodes?url={{ series.url | urlencode }}&lang={{ lang }}"
|
||||
hx-target="#player-container"
|
||||
hx-swap="innerHTML">
|
||||
<i class="fas fa-download"></i> <span>Télécharger</span>
|
||||
@@ -42,7 +43,7 @@
|
||||
{% if not in_watchlist %}
|
||||
<button class="btn btn-secondary btn-small btn-block"
|
||||
hx-post="/api/watchlist"
|
||||
hx-vals='{"anime_url": "{{ series.url }}", "anime_title": "{{ series.title }}", "provider_id": "fs7", "lang": "vf"}'
|
||||
hx-vals='{"anime_url": "{{ series.url }}", "anime_title": "{{ series.title }}", "provider_id": "{{ series.provider_id or 'fs7' }}", "lang": "{{ lang }}"}'
|
||||
hx-swap="none"
|
||||
hx-on::after-request="this.innerHTML='<i class=\'fas fa-check\'></i> Suivi'; this.disabled=true; this.classList.add('followed')">
|
||||
<i class="fas fa-plus"></i> Watchlist
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<h3 class="provider-title">{{ provider_id | upper }}</h3>
|
||||
<div class="anime-grid">
|
||||
{% for series in items %}
|
||||
{{ series_card(series) }}
|
||||
{{ series_card(series, lang=settings.default_lang if settings else 'vf') }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<div class="settings-container section-container">
|
||||
<div class="section-header">
|
||||
<h2>⚙️ Paramètres</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>
|
||||
|
||||
<form hx-patch="/api/settings" hx-swap="none" hx-ext="json-enc" class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="default_lang">Langue par défaut</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>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top: 20px;">
|
||||
<label for="theme">Thème</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>
|
||||
<option value="oled" {% if settings.theme == 'oled' %}selected{% endif %}>OLED (Noir absolu)</option>
|
||||
</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
|
||||
</button>
|
||||
</form>
|
||||
</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>
|
||||
<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
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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: 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;">
|
||||
<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 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn {% if provider.enabled %}btn-secondary{% else %}btn-accent{% endif %} btn-sm"
|
||||
hx-post="/api/settings/providers/{{ provider.id }}/toggle"
|
||||
hx-target="closest .settings-container"
|
||||
hx-get="/api/settings/ui"
|
||||
hx-trigger="click delay:200ms"
|
||||
style="min-width: 100px;">
|
||||
{% if provider.enabled %}Désactiver{% else %}Activer{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.settings-form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 5px currentColor;
|
||||
}
|
||||
</style>
|
||||
@@ -142,6 +142,14 @@
|
||||
{% include "components/downloads_section.html" %}
|
||||
</div>
|
||||
|
||||
<div id="tab-settings" class="tab-content" x-show="activeTab === 'settings'">
|
||||
<div hx-get="/api/settings/ui" hx-trigger="load, set-tab[detail.tab === 'settings'] from:window" hx-swap="innerHTML">
|
||||
<div class="loading-placeholder">
|
||||
<div class="spinner"></div> Chargement des paramètres...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- End of main-content -->
|
||||
|
||||
|
||||
Reference in New Issue
Block a user