Konubinix' opinionated web of thoughts

Interval Timer

Evergreen

Useful for kung fu training.

pour se brosser les dents

Adding wakelock support and offline mode

https://konubinix.eu/ipfs/bafybeiexbmasbybxs23rd2djeghls3i2guzsbugr3szni76oinz6tkjt7u

https://konubinix.eu/ipns/k51qzi5uqu5dmddig1cynyicnt0upc70dyh1zfeh5khdsrpkircc6717afabrz

From the original.

ipfs get https://konubinix.eu/ipfs/bafybeiapftob6e7j7pjv72auxamh522sivapb6fq2tx3yjuvmsmeertr5i -o /tmp/brush
Saving file(s) to /tmp/brush

To do so, I changed the service workers to cache the data

// taken from https://googlechrome.github.io/samples/service-worker/basic/

const PRECACHE = 'precache-v1';
const RUNTIME = 'runtime';

// A list of local resources we always want to be cached.
const PRECACHE_URLS = [
    'index.html',
    'js/main.js',
    "img/icon.png",
    "img/icon-48x48.png",
    "img/icon-96x96.png",
    "img/icon-144x144.png",
    "img/icon-192x192.png",
    "img/icon-512x512.png",
    "manifest.json",
    "css/framework.min.css",
    "sound/alert.ogg",
    "sound/end.ogg",
    "sound/gong.ogg",
    "sound/gong2.ogg",
];


self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(PRECACHE)
            .then(cache => cache.addAll(PRECACHE_URLS))
            .then(self.skipWaiting())
    );
});

// clean up old caches
self.addEventListener('activate', event => {
    const currentCaches = [PRECACHE, RUNTIME];
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return cacheNames.filter(cacheName => !currentCaches.includes(cacheName));
        }).then(cachesToDelete => {
            return Promise.all(cachesToDelete.map(cacheToDelete => {
                return caches.delete(cacheToDelete);
            }));
        }).then(() => self.clients.claim())
    );
});

self.addEventListener('fetch', event => {
    // Skip cross-origin requests, like those for Google Analytics.
    if (event.request.url.startsWith(self.location.origin)) {
        event.respondWith(
            caches.match(event.request).then(cachedResponse => {
                if (cachedResponse) {
                    return cachedResponse;
                }

                return caches.open(RUNTIME).then(cache => {
                    return fetch(event.request).then(response => {
                        // Put a copy of the response in the runtime cache.
                        return cache.put(event.request, response.clone()).then(() => {
                            return response;
                        });
                    });
                });
            })
        );
    }
});

And I registered it in the main.js application. Also I changed a bit the logic and the naming to indicate that it was about brushing the teeths.

document.addEventListener('DOMContentLoaded', function () {
    app.init();
});

