Skip to content

Commit eb115c1

Browse files
authored
Merge pull request #3847 from thyttan/spotrem
spotrem: add volume knob gesture
2 parents 555d8fc + d6a0dae commit eb115c1

File tree

8 files changed

+342
-6
lines changed

8 files changed

+342
-6
lines changed

apps/spotrem/ChangeLog

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ when fastloading.
1212
0.11: Further refactoring to shorten the code. Fixed search and play that was broken in v0.10.
1313
0.12: Fix some warnings from the linter.
1414
0.13: Move ui-handlers inside setUI-call.
15+
0.14: Add volume knob.
16+

apps/spotrem/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Swipe input:
88

99
Swipe left/right to go to previous/next track. Swipe up/down to change the volume.
1010

11+
If you have changed the volume by swipe up/down, you can use a "volume knob" to continuously change the volume. Clock wise circle gesture on the screen increases volume and vice versa. The knob will deactivate shortly after you release the finger from the watch screen, indicated by a double buzz.
12+
1113
It's possible to start tracks by searching with the remote. Search term without tags will override search with tags.
1214

1315
To start playing 'from cold' the command for previous/next track via touch or swipe can be used. Pressing just play/pause is not guaranteed to initiate spotify in all circumstances (this will probably change with subsequent releases).

apps/spotrem/app.js

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,18 +97,83 @@ let swipeHandler = function(LR, _) {
9797
}
9898
};
9999

100+
let dx = 0;
101+
let dy = 0;
102+
let volumeChangedThisGoAround = false;
103+
let knobTimeout;
104+
let dragHandler = function(e) {
105+
106+
let DialDisplay = require("Dial_Display");
107+
let volumeKnobVisual = new DialDisplay();
108+
109+
let cb = ud => {
110+
Bangle.musicControl(ud<0 ? "volumedown" : "volumeup");
111+
Bangle.buzz(20);
112+
}
113+
114+
let resetOuterScopeVariables = ()=>{
115+
dy=0;
116+
dx=0;
117+
volumeChangedThisGoAround=false;
118+
}
119+
120+
dx += e.dx;
121+
dy += e.dy;
122+
if (!e.b) {resetOuterScopeVariables();}
123+
124+
while (Math.abs(dy)>32) {
125+
if (dy>0) { dy-=32; cb(-1) }
126+
else { dy+=32; cb(1) }
127+
volumeChangedThisGoAround = true;
128+
}
129+
130+
if (volumeChangedThisGoAround && Math.abs(dx)>32) {
131+
// setup volume knob here.
132+
let cbVisual = (step)=>{
133+
cb(step);
134+
volumeKnobVisual.step(step);
135+
};
136+
cbVisual(Math.sign(dx)*Math.sign(g.getHeight()/2-e.y));
137+
resetOuterScopeVariables();
138+
let volumeKnob = require("dial")(cbVisual);
139+
let timingOutVolumeKnob = (e)=>{
140+
if (!e.b) {
141+
setKnobTimeout();
142+
} else if (knobTimeout) {
143+
clearTimeout(knobTimeout);
144+
knobTimeout = undefined;
145+
}
146+
volumeKnob(e);
147+
}
148+
let swipeMask = ()=>{
149+
E.stopEventPropagation();
150+
}
151+
let setKnobTimeout = ()=>{
152+
knobTimeout = setTimeout(()=>{
153+
Bangle.removeListener("drag", timingOutVolumeKnob)
154+
Bangle.removeListener("swipe", swipeMask);
155+
Bangle.buzz(40);
156+
setTimeout(Bangle.buzz, 150, 40, 0.8)
157+
gfx();
158+
knobTimeout = undefined;
159+
print("removed volume knob")
160+
}, 350);
161+
}
162+
Bangle.prependListener("drag", timingOutVolumeKnob);
163+
Bangle.prependListener("swipe", swipeMask);
164+
}
165+
};
166+
100167
// Navigation input on the main layout
101168
let setUI = function() {
102169
Bangle.setUI(
103-
{mode : "updown",
170+
{mode : "custom",
104171
touch: touchHandler,
105172
swipe: swipeHandler,
173+
drag: dragHandler,
106174
btn: ()=>load(),
107175
remove : ()=>widgetUtils.show(),
108-
},
109-
ud => {
110-
if (ud) Bangle.musicControl(ud>0 ? "volumedown" : "volumeup");
111-
}
176+
}
112177
);
113178
};
114179

apps/spotrem/metadata.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "spotrem",
33
"name": "Remote for Spotify",
4-
"version": "0.13",
4+
"version": "0.14",
55
"author": "thyttan",
66
"description": "Control spotify on your android device.",
77
"readme": "README.md",

