├── .gitignore ├── LICENSE ├── appinfo.json ├── jsdoc.json ├── releases ├── simply-js-v0.1.1-for-fw-v2.0-beta3.pbw ├── simply-js-v0.2.1-for-fw-v2.0-beta3.pbw ├── simply-js-v0.2.2-for-fw-v2.0-beta4.pbw ├── simply-js-v0.2.3-for-fw-v2.0-beta4.pbw ├── simply-js-v0.2.4-for-fw-v2.0-beta4.pbw ├── simply-js-v0.2.5-for-fw-v2.0-beta5.pbw ├── simply-js-v0.3.0-for-fw-v2.0.0.pbw ├── simply-js-v0.3.1-for-fw-v2.0.0.pbw ├── simply-js-v0.3.2-for-fw-v2.0.1.pbw ├── simply-js-v0.3.3-for-fw-v2.0.2.pbw └── simply-js-v0.3.4-for-fw-v2.0.2.pbw ├── resources ├── fonts │ └── UbuntuMono-Regular.ttf └── images │ ├── logo_splash.png │ └── menu_icon.png ├── src ├── html │ ├── demo.js │ └── settings.html ├── js │ ├── README.md │ ├── ajax.js │ ├── main.js │ ├── moment.min.js │ ├── simply.js │ ├── simply.pebble.js │ └── util2.js ├── simply_accel.c ├── simply_accel.h ├── simply_msg.c ├── simply_msg.h ├── simply_splash.c ├── simply_splash.h ├── simply_ui.c ├── simply_ui.h ├── simplyjs.c └── simplyjs.h └── wscript /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /.lock-* 3 | /build/ 4 | /out/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Meiguro 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /appinfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "133215f0-cf20-4c05-997b-3c9be5a64e5b", 3 | "shortName": "Simply.js", 4 | "longName": "Simply.js", 5 | "companyName": "Meiguro", 6 | "versionCode": 1, 7 | "versionLabel": "0.4", 8 | "capabilities": [ "configurable" ], 9 | "watchapp": { 10 | "watchface": false 11 | }, 12 | "appKeys": {}, 13 | "resources": { 14 | "media": [ 15 | { 16 | "menuIcon": true, 17 | "type": "png", 18 | "name": "IMAGE_MENU_ICON", 19 | "file": "images/menu_icon.png" 20 | }, { 21 | "type": "png", 22 | "name": "IMAGE_LOGO_SPLASH", 23 | "file": "images/logo_splash.png" 24 | }, { 25 | "type": "font", 26 | "name": "MONO_FONT_14", 27 | "file": "fonts/UbuntuMono-Regular.ttf" 28 | } 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "include": [ 4 | "src/js/ajax.js", 5 | "src/js/simply.js" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /releases/simply-js-v0.1.1-for-fw-v2.0-beta3.pbw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meiguro/simplyjs/8cd7a10306fb80eebcdb99b5f55e5f9ae0df8ddd/releases/simply-js-v0.1.1-for-fw-v2.0-beta3.pbw -------------------------------------------------------------------------------- /releases/simply-js-v0.2.1-for-fw-v2.0-beta3.pbw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meiguro/simplyjs/8cd7a10306fb80eebcdb99b5f55e5f9ae0df8ddd/releases/simply-js-v0.2.1-for-fw-v2.0-beta3.pbw -------------------------------------------------------------------------------- /releases/simply-js-v0.2.2-for-fw-v2.0-beta4.pbw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meiguro/simplyjs/8cd7a10306fb80eebcdb99b5f55e5f9ae0df8ddd/releases/simply-js-v0.2.2-for-fw-v2.0-beta4.pbw -------------------------------------------------------------------------------- /releases/simply-js-v0.2.3-for-fw-v2.0-beta4.pbw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meiguro/simplyjs/8cd7a10306fb80eebcdb99b5f55e5f9ae0df8ddd/releases/simply-js-v0.2.3-for-fw-v2.0-beta4.pbw -------------------------------------------------------------------------------- /releases/simply-js-v0.2.4-for-fw-v2.0-beta4.pbw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meiguro/simplyjs/8cd7a10306fb80eebcdb99b5f55e5f9ae0df8ddd/releases/simply-js-v0.2.4-for-fw-v2.0-beta4.pbw -------------------------------------------------------------------------------- /releases/simply-js-v0.2.5-for-fw-v2.0-beta5.pbw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meiguro/simplyjs/8cd7a10306fb80eebcdb99b5f55e5f9ae0df8ddd/releases/simply-js-v0.2.5-for-fw-v2.0-beta5.pbw -------------------------------------------------------------------------------- /releases/simply-js-v0.3.0-for-fw-v2.0.0.pbw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meiguro/simplyjs/8cd7a10306fb80eebcdb99b5f55e5f9ae0df8ddd/releases/simply-js-v0.3.0-for-fw-v2.0.0.pbw -------------------------------------------------------------------------------- /releases/simply-js-v0.3.1-for-fw-v2.0.0.pbw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meiguro/simplyjs/8cd7a10306fb80eebcdb99b5f55e5f9ae0df8ddd/releases/simply-js-v0.3.1-for-fw-v2.0.0.pbw -------------------------------------------------------------------------------- /releases/simply-js-v0.3.2-for-fw-v2.0.1.pbw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meiguro/simplyjs/8cd7a10306fb80eebcdb99b5f55e5f9ae0df8ddd/releases/simply-js-v0.3.2-for-fw-v2.0.1.pbw -------------------------------------------------------------------------------- /releases/simply-js-v0.3.3-for-fw-v2.0.2.pbw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meiguro/simplyjs/8cd7a10306fb80eebcdb99b5f55e5f9ae0df8ddd/releases/simply-js-v0.3.3-for-fw-v2.0.2.pbw -------------------------------------------------------------------------------- /releases/simply-js-v0.3.4-for-fw-v2.0.2.pbw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meiguro/simplyjs/8cd7a10306fb80eebcdb99b5f55e5f9ae0df8ddd/releases/simply-js-v0.3.4-for-fw-v2.0.2.pbw -------------------------------------------------------------------------------- /resources/fonts/UbuntuMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meiguro/simplyjs/8cd7a10306fb80eebcdb99b5f55e5f9ae0df8ddd/resources/fonts/UbuntuMono-Regular.ttf -------------------------------------------------------------------------------- /resources/images/logo_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meiguro/simplyjs/8cd7a10306fb80eebcdb99b5f55e5f9ae0df8ddd/resources/images/logo_splash.png -------------------------------------------------------------------------------- /resources/images/menu_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meiguro/simplyjs/8cd7a10306fb80eebcdb99b5f55e5f9ae0df8ddd/resources/images/menu_icon.png -------------------------------------------------------------------------------- /src/html/demo.js: -------------------------------------------------------------------------------- 1 | console.log('Simply.js demo!'); 2 | 3 | simply.on('singleClick', function(e) { 4 | console.log(util2.format('single clicked $button!', e)); 5 | simply.subtitle('Pressed ' + e.button + '!'); 6 | }); 7 | 8 | simply.on('longClick', function(e) { 9 | console.log(util2.format('long clicked $button!', e)); 10 | simply.vibe(); 11 | simply.scrollable(e.button !== 'select'); 12 | }); 13 | 14 | simply.on('accelTap', function(e) { 15 | console.log(util2.format('tapped accel axis $axis $direction!', e)); 16 | simply.subtitle('Tapped ' + (e.direction > 0 ? '+' : '-') + e.axis + '!'); 17 | }); 18 | 19 | simply.text({ 20 | title: 'Simply Demo!', 21 | body: 'This is a demo. Press buttons or tap the watch!', 22 | }, true); 23 | 24 | -------------------------------------------------------------------------------- /src/html/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simply.js Settings 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

Simply.js Settings

23 |
24 |
25 |
26 |
27 |
28 | 29 | 30 | 33 |
34 |
35 |
36 | 37 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/js/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Simply.js 3 | 4 | Simply.js allows you to write interactive text for your Pebble with just JavaScript. 5 | 6 | This is the API reference of Simply.js generated with JSDoc. 7 | 8 | Simply.js provides the following modules: 9 | 10 | * [simply](simply.html) - The Simply.js framework. 11 | * [ajax](global.html#ajax) - An ajax micro library. 12 | * [require](global.html#require) - A synchronous dependency loader provided by simply. 13 | 14 | Visit [simplyjs.meiguro.com](http://simplyjs.meiguro.com) for more details. 15 | 16 | -------------------------------------------------------------------------------- /src/js/ajax.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ajax.js by Meiguro - MIT License 3 | */ 4 | 5 | var ajax = (function(){ 6 | 7 | var formify = function(data) { 8 | var params = [], i = 0; 9 | for (var name in data) { 10 | params[i++] = encodeURI(name) + '=' + encodeURI(data[name]); 11 | } 12 | return params.join('&'); 13 | }; 14 | 15 | var deformify = function(form) { 16 | var params = {}; 17 | form.replace(/(?:([^=&]*)=?([^&]*)?)(?:&|$)/g, function(_, name, value) { 18 | if (name) { 19 | params[name] = value || true; 20 | } 21 | return _; 22 | }); 23 | return params; 24 | }; 25 | 26 | /** 27 | * ajax options. There are various properties with url being the only required property. 28 | * @typedef ajaxOptions 29 | * @property {string} [method='get'] - The HTTP method to use: 'get', 'post', 'put', 'delete', 'options', 30 | * or any other standard method supported by the running environment. 31 | * @property {string} url - The URL to make the ajax request to. e.g. 'http://www.example.com?name=value' 32 | * @property {string} [type='text'] - The expected response format. Specify 'json' to have ajax parse 33 | * the response as json and pass an object as the data parameter. 34 | * @property {object} [data] - The request body, mainly to be used in combination with 'post' or 'put'. 35 | * e.g. { username: 'guest' } 36 | * @property {object} headers - Custom HTTP headers. Specify additional headers. 37 | * e.g. { 'x-extra': 'Extra Header' } 38 | * @property {boolean} [async=true] - Whether the request will be asynchronous. 39 | * Specify false for a blocking, synchronous request. 40 | * @property {boolean} [cache=true] - Whether the result may be cached. 41 | * Specify false to use the internal cache buster which appends the URL with the query parameter _ 42 | * set to the current time in milliseconds. 43 | */ 44 | 45 | /** 46 | * ajax allows you to make various http or https requests. 47 | * See {@link ajaxOptions} 48 | * @global 49 | * @param {ajaxOptions} opt - Options specifying the type of ajax request to make. 50 | * @param {function} success - The success handler that is called when a HTTP 200 response is given. 51 | * @param {function} failure - The failure handler when the HTTP request fails or is not 200. 52 | */ 53 | var ajax = function(opt, success, failure) { 54 | if (typeof opt === 'string') { 55 | opt = { url: opt }; 56 | } 57 | var method = opt.method || 'GET'; 58 | var url = opt.url; 59 | //console.log(method + ' ' + url); 60 | 61 | var onHandler = ajax.onHandler; 62 | if (onHandler) { 63 | if (success) { success = onHandler('success', success); } 64 | if (failure) { failure = onHandler('failure', failure); } 65 | } 66 | 67 | if (opt.cache === false) { 68 | var appendSymbol = url.indexOf('?') === -1 ? '?' : '&'; 69 | url += appendSymbol + '_=' + new Date().getTime(); 70 | } 71 | 72 | var req = new XMLHttpRequest(); 73 | req.open(method.toUpperCase(), url, opt.async !== false); 74 | 75 | var headers = opt.headers; 76 | if (headers) { 77 | for (var name in headers) { 78 | req.setRequestHeader(name, headers[name]); 79 | } 80 | } 81 | 82 | var data = opt.data; 83 | if (data) { 84 | if (opt.type === 'json') { 85 | req.setRequestHeader('Content-Type', 'application/json'); 86 | data = JSON.stringify(opt.data); 87 | } else if (opt.type !== 'text') { 88 | req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 89 | data = formify(opt.data); 90 | } 91 | } 92 | 93 | req.onreadystatechange = function(e) { 94 | if (req.readyState === 4) { 95 | var body = req.responseText; 96 | var okay = req.status >= 200 && req.status < 300 || req.status === 304; 97 | 98 | try { 99 | if (opt.type === 'json') { 100 | body = JSON.parse(body); 101 | } else if (opt.type === 'form') { 102 | body = deformify(body); 103 | } 104 | } catch (err) { 105 | okay = false; 106 | } 107 | var callback = okay ? success : failure; 108 | if (callback) { 109 | callback(body, req.status, req); 110 | } 111 | } 112 | }; 113 | 114 | req.send(data); 115 | }; 116 | 117 | ajax.formify = formify; 118 | ajax.deformify = deformify; 119 | 120 | return ajax; 121 | 122 | })(); 123 | -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | /* global SimplyPebble */ 2 | 3 | (function() { 4 | 5 | Pebble.addEventListener('ready', function(e) { 6 | SimplyPebble.init(); 7 | }); 8 | 9 | })(); 10 | -------------------------------------------------------------------------------- /src/js/moment.min.js: -------------------------------------------------------------------------------- 1 | //! moment.js 2 | //! version : 2.5.0 3 | //! authors : Tim Wood, Iskren Chernev, Moment.js contributors 4 | //! license : MIT 5 | //! momentjs.com 6 | (function(a){function b(a,b){return function(c){return i(a.call(this,c),b)}}function c(a,b){return function(c){return this.lang().ordinal(a.call(this,c),b)}}function d(){}function e(a){u(a),g(this,a)}function f(a){var b=o(a),c=b.year||0,d=b.month||0,e=b.week||0,f=b.day||0,g=b.hour||0,h=b.minute||0,i=b.second||0,j=b.millisecond||0;this._milliseconds=+j+1e3*i+6e4*h+36e5*g,this._days=+f+7*e,this._months=+d+12*c,this._data={},this._bubble()}function g(a,b){for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c]);return b.hasOwnProperty("toString")&&(a.toString=b.toString),b.hasOwnProperty("valueOf")&&(a.valueOf=b.valueOf),a}function h(a){return 0>a?Math.ceil(a):Math.floor(a)}function i(a,b,c){for(var d=Math.abs(a)+"",e=a>=0;d.lengthd;d++)(c&&a[d]!==b[d]||!c&&q(a[d])!==q(b[d]))&&g++;return g+f}function n(a){if(a){var b=a.toLowerCase().replace(/(.)s$/,"$1");a=Qb[a]||Rb[b]||b}return a}function o(a){var b,c,d={};for(c in a)a.hasOwnProperty(c)&&(b=n(c),b&&(d[b]=a[c]));return d}function p(b){var c,d;if(0===b.indexOf("week"))c=7,d="day";else{if(0!==b.indexOf("month"))return;c=12,d="month"}cb[b]=function(e,f){var g,h,i=cb.fn._lang[b],j=[];if("number"==typeof e&&(f=e,e=a),h=function(a){var b=cb().utc().set(d,a);return i.call(cb.fn._lang,b,e||"")},null!=f)return h(f);for(g=0;c>g;g++)j.push(h(g));return j}}function q(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=b>=0?Math.floor(b):Math.ceil(b)),c}function r(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function s(a){return t(a)?366:365}function t(a){return a%4===0&&a%100!==0||a%400===0}function u(a){var b;a._a&&-2===a._pf.overflow&&(b=a._a[ib]<0||a._a[ib]>11?ib:a._a[jb]<1||a._a[jb]>r(a._a[hb],a._a[ib])?jb:a._a[kb]<0||a._a[kb]>23?kb:a._a[lb]<0||a._a[lb]>59?lb:a._a[mb]<0||a._a[mb]>59?mb:a._a[nb]<0||a._a[nb]>999?nb:-1,a._pf._overflowDayOfYear&&(hb>b||b>jb)&&(b=jb),a._pf.overflow=b)}function v(a){a._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function w(a){return null==a._isValid&&(a._isValid=!isNaN(a._d.getTime())&&a._pf.overflow<0&&!a._pf.empty&&!a._pf.invalidMonth&&!a._pf.nullInput&&!a._pf.invalidFormat&&!a._pf.userInvalidated,a._strict&&(a._isValid=a._isValid&&0===a._pf.charsLeftOver&&0===a._pf.unusedTokens.length)),a._isValid}function x(a){return a?a.toLowerCase().replace("_","-"):a}function y(a,b){return b._isUTC?cb(a).zone(b._offset||0):cb(a).local()}function z(a,b){return b.abbr=a,ob[a]||(ob[a]=new d),ob[a].set(b),ob[a]}function A(a){delete ob[a]}function B(a){var b,c,d,e,f=0,g=function(a){if(!ob[a]&&pb)try{require("./lang/"+a)}catch(b){}return ob[a]};if(!a)return cb.fn._lang;if(!k(a)){if(c=g(a))return c;a=[a]}for(;f0;){if(c=g(e.slice(0,b).join("-")))return c;if(d&&d.length>=b&&m(e,d,!0)>=b-1)break;b--}f++}return cb.fn._lang}function C(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function D(a){var b,c,d=a.match(tb);for(b=0,c=d.length;c>b;b++)d[b]=Vb[d[b]]?Vb[d[b]]:C(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function E(a,b){return a.isValid()?(b=F(b,a.lang()),Sb[b]||(Sb[b]=D(b)),Sb[b](a)):a.lang().invalidDate()}function F(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(ub.lastIndex=0;d>=0&&ub.test(a);)a=a.replace(ub,c),ub.lastIndex=0,d-=1;return a}function G(a,b){var c,d=b._strict;switch(a){case"DDDD":return Gb;case"YYYY":case"GGGG":case"gggg":return d?Hb:xb;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return d?Ib:yb;case"S":if(d)return Eb;case"SS":if(d)return Fb;case"SSS":case"DDD":return d?Gb:wb;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Ab;case"a":case"A":return B(b._l)._meridiemParse;case"X":return Db;case"Z":case"ZZ":return Bb;case"T":return Cb;case"SSSS":return zb;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return d?Fb:vb;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return d?Eb:vb;default:return c=new RegExp(O(N(a.replace("\\","")),"i"))}}function H(a){a=a||"";var b=a.match(Bb)||[],c=b[b.length-1]||[],d=(c+"").match(Nb)||["-",0,0],e=+(60*d[1])+q(d[2]);return"+"===d[0]?-e:e}function I(a,b,c){var d,e=c._a;switch(a){case"M":case"MM":null!=b&&(e[ib]=q(b)-1);break;case"MMM":case"MMMM":d=B(c._l).monthsParse(b),null!=d?e[ib]=d:c._pf.invalidMonth=b;break;case"D":case"DD":null!=b&&(e[jb]=q(b));break;case"DDD":case"DDDD":null!=b&&(c._dayOfYear=q(b));break;case"YY":e[hb]=q(b)+(q(b)>68?1900:2e3);break;case"YYYY":case"YYYYY":case"YYYYYY":e[hb]=q(b);break;case"a":case"A":c._isPm=B(c._l).isPM(b);break;case"H":case"HH":case"h":case"hh":e[kb]=q(b);break;case"m":case"mm":e[lb]=q(b);break;case"s":case"ss":e[mb]=q(b);break;case"S":case"SS":case"SSS":case"SSSS":e[nb]=q(1e3*("0."+b));break;case"X":c._d=new Date(1e3*parseFloat(b));break;case"Z":case"ZZ":c._useUTC=!0,c._tzm=H(b);break;case"w":case"ww":case"W":case"WW":case"d":case"dd":case"ddd":case"dddd":case"e":case"E":a=a.substr(0,1);case"gg":case"gggg":case"GG":case"GGGG":case"GGGGG":a=a.substr(0,2),b&&(c._w=c._w||{},c._w[a]=b)}}function J(a){var b,c,d,e,f,g,h,i,j,k,l=[];if(!a._d){for(d=L(a),a._w&&null==a._a[jb]&&null==a._a[ib]&&(f=function(b){var c=parseInt(b,10);return b?b.length<3?c>68?1900+c:2e3+c:c:null==a._a[hb]?cb().weekYear():a._a[hb]},g=a._w,null!=g.GG||null!=g.W||null!=g.E?h=Y(f(g.GG),g.W||1,g.E,4,1):(i=B(a._l),j=null!=g.d?U(g.d,i):null!=g.e?parseInt(g.e,10)+i._week.dow:0,k=parseInt(g.w,10)||1,null!=g.d&&js(e)&&(a._pf._overflowDayOfYear=!0),c=T(e,0,a._dayOfYear),a._a[ib]=c.getUTCMonth(),a._a[jb]=c.getUTCDate()),b=0;3>b&&null==a._a[b];++b)a._a[b]=l[b]=d[b];for(;7>b;b++)a._a[b]=l[b]=null==a._a[b]?2===b?1:0:a._a[b];l[kb]+=q((a._tzm||0)/60),l[lb]+=q((a._tzm||0)%60),a._d=(a._useUTC?T:S).apply(null,l)}}function K(a){var b;a._d||(b=o(a._i),a._a=[b.year,b.month,b.day,b.hour,b.minute,b.second,b.millisecond],J(a))}function L(a){var b=new Date;return a._useUTC?[b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate()]:[b.getFullYear(),b.getMonth(),b.getDate()]}function M(a){a._a=[],a._pf.empty=!0;var b,c,d,e,f,g=B(a._l),h=""+a._i,i=h.length,j=0;for(d=F(a._f,g).match(tb)||[],b=0;b0&&a._pf.unusedInput.push(f),h=h.slice(h.indexOf(c)+c.length),j+=c.length),Vb[e]?(c?a._pf.empty=!1:a._pf.unusedTokens.push(e),I(e,c,a)):a._strict&&!c&&a._pf.unusedTokens.push(e);a._pf.charsLeftOver=i-j,h.length>0&&a._pf.unusedInput.push(h),a._isPm&&a._a[kb]<12&&(a._a[kb]+=12),a._isPm===!1&&12===a._a[kb]&&(a._a[kb]=0),J(a),u(a)}function N(a){return a.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e})}function O(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function P(a){var b,c,d,e,f;if(0===a._f.length)return a._pf.invalidFormat=!0,a._d=new Date(0/0),void 0;for(e=0;ef)&&(d=f,c=b));g(a,c||b)}function Q(a){var b,c=a._i,d=Jb.exec(c);if(d){for(a._pf.iso=!0,b=4;b>0;b--)if(d[b]){a._f=Lb[b-1]+(d[6]||" ");break}for(b=0;4>b;b++)if(Mb[b][1].exec(c)){a._f+=Mb[b][0];break}c.match(Bb)&&(a._f+="Z"),M(a)}else a._d=new Date(c)}function R(b){var c=b._i,d=qb.exec(c);c===a?b._d=new Date:d?b._d=new Date(+d[1]):"string"==typeof c?Q(b):k(c)?(b._a=c.slice(0),J(b)):l(c)?b._d=new Date(+c):"object"==typeof c?K(b):b._d=new Date(c)}function S(a,b,c,d,e,f,g){var h=new Date(a,b,c,d,e,f,g);return 1970>a&&h.setFullYear(a),h}function T(a){var b=new Date(Date.UTC.apply(null,arguments));return 1970>a&&b.setUTCFullYear(a),b}function U(a,b){if("string"==typeof a)if(isNaN(a)){if(a=b.weekdaysParse(a),"number"!=typeof a)return null}else a=parseInt(a,10);return a}function V(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function W(a,b,c){var d=gb(Math.abs(a)/1e3),e=gb(d/60),f=gb(e/60),g=gb(f/24),h=gb(g/365),i=45>d&&["s",d]||1===e&&["m"]||45>e&&["mm",e]||1===f&&["h"]||22>f&&["hh",f]||1===g&&["d"]||25>=g&&["dd",g]||45>=g&&["M"]||345>g&&["MM",gb(g/30)]||1===h&&["y"]||["yy",h];return i[2]=b,i[3]=a>0,i[4]=c,V.apply({},i)}function X(a,b,c){var d,e=c-b,f=c-a.day();return f>e&&(f-=7),e-7>f&&(f+=7),d=cb(a).add("d",f),{week:Math.ceil(d.dayOfYear()/7),year:d.year()}}function Y(a,b,c,d,e){var f,g,h=new Date(i(a,6,!0)+"-01-01").getUTCDay();return c=null!=c?c:e,f=e-h+(h>d?7:0),g=7*(b-1)+(c-e)+f+1,{year:g>0?a:a-1,dayOfYear:g>0?g:s(a-1)+g}}function Z(a){var b=a._i,c=a._f;return"undefined"==typeof a._pf&&v(a),null===b?cb.invalid({nullInput:!0}):("string"==typeof b&&(a._i=b=B().preparse(b)),cb.isMoment(b)?(a=g({},b),a._d=new Date(+b._d)):c?k(c)?P(a):M(a):R(a),new e(a))}function $(a,b){cb.fn[a]=cb.fn[a+"s"]=function(a){var c=this._isUTC?"UTC":"";return null!=a?(this._d["set"+c+b](a),cb.updateOffset(this),this):this._d["get"+c+b]()}}function _(a){cb.duration.fn[a]=function(){return this._data[a]}}function ab(a,b){cb.duration.fn["as"+a]=function(){return+this/b}}function bb(a){var b=!1,c=cb;"undefined"==typeof ender&&(a?(fb.moment=function(){return!b&&console&&console.warn&&(b=!0,console.warn("Accessing Moment through the global scope is deprecated, and will be removed in an upcoming release.")),c.apply(null,arguments)},g(fb.moment,c)):fb.moment=cb)}for(var cb,db,eb="2.5.0",fb=this,gb=Math.round,hb=0,ib=1,jb=2,kb=3,lb=4,mb=5,nb=6,ob={},pb="undefined"!=typeof module&&module.exports&&"undefined"!=typeof require,qb=/^\/?Date\((\-?\d+)/i,rb=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,sb=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,tb=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,ub=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,vb=/\d\d?/,wb=/\d{1,3}/,xb=/\d{1,4}/,yb=/[+\-]?\d{1,6}/,zb=/\d+/,Ab=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Bb=/Z|[\+\-]\d\d:?\d\d/gi,Cb=/T/i,Db=/[\+\-]?\d+(\.\d{1,3})?/,Eb=/\d/,Fb=/\d\d/,Gb=/\d{3}/,Hb=/\d{4}/,Ib=/[+\-]?\d{6}/,Jb=/^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Kb="YYYY-MM-DDTHH:mm:ssZ",Lb=["YYYY-MM-DD","GGGG-[W]WW","GGGG-[W]WW-E","YYYY-DDD"],Mb=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],Nb=/([\+\-]|\d\d)/gi,Ob="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),Pb={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},Qb={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",D:"date",w:"week",W:"isoWeek",M:"month",y:"year",DDD:"dayOfYear",e:"weekday",E:"isoWeekday",gg:"weekYear",GG:"isoWeekYear"},Rb={dayofyear:"dayOfYear",isoweekday:"isoWeekday",isoweek:"isoWeek",weekyear:"weekYear",isoweekyear:"isoWeekYear"},Sb={},Tb="DDD w W M D d".split(" "),Ub="M D H h m s w W".split(" "),Vb={M:function(){return this.month()+1},MMM:function(a){return this.lang().monthsShort(this,a)},MMMM:function(a){return this.lang().months(this,a)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(a){return this.lang().weekdaysMin(this,a)},ddd:function(a){return this.lang().weekdaysShort(this,a)},dddd:function(a){return this.lang().weekdays(this,a)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return i(this.year()%100,2)},YYYY:function(){return i(this.year(),4)},YYYYY:function(){return i(this.year(),5)},YYYYYY:function(){var a=this.year(),b=a>=0?"+":"-";return b+i(Math.abs(a),6)},gg:function(){return i(this.weekYear()%100,2)},gggg:function(){return this.weekYear()},ggggg:function(){return i(this.weekYear(),5)},GG:function(){return i(this.isoWeekYear()%100,2)},GGGG:function(){return this.isoWeekYear()},GGGGG:function(){return i(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return q(this.milliseconds()/100)},SS:function(){return i(q(this.milliseconds()/10),2)},SSS:function(){return i(this.milliseconds(),3)},SSSS:function(){return i(this.milliseconds(),3)},Z:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+i(q(a/60),2)+":"+i(q(a)%60,2)},ZZ:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+i(q(a/60),2)+i(q(a)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()},Q:function(){return this.quarter()}},Wb=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];Tb.length;)db=Tb.pop(),Vb[db+"o"]=c(Vb[db],db);for(;Ub.length;)db=Ub.pop(),Vb[db+db]=b(Vb[db],2);for(Vb.DDDD=b(Vb.DDD,3),g(d.prototype,{set:function(a){var b,c;for(c in a)b=a[c],"function"==typeof b?this[c]=b:this["_"+c]=b},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(a){return this._months[a.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(a){return this._monthsShort[a.month()]},monthsParse:function(a){var b,c,d;for(this._monthsParse||(this._monthsParse=[]),b=0;12>b;b++)if(this._monthsParse[b]||(c=cb.utc([2e3,b]),d="^"+this.months(c,"")+"|^"+this.monthsShort(c,""),this._monthsParse[b]=new RegExp(d.replace(".",""),"i")),this._monthsParse[b].test(a))return b},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(a){return this._weekdays[a.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(a){return this._weekdaysShort[a.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(a){return this._weekdaysMin[a.day()]},weekdaysParse:function(a){var b,c,d;for(this._weekdaysParse||(this._weekdaysParse=[]),b=0;7>b;b++)if(this._weekdaysParse[b]||(c=cb([2e3,1]).day(b),d="^"+this.weekdays(c,"")+"|^"+this.weekdaysShort(c,"")+"|^"+this.weekdaysMin(c,""),this._weekdaysParse[b]=new RegExp(d.replace(".",""),"i")),this._weekdaysParse[b].test(a))return b},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(a){var b=this._longDateFormat[a];return!b&&this._longDateFormat[a.toUpperCase()]&&(b=this._longDateFormat[a.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a]=b),b},isPM:function(a){return"p"===(a+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(a,b){var c=this._calendar[a];return"function"==typeof c?c.apply(b):c},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(a,b,c,d){var e=this._relativeTime[c];return"function"==typeof e?e(a,b,c,d):e.replace(/%d/i,a)},pastFuture:function(a,b){var c=this._relativeTime[a>0?"future":"past"];return"function"==typeof c?c(b):c.replace(/%s/i,b)},ordinal:function(a){return this._ordinal.replace("%d",a)},_ordinal:"%d",preparse:function(a){return a},postformat:function(a){return a},week:function(a){return X(a,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),cb=function(b,c,d,e){return"boolean"==typeof d&&(e=d,d=a),Z({_i:b,_f:c,_l:d,_strict:e,_isUTC:!1})},cb.utc=function(b,c,d,e){var f;return"boolean"==typeof d&&(e=d,d=a),f=Z({_useUTC:!0,_isUTC:!0,_l:d,_i:b,_f:c,_strict:e}).utc()},cb.unix=function(a){return cb(1e3*a)},cb.duration=function(a,b){var c,d,e,g=a,h=null;return cb.isDuration(a)?g={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(g={},b?g[b]=a:g.milliseconds=a):(h=rb.exec(a))?(c="-"===h[1]?-1:1,g={y:0,d:q(h[jb])*c,h:q(h[kb])*c,m:q(h[lb])*c,s:q(h[mb])*c,ms:q(h[nb])*c}):(h=sb.exec(a))&&(c="-"===h[1]?-1:1,e=function(a){var b=a&&parseFloat(a.replace(",","."));return(isNaN(b)?0:b)*c},g={y:e(h[2]),M:e(h[3]),d:e(h[4]),h:e(h[5]),m:e(h[6]),s:e(h[7]),w:e(h[8])}),d=new f(g),cb.isDuration(a)&&a.hasOwnProperty("_lang")&&(d._lang=a._lang),d},cb.version=eb,cb.defaultFormat=Kb,cb.updateOffset=function(){},cb.lang=function(a,b){var c;return a?(b?z(x(a),b):null===b?(A(a),a="en"):ob[a]||B(a),c=cb.duration.fn._lang=cb.fn._lang=B(a),c._abbr):cb.fn._lang._abbr},cb.langData=function(a){return a&&a._lang&&a._lang._abbr&&(a=a._lang._abbr),B(a)},cb.isMoment=function(a){return a instanceof e},cb.isDuration=function(a){return a instanceof f},db=Wb.length-1;db>=0;--db)p(Wb[db]);for(cb.normalizeUnits=function(a){return n(a)},cb.invalid=function(a){var b=cb.utc(0/0);return null!=a?g(b._pf,a):b._pf.userInvalidated=!0,b},cb.parseZone=function(a){return cb(a).parseZone()},g(cb.fn=e.prototype,{clone:function(){return cb(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().lang("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var a=cb(this).utc();return 00:!1},parsingFlags:function(){return g({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(a){var b=E(this,a||cb.defaultFormat);return this.lang().postformat(b)},add:function(a,b){var c;return c="string"==typeof a?cb.duration(+b,a):cb.duration(a,b),j(this,c,1),this},subtract:function(a,b){var c;return c="string"==typeof a?cb.duration(+b,a):cb.duration(a,b),j(this,c,-1),this},diff:function(a,b,c){var d,e,f=y(a,this),g=6e4*(this.zone()-f.zone());return b=n(b),"year"===b||"month"===b?(d=432e5*(this.daysInMonth()+f.daysInMonth()),e=12*(this.year()-f.year())+(this.month()-f.month()),e+=(this-cb(this).startOf("month")-(f-cb(f).startOf("month")))/d,e-=6e4*(this.zone()-cb(this).startOf("month").zone()-(f.zone()-cb(f).startOf("month").zone()))/d,"year"===b&&(e/=12)):(d=this-f,e="second"===b?d/1e3:"minute"===b?d/6e4:"hour"===b?d/36e5:"day"===b?(d-g)/864e5:"week"===b?(d-g)/6048e5:d),c?e:h(e)},from:function(a,b){return cb.duration(this.diff(a)).lang(this.lang()._abbr).humanize(!b)},fromNow:function(a){return this.from(cb(),a)},calendar:function(){var a=y(cb(),this).startOf("day"),b=this.diff(a,"days",!0),c=-6>b?"sameElse":-1>b?"lastWeek":0>b?"lastDay":1>b?"sameDay":2>b?"nextDay":7>b?"nextWeek":"sameElse";return this.format(this.lang().calendar(c,this))},isLeapYear:function(){return t(this.year())},isDST:function(){return this.zone()+cb(a).startOf(b)},isBefore:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)<+cb(a).startOf(b)},isSame:function(a,b){return b=b||"ms",+this.clone().startOf(b)===+y(a,this).startOf(b)},min:function(a){return a=cb.apply(null,arguments),this>a?this:a},max:function(a){return a=cb.apply(null,arguments),a>this?this:a},zone:function(a){var b=this._offset||0;return null==a?this._isUTC?b:this._d.getTimezoneOffset():("string"==typeof a&&(a=H(a)),Math.abs(a)<16&&(a=60*a),this._offset=a,this._isUTC=!0,b!==a&&j(this,cb.duration(b-a,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.zone(this._tzm):"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(a){return a=a?cb(a).zone():0,(this.zone()-a)%60===0},daysInMonth:function(){return r(this.year(),this.month())},dayOfYear:function(a){var b=gb((cb(this).startOf("day")-cb(this).startOf("year"))/864e5)+1;return null==a?b:this.add("d",a-b)},quarter:function(){return Math.ceil((this.month()+1)/3)},weekYear:function(a){var b=X(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==a?b:this.add("y",a-b)},isoWeekYear:function(a){var b=X(this,1,4).year;return null==a?b:this.add("y",a-b)},week:function(a){var b=this.lang().week(this);return null==a?b:this.add("d",7*(a-b))},isoWeek:function(a){var b=X(this,1,4).week;return null==a?b:this.add("d",7*(a-b))},weekday:function(a){var b=(this.day()+7-this.lang()._week.dow)%7;return null==a?b:this.add("d",a-b)},isoWeekday:function(a){return null==a?this.day()||7:this.day(this.day()%7?a:a-7)},get:function(a){return a=n(a),this[a]()},set:function(a,b){return a=n(a),"function"==typeof this[a]&&this[a](b),this},lang:function(b){return b===a?this._lang:(this._lang=B(b),this)}}),db=0;db 0; 458 | if (useBack !== buttonState.config.back) { 459 | buttonState.config.back = useBack; 460 | return simply.buttonConfig(buttonState.config, true); 461 | } 462 | }; 463 | 464 | /** 465 | * The text definition parameter for {@link simply.text}. 466 | * @typedef {object} simply.textDef 467 | * @property {string} [title] - A new title for the first and largest text field. 468 | * @property {string} [subtitle] - A new subtitle for the second large text field. 469 | * @property {string} [body] - A new body for the last text field meant to display large bodies of text. 470 | */ 471 | 472 | /** 473 | * Sets a group of text fields at once. 474 | * For example, passing a text definition { title: 'A', subtitle: 'B', body: 'C' } 475 | * will set the title, subtitle, and body simultaneously. Not all fields need to be specified. 476 | * When setting a single field, consider using the specific text setters simply.title, simply.subtitle, simply.body. 477 | * @memberOf simply 478 | * @param {simply.textDef} textDef - An object defining new text values. 479 | * @param {boolean} [clear] - If true, all other text fields will be cleared. 480 | */ 481 | simply.text = function(textDef, clear) { 482 | return simply.impl.text.apply(this, arguments); 483 | }; 484 | 485 | simply.setText = simply.text; 486 | 487 | /** 488 | * Sets the title field. The title field is the first and largest text field available. 489 | * @memberOf simply 490 | * @param {string} text - The desired text to display. 491 | * @param {boolean} [clear] - If true, all other text fields will be cleared. 492 | */ 493 | simply.title = function(text, clear) { 494 | return simply.impl.textfield('title', text, clear); 495 | }; 496 | 497 | /** 498 | * Sets the subtitle field. The subtitle field is the second large text field available. 499 | * @memberOf simply 500 | * @param {string} text - The desired text to display. 501 | * @param {boolean} [clear] - If true, all other text fields will be cleared. 502 | */ 503 | simply.subtitle = function(text, clear) { 504 | return simply.impl.textfield('subtitle', text, clear); 505 | }; 506 | 507 | /** 508 | * Sets the body field. The body field is the last text field available meant to display large bodies of text. 509 | * This can be used to display entire text interfaces. 510 | * You may even clear the title and subtitle fields in order to display more in the body field. 511 | * @memberOf simply 512 | * @param {string} text - The desired text to display. 513 | * @param {boolean} [clear] - If true, all other text fields will be cleared. 514 | */ 515 | simply.body = function(text, clear) { 516 | return simply.impl.textfield('body', text, clear); 517 | }; 518 | 519 | /** 520 | * Vibrates the Pebble. 521 | * There are three support vibe types: short, long, and double. 522 | * @memberOf simply 523 | * @param {string} [type=short] - The vibe type. 524 | */ 525 | simply.vibe = function() { 526 | return simply.impl.vibe.apply(this, arguments); 527 | }; 528 | 529 | /** 530 | * Enable scrolling in the Pebble UI. 531 | * When scrolling is enabled, up and down button presses are no longer forwarded to JavaScript handlers. 532 | * Single select, long select, and accel tap events are still available to you however. 533 | * @memberOf simply 534 | * @param {boolean} scrollable - Whether to enable a scrollable view. 535 | */ 536 | 537 | simply.scrollable = function(scrollable) { 538 | return simply.impl.scrollable.apply(this, arguments); 539 | }; 540 | 541 | /** 542 | * Enable fullscreen in the Pebble UI. 543 | * Fullscreen removes the Pebble status bar, giving slightly more vertical display height. 544 | * @memberOf simply 545 | * @param {boolean} fullscreen - Whether to enable fullscreen mode. 546 | */ 547 | 548 | simply.fullscreen = function(fullscreen) { 549 | return simply.impl.fullscreen.apply(this, arguments); 550 | }; 551 | 552 | /** 553 | * Set the Pebble UI style. 554 | * The available styles are 'small', 'large', and 'mono'. Small and large correspond to the system notification styles. 555 | * Mono sets a monospace font for the body textfield, enabling more complex text UIs or ASCII art. 556 | * @memberOf simply 557 | * @param {string} type - The type of style to set: 'small', 'large', or 'mono'. 558 | */ 559 | 560 | simply.style = function(type) { 561 | return simply.impl.style.apply(this, arguments); 562 | }; 563 | 564 | simply.accelInit = function() { 565 | simply.state.accel = { 566 | rate: 100, 567 | samples: 25, 568 | subscribe: false, 569 | subscribeMode: 'auto', 570 | listeners: [], 571 | }; 572 | }; 573 | 574 | simply.accelAutoSubscribe = function() { 575 | var accelState = simply.state.accel; 576 | if (!accelState || accelState.subscribeMode !== 'auto') { 577 | return; 578 | } 579 | var subscribe = simply.countHandlers('accelData') > 0; 580 | if (subscribe !== simply.state.accel.subscribe) { 581 | return simply.accelConfig(subscribe, true); 582 | } 583 | }; 584 | 585 | /** 586 | * The accelerometer configuration parameter for {@link simply.accelConfig}. 587 | * The accelerometer data stream is useful for applications such as gesture recognition when accelTap is too limited. 588 | * However, keep in mind that smaller batch sample sizes and faster rates will drastically impact the battery life of both the Pebble and phone because of the taxing use of the processors and Bluetooth modules. 589 | * @typedef {object} simply.accelConf 590 | * @property {number} [rate] - The rate accelerometer data points are generated in hertz. Valid values are 10, 25, 50, and 100. Initializes as 100. 591 | * @property {number} [samples] - The number of accelerometer data points to accumulate in a batch before calling the event handler. Valid values are 1 to 25 inclusive. Initializes as 25. 592 | * @property {boolean} [subscribe] - Whether to subscribe to accelerometer data events. {@link simply.accelPeek} cannot be used when subscribed. Simply.js will automatically (un)subscribe for you depending on the amount of accelData handlers registered. 593 | */ 594 | 595 | /** 596 | * Changes the accelerometer configuration. 597 | * See {@link simply.accelConfig} 598 | * @memberOf simply 599 | * @param {simply.accelConfig} accelConf - An object defining the accelerometer configuration. 600 | */ 601 | simply.accelConfig = function(opt, auto) { 602 | var accelState = simply.state.accel; 603 | if (typeof opt === 'undefined') { 604 | return { 605 | rate: accelState.rate, 606 | samples: accelState.samples, 607 | subscribe: accelState.subscribe, 608 | }; 609 | } else if (typeof opt === 'boolean') { 610 | opt = { subscribe: opt }; 611 | } 612 | for (var k in opt) { 613 | if (k === 'subscribe') { 614 | accelState.subscribeMode = opt[k] && !auto ? 'manual' : 'auto'; 615 | } 616 | accelState[k] = opt[k]; 617 | } 618 | return simply.impl.accelConfig.apply(this, arguments); 619 | }; 620 | 621 | /** 622 | * Peeks at the current accelerometer values. 623 | * @memberOf simply 624 | * @param {simply.eventHandler} callback - A callback function that will be provided the accel data point as an event. 625 | */ 626 | simply.accelPeek = function(callback) { 627 | if (simply.state.accel.subscribe) { 628 | throw Error('Cannot use accelPeek when listening to accelData events'); 629 | } 630 | return simply.impl.accelPeek.apply(this, arguments); 631 | }; 632 | 633 | /** 634 | * Simply.js event. See all the possible event types. Subscribe to events using {@link simply.on}. 635 | * @typedef simply.event 636 | * @see simply.clickEvent 637 | * @see simply.accelTapEvent 638 | * @see simply.accelDataEvent 639 | */ 640 | 641 | /** 642 | * Simply.js button click event. This can either be a single click or long click. 643 | * Use the event type 'singleClick' or 'longClick' to subscribe to these events. 644 | * @typedef simply.clickEvent 645 | * @property {string} button - The button that was pressed: 'back', 'up', 'select', or 'down'. This is also the event subtype. 646 | */ 647 | 648 | simply.emitClick = function(type, button) { 649 | simply.emit(type, button, { 650 | button: button, 651 | }); 652 | }; 653 | 654 | /** 655 | * Simply.js accel tap event. 656 | * Use the event type 'accelTap' to subscribe to these events. 657 | * @typedef simply.accelTapEvent 658 | * @property {string} axis - The axis the tap event occurred on: 'x', 'y', or 'z'. This is also the event subtype. 659 | * @property {number} direction - The direction of the tap along the axis: 1 or -1. 660 | */ 661 | 662 | simply.emitAccelTap = function(axis, direction) { 663 | simply.emit('accelTap', axis, { 664 | axis: axis, 665 | direction: direction, 666 | }); 667 | }; 668 | 669 | /** 670 | * Simply.js accel data point. 671 | * Typical values for gravity is around -1000 on the z axis. 672 | * @typedef simply.accelPoint 673 | * @property {number} x - The acceleration across the x-axis. 674 | * @property {number} y - The acceleration across the y-axis. 675 | * @property {number} z - The acceleration across the z-axis. 676 | * @property {boolean} vibe - Whether the watch was vibrating when measuring this point. 677 | * @property {number} time - The amount of ticks in millisecond resolution when measuring this point. 678 | */ 679 | 680 | /** 681 | * Simply.js accel data event. 682 | * Use the event type 'accelData' to subscribe to these events. 683 | * @typedef simply.accelDataEvent 684 | * @property {number} samples - The number of accelerometer samples in this event. 685 | * @property {simply.accelPoint} accel - The first accel in the batch. This is provided for convenience. 686 | * @property {simply.accelPoint[]} accels - The accelerometer samples in an array. 687 | */ 688 | 689 | simply.emitAccelData = function(accels, callback) { 690 | var e = { 691 | samples: accels.length, 692 | accel: accels[0], 693 | accels: accels, 694 | }; 695 | if (callback) { 696 | return callback(e); 697 | } 698 | simply.emit('accelData', e); 699 | }; 700 | 701 | return simply; 702 | 703 | })(); 704 | 705 | Pebble.require = require; 706 | var require = simply.require; 707 | -------------------------------------------------------------------------------- /src/js/simply.pebble.js: -------------------------------------------------------------------------------- 1 | /* global simply */ 2 | 3 | var SimplyPebble = (function() { 4 | 5 | var commands = [{ 6 | name: 'setText', 7 | params: [{ 8 | name: 'title', 9 | }, { 10 | name: 'subtitle', 11 | }, { 12 | name: 'body', 13 | }, { 14 | name: 'clear', 15 | }], 16 | }, { 17 | name: 'singleClick', 18 | params: [{ 19 | name: 'button', 20 | }], 21 | }, { 22 | name: 'longClick', 23 | params: [{ 24 | name: 'button', 25 | }], 26 | }, { 27 | name: 'accelTap', 28 | params: [{ 29 | name: 'axis', 30 | }, { 31 | name: 'direction', 32 | }], 33 | }, { 34 | name: 'vibe', 35 | params: [{ 36 | name: 'type', 37 | }], 38 | }, { 39 | name: 'setScrollable', 40 | params: [{ 41 | name: 'scrollable', 42 | }], 43 | }, { 44 | name: 'setStyle', 45 | params: [{ 46 | name: 'type', 47 | }], 48 | }, { 49 | name: 'setFullscreen', 50 | params: [{ 51 | name: 'fullscreen', 52 | }], 53 | }, { 54 | name: 'accelData', 55 | params: [{ 56 | name: 'transactionId', 57 | }, { 58 | name: 'numSamples', 59 | }, { 60 | name: 'accelData', 61 | }], 62 | }, { 63 | name: 'getAccelData', 64 | params: [{ 65 | name: 'transactionId', 66 | }], 67 | }, { 68 | name: 'configAccelData', 69 | params: [{ 70 | name: 'rate', 71 | }, { 72 | name: 'samples', 73 | }, { 74 | name: 'subscribe', 75 | }], 76 | }, { 77 | name: 'configButtons', 78 | params: [{ 79 | name: 'back', 80 | }, { 81 | name: 'up', 82 | }, { 83 | name: 'select', 84 | }, { 85 | name: 'down', 86 | }], 87 | }]; 88 | 89 | var commandMap = {}; 90 | 91 | for (var i = 0, ii = commands.length; i < ii; ++i) { 92 | var command = commands[i]; 93 | commandMap[command.name] = command; 94 | command.id = i; 95 | 96 | var params = command.params; 97 | if (!params) { 98 | continue; 99 | } 100 | 101 | var paramMap = command.paramMap = {}; 102 | for (var j = 0, jj = params.length; j < jj; ++j) { 103 | var param = params[j]; 104 | paramMap[param.name] = param; 105 | param.id = j + 1; 106 | } 107 | } 108 | 109 | var buttons = [ 110 | 'back', 111 | 'up', 112 | 'select', 113 | 'down', 114 | ]; 115 | 116 | var accelAxes = [ 117 | 'x', 118 | 'y', 119 | 'z', 120 | ]; 121 | 122 | var vibeTypes = [ 123 | 'short', 124 | 'long', 125 | 'double', 126 | ]; 127 | 128 | var styleTypes = [ 129 | 'small', 130 | 'large', 131 | 'mono', 132 | ]; 133 | 134 | var SimplyPebble = {}; 135 | 136 | SimplyPebble.init = function() { 137 | simply.impl = SimplyPebble; 138 | simply.init(); 139 | }; 140 | 141 | var getExecPackage = function(execName) { 142 | var packages = simply.packages; 143 | for (var path in packages) { 144 | var package = packages[path]; 145 | if (package && package.execName === execName) { 146 | return path; 147 | } 148 | } 149 | }; 150 | 151 | var getExceptionFile = function(e, level) { 152 | var stack = e.stack.split('\n'); 153 | for (var i = level || 0, ii = stack.length; i < ii; ++i) { 154 | var line = stack[i]; 155 | if (line.match(/^\$\d/)) { 156 | var path = getExecPackage(line); 157 | if (path) { 158 | return path; 159 | } 160 | } 161 | } 162 | return stack[level]; 163 | }; 164 | 165 | var getExceptionScope = function(e, level) { 166 | var stack = e.stack.split('\n'); 167 | for (var i = level || 0, ii = stack.length; i < ii; ++i) { 168 | var line = stack[i]; 169 | if (!line || line.match('native code')) { continue; } 170 | return line.match(/^\$\d/) && getExecPackage(line) || line; 171 | } 172 | return stack[level]; 173 | }; 174 | 175 | var setHandlerPath = function(handler, path, level) { 176 | var level0 = 4; // caller -> wrap -> apply -> wrap -> set 177 | handler.path = path || getExceptionScope(new Error(), (level || 0) + level0) || simply.basename(); 178 | return handler; 179 | }; 180 | 181 | SimplyPebble.papply = function(f, args, path) { 182 | try { 183 | return f.apply(this, args); 184 | } catch (e) { 185 | var scope = !path && getExceptionFile(e) || getExecPackage(path) || path; 186 | console.log(scope + ':' + e.line + ': ' + e + '\n' + e.stack); 187 | simply.text({ 188 | subtitle: scope, 189 | body: e.line + ' ' + e.message, 190 | }, true); 191 | simply.state.run = false; 192 | } 193 | }; 194 | 195 | SimplyPebble.protect = function(f, path) { 196 | return function() { 197 | return SimplyPebble.papply(f, arguments, path); 198 | }; 199 | }; 200 | 201 | SimplyPebble.wrapHandler = function(handler, level) { 202 | if (!handler) { return; } 203 | setHandlerPath(handler, null, level || 1); 204 | var package = simply.packages[handler.path]; 205 | if (package) { 206 | return SimplyPebble.protect(package.fwrap(handler), handler.path); 207 | } else { 208 | return SimplyPebble.protect(handler, handler.path); 209 | } 210 | }; 211 | 212 | var toSafeName = function(name) { 213 | name = name.replace(/[^0-9A-Za-z_$]/g, '_'); 214 | if (name.match(/^[0-9]/)) { 215 | name = '_' + name; 216 | } 217 | return name; 218 | }; 219 | 220 | SimplyPebble.loadPackage = function(pkg, loader) { 221 | pkg.execName = '$' + simply.state.numPackages++ + toSafeName(pkg.name); 222 | pkg.fapply = simply.defun(pkg.execName, ['f', 'args'], 'return f.apply(this, args)'); 223 | pkg.fwrap = function(f) { return function() { return pkg.fapply(f, arguments); }; }; 224 | 225 | return SimplyPebble.papply(loader, null, pkg.name); 226 | }; 227 | 228 | SimplyPebble.onWebViewClosed = function(e) { 229 | if (!e.response) { 230 | return; 231 | } 232 | 233 | var options = JSON.parse(decodeURIComponent(e.response)); 234 | simply.loadMainScript(options.scriptUrl); 235 | }; 236 | 237 | SimplyPebble.getOptions = function() { 238 | return { 239 | scriptUrl: localStorage.getItem('mainJsUrl'), 240 | }; 241 | }; 242 | 243 | SimplyPebble.onShowConfiguration = function(e) { 244 | var options = encodeURIComponent(JSON.stringify(SimplyPebble.getOptions())); 245 | Pebble.openURL(simply.settingsUrl + '#' + options); 246 | }; 247 | 248 | function makePacket(command, def) { 249 | var packet = {}; 250 | packet[0] = command.id; 251 | if (def) { 252 | var paramMap = command.paramMap; 253 | for (var k in def) { 254 | var param = paramMap[k]; 255 | if (param) { 256 | packet[param.id] = def[k]; 257 | } 258 | } 259 | } 260 | return packet; 261 | } 262 | 263 | SimplyPebble.sendPacket = function(packet) { 264 | if (!simply.state.run) { 265 | return; 266 | } 267 | var send; 268 | send = function() { 269 | Pebble.sendAppMessage(packet, util2.noop, send); 270 | }; 271 | send(); 272 | }; 273 | 274 | SimplyPebble.buttonConfig = function(buttonConf) { 275 | var command = commandMap.configButtons; 276 | var packet = makePacket(command, buttonConf); 277 | SimplyPebble.sendPacket(packet); 278 | }; 279 | 280 | SimplyPebble.text = function(textDef, clear) { 281 | var command = commandMap.setText; 282 | var packetDef = {}; 283 | for (var k in textDef) { 284 | packetDef[k] = textDef[k].toString(); 285 | } 286 | var packet = makePacket(command, packetDef); 287 | if (clear) { 288 | packet[command.paramMap.clear.id] = 1; 289 | } 290 | SimplyPebble.sendPacket(packet); 291 | }; 292 | 293 | SimplyPebble.textfield = function(field, text, clear) { 294 | var command = commandMap.setText; 295 | var packet = makePacket(command); 296 | var param = command.paramMap[field]; 297 | if (param) { 298 | packet[param.id] = text.toString(); 299 | } 300 | if (clear) { 301 | packet[command.paramMap.clear.id] = 1; 302 | } 303 | SimplyPebble.sendPacket(packet); 304 | }; 305 | 306 | SimplyPebble.vibe = function(type) { 307 | var command = commandMap.vibe; 308 | var packet = makePacket(command); 309 | var vibeIndex = vibeTypes.indexOf(type); 310 | packet[command.paramMap.type.id] = vibeIndex !== -1 ? vibeIndex : 0; 311 | SimplyPebble.sendPacket(packet); 312 | }; 313 | 314 | SimplyPebble.scrollable = function(scrollable) { 315 | if (scrollable === null) { 316 | return simply.state.scrollable === true; 317 | } 318 | simply.state.scrollable = scrollable; 319 | 320 | var command = commandMap.setScrollable; 321 | var packet = makePacket(command); 322 | packet[command.paramMap.scrollable.id] = scrollable ? 1 : 0; 323 | SimplyPebble.sendPacket(packet); 324 | }; 325 | 326 | SimplyPebble.fullscreen = function(fullscreen) { 327 | if (fullscreen === null) { 328 | return simply.state.fullscreen === true; 329 | } 330 | simply.state.fullscreen = fullscreen; 331 | 332 | var command = commandMap.setFullscreen; 333 | var packet = makePacket(command); 334 | packet[command.paramMap.fullscreen.id] = fullscreen ? 1 : 0; 335 | SimplyPebble.sendPacket(packet); 336 | } 337 | 338 | SimplyPebble.style = function(type) { 339 | var command = commandMap.setStyle; 340 | var packet = makePacket(command); 341 | var styleIndex = styleTypes.indexOf(type); 342 | packet[command.paramMap.type.id] = styleIndex !== -1 ? styleIndex : 1; 343 | SimplyPebble.sendPacket(packet); 344 | }; 345 | 346 | SimplyPebble.accelConfig = function(configDef) { 347 | var command = commandMap.configAccelData; 348 | var packetDef = {}; 349 | for (var k in configDef) { 350 | packetDef[k] = configDef[k]; 351 | } 352 | var packet = makePacket(command, packetDef); 353 | SimplyPebble.sendPacket(packet); 354 | }; 355 | 356 | SimplyPebble.accelPeek = function(callback) { 357 | simply.state.accel.listeners.push(callback); 358 | var command = commandMap.getAccelData; 359 | var packet = makePacket(command); 360 | SimplyPebble.sendPacket(packet); 361 | }; 362 | 363 | readInt = function(packet, width, pos, signed) { 364 | var value = 0; 365 | pos = pos || 0; 366 | for (var i = 0; i < width; ++i) { 367 | value += (packet[pos + i] & 0xFF) << (i * 8); 368 | } 369 | if (signed) { 370 | var mask = 1 << (width * 8 - 1); 371 | if (value & mask) { 372 | value = value - (((mask - 1) << 1) + 1); 373 | } 374 | } 375 | return value; 376 | }; 377 | 378 | SimplyPebble.onAppMessage = function(e) { 379 | var payload = e.payload; 380 | var code = payload[0]; 381 | var command = commands[code]; 382 | 383 | switch (command.name) { 384 | case 'singleClick': 385 | case 'longClick': 386 | var button = buttons[payload[1]]; 387 | simply.emitClick(command.name, button); 388 | break; 389 | case 'accelTap': 390 | var axis = accelAxes[payload[1]]; 391 | simply.emitAccelTap(axis, payload[2]); 392 | break; 393 | case 'accelData': 394 | var transactionId = payload[1]; 395 | var samples = payload[2]; 396 | var data = payload[3]; 397 | var accels = []; 398 | for (var i = 0; i < samples; i++) { 399 | var pos = i * 15; 400 | var accel = { 401 | x: readInt(data, 2, pos, true), 402 | y: readInt(data, 2, pos + 2, true), 403 | z: readInt(data, 2, pos + 4, true), 404 | vibe: readInt(data, 1, pos + 6) ? true : false, 405 | time: readInt(data, 8, pos + 7), 406 | }; 407 | accels[i] = accel; 408 | } 409 | if (typeof transactionId === 'undefined') { 410 | simply.emitAccelData(accels); 411 | } else { 412 | var handlers = simply.state.accel.listeners; 413 | simply.state.accel.listeners = []; 414 | for (var i = 0, ii = handlers.length; i < ii; ++i) { 415 | simply.emitAccelData(accels, handlers[i]); 416 | } 417 | } 418 | break; 419 | } 420 | }; 421 | 422 | Pebble.addEventListener('showConfiguration', SimplyPebble.onShowConfiguration); 423 | Pebble.addEventListener('webviewclosed', SimplyPebble.onWebViewClosed); 424 | Pebble.addEventListener('appmessage', SimplyPebble.onAppMessage); 425 | 426 | return SimplyPebble; 427 | 428 | })(); 429 | -------------------------------------------------------------------------------- /src/js/util2.js: -------------------------------------------------------------------------------- 1 | /* 2 | * util2.js by Meiguro - MIT License 3 | */ 4 | 5 | var util2 = (function(util2){ 6 | 7 | util2.noop = function() {}; 8 | 9 | util2.copy = function (a, b) { 10 | b = b || (a instanceof Array ? [] : {}); 11 | for (var k in a) { b[k] = a[k]; } 12 | return b; 13 | }; 14 | 15 | util2.toInteger = function (x) { 16 | if (!isNaN(x = parseInt(x))) { return x; } 17 | }; 18 | 19 | util2.toNumber = function (x) { 20 | if (!isNaN(x = parseFloat(x))) { return x; } 21 | }; 22 | 23 | util2.toArray = function (x) { 24 | if (x instanceof Array) { return x; } 25 | return [x]; 26 | }; 27 | 28 | util2.trim = function (s) { 29 | return s ? s.toString().trim() : s; 30 | }; 31 | 32 | var chunkSize = 128; 33 | 34 | var randomBytes = function(chunkSize) { 35 | var z = []; 36 | for (var i = 0; i < chunkSize; ++i) { 37 | z[i] = String.fromCharCode(Math.random() * 256); 38 | } 39 | return z.join(''); 40 | }; 41 | 42 | util2.randomString = function (regex, size, acc) { 43 | if (!size) { 44 | return ''; 45 | } 46 | if (typeof regex === 'string') { 47 | regex = new RegExp('(?!'+regex+')[\\s\\S]', 'g'); 48 | } 49 | acc = acc || ''; 50 | var buf = randomBytes(chunkSize); 51 | if (buf) { 52 | acc += buf.replace(regex, ''); 53 | } 54 | if (acc.length >= size) { 55 | return acc.substr(0, size); 56 | } else { 57 | return util2.randomString(regex, size, acc); 58 | } 59 | }; 60 | 61 | var varpat = new RegExp("^([\\s\\S]*?)\\$([_a-zA-Z0-9]+)", "m"); 62 | 63 | util2.format = function (text, table) { 64 | var m, z = ''; 65 | while ((m = text.match(varpat))) { 66 | var subtext = m[0], value = table[m[2]]; 67 | if (typeof value === 'function') { value = value(); } 68 | z += value !== undefined ? m[1] + value.toString() : subtext; 69 | text = text.substring(subtext.length); 70 | } 71 | z += text; 72 | return z; 73 | }; 74 | 75 | return util2; 76 | 77 | })(typeof util2 !== 'undefined' ? util2 : {}); 78 | -------------------------------------------------------------------------------- /src/simply_accel.c: -------------------------------------------------------------------------------- 1 | #include "simply_accel.h" 2 | 3 | #include "simply_msg.h" 4 | 5 | #include "simplyjs.h" 6 | 7 | #include 8 | 9 | SimplyAccel *s_accel = NULL; 10 | 11 | static void handle_accel_data(AccelData *data, uint32_t num_samples) { 12 | simply_msg_accel_data(data, num_samples, TRANSACTION_ID_INVALID); 13 | } 14 | 15 | void simply_accel_set_data_rate(SimplyAccel *self, AccelSamplingRate rate) { 16 | self->rate = rate; 17 | accel_service_set_sampling_rate(rate); 18 | } 19 | 20 | void simply_accel_set_data_samples(SimplyAccel *self, uint32_t num_samples) { 21 | self->num_samples = num_samples; 22 | accel_service_set_samples_per_update(num_samples); 23 | if (!self->data_subscribed) { 24 | return; 25 | } 26 | simply_accel_set_data_subscribe(self, false); 27 | simply_accel_set_data_subscribe(self, true); 28 | } 29 | 30 | void simply_accel_set_data_subscribe(SimplyAccel *self, bool subscribe) { 31 | if (self->data_subscribed == subscribe) { 32 | return; 33 | } 34 | if (subscribe) { 35 | accel_data_service_subscribe(self->num_samples, handle_accel_data); 36 | accel_service_set_sampling_rate(self->rate); 37 | } else { 38 | accel_data_service_unsubscribe(); 39 | } 40 | self->data_subscribed = subscribe; 41 | } 42 | 43 | void simply_accel_peek(SimplyAccel *self, AccelData *data) { 44 | if (self->data_subscribed) { 45 | return; 46 | } 47 | accel_service_peek(data); 48 | } 49 | 50 | static void handle_accel_tap(AccelAxisType axis, int32_t direction) { 51 | simply_msg_accel_tap(axis, direction); 52 | } 53 | 54 | SimplyAccel *simply_accel_create(void) { 55 | if (s_accel) { 56 | return s_accel; 57 | } 58 | 59 | SimplyAccel *self = malloc(sizeof(*self)); 60 | *self = (SimplyAccel) { 61 | .rate = ACCEL_SAMPLING_100HZ, 62 | .num_samples = 25, 63 | }; 64 | s_accel = self; 65 | 66 | accel_tap_service_subscribe(handle_accel_tap); 67 | 68 | return self; 69 | } 70 | 71 | void simply_accel_destroy(SimplyAccel *self) { 72 | if (!s_accel) { 73 | return; 74 | } 75 | 76 | accel_tap_service_unsubscribe(); 77 | 78 | free(self); 79 | 80 | s_accel = NULL; 81 | } 82 | 83 | -------------------------------------------------------------------------------- /src/simply_accel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef struct SimplyAccel SimplyAccel; 6 | 7 | struct SimplyAccel { 8 | bool data_subscribed; 9 | AccelSamplingRate rate; 10 | uint32_t num_samples; 11 | }; 12 | 13 | SimplyAccel *simply_accel_create(void); 14 | 15 | void simply_accel_set_data_rate(SimplyAccel *self, AccelSamplingRate rate); 16 | 17 | void simply_accel_set_data_samples(SimplyAccel *self, uint32_t num_samples); 18 | 19 | void simply_accel_set_data_subscribe(SimplyAccel *self, bool subscribe); 20 | 21 | void simply_accel_peek(SimplyAccel *self, AccelData *data); 22 | 23 | void simply_accel_destroy(SimplyAccel *self); 24 | 25 | -------------------------------------------------------------------------------- /src/simply_msg.c: -------------------------------------------------------------------------------- 1 | #include "simply_msg.h" 2 | 3 | #include "simply_accel.h" 4 | #include "simply_ui.h" 5 | 6 | #include "simplyjs.h" 7 | 8 | #include 9 | 10 | typedef enum SimplyACmd SimplyACmd; 11 | 12 | enum SimplyACmd { 13 | SimplyACmd_setText = 0, 14 | SimplyACmd_singleClick, 15 | SimplyACmd_longClick, 16 | SimplyACmd_accelTap, 17 | SimplyACmd_vibe, 18 | SimplyACmd_setScrollable, 19 | SimplyACmd_setStyle, 20 | SimplyACmd_setFullscreen, 21 | SimplyACmd_accelData, 22 | SimplyACmd_getAccelData, 23 | SimplyACmd_configAccelData, 24 | SimplyACmd_configButtons, 25 | }; 26 | 27 | typedef enum VibeType VibeType; 28 | 29 | enum VibeType { 30 | VibeShort = 0, 31 | VibeLong = 1, 32 | VibeDouble = 2, 33 | }; 34 | 35 | static void check_splash(Simply *simply) { 36 | if (simply->splash) { 37 | simply_ui_show(simply->ui); 38 | } 39 | } 40 | 41 | static void handle_set_text(DictionaryIterator *iter, Simply *simply) { 42 | SimplyUi *ui = simply->ui; 43 | Tuple *tuple; 44 | bool clear = false; 45 | if ((tuple = dict_find(iter, 4))) { 46 | clear = true; 47 | } 48 | if ((tuple = dict_find(iter, 1)) || clear) { 49 | simply_ui_set_text(ui, &ui->title_text, tuple ? tuple->value->cstring : NULL); 50 | } 51 | if ((tuple = dict_find(iter, 2)) || clear) { 52 | simply_ui_set_text(ui, &ui->subtitle_text, tuple ? tuple->value->cstring : NULL); 53 | } 54 | if ((tuple = dict_find(iter, 3)) || clear) { 55 | simply_ui_set_text(ui, &ui->body_text, tuple ? tuple->value->cstring : NULL); 56 | } 57 | 58 | check_splash(simply); 59 | } 60 | 61 | static void handle_vibe(DictionaryIterator *iter, Simply *simply) { 62 | Tuple *tuple; 63 | if ((tuple = dict_find(iter, 1))) { 64 | switch ((VibeType) tuple->value->int32) { 65 | case VibeShort: vibes_short_pulse(); break; 66 | case VibeLong: vibes_short_pulse(); break; 67 | case VibeDouble: vibes_double_pulse(); break; 68 | } 69 | } 70 | } 71 | 72 | static void handle_set_scrollable(DictionaryIterator *iter, Simply *simply) { 73 | Tuple *tuple; 74 | if ((tuple = dict_find(iter, 1))) { 75 | simply_ui_set_scrollable(simply->ui, tuple->value->int32); 76 | } 77 | } 78 | 79 | static void handle_set_style(DictionaryIterator *iter, Simply *simply) { 80 | Tuple *tuple; 81 | if ((tuple = dict_find(iter, 1))) { 82 | simply_ui_set_style(simply->ui, tuple->value->int32); 83 | } 84 | } 85 | 86 | static void handle_set_fullscreen(DictionaryIterator *iter, Simply *simply) { 87 | Tuple *tuple; 88 | if ((tuple = dict_find(iter, 1))) { 89 | simply_ui_set_fullscreen(simply->ui, tuple->value->int32); 90 | } 91 | } 92 | 93 | static void handle_config_buttons(DictionaryIterator *iter, Simply *simply) { 94 | SimplyUi *ui = simply->ui; 95 | Tuple *tuple; 96 | for (int i = 0; i < NUM_BUTTONS; ++i) { 97 | if ((tuple = dict_find(iter, i + 1))) { 98 | simply_ui_set_button(ui, i, tuple->value->int32); 99 | } 100 | } 101 | } 102 | 103 | static void get_accel_data_timer_callback(void *context) { 104 | Simply *simply = context; 105 | AccelData data = { .x = 0 }; 106 | simply_accel_peek(simply->accel, &data); 107 | if (!simply_msg_accel_data(&data, 1, 0)) { 108 | app_timer_register(10, get_accel_data_timer_callback, simply); 109 | } 110 | } 111 | 112 | static void handle_get_accel_data(DictionaryIterator *iter, Simply *simply) { 113 | app_timer_register(10, get_accel_data_timer_callback, simply); 114 | } 115 | 116 | static void handle_set_accel_config(DictionaryIterator *iter, Simply *simply) { 117 | Tuple *tuple; 118 | if ((tuple = dict_find(iter, 1))) { 119 | simply_accel_set_data_rate(simply->accel, tuple->value->int32); 120 | } 121 | if ((tuple = dict_find(iter, 2))) { 122 | simply_accel_set_data_samples(simply->accel, tuple->value->int32); 123 | } 124 | if ((tuple = dict_find(iter, 3))) { 125 | simply_accel_set_data_subscribe(simply->accel, tuple->value->int32); 126 | } 127 | } 128 | 129 | static void received_callback(DictionaryIterator *iter, void *context) { 130 | Tuple *tuple = dict_find(iter, 0); 131 | if (!tuple) { 132 | return; 133 | } 134 | 135 | switch (tuple->value->uint8) { 136 | case SimplyACmd_setText: 137 | handle_set_text(iter, context); 138 | break; 139 | case SimplyACmd_vibe: 140 | handle_vibe(iter, context); 141 | break; 142 | case SimplyACmd_setScrollable: 143 | handle_set_scrollable(iter, context); 144 | break; 145 | case SimplyACmd_setStyle: 146 | handle_set_style(iter, context); 147 | break; 148 | case SimplyACmd_setFullscreen: 149 | handle_set_fullscreen(iter, context); 150 | break; 151 | case SimplyACmd_getAccelData: 152 | handle_get_accel_data(iter, context); 153 | break; 154 | case SimplyACmd_configAccelData: 155 | handle_set_accel_config(iter, context); 156 | break; 157 | case SimplyACmd_configButtons: 158 | handle_config_buttons(iter, context); 159 | break; 160 | } 161 | } 162 | 163 | static void dropped_callback(AppMessageResult reason, void *context) { 164 | } 165 | 166 | static void sent_callback(DictionaryIterator *iter, void *context) { 167 | } 168 | 169 | static void failed_callback(DictionaryIterator *iter, AppMessageResult reason, Simply *simply) { 170 | SimplyUi *ui = simply->ui; 171 | if (reason == APP_MSG_NOT_CONNECTED) { 172 | simply_ui_set_text(ui, &ui->subtitle_text, "Disconnected"); 173 | simply_ui_set_text(ui, &ui->body_text, "Run the Pebble Phone App"); 174 | 175 | check_splash(simply); 176 | } 177 | } 178 | 179 | void simply_msg_init(Simply *simply) { 180 | const uint32_t size_inbound = 2048; 181 | const uint32_t size_outbound = 512; 182 | app_message_open(size_inbound, size_outbound); 183 | 184 | app_message_set_context(simply); 185 | 186 | app_message_register_inbox_received(received_callback); 187 | app_message_register_inbox_dropped(dropped_callback); 188 | app_message_register_outbox_sent(sent_callback); 189 | app_message_register_outbox_failed((AppMessageOutboxFailed) failed_callback); 190 | } 191 | 192 | void simply_msg_deinit() { 193 | app_message_deregister_callbacks(); 194 | } 195 | 196 | bool simply_msg_single_click(ButtonId button) { 197 | DictionaryIterator *iter = NULL; 198 | if (app_message_outbox_begin(&iter) != APP_MSG_OK) { 199 | return false; 200 | } 201 | dict_write_uint8(iter, 0, SimplyACmd_singleClick); 202 | dict_write_uint8(iter, 1, button); 203 | return (app_message_outbox_send() == APP_MSG_OK); 204 | } 205 | 206 | bool simply_msg_long_click(ButtonId button) { 207 | DictionaryIterator *iter = NULL; 208 | if (app_message_outbox_begin(&iter) != APP_MSG_OK) { 209 | return false; 210 | } 211 | dict_write_uint8(iter, 0, SimplyACmd_longClick); 212 | dict_write_uint8(iter, 1, button); 213 | return (app_message_outbox_send() == APP_MSG_OK); 214 | } 215 | 216 | bool simply_msg_accel_tap(AccelAxisType axis, int32_t direction) { 217 | DictionaryIterator *iter = NULL; 218 | if (app_message_outbox_begin(&iter) != APP_MSG_OK) { 219 | return false; 220 | } 221 | dict_write_uint8(iter, 0, SimplyACmd_accelTap); 222 | dict_write_uint8(iter, 1, axis); 223 | dict_write_int8(iter, 2, direction); 224 | return (app_message_outbox_send() == APP_MSG_OK); 225 | } 226 | 227 | bool simply_msg_accel_data(AccelData *data, uint32_t num_samples, int32_t transaction_id) { 228 | DictionaryIterator *iter = NULL; 229 | if (app_message_outbox_begin(&iter) != APP_MSG_OK) { 230 | return false; 231 | } 232 | dict_write_uint8(iter, 0, SimplyACmd_accelData); 233 | if (transaction_id >= 0) { 234 | dict_write_int32(iter, 1, transaction_id); 235 | } 236 | dict_write_uint8(iter, 2, num_samples); 237 | dict_write_data(iter, 3, (uint8_t*) data, sizeof(*data) * num_samples); 238 | return (app_message_outbox_send() == APP_MSG_OK); 239 | } 240 | -------------------------------------------------------------------------------- /src/simply_msg.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "simplyjs.h" 4 | 5 | #include 6 | 7 | #define TRANSACTION_ID_INVALID (-1) 8 | 9 | void simply_msg_init(Simply *simply); 10 | 11 | void simply_msg_deinit(); 12 | 13 | bool simply_msg_single_click(ButtonId button); 14 | 15 | bool simply_msg_long_click(ButtonId button); 16 | 17 | bool simply_msg_accel_tap(AccelAxisType axis, int32_t direction); 18 | 19 | bool simply_msg_accel_data(AccelData *accel, uint32_t num_samples, int32_t transaction_id); 20 | 21 | -------------------------------------------------------------------------------- /src/simply_splash.c: -------------------------------------------------------------------------------- 1 | #include "simply_splash.h" 2 | 3 | #include "simplyjs.h" 4 | 5 | #include 6 | 7 | static void window_load(Window *window) { 8 | SimplySplash *self = window_get_user_data(window); 9 | 10 | self->logo = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_LOGO_SPLASH); 11 | 12 | Layer *window_layer = window_get_root_layer(window); 13 | GRect bounds = layer_get_bounds(window_layer); 14 | 15 | self->logo_layer = bitmap_layer_create(bounds); 16 | bitmap_layer_set_bitmap(self->logo_layer, self->logo); 17 | bitmap_layer_set_alignment(self->logo_layer, GAlignCenter); 18 | layer_add_child(window_layer, bitmap_layer_get_layer(self->logo_layer)); 19 | } 20 | 21 | static void window_disappear(Window *window) { 22 | SimplySplash *self = window_get_user_data(window); 23 | bool animated = true; 24 | window_stack_remove(self->window, animated); 25 | simply_splash_destroy(self); 26 | } 27 | 28 | SimplySplash *simply_splash_create(Simply *simply) { 29 | SimplySplash *self = malloc(sizeof(*self)); 30 | *self = (SimplySplash) { .simply = simply }; 31 | 32 | self->window = window_create(); 33 | window_set_user_data(self->window, self); 34 | window_set_fullscreen(self->window, true); 35 | window_set_background_color(self->window, GColorBlack); 36 | window_set_window_handlers(self->window, (WindowHandlers) { 37 | .load = window_load, 38 | .disappear = window_disappear, 39 | }); 40 | 41 | return self; 42 | } 43 | 44 | void simply_splash_destroy(SimplySplash *self) { 45 | bitmap_layer_destroy(self->logo_layer); 46 | gbitmap_destroy(self->logo); 47 | window_destroy(self->window); 48 | 49 | self->simply->splash = NULL; 50 | 51 | free(self); 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/simply_splash.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "simplyjs.h" 4 | 5 | #include 6 | 7 | typedef struct SimplySplash SimplySplash; 8 | 9 | struct SimplySplash { 10 | Simply *simply; 11 | Window *window; 12 | BitmapLayer *logo_layer; 13 | GBitmap *logo; 14 | }; 15 | 16 | SimplySplash *simply_splash_create(Simply *simply); 17 | 18 | void simply_splash_destroy(SimplySplash *self); 19 | -------------------------------------------------------------------------------- /src/simply_ui.c: -------------------------------------------------------------------------------- 1 | #include "simply_ui.h" 2 | 3 | #include "simply_msg.h" 4 | 5 | #include "simplyjs.h" 6 | 7 | #include 8 | 9 | struct SimplyStyle { 10 | const char* title_font; 11 | const char* subtitle_font; 12 | const char* body_font; 13 | int custom_body_font_id; 14 | GFont custom_body_font; 15 | }; 16 | 17 | static SimplyStyle STYLES[] = { 18 | { 19 | .title_font = FONT_KEY_GOTHIC_24_BOLD, 20 | .subtitle_font = FONT_KEY_GOTHIC_18_BOLD, 21 | .body_font = FONT_KEY_GOTHIC_18, 22 | }, 23 | { 24 | .title_font = FONT_KEY_GOTHIC_28_BOLD, 25 | .subtitle_font = FONT_KEY_GOTHIC_28, 26 | .body_font = FONT_KEY_GOTHIC_24_BOLD, 27 | }, 28 | { 29 | .title_font = FONT_KEY_GOTHIC_24_BOLD, 30 | .subtitle_font = FONT_KEY_GOTHIC_18_BOLD, 31 | .custom_body_font_id = RESOURCE_ID_MONO_FONT_14, 32 | }, 33 | }; 34 | 35 | SimplyUi *s_ui = NULL; 36 | 37 | static void click_config_provider(SimplyUi *self); 38 | 39 | void simply_ui_set_style(SimplyUi *self, int style_index) { 40 | self->style = &STYLES[style_index]; 41 | layer_mark_dirty(self->display_layer); 42 | } 43 | 44 | void simply_ui_set_scrollable(SimplyUi *self, bool is_scrollable) { 45 | self->is_scrollable = is_scrollable; 46 | scroll_layer_set_click_config_onto_window(self->scroll_layer, self->window); 47 | 48 | if (!is_scrollable) { 49 | GRect bounds = layer_get_bounds(window_get_root_layer(self->window)); 50 | layer_set_bounds(self->display_layer, bounds); 51 | const bool animated = true; 52 | scroll_layer_set_content_offset(self->scroll_layer, GPointZero, animated); 53 | scroll_layer_set_content_size(self->scroll_layer, bounds.size); 54 | } 55 | 56 | layer_mark_dirty(self->display_layer); 57 | } 58 | 59 | void simply_ui_set_fullscreen(SimplyUi *self, bool is_fullscreen) { 60 | window_set_fullscreen(self->window, is_fullscreen); 61 | 62 | GRect frame = layer_get_frame(window_get_root_layer(self->window)); 63 | scroll_layer_set_frame(self->scroll_layer, frame); 64 | layer_set_frame(self->display_layer, frame); 65 | 66 | if (!window_stack_contains_window(self->window)) { 67 | return; 68 | } 69 | 70 | // HACK: Refresh app chrome state 71 | Window *window = window_create(); 72 | window_stack_push(window, false); 73 | window_stack_remove(window, false); 74 | window_destroy(window); 75 | } 76 | 77 | void simply_ui_set_button(SimplyUi *self, ButtonId button, bool enable) { 78 | if (enable) { 79 | self->button_mask |= 1 << button; 80 | } else { 81 | self->button_mask &= ~(1 << button); 82 | } 83 | } 84 | 85 | static void set_text(char **str_field, const char *str) { 86 | free(*str_field); 87 | 88 | if (!str || !str[0]) { 89 | *str_field = NULL; 90 | return; 91 | } 92 | 93 | size_t size = strlen(str) + 1; 94 | char *buffer = malloc(size); 95 | strncpy(buffer, str, size); 96 | buffer[size - 1] = '\0'; 97 | 98 | *str_field = buffer; 99 | } 100 | 101 | void simply_ui_set_text(SimplyUi *self, char **str_field, const char *str) { 102 | set_text(str_field, str); 103 | layer_mark_dirty(self->display_layer); 104 | } 105 | 106 | static bool is_string(const char *str) { 107 | return str && str[0]; 108 | } 109 | 110 | void display_layer_update_callback(Layer *layer, GContext *ctx) { 111 | SimplyUi *self = s_ui; 112 | 113 | GRect window_bounds = layer_get_bounds(window_get_root_layer(self->window)); 114 | GRect bounds = layer_get_bounds(layer); 115 | 116 | const SimplyStyle *style = self->style; 117 | GFont title_font = fonts_get_system_font(style->title_font); 118 | GFont subtitle_font = fonts_get_system_font(style->subtitle_font); 119 | GFont body_font = style->custom_body_font ? style->custom_body_font : fonts_get_system_font(style->body_font); 120 | 121 | const int16_t x_margin = 5; 122 | const int16_t y_margin = 2; 123 | 124 | GRect text_bounds = bounds; 125 | text_bounds.size.w -= 2 * x_margin; 126 | text_bounds.size.h += 1000; 127 | GPoint cursor = { x_margin, y_margin }; 128 | 129 | graphics_context_set_text_color(ctx, GColorBlack); 130 | 131 | bool has_title = is_string(self->title_text); 132 | bool has_subtitle = is_string(self->subtitle_text); 133 | bool has_body = is_string(self->body_text); 134 | 135 | GSize title_size, subtitle_size; 136 | GPoint title_pos, subtitle_pos; 137 | GRect body_rect; 138 | 139 | if (has_title) { 140 | title_size = graphics_text_layout_get_content_size(self->title_text, title_font, text_bounds, 141 | GTextOverflowModeWordWrap, GTextAlignmentLeft); 142 | title_size.w = text_bounds.size.w; 143 | title_pos = cursor; 144 | cursor.y += title_size.h; 145 | } 146 | 147 | if (has_subtitle) { 148 | subtitle_size = graphics_text_layout_get_content_size(self->subtitle_text, title_font, text_bounds, 149 | GTextOverflowModeWordWrap, GTextAlignmentLeft); 150 | subtitle_size.w = text_bounds.size.w; 151 | subtitle_pos = cursor; 152 | cursor.y += subtitle_size.h; 153 | } 154 | 155 | if (has_body) { 156 | body_rect = bounds; 157 | body_rect.origin = cursor; 158 | body_rect.size.w -= 2 * x_margin; 159 | body_rect.size.h -= 2 * y_margin + cursor.y; 160 | GSize body_size = graphics_text_layout_get_content_size(self->body_text, body_font, text_bounds, 161 | GTextOverflowModeWordWrap, GTextAlignmentLeft); 162 | if (self->is_scrollable) { 163 | body_rect.size = body_size; 164 | int16_t new_height = cursor.y + 2 * y_margin + body_size.h; 165 | bounds.size.h = window_bounds.size.h > new_height ? window_bounds.size.h : new_height; 166 | GRect original_frame = layer_get_frame(layer); 167 | if (!grect_equal(&bounds, &original_frame)) { 168 | layer_set_frame(layer, bounds); 169 | scroll_layer_set_content_size(self->scroll_layer, bounds.size); 170 | } 171 | } else if (!style->custom_body_font && body_size.h > body_rect.size.h) { 172 | body_font = fonts_get_system_font(FONT_KEY_GOTHIC_18); 173 | } 174 | } 175 | 176 | graphics_context_set_fill_color(ctx, GColorWhite); 177 | graphics_fill_rect(ctx, bounds, 4, GCornersAll); 178 | 179 | if (has_title) { 180 | graphics_draw_text(ctx, self->title_text, title_font, 181 | (GRect) { .origin = title_pos, .size = title_size }, 182 | GTextOverflowModeWordWrap, GTextAlignmentLeft, NULL); 183 | } 184 | 185 | if (has_subtitle) { 186 | graphics_draw_text(ctx, self->subtitle_text, subtitle_font, 187 | (GRect) { .origin = subtitle_pos, .size = subtitle_size }, 188 | GTextOverflowModeWordWrap, GTextAlignmentLeft, NULL); 189 | } 190 | 191 | if (has_body) { 192 | graphics_draw_text(ctx, self->body_text, body_font, body_rect, 193 | GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL); 194 | } 195 | } 196 | 197 | static void single_click_handler(ClickRecognizerRef recognizer, void *context) { 198 | SimplyUi *self = s_ui; 199 | ButtonId button = click_recognizer_get_button_id(recognizer); 200 | bool is_enabled = (self->button_mask & (1 << button)); 201 | if (button == BUTTON_ID_BACK && !is_enabled) { 202 | bool animated = true; 203 | window_stack_pop(animated); 204 | } 205 | if (is_enabled) { 206 | simply_msg_single_click(button); 207 | } 208 | } 209 | 210 | static void long_click_handler(ClickRecognizerRef recognizer, void *context) { 211 | SimplyUi *self = s_ui; 212 | ButtonId button = click_recognizer_get_button_id(recognizer); 213 | bool is_enabled = (self->button_mask & (1 << button)); 214 | if (is_enabled) { 215 | simply_msg_long_click(button); 216 | } 217 | } 218 | 219 | static void click_config_provider(SimplyUi *self) { 220 | for (int i = 0; i < NUM_BUTTONS; ++i) { 221 | if (!self->is_scrollable || (i != BUTTON_ID_UP && i != BUTTON_ID_DOWN)) { 222 | window_single_click_subscribe(i, (ClickHandler) single_click_handler); 223 | window_long_click_subscribe(i, 500, (ClickHandler) long_click_handler, NULL); 224 | } 225 | } 226 | } 227 | 228 | static void show_welcome_text(SimplyUi *self) { 229 | if (self->title_text || self->subtitle_text || self->body_text) { 230 | return; 231 | } 232 | 233 | simply_ui_set_text(self, &self->title_text, "Simply.js"); 234 | simply_ui_set_text(self, &self->subtitle_text, "Write apps with JS!"); 235 | simply_ui_set_text(self, &self->body_text, "Visit simplyjs.io for details."); 236 | 237 | simply_ui_show(self); 238 | } 239 | 240 | static void window_load(Window *window) { 241 | SimplyUi *self = window_get_user_data(window); 242 | 243 | Layer *window_layer = window_get_root_layer(window); 244 | GRect bounds = layer_get_bounds(window_layer); 245 | bounds.origin = GPointZero; 246 | 247 | ScrollLayer *scroll_layer = self->scroll_layer = scroll_layer_create(bounds); 248 | Layer *scroll_base_layer = scroll_layer_get_layer(scroll_layer); 249 | layer_add_child(window_layer, scroll_base_layer); 250 | 251 | Layer *display_layer = self->display_layer = layer_create(bounds); 252 | layer_set_update_proc(display_layer, display_layer_update_callback); 253 | scroll_layer_add_child(scroll_layer, display_layer); 254 | 255 | scroll_layer_set_context(scroll_layer, self); 256 | scroll_layer_set_callbacks(scroll_layer, (ScrollLayerCallbacks) { 257 | .click_config_provider = (ClickConfigProvider) click_config_provider, 258 | }); 259 | scroll_layer_set_click_config_onto_window(scroll_layer, window); 260 | 261 | simply_ui_set_style(self, 1); 262 | 263 | app_timer_register(10000, (AppTimerCallback) show_welcome_text, self); 264 | } 265 | 266 | static void window_unload(Window *window) { 267 | SimplyUi *self = window_get_user_data(window); 268 | 269 | layer_destroy(self->display_layer); 270 | scroll_layer_destroy(self->scroll_layer); 271 | window_destroy(window); 272 | } 273 | 274 | void simply_ui_show(SimplyUi *self) { 275 | if (!window_stack_contains_window(self->window)) { 276 | bool animated = true; 277 | window_stack_push(self->window, animated); 278 | } 279 | } 280 | 281 | SimplyUi *simply_ui_create(void) { 282 | if (s_ui) { 283 | return s_ui; 284 | } 285 | 286 | for (unsigned int i = 0; i < ARRAY_LENGTH(STYLES); ++i) { 287 | SimplyStyle *style = &STYLES[i]; 288 | if (style->custom_body_font_id) { 289 | style->custom_body_font = fonts_load_custom_font(resource_get_handle(style->custom_body_font_id)); 290 | } 291 | } 292 | 293 | SimplyUi *self = malloc(sizeof(*self)); 294 | *self = (SimplyUi) { .window = NULL }; 295 | s_ui = self; 296 | 297 | for (int i = 0; i < NUM_BUTTONS; ++i) { 298 | if (i != BUTTON_ID_BACK) { 299 | self->button_mask |= 1 << i; 300 | } 301 | } 302 | 303 | Window *window = self->window = window_create(); 304 | window_set_user_data(window, self); 305 | window_set_background_color(window, GColorBlack); 306 | window_set_click_config_provider(window, (ClickConfigProvider) click_config_provider); 307 | window_set_window_handlers(window, (WindowHandlers) { 308 | .unload = window_unload, 309 | }); 310 | 311 | window_load(self->window); 312 | 313 | return self; 314 | } 315 | 316 | void simply_ui_destroy(SimplyUi *self) { 317 | if (!s_ui) { 318 | return; 319 | } 320 | 321 | simply_ui_set_text(self, &self->title_text, NULL); 322 | simply_ui_set_text(self, &self->subtitle_text, NULL); 323 | simply_ui_set_text(self, &self->body_text, NULL); 324 | 325 | free(self); 326 | 327 | s_ui = NULL; 328 | } 329 | -------------------------------------------------------------------------------- /src/simply_ui.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef struct SimplyStyle SimplyStyle; 6 | 7 | typedef struct SimplyUi SimplyUi; 8 | 9 | struct SimplyUi { 10 | Window *window; 11 | const SimplyStyle *style; 12 | char *title_text; 13 | char *subtitle_text; 14 | char *body_text; 15 | ScrollLayer *scroll_layer; 16 | Layer *display_layer; 17 | bool is_scrollable; 18 | uint32_t button_mask; 19 | }; 20 | 21 | SimplyUi *simply_ui_create(void); 22 | 23 | void simply_ui_destroy(SimplyUi *self); 24 | 25 | void simply_ui_show(SimplyUi *self); 26 | 27 | void simply_ui_set_style(SimplyUi *self, int style_index); 28 | 29 | void simply_ui_set_text(SimplyUi *self, char **str_field, const char *str); 30 | 31 | void simply_ui_set_scrollable(SimplyUi *self, bool is_scrollable); 32 | 33 | void simply_ui_set_fullscreen(SimplyUi *self, bool is_fullscreen); 34 | 35 | void simply_ui_set_button(SimplyUi *self, ButtonId button, bool enable); 36 | 37 | -------------------------------------------------------------------------------- /src/simplyjs.c: -------------------------------------------------------------------------------- 1 | #include "simplyjs.h" 2 | 3 | #include "simply_accel.h" 4 | #include "simply_splash.h" 5 | #include "simply_ui.h" 6 | #include "simply_msg.h" 7 | 8 | #include 9 | 10 | static Simply *init(void) { 11 | Simply *simply = malloc(sizeof(*simply)); 12 | simply->accel = simply_accel_create(); 13 | simply->splash = simply_splash_create(simply); 14 | simply->ui = simply_ui_create(); 15 | 16 | bool animated = true; 17 | window_stack_push(simply->splash->window, animated); 18 | 19 | simply_msg_init(simply); 20 | return simply; 21 | } 22 | 23 | static void deinit(Simply *simply) { 24 | simply_msg_deinit(); 25 | simply_ui_destroy(simply->ui); 26 | simply_accel_destroy(simply->accel); 27 | } 28 | 29 | int main(void) { 30 | Simply *simply = init(); 31 | app_event_loop(); 32 | deinit(simply); 33 | } 34 | -------------------------------------------------------------------------------- /src/simplyjs.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define LOG(...) APP_LOG(APP_LOG_LEVEL_DEBUG, __VA_ARGS__) 4 | 5 | typedef struct Simply Simply; 6 | 7 | struct Simply { 8 | struct SimplyAccel *accel; 9 | struct SimplySplash *splash; 10 | struct SimplyUi *ui; 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /wscript: -------------------------------------------------------------------------------- 1 | from waflib.Configure import conf 2 | 3 | top = '.' 4 | out = 'build' 5 | 6 | def options(ctx): 7 | ctx.load('pebble_sdk') 8 | 9 | def configure(ctx): 10 | ctx.load('pebble_sdk') 11 | 12 | def build(ctx): 13 | ctx.load('pebble_sdk') 14 | 15 | ctx.pbl_program(source=ctx.path.ant_glob('src/**/*.c'), 16 | cflags=['-Wno-type-limits', 17 | '-Wno-address'], 18 | target='pebble-app.elf') 19 | 20 | js_target = ctx.concat_javascript(js=ctx.path.ant_glob('src/js/**/*.js')) 21 | 22 | ctx.pbl_bundle(elf='pebble-app.elf', 23 | js=js_target) 24 | 25 | @conf 26 | def concat_javascript(self, *k, **kw): 27 | js_nodes = kw['js'] 28 | 29 | if not js_nodes: 30 | return [] 31 | 32 | def concat_javascript_task(task): 33 | cmd = ['cat'] 34 | cmd.extend(['"{}"'.format(x.abspath()) for x in task.inputs]) 35 | cmd.extend(['>', "{}".format(task.outputs[0].abspath())]) 36 | task.exec_command(' '.join(cmd)) 37 | 38 | js_target = self.path.make_node('build/src/js/pebble-js-app.js') 39 | 40 | self(rule=concat_javascript_task, 41 | source=js_nodes, 42 | target=js_target) 43 | 44 | return js_target 45 | 46 | # vim:filetype=python 47 | --------------------------------------------------------------------------------