var app = {

    play: document.getElementById('play'),
    pause: document.getElementById('pause'),
    reset: document.getElementById('reset'),
    setDiv: document.getElementById('setDiv'),
    chronoDiv: document.getElementById('chronoDiv'),
    setsValue: document.getElementById('setsValue'),
    workValue: document.getElementById('workValue'),
    workValueInt: 20,
    workFinish: 0,
    restValue: document.getElementById('restValue'),
    restValueInt: 10,
    timeReset: 3000,
    positionNavigator: 0,
    state: 1, // 1 -> config, 2 -> chrono
    stateChrono: 1,  // 1 -> rest, 2 -> work
    cycle: document.getElementById('cycle'),
    timeRest: document.getElementById('timeRest'),
    timeRestInt: 0,
    textChrono: document.getElementById('textChrono'),
    interval: null,
    intervalButton: null,
    audio: new Audio(),
    isKaiOS: false,
    isPaused: false,
    modalPause: document.getElementById('modalPause'),
    modalReset: document.getElementById('modalReset'),
    noSleepVideo: document.getElementById('noSleepVideo'),

    wakelock: null,

    workTimeElement: document.getElementById('workTime'),
    workTime: 0,

    playSound: function(srcSound) {
        app.audio.src = "./sound/" + srcSound + ".ogg";
        app.audio.play();
    },

    init: function() {
        app.noSleepVideo.pause();
        document.getElementById("setsLess").focus();
        document.addEventListener('keydown', app.manejarTeclado);

        app.play.addEventListener('click', app.initChrono);
        app.reset.addEventListener('click', app.resetChrono);
        app.pause.addEventListener('click', app.pauseChrono);

        let classnameLess = document.getElementsByClassName('lessBtn');
        for (var i = 0; i < classnameLess.length; i++) {
            classnameLess[i].addEventListener('click', app.lessValue);
            classnameLess[i].addEventListener('mousedown', app.lessValueCont);
            classnameLess[i].addEventListener('touchstart', app.lessValueCont);
            classnameLess[i].addEventListener('mouseup', app.contEnd);
            classnameLess[i].addEventListener('touchend', app.contEnd);
        }

        let classnameMore = document.getElementsByClassName('moreBtn');
        for (var i = 0; i < classnameMore.length; i++) {
            classnameMore[i].addEventListener('click', app.moreValue);
            classnameMore[i].addEventListener('mousedown', app.moreValueCont);
            classnameMore[i].addEventListener('touchstart', app.moreValueCont);
            classnameMore[i].addEventListener('mouseup', app.contEnd);
            classnameMore[i].addEventListener('touchend', app.contEnd);
        }

        app.updateWorkTime();

        if ('serviceWorker' in navigator) {
            navigator.serviceWorker
                .register('./sw.js')
                .then(function() {
                    console.log('Service Worker Registered');
                });
        }
    },

    initChrono: function() {
        app.noSleepVideo.play();
        setDiv.classList.add("hide");
        chronoDiv.classList.remove("hide");
        app.state = 2;
        app.positionNavigator = 0;
        /* TODO refactorizar */
        document.getElementsByClassName('nv-select')[0].classList.remove('nv-select');
        document.getElementsByClassName('navigatorChrono')[0].classList.add('nv-select');
        /* TODO refactorizar */
        cycle.innerText = (app.workFinish + 1) + "/" + app.setsValue.innerText;
        //app.timeRest.innerText =  10; // app.restValue.innerText;
        document.getElementById('principalDiv').classList.add('rest');

        //init clock
        app.textChrono.innerText = "Attends"; /* TODO refactorizar */
        /* TODO refactorizar */
        document.getElementsByTagName("html")[0].classList.add('rest');
        document.getElementsByClassName("jumbotron")[0].classList.add('rest');
        /* TODO refactorizar */
        let restTime = 5; //app.restValue.innerText;
        app.timeRestInt = restTime;
        app.timeRest.innerText = app.formatTime(app.timeRestInt);
        app.interval = setInterval(function() {
            if (!app.isPaused) {
                if (restTime === 0) {
                    if (app.stateChrono === 1) {
                        restTime = app.workValueInt;
                        app.stateChrono = 2;
                        app.textChrono.innerText = "Brosse les dents"; /* TODO refactorizar */
                        /* TODO refactorizar */
                        document.getElementsByTagName("html")[0].classList.remove('rest');
                        document.getElementsByClassName("jumbotron")[0].classList.remove('rest');
                        document.getElementsByTagName("html")[0].classList.add('work');
                        document.getElementsByClassName("jumbotron")[0].classList.add('work');
                        /* TODO refactorizar */
                        app.playSound('alert');
                    } else {
                        restTime = app.restValueInt;
                        app.stateChrono = 1;
                        app.textChrono.innerText = "C'est la pause"; /* TODO refactorizar */
                        app.workFinish += 1;
                        if (app.workFinish >= app.setsValue.innerText){
                            clearInterval(app.interval);
                            app.playSound('gong');
                            app.textChrono.innerText = "Finish !!!"; /* TODO refactorizar */
                            clearInterval(app.interval);
                            restTime = 0;
                            setTimeout(function () {
                                app.showConfig();
                            }, app.timeReset);
                        } else {
                            cycle.innerText = (app.workFinish + 1) + "/" + app.setsValue.innerText;
                            /* TODO refactorizar */
                            document.getElementsByTagName("html")[0].classList.remove('work');
                            document.getElementsByClassName("jumbotron")[0].classList.remove('work');
                            document.getElementsByTagName("html")[0].classList.add('rest');
                            document.getElementsByClassName("jumbotron")[0].classList.add('rest');
                            /* TODO refactorizar */
                            app.playSound('gong2');
                        }
                    }
                } else {
                    restTime -=1;
                }
                app.timeRestInt = restTime;
                app.timeRest.innerText = app.formatTime(app.timeRestInt);
                if (restTime === 3) {
                    app.playSound('end');
                }
            }
        }, 1000);
        navigator.wakeLock.request("screen").then(
            (w) => {
                app.wakelock = w;
                // alert("acquired")
            }
        ).catch((err) => {
            alert(`${err.name}, ${err.message}`);
        });
    },

    resetChrono: function() {
        if(app.isKaiOS) {
            if ( (app.workFinish >= app.setsValue.innerText) || confirm('¿Reset chrono?')) { //TODO no preguntar
                clearInterval(app.interval);
                app.stateChrono = 1;
                app.workFinish = 0;
                app.showConfig();
            }
        } else {
            app.isPaused = true;
            app.modalReset.classList.remove('hide');

            let okReset = document.getElementById("okReset");
            let closeReset = document.getElementById("closeReset");
            let closeResetFunction;
            let okResetFunction = () => {
                app.isPaused = false;
                app.modalReset.classList.add('hide');
                okReset.removeEventListener("click", okResetFunction);
                closeReset.removeEventListener("click", closeResetFunction);
                clearInterval(app.interval);
                app.stateChrono = 1;
                app.workFinish = 0;
                app.showConfig();
            }

            closeResetFunction = () => {
                app.isPaused = false;
                app.modalReset.classList.add('hide');
                okReset.removeEventListener("click", okResetFunction);
                closeReset.removeEventListener("click", closeResetFunction);
            }

            okReset.addEventListener("click", okResetFunction);
            closeReset.addEventListener("click", closeResetFunction);
            /*
              document.getElementById('okReset').addEventListener('click', () => {
              app.isPaused = false;
              app.modalReset.classList.add('hide');
              document.getElementById('okReset').removeEventListener('click', ()=> {});
              clearInterval(app.interval);
              app.stateChrono = 1;
              app.workFinish = 0;
              app.showConfig();
              }, false);
              document.getElementById('closeReset').addEventListener('click', () => {
              app.isPaused = false;
              app.modalReset.classList.add('hide');
              document.getElementById('closeReset').removeEventListener('click', ()=> {});
              }, false);
            */
        }

    },

    updateWorkTime: function() {
        app.workTime = (app.workValueInt * parseInt(app.setsValue.innerText) ) + (app.restValueInt * (parseInt(app.setsValue.innerText) - 1) );
        app.workTimeElement.innerText = app.formatTime(app.workTime);
    },

    showConfig: function() {
        app.noSleepVideo.pause();
        /* TODO refactorizar */
        document.getElementsByTagName("html")[0].classList.remove('work');
        document.getElementsByClassName("jumbotron")[0].classList.remove('work');
        document.getElementsByTagName("html")[0].classList.remove('rest');
        document.getElementsByClassName("jumbotron")[0].classList.remove('rest');
        /* TODO refactorizar */
        app.setDiv.classList.remove("hide");
        app.chronoDiv.classList.add("hide");
        app.state = 1;
        app.positionNavigator = 0;
        app.workFinish = 0;
        /* TODO refactorizar */
        document.getElementsByClassName('nv-select')[0].classList.remove('nv-select');
        document.getElementsByClassName('navigatorConfig')[0].classList.add('nv-select');
        /* TODO refactorizar */
        if (app.wakelock != null) {
            app.wakelock.release().then(() => {
                app.wakelock = null;
                // alert("released");
            }).catch((err) => {
                alert(`${err.name}, ${err.message}`);
            });
        }

    },

    pauseChrono: function() {
        if(app.isKaiOS) {
            alert('Pause');
        } else {
            app.isPaused = true;
            app.modalPause.classList.remove('hide');
            document.getElementById('closePause').addEventListener('click', () => {
                app.isPaused = false;
                app.modalPause.classList.add('hide');
                document.getElementById('closePause').removeEventListener('click', ()=> {});
            });
        }

    },

    lessValue: function(type) {
        if (typeof type === "object"){
            type = type.target.getAttribute('data-type');
        }
        switch(type) {
        case 'sets':
            app.setsValue.innerText = parseInt(app.setsValue.innerText) - 1;
            if (parseInt(app.setsValue.innerText) < 1) {
                app.setsValue.innerText = 1;
            }
            break;
        case 'work':
            if (parseInt(app.workValueInt) < 60) {
                app.workValueInt = parseInt(app.workValueInt) - 5;
            } else if (parseInt(app.workValueInt) < 120) {
                app.workValueInt = parseInt(app.workValueInt) - 10;
            } else if (parseInt(app.workValueInt) < 180) {
                app.workValueInt = parseInt(app.workValueInt) - 15;
            } else if (parseInt(app.workValueInt) <= 300) {
                app.workValueInt = parseInt(app.workValueInt) - 30;
            }
            if (parseInt(app.workValueInt) < 5) {
                app.workValueInt = 5;
            }
            app.workValue.innerText = app.formatTime(app.workValueInt);
            break;
        case 'rest':
            if (parseInt(app.restValueInt) < 60) {
                app.restValueInt = parseInt(app.restValueInt) - 5;
            } else if (parseInt(app.restValueInt) <= 120) {
                app.restValueInt = parseInt(app.restValueInt) - 10;
            }
            if (parseInt(app.restValueInt) < 5) {
                app.restValueInt = 3;
            }
            app.restValue.innerText = app.formatTime(app.restValueInt);
            break;
        }
        app.updateWorkTime();
    },

    lessValueCont: function(type) {
        if (typeof type === "object") {
            type = type.target.getAttribute('data-type');
        }
        app.intervalButton = setInterval( function() {
            switch(type) {
            case 'sets':
                app.setsValue.innerText = parseInt(app.setsValue.innerText) - 1;
                if (parseInt(app.setsValue.innerText) < 1) {
                    app.setsValue.innerText = 1;
                }
                break;
            case 'work':
                if (parseInt(app.workValueInt) < 60) {
                    app.workValueInt = parseInt(app.workValueInt) - 5;
                } else if (parseInt(app.workValueInt) < 120) {
                    app.workValueInt = parseInt(app.workValueInt) - 10;
                } else if (parseInt(app.workValueInt) < 180) {
                    app.workValueInt = parseInt(app.workValueInt) - 15;
                } else if (parseInt(app.workValueInt) <= 300) {
                    app.workValueInt = parseInt(app.workValueInt) - 30;
                }
                if (parseInt(app.workValueInt) < 5) {
                    app.workValueInt = 3;
                }
                app.workValue.innerText = app.formatTime(app.workValueInt);
                break;
            case 'rest':
                if (parseInt(app.restValueInt) < 60) {
                    app.restValueInt = parseInt(app.restValueInt) - 5;
                } else if (parseInt(app.restValueInt) <= 120) {
                    app.restValueInt = parseInt(app.restValueInt) - 10;
                }
                if (parseInt(app.restValueInt) < 5) {
                    app.restValueInt = 5;
                }
                app.restValue.innerText = app.formatTime(app.restValueInt);
                break;
            }
            app.updateWorkTime();
        }, 250);
    },

    contEnd: function() {
        for (var i = 0; i <= app.intervalButton; i ++) {
            clearInterval(i);
        }
    },

    moreValue: function(type) {
        if (typeof type === "object"){
            type = type.target.getAttribute('data-type');
        }
        switch(type) {
        case 'sets':
            if (parseInt(app.setsValue.innerText) < 30) {
                app.setsValue.innerText = parseInt(app.setsValue.innerText) + 1;
                if (parseInt(app.setsValue.innerText) < 1) {
                    app.setsValue.innerText = 1;
                }
            }
            break;
        case 'work':
            if (parseInt(app.workValueInt) < 300) {
                if (parseInt(app.workValueInt) < 60) {
                    app.workValueInt = parseInt(app.workValueInt) + 5;
                } else if (parseInt(app.workValueInt) < 120) {
                    app.workValueInt = parseInt(app.workValueInt) + 10;
                } else if (parseInt(app.workValueInt) < 180) {
                    app.workValueInt = parseInt(app.workValueInt) + 15;
                } else if (parseInt(app.workValueInt) < 300) {
                    app.workValueInt = parseInt(app.workValueInt) + 30;
                }
                if (parseInt(app.workValueInt) < 5) {
                    app.workValueInt = 5;
                }
                app.workValue.innerText = app.formatTime(app.workValueInt);
            }
            break;
        case 'rest':
            if (parseInt(app.restValueInt) < 120) {
                if (parseInt(app.restValueInt) < 60) {
                    if (parseInt(app.restValueInt) < 5) {
                        app.restValueInt = 5;
                    } else {
                        app.restValueInt = parseInt(app.restValueInt) + 5;
                    }
                } else if (parseInt(app.restValueInt) < 120) {
                    app.restValueInt = parseInt(app.restValueInt) + 10;
                }
                if (parseInt(app.restValueInt) < 5) {
                    app.restValueInt = 3;
                }
            }
            app.restValue.innerText = app.formatTime(app.restValueInt);
            break;
        }
        app.updateWorkTime();
    },

    moreValueCont: function(type) {
        if (typeof type === "object"){
            type = type.target.getAttribute('data-type');
        }
        app.intervalButton = setInterval( function() {
            switch(type) {
            case 'sets':
                if (parseInt(app.setsValue.innerText) < 30) {
                    app.setsValue.innerText = parseInt(app.setsValue.innerText) + 1;
                    if (parseInt(app.setsValue.innerText) < 1) {
                        app.setsValue.innerText = 1;
                    }
                }
                break;
            case 'work':
                if (parseInt(app.workValue.innerText) < 300) {
                    if (parseInt(app.workValueInt) < 60) {
                        app.workValueInt = parseInt(app.workValueInt) + 5;
                    } else if (parseInt(app.workValueInt) < 120) {
                        app.workValueInt = parseInt(app.workValueInt) + 10;
                    } else if (parseInt(app.workValueInt) < 180) {
                        app.workValueInt = parseInt(app.workValueInt) + 15;
                    } else if (parseInt(app.workValueInt) < 300) {
                        app.workValueInt = parseInt(app.workValueInt) + 30;
                    }
                    if (parseInt(app.workValueInt) < 5) {
                        app.workValueInt = 3;
                    }
                    app.workValue.innerText = app.formatTime(app.workValueInt);
                }
                break;
            case 'rest':
                if (parseInt(app.restValueInt) < 120) {
                    if (parseInt(app.restValueInt) < 60) {
                        if (parseInt(app.restValueInt) < 5) {
                            app.restValueInt = 5;
                        } else {
                            app.restValueInt = parseInt(app.restValueInt) + 5;
                        }
                    } else if (parseInt(app.restValueInt) < 120) {
                        app.restValueInt = parseInt(app.restValueInt) + 10;
                    }
                    if (parseInt(app.restValueInt) < 5) {
                        app.restValueInt = 5;
                    }
                }
                app.restValue.innerText = app.formatTime(app.restValueInt);
                break;
            }
            app.updateWorkTime();
        }, 250);
    },

    manejarTeclado: function(e) {

        if (e.key === "Enter") {
            switch(document.getElementsByClassName('nv-select')[0].id) {
            case 'play':
                app.initChrono();
                break;
            case 'reset':
                app.resetChrono();
                break;
            case 'pause':
                app.pauseChrono();
                break;
            case 'setsLess':
                app.lessValue('sets');
                break;
            case 'workLess':
                app.lessValue('work');
                break;
            case 'restLess':
                app.lessValue('rest');
                break;
            case 'setsMore':
                app.moreValue('sets');
                break;
            case 'workMore':
                app.moreValue('work');
                break;
            case 'restMore':
                app.moreValue('rest');
                break;
            }
        }
        if (e.key === "ArrowDown" || e.key === "ArrowRight") {
            e.preventDefault();
            app.changeFocus(1);
        }
        if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
            e.preventDefault();
            app.changeFocus(-1);
        }

    },

    changeFocus: function(changePosicion) {
        let navigator = document.getElementsByClassName('navigatorConfig');
        if (app.state === 2) {
            navigator = document.getElementsByClassName('navigatorChrono');
        }
        navigator[app.positionNavigator].classList.remove('nv-select');
        app.positionNavigator += changePosicion;
        if(navigator[app.positionNavigator] === undefined) {
            app.positionNavigator = 0;
        }
        navigator[app.positionNavigator].classList.add('nv-select');
    },

    formatTime: function(time) {
        let seg = time%60;
        if (seg < 10) {
            seg = "0" + seg;
        }
        let min = parseInt(time/60);
        return min+":"+seg;
    }
};

