Interval Timer
Evergreen- External reference: https://salvacam.js.org/intervalTimer/
- External reference: https://ipfs.konubinix.eu/p/bafybeiapftob6e7j7pjv72auxamh522sivapb6fq2tx3yjuvmsmeertr5i
Useful for kung fu training.
pour se brosser les dents
Adding wakelock support and offline mode
From the original.
ipfs get https://ipfs.konubinix.eu/p/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"
}
This is hosted at https://konubinix.eu/brossage-dents
doing again, with old python2 kivy, to reuse my wiko cink peax
Shows how to change fullscreen settings,
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
from helpers.osc import oschandler, to_service
from helpers.wakelock import WakeLock
from android.runnable import run_on_ui_thread
from logging import getLogger
logger = getLogger(__name__)
pink = (1, 0.75, 0.8)
cyan = (0, 1, 1)
grey = (0.3, 0.3, 0.3)
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('intervaltimer: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):
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()
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):
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}
)
controls = True
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))
if controls:
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))
if controls:
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))
if controls:
layout.add_widget(rest_time_layout)
# Go button
self.go_button = Button(text="Go !!!",
background_color=(0, 0, 1, 1),
color=(1, 1, 1, 1),
size_hint=(1, 0.15))
self.go_button.bind(on_press=self.start_timer)
layout.add_widget(self.go_button)
# Total time display
total_time_label = Label(text=self.total_time,
color=(0, 0, 0, 1),
size_hint=(1, 0.15))
if controls:
layout.add_widget(total_time_label)
self.total_time_label = total_time_label
self.add_widget(layout)
self.update_display()
@oschandler("intervaltimer:start")
def _(config):
self.step_time = config["step_time"]
self.update_display()
self.start_timer()
self.bind(on_enter=self.enter_handler)
def enter_handler(self, *_):
to_service("intervaltimer:settings")
self.go_button.disabled = False
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):
self.go_button.disabled = True
Clock.schedule_once(self._start_timer)
def _start_timer(self, dt):
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))
self.pause_button = Button(text="Pause",
background_color=(0, 0, 1, 1),
color=(1, 1, 1, 1))
self.pause_button.bind(on_press=self.toggle_clock)
button_layout.add_widget(self.pause_button)
layout.add_widget(button_layout)
self.add_widget(layout)
self.bind(on_pre_enter=self.pre_enter_handler)
self.bind(on_pre_leave=self.pre_leave_handler)
self.keep_screen_on()
def keep_screen_on(self, revert=False):
from android import mActivity
from jnius import autoclass
from android.runnable import run_on_ui_thread
LayoutParams = autoclass('android.view.WindowManager$LayoutParams')
@run_on_ui_thread
def do():
if revert:
mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON)
else:
mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON)
do()
def pre_leave_handler(self, *args):
self.stop_clock()
self.keep_screen_on(revert=True)
from jnius import autoclass
from android import mActivity
View = autoclass('android.view.View')
option = View.SYSTEM_UI_FLAG_VISIBLE
@run_on_ui_thread
def fs():
logger.debug("Leaving fullscreen mode")
mActivity.getWindow().getDecorView().setSystemUiVisibility(option)
fs()
def pre_enter_handler(self, *args):
self.keep_screen_on()
from jnius import autoclass
from android import mActivity
View = autoclass('android.view.View')
option = View.SYSTEM_UI_FLAG_FULLSCREEN
@run_on_ui_thread
def fs():
logger.debug("Entering fullscreen mode")
mActivity.getWindow().getDecorView().setSystemUiVisibility(option)
fs()
def start_timer(
self,
what,
color,
duration,
current_number,
total_number,
on_done,
sound_map,
):
self.wanted_color = color
self.color.rgb = self.wanted_color
App.get_running_app().goto("intervaltimer: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
self.time_label.text = str(self.remaining_time)
if self.what == "Attends..." and self.current_number > 0:
self.label.text = "{} -> {}/{}".format(
self.current_number,
self.current_number+1,
self.total_number,
)
elif self.current_number:
self.label.text = "{}/{}".format(
self.current_number,
self.total_number,
)
else:
self.label.text = ""
if self.clock:
self.color.rgb = self.wanted_color
self.pause_button.text = "Pause"
else:
if self.finished:
self.what_label.text = "C'est fini !!!"
else:
self.color.rgb = grey
self.pause_button.text = "Continue"
def countdown(self, dt):
if self.remaining_time > 0:
self.remaining_time -= 1
else:
self.stop_clock()
self.on_done()
self.update_display()
self.update_sound()
def update_sound(self):
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):
to_service("intervaltimer:clock-started")
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 = 'intervaltimer:settings'
@staticmethod
def populate(sm):
try:
sm.get_screen('intervaltimer:settings')
return
except ScreenManagerException:
pass # not populated yet
sm.add_widget(SettingsScreen(name='intervaltimer:settings'))
sm.add_widget(TimerScreen(name='intervaltimer:timer'))
def build(self):
self.manager = ScreenManager()
self.populate(self.manager)
return self.manager
def run():
IntervalTimerApp().run()
a clone of moodbrush, with my python2 kivy runtime, on the wiko cink peax
A clone of moodbrush: pick one of six moods (Bright, Calm, Power, Love, Bedtime, Jazz), then brush along a 2 minutes cycle (4 zones of 30s) with the mood music playing, and get an uplifting message at the end. A canvas-drawn mouth shows the zone being brushed, with a wiggling toothbrush and a few sparkles, as a cheap stand-in for the shiny sprites of the original.
Same idea as the interval timer above, on top of a python runtime on android.
import math
import os
import random
from kivy.app import App
from kivy.clock import Clock
from kivy.graphics import Color, Rectangle
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.uix.widget import Widget
from kivy.core.audio import SoundLoader
from helpers.osc import oschandler, to_service
from android.runnable import run_on_ui_thread
from logging import getLogger
logger = getLogger(__name__)
here = os.path.dirname(__file__)
grey = (0.3, 0.3, 0.3)
moods = [
("Bright", (1, 0.85, 0.3), "bright.ogg"),
("Calm", (0.55, 0.8, 1), "calm.ogg"),
("Power", (1, 0.45, 0.25), "power.ogg"),
("Love", (1, 0.6, 0.8), "love.ogg"),
("Bedtime", (0.45, 0.4, 0.7), "bedtime.ogg"),
("Jazz", (0.4, 0.75, 0.65), "jazz.ogg"),
]
zone_time = 30
zones = [
"En bas a gauche",
"En bas a droite",
"En haut a gauche",
"En haut a droite",
]
messages = [
"Bravo, quel sourire !",
"Dents toutes propres !",
"Super brossage !",
"Continue comme ca !",
]
class MouthWidget(Widget):
"The teeth, a wiggling toothbrush and a few sparkles."
def __init__(self, **kwargs):
super(MouthWidget, self).__init__(**kwargs)
self.zone = None
self.phase = 0.0
self.sparkles = []
self.ticking = None
self.bind(size=self.trigger_redraw, pos=self.trigger_redraw)
def trigger_redraw(self, *_):
self.redraw()
def start(self):
self.stop()
self.ticking = Clock.schedule_interval(self.tick, 1 / 15.)
def stop(self):
if self.ticking:
Clock.unschedule(self.ticking)
self.ticking = None
def tick(self, dt):
self.phase += dt * 8
if self.zone is not None and random.random() < 0.4:
x0, y0, qw, qh = self.quadrant_rect(self.zone)
self.sparkles.append([x0 + random.random() * qw,
y0 + random.random() * qh,
(0.05 + random.random() * 0.08) * self.height,
1.0])
for sparkle in self.sparkles:
sparkle[3] -= dt * 1.5
self.sparkles = [s for s in self.sparkles if s[3] > 0]
self.redraw()
def mouth_rect(self):
width = self.width * 0.8
height = self.height * 0.8
return (self.x + (self.width - width) / 2,
self.y + (self.height - height) / 2,
width, height)
def quadrant_rect(self, zone):
mx, my, mw, mh = self.mouth_rect()
row = 0 if zone < 2 else 1 # 0 is at the bottom
column = zone % 2 # 0 is at the left
return (mx + column * mw / 2,
my + row * mh / 2,
mw / 2, mh / 2)
def redraw(self):
self.canvas.clear()
if self.zone is None:
return
with self.canvas:
for zone in range(len(zones)):
x0, y0, qw, qh = self.quadrant_rect(zone)
if zone == self.zone:
Color(1, 1, 1, 1)
else:
Color(0.8, 0.8, 0.8, 1)
tooth_w = qw / 4 * 0.7
tooth_h = qh * 0.5
# top row teeth hang from the top, bottom row teeth
# stand on the bottom
ty = y0 + qh - tooth_h if zone >= 2 else y0
for i in range(4):
tx = x0 + (i + 0.15) * qw / 4
Rectangle(pos=(tx, ty), size=(tooth_w, tooth_h))
self.draw_brush()
for x, y, size, alpha in self.sparkles:
Color(1, 1, 0.5, max(alpha, 0))
Rectangle(pos=(x - size / 2, y - size / 8),
size=(size, size / 4))
Rectangle(pos=(x - size / 8, y - size / 2),
size=(size / 4, size))
def draw_brush(self):
x0, y0, qw, qh = self.quadrant_rect(self.zone)
shift = math.sin(self.phase) * qw * 0.15
head_w = qw * 0.6
head_h = qh * 0.22
bristle_h = qh * 0.18
head_x = x0 + qw * 0.2 + shift
tooth_h = qh * 0.5
if self.zone >= 2: # top row, brushing from below
bristles_y = y0 + qh - tooth_h * 0.7 - bristle_h
head_y = bristles_y - head_h
else: # bottom row, brushing from above
bristles_y = y0 + tooth_h * 0.7
head_y = bristles_y + bristle_h
Color(0.6, 0.85, 1, 1)
for i in range(4):
Rectangle(pos=(head_x + (i + 0.15) * head_w / 4, bristles_y),
size=(head_w / 4 * 0.7, bristle_h))
Color(0.2, 0.5, 1, 1)
Rectangle(pos=(head_x, head_y), size=(head_w, head_h))
# the handle leaves toward the nearest corner of the mouth
handle_w = qw * 0.5
handle_x = head_x - handle_w if self.zone % 2 == 0 else head_x + head_w
Rectangle(pos=(handle_x, head_y + head_h * 0.25),
size=(handle_w, head_h * 0.5))
class MoodScreen(Screen):
def _update_rect(self, instance, value):
self.rect.pos = instance.pos
self.rect.size = instance.size
def __init__(self, **kwargs):
super(MoodScreen, 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 = GridLayout(cols=2)
for mood_name, color, _ in moods:
button = Button(text=mood_name,
font_size='30sp',
background_normal='',
background_color=color + (1,),
color=(0, 0, 0, 1))
button.bind(on_press=self.make_starter(mood_name))
layout.add_widget(button)
self.add_widget(layout)
@oschandler("moodbrush:start")
def _(config):
self.start(config["mood"])
self.bind(on_enter=self.enter_handler)
def enter_handler(self, *_):
to_service("moodbrush:moods")
def make_starter(self, mood_name):
def starter(instance):
self.start(mood_name)
return starter
def start(self, mood_name):
brush = self.manager.get_screen('moodbrush:timer')
Clock.schedule_once(lambda dt: brush.start_session(mood_name))
class BrushScreen(Screen):
def _update_rect(self, instance, value):
self.rect.pos = instance.pos
self.rect.size = instance.size
def __init__(self, **kwargs):
super(BrushScreen, 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 = GridLayout(cols=1)
self.zone_label = Label(text="", font_size='40sp', color=(0, 0, 0, 1))
layout.add_widget(self.zone_label)
self.time_label = Label(text="", font_size='80sp', color=(0, 0, 0, 1))
layout.add_widget(self.time_label)
self.mouth = MouthWidget(size_hint=(1, 2.))
layout.add_widget(self.mouth)
self.progress_label = Label(text="", font_size='40sp', color=(0, 0, 0, 1))
layout.add_widget(self.progress_label)
button_layout = BoxLayout(size_hint=(1, 0.7))
self.pause_button = Button(text="Pause",
background_color=(0, 0, 1, 1),
color=(1, 1, 1, 1))
self.pause_button.bind(on_press=self.toggle_clock)
button_layout.add_widget(self.pause_button)
layout.add_widget(button_layout)
self.add_widget(layout)
self.gong = SoundLoader.load(os.path.join(here, "gong.ogg"))
self.end = SoundLoader.load(os.path.join(here, "end.ogg"))
self.music = None
self.clock = None
self.finished = False
self.wanted_color = (1, 1, 1)
self.bind(on_pre_enter=self.pre_enter_handler)
self.bind(on_pre_leave=self.pre_leave_handler)
def keep_screen_on(self, revert=False):
from android import mActivity
from jnius import autoclass
LayoutParams = autoclass('android.view.WindowManager$LayoutParams')
@run_on_ui_thread
def do():
if revert:
mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON)
else:
mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON)
do()
def pre_leave_handler(self, *args):
self.stop_clock()
if self.music:
self.music.stop()
self.keep_screen_on(revert=True)
from jnius import autoclass
from android import mActivity
View = autoclass('android.view.View')
option = View.SYSTEM_UI_FLAG_VISIBLE
@run_on_ui_thread
def fs():
logger.debug("Leaving fullscreen mode")
mActivity.getWindow().getDecorView().setSystemUiVisibility(option)
fs()
def pre_enter_handler(self, *args):
self.keep_screen_on()
from jnius import autoclass
from android import mActivity
View = autoclass('android.view.View')
option = View.SYSTEM_UI_FLAG_FULLSCREEN
@run_on_ui_thread
def fs():
logger.debug("Entering fullscreen mode")
mActivity.getWindow().getDecorView().setSystemUiVisibility(option)
fs()
def start_session(self, mood_name):
mood = [m for m in moods if m[0] == mood_name][0]
self.wanted_color = mood[1]
self.color.rgb = self.wanted_color
App.get_running_app().goto("moodbrush:timer")
self.current_zone = 0
self.remaining_time = zone_time
self.finished = False
self.mouth.zone = 0
if self.music:
self.music.stop()
self.music = SoundLoader.load(os.path.join(here, mood[2]))
if self.music:
self.music.loop = True
self.music.play()
self.start_clock()
def update_display(self):
if self.finished:
self.zone_label.text = ""
self.time_label.text = ""
self.progress_label.text = self.message
return
self.zone_label.text = zones[self.current_zone]
self.time_label.text = str(self.remaining_time)
self.progress_label.text = "{}/{}".format(self.current_zone + 1, len(zones))
if self.clock:
self.color.rgb = self.wanted_color
self.pause_button.text = "Pause"
else:
self.color.rgb = grey
self.pause_button.text = "Continue"
def countdown(self, dt):
self.remaining_time -= 1
if self.remaining_time <= 0:
self.current_zone += 1
if self.current_zone >= len(zones):
self.finish()
return
self.mouth.zone = self.current_zone
if self.gong:
self.gong.play()
self.remaining_time = zone_time
self.update_display()
def start_clock(self):
to_service("moodbrush:clock-started")
self.clock = Clock.schedule_interval(self.countdown, 1)
self.mouth.start()
if self.music and self.music.state != 'play':
self.music.play()
self.update_display()
def stop_clock(self):
Clock.unschedule(self.clock)
self.clock = None
self.mouth.stop()
self.update_display()
def toggle_clock(self, instance=None):
if self.finished:
return
if self.clock:
self.stop_clock()
if self.music:
self.music.stop()
else:
self.start_clock()
def finish(self):
self.finished = True
self.message = random.choice(messages)
self.stop_clock()
self.mouth.zone = None
self.mouth.sparkles = []
self.mouth.redraw()
if self.music:
self.music.stop()
if self.end:
self.end.play()
Clock.schedule_once(self.leave, 5)
def leave(self, instance=None):
App.get_running_app().back()
class MoodBrushApp(App):
def goto(self, screen):
self.manager.current = screen
def back(self):
self.manager.current = 'moodbrush:moods'
@staticmethod
def populate(sm):
try:
sm.get_screen('moodbrush:moods')
return
except ScreenManagerException:
pass # not populated yet
sm.add_widget(MoodScreen(name='moodbrush:moods'))
sm.add_widget(BrushScreen(name='moodbrush:timer'))
def build(self):
self.manager = ScreenManager()
self.populate(self.manager)
return self.manager
def run():
MoodBrushApp().run()
The music files are expected next to main.py. I don’t have the moodbrush
tracks (and could not ship them anyway), so I generate placeholder loops with
sox, one flavor per mood: a plucked arpeggio for bright, pad chord
progressions for calm and bedtime, an overdriven square riff for power,
strummed chords for love and a walking bass for jazz, plus the zone change
gong and the end jingle. The music can be replaced by real tracks later, but
this way the whole app, sounds included, is fully reproducible from this
note: nothing binary to keep around.
mkdir -p /tmp/moodbrush
cd /tmp/moodbrush
bar () { # out dur wave note...
local out="$1"; shift
local dur="$1"; shift
local wave="$1"; shift
rm -f part_*.wav
local i=0
for note in "$@"
do
sox -n -r 22050 -c 1 "part_$(printf '%02d' "$i").wav" synth "$dur" "$wave" "$note" fade t 0.01 "$dur" 0.05
i=$((i+1))
done
sox part_*.wav "$out"
rm -f part_*.wav
}
pad () { # out dur note... (a chord faded in and out)
local out="$1"; shift
local dur="$1"; shift
local args=()
for note in "$@"
do
args+=(sine "$note")
done
sox -n -r 22050 -c 1 "$out" synth "$dur" "${args[@]}" remix - fade t 2 "$dur" 2
}
# bright: a happy plucked arpeggio, 2s bar
bar bar.wav 0.25 pluck C5 E5 G5 C6 G5 E5 G5 E5
sox bar.wav bright.ogg repeat 59 vol 0.5
# calm: a slow pad progression, 4 chords of 4s
pad c1.wav 4 C4 E4 G4
pad c2.wav 4 A3 C4 E4
pad c3.wav 4 F3 A3 C4
pad c4.wav 4 G3 B3 D4
sox c1.wav c2.wav c3.wav c4.wav calm_bar.wav
sox calm_bar.wav calm.ogg repeat 7 trim 0 120 vol 0.4 reverb
# power: a driving overdriven square riff, 1.6s bar
bar bar.wav 0.2 square C3 C3 G3 C3 F3 C3 G3 E3
sox bar.wav power.ogg repeat 74 overdrive 15 vol 0.25
# love: soft strummed chords, 8s bar
sox -n -r 22050 -c 1 l1.wav synth 4 pluck C4 pluck E4 pluck G4 pluck C5 delay 0 .1 .2 .3 remix - fade t 0.05 4 1
sox -n -r 22050 -c 1 l2.wav synth 4 pluck F3 pluck A3 pluck C4 pluck F4 delay 0 .1 .2 .3 remix - fade t 0.05 4 1
sox l1.wav l2.wav love_bar.wav
sox love_bar.wav love.ogg repeat 14 chorus 0.7 0.9 55 0.4 0.25 2 -t vol 0.4
# bedtime: low slow pads, 2 chords of 8s
pad b1.wav 8 C3 G3
pad b2.wav 8 A2 E3
sox b1.wav b2.wav bedtime_bar.wav
sox bedtime_bar.wav bedtime.ogg repeat 7 trim 0 120 vol 0.3 reverb 70
# jazz: a walking bass, 3.2s bar
bar bar.wav 0.4 pluck C2 E2 G2 A2 B2 A2 G2 E2
sox bar.wav jazz.ogg repeat 36 tempo 1.1 vol 0.5
# the zone change gong: inharmonic partials, long decay
sox -n -r 22050 -c 1 gong.ogg synth 3 sine 220 sine 331 sine 447 sine 587 remix - fade t 0.002 3 2.9 vol 0.5
# the end jingle: a happy ascending strum
sox -n -r 22050 -c 1 end.ogg synth 2 pluck C5 pluck E5 pluck G5 pluck C6 delay 0 .15 .3 .45 remix - fade t 0.01 2 1 vol 0.6
rm -f bar.wav c?.wav l?.wav b?.wav *_bar.wav
clk android -d cinkpeax adb shell mkdir -p /sdcard/poc/moodbrush/
for f in *.ogg
do
clk android -d cinkpeax adb push "$f" /sdcard/poc/moodbrush/
done
Notes linking here
- change fullscreen settings
- i9300
- pomodo timer on kivy
- pwa vs kivy app
- use the camera with kivy on android (blog)
- wiko cink peax 2