diff --git a/apps/timestamplog/ChangeLog b/apps/timestamplog/ChangeLog new file mode 100644 index 0000000000..ec66c5568c --- /dev/null +++ b/apps/timestamplog/ChangeLog @@ -0,0 +1 @@ +0.01: Initial version diff --git a/apps/timestamplog/README.md b/apps/timestamplog/README.md new file mode 100644 index 0000000000..96565e2d7e --- /dev/null +++ b/apps/timestamplog/README.md @@ -0,0 +1,55 @@ +# Timestamp Log + +Timestamp Log provides a convenient way to record points in time for later reference. Each time a button is tapped a date/time-stamped marker is logged. By default up to 30 entries can be stored at once; this can be increased up to 100 in the settings menu. + +![Timestamp Log screenshot](screenshot.png) + +## Usage and controls + +When the app starts you will see the log display. Initially the log is empty. The large button on the bottom left displays the current time and will add a date- and time-stamp when tapped. Each tap of the button adds a new entry to the bottom of the log. The small button on the bottom right opens the app settings menu. + +If the log contains more entries than can be displayed at once, swiping up and down will move through the entries one screenfull at a time. + +To delete an individual entry, display it on the screen and then tap on it. The entry's position in the list will be shown along with a Delete button. Tap this button to remove the entry. The Up and Down arrows on the right side of the screen can be used to move between log entries. Further deletions can be made. Finally, click the Back button in the upper-left to finish and return to the main log screen. + +## Settings + +The settings menu provides the following settings: + +### Log + +**Max entries:** Select the maximum number of entries that the log can hold. + +**Auto-delete oldest:** If turned on, adding a log entry when the log is full will cause the oldest entry to automatically be deleted to make room. Otherwise, it is not possible to add another log entry until some entries are manually deleted or the “Max entries” setting is increased. + +**Clear log:** Remove all log entries, leaving the log empty. + +### Appearance + +**Log font:** Select the font used to display log entries. + +**Log font H size** and **Log font V size**: Select the horizontal and vertical sizes, respectively, of the font. Reasonable values for bitmapped fonts are 1 or 2 for either setting. For Vector, values around 15 to 25 work best. Setting both sizes the same will display the font with normal proportions; varying the values will change the relative height or width of the font. + +### Button + +You can choose the action that the physical button (Bangle.js v2) performs when the screen is unlocked. + +**Log time:** Add a date/time stamp to the log. Same as tapping the large button on the touch screen. + +**Open settings:** Open this app settings menu. + +**Quit app:** Return to the main clock app. + +**Do nothing:** Perform no action. + +## Web interface + +Currently the web interface displays the list of dates and times, which can be copied and pasted as desired. The log cannot currently be edited with this interface, only displayed. + +## Support + +Issues and questions may be posted at: https://github.com/espruino/BangleApps/issues + +## Creator + +Travis Evans diff --git a/apps/timestamplog/app-icon.js b/apps/timestamplog/app-icon.js new file mode 100644 index 0000000000..b35f05e08b --- /dev/null +++ b/apps/timestamplog/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4cB/4ACBQX48AFDAAUkyVJAQoDCBZACDymSoEAgVJkQRJkskGAuJCJEpBwYDCyIRHpVICI0SogRGqQyEAgdIBwIUEyAODAQkJiVJxBoDwARIgJuBiIUBCIKzKCIOCQYWkCJUkpNQoiMBkARKgmJxUIxMlOgIQIQwOJyNBiKeBCJeRyUokUoGZPYAQMRyVFgiwDAAsGCIlI0GIBYfAAgUB2wECCINJikRCIfbAgVt2DaDCIMiwR9DjdggEDtg5DgTECSQIJDtuAEwYRFSQOSBIUN2xWCgANBgVSAYKSBR4k2AgYRCxQDBSQTGKgVRCISSBoARKxUpCIKSFAA0SqNFCIKSFAA0RlUo0iSHCI0losUSRARFkmo0SSEwAPFeoORkmViiSEiARHxJXB0SSFAgQCByEAggRCqiSEilChEgwUIXgMkBgVKSQmiqFBgkQoMUoArESQdE6Y1FxIRESQco0WIEYkRiQRDSQWRmnTpojEwRhFSQOKEYOJEYkQogRESQNNEZEIPoQUCEYeKkIjEoLFBCIdTEYc0EYsiCIlKpQjCkojCNIYREpMpEYwCCEYoCB0gjBkmEEYImCgQRGyWTNYJECbQQjHJQIDBygjNpSHCEZ0QAYIjODoJHPEAgjDA==")) diff --git a/apps/timestamplog/app.js b/apps/timestamplog/app.js new file mode 100644 index 0000000000..c44ab719bc --- /dev/null +++ b/apps/timestamplog/app.js @@ -0,0 +1,523 @@ +const Layout = require('Layout'); +const locale = require('locale'); + +const tsl = require('timestamplog'); + + +// Min number of pixels of movement to recognize a touchscreen drag/swipe +const DRAG_THRESHOLD = 30; + +// Width of scroll indicators +const SCROLL_BAR_WIDTH = 12; + + +// Fetch a stringified image +function getIcon(id) { + if (id == 'add') { +// Graphics.createImage(` +// XX X X X X +// XX X X X X +// XXXXXX X X X X +// XXXXXX X X X X +// XX X X X X +// XX X X X X +// X XX X X +// X X X X +// X XX X +// X X X +// X X +// X XX +// XXX XX +// XXXXX +// XXXX +// XX +// `); + return "\0\x17\x10\x81\x000\t\x12`$K\xF0\x91'\xE2D\x83\t\x12\x06$H\x00\xB1 \x01$\x80\x042\x00\b(\x00 \x00A\x80\x01\xCC\x00\x03\xE0\x00\x0F\x00\x00\x18\x00\x00"; + } else if (id == 'menu') { +// Graphics.createImage(` +// +// +// +// +// XXXXXXXXXXXXXXXX +// XXXXXXXXXXXXXXXX +// +// +// XXXXXXXXXXXXXXXX +// XXXXXXXXXXXXXXXX +// +// +// XXXXXXXXXXXXXXXX +// XXXXXXXXXXXXXXXX +// +// +// `); + return "\0\x10\x10\x81\0\0\0\0\0\0\0\0\0\xFF\xFF\xFF\xFF\0\0\0\0\xFF\xFF\xFF\xFF\0\0\0\0\xFF\xFF\xFF\xFF\0\0\0\0"; + } +} + + +// UI layout render callback for log entries +function renderLogItem(elem) { + if (elem.item) { + g.setColor(g.theme.bg) + .fillRect(elem.x, elem.y, elem.x + elem.w - 1, elem.y + elem.h - 1) + .setFont(tsl.fontSpec(tsl.SETTINGS.logFont, + tsl.SETTINGS.logFontHSize, tsl.SETTINGS.logFontVSize)) + .setFontAlign(-1, -1) + .setColor(g.theme.fg) + .drawLine(elem.x, elem.y, elem.x + elem.w - 1, elem.y) + .drawString(locale.date(elem.item.stamp, 1) + + '\n' + + locale.time(elem.item.stamp).trim(), + elem.x, elem.y + 1); + } else { + g.setColor(g.blendColor(g.theme.bg, g.theme.fg, 0.25)) + .fillRect(elem.x, elem.y, elem.x + elem.w - 1, elem.y + elem.h - 1); + } +} + +// Render a scroll indicator +// `scroll` format: { +// pos: int, +// min: int, +// max: int, +// itemsPerPage: int, +// } +function renderScrollBar(elem, scroll) { + const border = 1; + const boxArea = elem.h - 2 * border; + const boxSize = E.clip( + Math.round( + scroll.itemsPerPage / (scroll.max - scroll.min + 1) * (elem.h - 2) + ), + 3, + boxArea + ); + const boxTop = (scroll.max - scroll.min) ? + Math.round( + (scroll.pos - scroll.min) / (scroll.max - scroll.min) + * (boxArea - boxSize) + elem.y + border + ) : elem.y + border; + + // Draw border + g.setColor(g.theme.fg) + .fillRect(elem.x, elem.y, elem.x + elem.w - 1, elem.y + elem.h - 1) + // Draw scroll box area + .setColor(g.theme.bg) + .fillRect(elem.x + border, elem.y + border, + elem.x + elem.w - border - 1, elem.y + elem.h - border - 1) + // Draw scroll box + .setColor(g.blendColor(g.theme.bg, g.theme.fg, 0.5)) + .fillRect(elem.x + border, boxTop, + elem.x + elem.w - border - 1, boxTop + boxSize - 1); +} + +// Main app screen interface, launched by calling start() +class MainScreen { + + constructor() { + // Values set up by start() + this.itemsPerPage = null; + this.scrollPos = null; + this.layout = null; + + // Handlers/listeners + this.buttonTimeoutId = null; + this.listeners = {}; + } + + // Launch this UI and make it live + start() { + this._initLayout(); + this.layout.clear(); + this.scroll('b'); + this.render('buttons'); + + this._initTouch(); + } + + // Stop this UI, shut down all timers/listeners, and otherwise clean up + stop() { + if (this.buttonTimeoutId) { + clearTimeout(this.buttonTimeoutId); + this.buttonTimeoutId = null; + } + + // Kill layout handlers + Bangle.removeListener('drag', this.listeners.drag); + Bangle.removeListener('touch', this.listeners.touch); + clearWatch(this.listeners.btnWatch); + Bangle.setUI(); + } + + _initLayout() { + let layout = new Layout( + {type: 'v', + c: [ + // Placeholder to force bottom alignment when there is unused + // vertical screen space + {type: '', id: 'placeholder', fillx: 1, filly: 1}, + + {type: 'h', + c: [ + {type: 'v', + id: 'logItems', + + // To be filled in with log item elements once we + // determine how many will fit on screen + c: [], + }, + {type: 'custom', + id: 'logScroll', + render: elem => { renderScrollBar(elem, this.scrollBarInfo()); } + }, + ], + }, + {type: 'h', + id: 'buttons', + c: [ + {type: 'btn', font: '6x8:2', fillx: 1, label: '+ XX:XX', id: 'addBtn', + cb: this.addTimestamp.bind(this)}, + {type: 'btn', font: '6x8:2', label: getIcon('menu'), id: 'menuBtn', + cb: () => launchSettingsMenu(returnFromSettings)}, + ], + }, + ], + } + ); + + // Calculate how many log items per page we have space to display + layout.update(); + let availableHeight = layout.placeholder.h; + g.setFont(tsl.fontSpec(tsl.SETTINGS.logFont, + tsl.SETTINGS.logFontHSize, tsl.SETTINGS.logFontVSize)); + let logItemHeight = g.getFontHeight() * 2; + this.itemsPerPage = Math.max(1, + Math.floor(availableHeight / logItemHeight)); + + // Populate log items in layout + for (let i = 0; i < this.itemsPerPage; i++) { + layout.logItems.c.push( + {type: 'custom', render: renderLogItem, item: undefined, itemIdx: undefined, + fillx: 1, height: logItemHeight} + ); + } + layout.logScroll.height = logItemHeight * this.itemsPerPage; + layout.logScroll.width = SCROLL_BAR_WIDTH; + layout.update(); + + this.layout = layout; + } + + // Redraw a particular display `item`, or everything if `item` is falsey + render(item) { + if (!item || item == 'log') { + let layLogItems = this.layout.logItems; + let logIdx = this.scrollPos - this.itemsPerPage; + for (let elem of layLogItems.c) { + logIdx++; + elem.item = stampLog.log[logIdx]; + elem.itemIdx = logIdx; + } + this.layout.render(layLogItems); + this.layout.render(this.layout.logScroll); + } + + if (!item || item == 'buttons') { + let addBtn = this.layout.addBtn; + + if (!tsl.SETTINGS.rotateLog && stampLog.isFull()) { + // Dimmed appearance for unselectable button + addBtn.btnFaceCol = g.blendColor(g.theme.bg2, g.theme.bg, 0.5); + addBtn.btnBorderCol = g.blendColor(g.theme.fg2, g.theme.bg, 0.5); + + addBtn.label = 'Log full'; + } else { + addBtn.btnFaceCol = g.theme.bg2; + addBtn.btnBorderCol = g.theme.fg2; + + addBtn.label = getIcon('add') + ' ' + locale.time(new Date(), 1).trim(); + } + + this.layout.render(this.layout.buttons); + + // Auto-update time of day indication on log-add button upon + // next minute + if (!this.buttonTimeoutId) { + this.buttonTimeoutId = setTimeout( + () => { + this.buttonTimeoutId = null; + this.render('buttons'); + }, + 60000 - (Date.now() % 60000) + ); + } + } + + } + + _initTouch() { + let distanceY = null; + + function dragHandler(ev) { + // Handle up/down swipes for scrolling + if (ev.b) { + if (distanceY === null) { + // Drag started + distanceY = ev.dy; + } else { + // Drag in progress + distanceY += ev.dy; + } + } else { + // Drag released + distanceY = null; + } + if (Math.abs(distanceY) > DRAG_THRESHOLD) { + // Scroll threshold reached + Bangle.buzz(50, .5); + this.scroll(distanceY > 0 ? 'u' : 'd'); + distanceY = null; + } + } + + this.listeners.drag = dragHandler.bind(this); + Bangle.on('drag', this.listeners.drag); + + function touchHandler(button, xy) { + // Handle taps on log entries + let logUIItems = this.layout.logItems.c; + for (var logUIObj of logUIItems) { + if (!xy.type && + logUIObj.x <= xy.x && xy.x < logUIObj.x + logUIObj.w && + logUIObj.y <= xy.y && xy.y < logUIObj.y + logUIObj.h && + logUIObj.item) { + switchUI(new LogEntryScreen(stampLog, logUIObj.itemIdx)); + break; + } + } + } + + this.listeners.touch = touchHandler.bind(this); + Bangle.on('touch', this.listeners.touch); + + function buttonHandler() { + let act = tsl.SETTINGS.buttonAction; + if (act == 'Log time') { + if (currentUI != mainUI) { + switchUI(mainUI); + } + mainUI.addTimestamp(); + } else if (act == 'Open settings') { + launchSettingsMenu(returnFromSettings); + } else if (act == 'Quit app') { + Bangle.showClock(); + } + } + + this.listeners.btnWatch = setWatch(buttonHandler, BTN, + {edge: 'falling', debounce: 50, repeat: true}); + } + + // Add current timestamp to log if possible and update UI display + addTimestamp() { + if (tsl.SETTINGS.rotateLog || !stampLog.isFull()) { + stampLog.addEntry(); + this.scroll('b'); + this.render('buttons'); + } + } + + // Get scroll information for log display + scrollInfo() { + return { + pos: this.scrollPos, + min: (stampLog.log.length - 1) % this.itemsPerPage, + max: stampLog.log.length - 1, + itemsPerPage: this.itemsPerPage + }; + } + + // Like scrollInfo, but adjust the data so as to suggest scrollbar + // geometry that accurately reflects the nature of the scrolling + // (page by page rather than item by item) + scrollBarInfo() { + const info = this.scrollInfo(); + + function toPage(scrollPos) { + return Math.floor(scrollPos / info.itemsPerPage); + } + + return { + // Define 1 "screenfull" as the unit here + itemsPerPage: 1, + pos: toPage(info.pos), + min: toPage(info.min), + max: toPage(info.max), + }; + } + + // Scroll display in given direction or to given position: + // 'u': up, 'd': down, 't': to top, 'b': to bottom + scroll(how) { + let scroll = this.scrollInfo(); + + if (how == 'u') { + this.scrollPos -= scroll.itemsPerPage; + } else if (how == 'd') { + this.scrollPos += scroll.itemsPerPage; + } else if (how == 't') { + this.scrollPos = scroll.min; + } else if (how == 'b') { + this.scrollPos = scroll.max; + } + + this.scrollPos = E.clip(this.scrollPos, scroll.min, scroll.max); + + this.render('log'); + } +} + + +// Log entry screen interface, launched by calling start() +class LogEntryScreen { + + constructor(stampLog, logIdx) { + this.logIdx = logIdx; + this.logItem = stampLog.log[logIdx]; + + this.defaultFont = tsl.fontSpec( + tsl.SETTINGS.logFont, tsl.SETTINGS.logFontHSize, tsl.SETTINGS.logFontVSize); + } + + start() { + this._initLayout(); + this.layout.clear(); + this.refresh(); + } + + stop() { + Bangle.setUI(); + } + + back() { + this.stop(); + switchUI(mainUI); + } + + _initLayout() { + let layout = new Layout( + {type: 'v', + c: [ + {type: 'txt', font: this.defaultFont, id: 'entryno', label: 'Entry ?/?'}, + {type: 'txt', font: this.defaultFont, id: 'date', label: '?'}, + {type: 'txt', font: this.defaultFont, id: 'time', label: '?'}, + {type: '', id: 'placeholder', fillx: 1, filly: 1}, + {type: 'btn', font: '12x20', label: 'Delete', + cb: this.delLogItem.bind(this)}, + ], + }, + { + back: this.back.bind(this), + btns: [ + {label: '<', cb: this.prevLogItem.bind(this)}, + {label: '>', cb: this.nextLogItem.bind(this)}, + ], + } + ); + + layout.update(); + this.layout = layout; + } + + render(item) { + this.layout.clear(); + this.layout.render(); + } + + refresh() { + this.logItem = stampLog.log[this.logIdx]; + this.layout.entryno.label = 'Entry ' + (this.logIdx+1) + '/' + stampLog.log.length; + this.layout.date.label = locale.date(this.logItem.stamp, 1); + this.layout.time.label = locale.time(this.logItem.stamp).trim(); + this.layout.update(); + this.render(); + } + + prevLogItem() { + this.logIdx = this.logIdx ? this.logIdx-1 : stampLog.log.length-1; + this.refresh(); + } + + nextLogItem() { + this.logIdx = this.logIdx == stampLog.log.length-1 ? 0 : this.logIdx+1; + this.refresh(); + } + + delLogItem() { + stampLog.deleteEntries([this.logItem]); + if (!stampLog.log.length) { + this.back(); + return; + } else if (this.logIdx > stampLog.log.length - 1) { + this.logIdx = stampLog.log.length - 1; + } + + // Create a brief “blink” on the screen to provide user feedback + // that the deletion has been performed + this.layout.clear(); + setTimeout(this.refresh.bind(this), 250); + } + +} + + +function switchUI(newUI) { + currentUI.stop(); + currentUI = newUI; + currentUI.start(); +} + + +function saveErrorAlert() { + currentUI.stop(); + // Not `showAlert` because the icon plus message don't fit the + // screen well + E.showPrompt( + 'Trouble saving timestamp log; data may be lost!', + {title: "Can't save log", + buttons: {'Ok': true}} + ).then(currentUI.start.bind(currentUI)); +} + + +function launchSettingsMenu(backCb) { + currentUI.stop(); + stampLog.save(); + tsl.launchSettingsMenu(backCb); +} + +function returnFromSettings() { + // Reload stampLog to pick up any changes made from settings UI + stampLog = loadStampLog(); + currentUI.start(); +} + + +function loadStampLog() { + // Create a StampLog object with its data loaded from storage + return new tsl.StampLog(tsl.LOG_FILENAME, tsl.SETTINGS.maxLogLength); +} + + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +var stampLog = loadStampLog(); +E.on('kill', stampLog.save.bind(stampLog)); +stampLog.on('saveError', saveErrorAlert); + +var currentUI = new MainScreen(stampLog); +var mainUI = currentUI; +currentUI.start(); diff --git a/apps/timestamplog/app.png b/apps/timestamplog/app.png new file mode 100644 index 0000000000..ddb51fe40b Binary files /dev/null and b/apps/timestamplog/app.png differ diff --git a/apps/timestamplog/app.xcf b/apps/timestamplog/app.xcf new file mode 100644 index 0000000000..27406b1ed8 Binary files /dev/null and b/apps/timestamplog/app.xcf differ diff --git a/apps/timestamplog/interface.html b/apps/timestamplog/interface.html new file mode 100644 index 0000000000..b54dfb010e --- /dev/null +++ b/apps/timestamplog/interface.html @@ -0,0 +1,31 @@ + + + + + + +
Loading...
+ + + diff --git a/apps/timestamplog/lib.js b/apps/timestamplog/lib.js new file mode 100644 index 0000000000..59590bfb76 --- /dev/null +++ b/apps/timestamplog/lib.js @@ -0,0 +1,248 @@ +const storage = require('Storage'); + +// Storage filenames + +const LOG_FILENAME = 'timestamplog.json'; +const SETTINGS_FILENAME = 'timestamplog.settings.json'; + + +// Settings + +const SETTINGS = Object.assign({ + logFont: '12x20', + logFontHSize: 1, + logFontVSize: 1, + maxLogLength: 30, + rotateLog: false, + buttonAction: 'Log time', +}, storage.readJSON(SETTINGS_FILENAME, true) || {}); + +const SETTINGS_BUTTON_ACTION = [ + 'Log time', + 'Open settings', + 'Quit app', + 'Do nothing', +]; + + +function fontSpec(name, hsize, vsize) { + return name + ':' + hsize + 'x' + vsize; +} + + +//// Data models //// + +// High-level timestamp log object that provides an interface to the +// UI for managing log entries and automatically loading/saving +// changes to flash storage. +class StampLog { + constructor(filename, maxLength) { + // Name of file to save log to + this.filename = filename; + // Maximum entries for log before old entries are overwritten with + // newer ones + this.maxLength = maxLength; + + // `true` when we have changes that need to be saved + this.isDirty = false; + // Wait at most this many msec upon first data change before + // saving (this is to avoid excessive writes to flash if several + // changes happen quickly; we wait a little bit so they can be + // rolled into a single write) + this.saveTimeout = 30000; + // setTimeout ID for scheduled save job + this.saveId = null; + // Underlying raw log data object. Outside this class it's + // recommended to use only the class methods to change it rather + // than modifying the object directly to ensure that changes are + // recognized and saved to storage. + this.log = []; + + this.load(); + } + + // Read in the log data that is currently in storage + load() { + let log = storage.readJSON(this.filename, true); + if (!log) log = []; + // Convert stringified datetimes back into Date objects + for (let logEntry of log) { + logEntry.stamp = new Date(logEntry.stamp); + } + this.log = log; + } + + // Write current log data to storage if anything needs to be saved + save() { + // Cancel any pending scheduled calls to save() + if (this.saveId) { + clearTimeout(this.saveId); + this.saveId = null; + } + + if (this.isDirty) { + let logToSave = []; + for (let logEntry of this.log) { + // Serialize each Date object into an ISO string before saving + let newEntry = Object.assign({}, logEntry); + newEntry.stamp = logEntry.stamp.toISOString(); + logToSave.push(newEntry); + } + + if (storage.writeJSON(this.filename, logToSave)) { + console.log('stamplog: save to storage completed'); + this.isDirty = false; + } else { + console.log('stamplog: save to storage FAILED'); + this.emit('saveError'); + } + } else { + console.log('stamplog: skipping save to storage because no changes made'); + } + } + + // Mark log as needing to be (re)written to storage + setDirty() { + this.isDirty = true; + if (!this.saveId) { + this.saveId = setTimeout(this.save.bind(this), this.saveTimeout); + } + } + + // Add a timestamp for the current time to the end of the log + addEntry() { + // If log full, purge an old entry to make room for new one + if (this.maxLength) { + while (this.log.length + 1 > this.maxLength) { + this.log.shift(); + } + } + // Add new entry + this.log.push({ + stamp: new Date() + }); + this.setDirty(); + } + + // Delete the log objects given in the array `entries` from the log + deleteEntries(entries) { + this.log = this.log.filter(entry => !entries.includes(entry)); + this.setDirty(); + } + + // Does the log currently contain the maximum possible number of entries? + isFull() { + return this.log.length >= this.maxLength; + } +} + +function launchSettingsMenu(backCb) { + const fonts = g.getFonts(); + const stampLog = new StampLog(LOG_FILENAME, SETTINGS.maxLogLength); + + function saveSettings() { + console.log('Saving timestamp log and settings'); + stampLog.save(); + if (!storage.writeJSON(SETTINGS_FILENAME, SETTINGS)) { + E.showAlert('Trouble saving settings'); + } + } + E.on('kill', saveSettings); + + function endMenu() { + saveSettings(); + E.removeListener('kill', saveSettings); + backCb(); + } + + function topMenu() { + E.showMenu({ + '': { + title: 'Stamplog', + back: endMenu, + }, + 'Log': logMenu, + 'Appearance': appearanceMenu, + 'Button': { + value: SETTINGS_BUTTON_ACTION.indexOf(SETTINGS.buttonAction), + min: 0, max: SETTINGS_BUTTON_ACTION.length - 1, + format: v => SETTINGS_BUTTON_ACTION[v], + onchange: v => { + SETTINGS.buttonAction = SETTINGS_BUTTON_ACTION[v]; + }, + }, + }); + } + + function logMenu() { + E.showMenu({ + '': { + title: 'Log', + back: topMenu, + }, + 'Max entries': { + value: SETTINGS.maxLogLength, + min: 5, max: 100, step: 5, + onchange: v => { + SETTINGS.maxLogLength = v; + stampLog.maxLength = v; + } + }, + 'Auto-delete oldest': { + value: SETTINGS.rotateLog, + onchange: v => { + SETTINGS.rotateLog = !SETTINGS.rotateLog; + } + }, + 'Clear log': doClearLog, + }); + } + + function appearanceMenu() { + E.showMenu({ + '': { + title: 'Appearance', + back: topMenu, + }, + 'Log font': { + value: fonts.indexOf(SETTINGS.logFont), + min: 0, max: fonts.length - 1, + format: v => fonts[v], + onchange: v => { + SETTINGS.logFont = fonts[v]; + }, + }, + 'Log font H size': { + value: SETTINGS.logFontHSize, + min: 1, max: 50, + onchange: v => { + SETTINGS.logFontHSize = v; + }, + }, + 'Log font V size': { + value: SETTINGS.logFontVSize, + min: 1, max: 50, + onchange: v => { + SETTINGS.logFontVSize = v; + }, + }, + }); + } + + function doClearLog() { + E.showPrompt('Erase ALL log entries?', { + title: 'Clear log', + buttons: {'Erase':1, "Don't":0} + }).then((yes) => { + if (yes) { + stampLog.deleteEntries(stampLog.log); + } + logMenu(); + }); + } + + topMenu(); +} + +exports = {LOG_FILENAME, SETTINGS_FILENAME, SETTINGS, SETTINGS_BUTTON_ACTION, fontSpec, StampLog, + launchSettingsMenu}; diff --git a/apps/timestamplog/metadata.json b/apps/timestamplog/metadata.json new file mode 100644 index 0000000000..e1aa0eb239 --- /dev/null +++ b/apps/timestamplog/metadata.json @@ -0,0 +1,23 @@ +{ + "id": "timestamplog", + "name": "Timestamp log", + "shortName":"Timestamp log", + "icon": "app.png", + "version": "0.01", + "description": "Conveniently record a series of date/time stamps", + "screenshots": [ {"url": "screenshot.png" } ], + "readme": "README.md", + "tags": "timestamp, log", + "supports": ["BANGLEJS2"], + "interface": "interface.html", + "storage": [ + {"name": "timestamplog.app.js", "url": "app.js"}, + {"name": "timestamplog.img", "url": "app-icon.js", "evaluate": true}, + {"name": "timestamplog", "url": "lib.js"}, + {"name": "timestamplog.settings.js", "url": "settings.js"} + ], + "data": [ + {"name": "timestamplog.settings"}, + {"name": "timestamplog.json"} + ] +} diff --git a/apps/timestamplog/screenshot.png b/apps/timestamplog/screenshot.png new file mode 100644 index 0000000000..27f36ac329 Binary files /dev/null and b/apps/timestamplog/screenshot.png differ diff --git a/apps/timestamplog/settings.js b/apps/timestamplog/settings.js new file mode 100644 index 0000000000..137ed31dbe --- /dev/null +++ b/apps/timestamplog/settings.js @@ -0,0 +1,7 @@ +const tsl = require('timestamplog'); + +( + function(backCb) { + tsl.launchSettingsMenu(backCb); + } +);