In the end, I also needed to change a bit the html to make it about brushing the teeth.

<html lang="es"><head>
    <meta charset="UTF-8">
    <title>Brossage Dents</title>
    <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1, maximum-scale=2, user-scalable=no">
    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
    <meta name="theme-color" content="#CCC">
    <link rel="icon" href="img/icon.png">

    <style type="text/css">
      html{
      width: 100%;
      height: 100%;
      }

      html.rest,
      .jumbotron.rest {
      background: pink;
      }

      html.work,
      .jumbotron.work {
      background: cyan;
      }

      body {
      font-family: Helvetica;
      font-size: 16px;
      min-width: 240px;
      }

      header {
      background-color: #ccc;
      border-bottom: 1px solid #666;
      }

      header h3 {
      width: 200px;
      margin: 0 auto;
      font-size: 20px;
      font-weight: bold;
      padding: 5px 0;
      padding-left: 3%;
      padding-right: 3%;
      padding-bottom: 2px;
      padding-top: 4px;
      }

      header h3 img {
      margin-right: 15px;
      }

      .jumbotron {
      background-color: white;
      }

      .jumbotron.container {
      margin-right: 0px;
      margin-left: 0px;
      padding-left: 0px;
      padding-right: 5px;
      }

      .principal {
      margin-top: 5px;
      text-align: center;
      }


      .principal .title {
      display: inline-block;
      width: 50px;
      text-align: left;
      }

      .principal .value {
      width: 25px;
      height: 20px;
      font-size: 20px;
      margin-left: 10px;
      margin-right: 10px;
      }

      .principal a,
      .principal a:focus,
      .principal a:hover,
      .principal a.nv-select {
      height: 20px;
      font-size: 20px;
      background-color: #0000FF;
      color: #FFFFFF;
      border-radius: 10px;

      }


      .principal a.nv-select {
      border: 2px solid cyan;
      }

      .principal #setsValue {
      width: 48px;
      display: inline-block;
      }

      .chronoDiv span{
      width: 50px;
      height: 20px;
      font-size: 30px;
      margin-left: 10px;
      margin-right: 10px;
      }

      .chronoDiv span#timeRest {
      font-weight: bold;
      }

      .modalPause,
      .modalReset {
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      position: fixed;
      width: 100%;
      height: 100%;
      background: grey;
      color: #FFF;
      font-size: 24px;
      opacity: 0.8;
      }
      .modalPause .close,
      .modalReset .closeReset,
      .modalReset .okReset {
      height: 20px;
      margin-top: 10px;
      font-size: 20px;
      background-color: #0000FF;
      color: #FFFFFF;
      padding: 10px;
      border-radius: 10px;
      opacity: 1;
      text-align: center;
      }

      .modalPause .pauseText,
      .modalReset .resetText {
      color: #000;
      opacity: 1;
      background: lightgrey;
      padding: 10px;
      border-radius: 10px;
      }

      .modalReset .buttonDiv {
      display: inline-flex;
      justify-content: space-between;
      min-width: 100px;
      }

      div.clear {
      height: 5px;
      }

      div.clearx5 {
      height: 25px;
      }

      .hide {
      display: none;
      }
    </style>

    <link rel="stylesheet" href="css/framework.min.css">

    <link rel="manifest" href="./manifest.json">

    <meta name="description" content="Brosser les dents">
    <meta name="keywords" content="interval timer, timer, tabata, chronometer, chrono">

    <meta name="author" content="Salvador Camacho">

  </head>

  <body>

    <header>
      <h3>
        <img src="img/icon.png" alt="icon" height="24">Brossage Dents
      </h3>
    </header>

    <div id="modalPause" class="hide modalPause">
      <div class="pauseText">Pause</div>
      <div id="closePause" class="close">X</div>
    </div>

    <div id="modalReset" class="hide modalReset">
      <div class="resetText">Are sure?</div>
      <div class="buttonDiv">
        <div id="okReset" class="okReset">Ok</div>
        <div id="closeReset" class="closeReset">X</div>
      </div>
    </div>

    <div class="container principal jumbotron" id="principalDiv">

      <div id="setDiv" class="setDiv">
        <div>
          <span class="title">Côtés</span>
          <a id="setsLess" class="lessBtn btn btn-sm navigatorConfig nv-select" data-type="sets" tabindex="0">-</a>
          <span id="setsValue" class="value">4</span>
          <a id="setsMore" class="moreBtn btn btn-sm navigatorConfig" data-type="sets" tabindex="0">+</a>
        </div>
        <div class="clear"></div>
        <div>
          <span class="title">Temps</span>
          <a id="workLess" class="lessBtn btn btn-sm navigatorConfig" data-type="work" tabindex="0">-</a>
          <span id="workValue" class="value">0:20</span>
          <a id="workMore" class="moreBtn btn btn-sm navigatorConfig" data-type="work" tabindex="0">+</a>
        </div>
        <div class="clear"></div>
        <div>
          <span class="title">Pause</span>
          <a id="restLess" class="lessBtn btn btn-sm navigatorConfig" data-type="rest" tabindex="0">-</a>
          <span id="restValue" class="value">0:10</span>
          <a id="restMore" class="moreBtn btn btn-sm navigatorConfig" data-type="rest" tabindex="0">+</a>
        </div>
        <div class="clear"></div>
        <a id="play" class="btn btn-b btn-sm navigatorConfig" tabindex="0">Go !</a>

        <div class="clearx5"></div>
        <div>
          <span class="titleWork">Total</span>
          <span id="workTime" class="value">1:50</span>
        </div>

      </div>

      <div id="chronoDiv" class="chronoDiv hide">
        <span id="cycle">1/10</span>
        <div class="clear"></div>
        <span id="timeRest">10</span>
        <div class="clear"></div>
        <span id="textChrono">Rest</span>
        <div class="clear"></div>
        <a id="pause" class="btn btn-a btn-sm navigatorChrono" tabindex="0">Pause</a>
        <a id="reset" class="btn btn-c btn-sm navigatorChrono" tabindex="0">Reset</a> <!-- pause and confirm -->
      </div>
    </div>

    <video id="noSleepVideo" width="320" height="240" autoplay="" muted="" playsinline="" loop="" class="hide">
      <source src="data:video/mp4;base64,AAAAIGZ0eXBtcDQyAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAACKBtZGF0AAAC8wYF///v3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE0MiByMjQ3OSBkZDc5YTYxIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAxNCAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTEgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MToweDExMSBtZT1oZXggc3VibWU9MiBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0wIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MCA4eDhkY3Q9MCBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0wIHRocmVhZHM9NiBsb29rYWhlYWRfdGhyZWFkcz0xIHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNpbWF0ZT0xIGludGVybGFjZWQ9MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9MCBiZnJhbWVzPTMgYl9weXJhbWlkPTIgYl9hZGFwdD0xIGJfYmlhcz0wIGRpcmVjdD0xIHdlaWdodGI9MSBvcGVuX2dvcD0wIHdlaWdodHA9MSBrZXlpbnQ9MzAwIGtleWludF9taW49MzAgc2NlbmVjdXQ9NDAgaW50cmFfcmVmcmVzaD0wIHJjX2xvb2thaGVhZD0xMCByYz1jcmYgbWJ0cmVlPTEgY3JmPTIwLjAgcWNvbXA9MC42MCBxcG1pbj0wIHFwbWF4PTY5IHFwc3RlcD00IHZidl9tYXhyYXRlPTIwMDAwIHZidl9idWZzaXplPTI1MDAwIGNyZl9tYXg9MC4wIG5hbF9ocmQ9bm9uZSBmaWxsZXI9MCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAAAOWWIhAA3//p+C7v8tDDSTjf97w55i3SbRPO4ZY+hkjD5hbkAkL3zpJ6h/LR1CAABzgB1kqqzUorlhQAAAAxBmiQYhn/+qZYADLgAAAAJQZ5CQhX/AAj5IQAD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" type="video/mp4">
    </video>
    <script type="text/javascript" charset="utf-8" src="js/main.js"></script>
