modified src/render.rs +3 -0
| @@ -61,4 +61,7 @@ pub struct Shell { |
| 61 | 61 | /// Mes équipes pour le sélecteur de la pill d'en-tête — vide si je n'en |
| 62 | 62 | /// ai qu'une (pill inactive, pas de sheet). |
| 63 | 63 | pub teams: Vec<TeamChoiceView>, |
| 64 | + /// Pastille sur l'icône de l'app (Badging API) tant qu'un timer ou une |
| 65 | + /// mesure existe — indépendant de l'affichage de la pill flottante. |
| 66 | + pub badge: bool, |
| 64 | 67 | } |
| @@ -61,4 +61,7 @@ pub struct Shell { |
| 61 | /// Mes équipes pour le sélecteur de la pill d'en-tête — vide si je n'en | 61 | /// Mes équipes pour le sélecteur de la pill d'en-tête — vide si je n'en |
| 62 | /// ai qu'une (pill inactive, pas de sheet). | 62 | /// ai qu'une (pill inactive, pas de sheet). |
| 63 | pub teams: Vec<TeamChoiceView>, | 63 | pub teams: Vec<TeamChoiceView>, |
| | 64 | + /// Pastille sur l'icône de l'app (Badging API) tant qu'un timer ou une |
| | 65 | + /// mesure existe — indépendant de l'affichage de la pill flottante. |
| | 66 | + pub badge: bool, |
| 64 | } | 67 | } |
modified src/routes/mod.rs +4 -1
| @@ -122,7 +122,9 @@ pub async fn shell( |
| 122 | 122 | .await? |
| 123 | 123 | .unwrap_or_default(); |
| 124 | 124 | let invite_count = invitations::count_for(&state.pool, ctx.user.id).await?; |
| 125 | | - let timer = timers::get(&state.pool, ctx.user.id).await?.map(|t| { |
| 125 | + let timer_row = timers::get(&state.pool, ctx.user.id).await?; |
| 126 | + let badge = timer_row.is_some(); |
| 127 | + let timer = timer_row.map(|t| { |
| 126 | 128 | let foreign = t.team_id != ctx.team_id; |
| 127 | 129 | match t.measured_minutes() { |
| 128 | 130 | Some(m) => TimerView { |
| @@ -165,6 +167,7 @@ pub async fn shell( |
| 165 | 167 | }, |
| 166 | 168 | timer, |
| 167 | 169 | show_start: false, |
| 170 | + badge, |
| 168 | 171 | teams: { |
| 169 | 172 | let mine = teams_db::teams_of(&state.pool, ctx.user.id).await?; |
| 170 | 173 | if mine.len() > 1 { |
| @@ -122,7 +122,9 @@ pub async fn shell( |
| 122 | .await? | 122 | .await? |
| 123 | .unwrap_or_default(); | 123 | .unwrap_or_default(); |
| 124 | let invite_count = invitations::count_for(&state.pool, ctx.user.id).await?; | 124 | let invite_count = invitations::count_for(&state.pool, ctx.user.id).await?; |
| 125 | - let timer = timers::get(&state.pool, ctx.user.id).await?.map(|t| { | 125 | + let timer_row = timers::get(&state.pool, ctx.user.id).await?; |
| | 126 | + let badge = timer_row.is_some(); |
| | 127 | + let timer = timer_row.map(|t| { |
| 126 | let foreign = t.team_id != ctx.team_id; | 128 | let foreign = t.team_id != ctx.team_id; |
| 127 | match t.measured_minutes() { | 129 | match t.measured_minutes() { |
| 128 | Some(m) => TimerView { | 130 | Some(m) => TimerView { |
| @@ -165,6 +167,7 @@ pub async fn shell( |
| 165 | }, | 167 | }, |
| 166 | timer, | 168 | timer, |
| 167 | show_start: false, | 169 | show_start: false, |
| | 170 | + badge, |
| 168 | teams: { | 171 | teams: { |
| 169 | let mine = teams_db::teams_of(&state.pool, ctx.user.id).await?; | 172 | let mine = teams_db::teams_of(&state.pool, ctx.user.id).await?; |
| 170 | if mine.len() > 1 { | 173 | if mine.len() > 1 { |
modified templates/app.html +14 -0
| @@ -110,6 +110,20 @@ |
| 110 | 110 | </a> |
| 111 | 111 | </nav> |
| 112 | 112 | <script> |
| 113 | +// Pastille sur l'icône de l'app tant qu'un timer ou une mesure existe. |
| 114 | +// iOS exige la permission notifications : demandée une seule fois, dans le |
| 115 | +// geste « Lancer le minuteur » (Safari refuse les demandes hors interaction). |
| 116 | +(function () { |
| 117 | + if (!('setAppBadge' in navigator)) return; |
| 118 | + navigator.{% if shell.badge %}setAppBadge(1){% else %}clearAppBadge(){% endif %}.catch(function () {}); |
| 119 | + var start = document.querySelector('form[action="/timer/demarrer"]'); |
| 120 | + if (start && 'Notification' in window && Notification.permission === 'default') { |
| 121 | + start.addEventListener('submit', function (e) { |
| 122 | + e.preventDefault(); |
| 123 | + Notification.requestPermission().catch(function () {}).finally(function () { start.submit(); }); |
| 124 | + }, { once: true }); |
| 125 | + } |
| 126 | +})(); |
| 113 | 127 | // Amélioration progressive : le temps écoulé rendu par le serveur reste |
| 114 | 128 | // juste sans JS, ceci le rafraîchit toutes les 10 s sans recharger. |
| 115 | 129 | (function () { |
| @@ -110,6 +110,20 @@ |
| 110 | </a> | 110 | </a> |
| 111 | </nav> | 111 | </nav> |
| 112 | <script> | 112 | <script> |
| | 113 | +// Pastille sur l'icône de l'app tant qu'un timer ou une mesure existe. |
| | 114 | +// iOS exige la permission notifications : demandée une seule fois, dans le |
| | 115 | +// geste « Lancer le minuteur » (Safari refuse les demandes hors interaction). |
| | 116 | +(function () { |
| | 117 | + if (!('setAppBadge' in navigator)) return; |
| | 118 | + navigator.{% if shell.badge %}setAppBadge(1){% else %}clearAppBadge(){% endif %}.catch(function () {}); |
| | 119 | + var start = document.querySelector('form[action="/timer/demarrer"]'); |
| | 120 | + if (start && 'Notification' in window && Notification.permission === 'default') { |
| | 121 | + start.addEventListener('submit', function (e) { |
| | 122 | + e.preventDefault(); |
| | 123 | + Notification.requestPermission().catch(function () {}).finally(function () { start.submit(); }); |
| | 124 | + }, { once: true }); |
| | 125 | + } |
| | 126 | +})(); |
| 113 | // Amélioration progressive : le temps écoulé rendu par le serveur reste | 127 | // Amélioration progressive : le temps écoulé rendu par le serveur reste |
| 114 | // juste sans JS, ceci le rafraîchit toutes les 10 s sans recharger. | 128 | // juste sans JS, ceci le rafraîchit toutes les 10 s sans recharger. |
| 115 | (function () { | 129 | (function () { |