2024-04-24 17:18:00 +00:00
|
|
|
(() => {
|
2023-08-28 11:17:07 +00:00
|
|
|
if(!Bangle.prependListener){
|
|
|
|
type Event<T> = T extends `#on${infer Evt}` ? Evt : never;
|
|
|
|
|
|
|
|
Bangle.prependListener = function(
|
|
|
|
evt: Event<keyof BangleEvents>,
|
|
|
|
listener: () => void
|
|
|
|
){
|
|
|
|
// move our drag to the start of the event listener array
|
|
|
|
const handlers = (Bangle as BangleEvents)[`#on${evt}`]
|
|
|
|
|
|
|
|
if(!handlers){
|
|
|
|
Bangle.on(evt as any, listener);
|
|
|
|
}else{
|
|
|
|
if(typeof handlers === "function"){
|
|
|
|
// get Bangle to convert to array
|
|
|
|
Bangle.on(evt as any, listener);
|
|
|
|
}
|
|
|
|
|
|
|
|
// shuffle array
|
|
|
|
(Bangle as BangleEvents)[`#on${evt}`] = [listener as any].concat(
|
|
|
|
(handlers as Array<any>).filter((f: unknown) => f !== listener)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
class Overlay {
|
|
|
|
g2: Graphics;
|
|
|
|
width: number;
|
|
|
|
height: number;
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
// x padding: 10 each side
|
|
|
|
// y top: 24, y bottom: 10
|
|
|
|
this.width = g.getWidth() - 10 * 2;
|
|
|
|
this.height = g.getHeight() - 24 - 10;
|
|
|
|
|
|
|
|
this.g2 = Graphics.createArrayBuffer(
|
|
|
|
this.width,
|
|
|
|
this.height,
|
2024-04-24 17:08:20 +00:00
|
|
|
/*bpp*/4,
|
2023-08-28 11:17:07 +00:00
|
|
|
{ msb: true }
|
|
|
|
);
|
|
|
|
|
|
|
|
this.renderG2();
|
|
|
|
}
|
|
|
|
|
|
|
|
setBottom(bottom: number): void {
|
|
|
|
const { g2 } = this;
|
|
|
|
const y = bottom - this.height;
|
|
|
|
|
|
|
|
Bangle.setLCDOverlay(g2, 10, y - 10);
|
|
|
|
}
|
|
|
|
|
|
|
|
hide(): void {
|
|
|
|
Bangle.setLCDOverlay();
|
|
|
|
}
|
|
|
|
|
|
|
|
renderG2(): void {
|
2023-08-28 21:30:38 +00:00
|
|
|
this.g2
|
2023-08-28 11:19:46 +00:00
|
|
|
.reset()
|
2024-04-25 11:46:52 +00:00
|
|
|
.setColor(g.theme.bg)
|
|
|
|
.fillRect(0, 0, this.width, this.height)
|
|
|
|
.setColor(colour.on.bg)
|
2023-08-28 11:19:46 +00:00
|
|
|
.drawRect(0, 0, this.width - 1, this.height - 1)
|
|
|
|
.drawRect(1, 1, this.width - 2, this.height - 2);
|
2023-08-28 21:30:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-25 11:54:28 +00:00
|
|
|
type ControlCallback = (tap: boolean) => boolean | number;
|
2023-08-28 21:30:38 +00:00
|
|
|
type Control = {
|
|
|
|
x: number,
|
|
|
|
y: number,
|
2024-04-25 11:46:19 +00:00
|
|
|
fg: ColorResolvable,
|
|
|
|
bg: ColorResolvable,
|
2023-08-28 21:30:38 +00:00
|
|
|
text: string,
|
2024-04-25 11:54:28 +00:00
|
|
|
cb: ControlCallback,
|
2024-04-24 16:24:16 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const colour = {
|
|
|
|
on: {
|
2024-04-24 17:08:20 +00:00
|
|
|
fg: "#fff",
|
|
|
|
bg: "#00a",
|
2024-04-24 16:24:16 +00:00
|
|
|
},
|
|
|
|
off: {
|
2024-04-24 17:08:20 +00:00
|
|
|
fg: "#000",
|
|
|
|
bg: "#bbb",
|
2024-04-24 16:24:16 +00:00
|
|
|
},
|
2024-04-25 11:46:19 +00:00
|
|
|
} as const;
|
2023-08-28 21:30:38 +00:00
|
|
|
|
2024-04-25 11:54:28 +00:00
|
|
|
type FiveOf<X> = [X, X, X, X, X];
|
|
|
|
type ControlTemplate = { text: string, cb: ControlCallback };
|
|
|
|
|
2023-08-28 21:30:38 +00:00
|
|
|
class Controls {
|
2024-04-25 11:54:28 +00:00
|
|
|
controls: FiveOf<Control>;
|
2023-08-28 21:30:38 +00:00
|
|
|
|
2024-04-25 11:54:28 +00:00
|
|
|
constructor(g: Graphics, controls: FiveOf<ControlTemplate>) {
|
2023-08-28 21:30:38 +00:00
|
|
|
// const connected = NRF.getSecurityStatus().connected;
|
|
|
|
// if (0&&connected) {
|
|
|
|
// // TODO
|
|
|
|
// return [
|
|
|
|
// { text: "<", cb: hid.next },
|
|
|
|
// { text: "@", cb: hid.toggle },
|
|
|
|
// { text: ">", cb: hid.prev },
|
|
|
|
// { text: "-", cb: hid.down },
|
|
|
|
// { text: "+", cb: hid.up },
|
|
|
|
// ];
|
|
|
|
// }
|
|
|
|
|
|
|
|
const height = g.getHeight();
|
|
|
|
const centreY = height / 2;
|
|
|
|
const circleGapY = 30;
|
|
|
|
const width = g.getWidth();
|
|
|
|
|
|
|
|
this.controls = [
|
2024-04-25 11:54:28 +00:00
|
|
|
{ x: width / 4 - 10, y: centreY - circleGapY },
|
|
|
|
{ x: width / 2, y: centreY - circleGapY },
|
|
|
|
{ x: width * 3/4 + 10, y: centreY - circleGapY },
|
|
|
|
{ x: width / 3, y: centreY + circleGapY },
|
|
|
|
{ x: width * 2/3, y: centreY + circleGapY },
|
|
|
|
].map((xy, i) => {
|
|
|
|
const ctrl = xy as Control;
|
|
|
|
const from = controls[i]!;
|
|
|
|
ctrl.text = from.text;
|
|
|
|
ctrl.cb = from.cb;
|
|
|
|
Object.assign(ctrl, from.cb(false) ? colour.on : colour.off);
|
|
|
|
return ctrl;
|
|
|
|
}) as FiveOf<Control>;
|
2023-08-28 21:30:38 +00:00
|
|
|
}
|
2023-08-28 11:17:07 +00:00
|
|
|
|
2024-04-24 16:24:16 +00:00
|
|
|
draw(g: Graphics, single?: Control): void {
|
2023-08-28 11:17:07 +00:00
|
|
|
g
|
2023-08-28 11:19:46 +00:00
|
|
|
.setFontAlign(0, 0)
|
2024-04-26 17:31:45 +00:00
|
|
|
.setFont("4x6:3" as any);
|
2023-08-28 11:17:07 +00:00
|
|
|
|
2024-04-24 16:24:16 +00:00
|
|
|
for(const ctrl of single ? [single] : this.controls){
|
2023-08-28 21:30:38 +00:00
|
|
|
g
|
2024-04-25 11:46:19 +00:00
|
|
|
.setColor(ctrl.bg)
|
2023-08-28 21:30:38 +00:00
|
|
|
.fillCircle(ctrl.x, ctrl.y, 23)
|
2024-04-25 11:46:19 +00:00
|
|
|
.setColor(ctrl.fg)
|
2023-08-28 21:30:38 +00:00
|
|
|
.drawString(ctrl.text, ctrl.x, ctrl.y);
|
|
|
|
}
|
2023-08-28 11:21:58 +00:00
|
|
|
}
|
|
|
|
|
2023-08-28 21:30:38 +00:00
|
|
|
hitTest(x: number, y: number): Control | undefined {
|
2024-04-24 16:21:53 +00:00
|
|
|
let dist = Infinity;
|
|
|
|
let closest;
|
2023-08-28 11:17:07 +00:00
|
|
|
|
2024-04-24 16:21:53 +00:00
|
|
|
for(const ctrl of this.controls){
|
|
|
|
const dx = x-ctrl.x;
|
|
|
|
const dy = y-ctrl.y;
|
|
|
|
const d = Math.sqrt(dx*dx + dy*dy);
|
|
|
|
if(d < dist){
|
|
|
|
dist = d;
|
|
|
|
closest = ctrl;
|
|
|
|
}
|
|
|
|
}
|
2023-08-28 11:17:07 +00:00
|
|
|
|
2024-04-24 16:21:53 +00:00
|
|
|
return dist < 30 ? closest : undefined;
|
2023-08-28 11:17:07 +00:00
|
|
|
}
|
|
|
|
}
|
2023-08-04 21:48:40 +00:00
|
|
|
|
2023-07-27 16:21:36 +00:00
|
|
|
const enum State {
|
|
|
|
Idle,
|
|
|
|
TopDrag,
|
|
|
|
IgnoreCurrent,
|
|
|
|
Active,
|
|
|
|
}
|
2024-04-24 16:24:16 +00:00
|
|
|
type UI = { overlay: Overlay, ctrls: Controls };
|
2023-08-28 11:18:31 +00:00
|
|
|
let state = State.Idle;
|
2023-07-27 16:21:36 +00:00
|
|
|
let startY = 0;
|
2023-08-28 11:17:07 +00:00
|
|
|
let startedUpDrag = false;
|
|
|
|
let upDragAnim: IntervalId | undefined;
|
2024-04-24 16:24:16 +00:00
|
|
|
let ui: undefined | UI;
|
2023-08-28 11:17:07 +00:00
|
|
|
let touchDown = false;
|
2024-04-24 17:18:00 +00:00
|
|
|
|
2024-04-25 11:48:18 +00:00
|
|
|
const initUI = () => {
|
|
|
|
if (ui) return;
|
|
|
|
|
2024-04-25 11:54:28 +00:00
|
|
|
const controls: FiveOf<ControlTemplate> = [
|
|
|
|
{
|
|
|
|
text: "BLE",
|
|
|
|
cb: tap => {
|
|
|
|
const on = NRF.getSecurityStatus().advertising;
|
|
|
|
if(tap){
|
|
|
|
if(on) NRF.sleep();
|
|
|
|
else NRF.wake();
|
|
|
|
}
|
|
|
|
return on !== tap; // on ^ tap
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: "DnD",
|
|
|
|
cb: tap => {
|
2024-05-04 20:53:34 +00:00
|
|
|
let on;
|
2024-05-04 21:21:43 +00:00
|
|
|
if((on = !!origBuzz)){
|
2024-04-25 11:54:28 +00:00
|
|
|
if(tap){
|
|
|
|
Bangle.buzz = origBuzz;
|
|
|
|
origBuzz = undefined;
|
|
|
|
}
|
|
|
|
}else{
|
|
|
|
if(tap){
|
|
|
|
origBuzz = Bangle.buzz;
|
2024-04-25 17:29:23 +00:00
|
|
|
Bangle.buzz = () => Promise.resolve();
|
2024-04-25 11:54:28 +00:00
|
|
|
setTimeout(() => {
|
|
|
|
if(!origBuzz) return;
|
|
|
|
Bangle.buzz = origBuzz;
|
|
|
|
origBuzz = undefined;
|
|
|
|
}, 1000 * 60 * 10);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return on !== tap; // on ^ tap
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: "HRM",
|
|
|
|
cb: tap => {
|
|
|
|
const id = "widhid";
|
|
|
|
const hrm = (Bangle as any)._PWR?.HRM as undefined | Array<string> ;
|
|
|
|
const off = !hrm || hrm.indexOf(id) === -1;
|
|
|
|
if(off){
|
|
|
|
if(tap)
|
|
|
|
Bangle.setHRMPower(1, id);
|
|
|
|
}else if(tap){
|
|
|
|
Bangle.setHRMPower(0, id);
|
|
|
|
}
|
|
|
|
return !off !== tap; // on ^ tap
|
|
|
|
}
|
|
|
|
},
|
2024-04-26 17:43:05 +00:00
|
|
|
{
|
|
|
|
text: "clk",
|
|
|
|
cb: tap => {
|
|
|
|
if (tap) Bangle.showClock(), terminateUI();
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: "lch",
|
|
|
|
cb: tap => {
|
|
|
|
if (tap) Bangle.showLauncher(), terminateUI();
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
},
|
2024-04-25 11:54:28 +00:00
|
|
|
];
|
|
|
|
|
2024-04-25 11:48:18 +00:00
|
|
|
const overlay = new Overlay();
|
|
|
|
ui = {
|
|
|
|
overlay,
|
|
|
|
ctrls: new Controls(overlay.g2, controls),
|
|
|
|
};
|
|
|
|
ui.ctrls.draw(ui.overlay.g2);
|
|
|
|
};
|
|
|
|
|
2024-04-26 17:42:23 +00:00
|
|
|
const terminateUI = () => {
|
|
|
|
state = State.Idle;
|
|
|
|
ui?.overlay.hide();
|
|
|
|
ui = undefined;
|
|
|
|
};
|
|
|
|
|
2024-04-27 09:11:41 +00:00
|
|
|
const onSwipe = () => {
|
|
|
|
switch (state) {
|
|
|
|
case State.Idle:
|
|
|
|
case State.IgnoreCurrent:
|
|
|
|
return;
|
|
|
|
|
|
|
|
case State.TopDrag:
|
|
|
|
case State.Active:
|
|
|
|
E.stopEventPropagation?.();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
Bangle.prependListener('swipe', onSwipe);
|
|
|
|
|
2024-04-24 17:18:00 +00:00
|
|
|
const onDrag = (e => {
|
2023-08-28 11:17:07 +00:00
|
|
|
const dragDistance = 30;
|
2023-08-04 22:18:37 +00:00
|
|
|
|
2023-08-28 11:17:07 +00:00
|
|
|
if (e.b === 0) touchDown = startedUpDrag = false;
|
2023-08-04 22:18:37 +00:00
|
|
|
|
2023-07-27 16:21:36 +00:00
|
|
|
switch (state) {
|
|
|
|
case State.IgnoreCurrent:
|
2024-04-25 11:53:27 +00:00
|
|
|
if(e.b === 0)
|
2023-08-28 11:19:46 +00:00
|
|
|
state = State.Idle;
|
|
|
|
break;
|
2023-07-27 16:21:36 +00:00
|
|
|
|
|
|
|
case State.Idle:
|
2023-08-04 22:18:37 +00:00
|
|
|
if(e.b && !touchDown){ // no need to check Bangle.CLKINFO_FOCUS
|
2023-08-28 11:19:46 +00:00
|
|
|
if(e.y <= 40){
|
|
|
|
state = State.TopDrag
|
|
|
|
startY = e.y;
|
2024-04-27 09:00:22 +00:00
|
|
|
E.stopEventPropagation?.();
|
2024-04-24 16:25:48 +00:00
|
|
|
//console.log(" topdrag detected, starting @ " + startY);
|
2023-08-28 11:19:46 +00:00
|
|
|
}else{
|
2024-04-24 16:25:48 +00:00
|
|
|
//console.log(" ignoring this drag (too low @ " + e.y + ")");
|
2023-08-28 11:19:46 +00:00
|
|
|
state = State.IgnoreCurrent;
|
|
|
|
}
|
2023-08-28 11:17:07 +00:00
|
|
|
}
|
2023-08-28 11:19:46 +00:00
|
|
|
break;
|
2023-07-27 16:21:36 +00:00
|
|
|
|
|
|
|
case State.TopDrag:
|
|
|
|
if(e.b === 0){
|
2024-04-24 16:25:48 +00:00
|
|
|
//console.log("topdrag stopped, distance: " + (e.y - startY));
|
2023-08-28 11:19:46 +00:00
|
|
|
if(e.y > startY + dragDistance){
|
2024-04-24 16:25:48 +00:00
|
|
|
//console.log("activating");
|
2024-04-25 11:48:18 +00:00
|
|
|
initUI();
|
2023-08-28 11:19:46 +00:00
|
|
|
state = State.Active;
|
|
|
|
startY = 0;
|
|
|
|
Bangle.prependListener("touch", onTouch);
|
|
|
|
Bangle.buzz(20);
|
2023-08-28 21:30:38 +00:00
|
|
|
ui!.overlay.setBottom(g.getHeight());
|
2024-04-25 11:53:27 +00:00
|
|
|
}else{
|
|
|
|
//console.log("returning to idle");
|
2024-04-26 17:42:23 +00:00
|
|
|
terminateUI();
|
2024-04-27 09:00:22 +00:00
|
|
|
break; // skip stopEventPropagation
|
2023-08-28 11:19:46 +00:00
|
|
|
}
|
|
|
|
}else{
|
|
|
|
// partial drag, show UI feedback:
|
|
|
|
const dragOffset = 32;
|
2023-08-28 11:17:07 +00:00
|
|
|
|
2024-04-25 11:48:18 +00:00
|
|
|
initUI();
|
|
|
|
ui!.overlay.setBottom(e.y - dragOffset);
|
2023-08-28 11:19:46 +00:00
|
|
|
}
|
2024-04-24 16:25:16 +00:00
|
|
|
E.stopEventPropagation?.();
|
2023-08-28 11:19:46 +00:00
|
|
|
break;
|
2023-07-27 16:21:36 +00:00
|
|
|
|
|
|
|
case State.Active:
|
2023-08-28 21:30:38 +00:00
|
|
|
//console.log("stolen drag handling, do whatever here");
|
2023-08-28 11:19:46 +00:00
|
|
|
E.stopEventPropagation?.();
|
|
|
|
if(e.b){
|
|
|
|
if(!touchDown){
|
|
|
|
startY = e.y;
|
|
|
|
}else if(startY){
|
|
|
|
const dist = Math.max(0, startY - e.y);
|
|
|
|
|
|
|
|
if (startedUpDrag || (startedUpDrag = dist > 10)) // ignore small drags
|
2023-08-28 21:30:38 +00:00
|
|
|
ui!.overlay.setBottom(g.getHeight() - dist);
|
|
|
|
}
|
|
|
|
}else if(e.b === 0){
|
|
|
|
if((startY - e.y) > dragDistance){
|
|
|
|
let bottom = g.getHeight() - Math.max(0, startY - e.y);
|
|
|
|
|
|
|
|
if (upDragAnim) clearInterval(upDragAnim);
|
|
|
|
upDragAnim = setInterval(() => {
|
|
|
|
if (!ui || bottom <= 0) {
|
|
|
|
clearInterval(upDragAnim!);
|
|
|
|
upDragAnim = undefined;
|
2024-04-26 17:42:23 +00:00
|
|
|
terminateUI();
|
2023-08-28 21:30:38 +00:00
|
|
|
return;
|
|
|
|
}
|
2024-04-25 11:48:18 +00:00
|
|
|
ui.overlay.setBottom(bottom);
|
2023-08-28 21:30:38 +00:00
|
|
|
bottom -= 30;
|
|
|
|
}, 50)
|
|
|
|
|
|
|
|
Bangle.removeListener("touch", onTouch);
|
|
|
|
state = State.Idle;
|
|
|
|
}else{
|
|
|
|
ui!.overlay.setBottom(g.getHeight());
|
2023-08-28 11:17:07 +00:00
|
|
|
}
|
2023-08-28 11:19:46 +00:00
|
|
|
}
|
|
|
|
break;
|
2024-04-24 17:18:00 +00:00
|
|
|
}
|
2023-08-28 11:17:07 +00:00
|
|
|
if(e.b) touchDown = true;
|
2024-04-24 17:18:00 +00:00
|
|
|
}) satisfies DragCallback;
|
|
|
|
|
2023-08-28 21:30:38 +00:00
|
|
|
const onTouch = ((_btn, xy) => {
|
|
|
|
if(!ui || !xy) return;
|
|
|
|
|
|
|
|
const top = g.getHeight() - ui.overlay.height; // assumed anchored to bottom
|
|
|
|
const left = (g.getWidth() - ui.overlay.width) / 2; // more assumptions
|
|
|
|
|
|
|
|
const ctrl = ui.ctrls.hitTest(xy.x - left, xy.y - top);
|
2024-04-24 16:24:16 +00:00
|
|
|
if(ctrl){
|
|
|
|
onCtrlTap(ctrl, ui);
|
2024-04-24 16:25:16 +00:00
|
|
|
E.stopEventPropagation?.();
|
2024-04-24 16:24:16 +00:00
|
|
|
}
|
2023-08-28 11:17:07 +00:00
|
|
|
}) satisfies TouchCallback;
|
2023-08-04 22:18:37 +00:00
|
|
|
|
2024-04-24 16:24:16 +00:00
|
|
|
let origBuzz: undefined | (() => Promise<void>);
|
|
|
|
const onCtrlTap = (ctrl: Control, ui: UI) => {
|
2024-04-25 11:52:41 +00:00
|
|
|
Bangle.buzz(20);
|
2024-04-24 18:38:44 +00:00
|
|
|
|
2024-04-25 11:54:28 +00:00
|
|
|
const col = ctrl.cb(true) ? colour.on : colour.off;
|
2024-04-24 16:24:16 +00:00
|
|
|
ctrl.fg = col.fg;
|
|
|
|
ctrl.bg = col.bg;
|
|
|
|
//console.log("hit on " + ctrl.text + ", col: " + ctrl.fg);
|
|
|
|
|
|
|
|
ui.ctrls.draw(ui.overlay.g2, ctrl);
|
|
|
|
};
|
|
|
|
|
2023-08-28 11:17:07 +00:00
|
|
|
Bangle.prependListener("drag", onDrag);
|
2024-04-26 17:42:23 +00:00
|
|
|
Bangle.on("lock", terminateUI);
|
2023-07-27 16:21:36 +00:00
|
|
|
|
2024-04-24 17:18:00 +00:00
|
|
|
|
2024-04-24 16:07:07 +00:00
|
|
|
/*
|
|
|
|
const settings = require("Storage").readJSON("setting.json", true) as Settings || ({ HID: false } as Settings);
|
|
|
|
const haveMedia = settings.HID === "kbmedia";
|
|
|
|
// @ts-ignore
|
|
|
|
delete settings;
|
|
|
|
|
|
|
|
const sendHid = (code: number) => {
|
|
|
|
try{
|
|
|
|
NRF.sendHIDReport(
|
|
|
|
[1, code],
|
|
|
|
() => NRF.sendHIDReport([1, 0]),
|
|
|
|
);
|
|
|
|
}catch(e){
|
|
|
|
console.log("sendHIDReport:", e);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const hid = haveMedia ? {
|
|
|
|
next: () => sendHid(0x01),
|
|
|
|
prev: () => sendHid(0x02),
|
|
|
|
toggle: () => sendHid(0x10),
|
|
|
|
up: () => sendHid(0x40),
|
|
|
|
down: () => sendHid(0x80),
|
|
|
|
} : null;
|
|
|
|
*/
|
2024-04-24 17:18:00 +00:00
|
|
|
})()
|