</body></html>

Same for the manifest

{
  "lang": "en",
  "name": "Brossage Dents",
  "short_name": "Brossage Dents",
  "icons": [{
        "src": "./img/icon-48x48.png",
        "sizes": "48x48",
        "type": "image/png"
      },{
        "src": "./img/icon-96x96.png",
        "sizes": "96x96",
        "type": "image/png"
      },{
        "src": "./img/icon-144x144.png",
        "sizes": "144x144",
        "type": "image/png"
      },{
        "src": "./img/icon-192x192.png",
        "sizes": "192x192",
        "type": "image/png"
      },{
        "src": "./img/icon-512x512.png",
        "sizes": "512x512",
        "type": "image/png"
      }],
  "start_url": ".",
  "display": "fullscreen",
  "orientation": "portrait",
  "theme_color": "#CCC",
  "background_color": "#DEDEDE"
}

doing again, with old python2 kivy, to reuse my wiko cink peax

import os

from kivy.app import App
from kivy.clock import Clock
from kivy.graphics import Color, Rectangle
from kivy.properties import BooleanProperty, NumericProperty, StringProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.screenmanager import Screen, ScreenManager, ScreenManagerException
from kivy.core.audio import SoundLoader

pink = (1, 0.75, 0.8)
cyan = (0, 1, 1)


