forked from FOSS/BangleApps
0.06: Complete rewrite in 80x80, better perf, add settings
parent
61d2c84284
commit
e40829ea73
|
@ -1092,14 +1092,16 @@
|
|||
},
|
||||
{ "id": "toucher",
|
||||
"name": "Touch Launcher",
|
||||
"shortName":"Menu",
|
||||
"shortName":"Toucher",
|
||||
"icon": "app.png",
|
||||
"version":"0.06",
|
||||
"description": "Touch enable left to right launcher.",
|
||||
"tags": "tool,system,launcher",
|
||||
"type":"launch",
|
||||
"storage": [
|
||||
{"name":"toucher.app.js","url":"app.js"}
|
||||
{"name":"toucher.app.js","url":"app.js"},
|
||||
{"name":"toucher.settings.js","url":"settings.js"},
|
||||
{"name":"toucher.json"}
|
||||
],
|
||||
"sortorder" : -10
|
||||
},
|
||||
|
|
|
@ -3,4 +3,4 @@
|
|||
0.03: Close launcher when lcd turn off
|
||||
0.04: Complete rewrite to add animation and loop ( issue #210 )
|
||||
0.05: Improve perf
|
||||
0.06: Only store relevant app data (saves RAM when many apps)
|
||||
0.06: Complete rewrite in 80x80, better perf, add settings
|
|
@ -1,160 +1,206 @@
|
|||
Bangle.setLCDMode("120x120");
|
||||
const Storage = require("Storage");
|
||||
const filename = 'toucher.json';
|
||||
let settings = Storage.readJSON(filename,1) || {
|
||||
hightres: true,
|
||||
animation : true,
|
||||
frame : 3,
|
||||
debug: false
|
||||
};
|
||||
|
||||
if(!settings.highres) Bangle.setLCDMode("80x80");
|
||||
else Bangle.setLCDMode();
|
||||
|
||||
g.clear();
|
||||
g.flip();
|
||||
|
||||
const Storage = require("Storage");
|
||||
let icons = {};
|
||||
|
||||
const HEIGHT = g.getHeight();
|
||||
const WIDTH = g.getWidth();
|
||||
const HALF = WIDTH/2;
|
||||
const ORIGINAL_ICON_SIZE = 48;
|
||||
|
||||
const STATE = {
|
||||
settings_open: false,
|
||||
index: 0,
|
||||
target: 240,
|
||||
offset: 0
|
||||
};
|
||||
|
||||
function getPosition(index){
|
||||
return (index*HALF);
|
||||
}
|
||||
|
||||
function getApps(){
|
||||
return Storage.list(/\.info$/).map(app=>{var a=Storage.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src,version:a.version}})
|
||||
.filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type))
|
||||
const exit_app = {
|
||||
name: 'Exit',
|
||||
special: true
|
||||
};
|
||||
const raw_apps = Storage.list(/\.info$/).filter(app => app.endsWith('.info')).map(app => Storage.readJSON(app,1) || { name: "DEAD: "+app.substr(1) })
|
||||
.filter(app=>app.type=="app" || app.type=="clock" || !app.type)
|
||||
.sort((a,b)=>{
|
||||
var n=(0|a.sortorder)-(0|b.sortorder);
|
||||
if (n) return n; // do sortorder first
|
||||
if (a.name<b.name) return -1;
|
||||
if (a.name>b.name) return 1;
|
||||
return 0;
|
||||
}).map(raw => ({
|
||||
name: raw.name,
|
||||
src: raw.src,
|
||||
icon: raw.icon,
|
||||
version: raw.version
|
||||
}));
|
||||
|
||||
const apps = [Object.assign({}, exit_app)].concat(raw_apps);
|
||||
apps.push(exit_app);
|
||||
return apps.map((app, i) => {
|
||||
app.x = getPosition(i);
|
||||
return app;
|
||||
});
|
||||
}
|
||||
|
||||
const HEIGHT = g.getHeight();
|
||||
const WIDTH = g.getWidth();
|
||||
const HALF = WIDTH/2;
|
||||
const ANIMATION_FRAME = 4;
|
||||
const ANIMATION_STEP = HALF / ANIMATION_FRAME;
|
||||
const APPS = getApps();
|
||||
|
||||
function getPosition(index){
|
||||
return (index*HALF);
|
||||
function noIcon(x, y, scale){
|
||||
if(scale < 0.2) return;
|
||||
g.setColor(scale, scale, scale);
|
||||
g.setFontAlign(0,0);
|
||||
g.setFont('6x8',settings.highres ? 6:3);
|
||||
g.drawString('x_x', x+1.5, y);
|
||||
const h = (ORIGINAL_ICON_SIZE/3);
|
||||
g.drawRect(x-h, y-h, x+h, y+h);
|
||||
}
|
||||
|
||||
let current_app = 0;
|
||||
let target = 0;
|
||||
let slideOffset = 0;
|
||||
function render(){
|
||||
const start = Date.now();
|
||||
|
||||
const back = {
|
||||
name: 'BACK',
|
||||
back: true
|
||||
};
|
||||
const ANIMATION_FRAME = settings.frame;
|
||||
const ANIMATION_STEP = Math.floor(HALF / ANIMATION_FRAME);
|
||||
const THRESHOLD = ANIMATION_STEP - 1;
|
||||
|
||||
let icons = {};
|
||||
g.clear();
|
||||
const visibleApps = APPS.filter(app => app.x >= STATE.offset-HALF && app.x <= STATE.offset+WIDTH-HALF );
|
||||
|
||||
const apps = [back].concat(getApps());
|
||||
apps.push(back);
|
||||
visibleApps.forEach(app => {
|
||||
|
||||
function noIcon(x, y, size){
|
||||
const half = size/2;
|
||||
g.setColor(1,1,1);
|
||||
g.setFontAlign(-0,0);
|
||||
const fontSize = Math.floor(size / 30 * 2);
|
||||
g.setFont('6x8', fontSize);
|
||||
if(fontSize) g.drawString('-?-', x+1.5, y);
|
||||
g.drawRect(x-half, y-half, x+half, y+half);
|
||||
}
|
||||
const x = app.x+HALF-STATE.offset;
|
||||
const y = HALF - (HALF*0.3);
|
||||
|
||||
function drawIcons(offset){
|
||||
apps.forEach((app, i) => {
|
||||
const x = getPosition(i) + HALF - offset;
|
||||
const y = HALF - (HALF*0.3);//-(HALF*0.7);
|
||||
let diff = (x - HALF);
|
||||
if(diff < 0) diff *=-1;
|
||||
let dist = HALF - x;
|
||||
if(dist < 0) dist *= -1;
|
||||
|
||||
const dontRender = x+(HALF/2)<0 || x-(HALF/2)>120;
|
||||
if(dontRender) {
|
||||
delete icons[app.name];
|
||||
const scale = 1 - (dist / HALF);
|
||||
|
||||
if(!scale) return;
|
||||
|
||||
if(app.special){
|
||||
const font = settings.highres ? '6x8' : '4x6';
|
||||
const fontSize = settings.highres ? 2 : 1;
|
||||
g.setFont(font, fontSize);
|
||||
g.setColor(scale,scale,scale);
|
||||
g.setFontAlign(0,0);
|
||||
g.drawString(app.name, HALF, HALF);
|
||||
return;
|
||||
}
|
||||
let size = 30;
|
||||
if((diff*0.5) < size) size -= (diff*0.5);
|
||||
else size = 0;
|
||||
|
||||
const scale = size / 30;
|
||||
if(size){
|
||||
let c = size / 30 * 2;
|
||||
c = c -1;
|
||||
if(c < 0) c = 0;
|
||||
|
||||
if(app.back){
|
||||
g.setFont('6x8', 1);
|
||||
g.setFontAlign(0, -1);
|
||||
g.setColor(c,c,c);
|
||||
g.drawString('Back', HALF, HALF);
|
||||
return;
|
||||
}
|
||||
// icon
|
||||
|
||||
//draw icon
|
||||
const icon = app.icon ?
|
||||
icons[app.name] ? icons[app.name] : Storage.read(app.icon)
|
||||
: null;
|
||||
|
||||
if(icon){
|
||||
icons[app.name] = icon;
|
||||
try {
|
||||
g.drawImage(icon, x-(scale*24), y-(scale*24), { scale: scale });
|
||||
const rescale = settings.highres ? scale*ORIGINAL_ICON_SIZE : (scale*(ORIGINAL_ICON_SIZE/2));
|
||||
const imageScale = settings.highres ? scale*2 : scale;
|
||||
g.drawImage(icon, x-rescale, y-rescale, { scale: imageScale });
|
||||
} catch(e){
|
||||
noIcon(x, y, size);
|
||||
noIcon(x, y, scale);
|
||||
}
|
||||
}else{
|
||||
noIcon(x, y, size);
|
||||
noIcon(x, y, scale);
|
||||
}
|
||||
//text
|
||||
g.setFont('6x8', 1);
|
||||
g.setFontAlign(0, -1);
|
||||
g.setColor(c,c,c);
|
||||
g.drawString(app.name, HALF, HEIGHT - (HALF*0.7));
|
||||
|
||||
//draw text
|
||||
g.setColor(scale,scale,scale);
|
||||
if(scale > 0.1){
|
||||
const font = settings.highres ? '6x8': '4x6';
|
||||
const fontSize = settings.highres ? 2 : 1;
|
||||
g.setFont(font, fontSize);
|
||||
g.setFontAlign(0,0);
|
||||
g.drawString(app.name, HALF, HEIGHT/4*3);
|
||||
}
|
||||
|
||||
if(settings.highres){
|
||||
const type = app.type ? app.type : 'App';
|
||||
const version = app.version ? app.version : '0.00';
|
||||
const info = type+' v'+version;
|
||||
g.setFontAlign(0,1);
|
||||
g.setFont('4x6', 0.25);
|
||||
g.setColor(c,c,c);
|
||||
g.drawString(info, HALF, 110, { scale: scale });
|
||||
g.setFont('6x8', 1.5);
|
||||
g.setColor(scale,scale,scale);
|
||||
g.drawString(info, HALF, 215, { scale: scale });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function draw(ignoreLoop){
|
||||
g.setColor(0,0,0);
|
||||
g.fillRect(0,0,WIDTH,HEIGHT);
|
||||
drawIcons(slideOffset);
|
||||
});
|
||||
|
||||
const duration = Math.floor(Date.now()-start);
|
||||
if(settings.debug){
|
||||
g.setFontAlign(0,1);
|
||||
g.setColor(0, 1, 0);
|
||||
const fontSize = settings.highres ? 2 : 1;
|
||||
g.setFont('4x6',fontSize);
|
||||
g.drawString('Render: '+duration+'ms', HALF, HEIGHT);
|
||||
}
|
||||
g.flip();
|
||||
if(slideOffset == target) return;
|
||||
if(slideOffset < target) slideOffset+= ANIMATION_STEP;
|
||||
else if(slideOffset > target) slideOffset -= ANIMATION_STEP;
|
||||
if(!ignoreLoop) draw();
|
||||
if(STATE.offset == STATE.target) return;
|
||||
|
||||
if(STATE.offset < STATE.target) STATE.offset += ANIMATION_STEP;
|
||||
else if(STATE.offset > STATE.target) STATE.offset -= ANIMATION_STEP;
|
||||
|
||||
if(STATE.offset >= STATE.target-THRESHOLD && STATE.offset < STATE.target) STATE.offset = STATE.target;
|
||||
if(STATE.offset <= STATE.target+THRESHOLD && STATE.offset > STATE.target) STATE.offset = STATE.target;
|
||||
setTimeout(render, 0);
|
||||
}
|
||||
|
||||
function animateTo(index){
|
||||
target = getPosition(index);
|
||||
draw();
|
||||
}
|
||||
function goTo(index){
|
||||
current_app = index;
|
||||
target = getPosition(index);
|
||||
slideOffset = target;
|
||||
draw(true);
|
||||
STATE.index = index;
|
||||
STATE.target = getPosition(index);
|
||||
render();
|
||||
}
|
||||
|
||||
goTo(1);
|
||||
function jumpTo(index){
|
||||
STATE.index = index;
|
||||
STATE.target = getPosition(index);
|
||||
STATE.offset = STATE.target;
|
||||
render();
|
||||
}
|
||||
|
||||
function prev(){
|
||||
if(current_app == 0) goTo(apps.length-1);
|
||||
current_app -= 1;
|
||||
if(current_app < 0) current_app = 0;
|
||||
animateTo(current_app);
|
||||
if(STATE.settings_open) return;
|
||||
if(STATE.index == 0) jumpTo(APPS.length-1);
|
||||
setTimeout(() => {
|
||||
if(!settings.animation) jumpTo(STATE.index-1);
|
||||
else animateTo(STATE.index-1);
|
||||
},1);
|
||||
}
|
||||
|
||||
function next(){
|
||||
if(current_app == apps.length-1) goTo(0);
|
||||
current_app += 1;
|
||||
if(current_app > apps.length-1) current_app = apps.length-1;
|
||||
animateTo(current_app);
|
||||
if(STATE.settings_open) return;
|
||||
if(STATE.index == APPS.length-1) jumpTo(0);
|
||||
setTimeout(() => {
|
||||
if(!settings.animation) jumpTo(STATE.index+1);
|
||||
else animateTo(STATE.index+1);
|
||||
},1);
|
||||
}
|
||||
|
||||
function run() {
|
||||
const app = apps[current_app];
|
||||
if(app.back) return load();
|
||||
function run(){
|
||||
|
||||
const app = APPS[STATE.index];
|
||||
if(app.name == 'Exit') return load();
|
||||
|
||||
if (Storage.read(app.src)===undefined) {
|
||||
E.showMessage("App Source\nNot found");
|
||||
setTimeout(draw, 2000);
|
||||
setTimeout(render, 2000);
|
||||
} else {
|
||||
Bangle.setLCDMode();
|
||||
g.clear();
|
||||
|
@ -162,15 +208,12 @@ function run() {
|
|||
E.showMessage("Loading...");
|
||||
load(app.src);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
setWatch(prev, BTN1, { repeat: true });
|
||||
setWatch(next, BTN3, { repeat: true });
|
||||
setWatch(run, BTN2, {repeat:true,edge:"falling"});
|
||||
|
||||
// Screen event
|
||||
Bangle.on('touch', function(button){
|
||||
if(STATE.settings_open) return;
|
||||
switch(button){
|
||||
case 1:
|
||||
prev();
|
||||
|
@ -185,6 +228,7 @@ Bangle.on('touch', function(button){
|
|||
});
|
||||
|
||||
Bangle.on('swipe', dir => {
|
||||
if(STATE.settings_open) return;
|
||||
if(dir == 1) prev();
|
||||
else next();
|
||||
});
|
||||
|
@ -193,3 +237,10 @@ Bangle.on('swipe', dir => {
|
|||
Bangle.on('lcdPower', on => {
|
||||
if(!on) return load();
|
||||
});
|
||||
|
||||
|
||||
setWatch(prev, BTN1, { repeat: true });
|
||||
setWatch(next, BTN3, { repeat: true });
|
||||
setWatch(run, BTN2, { repeat:true });
|
||||
|
||||
jumpTo(1);
|
|
@ -0,0 +1,59 @@
|
|||
(function(back) {
|
||||
|
||||
const Storage = require("Storage");
|
||||
const filename = 'toucher.json';
|
||||
let settings = Storage.readJSON(filename,1)|| null;
|
||||
|
||||
function getSettings(){
|
||||
return {
|
||||
highres: true,
|
||||
animation : true,
|
||||
frame : 3,
|
||||
debug: true
|
||||
};
|
||||
}
|
||||
|
||||
function updateSettings() {
|
||||
require("Storage").writeJSON(filename, settings);
|
||||
Bangle.buzz();
|
||||
}
|
||||
|
||||
if(!settings){
|
||||
settings = getSettings();
|
||||
updateSettings();
|
||||
}
|
||||
|
||||
function saveChange(name){
|
||||
return function(v){
|
||||
settings[name] = v;
|
||||
updateSettings();
|
||||
}
|
||||
}
|
||||
|
||||
E.showMenu({
|
||||
'': { 'title': 'Toucher settings' },
|
||||
"Resolution" : {
|
||||
value : settings.highres,
|
||||
format : v => v?"High":"Low",
|
||||
onchange: v => {
|
||||
saveChange('highres')(!settings.highres);
|
||||
}
|
||||
},
|
||||
"Animation" : {
|
||||
value : settings.animation,
|
||||
format : v => v?"On":"Off",
|
||||
onchange : saveChange('animation')
|
||||
},
|
||||
"Frame rate" : {
|
||||
value : settings.frame,
|
||||
min: 1, max: 10, step: 1,
|
||||
onchange : saveChange('frame')
|
||||
},
|
||||
"Debug" : {
|
||||
value : settings.debug,
|
||||
format : v => v?"On":"Off",
|
||||
onchange : saveChange('debug')
|
||||
},
|
||||
'< Back': back
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue