msansm1/rss-podcast-dlpublic Fork
⑂ ed3f0cc
Commits
⬇ Clone ▾
git clone https://git.rickub.com/msansm1/rss-podcast-dl.git
git clone ssh://git@rickub.com/msansm1/rss-podcast-dl.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

Phase 8: Playlists, Tags et Filtrage (T8.3-T8.6)

msansm1 committed 2026-07-05T21:57:52+02:00 Browse files
ed3f0cc parent: a614bb5
modified spec/init-plan.md +1 -1
@@ -322,7 +322,7 @@ pub async fn download_episode(
322322 | Phase 5 | 2 jours | J11-J12 | ✅ |
323323 | Phase 6 | 2 jours | J13-J14 | ✅ |
324324 | Phase 7 | 1 jour | J15 | ✅ |
325-| Phase 8 | 3 jours | J16-J18 | 🟡 25% (2/8 tâches) |
325+| Phase 8 | 3 jours | J16-J18 | 🟡 75% (6/8 tâches) |
326326 | **Total** | **18 jours** | | |
327327
328328 ---
@@ -322,7 +322,7 @@ pub async fn download_episode(
322 | Phase 5 | 2 jours | J11-J12 | ✅ |322 | Phase 5 | 2 jours | J11-J12 | ✅ |
323 | Phase 6 | 2 jours | J13-J14 | ✅ |323 | Phase 6 | 2 jours | J13-J14 | ✅ |
324 | Phase 7 | 1 jour | J15 | ✅ |324 | Phase 7 | 1 jour | J15 | ✅ |
325-| Phase 8 | 3 jours | J16-J18 | 🟡 25% (2/8 tâches) |325+| Phase 8 | 3 jours | J16-J18 | 🟡 75% (6/8 tâches) |
326 | **Total** | **18 jours** | | |326 | **Total** | **18 jours** | | |
327 327
328 ---328 ---
modified spec/init-tasks.md +6 -6
@@ -182,10 +182,10 @@
182182 |----|-------|----------|-------|---------|-------------|--------|
183183 | T8.1 | Implémenter la détection automatique des nouveaux épisodes | H | 3h | `src/rss/sync.rs`, `src/config/feed.rs` | T5.2, T5.4 | ✅ |
184184 | T8.2 | Implémenter les notifications de téléchargement (popups) | H | 4h | `src/ui/notifications.rs`, `src/ui/mod.rs`, `src/app.rs`, `src/config/app_state.rs`, `src/ui/home.rs`, `src/ui/feed_list.rs` | T4.13, T5.3 | ✅ |
185-| T8.3 | Implémenter le support des playlists (groupement de flux) | M | 3h | `src/config/playlist.rs`, `src/ui/playlist_list.rs` | T2.4, T4.7 | ⬜ |
186-| T8.4 | Implémenter le filtrage par date | M | 2h | `src/ui/filter_panel.rs`, `src/rss/sync.rs` | T5.2 | ⬜ |
187-| T8.5 | Implémenter le filtrage par numéro d'épisode | M | 2h | `src/ui/filter_panel.rs`, `src/config/episode.rs` | T2.3, T8.4 | ⬜ |
188-| T8.6 | Ajouter un système de tags pour les flux | L | 2h | `src/config/feed.rs`, `src/ui/feed_edit_dialog.rs` | T2.1 | ⬜ |
185+| T8.3 | Implémenter le support des playlists (groupement de flux) | M | 3h | `src/config/playlist.rs`, `src/ui/playlist_list.rs`, `src/config/mod.rs`, `src/config/app_state.rs`, `src/config/storage.rs`, `src/app.rs`, `src/ui/home.rs`, `src/ui/mod.rs` | T2.4, T4.7 | ✅ |
186+| T8.4 | Implémenter le filtrage par date | M | 2h | `src/ui/filter_panel.rs`, `src/config/app_state.rs`, `src/ui/feed_list.rs`, `src/ui/home.rs`, `src/ui/mod.rs` | T5.2 | ✅ |
187+| T8.5 | Implémenter le filtrage par numéro d'épisode | M | 2h | `src/ui/filter_panel.rs`, `src/config/episode.rs`, `src/config/app_state.rs` | T2.3, T8.4 | ✅ |
188+| T8.6 | Ajouter un système de tags pour les flux | L | 2h | `src/config/feed.rs`, `src/ui/feed_edit_dialog.rs`, `src/ui/feed_list.rs`, `src/config/app_state.rs` | T2.1 | ✅ |
189189 | T8.7 | Implémenter l'export des flux (fichier JSON personnalisé) | L | 2h | `src/config/storage.rs`, `src/ui/home.rs` | T2.8 | ⬜ |
190190 | T8.8 | Implémenter l'import des flux | L | 2h | `src/config/storage.rs`, `src/ui/home.rs` | T8.7 | ⬜ |
191191
@@ -204,8 +204,8 @@
204204 | Phase 5 | 10 tâches | 16h | ✅ Terminé |
205205 | Phase 6 | 12 tâches | 24h | ✅ Terminé |
206206 | Phase 7 | 10 tâches | 11h | ✅ Terminé |
207-| Phase 8 | 8 tâches | 20h | 🟡 25% complet (2/8) |
208-| **Total** | **88 tâches** | **145 heures** (~18 jours) | 🟡 93.2% complet |
207+| Phase 8 | 8 tâches | 20h | 🟡 75% complet (6/8) |
208+| **Total** | **88 tâches** | **145 heures** (~18 jours) | 🟡 97.7% complet |
209209
210210 ---
211211
@@ -182,10 +182,10 @@
182 |----|-------|----------|-------|---------|-------------|--------|182 |----|-------|----------|-------|---------|-------------|--------|
183 | T8.1 | Implémenter la détection automatique des nouveaux épisodes | H | 3h | `src/rss/sync.rs`, `src/config/feed.rs` | T5.2, T5.4 | ✅ |183 | T8.1 | Implémenter la détection automatique des nouveaux épisodes | H | 3h | `src/rss/sync.rs`, `src/config/feed.rs` | T5.2, T5.4 | ✅ |
184 | T8.2 | Implémenter les notifications de téléchargement (popups) | H | 4h | `src/ui/notifications.rs`, `src/ui/mod.rs`, `src/app.rs`, `src/config/app_state.rs`, `src/ui/home.rs`, `src/ui/feed_list.rs` | T4.13, T5.3 | ✅ |184 | T8.2 | Implémenter les notifications de téléchargement (popups) | H | 4h | `src/ui/notifications.rs`, `src/ui/mod.rs`, `src/app.rs`, `src/config/app_state.rs`, `src/ui/home.rs`, `src/ui/feed_list.rs` | T4.13, T5.3 | ✅ |
185-| T8.3 | Implémenter le support des playlists (groupement de flux) | M | 3h | `src/config/playlist.rs`, `src/ui/playlist_list.rs` | T2.4, T4.7 | ⬜ |185+| T8.3 | Implémenter le support des playlists (groupement de flux) | M | 3h | `src/config/playlist.rs`, `src/ui/playlist_list.rs`, `src/config/mod.rs`, `src/config/app_state.rs`, `src/config/storage.rs`, `src/app.rs`, `src/ui/home.rs`, `src/ui/mod.rs` | T2.4, T4.7 | ✅ |
186-| T8.4 | Implémenter le filtrage par date | M | 2h | `src/ui/filter_panel.rs`, `src/rss/sync.rs` | T5.2 | ⬜ |186+| T8.4 | Implémenter le filtrage par date | M | 2h | `src/ui/filter_panel.rs`, `src/config/app_state.rs`, `src/ui/feed_list.rs`, `src/ui/home.rs`, `src/ui/mod.rs` | T5.2 | ✅ |
187-| T8.5 | Implémenter le filtrage par numéro d'épisode | M | 2h | `src/ui/filter_panel.rs`, `src/config/episode.rs` | T2.3, T8.4 | ⬜ |187+| T8.5 | Implémenter le filtrage par numéro d'épisode | M | 2h | `src/ui/filter_panel.rs`, `src/config/episode.rs`, `src/config/app_state.rs` | T2.3, T8.4 | ✅ |
188-| T8.6 | Ajouter un système de tags pour les flux | L | 2h | `src/config/feed.rs`, `src/ui/feed_edit_dialog.rs` | T2.1 | ⬜ |188+| T8.6 | Ajouter un système de tags pour les flux | L | 2h | `src/config/feed.rs`, `src/ui/feed_edit_dialog.rs`, `src/ui/feed_list.rs`, `src/config/app_state.rs` | T2.1 | ✅ |
189 | T8.7 | Implémenter l'export des flux (fichier JSON personnalisé) | L | 2h | `src/config/storage.rs`, `src/ui/home.rs` | T2.8 | ⬜ |189 | T8.7 | Implémenter l'export des flux (fichier JSON personnalisé) | L | 2h | `src/config/storage.rs`, `src/ui/home.rs` | T2.8 | ⬜ |
190 | T8.8 | Implémenter l'import des flux | L | 2h | `src/config/storage.rs`, `src/ui/home.rs` | T8.7 | ⬜ |190 | T8.8 | Implémenter l'import des flux | L | 2h | `src/config/storage.rs`, `src/ui/home.rs` | T8.7 | ⬜ |
191 191
@@ -204,8 +204,8 @@
204 | Phase 5 | 10 tâches | 16h | ✅ Terminé |204 | Phase 5 | 10 tâches | 16h | ✅ Terminé |
205 | Phase 6 | 12 tâches | 24h | ✅ Terminé |205 | Phase 6 | 12 tâches | 24h | ✅ Terminé |
206 | Phase 7 | 10 tâches | 11h | ✅ Terminé |206 | Phase 7 | 10 tâches | 11h | ✅ Terminé |
207-| Phase 8 | 8 tâches | 20h | 🟡 25% complet (2/8) |207+| Phase 8 | 8 tâches | 20h | 🟡 75% complet (6/8) |
208-| **Total** | **88 tâches** | **145 heures** (~18 jours) | 🟡 93.2% complet |208+| **Total** | **88 tâches** | **145 heures** (~18 jours) | 🟡 97.7% complet |
209 209
210 ---210 ---
211 211
modified spec/progress.md +23 -23
@@ -1,7 +1,7 @@
11 # RSS Podcast Downloader - Suivi des Progrès
22
33 *Dernière mise à jour: 05/07/2026*
4-*Version: 2.1*
4+*Version: 2.2*
55
66 ---
77
@@ -255,7 +255,7 @@
255255
256256 ---
257257
258-## Phase 8 : Fonctionnalités Avancées (2/8 tâches - 25%)
258+## Phase 8 : Fonctionnalités Avancées (6/8 tâches - 75%)
259259
260260 - [x] T8.1 - Implémenter la détection automatique des nouveaux épisodes
261261 - *Fichiers: src/rss/sync.rs, src/rss/downloader.rs*
@@ -267,21 +267,25 @@
267267 - *Contenu: Module NotificationManager complet avec affichage dans UI, notifications pour synchronisation et suppression*
268268 - *Date: 05/07/2026*
269269
270-- [ ] T8.3 - Implémenter le support des playlists (groupement de flux)
271- - *Fichiers: src/config/playlist.rs, src/ui/playlist_list.rs*
272- - *Statut: Non commencé*
270+- [x] T8.3 - Implémenter le support des playlists (groupement de flux)
271+ - *Fichiers: src/config/playlist.rs, src/ui/playlist_list.rs, src/config/mod.rs, src/config/app_state.rs, src/config/storage.rs, src/app.rs, src/ui/home.rs, src/ui/mod.rs*
272+ - *Contenu: Structure Playlist avec feed_ids, UI pour créer/éditer/supprimer des playlists, persistance des playlists*
273+ - *Date: 05/07/2026*
273274
274-- [ ] T8.4 - Implémenter le filtrage par date
275- - *Fichiers: src/ui/filter_panel.rs, src/rss/sync.rs*
276- - *Statut: Non commencé*
275+- [x] T8.4 - Implémenter le filtrage par date
276+ - *Fichiers: src/ui/filter_panel.rs, src/config/app_state.rs, src/ui/feed_list.rs, src/ui/home.rs, src/ui/mod.rs*
277+ - *Contenu: Panneau de filtrage avec sélection de dates (depuis/jusqu'à), boutons rapides (7j, 30j)*
278+ - *Date: 05/07/2026*
277279
278-- [ ] T8.5 - Implémenter le filtrage par numéro d'épisode
279- - *Fichiers: src/ui/filter_panel.rs, src/config/episode.rs*
280- - *Statut: Non commencé*
280+- [x] T8.5 - Implémenter le filtrage par numéro d'épisode
281+ - *Fichiers: src/ui/filter_panel.rs, src/config/episode.rs, src/config/app_state.rs*
282+ - *Contenu: Filtrage par numéro dans le titre de l'épisode, extraction automatique du numéro*
283+ - *Date: 05/07/2026*
281284
282-- [ ] T8.6 - Ajouter un système de tags pour les flux
283- - *Fichiers: src/config/feed.rs, src/ui/feed_edit_dialog.rs*
284- - *Statut: Non commencé*
285+- [x] T8.6 - Ajouter un système de tags pour les flux
286+ - *Fichiers: src/config/feed.rs, src/ui/feed_edit_dialog.rs, src/ui/feed_list.rs, src/config/app_state.rs*
287+ - *Contenu: Champ tags dans Feed, édition des tags dans le dialogue d'édition, affichage des tags dans la liste*
288+ - *Date: 05/07/2026*
285289
286290 - [ ] T8.7 - Implémenter l'export des flux (fichier JSON personnalisé)
287291 - *Fichiers: src/config/storage.rs, src/ui/home.rs*
@@ -304,20 +308,16 @@
304308 | Phase 5 | 10 | 10 | 100% | Terminé |
305309 | Phase 6 | 12 | 12 | 100% | Terminé |
306310 | Phase 7 | 10 | 10 | 100% | Terminé |
307-| Phase 8 | 8 | 2 | 25% | En cours |
308-| **Total** | **88** | **82** | **93.2%** | En cours |
311+| Phase 8 | 8 | 6 | 75% | En cours |
312+| **Total** | **88** | **86** | **97.7%** | En cours |
309313
310-**Projet complété à 93.2%**
314+**Projet complété à 97.7%**
311315
312316 ---
313317
314318 ## Prochaines Étapes
315319
316-### Phase 8 en cours (2/8 tâches)
317-- Continuer avec T8.3 - Implémenter le support des playlists
318-- Continuer avec T8.4 - Implémenter le filtrage par date
319-- Continuer avec T8.5 - Implémenter le filtrage par numéro d'épisode
320-- Continuer avec T8.6 - Ajouter un système de tags pour les flux
320+### Phase 8 en cours (6/8 tâches - 75% complet)
321321 - Continuer avec T8.7 - Implémenter l'export des flux
322322 - Continuer avec T8.8 - Implémenter l'import des flux
323323
@@ -397,4 +397,4 @@ Phase 7: Finalisation
397397 ---
398398
399399 *Document généré et mis à jour le 05/07/2026*
400-*Projet RSS Podcast Downloader - 93.2% complet (Phase 8 en cours)*
400+*Projet RSS Podcast Downloader - 97.7% complet (Phase 8 en cours - 6/8 tâches)*
@@ -1,7 +1,7 @@
1 # RSS Podcast Downloader - Suivi des Progrès1 # RSS Podcast Downloader - Suivi des Progrès
2 2
3 *Dernière mise à jour: 05/07/2026*3 *Dernière mise à jour: 05/07/2026*
4-*Version: 2.1*4+*Version: 2.2*
5 5
6 ---6 ---
7 7
@@ -255,7 +255,7 @@
255 255
256 ---256 ---
257 257
258-## Phase 8 : Fonctionnalités Avancées (2/8 tâches - 25%)258+## Phase 8 : Fonctionnalités Avancées (6/8 tâches - 75%)
259 259
260 - [x] T8.1 - Implémenter la détection automatique des nouveaux épisodes260 - [x] T8.1 - Implémenter la détection automatique des nouveaux épisodes
261 - *Fichiers: src/rss/sync.rs, src/rss/downloader.rs*261 - *Fichiers: src/rss/sync.rs, src/rss/downloader.rs*
@@ -267,21 +267,25 @@
267 - *Contenu: Module NotificationManager complet avec affichage dans UI, notifications pour synchronisation et suppression*267 - *Contenu: Module NotificationManager complet avec affichage dans UI, notifications pour synchronisation et suppression*
268 - *Date: 05/07/2026*268 - *Date: 05/07/2026*
269 269
270-- [ ] T8.3 - Implémenter le support des playlists (groupement de flux)270+- [x] T8.3 - Implémenter le support des playlists (groupement de flux)
271- - *Fichiers: src/config/playlist.rs, src/ui/playlist_list.rs*271+ - *Fichiers: src/config/playlist.rs, src/ui/playlist_list.rs, src/config/mod.rs, src/config/app_state.rs, src/config/storage.rs, src/app.rs, src/ui/home.rs, src/ui/mod.rs*
272- - *Statut: Non commencé*272+ - *Contenu: Structure Playlist avec feed_ids, UI pour créer/éditer/supprimer des playlists, persistance des playlists*
273+ - *Date: 05/07/2026*
273 274
274-- [ ] T8.4 - Implémenter le filtrage par date275+- [x] T8.4 - Implémenter le filtrage par date
275- - *Fichiers: src/ui/filter_panel.rs, src/rss/sync.rs*276+ - *Fichiers: src/ui/filter_panel.rs, src/config/app_state.rs, src/ui/feed_list.rs, src/ui/home.rs, src/ui/mod.rs*
276- - *Statut: Non commencé*277+ - *Contenu: Panneau de filtrage avec sélection de dates (depuis/jusqu'à), boutons rapides (7j, 30j)*
278+ - *Date: 05/07/2026*
277 279
278-- [ ] T8.5 - Implémenter le filtrage par numéro d'épisode280+- [x] T8.5 - Implémenter le filtrage par numéro d'épisode
279- - *Fichiers: src/ui/filter_panel.rs, src/config/episode.rs*281+ - *Fichiers: src/ui/filter_panel.rs, src/config/episode.rs, src/config/app_state.rs*
280- - *Statut: Non commencé*282+ - *Contenu: Filtrage par numéro dans le titre de l'épisode, extraction automatique du numéro*
283+ - *Date: 05/07/2026*
281 284
282-- [ ] T8.6 - Ajouter un système de tags pour les flux285+- [x] T8.6 - Ajouter un système de tags pour les flux
283- - *Fichiers: src/config/feed.rs, src/ui/feed_edit_dialog.rs*286+ - *Fichiers: src/config/feed.rs, src/ui/feed_edit_dialog.rs, src/ui/feed_list.rs, src/config/app_state.rs*
284- - *Statut: Non commencé*287+ - *Contenu: Champ tags dans Feed, édition des tags dans le dialogue d'édition, affichage des tags dans la liste*
288+ - *Date: 05/07/2026*
285 289
286 - [ ] T8.7 - Implémenter l'export des flux (fichier JSON personnalisé)290 - [ ] T8.7 - Implémenter l'export des flux (fichier JSON personnalisé)
287 - *Fichiers: src/config/storage.rs, src/ui/home.rs*291 - *Fichiers: src/config/storage.rs, src/ui/home.rs*
@@ -304,20 +308,16 @@
304 | Phase 5 | 10 | 10 | 100% | Terminé |308 | Phase 5 | 10 | 10 | 100% | Terminé |
305 | Phase 6 | 12 | 12 | 100% | Terminé |309 | Phase 6 | 12 | 12 | 100% | Terminé |
306 | Phase 7 | 10 | 10 | 100% | Terminé |310 | Phase 7 | 10 | 10 | 100% | Terminé |
307-| Phase 8 | 8 | 2 | 25% | En cours |311+| Phase 8 | 8 | 6 | 75% | En cours |
308-| **Total** | **88** | **82** | **93.2%** | En cours |312+| **Total** | **88** | **86** | **97.7%** | En cours |
309 313
310-**Projet complété à 93.2%**314+**Projet complété à 97.7%**
311 315
312 ---316 ---
313 317
314 ## Prochaines Étapes318 ## Prochaines Étapes
315 319
316-### Phase 8 en cours (2/8 tâches)320+### Phase 8 en cours (6/8 tâches - 75% complet)
317-- Continuer avec T8.3 - Implémenter le support des playlists
318-- Continuer avec T8.4 - Implémenter le filtrage par date
319-- Continuer avec T8.5 - Implémenter le filtrage par numéro d'épisode
320-- Continuer avec T8.6 - Ajouter un système de tags pour les flux
321 - Continuer avec T8.7 - Implémenter l'export des flux321 - Continuer avec T8.7 - Implémenter l'export des flux
322 - Continuer avec T8.8 - Implémenter l'import des flux322 - Continuer avec T8.8 - Implémenter l'import des flux
323 323
@@ -397,4 +397,4 @@ Phase 7: Finalisation
397 ---397 ---
398 398
399 *Document généré et mis à jour le 05/07/2026*399 *Document généré et mis à jour le 05/07/2026*
400-*Projet RSS Podcast Downloader - 93.2% complet (Phase 8 en cours)*400+*Projet RSS Podcast Downloader - 97.7% complet (Phase 8 en cours - 6/8 tâches)*
modified src/app.rs +11 -0
@@ -143,6 +143,17 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
143143 }
144144 }
145145
146+ // Load playlists
147+ match crate::config::storage::load_playlists_default() {
148+ Ok(playlists) => {
149+ state.playlists = playlists;
150+ info!("Loaded {} playlists from configuration", state.playlists.len());
151+ }
152+ Err(e) => {
153+ info!("No existing playlists found or error loading: {}", e);
154+ }
155+ }
156+
146157 let app = RssPodcastApp::new(state);
147158
148159 if let Err(e) = eframe::run_native(
@@ -143,6 +143,17 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
143 }143 }
144 }144 }
145 145
146+ // Load playlists
147+ match crate::config::storage::load_playlists_default() {
148+ Ok(playlists) => {
149+ state.playlists = playlists;
150+ info!("Loaded {} playlists from configuration", state.playlists.len());
151+ }
152+ Err(e) => {
153+ info!("No existing playlists found or error loading: {}", e);
154+ }
155+ }
156+
146 let app = RssPodcastApp::new(state);157 let app = RssPodcastApp::new(state);
147 158
148 if let Err(e) = eframe::run_native(159 if let Err(e) = eframe::run_native(
modified src/config/app_state.rs +164 -0
@@ -3,7 +3,9 @@
33 //! This module defines the main application state structure.
44
55 use crate::config::feed::Feed;
6+use crate::config::playlist::Playlist;
67 use crate::ui::notifications::NotificationManager;
8+use chrono::{DateTime, Utc};
79 use std::collections::HashMap;
810 use std::path::PathBuf;
911
@@ -34,8 +36,31 @@ pub struct AppState {
3436 pub edit_feed_download_path: String,
3537 /// Temporary storage for edit feed enabled state
3638 pub edit_feed_enabled: bool,
39+ /// Temporary storage for edit feed tags (comma-separated)
40+ pub edit_feed_tags: String,
3741 /// Index of the feed being edited (if any)
3842 pub editing_feed_index: Option<usize>,
43+ /// Whether the add playlist dialog is visible
44+ #[allow(dead_code)]
45+ pub show_add_playlist_dialog: bool,
46+ /// Temporary storage for add playlist name
47+ #[allow(dead_code)]
48+ pub add_playlist_name: String,
49+ /// Temporary storage for add playlist description
50+ #[allow(dead_code)]
51+ pub add_playlist_description: String,
52+ /// Whether the edit playlist dialog is visible
53+ #[allow(dead_code)]
54+ pub show_edit_playlist_dialog: bool,
55+ /// Temporary storage for edit playlist name
56+ #[allow(dead_code)]
57+ pub edit_playlist_name: String,
58+ /// Temporary storage for edit playlist description
59+ #[allow(dead_code)]
60+ pub edit_playlist_description: String,
61+ /// Index of the playlist being edited (if any)
62+ #[allow(dead_code)]
63+ pub editing_playlist_index: Option<usize>,
3964 /// Whether a synchronization is currently in progress
4065 pub sync_in_progress: bool,
4166 /// Download progress for each feed (feed_id -> progress 0.0-1.0)
@@ -53,6 +78,24 @@ pub struct AppState {
5378 pub config_path: PathBuf,
5479 /// Manager for displaying notifications to the user
5580 pub notification_manager: NotificationManager,
81+ /// List of playlists for grouping feeds
82+ pub playlists: Vec<Playlist>,
83+ // Filter state
84+ /// Filter: date from (inclusive)
85+ #[allow(dead_code)]
86+ pub filter_date_from: Option<DateTime<Utc>>,
87+ /// Filter: date to (inclusive)
88+ #[allow(dead_code)]
89+ pub filter_date_to: Option<DateTime<Utc>>,
90+ /// Filter: episode number
91+ #[allow(dead_code)]
92+ pub filter_episode_number: String,
93+ /// Whether to show the date picker for 'from' date
94+ #[allow(dead_code)]
95+ pub show_date_picker_from: bool,
96+ /// Whether to show the date picker for 'to' date
97+ #[allow(dead_code)]
98+ pub show_date_picker_to: bool,
5699 }
57100
58101 impl AppState {
@@ -70,7 +113,15 @@ impl AppState {
70113 edit_feed_url: String::new(),
71114 edit_feed_download_path: String::new(),
72115 edit_feed_enabled: true,
116+ edit_feed_tags: String::new(),
73117 editing_feed_index: None,
118+ show_add_playlist_dialog: false,
119+ add_playlist_name: String::new(),
120+ add_playlist_description: String::new(),
121+ show_edit_playlist_dialog: false,
122+ edit_playlist_name: String::new(),
123+ edit_playlist_description: String::new(),
124+ editing_playlist_index: None,
74125 sync_in_progress: false,
75126 download_progress: HashMap::new(),
76127 error_message: None,
@@ -78,6 +129,12 @@ impl AppState {
78129 show_settings: false,
79130 config_path: config_path.into(),
80131 notification_manager: NotificationManager::new(),
132+ playlists: Vec::new(),
133+ filter_date_from: None,
134+ filter_date_to: None,
135+ filter_episode_number: String::new(),
136+ show_date_picker_from: false,
137+ show_date_picker_to: false,
81138 }
82139 }
83140
@@ -246,6 +303,7 @@ impl AppState {
246303 self.edit_feed_url = feed.url.clone();
247304 self.edit_feed_download_path = feed.download_path.to_string_lossy().to_string();
248305 self.edit_feed_enabled = feed.enabled;
306+ self.edit_feed_tags = feed.tags_string();
249307 }
250308 }
251309
@@ -255,8 +313,114 @@ impl AppState {
255313 self.edit_feed_url.clear();
256314 self.edit_feed_download_path.clear();
257315 self.edit_feed_enabled = true;
316+ self.edit_feed_tags.clear();
258317 self.editing_feed_index = None;
259318 }
319+
320+ // Playlist methods
321+
322+ /// Adds a new playlist
323+ #[allow(dead_code)]
324+ pub fn add_playlist(&mut self, playlist: Playlist) {
325+ self.playlists.push(playlist);
326+ self.notification_manager.success("Playlist ajoutée");
327+ }
328+
329+ /// Removes a playlist by index
330+ #[allow(dead_code)]
331+ pub fn remove_playlist(&mut self, index: usize) -> Option<Playlist> {
332+ if index < self.playlists.len() {
333+ let playlist = self.playlists.remove(index);
334+ self.notification_manager.success("Playlist supprimée");
335+ Some(playlist)
336+ } else {
337+ None
338+ }
339+ }
340+
341+ /// Gets a playlist by index
342+ #[allow(dead_code)]
343+ pub fn get_playlist(&self, index: usize) -> Option<&Playlist> {
344+ self.playlists.get(index)
345+ }
346+
347+ /// Gets a mutable playlist by index
348+ #[allow(dead_code)]
349+ pub fn get_playlist_mut(&mut self, index: usize) -> Option<&mut Playlist> {
350+ self.playlists.get_mut(index)
351+ }
352+
353+ /// Returns the number of playlists
354+ #[allow(dead_code)]
355+ pub fn playlist_count(&self) -> usize {
356+ self.playlists.len()
357+ }
358+
359+ /// Returns true if there are any playlists
360+ #[allow(dead_code)]
361+ pub fn has_playlists(&self) -> bool {
362+ !self.playlists.is_empty()
363+ }
364+
365+ /// Adds a feed to a playlist by index
366+ #[allow(dead_code)]
367+ pub fn add_feed_to_playlist(&mut self, playlist_index: usize, feed_id: &str) {
368+ if let Some(playlist) = self.playlists.get_mut(playlist_index) {
369+ playlist.add_feed(feed_id);
370+ }
371+ }
372+
373+ /// Removes a feed from a playlist by index
374+ #[allow(dead_code)]
375+ pub fn remove_feed_from_playlist(&mut self, playlist_index: usize, feed_id: &str) {
376+ if let Some(playlist) = self.playlists.get_mut(playlist_index) {
377+ playlist.remove_feed(feed_id);
378+ }
379+ }
380+
381+ /// Gets all playlists that contain a specific feed
382+ #[allow(dead_code)]
383+ pub fn get_playlists_containing_feed(&self, feed_id: &str) -> Vec<usize> {
384+ self.playlists
385+ .iter()
386+ .enumerate()
387+ .filter(|(_, p)| p.contains_feed(feed_id))
388+ .map(|(i, _)| i)
389+ .collect()
390+ }
391+
392+ /// Clears the add playlist form fields
393+ #[allow(dead_code)]
394+ pub fn clear_add_playlist_form(&mut self) {
395+ self.add_playlist_name.clear();
396+ self.add_playlist_description.clear();
397+ }
398+
399+ /// Clears the edit playlist form fields
400+ #[allow(dead_code)]
401+ pub fn clear_edit_playlist_form(&mut self) {
402+ self.edit_playlist_name.clear();
403+ self.edit_playlist_description.clear();
404+ self.editing_playlist_index = None;
405+ }
406+
407+ /// Loads playlist data into edit form fields
408+ #[allow(dead_code)]
409+ pub fn load_edit_playlist_form(&mut self, index: usize) {
410+ if let Some(playlist) = self.playlists.get(index) {
411+ self.edit_playlist_name = playlist.name.clone();
412+ self.edit_playlist_description = playlist.description.clone().unwrap_or_default();
413+ }
414+ }
415+
416+ /// Clears all filter criteria
417+ #[allow(dead_code)]
418+ pub fn clear_filters(&mut self) {
419+ self.filter_date_from = None;
420+ self.filter_date_to = None;
421+ self.filter_episode_number.clear();
422+ self.notification_manager.info("Filtres effacés");
423+ }
260424 }
261425
262426 #[cfg(test)]
@@ -3,7 +3,9 @@
3 //! This module defines the main application state structure.3 //! This module defines the main application state structure.
4 4
5 use crate::config::feed::Feed;5 use crate::config::feed::Feed;
6+use crate::config::playlist::Playlist;
6 use crate::ui::notifications::NotificationManager;7 use crate::ui::notifications::NotificationManager;
8+use chrono::{DateTime, Utc};
7 use std::collections::HashMap;9 use std::collections::HashMap;
8 use std::path::PathBuf;10 use std::path::PathBuf;
9 11
@@ -34,8 +36,31 @@ pub struct AppState {
34 pub edit_feed_download_path: String,36 pub edit_feed_download_path: String,
35 /// Temporary storage for edit feed enabled state37 /// Temporary storage for edit feed enabled state
36 pub edit_feed_enabled: bool,38 pub edit_feed_enabled: bool,
39+ /// Temporary storage for edit feed tags (comma-separated)
40+ pub edit_feed_tags: String,
37 /// Index of the feed being edited (if any)41 /// Index of the feed being edited (if any)
38 pub editing_feed_index: Option<usize>,42 pub editing_feed_index: Option<usize>,
43+ /// Whether the add playlist dialog is visible
44+ #[allow(dead_code)]
45+ pub show_add_playlist_dialog: bool,
46+ /// Temporary storage for add playlist name
47+ #[allow(dead_code)]
48+ pub add_playlist_name: String,
49+ /// Temporary storage for add playlist description
50+ #[allow(dead_code)]
51+ pub add_playlist_description: String,
52+ /// Whether the edit playlist dialog is visible
53+ #[allow(dead_code)]
54+ pub show_edit_playlist_dialog: bool,
55+ /// Temporary storage for edit playlist name
56+ #[allow(dead_code)]
57+ pub edit_playlist_name: String,
58+ /// Temporary storage for edit playlist description
59+ #[allow(dead_code)]
60+ pub edit_playlist_description: String,
61+ /// Index of the playlist being edited (if any)
62+ #[allow(dead_code)]
63+ pub editing_playlist_index: Option<usize>,
39 /// Whether a synchronization is currently in progress64 /// Whether a synchronization is currently in progress
40 pub sync_in_progress: bool,65 pub sync_in_progress: bool,
41 /// Download progress for each feed (feed_id -> progress 0.0-1.0)66 /// Download progress for each feed (feed_id -> progress 0.0-1.0)
@@ -53,6 +78,24 @@ pub struct AppState {
53 pub config_path: PathBuf,78 pub config_path: PathBuf,
54 /// Manager for displaying notifications to the user79 /// Manager for displaying notifications to the user
55 pub notification_manager: NotificationManager,80 pub notification_manager: NotificationManager,
81+ /// List of playlists for grouping feeds
82+ pub playlists: Vec<Playlist>,
83+ // Filter state
84+ /// Filter: date from (inclusive)
85+ #[allow(dead_code)]
86+ pub filter_date_from: Option<DateTime<Utc>>,
87+ /// Filter: date to (inclusive)
88+ #[allow(dead_code)]
89+ pub filter_date_to: Option<DateTime<Utc>>,
90+ /// Filter: episode number
91+ #[allow(dead_code)]
92+ pub filter_episode_number: String,
93+ /// Whether to show the date picker for 'from' date
94+ #[allow(dead_code)]
95+ pub show_date_picker_from: bool,
96+ /// Whether to show the date picker for 'to' date
97+ #[allow(dead_code)]
98+ pub show_date_picker_to: bool,
56 }99 }
57 100
58 impl AppState {101 impl AppState {
@@ -70,7 +113,15 @@ impl AppState {
70 edit_feed_url: String::new(),113 edit_feed_url: String::new(),
71 edit_feed_download_path: String::new(),114 edit_feed_download_path: String::new(),
72 edit_feed_enabled: true,115 edit_feed_enabled: true,
116+ edit_feed_tags: String::new(),
73 editing_feed_index: None,117 editing_feed_index: None,
118+ show_add_playlist_dialog: false,
119+ add_playlist_name: String::new(),
120+ add_playlist_description: String::new(),
121+ show_edit_playlist_dialog: false,
122+ edit_playlist_name: String::new(),
123+ edit_playlist_description: String::new(),
124+ editing_playlist_index: None,
74 sync_in_progress: false,125 sync_in_progress: false,
75 download_progress: HashMap::new(),126 download_progress: HashMap::new(),
76 error_message: None,127 error_message: None,
@@ -78,6 +129,12 @@ impl AppState {
78 show_settings: false,129 show_settings: false,
79 config_path: config_path.into(),130 config_path: config_path.into(),
80 notification_manager: NotificationManager::new(),131 notification_manager: NotificationManager::new(),
132+ playlists: Vec::new(),
133+ filter_date_from: None,
134+ filter_date_to: None,
135+ filter_episode_number: String::new(),
136+ show_date_picker_from: false,
137+ show_date_picker_to: false,
81 }138 }
82 }139 }
83 140
@@ -246,6 +303,7 @@ impl AppState {
246 self.edit_feed_url = feed.url.clone();303 self.edit_feed_url = feed.url.clone();
247 self.edit_feed_download_path = feed.download_path.to_string_lossy().to_string();304 self.edit_feed_download_path = feed.download_path.to_string_lossy().to_string();
248 self.edit_feed_enabled = feed.enabled;305 self.edit_feed_enabled = feed.enabled;
306+ self.edit_feed_tags = feed.tags_string();
249 }307 }
250 }308 }
251 309
@@ -255,8 +313,114 @@ impl AppState {
255 self.edit_feed_url.clear();313 self.edit_feed_url.clear();
256 self.edit_feed_download_path.clear();314 self.edit_feed_download_path.clear();
257 self.edit_feed_enabled = true;315 self.edit_feed_enabled = true;
316+ self.edit_feed_tags.clear();
258 self.editing_feed_index = None;317 self.editing_feed_index = None;
259 }318 }
319+
320+ // Playlist methods
321+
322+ /// Adds a new playlist
323+ #[allow(dead_code)]
324+ pub fn add_playlist(&mut self, playlist: Playlist) {
325+ self.playlists.push(playlist);
326+ self.notification_manager.success("Playlist ajoutée");
327+ }
328+
329+ /// Removes a playlist by index
330+ #[allow(dead_code)]
331+ pub fn remove_playlist(&mut self, index: usize) -> Option<Playlist> {
332+ if index < self.playlists.len() {
333+ let playlist = self.playlists.remove(index);
334+ self.notification_manager.success("Playlist supprimée");
335+ Some(playlist)
336+ } else {
337+ None
338+ }
339+ }
340+
341+ /// Gets a playlist by index
342+ #[allow(dead_code)]
343+ pub fn get_playlist(&self, index: usize) -> Option<&Playlist> {
344+ self.playlists.get(index)
345+ }
346+
347+ /// Gets a mutable playlist by index
348+ #[allow(dead_code)]
349+ pub fn get_playlist_mut(&mut self, index: usize) -> Option<&mut Playlist> {
350+ self.playlists.get_mut(index)
351+ }
352+
353+ /// Returns the number of playlists
354+ #[allow(dead_code)]
355+ pub fn playlist_count(&self) -> usize {
356+ self.playlists.len()
357+ }
358+
359+ /// Returns true if there are any playlists
360+ #[allow(dead_code)]
361+ pub fn has_playlists(&self) -> bool {
362+ !self.playlists.is_empty()
363+ }
364+
365+ /// Adds a feed to a playlist by index
366+ #[allow(dead_code)]
367+ pub fn add_feed_to_playlist(&mut self, playlist_index: usize, feed_id: &str) {
368+ if let Some(playlist) = self.playlists.get_mut(playlist_index) {
369+ playlist.add_feed(feed_id);
370+ }
371+ }
372+
373+ /// Removes a feed from a playlist by index
374+ #[allow(dead_code)]
375+ pub fn remove_feed_from_playlist(&mut self, playlist_index: usize, feed_id: &str) {
376+ if let Some(playlist) = self.playlists.get_mut(playlist_index) {
377+ playlist.remove_feed(feed_id);
378+ }
379+ }
380+
381+ /// Gets all playlists that contain a specific feed
382+ #[allow(dead_code)]
383+ pub fn get_playlists_containing_feed(&self, feed_id: &str) -> Vec<usize> {
384+ self.playlists
385+ .iter()
386+ .enumerate()
387+ .filter(|(_, p)| p.contains_feed(feed_id))
388+ .map(|(i, _)| i)
389+ .collect()
390+ }
391+
392+ /// Clears the add playlist form fields
393+ #[allow(dead_code)]
394+ pub fn clear_add_playlist_form(&mut self) {
395+ self.add_playlist_name.clear();
396+ self.add_playlist_description.clear();
397+ }
398+
399+ /// Clears the edit playlist form fields
400+ #[allow(dead_code)]
401+ pub fn clear_edit_playlist_form(&mut self) {
402+ self.edit_playlist_name.clear();
403+ self.edit_playlist_description.clear();
404+ self.editing_playlist_index = None;
405+ }
406+
407+ /// Loads playlist data into edit form fields
408+ #[allow(dead_code)]
409+ pub fn load_edit_playlist_form(&mut self, index: usize) {
410+ if let Some(playlist) = self.playlists.get(index) {
411+ self.edit_playlist_name = playlist.name.clone();
412+ self.edit_playlist_description = playlist.description.clone().unwrap_or_default();
413+ }
414+ }
415+
416+ /// Clears all filter criteria
417+ #[allow(dead_code)]
418+ pub fn clear_filters(&mut self) {
419+ self.filter_date_from = None;
420+ self.filter_date_to = None;
421+ self.filter_episode_number.clear();
422+ self.notification_manager.info("Filtres effacés");
423+ }
260 }424 }
261 425
262 #[cfg(test)]426 #[cfg(test)]
modified src/config/feed.rs +31 -0
@@ -24,6 +24,8 @@ pub struct Feed {
2424 pub last_episode_date: Option<DateTime<Utc>>,
2525 /// Whether the feed is enabled for automatic synchronization
2626 pub enabled: bool,
27+ /// Tags for categorizing the feed (e.g., "Tech", "News", "French")
28+ pub tags: Vec<String>,
2729 }
2830
2931 impl Default for Feed {
@@ -36,6 +38,7 @@ impl Default for Feed {
3638 last_checked: None,
3739 last_episode_date: None,
3840 enabled: true,
41+ tags: Vec::new(),
3942 }
4043 }
4144 }
@@ -51,6 +54,7 @@ impl Feed {
5154 last_checked: None,
5255 last_episode_date: None,
5356 enabled: true,
57+ tags: Vec::new(),
5458 }
5559 }
5660
@@ -92,6 +96,33 @@ impl Feed {
9296 pub fn is_valid(&self) -> bool {
9397 self.validate().is_ok()
9498 }
99+
100+ /// Adds a tag to the feed if not already present
101+ #[allow(dead_code)]
102+ pub fn add_tag(&mut self, tag: impl Into<String>) {
103+ let tag = tag.into();
104+ if !self.tags.contains(&tag) {
105+ self.tags.push(tag);
106+ }
107+ }
108+
109+ /// Removes a tag from the feed
110+ #[allow(dead_code)]
111+ pub fn remove_tag(&mut self, tag: &str) {
112+ self.tags.retain(|t| t != tag);
113+ }
114+
115+ /// Returns true if the feed has the given tag
116+ #[allow(dead_code)]
117+ pub fn has_tag(&self, tag: &str) -> bool {
118+ self.tags.contains(&tag.to_string())
119+ }
120+
121+ /// Returns the tags as a comma-separated string
122+ #[allow(dead_code)]
123+ pub fn tags_string(&self) -> String {
124+ self.tags.join(", ")
125+ }
95126 }
96127
97128 #[cfg(test)]
@@ -24,6 +24,8 @@ pub struct Feed {
24 pub last_episode_date: Option<DateTime<Utc>>,24 pub last_episode_date: Option<DateTime<Utc>>,
25 /// Whether the feed is enabled for automatic synchronization25 /// Whether the feed is enabled for automatic synchronization
26 pub enabled: bool,26 pub enabled: bool,
27+ /// Tags for categorizing the feed (e.g., "Tech", "News", "French")
28+ pub tags: Vec<String>,
27 }29 }
28 30
29 impl Default for Feed {31 impl Default for Feed {
@@ -36,6 +38,7 @@ impl Default for Feed {
36 last_checked: None,38 last_checked: None,
37 last_episode_date: None,39 last_episode_date: None,
38 enabled: true,40 enabled: true,
41+ tags: Vec::new(),
39 }42 }
40 }43 }
41 }44 }
@@ -51,6 +54,7 @@ impl Feed {
51 last_checked: None,54 last_checked: None,
52 last_episode_date: None,55 last_episode_date: None,
53 enabled: true,56 enabled: true,
57+ tags: Vec::new(),
54 }58 }
55 }59 }
56 60
@@ -92,6 +96,33 @@ impl Feed {
92 pub fn is_valid(&self) -> bool {96 pub fn is_valid(&self) -> bool {
93 self.validate().is_ok()97 self.validate().is_ok()
94 }98 }
99+
100+ /// Adds a tag to the feed if not already present
101+ #[allow(dead_code)]
102+ pub fn add_tag(&mut self, tag: impl Into<String>) {
103+ let tag = tag.into();
104+ if !self.tags.contains(&tag) {
105+ self.tags.push(tag);
106+ }
107+ }
108+
109+ /// Removes a tag from the feed
110+ #[allow(dead_code)]
111+ pub fn remove_tag(&mut self, tag: &str) {
112+ self.tags.retain(|t| t != tag);
113+ }
114+
115+ /// Returns true if the feed has the given tag
116+ #[allow(dead_code)]
117+ pub fn has_tag(&self, tag: &str) -> bool {
118+ self.tags.contains(&tag.to_string())
119+ }
120+
121+ /// Returns the tags as a comma-separated string
122+ #[allow(dead_code)]
123+ pub fn tags_string(&self) -> String {
124+ self.tags.join(", ")
125+ }
95 }126 }
96 127
97 #[cfg(test)]128 #[cfg(test)]
modified src/config/mod.rs +1 -0
@@ -5,6 +5,7 @@
55 pub mod app_state;
66 pub mod episode;
77 pub mod feed;
8+pub mod playlist;
89 pub mod storage;
910
1011 // Re-export main types for convenience
@@ -5,6 +5,7 @@
5 pub mod app_state;5 pub mod app_state;
6 pub mod episode;6 pub mod episode;
7 pub mod feed;7 pub mod feed;
8+pub mod playlist;
8 pub mod storage;9 pub mod storage;
9 10
10 // Re-export main types for convenience11 // Re-export main types for convenience
added src/config/playlist.rs +154 -0
new file mode 100644
@@ -0,0 +1,154 @@
1+//! Playlist module - Podcast feed playlist model
2+//!
3+//! This module defines the Playlist structure for grouping RSS feeds.
4+
5+use serde::{Deserialize, Serialize};
6+use uuid::Uuid;
7+
8+/// Represents a playlist that groups multiple RSS feeds
9+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10+pub struct Playlist {
11+ /// Unique identifier for the playlist
12+ pub id: String,
13+ /// Display name of the playlist
14+ pub name: String,
15+ /// List of feed IDs that belong to this playlist
16+ pub feed_ids: Vec<String>,
17+ /// Description of the playlist (optional)
18+ pub description: Option<String>,
19+}
20+
21+impl Default for Playlist {
22+ fn default() -> Self {
23+ Self {
24+ id: Uuid::new_v4().to_string(),
25+ name: String::new(),
26+ feed_ids: Vec::new(),
27+ description: None,
28+ }
29+ }
30+}
31+
32+impl Playlist {
33+ /// Creates a new empty playlist with the given name
34+ pub fn new(name: impl Into<String>) -> Self {
35+ Self {
36+ id: Uuid::new_v4().to_string(),
37+ name: name.into(),
38+ feed_ids: Vec::new(),
39+ description: None,
40+ }
41+ }
42+
43+ /// Creates a new playlist with the given name and feed IDs
44+ pub fn with_feeds(name: impl Into<String>, feed_ids: Vec<String>) -> Self {
45+ Self {
46+ id: Uuid::new_v4().to_string(),
47+ name: name.into(),
48+ feed_ids,
49+ description: None,
50+ }
51+ }
52+
53+ /// Adds a feed ID to the playlist if not already present
54+ pub fn add_feed(&mut self, feed_id: impl Into<String>) {
55+ let feed_id = feed_id.into();
56+ if !self.feed_ids.contains(&feed_id) {
57+ self.feed_ids.push(feed_id);
58+ }
59+ }
60+
61+ /// Removes a feed ID from the playlist
62+ pub fn remove_feed(&mut self, feed_id: &str) {
63+ self.feed_ids.retain(|id| id != feed_id);
64+ }
65+
66+ /// Returns true if the playlist contains the given feed ID
67+ pub fn contains_feed(&self, feed_id: &str) -> bool {
68+ self.feed_ids.contains(&feed_id.to_string())
69+ }
70+
71+ /// Returns the number of feeds in the playlist
72+ pub fn feed_count(&self) -> usize {
73+ self.feed_ids.len()
74+ }
75+
76+ /// Sets the description of the playlist
77+ pub fn set_description(&mut self, description: impl Into<String>) {
78+ self.description = Some(description.into());
79+ }
80+
81+ /// Clears the description of the playlist
82+ pub fn clear_description(&mut self) {
83+ self.description = None;
84+ }
85+}
86+
87+#[cfg(test)]
88+mod tests {
89+ use super::*;
90+
91+ #[test]
92+ fn test_playlist_default() {
93+ let playlist = Playlist::default();
94+ assert!(!playlist.id.is_empty());
95+ assert!(playlist.name.is_empty());
96+ assert!(playlist.feed_ids.is_empty());
97+ assert!(playlist.description.is_none());
98+ }
99+
100+ #[test]
101+ fn test_playlist_new() {
102+ let playlist = Playlist::new("My Playlist");
103+ assert_eq!(playlist.name, "My Playlist");
104+ assert!(playlist.feed_ids.is_empty());
105+ }
106+
107+ #[test]
108+ fn test_playlist_with_feeds() {
109+ let feed_ids = vec!["feed-1".to_string(), "feed-2".to_string()];
110+ let playlist = Playlist::with_feeds("Test Playlist", feed_ids.clone());
111+ assert_eq!(playlist.name, "Test Playlist");
112+ assert_eq!(playlist.feed_ids, feed_ids);
113+ }
114+
115+ #[test]
116+ fn test_add_feed() {
117+ let mut playlist = Playlist::new("Test");
118+ playlist.add_feed("feed-1");
119+ playlist.add_feed("feed-2");
120+ playlist.add_feed("feed-1"); // Duplicate - should not be added
121+
122+ assert_eq!(playlist.feed_ids.len(), 2);
123+ assert!(playlist.contains_feed("feed-1"));
124+ assert!(playlist.contains_feed("feed-2"));
125+ }
126+
127+ #[test]
128+ fn test_remove_feed() {
129+ let mut playlist = Playlist::with_feeds("Test", vec!["feed-1".to_string(), "feed-2".to_string()]);
130+ playlist.remove_feed("feed-1");
131+
132+ assert_eq!(playlist.feed_ids.len(), 1);
133+ assert!(!playlist.contains_feed("feed-1"));
134+ assert!(playlist.contains_feed("feed-2"));
135+ }
136+
137+ #[test]
138+ fn test_feed_count() {
139+ let playlist = Playlist::with_feeds("Test", vec!["feed-1".to_string(), "feed-2".to_string(), "feed-3".to_string()]);
140+ assert_eq!(playlist.feed_count(), 3);
141+ }
142+
143+ #[test]
144+ fn test_description() {
145+ let mut playlist = Playlist::new("Test");
146+ assert!(playlist.description.is_none());
147+
148+ playlist.set_description("A test playlist");
149+ assert_eq!(playlist.description, Some("A test playlist".to_string()));
150+
151+ playlist.clear_description();
152+ assert!(playlist.description.is_none());
153+ }
154+}
new file mode 100644
@@ -0,0 +1,154 @@
1+//! Playlist module - Podcast feed playlist model
2+//!
3+//! This module defines the Playlist structure for grouping RSS feeds.
4+
5+use serde::{Deserialize, Serialize};
6+use uuid::Uuid;
7+
8+/// Represents a playlist that groups multiple RSS feeds
9+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10+pub struct Playlist {
11+ /// Unique identifier for the playlist
12+ pub id: String,
13+ /// Display name of the playlist
14+ pub name: String,
15+ /// List of feed IDs that belong to this playlist
16+ pub feed_ids: Vec<String>,
17+ /// Description of the playlist (optional)
18+ pub description: Option<String>,
19+}
20+
21+impl Default for Playlist {
22+ fn default() -> Self {
23+ Self {
24+ id: Uuid::new_v4().to_string(),
25+ name: String::new(),
26+ feed_ids: Vec::new(),
27+ description: None,
28+ }
29+ }
30+}
31+
32+impl Playlist {
33+ /// Creates a new empty playlist with the given name
34+ pub fn new(name: impl Into<String>) -> Self {
35+ Self {
36+ id: Uuid::new_v4().to_string(),
37+ name: name.into(),
38+ feed_ids: Vec::new(),
39+ description: None,
40+ }
41+ }
42+
43+ /// Creates a new playlist with the given name and feed IDs
44+ pub fn with_feeds(name: impl Into<String>, feed_ids: Vec<String>) -> Self {
45+ Self {
46+ id: Uuid::new_v4().to_string(),
47+ name: name.into(),
48+ feed_ids,
49+ description: None,
50+ }
51+ }
52+
53+ /// Adds a feed ID to the playlist if not already present
54+ pub fn add_feed(&mut self, feed_id: impl Into<String>) {
55+ let feed_id = feed_id.into();
56+ if !self.feed_ids.contains(&feed_id) {
57+ self.feed_ids.push(feed_id);
58+ }
59+ }
60+
61+ /// Removes a feed ID from the playlist
62+ pub fn remove_feed(&mut self, feed_id: &str) {
63+ self.feed_ids.retain(|id| id != feed_id);
64+ }
65+
66+ /// Returns true if the playlist contains the given feed ID
67+ pub fn contains_feed(&self, feed_id: &str) -> bool {
68+ self.feed_ids.contains(&feed_id.to_string())
69+ }
70+
71+ /// Returns the number of feeds in the playlist
72+ pub fn feed_count(&self) -> usize {
73+ self.feed_ids.len()
74+ }
75+
76+ /// Sets the description of the playlist
77+ pub fn set_description(&mut self, description: impl Into<String>) {
78+ self.description = Some(description.into());
79+ }
80+
81+ /// Clears the description of the playlist
82+ pub fn clear_description(&mut self) {
83+ self.description = None;
84+ }
85+}
86+
87+#[cfg(test)]
88+mod tests {
89+ use super::*;
90+
91+ #[test]
92+ fn test_playlist_default() {
93+ let playlist = Playlist::default();
94+ assert!(!playlist.id.is_empty());
95+ assert!(playlist.name.is_empty());
96+ assert!(playlist.feed_ids.is_empty());
97+ assert!(playlist.description.is_none());
98+ }
99+
100+ #[test]
101+ fn test_playlist_new() {
102+ let playlist = Playlist::new("My Playlist");
103+ assert_eq!(playlist.name, "My Playlist");
104+ assert!(playlist.feed_ids.is_empty());
105+ }
106+
107+ #[test]
108+ fn test_playlist_with_feeds() {
109+ let feed_ids = vec!["feed-1".to_string(), "feed-2".to_string()];
110+ let playlist = Playlist::with_feeds("Test Playlist", feed_ids.clone());
111+ assert_eq!(playlist.name, "Test Playlist");
112+ assert_eq!(playlist.feed_ids, feed_ids);
113+ }
114+
115+ #[test]
116+ fn test_add_feed() {
117+ let mut playlist = Playlist::new("Test");
118+ playlist.add_feed("feed-1");
119+ playlist.add_feed("feed-2");
120+ playlist.add_feed("feed-1"); // Duplicate - should not be added
121+
122+ assert_eq!(playlist.feed_ids.len(), 2);
123+ assert!(playlist.contains_feed("feed-1"));
124+ assert!(playlist.contains_feed("feed-2"));
125+ }
126+
127+ #[test]
128+ fn test_remove_feed() {
129+ let mut playlist = Playlist::with_feeds("Test", vec!["feed-1".to_string(), "feed-2".to_string()]);
130+ playlist.remove_feed("feed-1");
131+
132+ assert_eq!(playlist.feed_ids.len(), 1);
133+ assert!(!playlist.contains_feed("feed-1"));
134+ assert!(playlist.contains_feed("feed-2"));
135+ }
136+
137+ #[test]
138+ fn test_feed_count() {
139+ let playlist = Playlist::with_feeds("Test", vec!["feed-1".to_string(), "feed-2".to_string(), "feed-3".to_string()]);
140+ assert_eq!(playlist.feed_count(), 3);
141+ }
142+
143+ #[test]
144+ fn test_description() {
145+ let mut playlist = Playlist::new("Test");
146+ assert!(playlist.description.is_none());
147+
148+ playlist.set_description("A test playlist");
149+ assert_eq!(playlist.description, Some("A test playlist".to_string()));
150+
151+ playlist.clear_description();
152+ assert!(playlist.description.is_none());
153+ }
154+}
modified src/config/storage.rs +68 -0
@@ -3,6 +3,7 @@
33 //! This module handles saving and loading feed configurations to/from JSON files.
44
55 use crate::config::feed::Feed;
6+use crate::config::playlist::Playlist;
67 use log::{info, warn};
78 #[allow(unused_imports)]
89 use serde::{Deserialize, Serialize};
@@ -132,6 +133,73 @@ pub fn load_feeds_default() -> Result<Vec<Feed>, StorageError> {
132133 load_feeds(&get_config_path())
133134 }
134135
136+/// Gets the default playlists configuration path
137+pub fn get_playlists_path() -> std::path::PathBuf {
138+ let mut path = get_config_path();
139+ let filename = path.file_name().unwrap_or_default().to_string_lossy().to_string();
140+ path.set_file_name(format!("{}_playlists", filename));
141+ path
142+}
143+
144+/// Saves the list of playlists to a JSON file
145+pub fn save_playlists(playlists: &[Playlist], path: &Path) -> Result<(), StorageError> {
146+ info!("Saving {} playlists to {}", playlists.len(), path.display());
147+
148+ // Create parent directories if they don't exist
149+ if let Some(parent) = path.parent() {
150+ if !parent.exists() {
151+ info!("Creating directory: {}", parent.display());
152+ fs::create_dir_all(parent)
153+ .map_err(|e| StorageError::InvalidDirectory(format!("{}: {}", parent.display(), e)))?;
154+ }
155+ }
156+
157+ // Serialize playlists to JSON
158+ let json = serde_json::to_string_pretty(playlists)
159+ .map_err(|e| StorageError::SerializeError(e.to_string()))?;
160+
161+ // Write to file
162+ fs::write(path, json)
163+ .map_err(|e| StorageError::WriteError(format!("{}: {}", path.display(), e)))?;
164+
165+ info!("Successfully saved {} playlists", playlists.len());
166+ Ok(())
167+}
168+
169+/// Loads the list of playlists from a JSON file
170+pub fn load_playlists(path: &Path) -> Result<Vec<Playlist>, StorageError> {
171+ info!("Loading playlists from {}", path.display());
172+
173+ // Check if file exists
174+ if !path.exists() {
175+ warn!("Playlists file not found: {}", path.display());
176+ return Ok(Vec::new()); // Return empty list if file doesn't exist
177+ }
178+
179+ // Read file content
180+ let content = fs::read_to_string(path)
181+ .map_err(|e| StorageError::ReadError(format!("{}: {}", path.display(), e)))?;
182+
183+ // Parse JSON
184+ let playlists: Vec<Playlist> = serde_json::from_str(&content)
185+ .map_err(|e| StorageError::ParseError(format!("{}: {}", path.display(), e)))?;
186+
187+ info!("Successfully loaded {} playlists", playlists.len());
188+ Ok(playlists)
189+}
190+
191+/// Saves playlists to the default configuration path
192+#[allow(dead_code)]
193+pub fn save_playlists_default(playlists: &[Playlist]) -> Result<(), StorageError> {
194+ save_playlists(playlists, &get_playlists_path())
195+}
196+
197+/// Loads playlists from the default configuration path
198+#[allow(dead_code)]
199+pub fn load_playlists_default() -> Result<Vec<Playlist>, StorageError> {
200+ load_playlists(&get_playlists_path())
201+}
202+
135203 #[cfg(test)]
136204 mod tests {
137205 use super::*;
@@ -3,6 +3,7 @@
3 //! This module handles saving and loading feed configurations to/from JSON files.3 //! This module handles saving and loading feed configurations to/from JSON files.
4 4
5 use crate::config::feed::Feed;5 use crate::config::feed::Feed;
6+use crate::config::playlist::Playlist;
6 use log::{info, warn};7 use log::{info, warn};
7 #[allow(unused_imports)]8 #[allow(unused_imports)]
8 use serde::{Deserialize, Serialize};9 use serde::{Deserialize, Serialize};
@@ -132,6 +133,73 @@ pub fn load_feeds_default() -> Result<Vec<Feed>, StorageError> {
132 load_feeds(&get_config_path())133 load_feeds(&get_config_path())
133 }134 }
134 135
136+/// Gets the default playlists configuration path
137+pub fn get_playlists_path() -> std::path::PathBuf {
138+ let mut path = get_config_path();
139+ let filename = path.file_name().unwrap_or_default().to_string_lossy().to_string();
140+ path.set_file_name(format!("{}_playlists", filename));
141+ path
142+}
143+
144+/// Saves the list of playlists to a JSON file
145+pub fn save_playlists(playlists: &[Playlist], path: &Path) -> Result<(), StorageError> {
146+ info!("Saving {} playlists to {}", playlists.len(), path.display());
147+
148+ // Create parent directories if they don't exist
149+ if let Some(parent) = path.parent() {
150+ if !parent.exists() {
151+ info!("Creating directory: {}", parent.display());
152+ fs::create_dir_all(parent)
153+ .map_err(|e| StorageError::InvalidDirectory(format!("{}: {}", parent.display(), e)))?;
154+ }
155+ }
156+
157+ // Serialize playlists to JSON
158+ let json = serde_json::to_string_pretty(playlists)
159+ .map_err(|e| StorageError::SerializeError(e.to_string()))?;
160+
161+ // Write to file
162+ fs::write(path, json)
163+ .map_err(|e| StorageError::WriteError(format!("{}: {}", path.display(), e)))?;
164+
165+ info!("Successfully saved {} playlists", playlists.len());
166+ Ok(())
167+}
168+
169+/// Loads the list of playlists from a JSON file
170+pub fn load_playlists(path: &Path) -> Result<Vec<Playlist>, StorageError> {
171+ info!("Loading playlists from {}", path.display());
172+
173+ // Check if file exists
174+ if !path.exists() {
175+ warn!("Playlists file not found: {}", path.display());
176+ return Ok(Vec::new()); // Return empty list if file doesn't exist
177+ }
178+
179+ // Read file content
180+ let content = fs::read_to_string(path)
181+ .map_err(|e| StorageError::ReadError(format!("{}: {}", path.display(), e)))?;
182+
183+ // Parse JSON
184+ let playlists: Vec<Playlist> = serde_json::from_str(&content)
185+ .map_err(|e| StorageError::ParseError(format!("{}: {}", path.display(), e)))?;
186+
187+ info!("Successfully loaded {} playlists", playlists.len());
188+ Ok(playlists)
189+}
190+
191+/// Saves playlists to the default configuration path
192+#[allow(dead_code)]
193+pub fn save_playlists_default(playlists: &[Playlist]) -> Result<(), StorageError> {
194+ save_playlists(playlists, &get_playlists_path())
195+}
196+
197+/// Loads playlists from the default configuration path
198+#[allow(dead_code)]
199+pub fn load_playlists_default() -> Result<Vec<Playlist>, StorageError> {
200+ load_playlists(&get_playlists_path())
201+}
202+
135 #[cfg(test)]203 #[cfg(test)]
136 mod tests {204 mod tests {
137 use super::*;205 use super::*;
modified src/ui/feed_edit_dialog.rs +12 -1
@@ -41,6 +41,10 @@ pub fn draw_edit_feed_dialog(state: &mut AppState, ui: &mut Ui) {
4141
4242 ui.checkbox(&mut state.edit_feed_enabled, "Activé");
4343
44+ ui.label("Tags (séparés par des virgules)");
45+ ui.text_edit_singleline(&mut state.edit_feed_tags);
46+ ui.label("Exemple: Tech,Podcast,Français");
47+
4448 ui.separator();
4549
4650 ui.horizontal(|ui| {
@@ -51,9 +55,16 @@ pub fn draw_edit_feed_dialog(state: &mut AppState, ui: &mut Ui) {
5155 state.feeds[editing_index].download_path = PathBuf::from(state.edit_feed_download_path.clone());
5256 state.feeds[editing_index].enabled = state.edit_feed_enabled;
5357
58+ // Parse tags from comma-separated string
59+ state.feeds[editing_index].tags = state.edit_feed_tags
60+ .split(',')
61+ .map(|s| s.trim().to_string())
62+ .filter(|s| !s.is_empty())
63+ .collect();
64+
5465 super::super::config::storage::save_feeds_default(&state.feeds).ok();
5566 log::info!("Feed updated successfully");
56- state.set_info("Flux modifié".to_string());
67+ state.notification_manager.success("Flux modifié");
5768 state.clear_edit_feed_form();
5869 state.show_edit_feed_dialog = false;
5970 }
@@ -41,6 +41,10 @@ pub fn draw_edit_feed_dialog(state: &mut AppState, ui: &mut Ui) {
41 41
42 ui.checkbox(&mut state.edit_feed_enabled, "Activé");42 ui.checkbox(&mut state.edit_feed_enabled, "Activé");
43 43
44+ ui.label("Tags (séparés par des virgules)");
45+ ui.text_edit_singleline(&mut state.edit_feed_tags);
46+ ui.label("Exemple: Tech,Podcast,Français");
47+
44 ui.separator();48 ui.separator();
45 49
46 ui.horizontal(|ui| {50 ui.horizontal(|ui| {
@@ -51,9 +55,16 @@ pub fn draw_edit_feed_dialog(state: &mut AppState, ui: &mut Ui) {
51 state.feeds[editing_index].download_path = PathBuf::from(state.edit_feed_download_path.clone());55 state.feeds[editing_index].download_path = PathBuf::from(state.edit_feed_download_path.clone());
52 state.feeds[editing_index].enabled = state.edit_feed_enabled;56 state.feeds[editing_index].enabled = state.edit_feed_enabled;
53 57
58+ // Parse tags from comma-separated string
59+ state.feeds[editing_index].tags = state.edit_feed_tags
60+ .split(',')
61+ .map(|s| s.trim().to_string())
62+ .filter(|s| !s.is_empty())
63+ .collect();
64+
54 super::super::config::storage::save_feeds_default(&state.feeds).ok();65 super::super::config::storage::save_feeds_default(&state.feeds).ok();
55 log::info!("Feed updated successfully");66 log::info!("Feed updated successfully");
56- state.set_info("Flux modifié".to_string());67+ state.notification_manager.success("Flux modifié");
57 state.clear_edit_feed_form();68 state.clear_edit_feed_form();
58 state.show_edit_feed_dialog = false;69 state.show_edit_feed_dialog = false;
59 }70 }
modified src/ui/feed_list.rs +15 -3
@@ -7,11 +7,17 @@ use egui::{Align, Layout, ScrollArea, Ui};
77
88 /// Draws the feed list
99 pub fn draw_feed_list(state: &mut AppState, ui: &mut Ui) {
10+ // Apply filters
11+ let filtered_indices = crate::ui::filter_panel::filter_feeds(&state.feeds, state);
12+
13+ if filtered_indices.is_empty() {
14+ ui.label("Aucun flux correspondant aux filtres.");
15+ return;
16+ }
17+
1018 ScrollArea::vertical().show(ui, |ui| {
11- let mut index = 0;
12- while index < state.feeds.len() {
19+ for &index in &filtered_indices {
1320 draw_feed_item(state, ui, index);
14- index += 1;
1521 }
1622 });
1723 }
@@ -50,6 +56,12 @@ fn draw_feed_item(state: &mut AppState, ui: &mut Ui, index: usize) {
5056 super::super::config::storage::save_feeds_default(&state.feeds).ok();
5157 }
5258
59+ // Tags
60+ let feed = &state.feeds[index];
61+ if !feed.tags.is_empty() {
62+ ui.label(format!("Tags: {}", feed.tags_string()));
63+ }
64+
5365 // Last checked
5466 if let Some(date) = feed_last_checked {
5567 ui.label(format!("Vérifié: {}", date.format("%Y-%m-%d %H:%M")));
@@ -7,11 +7,17 @@ use egui::{Align, Layout, ScrollArea, Ui};
7 7
8 /// Draws the feed list8 /// Draws the feed list
9 pub fn draw_feed_list(state: &mut AppState, ui: &mut Ui) {9 pub fn draw_feed_list(state: &mut AppState, ui: &mut Ui) {
10+ // Apply filters
11+ let filtered_indices = crate::ui::filter_panel::filter_feeds(&state.feeds, state);
12+
13+ if filtered_indices.is_empty() {
14+ ui.label("Aucun flux correspondant aux filtres.");
15+ return;
16+ }
17+
10 ScrollArea::vertical().show(ui, |ui| {18 ScrollArea::vertical().show(ui, |ui| {
11- let mut index = 0;19+ for &index in &filtered_indices {
12- while index < state.feeds.len() {
13 draw_feed_item(state, ui, index);20 draw_feed_item(state, ui, index);
14- index += 1;
15 }21 }
16 });22 });
17 }23 }
@@ -50,6 +56,12 @@ fn draw_feed_item(state: &mut AppState, ui: &mut Ui, index: usize) {
50 super::super::config::storage::save_feeds_default(&state.feeds).ok();56 super::super::config::storage::save_feeds_default(&state.feeds).ok();
51 }57 }
52 58
59+ // Tags
60+ let feed = &state.feeds[index];
61+ if !feed.tags.is_empty() {
62+ ui.label(format!("Tags: {}", feed.tags_string()));
63+ }
64+
53 // Last checked65 // Last checked
54 if let Some(date) = feed_last_checked {66 if let Some(date) = feed_last_checked {
55 ui.label(format!("Vérifié: {}", date.format("%Y-%m-%d %H:%M")));67 ui.label(format!("Vérifié: {}", date.format("%Y-%m-%d %H:%M")));
added src/ui/filter_panel.rs +239 -0
new file mode 100644
@@ -0,0 +1,239 @@
1+//! Filter Panel UI component
2+//!
3+//! This module contains the UI for filtering feeds and episodes.
4+
5+use crate::config::AppState;
6+use chrono::{DateTime, Utc};
7+use egui::Ui;
8+
9+/// Draws the filter panel for feeds
10+pub fn draw_filter_panel(state: &mut AppState, ui: &mut Ui) {
11+ ui.horizontal(|ui| {
12+ ui.label("Filtres:");
13+
14+ // Date filter
15+ ui.label("Date de publication:");
16+
17+ // Date from filter
18+ if let Some(date_from) = state.filter_date_from {
19+ if ui.button(format!("Depuis: {}", date_from.format("%Y-%m-%d"))).clicked() {
20+ state.show_date_picker_from = true;
21+ }
22+ } else {
23+ if ui.button("Depuis...").clicked() {
24+ state.show_date_picker_from = true;
25+ }
26+ }
27+
28+ ui.add_space(5.0);
29+
30+ // Date to filter
31+ if let Some(date_to) = state.filter_date_to {
32+ if ui.button(format!("Jusqu'à: {}", date_to.format("%Y-%m-%d"))).clicked() {
33+ state.show_date_picker_to = true;
34+ }
35+ } else {
36+ if ui.button("Jusqu'à...").clicked() {
37+ state.show_date_picker_to = true;
38+ }
39+ }
40+
41+ ui.add_space(10.0);
42+
43+ // Episode number filter
44+ ui.label("Numéro d'épisode:");
45+ ui.add(egui::TextEdit::singleline(&mut state.filter_episode_number)
46+ .hint_text("ex: 10"));
47+
48+ ui.add_space(10.0);
49+
50+ // Clear filters button
51+ if ui.button("Effacer les filtres").clicked() {
52+ state.clear_filters();
53+ }
54+ });
55+
56+ ui.separator();
57+
58+ // Draw date pickers
59+ draw_date_picker_from(state, ui);
60+ draw_date_picker_to(state, ui);
61+}
62+
63+/// Draws the date picker for 'from' date
64+fn draw_date_picker_from(state: &mut AppState, ui: &mut Ui) {
65+ if !state.show_date_picker_from {
66+ return;
67+ }
68+
69+ egui::Window::new("Sélectionner la date de début")
70+ .resizable(false)
71+ .show(ui.ctx(), |ui| {
72+ ui.vertical(|ui| {
73+ ui.label("Date de début (yyyy-mm-dd):");
74+
75+ let mut date_str = state.filter_date_from
76+ .map(|d| d.format("%Y-%m-%d").to_string())
77+ .unwrap_or_default();
78+
79+ if ui.text_edit_singleline(&mut date_str).changed() {
80+ if let Ok(date) = chrono::NaiveDate::parse_from_str(&date_str, "%Y-%m-%d") {
81+ state.filter_date_from = Some(DateTime::<Utc>::from_naive_utc_and_offset(date.and_hms_opt(0, 0, 0).unwrap(), Utc));
82+ state.show_date_picker_from = false;
83+ }
84+ }
85+
86+ ui.separator();
87+
88+ ui.horizontal(|ui| {
89+ // Quick date buttons
90+ if ui.button("Aujourd'hui").clicked() {
91+ state.filter_date_from = Some(Utc::now());
92+ state.show_date_picker_from = false;
93+ }
94+
95+ if ui.button("7 derniers jours").clicked() {
96+ state.filter_date_from = Some(Utc::now() - chrono::Duration::days(7));
97+ state.show_date_picker_from = false;
98+ }
99+
100+ if ui.button("30 derniers jours").clicked() {
101+ state.filter_date_from = Some(Utc::now() - chrono::Duration::days(30));
102+ state.show_date_picker_from = false;
103+ }
104+
105+ if ui.button("Annuler").clicked() {
106+ state.show_date_picker_from = false;
107+ }
108+ });
109+ });
110+ });
111+}
112+
113+/// Draws the date picker for 'to' date
114+fn draw_date_picker_to(state: &mut AppState, ui: &mut Ui) {
115+ if !state.show_date_picker_to {
116+ return;
117+ }
118+
119+ egui::Window::new("Sélectionner la date de fin")
120+ .resizable(false)
121+ .show(ui.ctx(), |ui| {
122+ ui.vertical(|ui| {
123+ ui.label("Date de fin (yyyy-mm-dd):");
124+
125+ let mut date_str = state.filter_date_to
126+ .map(|d| d.format("%Y-%m-%d").to_string())
127+ .unwrap_or_default();
128+
129+ if ui.text_edit_singleline(&mut date_str).changed() {
130+ if let Ok(date) = chrono::NaiveDate::parse_from_str(&date_str, "%Y-%m-%d") {
131+ state.filter_date_to = Some(DateTime::<Utc>::from_naive_utc_and_offset(date.and_hms_opt(23, 59, 59).unwrap(), Utc));
132+ state.show_date_picker_to = false;
133+ }
134+ }
135+
136+ ui.separator();
137+
138+ ui.horizontal(|ui| {
139+ // Quick date buttons
140+ if ui.button("Aujourd'hui").clicked() {
141+ state.filter_date_to = Some(Utc::now());
142+ state.show_date_picker_to = false;
143+ }
144+
145+ if ui.button("7 derniers jours").clicked() {
146+ state.filter_date_to = Some(Utc::now());
147+ state.filter_date_from = Some(Utc::now() - chrono::Duration::days(7));
148+ state.show_date_picker_to = false;
149+ }
150+
151+ if ui.button("Annuler").clicked() {
152+ state.show_date_picker_to = false;
153+ }
154+ });
155+ });
156+ });
157+}
158+
159+/// Filters feeds based on current filter criteria
160+pub fn filter_feeds(feeds: &[crate::config::feed::Feed], state: &AppState) -> Vec<usize> {
161+ feeds.iter()
162+ .enumerate()
163+ .filter(|(_, feed)| {
164+ // Check date filter
165+ if let Some(date_from) = state.filter_date_from {
166+ if let Some(last_episode) = feed.last_episode_date {
167+ if last_episode < date_from {
168+ return false;
169+ }
170+ } else {
171+ // No episodes, don't show if filtering by date
172+ return false;
173+ }
174+ }
175+
176+ if let Some(date_to) = state.filter_date_to {
177+ if let Some(last_episode) = feed.last_episode_date {
178+ if last_episode > date_to {
179+ return false;
180+ }
181+ } else {
182+ // No episodes, don't show if filtering by date
183+ return false;
184+ }
185+ }
186+
187+ true
188+ })
189+ .map(|(i, _)| i)
190+ .collect()
191+}
192+
193+/// Filters episodes based on current filter criteria
194+#[allow(dead_code)]
195+pub fn filter_episodes(episodes: &[crate::config::episode::Episode], state: &AppState) -> Vec<usize> {
196+ episodes.iter()
197+ .enumerate()
198+ .filter(|(_, episode)| {
199+ // Check date filter
200+ if let Some(date_from) = state.filter_date_from {
201+ if episode.pub_date < date_from {
202+ return false;
203+ }
204+ }
205+
206+ if let Some(date_to) = state.filter_date_to {
207+ if episode.pub_date > date_to {
208+ return false;
209+ }
210+ }
211+
212+ // Check episode number filter
213+ if !state.filter_episode_number.is_empty() {
214+ if let Ok(ep_num) = state.filter_episode_number.parse::<u32>() {
215+ // Try to extract episode number from title (e.g., "Episode 10" or "#10")
216+ let title_lower = episode.title.to_lowercase();
217+ if title_lower.contains("episode") || title_lower.contains("ep") || title_lower.contains("#") {
218+ // Simple extraction - look for numbers in title
219+ let found_number = episode.title
220+ .chars()
221+ .filter(|c| c.is_ascii_digit())
222+ .collect::<String>()
223+ .parse::<u32>()
224+ .ok();
225+
226+ if let Some(found_num) = found_number {
227+ if found_num != ep_num {
228+ return false;
229+ }
230+ }
231+ }
232+ }
233+ }
234+
235+ true
236+ })
237+ .map(|(i, _)| i)
238+ .collect()
239+}
new file mode 100644
@@ -0,0 +1,239 @@
1+//! Filter Panel UI component
2+//!
3+//! This module contains the UI for filtering feeds and episodes.
4+
5+use crate::config::AppState;
6+use chrono::{DateTime, Utc};
7+use egui::Ui;
8+
9+/// Draws the filter panel for feeds
10+pub fn draw_filter_panel(state: &mut AppState, ui: &mut Ui) {
11+ ui.horizontal(|ui| {
12+ ui.label("Filtres:");
13+
14+ // Date filter
15+ ui.label("Date de publication:");
16+
17+ // Date from filter
18+ if let Some(date_from) = state.filter_date_from {
19+ if ui.button(format!("Depuis: {}", date_from.format("%Y-%m-%d"))).clicked() {
20+ state.show_date_picker_from = true;
21+ }
22+ } else {
23+ if ui.button("Depuis...").clicked() {
24+ state.show_date_picker_from = true;
25+ }
26+ }
27+
28+ ui.add_space(5.0);
29+
30+ // Date to filter
31+ if let Some(date_to) = state.filter_date_to {
32+ if ui.button(format!("Jusqu'à: {}", date_to.format("%Y-%m-%d"))).clicked() {
33+ state.show_date_picker_to = true;
34+ }
35+ } else {
36+ if ui.button("Jusqu'à...").clicked() {
37+ state.show_date_picker_to = true;
38+ }
39+ }
40+
41+ ui.add_space(10.0);
42+
43+ // Episode number filter
44+ ui.label("Numéro d'épisode:");
45+ ui.add(egui::TextEdit::singleline(&mut state.filter_episode_number)
46+ .hint_text("ex: 10"));
47+
48+ ui.add_space(10.0);
49+
50+ // Clear filters button
51+ if ui.button("Effacer les filtres").clicked() {
52+ state.clear_filters();
53+ }
54+ });
55+
56+ ui.separator();
57+
58+ // Draw date pickers
59+ draw_date_picker_from(state, ui);
60+ draw_date_picker_to(state, ui);
61+}
62+
63+/// Draws the date picker for 'from' date
64+fn draw_date_picker_from(state: &mut AppState, ui: &mut Ui) {
65+ if !state.show_date_picker_from {
66+ return;
67+ }
68+
69+ egui::Window::new("Sélectionner la date de début")
70+ .resizable(false)
71+ .show(ui.ctx(), |ui| {
72+ ui.vertical(|ui| {
73+ ui.label("Date de début (yyyy-mm-dd):");
74+
75+ let mut date_str = state.filter_date_from
76+ .map(|d| d.format("%Y-%m-%d").to_string())
77+ .unwrap_or_default();
78+
79+ if ui.text_edit_singleline(&mut date_str).changed() {
80+ if let Ok(date) = chrono::NaiveDate::parse_from_str(&date_str, "%Y-%m-%d") {
81+ state.filter_date_from = Some(DateTime::<Utc>::from_naive_utc_and_offset(date.and_hms_opt(0, 0, 0).unwrap(), Utc));
82+ state.show_date_picker_from = false;
83+ }
84+ }
85+
86+ ui.separator();
87+
88+ ui.horizontal(|ui| {
89+ // Quick date buttons
90+ if ui.button("Aujourd'hui").clicked() {
91+ state.filter_date_from = Some(Utc::now());
92+ state.show_date_picker_from = false;
93+ }
94+
95+ if ui.button("7 derniers jours").clicked() {
96+ state.filter_date_from = Some(Utc::now() - chrono::Duration::days(7));
97+ state.show_date_picker_from = false;
98+ }
99+
100+ if ui.button("30 derniers jours").clicked() {
101+ state.filter_date_from = Some(Utc::now() - chrono::Duration::days(30));
102+ state.show_date_picker_from = false;
103+ }
104+
105+ if ui.button("Annuler").clicked() {
106+ state.show_date_picker_from = false;
107+ }
108+ });
109+ });
110+ });
111+}
112+
113+/// Draws the date picker for 'to' date
114+fn draw_date_picker_to(state: &mut AppState, ui: &mut Ui) {
115+ if !state.show_date_picker_to {
116+ return;
117+ }
118+
119+ egui::Window::new("Sélectionner la date de fin")
120+ .resizable(false)
121+ .show(ui.ctx(), |ui| {
122+ ui.vertical(|ui| {
123+ ui.label("Date de fin (yyyy-mm-dd):");
124+
125+ let mut date_str = state.filter_date_to
126+ .map(|d| d.format("%Y-%m-%d").to_string())
127+ .unwrap_or_default();
128+
129+ if ui.text_edit_singleline(&mut date_str).changed() {
130+ if let Ok(date) = chrono::NaiveDate::parse_from_str(&date_str, "%Y-%m-%d") {
131+ state.filter_date_to = Some(DateTime::<Utc>::from_naive_utc_and_offset(date.and_hms_opt(23, 59, 59).unwrap(), Utc));
132+ state.show_date_picker_to = false;
133+ }
134+ }
135+
136+ ui.separator();
137+
138+ ui.horizontal(|ui| {
139+ // Quick date buttons
140+ if ui.button("Aujourd'hui").clicked() {
141+ state.filter_date_to = Some(Utc::now());
142+ state.show_date_picker_to = false;
143+ }
144+
145+ if ui.button("7 derniers jours").clicked() {
146+ state.filter_date_to = Some(Utc::now());
147+ state.filter_date_from = Some(Utc::now() - chrono::Duration::days(7));
148+ state.show_date_picker_to = false;
149+ }
150+
151+ if ui.button("Annuler").clicked() {
152+ state.show_date_picker_to = false;
153+ }
154+ });
155+ });
156+ });
157+}
158+
159+/// Filters feeds based on current filter criteria
160+pub fn filter_feeds(feeds: &[crate::config::feed::Feed], state: &AppState) -> Vec<usize> {
161+ feeds.iter()
162+ .enumerate()
163+ .filter(|(_, feed)| {
164+ // Check date filter
165+ if let Some(date_from) = state.filter_date_from {
166+ if let Some(last_episode) = feed.last_episode_date {
167+ if last_episode < date_from {
168+ return false;
169+ }
170+ } else {
171+ // No episodes, don't show if filtering by date
172+ return false;
173+ }
174+ }
175+
176+ if let Some(date_to) = state.filter_date_to {
177+ if let Some(last_episode) = feed.last_episode_date {
178+ if last_episode > date_to {
179+ return false;
180+ }
181+ } else {
182+ // No episodes, don't show if filtering by date
183+ return false;
184+ }
185+ }
186+
187+ true
188+ })
189+ .map(|(i, _)| i)
190+ .collect()
191+}
192+
193+/// Filters episodes based on current filter criteria
194+#[allow(dead_code)]
195+pub fn filter_episodes(episodes: &[crate::config::episode::Episode], state: &AppState) -> Vec<usize> {
196+ episodes.iter()
197+ .enumerate()
198+ .filter(|(_, episode)| {
199+ // Check date filter
200+ if let Some(date_from) = state.filter_date_from {
201+ if episode.pub_date < date_from {
202+ return false;
203+ }
204+ }
205+
206+ if let Some(date_to) = state.filter_date_to {
207+ if episode.pub_date > date_to {
208+ return false;
209+ }
210+ }
211+
212+ // Check episode number filter
213+ if !state.filter_episode_number.is_empty() {
214+ if let Ok(ep_num) = state.filter_episode_number.parse::<u32>() {
215+ // Try to extract episode number from title (e.g., "Episode 10" or "#10")
216+ let title_lower = episode.title.to_lowercase();
217+ if title_lower.contains("episode") || title_lower.contains("ep") || title_lower.contains("#") {
218+ // Simple extraction - look for numbers in title
219+ let found_number = episode.title
220+ .chars()
221+ .filter(|c| c.is_ascii_digit())
222+ .collect::<String>()
223+ .parse::<u32>()
224+ .ok();
225+
226+ if let Some(found_num) = found_number {
227+ if found_num != ep_num {
228+ return false;
229+ }
230+ }
231+ }
232+ }
233+ }
234+
235+ true
236+ })
237+ .map(|(i, _)| i)
238+ .collect()
239+}
modified src/ui/home.rs +14 -0
@@ -14,6 +14,9 @@ pub fn draw_home(state: &mut AppState, ui: &mut Ui) {
1414
1515 ui.separator();
1616
17+ // Filter panel
18+ crate::ui::filter_panel::draw_filter_panel(state, ui);
19+
1720 // Action buttons
1821 draw_action_buttons(state, ui);
1922
@@ -26,11 +29,22 @@ pub fn draw_home(state: &mut AppState, ui: &mut Ui) {
2629 draw_empty_state(ui);
2730 }
2831
32+ ui.separator();
33+
34+ // Playlist list
35+ crate::ui::playlist_list::draw_playlist_list(state, ui);
36+
2937 // Draw add feed dialog
3038 crate::ui::add_feed_dialog::draw_add_feed_dialog(state, ui);
3139
3240 // Draw edit feed dialog
3341 crate::ui::feed_edit_dialog::draw_edit_feed_dialog(state, ui);
42+
43+ // Draw add playlist dialog
44+ crate::ui::playlist_list::draw_add_playlist_dialog(state, ui);
45+
46+ // Draw edit playlist dialog
47+ crate::ui::playlist_list::draw_edit_playlist_dialog(state, ui);
3448 }
3549
3650 /// Draws the action buttons
@@ -14,6 +14,9 @@ pub fn draw_home(state: &mut AppState, ui: &mut Ui) {
14 14
15 ui.separator();15 ui.separator();
16 16
17+ // Filter panel
18+ crate::ui::filter_panel::draw_filter_panel(state, ui);
19+
17 // Action buttons20 // Action buttons
18 draw_action_buttons(state, ui);21 draw_action_buttons(state, ui);
19 22
@@ -26,11 +29,22 @@ pub fn draw_home(state: &mut AppState, ui: &mut Ui) {
26 draw_empty_state(ui);29 draw_empty_state(ui);
27 }30 }
28 31
32+ ui.separator();
33+
34+ // Playlist list
35+ crate::ui::playlist_list::draw_playlist_list(state, ui);
36+
29 // Draw add feed dialog37 // Draw add feed dialog
30 crate::ui::add_feed_dialog::draw_add_feed_dialog(state, ui);38 crate::ui::add_feed_dialog::draw_add_feed_dialog(state, ui);
31 39
32 // Draw edit feed dialog40 // Draw edit feed dialog
33 crate::ui::feed_edit_dialog::draw_edit_feed_dialog(state, ui);41 crate::ui::feed_edit_dialog::draw_edit_feed_dialog(state, ui);
42+
43+ // Draw add playlist dialog
44+ crate::ui::playlist_list::draw_add_playlist_dialog(state, ui);
45+
46+ // Draw edit playlist dialog
47+ crate::ui::playlist_list::draw_edit_playlist_dialog(state, ui);
34 }48 }
35 49
36 /// Draws the action buttons50 /// Draws the action buttons
modified src/ui/mod.rs +2 -0
@@ -8,3 +8,5 @@ pub mod add_feed_dialog;
88 pub mod feed_edit_dialog;
99 pub mod status_bar;
1010 pub mod notifications;
11+pub mod playlist_list;
12+pub mod filter_panel;
@@ -8,3 +8,5 @@ pub mod add_feed_dialog;
8 pub mod feed_edit_dialog;8 pub mod feed_edit_dialog;
9 pub mod status_bar;9 pub mod status_bar;
10 pub mod notifications;10 pub mod notifications;
11+pub mod playlist_list;
12+pub mod filter_panel;
added src/ui/playlist_list.rs +166 -0
new file mode 100644
@@ -0,0 +1,166 @@
1+//! Playlist List UI component
2+//!
3+//! This module contains the UI for displaying and managing playlists.
4+
5+use crate::config::playlist::Playlist;
6+use crate::config::AppState;
7+use egui::{Align, Layout, ScrollArea, Ui};
8+
9+/// Draws the playlist list
10+pub fn draw_playlist_list(state: &mut AppState, ui: &mut Ui) {
11+ ui.horizontal(|ui| {
12+ ui.label("Playlists:");
13+ if ui.button("➕ Ajouter").clicked() {
14+ state.show_add_playlist_dialog = true;
15+ }
16+ });
17+
18+ ui.separator();
19+
20+ if state.playlists.is_empty() {
21+ ui.label("Aucune playlist. Cliquez sur 'Ajouter' pour créer une playlist.");
22+ return;
23+ }
24+
25+ ScrollArea::vertical().show(ui, |ui| {
26+ let playlist_count = state.playlists.len();
27+ for index in 0..playlist_count {
28+ draw_playlist_item(state, ui, index);
29+ }
30+ });
31+}
32+
33+/// Draws a single playlist item
34+fn draw_playlist_item(state: &mut AppState, ui: &mut Ui, index: usize) {
35+ let playlist = &state.playlists[index];
36+ let playlist_name = playlist.name.clone();
37+ let feed_count = playlist.feed_count();
38+
39+ ui.horizontal(|ui| {
40+ // Playlist name and info
41+ ui.vertical(|ui| {
42+ ui.label(&playlist_name);
43+ ui.label(format!("{} flux", feed_count));
44+ });
45+
46+ // Actions
47+ ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
48+ // Edit button
49+ if ui.button("✏️").on_hover_text("Éditer").clicked() {
50+ state.editing_playlist_index = Some(index);
51+ state.edit_playlist_name = playlist_name;
52+ state.show_edit_playlist_dialog = true;
53+ }
54+
55+ // Delete button
56+ if ui.button("🗑️").on_hover_text("Supprimer").clicked() {
57+ if let Some(playlist) = state.remove_playlist(index) {
58+ log::info!("Deleted playlist: {}", playlist.name);
59+ super::super::config::storage::save_playlists_default(&state.playlists).ok();
60+ }
61+ }
62+ });
63+ });
64+
65+ ui.separator();
66+}
67+
68+/// Draws the add playlist dialog
69+pub fn draw_add_playlist_dialog(state: &mut AppState, ui: &mut Ui) {
70+ if !state.show_add_playlist_dialog {
71+ return;
72+ }
73+
74+ egui::Window::new("Ajouter une playlist")
75+ .resizable(false)
76+ .show(ui.ctx(), |ui| {
77+ ui.vertical(|ui| {
78+ ui.label("Nouvelle playlist");
79+ ui.separator();
80+
81+ ui.label("Nom de la playlist");
82+ ui.text_edit_singleline(&mut state.add_playlist_name);
83+
84+ ui.label("Description (optionnel)");
85+ ui.text_edit_multiline(&mut state.add_playlist_description);
86+
87+ ui.separator();
88+
89+ ui.horizontal(|ui| {
90+ if !state.add_playlist_name.is_empty() && ui.button("Ajouter").clicked() {
91+ log::info!("Adding new playlist: {}", state.add_playlist_name);
92+ let mut playlist = Playlist::new(state.add_playlist_name.clone());
93+ if !state.add_playlist_description.is_empty() {
94+ playlist.set_description(state.add_playlist_description.clone());
95+ }
96+ state.add_playlist(playlist);
97+ super::super::config::storage::save_playlists_default(&state.playlists).ok();
98+ log::info!("Playlist added successfully");
99+ state.clear_add_playlist_form();
100+ state.show_add_playlist_dialog = false;
101+ } else {
102+ ui.label("Veuillez entrer un nom");
103+ }
104+
105+ if ui.button("Annuler").clicked() {
106+ state.clear_add_playlist_form();
107+ state.show_add_playlist_dialog = false;
108+ }
109+ });
110+ });
111+ });
112+}
113+
114+/// Draws the edit playlist dialog
115+pub fn draw_edit_playlist_dialog(state: &mut AppState, ui: &mut Ui) {
116+ let editing_index = match state.editing_playlist_index {
117+ Some(idx) => idx,
118+ None => return,
119+ };
120+
121+ if editing_index >= state.playlists.len() {
122+ state.clear_edit_playlist_form();
123+ return;
124+ }
125+
126+ egui::Window::new("Éditer la playlist")
127+ .resizable(false)
128+ .show(ui.ctx(), |ui| {
129+ ui.vertical(|ui| {
130+ ui.label(format!("Éditer: {}", state.edit_playlist_name));
131+ ui.separator();
132+
133+ ui.label("Nom de la playlist");
134+ ui.text_edit_singleline(&mut state.edit_playlist_name);
135+
136+ ui.label("Description (optionnel)");
137+ ui.text_edit_multiline(&mut state.edit_playlist_description);
138+
139+ ui.separator();
140+
141+ ui.horizontal(|ui| {
142+ if !state.edit_playlist_name.is_empty() && ui.button("Sauvegarder").clicked() {
143+ log::info!("Updating playlist at index {}", editing_index);
144+ state.playlists[editing_index].name = state.edit_playlist_name.clone();
145+ if !state.edit_playlist_description.is_empty() {
146+ state.playlists[editing_index].set_description(state.edit_playlist_description.clone());
147+ } else {
148+ state.playlists[editing_index].clear_description();
149+ }
150+ super::super::config::storage::save_playlists_default(&state.playlists).ok();
151+ log::info!("Playlist updated successfully");
152+ state.notification_manager.success("Playlist modifiée");
153+ state.clear_edit_playlist_form();
154+ state.show_edit_playlist_dialog = false;
155+ } else {
156+ ui.label("Veuillez entrer un nom");
157+ }
158+
159+ if ui.button("Annuler").clicked() {
160+ state.clear_edit_playlist_form();
161+ state.show_edit_playlist_dialog = false;
162+ }
163+ });
164+ });
165+ });
166+}
new file mode 100644
@@ -0,0 +1,166 @@
1+//! Playlist List UI component
2+//!
3+//! This module contains the UI for displaying and managing playlists.
4+
5+use crate::config::playlist::Playlist;
6+use crate::config::AppState;
7+use egui::{Align, Layout, ScrollArea, Ui};
8+
9+/// Draws the playlist list
10+pub fn draw_playlist_list(state: &mut AppState, ui: &mut Ui) {
11+ ui.horizontal(|ui| {
12+ ui.label("Playlists:");
13+ if ui.button("➕ Ajouter").clicked() {
14+ state.show_add_playlist_dialog = true;
15+ }
16+ });
17+
18+ ui.separator();
19+
20+ if state.playlists.is_empty() {
21+ ui.label("Aucune playlist. Cliquez sur 'Ajouter' pour créer une playlist.");
22+ return;
23+ }
24+
25+ ScrollArea::vertical().show(ui, |ui| {
26+ let playlist_count = state.playlists.len();
27+ for index in 0..playlist_count {
28+ draw_playlist_item(state, ui, index);
29+ }
30+ });
31+}
32+
33+/// Draws a single playlist item
34+fn draw_playlist_item(state: &mut AppState, ui: &mut Ui, index: usize) {
35+ let playlist = &state.playlists[index];
36+ let playlist_name = playlist.name.clone();
37+ let feed_count = playlist.feed_count();
38+
39+ ui.horizontal(|ui| {
40+ // Playlist name and info
41+ ui.vertical(|ui| {
42+ ui.label(&playlist_name);
43+ ui.label(format!("{} flux", feed_count));
44+ });
45+
46+ // Actions
47+ ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
48+ // Edit button
49+ if ui.button("✏️").on_hover_text("Éditer").clicked() {
50+ state.editing_playlist_index = Some(index);
51+ state.edit_playlist_name = playlist_name;
52+ state.show_edit_playlist_dialog = true;
53+ }
54+
55+ // Delete button
56+ if ui.button("🗑️").on_hover_text("Supprimer").clicked() {
57+ if let Some(playlist) = state.remove_playlist(index) {
58+ log::info!("Deleted playlist: {}", playlist.name);
59+ super::super::config::storage::save_playlists_default(&state.playlists).ok();
60+ }
61+ }
62+ });
63+ });
64+
65+ ui.separator();
66+}
67+
68+/// Draws the add playlist dialog
69+pub fn draw_add_playlist_dialog(state: &mut AppState, ui: &mut Ui) {
70+ if !state.show_add_playlist_dialog {
71+ return;
72+ }
73+
74+ egui::Window::new("Ajouter une playlist")
75+ .resizable(false)
76+ .show(ui.ctx(), |ui| {
77+ ui.vertical(|ui| {
78+ ui.label("Nouvelle playlist");
79+ ui.separator();
80+
81+ ui.label("Nom de la playlist");
82+ ui.text_edit_singleline(&mut state.add_playlist_name);
83+
84+ ui.label("Description (optionnel)");
85+ ui.text_edit_multiline(&mut state.add_playlist_description);
86+
87+ ui.separator();
88+
89+ ui.horizontal(|ui| {
90+ if !state.add_playlist_name.is_empty() && ui.button("Ajouter").clicked() {
91+ log::info!("Adding new playlist: {}", state.add_playlist_name);
92+ let mut playlist = Playlist::new(state.add_playlist_name.clone());
93+ if !state.add_playlist_description.is_empty() {
94+ playlist.set_description(state.add_playlist_description.clone());
95+ }
96+ state.add_playlist(playlist);
97+ super::super::config::storage::save_playlists_default(&state.playlists).ok();
98+ log::info!("Playlist added successfully");
99+ state.clear_add_playlist_form();
100+ state.show_add_playlist_dialog = false;
101+ } else {
102+ ui.label("Veuillez entrer un nom");
103+ }
104+
105+ if ui.button("Annuler").clicked() {
106+ state.clear_add_playlist_form();
107+ state.show_add_playlist_dialog = false;
108+ }
109+ });
110+ });
111+ });
112+}
113+
114+/// Draws the edit playlist dialog
115+pub fn draw_edit_playlist_dialog(state: &mut AppState, ui: &mut Ui) {
116+ let editing_index = match state.editing_playlist_index {
117+ Some(idx) => idx,
118+ None => return,
119+ };
120+
121+ if editing_index >= state.playlists.len() {
122+ state.clear_edit_playlist_form();
123+ return;
124+ }
125+
126+ egui::Window::new("Éditer la playlist")
127+ .resizable(false)
128+ .show(ui.ctx(), |ui| {
129+ ui.vertical(|ui| {
130+ ui.label(format!("Éditer: {}", state.edit_playlist_name));
131+ ui.separator();
132+
133+ ui.label("Nom de la playlist");
134+ ui.text_edit_singleline(&mut state.edit_playlist_name);
135+
136+ ui.label("Description (optionnel)");
137+ ui.text_edit_multiline(&mut state.edit_playlist_description);
138+
139+ ui.separator();
140+
141+ ui.horizontal(|ui| {
142+ if !state.edit_playlist_name.is_empty() && ui.button("Sauvegarder").clicked() {
143+ log::info!("Updating playlist at index {}", editing_index);
144+ state.playlists[editing_index].name = state.edit_playlist_name.clone();
145+ if !state.edit_playlist_description.is_empty() {
146+ state.playlists[editing_index].set_description(state.edit_playlist_description.clone());
147+ } else {
148+ state.playlists[editing_index].clear_description();
149+ }
150+ super::super::config::storage::save_playlists_default(&state.playlists).ok();
151+ log::info!("Playlist updated successfully");
152+ state.notification_manager.success("Playlist modifiée");
153+ state.clear_edit_playlist_form();
154+ state.show_edit_playlist_dialog = false;
155+ } else {
156+ ui.label("Veuillez entrer un nom");
157+ }
158+
159+ if ui.button("Annuler").clicked() {
160+ state.clear_edit_playlist_form();
161+ state.show_edit_playlist_dialog = false;
162+ }
163+ });
164+ });
165+ });
166+}