class StateMachine(object):

    def __init__(self, manager, steps, step_time, rest_time):
        self.manager = manager
        self.steps = steps
        self.current_step = 0
        self.step_time = step_time
        self.rest_time = rest_time
        self.state = "init"
        self.timer = self.manager.get_screen('timer')
        self.alert = SoundLoader.load(os.path.join(os.path.dirname(__file__), "alert.ogg"))
        self.end = SoundLoader.load(os.path.join(os.path.dirname(__file__), "end.ogg"))
        self.gong = SoundLoader.load(os.path.join(os.path.dirname(__file__), "gong.ogg"))
        self.gong2 = SoundLoader.load(os.path.join(os.path.dirname(__file__), "gong2.ogg"))

    def update(self):
        print("update")
        if self.state in ("init", "rest"):
            self.current_step += 1
            self.state = "step"
            what = "Brosse les dents !"
            color = cyan
            duration = self.step_time
            current_number = self.current_step
            total_number = self.steps
            sound_map = {None: self.alert, 2:self.end}
        elif self.state == "step":
            if self.current_step == self.steps:
                self.gong.play()
                self.timer.finish()
                return
            self.state = "rest"
            what = "Attends..."
            color = pink
            duration = self.rest_time
            current_number = self.current_step
            total_number = self.steps
            sound_map = {2: self.end, None:self.gong2}
        else:
            raise NotImplementedError()
        print("going to step " + what)
        self.timer.start_timer(
            what=what,
            color=color,
            duration=duration,
            current_number=current_number,
            total_number=total_number,
            on_done=self.update,
            sound_map=sound_map,
        )

    def start(self):
        print("starting")

        self.timer.start_timer(
            what="Attends...",
            color=pink,
            duration=5,
            current_number=0,
            total_number=0,
            on_done=self.update,
            sound_map={2: self.end}
        )