modules/Dial_Display.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
function DialDisplay(options) {
2+
const SCREEN_W = g.getWidth();
3+
const SCREEN_H = g.getHeight();
4+
5+
this.options = Object.assign(
6+
{
7+
stepsPerWholeTurn : 7, // 7 chosen as it felt the best in use.
8+
dialRect : {
9+
x: 0,
10+
y: 0,
11+
w: SCREEN_W,
12+
h: SCREEN_H,
13+
},
14+
}, options);
15+
16+
this.value = 0;
17+
this.isFullDraw = true;
18+
}
19+
20+
DialDisplay.prototype.queueRedraw = function() {
21+
this.isFullDraw = true;
22+
this.prevDrawnValue = null;
23+
};
24+
25+
DialDisplay.prototype.set = function(value) {
26+
this.value = value;
27+
};
28+
29+
DialDisplay.prototype.step = function(step) {
30+
"ram";
31+
this.value += step;
32+
//g.setFont("Vector:30");
33+
//g.drawString(this.value);
34+
35+
const DIAL_RECT = this.options.dialRect;
36+
37+
const CENTER = {
38+
x: DIAL_RECT.x + DIAL_RECT.w / 2,
39+
y: DIAL_RECT.y + DIAL_RECT.h / 2,
40+
};
41+
42+
let drawCircle = (value, R, G, B, rad, isFill)=>{
43+
let x = CENTER.x+27*Math.sin(value*(2*Math.PI/this.options.stepsPerWholeTurn));
44+
let y = CENTER.y-27*Math.cos(value*(2*Math.PI/this.options.stepsPerWholeTurn));
45+
g.setColor(R,G,B)
46+
if (!isFill) g.drawCircle(x, y, rad);
47+
if (isFill) g.fillCircle(x, y, rad);
48+
}
49+
if (this.isFullDraw) {
50+
g.setColor(0,0,0).fillCircle(CENTER.x, CENTER.y, 25);
51+
g.setColor(1,1,1).drawCircle(CENTER.x, CENTER.y, 25);
52+
for (let i=0; i<this.options.stepsPerWholeTurn; i++) {
53+
drawCircle(i, 1, 1, 1, 1, true);
54+
}
55+
this.isFullDraw = false;
56+
}
57+
58+
//drawCircle(this.value, 1, 1, 1, 2, false);
59+
//drawCircle(prevValue, 0, 0, 0, 2, false);
60+
g.setColor(0,0,0).drawLine(CENTER.x, CENTER.y, CENTER.x+23*Math.sin(this.prevDrawnValue*(2*Math.PI/this.options.stepsPerWholeTurn)), CENTER.y-23*Math.cos(this.prevDrawnValue*(2*Math.PI/this.options.stepsPerWholeTurn)));
61+
g.setColor(1,1,1).drawLine(CENTER.x, CENTER.y, CENTER.x+23*Math.sin(this.value*(2*Math.PI/this.options.stepsPerWholeTurn)), CENTER.y-23*Math.cos(this.value*(2*Math.PI/this.options.stepsPerWholeTurn)));
62+
g.setColor(0,0,0).fillCircle(CENTER.x, CENTER.y, 9);
63+
64+
this.prevDrawnValue = this.value;
65+
};
66+
67+
exports = DialDisplay;

modules/Dial_Display.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
Bangle.js Dial Display Module
2+
=============================
3+
4+
5+
> Take a look at README.md for hints on developing with this module.
6+
7+
Usage
8+
-----
9+
10+
```JS
11+
var DialDisplay = require("Dial_Display");
12+
var dialDisplay = new DialDisplay(options);
13+
14+
dialDisplay.step(-1);
15+
16+
var value = dialDisplay.value;
17+
18+
// ... after some time:
19+
dialDisplay.queueRedraw();
20+
dialDisplay.set(0);
21+
```
22+
23+
For example in use with the Dial module:
24+
25+
```JS
26+
var options = {
27+
stepsPerWholeTurn : 6,
28+
dialRect : {
29+
x: 0,
30+
y: 0,
31+
w: g.getWidth()/2,
32+
h: g.getHeight()/2,
33+
}
34+
}
35+
36+
var DialDisplay = require("Dial_Display");
37+
var dialDisplay = new DialDisplay(options);
38+
39+
var cb = (step) => {
40+
dialDisplay.step(step);
41+
};
42+
43+
var dial = require("dial")(cb, options)
44+
Bangle.on("drag", dial);
45+
```
46+
47+
`options` (first argument) (optional) is an object containing:
48+
49+
`stepsPerWholeTurn` - how many steps there are in one complete turn of the dial.
50+
`dialRect` - decides the area of the screen the dial is set up on.
51+
52+
Defaults:
53+
```js
54+
{ stepsPerWholeTurn : 7
55+
dialRect : {
56+
x: 0,
57+
y: 0,
58+
w: g.getWidth(),
59+
h: g.getHeight(),
60+
},
61+
}
62+
```
63+
64+
The Dial Display has three functions:
65+
`step(amount)` - +1 or -1 to step the dial.
66+
`set(value)` - set the value for the next `step()` to act on.
67+
`queueRedraw()` - draw all of the dial (instead of just the indicator) on the next `step()`.
68+
69+
Notes:
70+
======
71+
- It would be nice to choose what colors are used. Something for the future.

