@@ -460,27 +460,32 @@ if (modeMenuBtn && modeMenu) {
460460 modeMenu . hidden = true ;
461461 modeMenuBtn . setAttribute ( 'aria-expanded' , 'false' ) ;
462462 } ) ;
463- window . addEventListener ( 'keydown' , ( ev ) => {
464- if ( ev . key === 'Escape' && ! modeMenu . hidden ) {
465- modeMenu . hidden = true ; modeMenuBtn . setAttribute ( 'aria-expanded' , 'false' ) ;
463+ }
464+
465+ function startUpdateTimer ( ) {
466+ const timer = document . querySelector ( '.update-timer' ) ;
467+ if ( ! timer ) return ;
468+ // Ne pas réinitialiser si déjà en cours
469+ if ( updateTimerStart === null ) updateTimerStart = Date . now ( ) ;
470+ if ( updateTimerInterval ) return ;
471+ const updateTimerText = ( ) => {
472+ const elapsed = Math . max ( 0 , Math . floor ( ( Date . now ( ) - updateTimerStart ) / 1000 ) ) ;
473+ if ( elapsed < 60 ) {
474+ timer . textContent = `${ elapsed } s` ;
475+ } else {
476+ const min = Math . floor ( elapsed / 60 ) ;
477+ const sec = String ( elapsed % 60 ) . padStart ( 2 , '0' ) ;
478+ timer . textContent = `${ min } :${ sec } ` ;
466479 }
467- } ) ;
468- modeMenu . addEventListener ( 'click' , ( ev ) => {
469- const opt = ev . target . closest ( '.mode-option' ) ;
470- if ( ! opt ) return ;
471- const mode = opt . getAttribute ( 'data-mode' ) ;
472- if ( ! mode || mode === state . viewMode ) { modeMenu . hidden = true ; modeMenuBtn . setAttribute ( 'aria-expanded' , 'false' ) ; return ; }
473- if ( ! [ 'grid' , 'list' , 'icons' , 'cards' ] . includes ( mode ) ) return ;
474- state . viewMode = mode ;
475- localStorage . setItem ( 'viewMode' , state . viewMode ) ;
476- currentViewMode = state . viewMode ;
477- updateModeMenuUI ( ) ;
478- rerenderActiveCategory ( ) ;
479- // Remettre le scroll en haut à chaque changement de mode
480- if ( scrollShell ) scrollShell . scrollTop = 0 ;
481- modeMenu . hidden = true ;
482- modeMenuBtn . setAttribute ( 'aria-expanded' , 'false' ) ;
483- } ) ;
480+ } ;
481+ updateTimerText ( ) ;
482+ updateTimerInterval = setInterval ( updateTimerText , 1000 ) ;
483+ }
484+
485+ function stopUpdateTimer ( ) {
486+ if ( updateTimerInterval ) clearInterval ( updateTimerInterval ) ;
487+ updateTimerInterval = null ;
488+ updateTimerStart = null ;
484489}
485490
486491updateModeMenuUI ( ) ;
@@ -2594,7 +2599,8 @@ tabs.forEach(tab => {
25942599 setUpdateSpinnerBusy ( false ) ;
25952600 }
25962601 } else {
2597- setUpdateSpinnerBusy ( false ) ;
2602+ // Conserver l'état du spinner si une mise à jour tourne encore
2603+ setUpdateSpinnerBusy ( updateInProgress ) ;
25982604 }
25992605 // Pas de terminal dans le mode avancé désormais
26002606 if ( document . body . classList . contains ( 'details-mode' ) ) {
@@ -2612,20 +2618,70 @@ tabs.forEach(tab => {
26122618function hasUpdatesStreamingSupport ( ) {
26132619 return ! ! ( window . electronAPI ?. startUpdates && window . electronAPI ?. onUpdatesProgress ) ;
26142620}
2621+ let updateTimerInterval = null ;
2622+ let updateTimerStart = null ;
26152623
26162624function setUpdateSpinnerBusy ( isBusy ) {
26172625 if ( ! updateSpinner ) return ;
26182626 updateSpinnerBusy = ! ! isBusy ;
26192627 updateSpinner . setAttribute ( 'data-busy' , updateSpinnerBusy ? 'true' : 'false' ) ;
2628+ const hourglass = updateSpinner . querySelector ( '.update-hourglass' ) ;
2629+ const timer = updateSpinner . querySelector ( '.update-timer' ) ;
26202630 const label = updateSpinner . querySelector ( '.spinner-label' ) ;
2621- if ( label ) label . textContent = t ( updateSpinnerBusy ? 'updates.loading' : 'updates.ready' ) ;
2631+ if ( runUpdatesBtn ) runUpdatesBtn . classList . toggle ( 'loading' , updateSpinnerBusy ) ;
2632+ if ( updateSpinnerBusy ) {
2633+ if ( hourglass ) hourglass . style . display = 'inline-block' ;
2634+ if ( timer ) timer . style . display = 'inline-block' ;
2635+ if ( label ) {
2636+ label . textContent = t ( 'updates.loading' ) ;
2637+ label . style . display = '' ;
2638+ }
2639+ startUpdateTimer ( ) ;
2640+ } else {
2641+ if ( hourglass ) hourglass . style . display = 'none' ;
2642+ if ( timer ) timer . style . display = 'none' ;
2643+ if ( label ) {
2644+ label . textContent = '' ;
2645+ label . style . display = 'none' ;
2646+ }
2647+ stopUpdateTimer ( ) ;
2648+ }
2649+ }
2650+
2651+ function startUpdateTimer ( ) {
2652+ const timer = document . querySelector ( '.update-timer' ) ;
2653+ if ( ! timer ) return ;
2654+ // Ne pas réinitialiser si déjà en cours
2655+ if ( updateTimerStart === null ) updateTimerStart = Date . now ( ) ;
2656+ if ( updateTimerInterval ) return ;
2657+ const updateTimerText = ( ) => {
2658+ const elapsed = Math . max ( 0 , Math . floor ( ( Date . now ( ) - updateTimerStart ) / 1000 ) ) ;
2659+ if ( elapsed < 60 ) {
2660+ timer . textContent = `${ elapsed } s` ;
2661+ } else {
2662+ const min = Math . floor ( elapsed / 60 ) ;
2663+ const sec = String ( elapsed % 60 ) . padStart ( 2 , '0' ) ;
2664+ timer . textContent = `${ min } :${ sec } ` ;
2665+ }
2666+ } ;
2667+ updateTimerText ( ) ;
2668+ updateTimerInterval = setInterval ( updateTimerText , 1000 ) ;
2669+ }
2670+
2671+ function stopUpdateTimer ( ) {
2672+ if ( updateTimerInterval ) clearInterval ( updateTimerInterval ) ;
2673+ updateTimerInterval = null ;
2674+ updateTimerStart = null ;
26222675}
26232676
26242677function updateUpdatesToggleUi ( ) {
26252678 if ( ! updatesToggleBtn ) return ;
2626- const key = updatesTerminalExpanded ? 'updates.toggleHide' : 'updates.toggleShow' ;
2627- updatesToggleBtn . textContent = t ( key ) ;
26282679 updatesToggleBtn . setAttribute ( 'aria-expanded' , updatesTerminalExpanded ? 'true' : 'false' ) ;
2680+ const section = document . getElementById ( 'updatesLogSection' ) ;
2681+ if ( section ) section . setAttribute ( 'data-open' , updatesTerminalExpanded ? 'true' : 'false' ) ;
2682+ // Mettre à jour la flèche
2683+ const caret = updatesToggleBtn . querySelector ( '.updates-log-caret' ) ;
2684+ if ( caret ) caret . textContent = updatesTerminalExpanded ? '▾' : '▸' ;
26292685}
26302686
26312687function applyUpdatesTerminalVisibility ( ) {
@@ -2849,10 +2905,13 @@ function handleUpdateCompletion(fullText){
28492905 }
28502906 const updated = parseUpdatedApps ( sanitized ) ;
28512907 const nothingPhrase = / N o t h i n g t o d o h e r e ! ? / i. test ( sanitized || '' ) ;
2852- let toShow = updated ;
2908+ let toShow = new Set ( ) ;
2909+ // Si on a trouvé la section officielle, on ne montre QUE ces apps
28532910 if ( filteredUpdated && filteredUpdated . size > 0 ) {
2854- // Ne garder que les apps détectées ET listées dans la section
2855- toShow = new Set ( [ ...updated ] . filter ( x => filteredUpdated . has ( x ) ) ) ;
2911+ toShow = filteredUpdated ;
2912+ } else if ( ! nothingPhrase && updated . size > 0 ) {
2913+ // Sinon fallback sur le parsing ligne par ligne, seulement si pas de message "rien à faire"
2914+ toShow = updated ;
28562915 }
28572916 if ( toShow . size > 0 ) {
28582917 if ( updateFinalMessage ) updateFinalMessage . textContent = t ( 'updates.updatedApps' ) ;
@@ -2950,9 +3009,11 @@ runUpdatesBtn?.addEventListener('click', async () => {
29503009 const result = await fetchUpdatesOutput ( ) ;
29513010 const raw = typeof result ?. output === 'string' ? result . output : '' ;
29523011 handleUpdateCompletion ( raw ) ;
2953- await refreshAfterUpdates ( ) ;
29543012 const dur = Math . round ( ( performance . now ( ) - start ) / 1000 ) ;
29553013 if ( updateFinalMessage && updateFinalMessage . textContent ) updateFinalMessage . textContent += t ( 'updates.duration' , { dur} ) ;
3014+ // Arrêter le spinner dès que le résultat est prêt, avant le refresh lourd
3015+ setUpdateSpinnerBusy ( false ) ;
3016+ await refreshAfterUpdates ( ) ;
29563017 } catch ( err ) {
29573018 console . error ( 'Updates failed' , err ) ;
29583019 showToast ( t ( 'toast.updateFailed' ) || 'Échec des mises à jour' ) ;
@@ -3303,6 +3364,54 @@ function showPopupWarning(msg) {
33033364 modal . style . display = 'block' ;
33043365 }
33053366}
3367+
3368+ // Gestion de la confirmation de fermeture avec installation en cours
3369+ const closeConfirmModal = document . getElementById ( 'closeConfirmModal' ) ;
3370+ const closeConfirmStay = document . getElementById ( 'closeConfirmStay' ) ;
3371+ const closeConfirmQuit = document . getElementById ( 'closeConfirmQuit' ) ;
3372+
3373+ function showCloseConfirm ( ) {
3374+ if ( ! closeConfirmModal ) return ;
3375+ closeConfirmModal . hidden = false ;
3376+ applyTranslations ( ) ; // Mettre à jour les textes traduits
3377+ if ( closeConfirmQuit ) closeConfirmQuit . focus ( ) ;
3378+ }
3379+
3380+ function hideCloseConfirm ( ) {
3381+ if ( ! closeConfirmModal ) return ;
3382+ closeConfirmModal . hidden = true ;
3383+ }
3384+
3385+ closeConfirmStay ?. addEventListener ( 'click' , ( ) => {
3386+ hideCloseConfirm ( ) ;
3387+ } ) ;
3388+
3389+ closeConfirmQuit ?. addEventListener ( 'click' , async ( ) => {
3390+ hideCloseConfirm ( ) ;
3391+ // Annuler l'installation en cours
3392+ if ( activeInstallSession && ! activeInstallSession . done ) {
3393+ try {
3394+ await cancelActiveInstall ( ) ;
3395+ } catch ( err ) {
3396+ console . error ( 'Failed to cancel installation' , err ) ;
3397+ }
3398+ }
3399+ // Fermer l'application
3400+ if ( window . electronAPI ?. closeWindow ) {
3401+ window . electronAPI . closeWindow ( ) ;
3402+ }
3403+ } ) ;
3404+
3405+ // Intercepter la tentative de fermeture
3406+ if ( window . electronAPI ?. onBeforeClose ) {
3407+ window . electronAPI . onBeforeClose ( ( ) => {
3408+ // Vérifier si une installation est en cours
3409+ if ( activeInstallSession && activeInstallSession . id && ! activeInstallSession . done ) {
3410+ showCloseConfirm ( ) ;
3411+ }
3412+ } ) ;
3413+ }
3414+
33063415//# sourceMappingURL=app.js.map
33073416
33083417
0 commit comments