class SettingsScreen(Screen):
    steps = NumericProperty(4)
    step_time = NumericProperty(20)
    rest_time = NumericProperty(10)
    total_time = StringProperty("Total: 0s")

    def _update_rect(self, instance, value):
        self.rect.pos = instance.pos
        self.rect.size = instance.size

    def __init__(self, **kwargs):
        super(SettingsScreen, self).__init__(**kwargs)
        with self.canvas.before:
            self.color = Color(1, 1, 1, 1)
            self.rect = Rectangle(size=self.size, pos=self.pos)

        self.bind(size=self._update_rect, pos=self._update_rect)

        layout = BoxLayout(orientation='vertical')

        # Step count setting
        step_layout = BoxLayout(size_hint=(1, 0.15))
        step_layout.add_widget(
            Button(text="-",
                   background_color=(0, 0, 1, 1),
                   color=(1, 1, 1, 1),
                   on_press=self.decrease_steps))
        self.steps_label = Label(text="", color=(0, 0, 0, 1))
        step_layout.add_widget(self.steps_label)
        step_layout.add_widget(
            Button(text="+",
                   background_color=(0, 0, 1, 1),
                   color=(1, 1, 1, 1),
                   on_press=self.increase_steps))
        layout.add_widget(step_layout)

        # Step time setting
        step_time_layout = BoxLayout(size_hint=(1, 0.15))
        step_time_layout.add_widget(
            Button(text="-",
                   background_color=(0, 0, 1, 1),
                   color=(1, 1, 1, 1),
                   on_press=self.decrease_step_time))
        self.step_time_label = Label(text="", color=(0, 0, 0, 1))
        step_time_layout.add_widget(self.step_time_label)
        step_time_layout.add_widget(
            Button(text="+",
                   background_color=(0, 0, 1, 1),
                   color=(1, 1, 1, 1),
                   on_press=self.increase_step_time))
        layout.add_widget(step_time_layout)

        # Rest time setting
        rest_time_layout = BoxLayout(size_hint=(1, 0.15))
        rest_time_layout.add_widget(
            Button(text="-",
                   background_color=(0, 0, 1, 1),
                   color=(1, 1, 1, 1),
                   on_press=self.decrease_rest_time))
        self.rest_time_label = Label(text="",
                  color=(0, 0, 0, 1))
        rest_time_layout.add_widget(self.rest_time_label)
        rest_time_layout.add_widget(
            Button(text="+",
                   background_color=(0, 0, 1, 1),
                   color=(1, 1, 1, 1),
                   on_press=self.increase_rest_time))
        layout.add_widget(rest_time_layout)

        # Go button
        go_button = Button(text="Go !!!",
                           background_color=(0, 0, 1, 1),
                           color=(1, 1, 1, 1),
                           size_hint=(1, 0.15))
        go_button.bind(on_press=self.start_timer)
        layout.add_widget(go_button)

        # Total time display
        total_time_label = Label(text=self.total_time,
                                 color=(0, 0, 0, 1),
                                 size_hint=(1, 0.15))
        layout.add_widget(total_time_label)
        self.total_time_label = total_time_label

        self.add_widget(layout)
        self.update_display()

    def update_display(self):
        total = (self.step_time + self.rest_time) * self.steps - self.rest_time
        self.steps_label.text = "Nombre: " + str(self.steps)
        self.step_time_label.text = "Temps: " + str(self.step_time) + "s"
        self.rest_time_label.text = "Pause: " + str(self.rest_time) + "s"
        self.total_time_label.text = "Total Time: {}s".format(total)

    def increase_steps(self, instance):
        self.steps += 1
        self.update_display()

    def decrease_steps(self, instance):
        if self.steps > 1:
            self.steps -= 1
        self.update_display()

    def increase_step_time(self, instance):
        self.step_time += 10
        self.update_display()

    def decrease_step_time(self, instance):
        if self.step_time > 10:
            self.step_time -= 10
        self.update_display()

    def increase_rest_time(self, instance):
        self.rest_time += 5
        self.update_display()

    def decrease_rest_time(self, instance):
        if self.rest_time > 10:
            self.rest_time -= 10
        self.update_display()

    def start_timer(self, instance):
        app = App.get_running_app()
        app.root.state_machine = StateMachine(
            self.manager,
            self.steps,
            self.step_time,
            self.rest_time,
        )
        app.root.state_machine.start()