modules/dial.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
function dial(cb, options) {
2+
"ram";
3+
const SCREEN_W = g.getWidth();
4+
const SCREEN_H = g.getHeight();
5+
6+
options = Object.assign(
7+
{ stepsPerWholeTurn : 7, // 7 chosen as it felt the best in use.
8+
dialRect : {
9+
x: 0,
10+
y: 0,
11+
w: SCREEN_W,
12+
h: SCREEN_H,
13+
},
14+
}, options);
15+
16+
const DIAL_RECT = options.dialRect;
17+
18+
const CENTER = {
19+
x: DIAL_RECT.x + DIAL_RECT.w / 2,
20+
y: DIAL_RECT.y + DIAL_RECT.h / 2,
21+
};
22+
23+
const BASE_SCREEN_W = 176;
24+
const STEPS_PER_TURN = options.stepsPerWholeTurn;
25+
const BASE_THRESHOLD = 50;
26+
const THRESHOLD =
27+
BASE_THRESHOLD *
28+
(10 / STEPS_PER_TURN) *
29+
(DIAL_RECT.w / BASE_SCREEN_W);
30+
31+
let cumulative = 0;
32+
33+
function onDrag(e) {
34+
"ram";
35+
36+
if (
37+
e.x < DIAL_RECT.x ||
38+
e.x > DIAL_RECT.x+DIAL_RECT.w-1 ||
39+
e.y < DIAL_RECT.y ||
40+
e.y > DIAL_RECT.y+DIAL_RECT.h-1
41+
) {
42+
return;
43+
}
44+
45+
if (e.y < CENTER.y) {
46+
cumulative += e.dx;
47+
} else {
48+
cumulative -= e.dx;
49+
}
50+
51+
if (e.x < CENTER.x) {
52+
cumulative -= e.dy;
53+
} else {
54+
cumulative += e.dy;
55+
}
56+
57+
function stepHandler(step) {
58+
cumulative -= THRESHOLD * step;
59+
cb(step);
60+
}
61+
62+
while (cumulative > THRESHOLD) {
63+
stepHandler(1);
64+
}
65+
while (cumulative < -THRESHOLD) {
66+
stepHandler(-1);
67+
}
68+
69+
E.stopEventPropagation();
70+
}
71+
72+
return onDrag;
73+
}
74+
75+
exports = dial;

modules/dial.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
Bangle.js Dial Module
2+
=====================
3+
4+
> Take a look at README.md for hints on developing with this module.
5+
6+
Usage
7+
-----
8+
9+
```JS
10+
var dial = require("dial")(cb, options);
11+
Bangle.on("drag", dial);
12+
```
13+
14+
For example:
15+
16+
```JS
17+
let cb = (plusOrMinusOne)=>{print(plusOrMinusOne)};
18+
let options = {
19+
stepsPerWholeTurn : 6,
20+
dialRect : {
21+
x: 0,
22+
y: 0,
23+
w: g.getWidth()/2,
24+
h: g.getHeight()/2,
25+
}
26+
}
27+
28+
let dial = require("dial")(cb, options);
29+
Bangle.on("drag", dial);
30+
```
31+
32+
`cb` (first argument) is a callback function that should expect either `+1` or `-1` as argument when called.
33+
34+
`options` (second argument) (optional) is an object containing:
35+
36+
`stepsPerWholeTurn` - how many steps there are in one complete turn of the dial.
37+
`dialRect` - decides the area of the screen the dial is set up on.
38+
39+
Defaults:
40+
```js
41+
{ stepsPerWholeTurn : 7
42+
dialRect : {
43+
x: 0,
44+
y: 0,
45+
w: g.getWidth(),
46+
h: g.getHeight(),
47+
},
48+
}
49+
```
50+
51+
Notes
52+
-----
53+
54+
A complementary module for drawing graphics is provided in the Dial_Display module.

0 commit comments

Comments
 (0)