msansm1/rss-podcast-dlpublic Fork
⑂ a614bb5
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: Implémentation des notifications et détection automatique

msansm1 committed 2026-07-05T21:48:57+02:00 Browse files
a614bb5 parent: 653b07c
modified spec/init-plan.md +24 -8
@@ -296,18 +296,34 @@ pub async fn download_episode(
296296
297297 ---
298298
299+### 4.8 Phase 8 : Fonctionnalités Avancées (Durée : 3 jours)
300+
301+**Objectif** : Ajouter des fonctionnalités avancées pour améliorer l'expérience utilisateur.
302+
303+| Tâche | Priorité | Description | Livrable |
304+|-------|----------|-------------|----------|
305+| 8.1 | High | Implémenter la détection automatique des nouveaux épisodes | Comparaison avec last_episode_date |
306+| 8.2 | High | Ajouter des notifications de téléchargement | Popups ou notifications système |
307+| 8.3 | Medium | Implémenter le support des playlists | Groupement de flux |
308+| 8.4 | Medium | Implémenter le filtrage par date ou numéro d'épisode | UI de filtrage |
309+| 8.5 | Low | Ajouter un système de tags pour les flux | Catégorisation |
310+| 8.6 | Low | Implémenter l'export/import des flux | Format JSON étendu |
311+
312+---
313+
299314 ## 5. Calendrier Global
300315
301316 | Phase | Durée | Dates Estimées | Statut |
302317 |-------|-------|----------------|--------|
303-| Phase 1 | 1 jour | J1 | ⬜ |
304-| Phase 2 | 2 jours | J2-J3 | ⬜ |
305-| Phase 3 | 3 jours | J4-J6 | ⬜ |
306-| Phase 4 | 4 jours | J7-J10 | ⬜ |
307-| Phase 5 | 2 jours | J11-J12 | ⬜ |
308-| Phase 6 | 2 jours | J13-J14 | ⬜ |
309-| Phase 7 | 1 jour | J15 | ⬜ |
310-| **Total** | **15 jours** | | |
318+| Phase 1 | 1 jour | J1 | ✅ |
319+| Phase 2 | 2 jours | J2-J3 | ✅ |
320+| Phase 3 | 3 jours | J4-J6 | ✅ |
321+| Phase 4 | 4 jours | J7-J10 | ✅ |
322+| Phase 5 | 2 jours | J11-J12 | ✅ |
323+| Phase 6 | 2 jours | J13-J14 | ✅ |
324+| Phase 7 | 1 jour | J15 | ✅ |
325+| Phase 8 | 3 jours | J16-J18 | 🟡 25% (2/8 tâches) |
326+| **Total** | **18 jours** | | |
311327
312328 ---
313329
@@ -296,18 +296,34 @@ pub async fn download_episode(
296 296
297 ---297 ---
298 298
299+### 4.8 Phase 8 : Fonctionnalités Avancées (Durée : 3 jours)
300+
301+**Objectif** : Ajouter des fonctionnalités avancées pour améliorer l'expérience utilisateur.
302+
303+| Tâche | Priorité | Description | Livrable |
304+|-------|----------|-------------|----------|
305+| 8.1 | High | Implémenter la détection automatique des nouveaux épisodes | Comparaison avec last_episode_date |
306+| 8.2 | High | Ajouter des notifications de téléchargement | Popups ou notifications système |
307+| 8.3 | Medium | Implémenter le support des playlists | Groupement de flux |
308+| 8.4 | Medium | Implémenter le filtrage par date ou numéro d'épisode | UI de filtrage |
309+| 8.5 | Low | Ajouter un système de tags pour les flux | Catégorisation |
310+| 8.6 | Low | Implémenter l'export/import des flux | Format JSON étendu |
311+
312+---
313+
299 ## 5. Calendrier Global314 ## 5. Calendrier Global
300 315
301 | Phase | Durée | Dates Estimées | Statut |316 | Phase | Durée | Dates Estimées | Statut |
302 |-------|-------|----------------|--------|317 |-------|-------|----------------|--------|
303-| Phase 1 | 1 jour | J1 | ⬜ |318+| Phase 1 | 1 jour | J1 | ✅ |
304-| Phase 2 | 2 jours | J2-J3 | ⬜ |319+| Phase 2 | 2 jours | J2-J3 | ✅ |
305-| Phase 3 | 3 jours | J4-J6 | ⬜ |320+| Phase 3 | 3 jours | J4-J6 | ✅ |
306-| Phase 4 | 4 jours | J7-J10 | ⬜ |321+| Phase 4 | 4 jours | J7-J10 | ✅ |
307-| Phase 5 | 2 jours | J11-J12 | ⬜ |322+| Phase 5 | 2 jours | J11-J12 | ✅ |
308-| Phase 6 | 2 jours | J13-J14 | ⬜ |323+| Phase 6 | 2 jours | J13-J14 | ✅ |
309-| Phase 7 | 1 jour | J15 | ⬜ |324+| Phase 7 | 1 jour | J15 | ✅ |
310-| **Total** | **15 jours** | | |325+| Phase 8 | 3 jours | J16-J18 | 🟡 25% (2/8 tâches) |
326+| **Total** | **18 jours** | | |
311 327
312 ---328 ---
313 329
modified spec/init-tasks.md +22 -2
@@ -168,12 +168,31 @@
168168 | T7.7 | Vérifier les warnings de compilation | L | 1h | - | Toutes | ⬜ |
169169 | T7.8 | Créer un fichier `.gitignore` approprié | L | 0.5h | `.gitignore` | T1.1 | ⬜ |
170170 | T7.9 | Commiter tout le code avec des messages clairs | L | 1h | - | Toutes | ⬜ |
171-| T7.10 | Créer une release GitHub (optionnel) | L | 1h | - | T7.9 | ⬜ |
171+| T7.10 | Créer une release GitHub (optionnel) | L | 1h | - | T7.9 | ✅ |
172172
173173 **Total Phase 7** : 11 heures (1.5 jours révisés)
174174
175175 ---
176176
177+## Phase 8 : Fonctionnalités Avancées
178+
179+### Objectif : Ajouter des fonctionnalités avancées pour améliorer l'expérience utilisateur
180+
181+| ID | Tâche | Priorité | Durée | Fichiers | Dépendances | Statut |
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 | ✅ |
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 | ⬜ |
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 | ⬜ |
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 | ⬜ |
191+
192+**Total Phase 8** : 20 heures (2.5 jours)
193+
194+---
195+
177196 ## Résumé Global
178197
179198 | Phase | Nombre de Tâches | Durée Totale | Statut |
@@ -185,7 +204,8 @@
185204 | Phase 5 | 10 tâches | 16h | ✅ Terminé |
186205 | Phase 6 | 12 tâches | 24h | ✅ Terminé |
187206 | Phase 7 | 10 tâches | 11h | ✅ Terminé |
188-| **Total** | **80 tâches** | **125 heures** (~16 jours) | 🟢 100% complet |
207+| Phase 8 | 8 tâches | 20h | 🟡 25% complet (2/8) |
208+| **Total** | **88 tâches** | **145 heures** (~18 jours) | 🟡 93.2% complet |
189209
190210 ---
191211
@@ -168,12 +168,31 @@
168 | T7.7 | Vérifier les warnings de compilation | L | 1h | - | Toutes | ⬜ |168 | T7.7 | Vérifier les warnings de compilation | L | 1h | - | Toutes | ⬜ |
169 | T7.8 | Créer un fichier `.gitignore` approprié | L | 0.5h | `.gitignore` | T1.1 | ⬜ |169 | T7.8 | Créer un fichier `.gitignore` approprié | L | 0.5h | `.gitignore` | T1.1 | ⬜ |
170 | T7.9 | Commiter tout le code avec des messages clairs | L | 1h | - | Toutes | ⬜ |170 | T7.9 | Commiter tout le code avec des messages clairs | L | 1h | - | Toutes | ⬜ |
171-| T7.10 | Créer une release GitHub (optionnel) | L | 1h | - | T7.9 | ⬜ |171+| T7.10 | Créer une release GitHub (optionnel) | L | 1h | - | T7.9 | ✅ |
172 172
173 **Total Phase 7** : 11 heures (1.5 jours révisés)173 **Total Phase 7** : 11 heures (1.5 jours révisés)
174 174
175 ---175 ---
176 176
177+## Phase 8 : Fonctionnalités Avancées
178+
179+### Objectif : Ajouter des fonctionnalités avancées pour améliorer l'expérience utilisateur
180+
181+| ID | Tâche | Priorité | Durée | Fichiers | Dépendances | Statut |
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 | ✅ |
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 | ⬜ |
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 | ⬜ |
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 | ⬜ |
191+
192+**Total Phase 8** : 20 heures (2.5 jours)
193+
194+---
195+
177 ## Résumé Global196 ## Résumé Global
178 197
179 | Phase | Nombre de Tâches | Durée Totale | Statut |198 | Phase | Nombre de Tâches | Durée Totale | Statut |
@@ -185,7 +204,8 @@
185 | Phase 5 | 10 tâches | 16h | ✅ Terminé |204 | Phase 5 | 10 tâches | 16h | ✅ Terminé |
186 | Phase 6 | 12 tâches | 24h | ✅ Terminé |205 | Phase 6 | 12 tâches | 24h | ✅ Terminé |
187 | Phase 7 | 10 tâches | 11h | ✅ Terminé |206 | Phase 7 | 10 tâches | 11h | ✅ Terminé |
188-| **Total** | **80 tâches** | **125 heures** (~16 jours) | 🟢 100% complet |207+| Phase 8 | 8 tâches | 20h | 🟡 25% complet (2/8) |
208+| **Total** | **88 tâches** | **145 heures** (~18 jours) | 🟡 93.2% complet |
189 209
190 ---210 ---
191 211
modified spec/progress.md +51 -7
@@ -1,7 +1,7 @@
11 # RSS Podcast Downloader - Suivi des Progrès
22
33 *Dernière mise à jour: 05/07/2026*
4-*Version: 2.0*
4+*Version: 2.1*
55
66 ---
77
@@ -202,7 +202,7 @@
202202
203203 ---
204204
205-## Phase 7 : Finalisation (9/10 tâches - 90%)
205+## Phase 7 : Finalisation (10/10 tâches - 100%)
206206
207207 - [x] T7.1 - Ajouter une icône pour l'application (fichier `assets/icon.png`)
208208 - *Fichiers: assets/icon.png*
@@ -255,6 +255,44 @@
255255
256256 ---
257257
258+## Phase 8 : Fonctionnalités Avancées (2/8 tâches - 25%)
259+
260+- [x] T8.1 - Implémenter la détection automatique des nouveaux épisodes
261+ - *Fichiers: src/rss/sync.rs, src/rss/downloader.rs*
262+ - *Contenu: Fonction is_episode_downloaded() et needs_download() déjà implémentées*
263+ - *Date: 05/07/2026*
264+
265+- [x] T8.2 - Implémenter les notifications de téléchargement (popups)
266+ - *Fichiers: 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*
267+ - *Contenu: Module NotificationManager complet avec affichage dans UI, notifications pour synchronisation et suppression*
268+ - *Date: 05/07/2026*
269+
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é*
273+
274+- [ ] T8.4 - Implémenter le filtrage par date
275+ - *Fichiers: src/ui/filter_panel.rs, src/rss/sync.rs*
276+ - *Statut: Non commencé*
277+
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é*
281+
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+
286+- [ ] T8.7 - Implémenter l'export des flux (fichier JSON personnalisé)
287+ - *Fichiers: src/config/storage.rs, src/ui/home.rs*
288+ - *Statut: Non commencé*
289+
290+- [ ] T8.8 - Implémenter l'import des flux
291+ - *Fichiers: src/config/storage.rs, src/ui/home.rs*
292+ - *Statut: Non commencé*
293+
294+---
295+
258296 ## Résumé Global
259297
260298 | Phase | Total | Terminées | % Complet | Statut |
@@ -266,16 +304,22 @@
266304 | Phase 5 | 10 | 10 | 100% | Terminé |
267305 | Phase 6 | 12 | 12 | 100% | Terminé |
268306 | Phase 7 | 10 | 10 | 100% | Terminé |
269-| **Total** | **80** | **80** | **100%** | Terminé |
307+| Phase 8 | 8 | 2 | 25% | En cours |
308+| **Total** | **88** | **82** | **93.2%** | En cours |
270309
271-**Projet complété à 100%!**
310+**Projet complété à 93.2%**
272311
273312 ---
274313
275314 ## Prochaines Étapes
276315
277-### Projet Complété
278-Toutes les phases ont été implémentées avec succès!
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
321+- Continuer avec T8.7 - Implémenter l'export des flux
322+- Continuer avec T8.8 - Implémenter l'import des flux
279323
280324 ### Historique des commits
281325 - Phase 1: Initialisation du projet
@@ -353,4 +397,4 @@ Phase 7: Finalisation
353397 ---
354398
355399 *Document généré et mis à jour le 05/07/2026*
356-*Projet RSS Podcast Downloader - COMPLÉTÉ À 100%*
400+*Projet RSS Podcast Downloader - 93.2% complet (Phase 8 en cours)*
@@ -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.0*4+*Version: 2.1*
5 5
6 ---6 ---
7 7
@@ -202,7 +202,7 @@
202 202
203 ---203 ---
204 204
205-## Phase 7 : Finalisation (9/10 tâches - 90%)205+## Phase 7 : Finalisation (10/10 tâches - 100%)
206 206
207 - [x] T7.1 - Ajouter une icône pour l'application (fichier `assets/icon.png`)207 - [x] T7.1 - Ajouter une icône pour l'application (fichier `assets/icon.png`)
208 - *Fichiers: assets/icon.png*208 - *Fichiers: assets/icon.png*
@@ -255,6 +255,44 @@
255 255
256 ---256 ---
257 257
258+## Phase 8 : Fonctionnalités Avancées (2/8 tâches - 25%)
259+
260+- [x] T8.1 - Implémenter la détection automatique des nouveaux épisodes
261+ - *Fichiers: src/rss/sync.rs, src/rss/downloader.rs*
262+ - *Contenu: Fonction is_episode_downloaded() et needs_download() déjà implémentées*
263+ - *Date: 05/07/2026*
264+
265+- [x] T8.2 - Implémenter les notifications de téléchargement (popups)
266+ - *Fichiers: 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*
267+ - *Contenu: Module NotificationManager complet avec affichage dans UI, notifications pour synchronisation et suppression*
268+ - *Date: 05/07/2026*
269+
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é*
273+
274+- [ ] T8.4 - Implémenter le filtrage par date
275+ - *Fichiers: src/ui/filter_panel.rs, src/rss/sync.rs*
276+ - *Statut: Non commencé*
277+
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é*
281+
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+
286+- [ ] T8.7 - Implémenter l'export des flux (fichier JSON personnalisé)
287+ - *Fichiers: src/config/storage.rs, src/ui/home.rs*
288+ - *Statut: Non commencé*
289+
290+- [ ] T8.8 - Implémenter l'import des flux
291+ - *Fichiers: src/config/storage.rs, src/ui/home.rs*
292+ - *Statut: Non commencé*
293+
294+---
295+
258 ## Résumé Global296 ## Résumé Global
259 297
260 | Phase | Total | Terminées | % Complet | Statut |298 | Phase | Total | Terminées | % Complet | Statut |
@@ -266,16 +304,22 @@
266 | Phase 5 | 10 | 10 | 100% | Terminé |304 | Phase 5 | 10 | 10 | 100% | Terminé |
267 | Phase 6 | 12 | 12 | 100% | Terminé |305 | Phase 6 | 12 | 12 | 100% | Terminé |
268 | Phase 7 | 10 | 10 | 100% | Terminé |306 | Phase 7 | 10 | 10 | 100% | Terminé |
269-| **Total** | **80** | **80** | **100%** | Terminé |307+| Phase 8 | 8 | 2 | 25% | En cours |
308+| **Total** | **88** | **82** | **93.2%** | En cours |
270 309
271-**Projet complété à 100%!**310+**Projet complété à 93.2%**
272 311
273 ---312 ---
274 313
275 ## Prochaines Étapes314 ## Prochaines Étapes
276 315
277-### Projet Complété316+### Phase 8 en cours (2/8 tâches)
278-Toutes les phases ont été implémentées avec succès!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 flux
322+- Continuer avec T8.8 - Implémenter l'import des flux
279 323
280 ### Historique des commits324 ### Historique des commits
281 - Phase 1: Initialisation du projet325 - Phase 1: Initialisation du projet
@@ -353,4 +397,4 @@ Phase 7: Finalisation
353 ---397 ---
354 398
355 *Document généré et mis à jour le 05/07/2026*399 *Document généré et mis à jour le 05/07/2026*
356-*Projet RSS Podcast Downloader - COMPLÉTÉ À 100%*400+*Projet RSS Podcast Downloader - 93.2% complet (Phase 8 en cours)*
modified src/app.rs +9 -0
@@ -106,6 +106,15 @@ impl eframe::App for RssPodcastApp {
106106 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
107107 ctx.send_viewport_cmd(egui::ViewportCommand::Title(self.title.clone()));
108108
109+ // Draw notifications panel at the top
110+ egui::TopBottomPanel::top("notifications_panel")
111+ .frame(egui::Frame::default().fill(egui::Color32::TRANSPARENT))
112+ .show(ctx, |ui| {
113+ ui.vertical_centered(|ui| {
114+ self.state.notification_manager.draw(ui);
115+ });
116+ });
117+
109118 egui::CentralPanel::default()
110119 .frame(egui::Frame::default().fill(self.clear_color))
111120 .show(ctx, |ui| {
@@ -106,6 +106,15 @@ impl eframe::App for RssPodcastApp {
106 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {106 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
107 ctx.send_viewport_cmd(egui::ViewportCommand::Title(self.title.clone()));107 ctx.send_viewport_cmd(egui::ViewportCommand::Title(self.title.clone()));
108 108
109+ // Draw notifications panel at the top
110+ egui::TopBottomPanel::top("notifications_panel")
111+ .frame(egui::Frame::default().fill(egui::Color32::TRANSPARENT))
112+ .show(ctx, |ui| {
113+ ui.vertical_centered(|ui| {
114+ self.state.notification_manager.draw(ui);
115+ });
116+ });
117+
109 egui::CentralPanel::default()118 egui::CentralPanel::default()
110 .frame(egui::Frame::default().fill(self.clear_color))119 .frame(egui::Frame::default().fill(self.clear_color))
111 .show(ctx, |ui| {120 .show(ctx, |ui| {
modified src/config/app_state.rs +5 -1
@@ -3,6 +3,7 @@
33 //! This module defines the main application state structure.
44
55 use crate::config::feed::Feed;
6+use crate::ui::notifications::NotificationManager;
67 use std::collections::HashMap;
78 use std::path::PathBuf;
89
@@ -50,6 +51,8 @@ pub struct AppState {
5051 /// Path to the configuration file
5152 #[allow(dead_code)]
5253 pub config_path: PathBuf,
54+ /// Manager for displaying notifications to the user
55+ pub notification_manager: NotificationManager,
5356 }
5457
5558 impl AppState {
@@ -74,6 +77,7 @@ impl AppState {
7477 info_message: None,
7578 show_settings: false,
7679 config_path: config_path.into(),
80+ notification_manager: NotificationManager::new(),
7781 }
7882 }
7983
@@ -104,7 +108,7 @@ impl AppState {
104108 /// Adds a new feed to the application state
105109 pub fn add_feed(&mut self, feed: Feed) {
106110 self.feeds.push(feed);
107- self.info_message = Some("Flux ajouté avec succès".to_string());
111+ self.notification_manager.success("Flux ajouté avec succès");
108112 }
109113
110114 /// Removes a feed by index
@@ -3,6 +3,7 @@
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::ui::notifications::NotificationManager;
6 use std::collections::HashMap;7 use std::collections::HashMap;
7 use std::path::PathBuf;8 use std::path::PathBuf;
8 9
@@ -50,6 +51,8 @@ pub struct AppState {
50 /// Path to the configuration file51 /// Path to the configuration file
51 #[allow(dead_code)]52 #[allow(dead_code)]
52 pub config_path: PathBuf,53 pub config_path: PathBuf,
54+ /// Manager for displaying notifications to the user
55+ pub notification_manager: NotificationManager,
53 }56 }
54 57
55 impl AppState {58 impl AppState {
@@ -74,6 +77,7 @@ impl AppState {
74 info_message: None,77 info_message: None,
75 show_settings: false,78 show_settings: false,
76 config_path: config_path.into(),79 config_path: config_path.into(),
80+ notification_manager: NotificationManager::new(),
77 }81 }
78 }82 }
79 83
@@ -104,7 +108,7 @@ impl AppState {
104 /// Adds a new feed to the application state108 /// Adds a new feed to the application state
105 pub fn add_feed(&mut self, feed: Feed) {109 pub fn add_feed(&mut self, feed: Feed) {
106 self.feeds.push(feed);110 self.feeds.push(feed);
107- self.info_message = Some("Flux ajouté avec succès".to_string());111+ self.notification_manager.success("Flux ajouté avec succès");
108 }112 }
109 113
110 /// Removes a feed by index114 /// Removes a feed by index
modified src/ui/feed_list.rs +5 -5
@@ -77,7 +77,7 @@ fn draw_feed_item(state: &mut AppState, ui: &mut Ui, index: usize) {
7777 state.feeds.remove(index);
7878 super::super::config::storage::save_feeds_default(&state.feeds).ok();
7979 log::info!("Feed deleted successfully");
80- state.set_info("Flux supprimé".to_string());
80+ state.notification_manager.success("Flux supprimé");
8181 if state.current_feed == Some(index) {
8282 state.select_feed(None);
8383 }
@@ -92,12 +92,12 @@ fn draw_feed_item(state: &mut AppState, ui: &mut Ui, index: usize) {
9292 /// Synchronizes a single feed
9393 fn sync_feed(state: &mut AppState, index: usize) {
9494 if index >= state.feeds.len() {
95- state.set_error("Index de flux invalide".to_string());
95+ state.notification_manager.error("Index de flux invalide");
9696 return;
9797 }
9898
9999 state.set_sync_state(true);
100- state.set_info(format!("Synchronisation du flux: {}", state.feeds[index].name));
100+ state.notification_manager.info(format!("Synchronisation du flux: {}", state.feeds[index].name));
101101
102102 log::info!("Starting synchronization of feed: {}", state.feeds[index].name);
103103
@@ -108,12 +108,12 @@ fn sync_feed(state: &mut AppState, index: usize) {
108108
109109 match result {
110110 Ok(episodes) => {
111- state.set_info(format!("Flux synchronisé: {} épisodes trouvés", episodes.len()));
111+ state.notification_manager.success(format!("Flux synchronisé: {} épisodes trouvés", episodes.len()));
112112 // Save the updated feeds
113113 super::super::config::storage::save_feeds_default(&state.feeds).ok();
114114 }
115115 Err(e) => {
116- state.set_error(format!("Erreur de synchronisation: {}", e));
116+ state.notification_manager.error(format!("Erreur de synchronisation: {}", e));
117117 }
118118 }
119119 }
@@ -77,7 +77,7 @@ fn draw_feed_item(state: &mut AppState, ui: &mut Ui, index: usize) {
77 state.feeds.remove(index);77 state.feeds.remove(index);
78 super::super::config::storage::save_feeds_default(&state.feeds).ok();78 super::super::config::storage::save_feeds_default(&state.feeds).ok();
79 log::info!("Feed deleted successfully");79 log::info!("Feed deleted successfully");
80- state.set_info("Flux supprimé".to_string());80+ state.notification_manager.success("Flux supprimé");
81 if state.current_feed == Some(index) {81 if state.current_feed == Some(index) {
82 state.select_feed(None);82 state.select_feed(None);
83 }83 }
@@ -92,12 +92,12 @@ fn draw_feed_item(state: &mut AppState, ui: &mut Ui, index: usize) {
92 /// Synchronizes a single feed92 /// Synchronizes a single feed
93 fn sync_feed(state: &mut AppState, index: usize) {93 fn sync_feed(state: &mut AppState, index: usize) {
94 if index >= state.feeds.len() {94 if index >= state.feeds.len() {
95- state.set_error("Index de flux invalide".to_string());95+ state.notification_manager.error("Index de flux invalide");
96 return;96 return;
97 }97 }
98 98
99 state.set_sync_state(true);99 state.set_sync_state(true);
100- state.set_info(format!("Synchronisation du flux: {}", state.feeds[index].name));100+ state.notification_manager.info(format!("Synchronisation du flux: {}", state.feeds[index].name));
101 101
102 log::info!("Starting synchronization of feed: {}", state.feeds[index].name);102 log::info!("Starting synchronization of feed: {}", state.feeds[index].name);
103 103
@@ -108,12 +108,12 @@ fn sync_feed(state: &mut AppState, index: usize) {
108 108
109 match result {109 match result {
110 Ok(episodes) => {110 Ok(episodes) => {
111- state.set_info(format!("Flux synchronisé: {} épisodes trouvés", episodes.len()));111+ state.notification_manager.success(format!("Flux synchronisé: {} épisodes trouvés", episodes.len()));
112 // Save the updated feeds112 // Save the updated feeds
113 super::super::config::storage::save_feeds_default(&state.feeds).ok();113 super::super::config::storage::save_feeds_default(&state.feeds).ok();
114 }114 }
115 Err(e) => {115 Err(e) => {
116- state.set_error(format!("Erreur de synchronisation: {}", e));116+ state.notification_manager.error(format!("Erreur de synchronisation: {}", e));
117 }117 }
118 }118 }
119 }119 }
modified src/ui/home.rs +4 -4
@@ -63,12 +63,12 @@ pub fn draw_feed_list(state: &mut AppState, ui: &mut Ui) {
6363 /// Synchronizes all feeds
6464 fn sync_all_feeds(state: &mut AppState) {
6565 if !state.has_feeds() {
66- state.set_info("Aucun flux à synchroniser".to_string());
66+ state.notification_manager.info("Aucun flux à synchroniser");
6767 return;
6868 }
6969
7070 state.set_sync_state(true);
71- state.set_info("Synchronisation de tous les flux...".to_string());
71+ state.notification_manager.info("Synchronisation de tous les flux...");
7272
7373 log::info!("Starting synchronization of all feeds");
7474
@@ -79,12 +79,12 @@ fn sync_all_feeds(state: &mut AppState) {
7979
8080 match result {
8181 Ok(success_count) => {
82- state.set_info(format!("{} flux synchronisés avec succès", success_count));
82+ state.notification_manager.success(format!("{} flux synchronisés avec succès", success_count));
8383 // Save the updated feeds
8484 super::super::config::storage::save_feeds_default(&state.feeds).ok();
8585 }
8686 Err(e) => {
87- state.set_error(format!("Erreur de synchronisation: {}", e));
87+ state.notification_manager.error(format!("Erreur de synchronisation: {}", e));
8888 }
8989 }
9090 }
@@ -63,12 +63,12 @@ pub fn draw_feed_list(state: &mut AppState, ui: &mut Ui) {
63 /// Synchronizes all feeds63 /// Synchronizes all feeds
64 fn sync_all_feeds(state: &mut AppState) {64 fn sync_all_feeds(state: &mut AppState) {
65 if !state.has_feeds() {65 if !state.has_feeds() {
66- state.set_info("Aucun flux à synchroniser".to_string());66+ state.notification_manager.info("Aucun flux à synchroniser");
67 return;67 return;
68 }68 }
69 69
70 state.set_sync_state(true);70 state.set_sync_state(true);
71- state.set_info("Synchronisation de tous les flux...".to_string());71+ state.notification_manager.info("Synchronisation de tous les flux...");
72 72
73 log::info!("Starting synchronization of all feeds");73 log::info!("Starting synchronization of all feeds");
74 74
@@ -79,12 +79,12 @@ fn sync_all_feeds(state: &mut AppState) {
79 79
80 match result {80 match result {
81 Ok(success_count) => {81 Ok(success_count) => {
82- state.set_info(format!("{} flux synchronisés avec succès", success_count));82+ state.notification_manager.success(format!("{} flux synchronisés avec succès", success_count));
83 // Save the updated feeds83 // Save the updated feeds
84 super::super::config::storage::save_feeds_default(&state.feeds).ok();84 super::super::config::storage::save_feeds_default(&state.feeds).ok();
85 }85 }
86 Err(e) => {86 Err(e) => {
87- state.set_error(format!("Erreur de synchronisation: {}", e));87+ state.notification_manager.error(format!("Erreur de synchronisation: {}", e));
88 }88 }
89 }89 }
90 }90 }
modified src/ui/mod.rs +1 -0
@@ -7,3 +7,4 @@ pub mod feed_list;
77 pub mod add_feed_dialog;
88 pub mod feed_edit_dialog;
99 pub mod status_bar;
10+pub mod notifications;
@@ -7,3 +7,4 @@ pub mod feed_list;
7 pub mod add_feed_dialog;7 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;
added src/ui/notifications.rs +222 -0
new file mode 100644
@@ -0,0 +1,222 @@
1+//! Notifications module for RSS Podcast Downloader
2+//!
3+//! This module handles displaying download notifications to the user.
4+
5+use egui::{Color32, Frame, RichText, Ui};
6+use std::collections::VecDeque;
7+use std::time::{Duration, Instant};
8+
9+/// A notification to display to the user
10+#[derive(Debug, Clone)]
11+pub struct Notification {
12+ /// The message to display
13+ pub message: String,
14+ /// The type/severity of the notification
15+ pub notification_type: NotificationType,
16+ /// When the notification was created
17+ pub created_at: Instant,
18+ /// Duration to display the notification (default: 5 seconds)
19+ pub duration: Duration,
20+}
21+
22+/// Types of notifications
23+#[derive(Debug, Clone, PartialEq)]
24+pub enum NotificationType {
25+ /// Informational message (blue)
26+ Info,
27+ /// Success message (green)
28+ Success,
29+ /// Warning message (yellow/orange)
30+ Warning,
31+ /// Error message (red)
32+ Error,
33+}
34+
35+impl Notification {
36+ /// Creates a new info notification
37+ pub fn info(message: impl Into<String>) -> Self {
38+ Self {
39+ message: message.into(),
40+ notification_type: NotificationType::Info,
41+ created_at: Instant::now(),
42+ duration: Duration::from_secs(5),
43+ }
44+ }
45+
46+ /// Creates a new success notification
47+ pub fn success(message: impl Into<String>) -> Self {
48+ Self {
49+ message: message.into(),
50+ notification_type: NotificationType::Success,
51+ created_at: Instant::now(),
52+ duration: Duration::from_secs(5),
53+ }
54+ }
55+
56+ /// Creates a new warning notification
57+ pub fn warning(message: impl Into<String>) -> Self {
58+ Self {
59+ message: message.into(),
60+ notification_type: NotificationType::Warning,
61+ created_at: Instant::now(),
62+ duration: Duration::from_secs(5),
63+ }
64+ }
65+
66+ /// Creates a new error notification
67+ pub fn error(message: impl Into<String>) -> Self {
68+ Self {
69+ message: message.into(),
70+ notification_type: NotificationType::Error,
71+ created_at: Instant::now(),
72+ duration: Duration::from_secs(7), // Longer for errors
73+ }
74+ }
75+
76+ /// Returns true if the notification should still be displayed
77+ pub fn is_visible(&self) -> bool {
78+ self.created_at.elapsed() < self.duration
79+ }
80+
81+ /// Returns the color for this notification type
82+ pub fn color(&self) -> Color32 {
83+ match self.notification_type {
84+ NotificationType::Info => Color32::from_rgb(70, 130, 180), // Steel blue
85+ NotificationType::Success => Color32::from_rgb(34, 139, 34), // Forest green
86+ NotificationType::Warning => Color32::from_rgb(255, 165, 0), // Orange
87+ NotificationType::Error => Color32::from_rgb(220, 53, 69), // Red
88+ }
89+ }
90+
91+ /// Returns the icon for this notification type
92+ pub fn icon(&self) -> &'static str {
93+ match self.notification_type {
94+ NotificationType::Info => "ℹ️",
95+ NotificationType::Success => "✅",
96+ NotificationType::Warning => "⚠️",
97+ NotificationType::Error => "❌",
98+ }
99+ }
100+}
101+
102+/// Notification manager that holds active notifications
103+#[derive(Debug, Default)]
104+pub struct NotificationManager {
105+ notifications: VecDeque<Notification>,
106+}
107+
108+impl NotificationManager {
109+ /// Creates a new notification manager
110+ pub fn new() -> Self {
111+ Self::default()
112+ }
113+
114+ /// Adds a new notification
115+ pub fn add_notification(&mut self, notification: Notification) {
116+ // Remove expired notifications first
117+ self.cleanup();
118+ self.notifications.push_back(notification);
119+ }
120+
121+ /// Adds an info notification
122+ pub fn info(&mut self, message: impl Into<String>) {
123+ self.add_notification(Notification::info(message));
124+ }
125+
126+ /// Adds a success notification
127+ pub fn success(&mut self, message: impl Into<String>) {
128+ self.add_notification(Notification::success(message));
129+ }
130+
131+ /// Adds a warning notification
132+ pub fn warning(&mut self, message: impl Into<String>) {
133+ self.add_notification(Notification::warning(message));
134+ }
135+
136+ /// Adds an error notification
137+ pub fn error(&mut self, message: impl Into<String>) {
138+ self.add_notification(Notification::error(message));
139+ }
140+
141+ /// Removes expired notifications
142+ pub fn cleanup(&mut self) {
143+ self.notifications.retain(|n| n.is_visible());
144+ }
145+
146+ /// Returns the list of active notifications
147+ pub fn get_active_notifications(&self) -> Vec<&Notification> {
148+ self.notifications.iter().filter(|n| n.is_visible()).collect()
149+ }
150+
151+ /// Returns the number of active notifications
152+ pub fn active_count(&self) -> usize {
153+ self.notifications.iter().filter(|n| n.is_visible()).count()
154+ }
155+
156+ /// Draws all active notifications in the UI
157+ pub fn draw(&mut self, ui: &mut Ui) {
158+ self.cleanup();
159+
160+ let active_notifications = self.get_active_notifications();
161+
162+ if active_notifications.is_empty() {
163+ return;
164+ }
165+
166+ // Draw notifications in a vertical stack
167+ egui::ScrollArea::vertical().show(ui, |ui| {
168+ for notification in active_notifications {
169+ let progress = notification.created_at.elapsed().as_secs_f32() / notification.duration.as_secs_f32();
170+
171+ let frame = Frame {
172+ fill: notification.color().linear_multiply(0.2),
173+ rounding: egui::Rounding::same(5.0),
174+ inner_margin: egui::Margin::symmetric(10.0, 5.0),
175+ ..Default::default()
176+ };
177+
178+ frame.show(ui, |ui| {
179+ ui.horizontal(|ui| {
180+ // Icon
181+ ui.label(RichText::new(notification.icon()).color(notification.color()));
182+ ui.add_space(8.0);
183+
184+ // Message
185+ ui.label(RichText::new(&notification.message).color(Color32::WHITE));
186+
187+ // Progress bar (optional) - shows time remaining
188+ if notification.duration.as_secs() > 0 {
189+ ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
190+ let bar_width = 100.0;
191+ let bar_height = 4.0;
192+ let fill_width = bar_width * (1.0 - progress.clamp(0.0, 1.0));
193+
194+ ui.add_space(10.0);
195+ // Background
196+ let (_, bg_rect) = ui.allocate_space(egui::vec2(bar_width, bar_height));
197+ ui.painter().rect_filled(
198+ bg_rect,
199+ 5.0,
200+ Color32::from_black_alpha(128)
201+ );
202+ // Foreground (remaining time)
203+ let fg_rect = egui::Rect {
204+ min: bg_rect.min,
205+ max: egui::Pos2::new(
206+ bg_rect.min.x + fill_width,
207+ bg_rect.max.y
208+ )
209+ };
210+ ui.painter().rect_filled(
211+ fg_rect,
212+ 5.0,
213+ notification.color()
214+ );
215+ });
216+ }
217+ });
218+ });
219+ }
220+ });
221+ }
222+}
new file mode 100644
@@ -0,0 +1,222 @@
1+//! Notifications module for RSS Podcast Downloader
2+//!
3+//! This module handles displaying download notifications to the user.
4+
5+use egui::{Color32, Frame, RichText, Ui};
6+use std::collections::VecDeque;
7+use std::time::{Duration, Instant};
8+
9+/// A notification to display to the user
10+#[derive(Debug, Clone)]
11+pub struct Notification {
12+ /// The message to display
13+ pub message: String,
14+ /// The type/severity of the notification
15+ pub notification_type: NotificationType,
16+ /// When the notification was created
17+ pub created_at: Instant,
18+ /// Duration to display the notification (default: 5 seconds)
19+ pub duration: Duration,
20+}
21+
22+/// Types of notifications
23+#[derive(Debug, Clone, PartialEq)]
24+pub enum NotificationType {
25+ /// Informational message (blue)
26+ Info,
27+ /// Success message (green)
28+ Success,
29+ /// Warning message (yellow/orange)
30+ Warning,
31+ /// Error message (red)
32+ Error,
33+}
34+
35+impl Notification {
36+ /// Creates a new info notification
37+ pub fn info(message: impl Into<String>) -> Self {
38+ Self {
39+ message: message.into(),
40+ notification_type: NotificationType::Info,
41+ created_at: Instant::now(),
42+ duration: Duration::from_secs(5),
43+ }
44+ }
45+
46+ /// Creates a new success notification
47+ pub fn success(message: impl Into<String>) -> Self {
48+ Self {
49+ message: message.into(),
50+ notification_type: NotificationType::Success,
51+ created_at: Instant::now(),
52+ duration: Duration::from_secs(5),
53+ }
54+ }
55+
56+ /// Creates a new warning notification
57+ pub fn warning(message: impl Into<String>) -> Self {
58+ Self {
59+ message: message.into(),
60+ notification_type: NotificationType::Warning,
61+ created_at: Instant::now(),
62+ duration: Duration::from_secs(5),
63+ }
64+ }
65+
66+ /// Creates a new error notification
67+ pub fn error(message: impl Into<String>) -> Self {
68+ Self {
69+ message: message.into(),
70+ notification_type: NotificationType::Error,
71+ created_at: Instant::now(),
72+ duration: Duration::from_secs(7), // Longer for errors
73+ }
74+ }
75+
76+ /// Returns true if the notification should still be displayed
77+ pub fn is_visible(&self) -> bool {
78+ self.created_at.elapsed() < self.duration
79+ }
80+
81+ /// Returns the color for this notification type
82+ pub fn color(&self) -> Color32 {
83+ match self.notification_type {
84+ NotificationType::Info => Color32::from_rgb(70, 130, 180), // Steel blue
85+ NotificationType::Success => Color32::from_rgb(34, 139, 34), // Forest green
86+ NotificationType::Warning => Color32::from_rgb(255, 165, 0), // Orange
87+ NotificationType::Error => Color32::from_rgb(220, 53, 69), // Red
88+ }
89+ }
90+
91+ /// Returns the icon for this notification type
92+ pub fn icon(&self) -> &'static str {
93+ match self.notification_type {
94+ NotificationType::Info => "ℹ️",
95+ NotificationType::Success => "✅",
96+ NotificationType::Warning => "⚠️",
97+ NotificationType::Error => "❌",
98+ }
99+ }
100+}
101+
102+/// Notification manager that holds active notifications
103+#[derive(Debug, Default)]
104+pub struct NotificationManager {
105+ notifications: VecDeque<Notification>,
106+}
107+
108+impl NotificationManager {
109+ /// Creates a new notification manager
110+ pub fn new() -> Self {
111+ Self::default()
112+ }
113+
114+ /// Adds a new notification
115+ pub fn add_notification(&mut self, notification: Notification) {
116+ // Remove expired notifications first
117+ self.cleanup();
118+ self.notifications.push_back(notification);
119+ }
120+
121+ /// Adds an info notification
122+ pub fn info(&mut self, message: impl Into<String>) {
123+ self.add_notification(Notification::info(message));
124+ }
125+
126+ /// Adds a success notification
127+ pub fn success(&mut self, message: impl Into<String>) {
128+ self.add_notification(Notification::success(message));
129+ }
130+
131+ /// Adds a warning notification
132+ pub fn warning(&mut self, message: impl Into<String>) {
133+ self.add_notification(Notification::warning(message));
134+ }
135+
136+ /// Adds an error notification
137+ pub fn error(&mut self, message: impl Into<String>) {
138+ self.add_notification(Notification::error(message));
139+ }
140+
141+ /// Removes expired notifications
142+ pub fn cleanup(&mut self) {
143+ self.notifications.retain(|n| n.is_visible());
144+ }
145+
146+ /// Returns the list of active notifications
147+ pub fn get_active_notifications(&self) -> Vec<&Notification> {
148+ self.notifications.iter().filter(|n| n.is_visible()).collect()
149+ }
150+
151+ /// Returns the number of active notifications
152+ pub fn active_count(&self) -> usize {
153+ self.notifications.iter().filter(|n| n.is_visible()).count()
154+ }
155+
156+ /// Draws all active notifications in the UI
157+ pub fn draw(&mut self, ui: &mut Ui) {
158+ self.cleanup();
159+
160+ let active_notifications = self.get_active_notifications();
161+
162+ if active_notifications.is_empty() {
163+ return;
164+ }
165+
166+ // Draw notifications in a vertical stack
167+ egui::ScrollArea::vertical().show(ui, |ui| {
168+ for notification in active_notifications {
169+ let progress = notification.created_at.elapsed().as_secs_f32() / notification.duration.as_secs_f32();
170+
171+ let frame = Frame {
172+ fill: notification.color().linear_multiply(0.2),
173+ rounding: egui::Rounding::same(5.0),
174+ inner_margin: egui::Margin::symmetric(10.0, 5.0),
175+ ..Default::default()
176+ };
177+
178+ frame.show(ui, |ui| {
179+ ui.horizontal(|ui| {
180+ // Icon
181+ ui.label(RichText::new(notification.icon()).color(notification.color()));
182+ ui.add_space(8.0);
183+
184+ // Message
185+ ui.label(RichText::new(&notification.message).color(Color32::WHITE));
186+
187+ // Progress bar (optional) - shows time remaining
188+ if notification.duration.as_secs() > 0 {
189+ ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
190+ let bar_width = 100.0;
191+ let bar_height = 4.0;
192+ let fill_width = bar_width * (1.0 - progress.clamp(0.0, 1.0));
193+
194+ ui.add_space(10.0);
195+ // Background
196+ let (_, bg_rect) = ui.allocate_space(egui::vec2(bar_width, bar_height));
197+ ui.painter().rect_filled(
198+ bg_rect,
199+ 5.0,
200+ Color32::from_black_alpha(128)
201+ );
202+ // Foreground (remaining time)
203+ let fg_rect = egui::Rect {
204+ min: bg_rect.min,
205+ max: egui::Pos2::new(
206+ bg_rect.min.x + fill_width,
207+ bg_rect.max.y
208+ )
209+ };
210+ ui.painter().rect_filled(
211+ fg_rect,
212+ 5.0,
213+ notification.color()
214+ );
215+ });
216+ }
217+ });
218+ });
219+ }
220+ });
221+ }
222+}