class TimerScreen(Screen):
    remaining_time = NumericProperty(0)
    current_number = NumericProperty(1)
    duration = NumericProperty(0)
    timer_running = BooleanProperty(False)
    what = StringProperty("")
    total_number = NumericProperty(0)

    def _update_rect(self, instance, value):
        self.rect.pos = instance.pos
        self.rect.size = instance.size

    def __init__(self, **kwargs):
        super(TimerScreen, self).__init__(**kwargs)

        with self.canvas.before:
            self.color = Color(0, 1, 1, 1)
            self.rect = Rectangle(size=self.size, pos=self.pos)

        self.bind(size=self._update_rect, pos=self._update_rect)

        layout = GridLayout(cols=1)
        self.label = Label(text="", font_size='40sp', color=(0, 0, 0, 1))
        layout.add_widget(self.label)

        self.time_label = Label(text="", font_size='80sp', color=(0, 0, 0, 1))
        layout.add_widget(self.time_label)

        self.what_label = Label(text="", font_size='40sp', color=(0, 0, 0, 1))
        layout.add_widget(self.what_label)

        button_layout = BoxLayout(size_hint=(1, 0.7))
        pause_button = Button(text="Pause",
                              background_color=(0, 0, 1, 1),
                              color=(1, 1, 1, 1))
        pause_button.bind(on_press=self.toggle_clock)
        button_layout.add_widget(pause_button)

        reset_button = Button(text="Reset",
                              background_color=(0, 0, 1, 1),
                              color=(1, 1, 1, 1))
        reset_button.bind(on_press=self.leave)
        button_layout.add_widget(reset_button)

        layout.add_widget(button_layout)
        self.add_widget(layout)
        self.bind(on_pre_leave=self.pre_leave_handler)

    def pre_leave_handler(self, *args):
        "make sure the screen won't work in the background"
        self.stop_clock()

    def start_timer(
        self,
        what,
        color,
        duration,
        current_number,
        total_number,
        on_done,
        sound_map,
    ):
        self.color.rgb = color
        App.get_running_app().goto("timer")
        self.duration = duration
        self.what = what
        self.current_number = current_number
        self.remaining_time = duration
        self.total_number = total_number
        self.on_done = on_done
        self.finished = False
        self.sound_map = sound_map
        self.sound = self.sound_map.get(None)
        if self.sound:
            self.sound.play()
        self.start_clock()

    def update_display(self):
        self.what_label.text = self.what

        if self.current_number:
            self.label.text = "{}/{}".format(
                self.current_number,
                self.total_number,
            )
        else:
            self.label.text = ""
        if not self.clock:
            if self.finished:
                self.what_label.text = "C'est fini !!!"
            else:
                self.what_label.text = "(Paused) " + self.what_label.text
        self.time_label.text = str(self.remaining_time)

    def countdown(self, dt):
        print("tick " + str(self.remaining_time))
        if self.remaining_time > 0:
            self.remaining_time -= 1
        else:
            print("stopping and goto next")
            self.stop_clock()
            self.on_done()
        self.update_display()
        new_sound = self.sound_map.get(self.remaining_time)
        if new_sound:
            if self.sound:
                self.sound.stop()
            self.sound = new_sound
            self.sound.play()

    def start_clock(self):
        print("start clock")
        self.clock = Clock.schedule_interval(self.countdown, 1)
        self.update_display()

    def stop_clock(self):
        Clock.unschedule(self.clock)
        self.clock = None
        self.update_display()

    def toggle_clock(self, instance=None):
        if self.clock:
            self.stop_clock()
        else:
            self.start_clock()

    def finish(self):
        self.finished = True
        Clock.schedule_once(self.leave, 2)

    def leave(self, instance=None):
        App.get_running_app().back()


class IntervalTimerApp(App):

    def goto(self, screen):
        self.manager.current = screen

    def back(self):
        self.manager.current = 'settings'

    @staticmethod
    def populate(sm):
        try:
            sm.get_screen('settings')
            return
        except ScreenManagerException:
            pass # not populated yet

        sm.add_widget(SettingsScreen(name='settings'))
        sm.add_widget(TimerScreen(name='timer'))

    def build(self):
        sm = ScreenManager()
        self.populate(sm)
        return sm


def run():
    IntervalTimerApp().run()

Notes linking here