diff --git a/README.md b/README.md index eb081ca..8feeb4c 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,27 @@ # metrix-fitbit -A Fitbit Ionic clock face showing metrics activities. +A Fitbit Ionic & Versa clock face showing metrics activities. -You can change each metric to one of the 9 available. You can even have the same activity multiple times and showing different states value. +You can change each metric to one of the 9 available. You can even have the same activity multiple times and showing different stats format. For example, for steps, you could show both: -* the total steps value -* percentage achievement goal of steps +* the total steps +* percentage achievement goal -![metrix.gif](metrix.gif) - -![metrix.png](metrix.png) +| Ionic | Versa | +|----------|:-------------:| +| ![ionic.png](screenshots/ionic.png) | ![versa.png](screenshots/versa.png) | +| ![ionic2.png](screenshots/ionic2.png) | ![versa2.png](screenshots/versa2.png) | +| ![ionic.gif](screenshots/ionic.gif) |![versa.gif](screenshots/versa.gif) | ## Changelog -15/11/18 +### 17/11/18 + +* Now support Versa +* Handle missing permissions (in case you deny some of them) + +### 15/11/18 * Add weather metric * Make the mode (stats/switch) button bigger * Fix save color for clock @@ -48,13 +55,13 @@ Because this clock face shows personal goals, heart rate and weather data, it us * Heart rate sensor * **GPS location** for the weather -No data is keep nor send to first or third parties entities, companies or individuals. You can check by looking at the source code. +No data is kept nor send to first or third parties entities, companies or individuals. You can check by looking at the source code. When installing the clock face, it'll ask you for these permissions. You can deny all, some or none. The non-functional activities metrics won't show on the clock face in that case. ## Ionic vs. Versa -Soon. +The clock face support both Ionic & Versa with a layout adaptation. ## Settings @@ -68,9 +75,9 @@ You can change the refresh time rate in the settings. By default, the cache last Weather data is only updated when the clock face is active (i.e. screen turned on) and is not updated in background every X time. This is because there's a usage limite of the API. -As I pay for any extra usage of the DarkSky API, feel free to support the clock face if you use this daily. +As I pay for for the DarkSky API usage, I can't afford all API requests. Feel free to support the clock face if you use this daily. -_Because there's a API usage limit and I pay for extra queries, the weather data may not update right away. This will depend on the amount of users this clock face has._ +_Because there's a API usage limit, the weather data may not update right away. This will depend on the amount of users using this clock face._ ## Contributing @@ -80,14 +87,19 @@ Feel free to contribute to this project by: * Openning an issue if * you want to propose a new feature * or if you encounter a problem +* Pay me a tea ## Resources -Thanks for the following: +Acitivities icons are provided by: * [Fitbit's icons](https://github.com/Fitbit/sdk-design-assets) -The following assets are from [www.flaticon.com](https://www.flaticon.com/") and is licensed by [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0). +Weather API wrapper is provided by: + +* [fitbit-weather](https://github.com/gregoiresage/fitbit-weather) + +Weather icons are from [www.flaticon.com](https://www.flaticon.com/"), is licensed by [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0), and provided by: * [Good Ware](https://www.flaticon.com/authors/good-ware) * [Freepik](https://www.freepik.com) diff --git a/app/activities.js b/app/activities.js index f5c8f4d..796f2bf 100644 --- a/app/activities.js +++ b/app/activities.js @@ -1,5 +1,3 @@ -import * as settings from './settings'; - import { activeMinutes } from './activities/activeMinutes'; import { calories } from './activities/calories'; import { clock } from './activities/clock'; @@ -10,6 +8,9 @@ import { hr } from './activities/hr'; import { steps } from './activities/steps'; import { weather } from './activities/weather'; +/** + * All activities displayable on screen. + */ export const activities = [ activeMinutes, calories, diff --git a/app/activities/activeMinutes.js b/app/activities/activeMinutes.js index 1e9120a..592bc48 100644 --- a/app/activities/activeMinutes.js +++ b/app/activities/activeMinutes.js @@ -1,13 +1,10 @@ -import document from 'document'; - -import * as util from '../../common/utils'; - +import document from 'document'; import { getActivityValue } from '../userActivity'; +import * as util from '../../common/utils'; import { onClickActivity, saveActivitySettings, switchToNextActivity, - } from '../activityActions'; export const activeMinutes = { diff --git a/app/activities/calories.js b/app/activities/calories.js index 3c15558..5dbe125 100644 --- a/app/activities/calories.js +++ b/app/activities/calories.js @@ -1,13 +1,10 @@ -import document from 'document'; - -import * as util from '../../common/utils'; - +import document from 'document'; import { getActivityValue } from '../userActivity'; +import * as util from '../../common/utils'; import { onClickActivity, saveActivitySettings, switchToNextActivity, - } from '../activityActions'; export const calories = { diff --git a/app/activities/date.js b/app/activities/date.js index 7b40acb..64c889d 100644 --- a/app/activities/date.js +++ b/app/activities/date.js @@ -1,6 +1,5 @@ -import document from 'document'; - -import * as util from '../../common/utils'; +import document from 'document'; +import * as util from '../../common/utils'; import { onClickActivity, saveActivitySettings, diff --git a/app/activities/distance.js b/app/activities/distance.js index dfcc6ce..5ff3ca8 100644 --- a/app/activities/distance.js +++ b/app/activities/distance.js @@ -1,7 +1,5 @@ -import document from 'document'; - -import * as util from '../../common/utils'; - +import document from 'document'; +import * as util from '../../common/utils'; import { getActivityValue } from '../userActivity'; import { onClickActivity, diff --git a/app/activities/elevationGain.js b/app/activities/elevationGain.js index 9544797..1c0f029 100644 --- a/app/activities/elevationGain.js +++ b/app/activities/elevationGain.js @@ -1,7 +1,5 @@ -import document from 'document'; - -import * as util from '../../common/utils'; - +import document from 'document'; +import * as util from '../../common/utils'; import { getActivityValue } from '../userActivity'; import { onClickActivity, diff --git a/app/activities/steps.js b/app/activities/steps.js index 95ffbe7..7f867d4 100644 --- a/app/activities/steps.js +++ b/app/activities/steps.js @@ -1,7 +1,5 @@ -import document from 'document'; - -import * as util from '../../common/utils'; - +import document from 'document'; +import * as util from '../../common/utils'; import { getActivityValue } from '../userActivity'; import { onClickActivity, diff --git a/app/activities/weather.js b/app/activities/weather.js index b7e8369..a33219b 100644 --- a/app/activities/weather.js +++ b/app/activities/weather.js @@ -1,11 +1,9 @@ -import document from 'document'; +import document from 'document'; -import * as util from '../../common/utils'; -import * as colors from '../../common/colors'; - -import * as settings from '../settings'; - -import * as api from '../../lib/fitbit-weather/app'; +import * as api from '../../lib/fitbit-weather/app'; +import * as colors from '../../common/colors'; +import * as settings from '../settings'; +import * as util from '../../common/utils'; import { getWeatherIcon } from '../../common/icons'; @@ -59,10 +57,9 @@ export const weather = { const refreshTime = weatherRefreshTime ? parseInt(weatherRefreshTime.values[0].name) : 60; - // return the cached value if it is less than 30 minutes old by default + // return the cached value if it is less than 60 minutes old by default api.fetch(weatherRefreshTime * 60 * 1000) .then(data => { -// console.log(JSON.stringify(data)); const format = this.format; metricElem.text = `${getText({ data, format, imperialUnit })}`; diff --git a/app/data.js b/app/data.js index da0bbbc..d9c3596 100644 --- a/app/data.js +++ b/app/data.js @@ -1,12 +1,10 @@ -import * as messaging from "messaging"; +import document from 'document'; -import document from 'document'; - -import { activities } from './activities'; -import { metrics } from './metrics'; -import * as settings from './settings'; - -let _weatherFetched = false; +import { activities } from './activities'; +import * as layout from './layout'; +import { metrics } from './metrics'; +import * as settings from './settings'; +import * as permissions from './permissions'; export function initialize() { metrics.map(metric => { @@ -14,7 +12,7 @@ export function initialize() { metric.initActivity = initActivity; metric.activityCount = activities.length; }); - + bindSwitchTapMode(); } @@ -34,15 +32,16 @@ export function reinitialize({ activityName }) { function bindSwitchTapMode() { const icon = document.getElementById('action-switcher-img'); - + const tapMode = settings.getData('tapMode') || 'stats'; - + + icon.y = layout.getModeIconY(); icon.href = tapMode === 'cycles' ? 'icons/cycles.png' : 'icons/stats.png'; - + icon.onclick = (e) => { tapMode = tapMode === 'stats' ? 'cycles' : 'stats'; icon.href = tapMode === 'cycles' ? 'icons/cycles.png' : 'icons/stats.png'; - + settings.update({ key: 'tapMode', value: tapMode }); } } @@ -55,42 +54,48 @@ function bindSwitchTapMode() { function initActivity({ metric, asked }) { if (!asked) { const savedData = settings.getData(`metric${metric.metricNumber}`); - + if (savedData) { - metric.activity = savedData.activity ? + metric.activity = savedData.activity ? savedData.activity : metric.activity; - metric.format = savedData.format ? + metric.format = savedData.format ? savedData.format : metric.format; - metric.color = savedData.color ? + metric.color = savedData.color ? savedData.color : metric.color; } } - + + // Permission check + metric.activity = permissions.getNextAllowedActivity(metric.activity); + const activity = activities[metric.activity]; - + const textElem = document.getElementById(`metric${metric.metricNumber}`) const icon = document.getElementById(`metric${metric.metricNumber}-img`); - + if (activity.icon) { icon.style.visibility = 'visible'; icon.style.opacity = 1; - textElem.x = 280; - + textElem.x = layout.getTextX({ icon: true }); + icon.href = activity.icon; icon.style.fill = activity.iconFill ? activity.iconFill : 'white'; - + + icon.x = layout.getIconX(); + icon.y = layout.getIconY({ metricNumber: metric.metricNumber }); + } else { icon.style.visibility = 'hidden'; - textElem.x = 320; + textElem.x = layout.getTextX({ icon: false }); } - + if (metric.color) { icon.style.fill = metric.color; textElem.style.fill = metric.color; } - + textElem.text = '--'; textElem.style.fill = metric.color ? metric.color : activity.textFill ? activity.textFill : 'white'; textElem.style.fontSize = metric.fontSize ? metric.fontSize : 50; @@ -104,19 +109,20 @@ function initActivity({ metric, asked }) { metric.switchToNext = activity.switchToNext; metric.saveSettings = activity.saveSettings; metric.update = activity.update; - + textElem.onclick = () => metric.onClick(metric) } +// Listen to settings changes settings.initialize(data => { if (!data) return; - + if (data.backgroundColor) { const bg = document.getElementById('background'); bg.style.fill = data.backgroundColor; } }); -settings.bindReinitialize(({ activityName }) => { +settings.bindReinitialize(({ activityName }) => { reinitialize({ activityName }) }); diff --git a/app/index.js b/app/index.js index ffddf4d..657ab0e 100644 --- a/app/index.js +++ b/app/index.js @@ -1,9 +1,7 @@ -import clock from "clock"; -import { display } from "display"; - -import * as data from './data'; - -import { HeartRateSensor } from "heart-rate"; +import clock from "clock"; +import * as data from './data'; +import { display } from "display"; +import { HeartRateSensor } from "heart-rate"; const hrm = new HeartRateSensor(); diff --git a/app/layout.js b/app/layout.js new file mode 100644 index 0000000..ef0dcbd --- /dev/null +++ b/app/layout.js @@ -0,0 +1,76 @@ +import { me as device } from 'device'; + +if (!device.screen) { + device.screen = { width: 348, height: 250 }; +} + +const { height, width } = device.screen; + +const deviceType = width === 300 && height === 300 ? 'Versa' : 'Ionic'; + +/** + * Return horizontal icon position. + */ +export function getIconX() { + const hPos = { + Ionic: 290, + Versa: 250, + }; + + return hPos[deviceType]; +} + +/** + * Return vertical icon position. + */ +export function getIconY({ metricNumber }) { + const vPos = { + Ionic: { + 0: 20, + 1: 77, + 2: 120, + 3: 165, + 4: 210, + }, + Versa: { + 0: 30, + 1: 90, + 2: 150, + 3: 200, + 4: 255, + }, + }; + + return vPos[deviceType][metricNumber]; +} + +/** + * Return vertical icon mode position. + * (mode = stats format/switch next activity) + */ +export function getModeIconY() { + const vPos = { + Ionic: 200, + Versa: 240, + }; + + return vPos[deviceType]; +} + +/** + * Return vertical text position. + */ +export function getTextX({ icon } = {}) { + const hPos = { + true: { + Ionic: 280, + Versa: 240, + }, + false: { + Ionic: 320, + Versa: 290, + } + }; + + return hPos[icon][deviceType]; +} diff --git a/app/metrics.js b/app/metrics.js index 4b392b8..dc02e03 100644 --- a/app/metrics.js +++ b/app/metrics.js @@ -1,33 +1,36 @@ +/** + * Visual representation on screen. + */ export const metrics = [ { - activity: 2, // clock - color: undefined, - fontSize: 70, - format: 'current', + activity : 2, // clock + color : undefined, + fontSize : 70, + format : 'current', metricNumber: 0, // vertical visual position on the clock face }, { - activity: 3, // date - color: undefined, - format: 'current', + activity : 3, // date + color : undefined, + format : 'current', metricNumber: 1, }, { - activity: 7, // steps - color: undefined, - format: 'current', + activity : 7, // steps + color : undefined, + format : 'current', metricNumber: 2, }, { - activity: 0, // active minutes - color: undefined, - format: 'current', + activity : 0, // active minutes + color : undefined, + format : 'current', metricNumber: 3, }, { - activity: 5, // elevation gain - color: undefined, - format: 'current', + activity : 5, // elevation gain + color : undefined, + format : 'current', metricNumber: 4, }, ] diff --git a/app/permissions.js b/app/permissions.js new file mode 100644 index 0000000..45a7e90 --- /dev/null +++ b/app/permissions.js @@ -0,0 +1,35 @@ +import { me } from "appbit"; + +export function getNextAllowedActivity(activityNumber) { + const { granted } = me.permissions; + + const grantedActivity = granted('access_activity'); + const grantedWeather = granted('access_location') && + granted('access_internet'); + + const grantedPermissions = { + 0: grantedActivity, + 1: grantedActivity, + 2: true, + 3: true, + 4: grantedActivity, + 5: grantedActivity, + 6: granted('access_heart_rate'), + 7: grantedActivity, + 8: grantedWeather, + }; + + const lengthPerm = Object.keys(grantedPermissions).length; + + if (grantedPermissions[activityNumber]) { + return activityNumber; + } + + let i = activityNumber; + + while (!grantedPermissions[i]) { + i = (i + 1) % lengthPerm; + } + + return i; +} diff --git a/app/settings.js b/app/settings.js index c3fd1bb..296ce24 100644 --- a/app/settings.js +++ b/app/settings.js @@ -4,10 +4,8 @@ Callback should be used to update your UI. */ import { me } from "appbit"; -import { me as device } from "device"; import * as fs from "fs"; import * as messaging from "messaging"; -//import { reinitialize } from './data'; const SETTINGS_TYPE = "cbor"; const SETTINGS_FILE = "settings.cbor"; @@ -16,7 +14,7 @@ const KEY_TEMPERATURE_UNIT = 'imperialUnit'; let settings = {}; let onsettingschange; -let reinit; +let reinitialize; export function initialize(callback) { settings = loadSettings(); @@ -25,7 +23,7 @@ export function initialize(callback) { } export function bindReinitialize(callback) { - reinit = callback; + reinitialize = callback; } export function getData(key = '') { @@ -41,11 +39,10 @@ export function update({ key, value }) { messaging.peerSocket.addEventListener("message", function(evt) { settings[evt.data.key] = evt.data.value; onsettingschange(settings); - + // Update Immediately weather value when unit changed if (evt.data.key === KEY_TEMPERATURE_UNIT) { -// reinitialize({ activityName: 'weather' }); - reinit({ activityName: 'weather' }); + reinitialize({ activityName: 'weather' }); } }) diff --git a/app/userActivity.js b/app/userActivity.js index b363cf6..74d5842 100644 --- a/app/userActivity.js +++ b/app/userActivity.js @@ -8,11 +8,17 @@ export function getActivityValue({ activity, metric }) { text = `${today.local[activity]}`; } else if (metric.format === 'percentage') { - text = today.local[activity] / goals[activity] * 100; + const activityGoal = typeof goals[activity] !== 'undefined' ? + goals[activity] : 0; + + text = today.local[activity] / activityGoal * 100; text = Math.floor(text) + '%'; } else { - text = today.local[activity] - goals[activity]; + const activityGoal = typeof goals[activity] !== 'undefined' ? + goals[activity] : 0; + + text = today.local[activity] - activityGoal; if (text > 0) text = '+' + text; } diff --git a/metrix.gif b/metrix.gif deleted file mode 100644 index 6712eaa..0000000 Binary files a/metrix.gif and /dev/null differ diff --git a/package.json b/package.json index 4dbb5a0..154d0fa 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "requestedPermissions": [ "access_activity", "access_heart_rate", - "run_background", "access_location", "access_internet" ], diff --git a/screenshots/ionic.gif b/screenshots/ionic.gif new file mode 100644 index 0000000..c0597b1 Binary files /dev/null and b/screenshots/ionic.gif differ diff --git a/metrix.png b/screenshots/ionic.png similarity index 100% rename from metrix.png rename to screenshots/ionic.png diff --git a/screenshots/ionic2.png b/screenshots/ionic2.png new file mode 100644 index 0000000..6f62465 Binary files /dev/null and b/screenshots/ionic2.png differ diff --git a/screenshots/versa.gif b/screenshots/versa.gif new file mode 100644 index 0000000..9f79c44 Binary files /dev/null and b/screenshots/versa.gif differ diff --git a/screenshots/versa.png b/screenshots/versa.png new file mode 100644 index 0000000..8184b28 Binary files /dev/null and b/screenshots/versa.png differ diff --git a/screenshots/versa2.png b/screenshots/versa2.png new file mode 100644 index 0000000..b31901d Binary files /dev/null and b/screenshots/versa2.png differ