Merge pull request #2995 from SarahAlroe/master

Add rebbleagenda, pebble-like animated agenda.
pull/2997/head
Gordon Williams 2023-09-04 16:40:40 +01:00 committed by GitHub
commit e2d8a23672
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 703 additions and 0 deletions

View File

@ -0,0 +1 @@
0.01: Initial version

View File

@ -0,0 +1,24 @@
# Rebble Agenda
Agenda app for showing upcoming events in an animated fashion.
Heavily inspired by the inbuilt agenda of the pebble time.
Switch between calendar events by swiping up or down. Click the button to exit.
![Two events shown using the default light system theme](./screenshot_rebbleagenda_events.png) ![The last event of the agenda shown using a custom red theme](./screenshot_rebbleagenda_customtheme.png) ![An animated sun shows the day of the following events](./screenshot_rebbleagenda_sun.png)
## Settings
- *Use system theme* - Use the colors of the system theme. Otherwise use following colors.
- *Accent* - The color of the rightmost accent bar if not following system theme.
- *Background* - The background color to use if not following system theme.
- *Foreground* - The foreground color to use if not following system theme.
## Notes
- The weather icon in the top right corner is currently just showing the current weather as provided by [weather](https://github.com/espruino/BangleApps/blob/master/apps/weather/). Closest forecast to be implemented in a future release.
- Events only show as much of their title and description as can be fit on the screen, which is one and four (wrapped) lines respectively.
- Events are loaded from ```android.calendar.json```, which is read in its entirety. If you have a very busy schedule, loading may take a second or two.
## Creator
- [Sarah Alrøe](https://github.com/SarahAlroe), August+September 2023

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/ADuIUCARRDhgePCKIv13YAEDoYJFAA4RJFyQvcGBYRGy4dDy4uLCJgv/DoOBDgOBF5oRLF6IeBDgIvNCJYvQDwQuNCJovRADov/F9OsAEgv/F/4v/F6OIAAgHRF/4v/F/4v/CI4AdF/6HR3YAEF/4v/F/4v/F5IAdF/4v/F/4vJGEguKGEYuMAH4A/AH4A/ADIA=="))

583
apps/rebbleagenda/app.js Normal file
View File

@ -0,0 +1,583 @@
{
/* Requires */
const weather = require('weather');
require("Font6x12").add(Graphics);
require("Font8x16").add(Graphics);
const SETTINGS_FILE = "rebbleagenda.json";
const settings = require("Storage").readJSON(SETTINGS_FILE, 1) || {'system':true, 'bg': '#fff','fg': '#000','acc': '#0FF'};
/* Layout consts */
const MARKER_SIZE = 4;
const BORDER_SIZE = 6;
const WIDGET_SIZE = 24;
const PRIMARY_OFFSET = WIDGET_SIZE + BORDER_SIZE + MARKER_SIZE - 20 / 2;
const SECONDARY_OFFSET = g.getHeight() - WIDGET_SIZE - 16 - 20;
const MARKER_POS_UPPER = Uint8Array([g.getWidth() - BORDER_SIZE - MARKER_SIZE, WIDGET_SIZE + BORDER_SIZE + MARKER_SIZE]);
const PIN_SIZE = 10;
const ACCENT_WIDTH = 2 * BORDER_SIZE + 2 * MARKER_SIZE; // <20>=2r, borders each side.
const TEXT_COLOR = settings.system?g.theme.fg:settings.fg;
const BG_COLOR = settings.system?g.theme.bg:settings.bg;
const ACCENT_COLOR = settings.system?g.theme.bgH:settings.acc;
const SUN_COLOR_START = 0xF800;
const SUN_COLOR_END = 0xFFE0;
const SUN_FACE = 0x0000;
/* Animation polygon sets*/
const CLEAR_POLYS_1 = [
new Uint8Array([0, 176, 0, 0, 176, 0, 176, 0, 0, 0, 0, 176]),
new Uint8Array([0, 176, 0, 0, 176, 0, 170, 7, 10, 12, 7, 168]),
new Uint8Array([0, 176, 0, 0, 176, 0, 139, 49, 41, 45, 43, 125]),
new Uint8Array([0, 176, 0, 0, 176, 0, 90, 81, 82, 86, 85, 94]),
new Uint8Array([0, 176, 0, 0, 176, 0, 91, 85, 85, 85, 85, 91])
];
const CLEAR_POLYS_2 = [
new Uint8Array([0, 176, 176, 176, 176, 0, 176, 0, 176, 176, 0, 176]),
new Uint8Array([0, 176, 176, 176, 176, 0, 170, 7, 162, 161, 7, 168]),
new Uint8Array([0, 176, 176, 176, 176, 0, 139, 49, 130, 126, 43, 125]),
new Uint8Array([0, 176, 176, 176, 176, 0, 90, 81, 95, 89, 85, 94]),
new Uint8Array([0, 176, 176, 176, 176, 0, 91, 85, 91, 91, 85, 91])
];
const BREATHING_POLYS = [
new Uint8Array([72, 88, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 78, 84, 88]),
new Uint8Array([63, 88, 64, 73, 78, 73, 78, 73, 78, 73, 78, 73, 92, 73, 93, 88]),
new Uint8Array([60, 88, 56, 76, 78, 60, 78, 60, 78, 60, 78, 60, 100, 76, 96, 88]),
new Uint8Array([56, 88, 50, 78, 64, 54, 78, 54, 78, 54, 92, 54, 106, 78, 100, 88]),
new Uint8Array([53, 88, 47, 80, 52, 53, 78, 41, 78, 41, 104, 53, 109, 80, 103, 88]),
new Uint8Array([50, 88, 43, 81, 43, 51, 63, 32, 92, 32, 113, 51, 113, 81, 106, 88])];
const SUN_EYE_LEFT_POLY = new Uint8Array([56, 52, 64, 44, 72, 52, 72, 55, 69, 54, 64, 50, 58, 55, 56, 55]);
const SUN_EYE_RIGHT_OFFSET = 30;
const MOUTH_POLY = new Uint8Array([78, 77, 68, 75, 67, 73, 69, 71, 78, 73, 87, 71, 89, 73, 88, 75]);
/* Animation timings */
const TIME_CLEAR_ANIM = 400;
const TIME_CLEAR_BREAK = 10;
const TIME_DEFAULT_ANIM = 300;
const TIME_BUMP_ANIM = 200;
const TIME_EXIT_ANIM = 500;
const TIME_EVENT_CHANGE = 150;
const TIME_EVENT_BREAK_IN = 300;
const TIME_EVENT_BREAK_ANIM = 800;
const TIME_EVENT_BREAK_HALT = 500;
const TIME_EVENT_BREAK_OUT = 500;
/* Utility functions */
/**
* Check if two dates occur on the same day
* @param {Date} d1 The first date to compare
* @param {Date} d2 The second date to compare
* @returns {Boolean} The two dates are on the same day
*/
const isSameDay = function (d1, d2) {
return (d1.getDate() == d2.getDate() && d1.getMonth() == d2.getMonth() && d1.getFullYear() == d2.getFullYear());
};
/**
* Apply sinusoidal easing to a value 0-1
* @param {Number} x Number to ease
* @returns {Number} Ease of x
*/
const ease = function (x) {
"jit";
return 1 - (Math.cos(Math.PI * x) + 1) / 2;
};
/**
* Map from 0-1 to a number interval
* @param {Number} outMin Minimum output number
* @param {Number} outMax Maximum output number
* @param {Number} x Number between 0 and 1 to map from
* @returns {Number} x mapped between min and max
*/
const map = function (outMin, outMax, x) {
"jit";
return outMin + x * (outMax - outMin);
};
/**
* Return [0-1] progress through an interval
* @param {Number} start When the interval was started in ms
* @param {Number} end When the interval is supposed to stop in ms
* @returns {Number} Value between 0 and 1 reflecting progress through interval
*/
const timeProgress = function (start, end) {
"jit";
const length = end - start;
const delta = Date.now() - start;
return Math.min(Math.max(delta / length, 0), 1);
};
/**
* Interpolate between sets of polygon coordinates
* @param {Array} polys An array of arrays, each containing an equally long set of coordinates
* @param {Number} pos Progress through interpolation [0-1]
* @returns {Array} Interpolation between the two closest sets of coordinates
*/
const interpolatePoly = function (polys, pos) {
const span = polys.length - 1;
pos = pos * span;
pos = pos > span ? span : pos;
const upper = polys[Math.ceil(pos)];
const lower = polys[Math.floor(Math.max(pos - 0.000001, 0))];
const interp = pos - Math.floor(pos - 0.000001);
return upper.map((up, i) => {
return Math.round(up * interp + lower[i] * (1 - interp));
});
};
/**
* Repeatedly call callback with progress through an interval of length time
* @param {Function} anim Callback which takes i, animation progress [0-1]
* @param {Number} time How many ms the animation should last
* @returns {void}
*/
const doAnim = function (anim, time) {
const animStart = Date.now();
const animEnd = animStart + time;
let i = 0;
do {
i = timeProgress(animStart, animEnd);
anim(i);
} while (i < 1);
anim(1);
};
/* Screen draw functions */
/**
* Draw an event
* @param {Number} index Index in the events array of event to draw
* @param {Number} yOffset Vertical pixel offset of the draw
* @param {Boolean} drawSecondary Should secondary event be drawn if possible?
*/
const drawEvent = function (index, yOffset, drawSecondary) {
g.setColor(TEXT_COLOR);
// Draw the event time
g.setFontAlign(-1, -1, 0);
g.setFont("Vector", 20);
g.drawString(events[index].time, BORDER_SIZE, PRIMARY_OFFSET + yOffset);
// Draw the event title
g.setFont("8x16");
g.drawString(events[index].title, BORDER_SIZE, PRIMARY_OFFSET + 20 + yOffset);
// And the event description
g.setFont("6x12");
g.drawString(events[index].description, BORDER_SIZE, PRIMARY_OFFSET + 20 + 12 + 2 + yOffset);
// Draw a secondary event if asked to and exists
if (drawSecondary) {
if (index + 1 < events.length) {
if (events[index].date != events[index + 1].date) {
// If event belongs to another day, draw circle
g.fillCircle((g.getWidth() - ACCENT_WIDTH) / 2, g.getHeight() - MARKER_SIZE - WIDGET_SIZE - BORDER_SIZE + yOffset, MARKER_SIZE);
} else {
// Draw event time and title
g.setFont("Vector", 20);
g.drawString(events[index + 1].time, BORDER_SIZE, SECONDARY_OFFSET + yOffset);
g.setFont("8x16");
g.drawString(events[index + 1].title, BORDER_SIZE, SECONDARY_OFFSET + 20 + yOffset);
}
} else {
// If no more events exist, draw end
g.setFontAlign(0, 1, 0);
g.setFont("Vector", 20);
g.drawString("End", (g.getWidth() - ACCENT_WIDTH) / 2, g.getHeight() - BORDER_SIZE + yOffset);
}
}
};
/**
* Draw a two-line caption beneath a figure (Just beneath centre)
* @param {String} first Top string to draw
* @param {String} second Bottom string to draw
* @param {Number} yOffset Vertical pixel offset of the draw
*/
const drawFigureCaption = function (first, second, yOffset) {
g.setFontAlign(0, -1, 0);
g.setFont("Vector", 18);
g.setColor(TEXT_COLOR);
g.drawString(first, (g.getWidth() - ACCENT_WIDTH) / 2, g.getHeight() / 2 + BORDER_SIZE + yOffset);
g.drawString(second, (g.getWidth() - ACCENT_WIDTH) / 2, g.getHeight() / 2 + BORDER_SIZE + 20 + yOffset);
};
/**
* Clear the contents area of the default layout
*/
const clearContent = function () {
g.setColor(BG_COLOR);
g.fillRect(0, 0, g.getWidth() - ACCENT_WIDTH - PIN_SIZE, g.getHeight());
};
/**
* Draw the sun figure (above centre, in content area)
* @param {Number} progress Progress through the sun expansion animation, between 0 and 1
* @param {Number} yOffset Vertical pixel offset of the draw
*/
const drawSun = function (progress, yOffset) {
const p = ease(progress);
const sunColor = progress == 1 ? SUN_COLOR_END : g.blendColor(SUN_COLOR_START, SUN_COLOR_END, p);
g.setColor(sunColor);
g.fillPoly(g.transformVertices(interpolatePoly(BREATHING_POLYS, p), { y: yOffset }));
if (progress > 0.6) {
const faceP = ease((progress - 0.6) * 2.5);
g.setColor(g.blendColor(sunColor, SUN_FACE, faceP));
g.fillPoly(g.transformVertices(SUN_EYE_LEFT_POLY, { y: map(20, 0, faceP) + yOffset }));
g.fillPoly(g.transformVertices(SUN_EYE_LEFT_POLY, { x: SUN_EYE_RIGHT_OFFSET, y: map(20, 0, faceP) + yOffset }));
g.fillPoly(g.transformVertices(MOUTH_POLY, { y: map(10, 0, faceP) + yOffset }));
}
g.setColor(TEXT_COLOR);
g.fillRect({
x: map((g.getWidth() - ACCENT_WIDTH) / 2 - MARKER_SIZE, 20, p),
y: map(g.getHeight() / 2 - MARKER_SIZE, g.getHeight() / 2 - MARKER_SIZE / 2, p) + yOffset,
x2: map((g.getWidth() - ACCENT_WIDTH) / 2 + MARKER_SIZE, (g.getWidth() - ACCENT_WIDTH) - 20, p),
y2: map(g.getHeight() / 2 + MARKER_SIZE / 2, g.getHeight() / 2, p) + yOffset
});
};
/* Animation functions */
/**
* Animate clearing the screen to accent color with a single dot in the middle
*/
const animClearScreen = function () {
let oldPoly1 = CLEAR_POLYS_1[0];
let oldPoly2 = CLEAR_POLYS_2[0];
doAnim(i => {
i = ease(i);
poly1 = interpolatePoly(CLEAR_POLYS_1, i);
poly2 = interpolatePoly(CLEAR_POLYS_2, i);
// Fill in black line
g.setColor(TEXT_COLOR);
g.fillPoly(poly1);
g.fillPoly(poly2);
// Fill in outer shape
g.setColor(ACCENT_COLOR);
g.fillPoly(oldPoly1);
g.fillPoly(oldPoly2);
g.flip();
// Save poly for next loop outer shape
oldPoly1 = poly1;
oldPoly2 = poly2;
}, TIME_CLEAR_ANIM);
// Draw circle
g.setColor(TEXT_COLOR);
g.fillCircle(g.getWidth() / 2, g.getHeight() / 2, MARKER_SIZE);
g.flip();
};
/**
* Animate from a cleared screen and dot to the default layout
*/
const animDefaultScreen = function () {
doAnim(i => {
// Draw the circle moving into the corner
i = ease(i);
const circleX = map(g.getWidth() / 2, MARKER_POS_UPPER[0], i);
const circleY = map(g.getHeight() / 2, MARKER_POS_UPPER[1], i);
g.setColor(TEXT_COLOR);
g.fillCircle(circleX, circleY, MARKER_SIZE);
// Move the background poly in from the left
g.setColor(BG_COLOR);
const accentX = map(0, g.getWidth() - ACCENT_WIDTH, i);
g.fillPoly([0, 0, accentX, 0, accentX, MARKER_POS_UPPER[1] - PIN_SIZE, accentX - PIN_SIZE, MARKER_POS_UPPER[1], accentX, MARKER_POS_UPPER[1] + PIN_SIZE, accentX, 176, 0, 176]);
g.flip();
// Clear the circle for the next loop
g.setColor(ACCENT_COLOR);
g.fillCircle(circleX, circleY, MARKER_SIZE + 2);
}, TIME_DEFAULT_ANIM);
// Finish up the circle
const w = weather.get();
if (w && (w.code || w.txt)) {
doAnim(i => {
weather.drawIcon(w, MARKER_POS_UPPER[0], MARKER_POS_UPPER[1], MARKER_SIZE * 2);
g.setColor(TEXT_COLOR);
g.fillCircle(MARKER_POS_UPPER[0], MARKER_POS_UPPER[1], MARKER_SIZE * ease(1 - i));
g.flip();
}, 100);
} else {
g.setColor(TEXT_COLOR);
g.fillCircle(MARKER_POS_UPPER[0], MARKER_POS_UPPER[1], MARKER_SIZE);
}
};
/**
* Animate the sun figure expand or shrink fully
* @param {Number} direction Direction in which to animate. +1 = Expand. -1 = Shrink
*/
const animSun = function (direction) {
doAnim(i => {
// Clear and redraw just the sun area
g.setColor(BG_COLOR);
g.fillRect(0, 31, g.getWidth() - ACCENT_WIDTH - PIN_SIZE, g.getHeight() / 2 + 4);
drawSun((direction == 1 ? 0 : 1) + i * direction, 0);
g.flip();
}, TIME_EVENT_BREAK_ANIM);
};
/**
* Animate from centre dot to an event or backwards. Used for entering (forwards) or leaving (backwards) the day-change animation
* @param {Number} index Index of the event to draw animate in or out
* @param {Number} direction Direction of the animation. +1 = Event -> Dot. -1 = Dot -> Event
*/
const animEventToMarker = function (index, direction) {
doAnim(i => {
let ei = direction == 1 ? ease(i) : ease(1 - i);
clearContent();
drawEvent(index, -(SECONDARY_OFFSET - PRIMARY_OFFSET) * ei, false);
g.fillCircle((g.getWidth() - ACCENT_WIDTH) / 2, map(g.getHeight() - MARKER_SIZE - WIDGET_SIZE - BORDER_SIZE, g.getHeight() / 2, ei), MARKER_SIZE);
g.flip();
}, TIME_EVENT_BREAK_IN);
};
/**
* Blit the current contents of content area out of screen, replacing it with something. Currently only for moving stuff upwards.
* @param {Function} thing Callback for the new thing to draw on the screen
* @param {Number} time How long the animation should last
*/
const animBlitToX = function (thing, time) {
let oldI = 0;
doAnim(i => {
// Move stuff out of frame, index into frame
g.blit({
x1: 0,
y1: 0,
w: g.getWidth() - ACCENT_WIDTH - PIN_SIZE,
h: ease(1 - oldI) * g.getHeight(),
x2: 0,
y2: - (ease(i) - ease(oldI)) * g.getHeight(),
setModified: true
});
g.setColor(BG_COLOR);
// Only clear where old stuff no longer is
g.fillRect(0, g.getHeight() * (1 - ease(i)), g.getWidth() - ACCENT_WIDTH - PIN_SIZE, g.getHeight());
thing(i);
g.flip();
oldI = i;
}, time);
};
/**
* Transition between one event and another, showing a day-change animation if needed
* @param {Number} startIndex The event index that we are animating out of
* @param {Number} endIndex The event index that we are animating into
*/
const animEventTransition = function (startIndex, endIndex) {
if (events[startIndex].date == events[endIndex].date) {
// If both events are within the same day, just scroll from one to the other.
// First determine which event is on top and which direction we are animating in
let topIndex = (startIndex < endIndex) ? startIndex : endIndex;
let botIndex = (startIndex < endIndex) ? endIndex : startIndex;
let direction = (startIndex < endIndex) ? 1 : -1;
let offset = (startIndex < endIndex) ? 0 : 1;
doAnim(i => {
// Animate the two events moving towards their destinations
clearContent();
drawEvent(topIndex, -(SECONDARY_OFFSET - PRIMARY_OFFSET) * ease(offset + direction * i), false);
drawEvent(botIndex, (SECONDARY_OFFSET - PRIMARY_OFFSET) - (SECONDARY_OFFSET - PRIMARY_OFFSET) * ease(offset + direction * i), true);
g.flip();
}, TIME_EVENT_CHANGE);
// Finally, reset contents and redraw for good measure
clearContent();
drawEvent(endIndex, 0, true);
g.flip();
} else {
// The events are on different days, trigger day-change animation
if (startIndex < endIndex) {
// Destination is later, Stuff moves upwards
animEventToMarker(startIndex, 1); // The day-end dot moves to center of screen
drawFigureCaption(events[endIndex].weekday, events[endIndex].date, 0); // Caption between sun appears, no need to continuously redraw
animSun(1); // Animate the sun expanding
doAnim(i => { }, TIME_EVENT_BREAK_HALT); // Wait for a moment
animBlitToX(i => { drawEvent(endIndex, g.getHeight() - g.getHeight() * ease(i), true); }, TIME_EVENT_BREAK_OUT); // Blit the sun and caption out, replacing with destination event
} else {
// Destination is earlier, content moves downwards
doAnim(i => {
// Can't animBlit, draw sun and figure caption replacing origin event
clearContent();
drawEvent(startIndex, g.getHeight() * ease(i), true);
drawSun(1, - g.getHeight() * ease(1 - i));
drawFigureCaption(events[endIndex].weekday, events[endIndex].date, - g.getHeight() * ease(1 - i));
g.flip();
}, TIME_EVENT_BREAK_OUT);
doAnim(i => { }, TIME_EVENT_BREAK_HALT); // Wait for a moment
animSun(-1); // Collapse the sun
animEventToMarker(endIndex, -1); // Animate from dot to destination event
}
}
g.flip();
};
/**
* Bump the event because we've reached an end
* @param {Number} index The index of the event which we are currently at (probably last)
* @param {Number} direction Which direction to bump. +1 = content moves down, then up. -1 = content moves up, back down
*/
const animEventBump = function (index, direction) {
doAnim(i => {
clearContent();
drawEvent(index, Math.sin(Math.PI * i) * 24 * direction, true);
g.flip();
}, TIME_BUMP_ANIM);
};
/**
* Run the exit animation of the application
*/
const animExit = function () {
// First, move out (downwards) the current event
doAnim(i => {
clearContent();
drawEvent(currentEventIndex, ease(i) * g.getHeight(), true);
g.flip();
}, TIME_EXIT_ANIM / 3 * 2);
// Clear the screen leftwards with the accent color
g.setColor(ACCENT_COLOR);
doAnim(i => {
g.fillRect(ease(1 - i) * g.getWidth(), 0, g.getWidth(), g.getHeight());
g.flip();
}, TIME_EXIT_ANIM / 3);
};
/**
* Animate from empty default screen to the first event to show.
* If the event we're moving to is not later today, show the date first.
*/
const animFirstEvent = function () {
if (!isSameDay(new Date(events[currentEventIndex].timestamp * 1000), new Date())) {
drawFigureCaption(events[currentEventIndex].weekday, events[currentEventIndex].date, 0);
animSun(1);
doAnim(i => { }, TIME_EVENT_BREAK_HALT);
animBlitToX(i => { drawEvent(currentEventIndex, g.getHeight() - g.getHeight() * ease(i), true); }, TIME_EVENT_BREAK_OUT, 1);
} else {
drawEvent(currentEventIndex, 0, true);
}
};
/* Setup */
/* Load events */
const today = new Date();
const tomorrow = new Date();
const yesterday = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
yesterday.setDate(yesterday.getDate() - 1);
g.setFont("6x12");
const locale = require("locale");
let events = (require("Storage").readJSON("android.calendar.json", true) || []).map(event => {
// Title uses 8x16 font, 8 px wide characters. Limit title to fit on a line.
let title = event.title;
if (title.length > (g.getWidth() - 2 * BORDER_SIZE - ACCENT_WIDTH) / 8) {
title = title.slice(0, ((g.getWidth() - 2 * BORDER_SIZE - ACCENT_WIDTH) / 8) - 3) + "...";
}
// Wrap description to fit four lines of content
let description = g.wrapString(event.description, g.getWidth() - 2 * BORDER_SIZE - ACCENT_WIDTH - PIN_SIZE).slice(0, 4).join("\n");
// Set weekday text
let eventDate = new Date(event.timestamp * 1000);
let weekday = locale.dow(eventDate);
if (isSameDay(eventDate, today)) {
weekday = /*LANG*/"Today";
} else if (isSameDay(eventDate, tomorrow)) {
weekday = /*LANG*/"Tomorrow";
} else if (isSameDay(eventDate, yesterday)) {
weekday = /*LANG*/"Yesterday";
}
return {
timestamp: event.timestamp,
weekday: weekday,
date: locale.date(eventDate, 1),
time: locale.time(eventDate, 1) + locale.meridian(eventDate),
title: title,
description: description
};
}).sort((a, b) => { return a.timestamp - b.timestamp; });
// If no events, add a note.
if (events.length == 0) {
events[0] = {
timestamp: Date.now() / 1000,
weekday: /*LANG*/"Today",
date: require("locale").date(new Date(), 1),
time: require("locale").time(new Date(), 1),
title: /*LANG*/"No events",
description: /*LANG*/"Nothing to do"
};
}
// We should start at the first event later than now
let currentEventIndex = events.findIndex((event) => { return event.timestamp * 1000 > Date.now(); });
if (currentEventIndex == -1) currentEventIndex = 0; // Or just first event if none found
// Setup the UI with remove to support fast load
Bangle.setUI({
mode: "custom",
btn: () => { animExit(); Bangle.load(); },
remove: function () {
require("widget_utils").show();
delete Graphics.prototype.Font6x12;
delete Graphics.prototype.Font8x16;
Bangle.removeListener('swipe', onSwipe);
},
});
/**
* Callback for swipe gesture. Transitions between adjacent events.
* @param {Number} directionLR Unused.
* @param {Number} directionUD Whether swipe direction is up or down
*/
const onSwipe = function (directionLR, directionUD) {
if (directionUD == -1) {
// Swiping up
if (currentEventIndex + 1 < events.length) {
// Animate to the next event
animEventTransition(currentEventIndex, currentEventIndex + 1);
currentEventIndex += 1;
} else {
// We've hit the end, bump
animEventBump(currentEventIndex, -1);
}
} else if (directionUD == 1) {
//Swiping down
if (currentEventIndex > 0) {
// Animate to the previous event
animEventTransition(currentEventIndex, currentEventIndex - 1);
currentEventIndex -= 1;
} else {
// If swiping earlier than earliest event, exit back to watchface
animExit();
Bangle.load();
}
}
};
// Ready animations for showing the first event, then register swipe listener for switching events
setTimeout(() => {
animDefaultScreen();
animFirstEvent();
Bangle.on('swipe', onSwipe);
}, TIME_CLEAR_ANIM + TIME_CLEAR_BREAK);
animClearScreen(); // Start visible changes by clearing the screen
// Load and hide widgets to background
Bangle.loadWidgets();
require("widget_utils").hide();
}

BIN
apps/rebbleagenda/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

View File

@ -0,0 +1,25 @@
{ "id": "rebbleagenda",
"name": "Rebble Agenda",
"shortName":"Agenda",
"version":"0.01",
"description": "A pebble-inspired animated agenda",
"icon": "app.png",
"screenshots" : [
{ "url":"screenshot_rebbleagenda_events.png" },
{ "url":"screenshot_rebbleagenda_customtheme.png" },
{ "url":"screenshot_rebbleagenda_sun.png" }
],
"type": "app",
"tags": "agenda,tool",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"dependencies" : { "weather":"app" },
"storage": [
{"name":"rebbleagenda.app.js","url":"app.js"},
{"name":"rebbleagenda.settings.js","url":"settings.js"},
{"name":"rebbleagenda.img","url":"app-icon.js","evaluate":true}
],
"data": [
{"name":"rebbleagenda.json"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,69 @@
(function (back) {
const SETTINGS_FILE = "rebbleagenda.json";
// initialize with default settings...
let s = {
'system': true,
'bg': "#FFF",
'fg': "#000",
'acc': "#0FF"
};
// ...and overwrite them with any saved values
// This way saved values are preserved if a new version adds more settings
const storage = require('Storage');
let settings = storage.readJSON(SETTINGS_FILE, 1) || {};
const saved = settings || {};
for (const key in saved) {
s[key] = saved[key];
}
const save = function () {
settings = s;
storage.write(SETTINGS_FILE, settings);
};
const color_options = [/*LANG*/"Red", /*LANG*/"Green", /*LANG*/"Blue", /*LANG*/"Purple", /*LANG*/"Cyan", /*LANG*/"Orange", /*LANG*/"Grey"];
const color_codes = ['#F00','#0F0','#00F','#F0F','#0FF','#FF0', "#888"];
const ground_options = [/*LANG*/"Black", /*LANG*/"White", /*LANG*/"Dark Blue", /*LANG*/"Dark Red", /*LANG*/"Dark Green", /*LANG*/"Light Blue", /*LANG*/"Light Red", /*LANG*/"Light Green"];
const ground_codes = ["#000", "#FFF", "#003", "#300", "#030", "#BBF", "#FBB", "#BFB"];
E.showMenu({
'': { 'title': 'Rebble Agenda' },
/*LANG*/'< Back': back,
/*LANG*/'Use system theme': {
value: !!s.system,
onchange: v => {
s.system = v;
save();
},
},
/*LANG*/'Accent': {
value: 0 | color_codes.indexOf(s.acc),
min: 0, max: color_codes.length-1,
format: v => color_options[v],
onchange: v => {
s.acc = color_codes[v];
save();
},
},
/*LANG*/'Background': {
value: 0 | ground_codes.indexOf(s.bg),
min: 0, max: ground_codes.length-1,
format: v => ground_options[v],
onchange: v => {
s.bg = ground_codes[v];
save();
},
},
/*LANG*/'Foreground': {
value: 0 | ground_codes.indexOf(s.fg),
min: 0, max: ground_codes.length-1,
format: v => ground_options[v],
onchange: v => {
s.fg = ground_codes[v];
save();
},
}
});
});