mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-15 11:18:05 +02:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1295bd12ef | ||
|
|
dfc8a33916 | ||
|
|
0541e1b0e3 | ||
|
|
8ba57a7367 | ||
|
|
8bed6cf7b8 | ||
|
|
7fa9897d5c | ||
|
|
aacfa54604 |
No files matched your search
@@ -8,7 +8,6 @@
|
||||
build/
|
||||
bin/.dart_tool
|
||||
bin/.packages
|
||||
bin/pubspec.lock
|
||||
ios/
|
||||
android/
|
||||
windows/
|
||||
|
||||
@@ -6,6 +6,12 @@ on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
# Un push qui en suit un autre annule le run précédent : inutile de
|
||||
# consommer un runner pour un commit déjà obsolète.
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
frontend:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -13,7 +19,13 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
# Version épinglée : `channel: stable` seul fait dériver le SDK au
|
||||
# fil du temps (une release Flutter peut casser la CI sans aucun
|
||||
# changement dans le dépôt). Le cache évite de retélécharger SDK et
|
||||
# dépendances pub à chaque run.
|
||||
flutter-version: 3.38.4
|
||||
channel: stable
|
||||
cache: true
|
||||
- run: flutter pub get
|
||||
# Infos (pre-existing withOpacity/dart:html deprecations) are not
|
||||
# fatal; errors and warnings still fail the build.
|
||||
@@ -30,7 +42,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dart-lang/setup-dart@v1
|
||||
with:
|
||||
sdk: stable
|
||||
sdk: 3.13.2
|
||||
- run: dart pub get
|
||||
- run: dart analyze
|
||||
- run: dart test
|
||||
@@ -1,7 +1,12 @@
|
||||
name: Docker Publish
|
||||
|
||||
# Chaîné sur la CI : l'image :latest n'est publiée que si analyze/test/build
|
||||
# sont verts sur main. L'ancien déclencheur `push: [main]` tournait en
|
||||
# parallèle de la CI et pouvait publier une image cassée.
|
||||
on:
|
||||
push:
|
||||
workflow_run:
|
||||
workflows: [CI]
|
||||
types: [completed]
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -11,11 +16,18 @@ env:
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Sur workflow_run, github.sha pointe sur le commit du workflow
|
||||
# par défaut : construire exactement le commit validé par la CI.
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
@@ -34,6 +46,6 @@ jobs:
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}:latest
|
||||
${{ env.IMAGE_NAME }}:${{ github.sha }}
|
||||
${{ env.IMAGE_NAME }}:${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -49,4 +49,7 @@ app.*.map.json
|
||||
# Hive data (local development)
|
||||
*.hive
|
||||
*.lock
|
||||
# Les lockfiles pub sont versionnés : sans eux, chaque build Docker/CI
|
||||
# résout des versions différentes (builds non reproductibles).
|
||||
!pubspec.lock
|
||||
.claude/settings.local.json
|
||||
@@ -2,6 +2,21 @@
|
||||
|
||||
## Non publié
|
||||
|
||||
### 🧰 Qualité / Infra
|
||||
- **Builds reproductibles** : `pubspec.lock` (frontend et backend) désormais versionnés et utilisés par le Dockerfile — chaque build résolvait jusqu'ici des versions fraîches
|
||||
- **CI durcie** : versions Flutter/Dart épinglées, cache pub, annulation des runs obsolètes (`concurrency`) ; `docker-publish` ne publie plus `:latest` qu'après une CI verte sur main (il tournait en parallèle et pouvait publier une image cassée)
|
||||
- **docker-compose** : défaut `RECORDINGS_PATH` porté à `./data/recordings` (l'ancien défaut était un chemin unRAID spécifique à une machine), variable `TZ` ajoutée
|
||||
- **README réécrit** : il décrivait une architecture disparue (Hive/IndexedDB, SHA-256, dhttpd port 8080) — remplacé par l'état réel (SQLite serveur, bcrypt, binaire natif port 8089, enregistrements, CI)
|
||||
- `DEPLOYMENT_CHECKLIST.md` archivé et avertissement ajouté sur `docs/archive/` (plusieurs documents s'y déclarent « COMPLETE » à tort)
|
||||
|
||||
### ✨ Fonctionnalités
|
||||
- **Guide TV et Season Passes de retour** : l'onglet Enregistrements retrouve ses 3 vues (Guide TV pour programmer depuis l'EPG, liste des enregistrements, Season Passes) — le code existait mais n'était plus branché depuis une refonte
|
||||
- **Favoris enfin utilisables** : bouton cœur sur les tuiles chaînes (desktop et mobile) ; le filtre « Favoris » affichait toujours vide faute de moyen d'en ajouter
|
||||
- **Reprise de lecture** : films et épisodes reprennent où on s'était arrêté (les positions étaient sauvegardées mais jamais relues) ; le live ne pollue plus le stockage de positions
|
||||
- **Enregistrements sur mobile** : nouvel onglet REC dans la barre de navigation ; les onglets mobiles conservent leur état (IndexedStack) au lieu d'être reconstruits à chaque bascule
|
||||
- **Menu profil** sur l'avatar de la sidebar : nom d'utilisateur + déconnexion (le bouton était mort, aucune déconnexion possible depuis le dashboard)
|
||||
- **Confirmation avant suppression** d'un enregistrement, et messages d'erreur avec bouton « Réessayer » (chaînes, enregistrements) au lieu d'exceptions brutes
|
||||
|
||||
### 🔧 Fiabilité des enregistrements
|
||||
- **Fuseaux horaires unifiés** : le backend exige des dates ISO-8601 avec fuseau (400 sinon) et stocke tout en UTC ; le frontend passe par un helper unique `postRecording()` — fini les enregistrements décalés de 1-2 h selon l'écran utilisé
|
||||
- **Contrôle de propriété** : stop/suppression/logs d'un enregistrement et suppression d'un season pass ne sont plus possibles que par leur propriétaire (ou un admin)
|
||||
|
||||
+4
-3
@@ -12,9 +12,10 @@ ENV FLUTTER_NO_ANALYTICS=1
|
||||
RUN flutter config --enable-web && flutter precache --web
|
||||
|
||||
# 2. Dependency Resolution (ONLY UPDATES IF PUBSPEC CHANGES)
|
||||
COPY pubspec.yaml ./
|
||||
COPY bin/pubspec.yaml ./bin/
|
||||
# Note: No .lock files found locally, so we fetch fresh ones here
|
||||
# Les lockfiles sont versionnés : le build résout exactement les versions
|
||||
# committées au lieu d'en chercher de nouvelles à chaque build.
|
||||
COPY pubspec.yaml pubspec.lock ./
|
||||
COPY bin/pubspec.yaml bin/pubspec.lock ./bin/
|
||||
RUN flutter pub get && cd bin && dart pub get
|
||||
|
||||
# 3. Source Code Copy (CHANGES OFTEN)
|
||||
|
||||
@@ -1,282 +1,103 @@
|
||||
# XtremFlow - IPTV Web Application
|
||||
# XtremFlow — Application Web IPTV
|
||||
|
||||
High-performance, containerized IPTV Web Application using Flutter Web and Xtream Codes API.
|
||||
Application IPTV auto-hébergée : frontend Flutter Web + serveur Dart natif, empaquetés dans une seule image Docker. Se connecte à un abonnement Xtream Codes et ajoute le magnétoscope (enregistrements planifiés, season passes), l'EPG avec repli XMLTV, et le transcodage FFmpeg à la demande.
|
||||
|
||||
## Features
|
||||
## Fonctionnalités
|
||||
|
||||
✅ **Local Authentication System**
|
||||
- Default admin user (`admin`/`admin`)
|
||||
- Secure salt-based password hashing (SHA-256)
|
||||
- No public signup - private app only
|
||||
- **Live TV, Films, Séries** : catalogues Xtream avec catégories, recherche, favoris, reprise de lecture
|
||||
- **Guide TV (EPG)** : panneau de l'abonné en priorité, repli automatique sur un dump XMLTV quand le panneau est figé
|
||||
- **Enregistrements TV** : planification depuis le guide, capture FFmpeg (`-c copy`), reprise après coupure amont ou redémarrage du serveur, fusion automatique des parties
|
||||
- **Season Passes** : enregistrement automatique de toutes les diffusions d'une émission (scan EPG toutes les 4 h)
|
||||
- **Transcodage à la demande** : `source | high | medium | low` (live et VOD), `source` = zéro transcodage ; NVENC optionnel
|
||||
- **Multi-utilisateurs** : comptes locaux (bcrypt), playlists par utilisateur, panneau d'administration
|
||||
|
||||
✅ **Multi-Playlist Management**
|
||||
- Centralized Xtream credentials management
|
||||
- Playlist assignment to users
|
||||
- Easy switching between playlists
|
||||
## Stack
|
||||
|
||||
✅ **High-Performance Dashboard (60fps)**
|
||||
- Category-based pagination (100 items/page for Live TV, 50 for Movies)
|
||||
- Lazy loading with `ListView.builder` / `GridView.builder`
|
||||
- Image caching with `cached_network_image`
|
||||
| Couche | Techno |
|
||||
|---|---|
|
||||
| Frontend | Flutter Web (Riverpod, GoRouter), players HTML (hls.js / mpegts.js vendorisés) |
|
||||
| Backend | Dart compilé en natif (`dart compile exe`), shelf |
|
||||
| Base | SQLite côté serveur (`/app/data/xtremflow.db`) |
|
||||
| Capture/Transcodage | FFmpeg (build BtbN, NVENC inclus) |
|
||||
| Conteneur | Debian slim multi-stage, port **8089** |
|
||||
|
||||
✅ **Live TV with EPG**
|
||||
- Electronic Program Guide (EPG) overlay
|
||||
- "Now & Next" program display
|
||||
- Real-time progress bar
|
||||
|
||||
✅ **VOD & Series**
|
||||
- Movies and Series organized by categories
|
||||
- Grid layout with posters
|
||||
- Optimized ratings display (1 decimal place)
|
||||
|
||||
✅ **Docker Deployment**
|
||||
- Multi-stage build with Flutter and Dart
|
||||
- Custom Dart Server (`bin/server.dart`)
|
||||
- **FFmpeg Transcoding** for mobile compatibility
|
||||
- **Cache Management** system for temporary files
|
||||
- External network support (`nginx_default`)
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: Flutter Web
|
||||
- **State Management**: Riverpod
|
||||
- **Local Database**: Hive (Web IndexedDB) with AES encryption
|
||||
- **Networking**: Dio with cache interceptors
|
||||
- **Routing**: GoRouter with auth guards
|
||||
- **Video Player**: `video_player` + `chewie`
|
||||
- **UI**: Google Fonts, Material Design 3
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker & Docker Compose
|
||||
- Existing `nginx_default` network (for reverse proxy routing)
|
||||
- Flutter SDK (for local development only)
|
||||
|
||||
## Quick Start (Docker)
|
||||
|
||||
### 1. Build the Docker image
|
||||
## Démarrage rapide (Docker)
|
||||
|
||||
```bash
|
||||
docker-compose build
|
||||
docker-compose up -d --build
|
||||
# Application sur http://localhost:8089
|
||||
```
|
||||
|
||||
### 2. Start the container
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 3. Access via reverse proxy
|
||||
|
||||
Configure your reverse proxy (Nginx/Traefik) to route traffic to:
|
||||
- **Container**: `xtremflow`
|
||||
- **Internal Port**: `8080`
|
||||
- **Network**: `nginx_default`
|
||||
|
||||
Example Nginx configuration:
|
||||
|
||||
```nginx
|
||||
location /iptv {
|
||||
proxy_pass http://xtremflow:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Login
|
||||
|
||||
- **URL**: `http://your-domain/iptv`
|
||||
- **Default Credentials**:
|
||||
- Username: `admin`
|
||||
- Password: `admin`
|
||||
|
||||
⚠️ **Change the admin password immediately after first login!**
|
||||
|
||||
## Local Development
|
||||
|
||||
### Install dependencies
|
||||
Au premier démarrage, un compte `admin` est créé avec un **mot de passe aléatoire affiché une seule fois dans les logs** (`docker logs xtremflow`), sauf si `ADMIN_INITIAL_PASSWORD` est défini. Changez-le après la première connexion.
|
||||
|
||||
### Variables d'environnement (docker-compose.yml)
|
||||
|
||||
| Variable | Défaut | Rôle |
|
||||
|---|---|---|
|
||||
| `RECORDINGS_PATH` | `./data/recordings` | Dossier hôte des enregistrements |
|
||||
| `TZ` | `Europe/Paris` | Fuseau du conteneur (les enregistrements sont stockés en UTC) |
|
||||
| `MAX_CONCURRENT_RECORDINGS` | `2` | Enregistrements simultanés |
|
||||
| `EPG_XMLTV_URLS` | dump FR | Sources XMLTV de repli (vide = aucun appel sortant) |
|
||||
| `NVIDIA_GPU` | `false` | Transcodage NVENC |
|
||||
| `ADMIN_INITIAL_PASSWORD` | *(généré)* | Mot de passe initial du compte admin |
|
||||
| `MIN_FREE_DISK_MB` | `500` | Espace libre minimal pour démarrer une capture |
|
||||
| `RECORDINGS_QUOTA_GB` | `0` (off) | Quota du dossier d'enregistrements (rotation des plus anciens terminés) |
|
||||
| `TRUSTED_PROXIES` | loopback + RFC1918 | IPs de reverse proxy dont `X-Forwarded-For` est honoré |
|
||||
|
||||
## Développement local
|
||||
|
||||
```bash
|
||||
# Frontend
|
||||
flutter pub get
|
||||
flutter analyze && flutter test
|
||||
flutter run -d chrome # nécessite le backend lancé pour les routes /api
|
||||
|
||||
# Backend
|
||||
cd bin
|
||||
dart pub get
|
||||
dart analyze && dart test
|
||||
dart run server.dart --port 8089 --path ../build/web
|
||||
```
|
||||
|
||||
### Generate Hive adapters (if modified)
|
||||
Le build Windows natif est documenté dans `BUILD.md`.
|
||||
|
||||
```bash
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
```
|
||||
|
||||
### Run web app
|
||||
|
||||
```bash
|
||||
flutter run -d chrome
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
## Structure du projet
|
||||
|
||||
```
|
||||
lib/
|
||||
├── core/
|
||||
│ ├── database/
|
||||
│ │ └── hive_service.dart # Hive initialization & encryption
|
||||
│ ├── models/
|
||||
│ │ ├── app_user.dart # User model (Hive)
|
||||
│ │ ├── playlist_config.dart # Playlist credentials (Hive)
|
||||
│ │ └── iptv_models.dart # Channel, VOD, Series, EPG models
|
||||
│ ├── router/
|
||||
│ │ └── app_router.dart # GoRouter configuration
|
||||
│ └── utils/
|
||||
│ └── crypto_utils.dart # Password hashing utilities
|
||||
├── features/
|
||||
│ ├── auth/
|
||||
│ │ ├── providers/
|
||||
│ │ │ └── auth_provider.dart # Authentication state
|
||||
│ │ └── screens/
|
||||
│ │ └── login_screen.dart
|
||||
│ ├── admin/
|
||||
│ │ └── screens/
|
||||
│ │ └── admin_panel.dart # User & Playlist CRUD
|
||||
│ └── iptv/
|
||||
│ ├── services/
|
||||
│ │ └── xtream_service.dart # Xtream API client
|
||||
│ ├── providers/
|
||||
│ │ └── xtream_provider.dart # Riverpod providers
|
||||
│ ├── screens/
|
||||
│ │ └── player_screen.dart # Video player
|
||||
│ └── widgets/
|
||||
│ ├── live_tv_tab.dart # Live TV with pagination
|
||||
│ ├── movies_tab.dart # Movies grid
|
||||
│ ├── series_tab.dart # Series grid
|
||||
│ └── epg_overlay.dart # EPG display
|
||||
└── main.dart
|
||||
bin/ Serveur Dart
|
||||
├── server.dart Point d'entrée, routage, arrêt gracieux
|
||||
├── api/ Handlers HTTP (auth, playlists, recordings, EPG, proxy…)
|
||||
├── services/ Scheduler d'enregistrement, sessions FFmpeg, XMLTV
|
||||
├── database/ SQLite (schéma, migrations)
|
||||
├── middleware/ Auth (session), sécurité (rate limit, honeypot, logs redactés)
|
||||
└── test/ Tests backend (dart test)
|
||||
|
||||
lib/ Frontend Flutter
|
||||
├── core/ Modèles, thème, router, clients API
|
||||
├── features/ auth / admin / iptv (desktop)
|
||||
└── mobile/ Variantes d'écrans mobiles
|
||||
|
||||
web/ Players HTML + libs vendorisées (hls.js, mpegts.js)
|
||||
```
|
||||
|
||||
## Security Features
|
||||
## Sécurité
|
||||
|
||||
### Password Storage
|
||||
- **Algorithm**: SHA-256 with random UUID-based salt
|
||||
- **Format**: `salt:hash` (stored in Hive)
|
||||
- **Legacy Support**: Fallback to unsalted comparison for migration
|
||||
- Mots de passe **bcrypt** (migration lazy depuis les anciens hashes au login)
|
||||
- Les credentials Xtream ne quittent jamais le serveur : passerelle `/api/xtream-api` et proxy `/api/xtream` (authentifié par session) avec injection côté serveur
|
||||
- Redaction des credentials dans tous les logs ; anti-SSRF avec revalidation des redirections
|
||||
- Cookie de session HttpOnly ; rate limiting global + limite de tentatives de login par IP
|
||||
|
||||
### Database Encryption
|
||||
- **Hive AES Cipher** (256-bit key)
|
||||
- Key stored in `FlutterSecureStorage`
|
||||
- Automatic key generation on first run
|
||||
## CI / Publication
|
||||
|
||||
### Authentication Flow
|
||||
1. User enters credentials
|
||||
2. System retrieves stored hash
|
||||
3. Input password is hashed with same salt
|
||||
4. Constant-time comparison prevents timing attacks
|
||||
`ci.yml` (analyze + tests + build web/backend) tourne sur chaque PR et push main ; `docker-publish.yml` publie `ghcr.io/r0m1k3/xtremflow:latest` **uniquement après une CI verte** sur main.
|
||||
|
||||
## Performance Optimizations
|
||||
## Dépannage
|
||||
|
||||
### Memory Management (20k+ channels)
|
||||
- **Grouping**: Channels organized by category
|
||||
- **Pagination**: 100 items per page (Live TV), 50 per page (Movies)
|
||||
- **Lazy Loading**: Only render visible items
|
||||
- **Image Caching**: Disk/memory cache with `cached_network_image`
|
||||
- **Logs** : `docker logs xtremflow` (les URLs y sont redactées)
|
||||
- **Mot de passe admin perdu** : supprimer le volume `xtremflow-data` recrée la base et affiche un nouveau mot de passe (⚠ efface utilisateurs et historique d'enregistrements)
|
||||
- **EPG vide** : vérifier `EPG_XMLTV_URLS` et que la chaîne a un `epg_channel_id` ; l'en-tête `X-Epg-Source` des réponses `/api/epg/<id>` indique la source utilisée
|
||||
- **Enregistrement échoué** : bouton « Logs » sur la ligne de l'enregistrement ; la raison (`error_reason`) est affichée sous le titre
|
||||
|
||||
### Network Optimization
|
||||
- **Dio Cache Interceptor**: 1-hour cache for API responses
|
||||
- **EPG Cache**: 5-minute refresh for program data
|
||||
- **Hive Disk Store**: Persistent cache across sessions
|
||||
## Licence
|
||||
|
||||
### Rendering (60fps Target)
|
||||
- `ListView.builder` with fixed `itemExtent`
|
||||
- `AutomaticKeepAliveClientMixin` for tab state
|
||||
- Expansion panels for category navigation
|
||||
- Grid with fixed `crossAxisCount` and `childAspectRatio`
|
||||
|
||||
## Xtream API Integration
|
||||
|
||||
### Supported Endpoints
|
||||
|
||||
| Endpoint | Purpose | Caching |
|
||||
|----------|---------|---------|
|
||||
| `player_api.php` | Authentication | 1 hour |
|
||||
| `get_live_streams` | Live TV channels | 1 hour |
|
||||
| `get_vod_streams` | Movies | 1 hour |
|
||||
| `get_series` | Series | 1 hour |
|
||||
| `get_short_epg` | EPG data | 5 minutes |
|
||||
|
||||
### Stream URL Formats
|
||||
|
||||
```dart
|
||||
// Live TV
|
||||
http://[dns]/live/[username]/[password]/[stream_id].m3u8
|
||||
|
||||
// Movies
|
||||
http://[dns]/movie/[username]/[password]/[stream_id].[container_extension]
|
||||
|
||||
// Series
|
||||
http://[dns]/series/[username]/[password]/[stream_id].[container_extension]
|
||||
```
|
||||
|
||||
## Docker Configuration
|
||||
|
||||
### Dockerfile (Multi-Stage)
|
||||
|
||||
**Stage 1: Builder**
|
||||
- Base: `cirrusci/flutter:stable`
|
||||
- Build: `flutter build web --release --web-renderer html`
|
||||
|
||||
**Stage 2: Runtime**
|
||||
- Base: `dart:stable`
|
||||
- Server: `dhttpd --host 0.0.0.0 --port 8080`
|
||||
- Size: ~150MB (compressed)
|
||||
|
||||
### docker-compose.yml
|
||||
|
||||
```yaml
|
||||
services:
|
||||
iptv-web:
|
||||
build: .
|
||||
container_name: xtremflow
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- nginx_default
|
||||
|
||||
networks:
|
||||
nginx_default:
|
||||
external: true
|
||||
```
|
||||
|
||||
**No port mapping** - Access via reverse proxy only.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Container won't start
|
||||
```bash
|
||||
# Check logs
|
||||
docker logs xtremflow
|
||||
|
||||
# Verify network exists
|
||||
docker network ls | grep nginx_default
|
||||
|
||||
# Create network if missing
|
||||
docker network create nginx_default
|
||||
```
|
||||
|
||||
### Login fails with admin/admin
|
||||
- Check Hive database initialization in logs
|
||||
- Verify `HiveService.init()` completed successfully
|
||||
- Default admin is seeded only if `users` box is empty
|
||||
|
||||
### EPG not displaying
|
||||
- EPG is optional and gracefully degrades
|
||||
- Check if Xtream server supports `get_short_epg`
|
||||
- Verify stream has `epg_channel_id`
|
||||
|
||||
### Performance issues (FPS drops)
|
||||
- Reduce `_itemsPerPage` constant (currently 100 for Live TV)
|
||||
- Disable image caching temporarily
|
||||
- Check browser DevTools Performance tab
|
||||
|
||||
## License
|
||||
|
||||
Proprietary - Private Use Only
|
||||
|
||||
## Support
|
||||
|
||||
For Xtream API documentation, consult your IPTV provider.
|
||||
Propriétaire — usage privé uniquement.
|
||||
@@ -0,0 +1,477 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "9a3386eea899815698dd55995277cf7cb8572ee52b399a6edfb7ae2b50e5fc19"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "105.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "62993bed6eadbe9596c5c20d5c167e7bc563c5fe266657a04ddeb93bdb84f4c9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.1.0"
|
||||
args:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.1"
|
||||
bcrypt:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: bcrypt
|
||||
sha256: "6073a700cbbc59f1d4ab27cd532755e3de5e676c4941f535f351374df849270b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
cli_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_config
|
||||
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
coverage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.1"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
equatable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: equatable
|
||||
sha256: "3bce007a596ff8b3119c45d68aaef631272537c03d30e5d4534dd24bf4c5eaa2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
hive:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hive
|
||||
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
http_methods:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_methods
|
||||
sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.20"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_preamble
|
||||
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
shelf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shelf
|
||||
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
shelf_packages_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_packages_handler
|
||||
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
shelf_router:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shelf_router
|
||||
sha256: f5e5d492440a7fb165fe1e2e1a623f31f734d3370900070b2b1e0d0428d59864
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.4"
|
||||
shelf_static:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shelf_static
|
||||
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_map_stack_trace
|
||||
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
source_maps:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
sha256: "14c2945847669b44089bb1222f66873d7ff7103c58911917f2a63c5a62327898"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.14"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.2"
|
||||
sqlite3:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.4"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
sha256: "0d5ba5602ec3baa28c8ce365e1efc5575969c765f45c554a3e167dc7945b9c30"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.31.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "475610b2aa23c19687cce2961e44b0cc57cafe220f67c2b80201231b2a07fbe7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.13"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: a39c204a4fc7a7ccb04a2b985e359fda3cc37e45e0b8ac61c3fb1a05aa832132
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.19"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
uuid:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.3.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webkit_inspection_protocol
|
||||
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
xml:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: xml
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.11.0 <4.0.0"
|
||||
+4
-1
@@ -13,8 +13,11 @@ services:
|
||||
# Pour changer: modifiez le chemin avant les deux points ":"
|
||||
# Exemple Windows : - D:\MesVideos\TV:/app/recordings
|
||||
# Exemple Linux : - /mnt/nas/videos:/app/recordings
|
||||
- ${RECORDINGS_PATH:-/mnt/user/Data/Sport}:/app/recordings
|
||||
- ${RECORDINGS_PATH:-./data/recordings}:/app/recordings
|
||||
environment:
|
||||
# Fuseau horaire du conteneur (affichage des logs ; les enregistrements
|
||||
# sont stockés en UTC quoi qu'il arrive)
|
||||
- TZ=${TZ:-Europe/Paris}
|
||||
# Origine externe autorisée pour CORS (optionnel — l'app est servie
|
||||
# same-origin, ne définir que si un autre domaine doit appeler l'API)
|
||||
- ALLOWED_ORIGIN=${ALLOWED_ORIGIN:-}
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,12 @@
|
||||
# Archives — documents historiques
|
||||
|
||||
⚠️ **Les documents de ce dossier sont historiques et ne décrivent pas l'état actuel du code.**
|
||||
|
||||
Ils datent de phases de conception ou de refontes antérieures. Plusieurs se déclarent « COMPLETE » ou « production-ready » alors que le travail décrit n'a jamais été intégré (ou a été remplacé depuis) :
|
||||
|
||||
- `RECORDING_SYSTEM_UPGRADE.txt` — décrit un remplacement du scheduler par `SimpleRecorder` qui n'a jamais été branché ; le système réel est `bin/services/recording_scheduler.dart`.
|
||||
- `THEME_REDESIGN_SUMMARY.md` — la « Phase 4: Integration » n'a pas eu lieu ; les widgets qu'il liste n'existent plus.
|
||||
- `ANALYSIS_AND_IMPROVEMENTS.md` — plan d'améliorations dont une partie seulement a été réalisée.
|
||||
- `DEPLOYMENT_CHECKLIST.md` — référence des fichiers et versions de dépendances qui n'ont jamais existé dans le dépôt.
|
||||
|
||||
Pour l'état actuel du projet, se référer au `README.md` racine et au `CHANGELOG.md`.
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../auth/providers/auth_provider.dart';
|
||||
import '../../../core/models/playlist_config.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/widgets/glass_container.dart';
|
||||
@@ -200,13 +201,61 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// Settings / Profile
|
||||
// Profil : nom d'utilisateur + déconnexion. L'avatar était
|
||||
// un bouton mort (onTap vide) — aucune déconnexion possible
|
||||
// depuis le dashboard desktop.
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
child: TvFocusableCard(
|
||||
onTap: () {},
|
||||
borderRadius: 50,
|
||||
scaleFactor: 1.1,
|
||||
child: PopupMenuButton<String>(
|
||||
tooltip: 'Profil',
|
||||
color: AppColors.surfaceContainerHigh,
|
||||
offset: const Offset(56, -12),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem<String>(
|
||||
enabled: false,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.person,
|
||||
size: 18,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
ref.read(authProvider).currentUser?.username ??
|
||||
'Utilisateur',
|
||||
style: const TextStyle(
|
||||
color: AppColors.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'logout',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.logout,
|
||||
size: 18,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Déconnexion',
|
||||
style: TextStyle(color: AppColors.onSurface),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
onSelected: (value) {
|
||||
if (value == 'logout') {
|
||||
ref.read(authProvider.notifier).logout();
|
||||
}
|
||||
},
|
||||
child: const CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: AppColors.surfaceContainerHigh,
|
||||
|
||||
@@ -271,8 +271,10 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
// Update watch history if relevant
|
||||
if (currentTime > 0) {
|
||||
// Update watch history if relevant. Jamais en live : la « position »
|
||||
// d'un flux continu n'a pas de sens et polluait le stockage avec une
|
||||
// entrée bidon par chaîne zappée.
|
||||
if (currentTime > 0 && widget.streamType != StreamType.live) {
|
||||
ref.read(playbackPositionsProvider.notifier).savePosition(
|
||||
widget.streamId,
|
||||
currentTime,
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../../core/models/playlist_config.dart';
|
||||
import '../models/xtream_models.dart';
|
||||
import '../providers/xtream_provider.dart';
|
||||
import '../providers/playback_positions_provider.dart';
|
||||
import '../providers/watch_history_provider.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import 'player_screen.dart';
|
||||
@@ -287,7 +288,14 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
|
||||
}
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
|
||||
// Reprise de lecture : le player sauvegarde la position sous
|
||||
// episode.id, on la relit ici pour reprendre où on s'était arrêté.
|
||||
final positions = ref.read(playbackPositionsProvider);
|
||||
final resumeAt = positions.hasPosition(episode.id)
|
||||
? positions.getPosition(episode.id)
|
||||
: null;
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -298,6 +306,7 @@ class _SeriesDetailScreenState extends ConsumerState<SeriesDetailScreen> {
|
||||
streamType: StreamType.series,
|
||||
containerExtension: episode.containerExtension ?? 'mkv',
|
||||
duration: episodeDuration,
|
||||
startTime: resumeAt,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -63,8 +63,23 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
|
||||
body: channelsAsync.when(
|
||||
loading: () => const ThemedLoading(),
|
||||
error: (e, s) => Center(
|
||||
child: Text('Error: $e',
|
||||
style: const TextStyle(color: AppColors.onSurface)),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Impossible de charger les chaînes',
|
||||
style: TextStyle(color: AppColors.onSurface),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
label: const Text('Réessayer'),
|
||||
onPressed: () => ref.invalidate(
|
||||
liveChannelsByPlaylistProvider(widget.playlist),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
data: (groupedChannels) {
|
||||
var categories = groupedChannels.keys.toList();
|
||||
@@ -515,6 +530,43 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
|
||||
),
|
||||
],
|
||||
),
|
||||
// Favori (Positioned top left) — toggleFavorite n'était appelé
|
||||
// nulle part : le filtre « Favoris » affichait toujours vide.
|
||||
Positioned(
|
||||
top: 8,
|
||||
left: 8,
|
||||
child: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final isFav = ref
|
||||
.watch(favoritesProvider)
|
||||
.contains(channel.streamId);
|
||||
return TvFocusableCard(
|
||||
onTap: () => ref
|
||||
.read(favoritesProvider.notifier)
|
||||
.toggleFavorite(channel.streamId),
|
||||
borderRadius: 20,
|
||||
scaleFactor: 1.2,
|
||||
semanticLabel: isFav
|
||||
? 'Retirer ${channel.name} des favoris'
|
||||
: 'Ajouter ${channel.name} aux favoris',
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
isFav ? Icons.favorite : Icons.favorite_border,
|
||||
color: isFav
|
||||
? AppColors.primary
|
||||
: AppColors.onSurface54,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Record Button Icon Overlay (Positioned top right)
|
||||
Positioned(
|
||||
top: 8,
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../../../core/utils/responsive_layout.dart';
|
||||
import '../../../core/widgets/hero_carousel.dart';
|
||||
import '../../../core/widgets/glass_container.dart';
|
||||
import '../../../core/widgets/tv_focusable_card.dart';
|
||||
import '../providers/playback_positions_provider.dart';
|
||||
import '../providers/watch_history_provider.dart';
|
||||
import '../models/xtream_models.dart';
|
||||
import '../providers/xtream_provider.dart';
|
||||
@@ -163,6 +164,13 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Reprise de lecture : les positions étaient sauvegardées par le player
|
||||
// mais jamais relues — le film repartait systématiquement de zéro.
|
||||
final positions = ref.read(playbackPositionsProvider);
|
||||
final resumeAt = positions.hasPosition(movie.streamId)
|
||||
? positions.getPosition(movie.streamId)
|
||||
: null;
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -173,6 +181,7 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
|
||||
streamType: StreamType.vod,
|
||||
containerExtension: movie.containerExtension ?? 'mp4',
|
||||
duration: movieDuration,
|
||||
startTime: resumeAt,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -15,6 +15,10 @@ import '../screens/player_screen.dart';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// ENTRÉE — Onglet "Enregistrements"
|
||||
// Trois vues : Guide TV (programmer depuis l'EPG), Enregistrements (liste),
|
||||
// Season Passes (enregistrements récurrents). _EpgGuideView et
|
||||
// _SeasonPassesView existaient déjà mais n'étaient plus instanciés depuis
|
||||
// une refonte : les fonctions étaient codées mais inaccessibles.
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
class RecordingsTab extends StatefulWidget {
|
||||
@@ -25,7 +29,24 @@ class RecordingsTab extends StatefulWidget {
|
||||
State<RecordingsTab> createState() => _RecordingsTabState();
|
||||
}
|
||||
|
||||
class _RecordingsTabState extends State<RecordingsTab> {
|
||||
class _RecordingsTabState extends State<RecordingsTab>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final TabController _tabController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Ouvrir sur la liste des enregistrements (onglet du milieu), l'usage le
|
||||
// plus fréquent ; le guide sert à en programmer de nouveaux.
|
||||
_tabController = TabController(length: 3, vsync: this, initialIndex: 1);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@@ -33,25 +54,59 @@ class _RecordingsTabState extends State<RecordingsTab> {
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 16),
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
|
||||
color: Colors.grey[900],
|
||||
child: const Row(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.videocam, color: AppColors.onSurface, size: 28),
|
||||
SizedBox(width: 12),
|
||||
Text(
|
||||
'Enregistrements',
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.onSurface,
|
||||
),
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.videocam, color: AppColors.onSurface, size: 28),
|
||||
SizedBox(width: 12),
|
||||
Text(
|
||||
'Enregistrements',
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
isScrollable: true,
|
||||
indicatorColor: AppColors.primary,
|
||||
labelColor: AppColors.onSurface,
|
||||
unselectedLabelColor: AppColors.onSurface54,
|
||||
tabs: const [
|
||||
Tab(
|
||||
icon: Icon(Icons.calendar_month, size: 18),
|
||||
text: 'Guide TV',
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.fiber_manual_record, size: 18),
|
||||
text: 'Enregistrements',
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.repeat, size: 18),
|
||||
text: 'Season Passes',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _RecordingsListView(playlist: widget.playlist),
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_EpgGuideView(playlist: widget.playlist),
|
||||
_RecordingsListView(playlist: widget.playlist),
|
||||
const _SeasonPassesView(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -825,7 +880,39 @@ class _RecordingsListViewState extends State<_RecordingsListView> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteRecording(String id) async {
|
||||
/// Demande confirmation avant suppression : l'ancienne corbeille supprimait
|
||||
/// immédiatement, sans retour ni possibilité d'annuler.
|
||||
Future<void> _deleteRecording(String id, String title) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: AppColors.surfaceContainer,
|
||||
title: Text(
|
||||
'Supprimer l\'enregistrement ?',
|
||||
style: GoogleFonts.fraunces(color: AppColors.onSurface, fontSize: 18),
|
||||
),
|
||||
content: Text(
|
||||
'« $title » et son fichier seront définitivement supprimés.',
|
||||
style: const TextStyle(color: AppColors.onSurfaceVariant),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.errorContainer,
|
||||
foregroundColor: AppColors.onErrorContainer,
|
||||
),
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
|
||||
await AuthedHttp.delete(Uri.parse('/api/recordings/$id'));
|
||||
_fetchRecordings();
|
||||
}
|
||||
@@ -986,9 +1073,20 @@ class _RecordingsListViewState extends State<_RecordingsListView> {
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _error != null
|
||||
? Center(
|
||||
child: Text(
|
||||
_error ?? 'Erreur',
|
||||
style: const TextStyle(color: AppColors.live),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Impossible de charger les enregistrements',
|
||||
style: TextStyle(color: AppColors.live),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
label: const Text('Réessayer'),
|
||||
onPressed: _fetchRecordings,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: _recordings.isEmpty
|
||||
@@ -1157,8 +1255,10 @@ class _RecordingsListViewState extends State<_RecordingsListView> {
|
||||
size: 20,
|
||||
),
|
||||
tooltip: 'Supprimer',
|
||||
onPressed: () =>
|
||||
_deleteRecording(rec['id']),
|
||||
onPressed: () => _deleteRecording(
|
||||
rec['id'],
|
||||
rec['title'] ?? '',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../core/models/playlist_config.dart';
|
||||
import '../../../widgets/mobile_scaffold.dart';
|
||||
import '../../../theme/mobile_theme.dart';
|
||||
import '../../../../features/iptv/widgets/recordings_tab.dart';
|
||||
import '../widgets/mobile_live_tv_tab.dart';
|
||||
import '../widgets/mobile_movies_tab.dart';
|
||||
import '../widgets/mobile_series_tab.dart';
|
||||
@@ -34,23 +35,20 @@ class _MobileDashboardScreenState extends ConsumerState<MobileDashboardScreen> {
|
||||
_currentIndex = index;
|
||||
});
|
||||
},
|
||||
child: _buildActiveTab(),
|
||||
// IndexedStack conserve l'état des onglets (position de scroll,
|
||||
// catalogues chargés) : l'ancien switch reconstruisait tout à chaque
|
||||
// changement d'onglet. Même approche que le dashboard desktop.
|
||||
child: IndexedStack(
|
||||
index: _currentIndex,
|
||||
children: [
|
||||
MobileLiveTVTab(playlist: widget.playlist),
|
||||
MobileMoviesTab(playlist: widget.playlist),
|
||||
MobileSeriesTab(playlist: widget.playlist),
|
||||
RecordingsTab(playlist: widget.playlist),
|
||||
const MobileSettingsTab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActiveTab() {
|
||||
switch (_currentIndex) {
|
||||
case 0:
|
||||
return MobileLiveTVTab(playlist: widget.playlist);
|
||||
case 1:
|
||||
return MobileMoviesTab(playlist: widget.playlist);
|
||||
case 2:
|
||||
return MobileSeriesTab(playlist: widget.playlist);
|
||||
case 3:
|
||||
return const MobileSettingsTab();
|
||||
default:
|
||||
return MobileLiveTVTab(playlist: widget.playlist);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -335,7 +335,7 @@ class _MobileLiveTVTabState extends ConsumerState<MobileLiveTVTab> {
|
||||
}
|
||||
}
|
||||
|
||||
class _MobileChannelTile extends StatelessWidget {
|
||||
class _MobileChannelTile extends ConsumerWidget {
|
||||
final Channel channel;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@@ -345,7 +345,10 @@ class _MobileChannelTile extends StatelessWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Sans ce bouton, aucun moyen d'ajouter un favori : le filtre « Favoris »
|
||||
// de l'en-tête affichait toujours une liste vide.
|
||||
final isFav = ref.watch(favoritesProvider).contains(channel.streamId);
|
||||
final iconUrl =
|
||||
channel.streamIcon.isNotEmpty && channel.streamIcon.startsWith('http')
|
||||
? '/api/xtream/${channel.streamIcon}'
|
||||
@@ -393,6 +396,18 @@ class _MobileChannelTile extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
tooltip: isFav ? 'Retirer des favoris' : 'Ajouter aux favoris',
|
||||
icon: Icon(
|
||||
isFav ? Icons.favorite : Icons.favorite_border,
|
||||
color: isFav ? AppColors.primary : AppColors.onSurface54,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () => ref
|
||||
.read(favoritesProvider.notifier)
|
||||
.toggleFavorite(channel.streamId),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
|
||||
@@ -78,6 +78,11 @@ class MobileScaffold extends ConsumerWidget {
|
||||
activeIcon: Icon(Icons.video_library_rounded),
|
||||
label: 'Series',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.videocam_outlined),
|
||||
activeIcon: Icon(Icons.videocam_rounded),
|
||||
label: 'REC',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
activeIcon: Icon(Icons.settings_rounded),
|
||||
|
||||
+1266
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user