├── .gitignore ├── LICENSE ├── README.md ├── other ├── Consolas.svg ├── LECO_1976-Regular.1.svg ├── avenir-next-regular.svg ├── avenirnext-demibold.svg └── david-libre-bold.svg ├── package.json ├── pebble_basalt_ani.gif ├── pebble_round_ani.gif ├── project_banner.gif ├── resources ├── data │ ├── BATTERY_BG.pdc │ ├── BATTERY_CHARGE.pdc │ ├── CLEAR_DAY.pdc │ ├── CLEAR_NIGHT.pdc │ ├── CLOUDY_DAY.pdc │ ├── DATE_BG.pdc │ ├── DISCONNECTED.pdc │ ├── HEALTH_HEART.pdc │ ├── HEALTH_SLEEP.pdc │ ├── HEALTH_STEPS.pdc │ ├── HEAVY_RAIN.pdc │ ├── HEAVY_SNOW.pdc │ ├── LIGHT_RAIN.pdc │ ├── LIGHT_SNOW.pdc │ ├── PARTLY_CLOUDY.pdc │ ├── PARTLY_CLOUDY_NIGHT.pdc │ ├── RAINING_AND_SNOWING.pdc │ ├── THUNDERSTORM.pdc │ └── WEATHER_GENERIC.pdc ├── fonts │ ├── AvenirNextDemiBold.ffont │ ├── AvenirNextRegular.ffont │ └── LECO1976-Regular.ffont └── images │ ├── menuicon~bw.png │ ├── menuicon~color.png │ └── menuicon~color~round.png ├── src ├── c │ ├── clock_area.c │ ├── clock_area.h │ ├── health.c │ ├── health.h │ ├── main.c │ ├── messaging.c │ ├── messaging.h │ ├── settings.c │ ├── settings.h │ ├── sidebar.c │ ├── sidebar.h │ ├── sidebar_widgets.c │ ├── sidebar_widgets.h │ ├── time_date.c │ ├── time_date.h │ ├── util.c │ ├── util.h │ ├── weather.c │ └── weather.h └── pkjs │ ├── index.js │ ├── languages.js │ ├── secrets_example.js │ ├── weather.js │ ├── weather_owm.js │ └── weather_wunderground.js ├── tools ├── fctx-compiler_regex.sh ├── frankenpebble.py └── localDates.py └── wscript /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | src/pkjs/secrets.js 3 | .vscode 4 | node_modules 5 | 6 | # Ignore waf build tool generated files 7 | .waf* 8 | build 9 | .lock-waf* 10 | 11 | # Ignore linked files/directories 12 | waf 13 | wscript 14 | include 15 | lib 16 | pebble_app.ld 17 | 18 | # Ignore other generated files 19 | serial_dump.txt 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TimeStyleBB 2 | A stylish, modern watchface for the Pebble and Pebble Time watches. 3 | 4 | This project is a fork of [freakified/TimeStylePebble](https://github.com/freakified/TimeStylePebble). 5 | In addition to the original version this version add various corrections and a bottom/top bar possibility that can display up to four widgets. 6 | 7 | 8 | 9 | Inspired by the visual language of the Timeline found on the Pebble Time, TimeStyle is designed as the “present” to complement the Timeline’s “past” and “future”. 10 | 11 | * Readable: With more than 80% of the display area devoted to the time and 6 font options, TimeStyle is designed for readability in all conditions. Unlike most other Pebble faces, time text is displayed using antialiasing, achieved using palette swapping. 12 | * Colorful: includes over 20 preset color schemes, and also supports custom colors using any color the Pebble Time can display—also supports saving, loading, and sharing custom presets! 13 | * Configurable: TimeStyle features a wide variety of different complications, including step counts, sleep times, weather forecasts, the week number, seconds, the time in another time zone, the battery level, and more. 14 | * Keeps you informed: TimeStyle automatically displays notifications when the battery is low or when your phone disconnects. 15 | * Works in 30 different languages, more than any other Pebble face: English, French, German, Spanish, Italian, Dutch, Turkish, Czech, Slovak, Portuguese, Greek, Swedish, Polish, Romanian, Vietnamese, Catalan, Norwegian, Russian, Estonian, Basque, Finnish, Danish, Lithuanian, Slovenian, Hungarian, Croatian, Serbian, Irish, Latviann, and Ukrainian. 16 | 17 | ## Want to try it? 18 | * Download on the Pebble store at the link below: https://apps.rebble.io/en_US/application/5a48b54f0dfc32823d0041d5?query=timestylr§ion=watchfaces 19 | * Or download the lastest PBW package here: https://github.com/plarus/TimeStyleBBPebble/releases 20 | 21 | ## Issues 22 | You have found an issue? You can report it here: https://github.com/plarus/TimeStyleBBPebble/issues/new 23 | 24 | ## Contributing 25 | Want to contribute to TimeStyle? Have a look at [the various feature requests that are still outstanding](https://github.com/freakified/TimeStylePebble/issues?q=is%3Aopen+is%3Aissue) -- just comment on one if you're interested in working on it! 26 | 27 | ## Gallery 28 | 29 | -------------------------------------------------------------------------------- /other/LECO_1976-Regular.1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Created by FontForge 20110222 at Mon Mar 7 13:18:00 2011 6 | By www-data 7 | Copyright (c) 2009 by Samuel Carnoky. All rights reserved. 8 | 9 | 10 | 11 | 12 | 27 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 76 | 78 | 80 | 82 | 84 | 86 | 88 | 90 | 92 | 94 | 96 | 98 | 100 | 102 | 104 | 106 | 108 | 110 | 112 | 114 | 116 | 118 | 120 | 122 | 124 | 126 | 128 | 130 | 132 | 134 | 136 | 138 | 140 | 142 | 144 | 146 | 148 | 150 | 152 | 154 | 156 | 158 | 160 | 162 | 164 | 166 | 168 | 170 | 172 | 174 | 176 | 178 | 180 | 182 | 184 | 186 | 188 | 190 | 192 | 194 | 196 | 198 | 200 | 202 | 204 | 206 | 208 | 210 | 212 | 214 | 216 | 218 | 220 | 222 | 224 | 226 | 228 | 230 | 232 | 234 | 236 | 238 | 240 | 242 | 244 | 246 | 248 | 250 | 252 | 254 | 256 | 258 | 260 | 262 | 264 | 266 | 268 | 270 | 272 | 274 | 276 | 278 | 280 | 282 | 284 | 286 | 288 | 290 | 292 | 294 | 296 | 298 | 300 | 302 | 304 | 306 | 308 | 310 | 312 | 314 | 316 | 318 | 320 | 322 | 324 | 326 | 328 | 330 | 332 | 334 | 336 | 338 | 340 | 342 | 344 | 346 | 348 | 350 | 352 | 354 | 356 | 358 | 360 | 362 | 364 | 366 | 368 | 370 | 372 | 374 | 376 | 378 | 380 | 382 | 384 | 386 | 388 | 390 | 392 | 394 | 396 | 398 | 400 | 402 | 404 | 406 | 408 | 410 | 412 | 414 | 416 | 418 | 420 | 422 | 424 | 426 | 428 | 430 | 432 | 434 | 436 | 438 | 440 | 442 | 444 | 446 | 448 | 450 | 452 | 454 | 456 | 458 | 460 | 462 | 464 | 466 | 468 | 470 | 471 | 472 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timestyle-bb-pebble", 3 | "author": "Peyot", 4 | "version": "1.1.0", 5 | "keywords": [ 6 | "pebble-watchface" 7 | ], 8 | "private": true, 9 | "dependencies": { 10 | "pebble-fctx": "^1.6.2" 11 | }, 12 | "pebble": { 13 | "displayName": "TimeStyle BB", 14 | "uuid": "bc612aa2-61f0-4abf-ad2d-0846efc4afa3", 15 | "sdkVersion": "3", 16 | "enableMultiJS": true, 17 | "targetPlatforms": [ 18 | "basalt", 19 | "chalk", 20 | "diorite", 21 | "emery" 22 | ], 23 | "capabilities": [ 24 | "location", 25 | "health", 26 | "configurable" 27 | ], 28 | "watchapp": { 29 | "onlyShownOnCommunication": false, 30 | "hiddenApp": false, 31 | "watchface": true 32 | }, 33 | "messageKeys": [ 34 | "WeatherCondition", 35 | "WeatherTemperature", 36 | "WeatherForecastCondition", 37 | "WeatherForecastHighTemp", 38 | "WeatherForecastLowTemp", 39 | "SettingAltClockName", 40 | "SettingAltClockOffset", 41 | "SettingDisableAutobattery", 42 | "SettingBluetoothVibe", 43 | "SettingDisconnectIcon", 44 | "SettingClockFontId", 45 | "SettingColorBG", 46 | "SettingColorSidebar", 47 | "SettingColorTime", 48 | "SettingDecimalSep", 49 | "SettingDisableWeather", 50 | "SettingHealthActivityDisplay", 51 | "SettingHealthUseRestfulSleep", 52 | "SettingHourlyVibe", 53 | "SettingLanguageID", 54 | "SettingLanguageDayNames[7]", 55 | "SettingLanguageMonthNames[12]", 56 | "SettingLanguageWordForWeek", 57 | "SettingShowBatteryPct", 58 | "SettingShowLeadingZero", 59 | "SettingCenterTime", 60 | "SettingSidebarPosition", 61 | "SettingSidebarTextColor", 62 | "SettingUseLargeFonts", 63 | "SettingUseMetric", 64 | "SettingWidget0ID", 65 | "SettingWidget1ID", 66 | "SettingWidget2ID", 67 | "SettingWidget3ID" 68 | ], 69 | 70 | "resources": { 71 | "media": [ 72 | { 73 | "file": "images/menuicon.png", 74 | "menuIcon": true, 75 | "name": "MENU_ICON", 76 | "type": "bitmap" 77 | }, 78 | { 79 | "file": "fonts/LECO1976-Regular.ffont", 80 | "name": "LECO_REGULAR_FFONT", 81 | "type": "raw" 82 | }, 83 | { 84 | "file": "fonts/AvenirNextRegular.ffont", 85 | "name": "AVENIR_REGULAR_FFONT", 86 | "type": "raw" 87 | }, 88 | { 89 | "file": "fonts/AvenirNextDemiBold.ffont", 90 | "name": "AVENIR_BOLD_FFONT", 91 | "type": "raw" 92 | }, 93 | { 94 | "file": "data/WEATHER_GENERIC.pdc", 95 | "name": "WEATHER_GENERIC", 96 | "type": "raw" 97 | }, 98 | { 99 | "file": "data/THUNDERSTORM.pdc", 100 | "name": "WEATHER_THUNDERSTORM", 101 | "type": "raw" 102 | }, 103 | { 104 | "file": "data/RAINING_AND_SNOWING.pdc", 105 | "name": "WEATHER_RAINING_AND_SNOWING", 106 | "type": "raw" 107 | }, 108 | { 109 | "file": "data/PARTLY_CLOUDY_NIGHT.pdc", 110 | "name": "WEATHER_PARTLY_CLOUDY_NIGHT", 111 | "type": "raw" 112 | }, 113 | { 114 | "file": "data/PARTLY_CLOUDY.pdc", 115 | "name": "WEATHER_PARTLY_CLOUDY", 116 | "type": "raw" 117 | }, 118 | { 119 | "file": "data/LIGHT_SNOW.pdc", 120 | "name": "WEATHER_LIGHT_SNOW", 121 | "type": "raw" 122 | }, 123 | { 124 | "file": "data/LIGHT_RAIN.pdc", 125 | "name": "WEATHER_LIGHT_RAIN", 126 | "type": "raw" 127 | }, 128 | { 129 | "file": "data/HEAVY_SNOW.pdc", 130 | "name": "WEATHER_HEAVY_SNOW", 131 | "type": "raw" 132 | }, 133 | { 134 | "file": "data/HEAVY_RAIN.pdc", 135 | "name": "WEATHER_HEAVY_RAIN", 136 | "type": "raw" 137 | }, 138 | { 139 | "file": "data/DISCONNECTED.pdc", 140 | "name": "DISCONNECTED", 141 | "type": "raw" 142 | }, 143 | { 144 | "file": "data/DATE_BG.pdc", 145 | "name": "DATE_BG", 146 | "type": "raw" 147 | }, 148 | { 149 | "file": "data/CLOUDY_DAY.pdc", 150 | "name": "WEATHER_CLOUDY", 151 | "type": "raw" 152 | }, 153 | { 154 | "file": "data/CLEAR_NIGHT.pdc", 155 | "name": "WEATHER_CLEAR_NIGHT", 156 | "type": "raw" 157 | }, 158 | { 159 | "file": "data/CLEAR_DAY.pdc", 160 | "name": "WEATHER_CLEAR_DAY", 161 | "type": "raw" 162 | }, 163 | { 164 | "file": "data/BATTERY_CHARGE.pdc", 165 | "name": "BATTERY_CHARGE", 166 | "type": "raw" 167 | }, 168 | { 169 | "file": "data/BATTERY_BG.pdc", 170 | "name": "BATTERY_BG", 171 | "type": "raw" 172 | }, 173 | { 174 | "file": "data/HEALTH_SLEEP.pdc", 175 | "name": "HEALTH_SLEEP", 176 | "type": "raw" 177 | }, 178 | { 179 | "file": "data/HEALTH_STEPS.pdc", 180 | "name": "HEALTH_STEPS", 181 | "type": "raw" 182 | }, 183 | { 184 | "file": "data/HEALTH_HEART.pdc", 185 | "name": "HEALTH_HEART", 186 | "type": "raw" 187 | } 188 | ] 189 | } 190 | }, 191 | "devDependencies": { 192 | "pebble-fctx-compiler": "^1.2.0" 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /pebble_basalt_ani.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/pebble_basalt_ani.gif -------------------------------------------------------------------------------- /pebble_round_ani.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/pebble_round_ani.gif -------------------------------------------------------------------------------- /project_banner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/project_banner.gif -------------------------------------------------------------------------------- /resources/data/BATTERY_BG.pdc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/data/BATTERY_BG.pdc -------------------------------------------------------------------------------- /resources/data/BATTERY_CHARGE.pdc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/data/BATTERY_CHARGE.pdc -------------------------------------------------------------------------------- /resources/data/CLEAR_DAY.pdc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/data/CLEAR_DAY.pdc -------------------------------------------------------------------------------- /resources/data/CLEAR_NIGHT.pdc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/data/CLEAR_NIGHT.pdc -------------------------------------------------------------------------------- /resources/data/CLOUDY_DAY.pdc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/data/CLOUDY_DAY.pdc -------------------------------------------------------------------------------- /resources/data/DATE_BG.pdc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/data/DATE_BG.pdc -------------------------------------------------------------------------------- /resources/data/DISCONNECTED.pdc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/data/DISCONNECTED.pdc -------------------------------------------------------------------------------- /resources/data/HEALTH_HEART.pdc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/data/HEALTH_HEART.pdc -------------------------------------------------------------------------------- /resources/data/HEALTH_SLEEP.pdc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/data/HEALTH_SLEEP.pdc -------------------------------------------------------------------------------- /resources/data/HEALTH_STEPS.pdc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/data/HEALTH_STEPS.pdc -------------------------------------------------------------------------------- /resources/data/HEAVY_RAIN.pdc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/data/HEAVY_RAIN.pdc -------------------------------------------------------------------------------- /resources/data/HEAVY_SNOW.pdc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/data/HEAVY_SNOW.pdc -------------------------------------------------------------------------------- /resources/data/LIGHT_RAIN.pdc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/data/LIGHT_RAIN.pdc -------------------------------------------------------------------------------- /resources/data/LIGHT_SNOW.pdc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/data/LIGHT_SNOW.pdc -------------------------------------------------------------------------------- /resources/data/PARTLY_CLOUDY.pdc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/data/PARTLY_CLOUDY.pdc -------------------------------------------------------------------------------- /resources/data/PARTLY_CLOUDY_NIGHT.pdc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/data/PARTLY_CLOUDY_NIGHT.pdc -------------------------------------------------------------------------------- /resources/data/RAINING_AND_SNOWING.pdc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/data/RAINING_AND_SNOWING.pdc -------------------------------------------------------------------------------- /resources/data/THUNDERSTORM.pdc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/data/THUNDERSTORM.pdc -------------------------------------------------------------------------------- /resources/data/WEATHER_GENERIC.pdc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/data/WEATHER_GENERIC.pdc -------------------------------------------------------------------------------- /resources/fonts/AvenirNextDemiBold.ffont: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/fonts/AvenirNextDemiBold.ffont -------------------------------------------------------------------------------- /resources/fonts/AvenirNextRegular.ffont: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/fonts/AvenirNextRegular.ffont -------------------------------------------------------------------------------- /resources/fonts/LECO1976-Regular.ffont: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/fonts/LECO1976-Regular.ffont -------------------------------------------------------------------------------- /resources/images/menuicon~bw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/images/menuicon~bw.png -------------------------------------------------------------------------------- /resources/images/menuicon~color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/images/menuicon~color.png -------------------------------------------------------------------------------- /resources/images/menuicon~color~round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plarus/TimeStyleBBPebble/cd5c4647ee347057d04b00d25bd3ca2445acf257/resources/images/menuicon~color~round.png -------------------------------------------------------------------------------- /src/c/clock_area.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "clock_area.h" 5 | #include "settings.h" 6 | #include "time_date.h" 7 | 8 | #define ROUND_VERTICAL_PADDING 15 9 | 10 | static Layer* clock_area_layer; 11 | 12 | // just allocate all the fonts at startup because i don't feel like 13 | // dealing with allocating and deallocating things 14 | static FFont* hours_font; 15 | static FFont* minutes_font; 16 | static FFont* colon_font; 17 | 18 | #ifndef PBL_ROUND 19 | static GFont date_font; 20 | static GFont am_pm_font; 21 | #else 22 | static GRect screen_rect; 23 | #endif 24 | 25 | static uint8_t prev_clockFontId; 26 | 27 | // "private" functions 28 | static void update_original_clock_area_layer(Layer *l, GContext* ctx, FContext* fctx) { 29 | // check layer bounds 30 | GRect bounds; 31 | 32 | #ifndef PBL_ROUND 33 | bounds = layer_get_unobstructed_bounds(l); 34 | #else 35 | bounds = GRect(0, ROUND_VERTICAL_PADDING, screen_rect.size.w, screen_rect.size.h - ROUND_VERTICAL_PADDING * 2); 36 | #endif 37 | 38 | // calculate font size 39 | int font_size = 4 * bounds.size.h / 7; 40 | 41 | // avenir + avenir bold metrics 42 | int v_padding = bounds.size.h / 16; 43 | int h_adjust = 0; 44 | int v_adjust = 0; 45 | 46 | // alternate metrics for LECO 47 | if(globalSettings.clockFontId == FONT_SETTING_LECO) { 48 | font_size += 6; 49 | v_padding = bounds.size.h / 20; 50 | h_adjust = -4; 51 | v_adjust = 0; 52 | 53 | // leco looks awful with antialiasing 54 | #ifdef PBL_COLOR 55 | fctx_enable_aa(false); 56 | } else { 57 | fctx_enable_aa(true); 58 | #endif 59 | } 60 | 61 | // if it's a round watch, EVERYTHING CHANGES 62 | #ifdef PBL_ROUND 63 | v_adjust = ROUND_VERTICAL_PADDING; 64 | 65 | if(globalSettings.clockFontId != FONT_SETTING_LECO) { 66 | h_adjust = -1; 67 | } 68 | #else 69 | // for rectangular watches, adjust X position based on sidebar position 70 | if(globalSettings.sidebarLocation == RIGHT) { 71 | h_adjust -= ACTION_BAR_WIDTH / 2 + 1; 72 | } else if(globalSettings.sidebarLocation == LEFT) { 73 | h_adjust += ACTION_BAR_WIDTH / 2; 74 | } 75 | #endif 76 | 77 | FPoint time_pos; 78 | 79 | // draw hours 80 | time_pos.x = INT_TO_FIXED(bounds.size.w / 2 + h_adjust); 81 | time_pos.y = INT_TO_FIXED(v_padding + v_adjust); 82 | fctx_begin_fill(fctx); 83 | fctx_set_offset(fctx, time_pos); 84 | fctx_set_text_em_height(fctx, hours_font, font_size); 85 | fctx_draw_string(fctx, time_date_hours, hours_font, GTextAlignmentCenter, FTextAnchorTop); 86 | fctx_end_fill(fctx); 87 | 88 | //draw minutes 89 | time_pos.y = INT_TO_FIXED(bounds.size.h - v_padding + v_adjust); 90 | fctx_begin_fill(fctx); 91 | fctx_set_offset(fctx, time_pos); 92 | fctx_set_text_em_height(fctx, minutes_font, font_size); 93 | fctx_draw_string(fctx, time_date_minutes, minutes_font, GTextAlignmentCenter, FTextAnchorBaseline); 94 | fctx_end_fill(fctx); 95 | } 96 | 97 | #ifndef PBL_ROUND 98 | static void update_clock_and_date_area_layer(Layer *l, GContext* ctx, FContext* fctx) { 99 | // check layer bounds 100 | GRect fullscreen_bounds = layer_get_bounds(l); 101 | 102 | // calculate font size 103 | int font_size = fullscreen_bounds.size.h / 3; 104 | 105 | // avenir + avenir bold metrics 106 | int v_padding = fullscreen_bounds.size.h / 16; 107 | int h_adjust = -2; 108 | int v_adjust = 0; 109 | 110 | // alternate metrics for LECO 111 | if(globalSettings.clockFontId == FONT_SETTING_LECO) { 112 | v_padding = fullscreen_bounds.size.h / 20; 113 | h_adjust = -3; 114 | v_adjust = 0; 115 | 116 | // leco looks awful with antialiasing 117 | #ifdef PBL_COLOR 118 | fctx_enable_aa(false); 119 | } else { 120 | fctx_enable_aa(true); 121 | #endif 122 | } 123 | 124 | // for rectangular watches, adjust X position based on sidebar position 125 | if(globalSettings.sidebarLocation == BOTTOM) { 126 | v_adjust -= 3; 127 | } else { 128 | GRect unobstructed_bounds = layer_get_unobstructed_bounds(l); 129 | int16_t obstruction_height = fullscreen_bounds.size.h - unobstructed_bounds.size.h; 130 | v_adjust += FIXED_WIDGET_HEIGHT - obstruction_height - 3; 131 | } 132 | 133 | int h_middle = fullscreen_bounds.size.w / 2; 134 | int h_colon_margin = 7; 135 | 136 | FPoint time_pos; 137 | 138 | #ifndef PBL_COLOR 139 | if(globalSettings.timeColor.argb == GColorLightGrayARGB8 && globalSettings.timeBgColor.argb == GColorWhiteARGB8) { 140 | graphics_context_set_text_color(ctx, GColorBlack); 141 | } else { 142 | #endif 143 | graphics_context_set_text_color(ctx, globalSettings.timeColor); 144 | #ifndef PBL_COLOR 145 | } 146 | #endif 147 | 148 | if(!clock_is_24h_style()) { 149 | // draw am/pm 150 | graphics_draw_text(ctx, 151 | time_date_isAmHour ? "AM" : "PM", 152 | am_pm_font, 153 | GRect(0, v_padding / 2 + v_adjust, fullscreen_bounds.size.w - h_colon_margin + h_adjust, 20), 154 | GTextOverflowModeFill, 155 | GTextAlignmentRight, 156 | NULL); 157 | } 158 | 159 | if(globalSettings.centerTime == false || globalSettings.clockFontId == FONT_SETTING_BOLD_H || globalSettings.clockFontId == FONT_SETTING_BOLD_M) { 160 | // draw hours 161 | time_pos.x = INT_TO_FIXED(h_middle - h_colon_margin + h_adjust); 162 | time_pos.y = INT_TO_FIXED(3 * v_padding + v_adjust); 163 | fctx_begin_fill(fctx); 164 | fctx_set_offset(fctx, time_pos); 165 | fctx_set_text_em_height(fctx, hours_font, font_size); 166 | fctx_draw_string(fctx, time_date_hours, hours_font, GTextAlignmentRight, FTextAnchorTop); 167 | fctx_end_fill(fctx); 168 | 169 | //draw ":" 170 | time_pos.x = INT_TO_FIXED(h_middle - 1); 171 | fctx_begin_fill(fctx); 172 | fctx_set_offset(fctx, time_pos); 173 | fctx_set_text_em_height(fctx, colon_font, font_size); 174 | fctx_draw_string(fctx, ":", colon_font, GTextAlignmentCenter, FTextAnchorTop); 175 | fctx_end_fill(fctx); 176 | 177 | //draw minutes 178 | time_pos.x = INT_TO_FIXED(h_middle + h_colon_margin + h_adjust); 179 | fctx_begin_fill(fctx); 180 | fctx_set_offset(fctx, time_pos); 181 | fctx_set_text_em_height(fctx, minutes_font, font_size); 182 | fctx_draw_string(fctx, time_date_minutes, minutes_font, GTextAlignmentLeft, FTextAnchorTop); 183 | fctx_end_fill(fctx); 184 | } else { 185 | // if only one font center all 186 | char time[6]; 187 | 188 | strncpy(time, time_date_hours, sizeof(time_date_hours)); 189 | strncat(time, ":" , 2); 190 | strncat(time, time_date_minutes, sizeof(time_date_minutes)); 191 | 192 | time_pos.x = INT_TO_FIXED(h_middle - 2); 193 | time_pos.y = INT_TO_FIXED(3 * v_padding + v_adjust); 194 | fctx_begin_fill(fctx); 195 | fctx_set_offset(fctx, time_pos); 196 | fctx_set_text_em_height(fctx, colon_font, font_size); 197 | fctx_draw_string(fctx, time, colon_font, GTextAlignmentCenter, FTextAnchorTop); 198 | fctx_end_fill(fctx); 199 | } 200 | 201 | char time_date_currentDate[21]; 202 | 203 | strncpy(time_date_currentDate, globalSettings.languageDayNames[time_date_currentDayName], sizeof(globalSettings.languageDayNames[time_date_currentDayName])); 204 | strncat(time_date_currentDate, " " , 2); 205 | strncat(time_date_currentDate, time_date_currentDayNum, sizeof(time_date_currentDayNum)); 206 | strncat(time_date_currentDate, " " , 2); 207 | strncat(time_date_currentDate, globalSettings.languageMonthNames[time_date_currentMonth], sizeof(globalSettings.languageMonthNames[time_date_currentMonth])); 208 | 209 | // draw date 210 | graphics_draw_text(ctx, 211 | time_date_currentDate, 212 | date_font, 213 | GRect(0, fullscreen_bounds.size.h / 2 - 11 + v_adjust, fullscreen_bounds.size.w, 30), 214 | GTextOverflowModeFill, 215 | GTextAlignmentCenter, 216 | NULL); 217 | } 218 | 219 | #else 220 | 221 | static void update_one_line_clock_area_layer(Layer *l, GContext* ctx, FContext* fctx) { 222 | // check layer bounds 223 | GRect fullscreen_bounds = layer_get_bounds(l); 224 | 225 | // calculate font size 226 | int font_size = fullscreen_bounds.size.h / 3 + 7; 227 | 228 | // avenir + avenir bold metrics 229 | int h_adjust = -2; 230 | 231 | // alternate metrics for LECO 232 | if(globalSettings.clockFontId == FONT_SETTING_LECO) { 233 | h_adjust = -3; 234 | 235 | // leco looks awful with antialiasing 236 | #ifdef PBL_COLOR 237 | fctx_enable_aa(false); 238 | } else { 239 | fctx_enable_aa(true); 240 | #endif 241 | } 242 | 243 | int h_middle = fullscreen_bounds.size.w / 2; 244 | int h_colon_margin = 7; 245 | 246 | FPoint time_pos; 247 | 248 | if(globalSettings.centerTime == false || globalSettings.clockFontId == FONT_SETTING_BOLD_H || globalSettings.clockFontId == FONT_SETTING_BOLD_M) { 249 | // draw hours 250 | time_pos.x = INT_TO_FIXED(h_middle - h_colon_margin + h_adjust); 251 | time_pos.y = INT_TO_FIXED(fullscreen_bounds.size.h / 2); 252 | fctx_begin_fill(fctx); 253 | fctx_set_offset(fctx, time_pos); 254 | fctx_set_text_em_height(fctx, hours_font, font_size); 255 | fctx_draw_string(fctx, time_date_hours, hours_font, GTextAlignmentRight, FTextAnchorMiddle); 256 | fctx_end_fill(fctx); 257 | 258 | //draw ":" 259 | time_pos.x = INT_TO_FIXED(h_middle - 1); 260 | fctx_begin_fill(fctx); 261 | fctx_set_offset(fctx, time_pos); 262 | fctx_set_text_em_height(fctx, colon_font, font_size); 263 | fctx_draw_string(fctx, ":", colon_font, GTextAlignmentCenter, FTextAnchorMiddle); 264 | fctx_end_fill(fctx); 265 | 266 | //draw minutes 267 | time_pos.x = INT_TO_FIXED(h_middle + h_colon_margin + h_adjust); 268 | fctx_begin_fill(fctx); 269 | fctx_set_offset(fctx, time_pos); 270 | fctx_set_text_em_height(fctx, minutes_font, font_size); 271 | fctx_draw_string(fctx, time_date_minutes, minutes_font, GTextAlignmentLeft, FTextAnchorMiddle); 272 | fctx_end_fill(fctx); 273 | } else { 274 | // if only one font center all 275 | char time[6]; 276 | 277 | strncpy(time, time_date_hours, sizeof(time_date_hours)); 278 | strncat(time, ":" , 2); 279 | strncat(time, time_date_minutes, sizeof(time_date_minutes)); 280 | 281 | time_pos.x = INT_TO_FIXED(h_middle - 2); 282 | time_pos.y = INT_TO_FIXED(fullscreen_bounds.size.h / 2); 283 | fctx_begin_fill(fctx); 284 | fctx_set_offset(fctx, time_pos); 285 | fctx_set_text_em_height(fctx, colon_font, font_size); 286 | fctx_draw_string(fctx, time, colon_font, GTextAlignmentCenter, FTextAnchorMiddle); 287 | fctx_end_fill(fctx); 288 | } 289 | } 290 | #endif 291 | 292 | static void update_clock_area_layer(Layer *l, GContext* ctx) { 293 | // initialize FCTX, the fancy 3rd party drawing library that all the cool kids use 294 | FContext fctx; 295 | 296 | fctx_init_context(&fctx, ctx); 297 | fctx_set_fill_color(&fctx, globalSettings.timeColor); 298 | 299 | if(globalSettings.sidebarLocation == BOTTOM || globalSettings.sidebarLocation == TOP) { 300 | #ifdef PBL_ROUND 301 | update_one_line_clock_area_layer(l, ctx, &fctx); 302 | #else 303 | update_clock_and_date_area_layer(l, ctx, &fctx); 304 | #endif // PBL_ROUND 305 | } else { 306 | update_original_clock_area_layer(l, ctx, &fctx); 307 | } 308 | 309 | fctx_deinit_context(&fctx); 310 | } 311 | 312 | void ClockArea_init(Window* window) { 313 | #ifndef PBL_ROUND 314 | GRect screen_rect; 315 | #endif 316 | 317 | // record the screen size, since we NEVER GET IT AGAIN 318 | screen_rect = layer_get_bounds(window_get_root_layer(window)); 319 | 320 | GRect bounds; 321 | bounds = GRect(0, 0, screen_rect.size.w, screen_rect.size.h); 322 | 323 | // init the clock area layer 324 | clock_area_layer = layer_create(bounds); 325 | layer_add_child(window_get_root_layer(window), clock_area_layer); 326 | layer_set_update_proc(clock_area_layer, update_clock_area_layer); 327 | 328 | prev_clockFontId = FONT_SETTING_UNSET; 329 | } 330 | 331 | void ClockArea_ffont_destroy(void) { 332 | ffont_destroy(hours_font); 333 | if(prev_clockFontId == FONT_SETTING_BOLD_H || prev_clockFontId == FONT_SETTING_BOLD_M) { 334 | ffont_destroy(minutes_font); 335 | } 336 | } 337 | 338 | void ClockArea_deinit(void) { 339 | layer_destroy(clock_area_layer); 340 | 341 | ClockArea_ffont_destroy(); 342 | } 343 | 344 | void ClockArea_redraw(void) { 345 | layer_mark_dirty(clock_area_layer); 346 | } 347 | 348 | void ClockArea_update_fonts(void) { 349 | #ifndef PBL_ROUND 350 | if(globalSettings.sidebarLocation == BOTTOM || globalSettings.sidebarLocation == TOP) { 351 | date_font = fonts_get_system_font(FONT_KEY_GOTHIC_28_BOLD); 352 | if(!clock_is_24h_style()) { 353 | am_pm_font = fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD); 354 | } 355 | } 356 | #endif 357 | 358 | if(prev_clockFontId != globalSettings.clockFontId) { 359 | if(prev_clockFontId != FONT_SETTING_UNSET) { 360 | ClockArea_ffont_destroy(); 361 | } 362 | 363 | FFont* avenir; 364 | FFont* avenir_bold; 365 | FFont* leco; 366 | 367 | switch(globalSettings.clockFontId) { 368 | case FONT_SETTING_DEFAULT: 369 | avenir = ffont_create_from_resource(RESOURCE_ID_AVENIR_REGULAR_FFONT); 370 | 371 | hours_font = avenir; 372 | minutes_font = avenir; 373 | colon_font = avenir; 374 | break; 375 | case FONT_SETTING_BOLD: 376 | avenir_bold = ffont_create_from_resource(RESOURCE_ID_AVENIR_BOLD_FFONT); 377 | 378 | hours_font = avenir_bold; 379 | minutes_font = avenir_bold; 380 | colon_font = avenir_bold; 381 | break; 382 | case FONT_SETTING_BOLD_H: 383 | avenir = ffont_create_from_resource(RESOURCE_ID_AVENIR_REGULAR_FFONT); 384 | avenir_bold = ffont_create_from_resource(RESOURCE_ID_AVENIR_BOLD_FFONT); 385 | 386 | hours_font = avenir_bold; 387 | minutes_font = avenir; 388 | colon_font = avenir; 389 | break; 390 | case FONT_SETTING_BOLD_M: 391 | avenir = ffont_create_from_resource(RESOURCE_ID_AVENIR_REGULAR_FFONT); 392 | avenir_bold = ffont_create_from_resource(RESOURCE_ID_AVENIR_BOLD_FFONT); 393 | 394 | hours_font = avenir; 395 | minutes_font = avenir_bold; 396 | colon_font = avenir; 397 | break; 398 | case FONT_SETTING_LECO: 399 | leco = ffont_create_from_resource(RESOURCE_ID_LECO_REGULAR_FFONT); 400 | 401 | hours_font = leco; 402 | minutes_font = leco; 403 | colon_font = leco; 404 | break; 405 | } 406 | prev_clockFontId = globalSettings.clockFontId; 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /src/c/clock_area.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | #define FONT_SETTING_DEFAULT 0 5 | #define FONT_SETTING_LECO 1 6 | #define FONT_SETTING_BOLD 2 7 | #define FONT_SETTING_BOLD_H 3 8 | #define FONT_SETTING_BOLD_M 4 9 | #define FONT_SETTING_UNSET 5 10 | 11 | // "public" functions 12 | void ClockArea_init(Window* window); 13 | void ClockArea_deinit(void); 14 | void ClockArea_redraw(void); 15 | void ClockArea_update_fonts(void); 16 | -------------------------------------------------------------------------------- /src/c/health.c: -------------------------------------------------------------------------------- 1 | #ifdef PBL_HEALTH 2 | #include 3 | #include "health.h" 4 | 5 | #define SECONDS_AFTER_WAKE_UP 1800 // Half hour 6 | 7 | static bool s_sleeping; 8 | static bool s_restfulSleeping; 9 | static time_t s_endSleepTime; 10 | static HealthValue s_sleep_seconds; 11 | static HealthValue s_restful_sleep_seconds; 12 | static HealthValue s_distance_walked; 13 | static HealthValue s_steps; 14 | static HealthValue s_active_seconds; 15 | static HealthValue s_active_kCalories; 16 | static HealthValue s_heart_rate; 17 | 18 | static inline bool is_health_metric_accessible(HealthMetric metric, time_t time_start, time_t time_end) { 19 | HealthServiceAccessibilityMask mask = health_service_metric_accessible(metric, time_start, time_end); 20 | return mask & HealthServiceAccessibilityMaskAvailable; 21 | } 22 | 23 | static inline HealthValue get_health_value_sum_today(HealthMetric metric) { 24 | time_t start = time_start_of_today(); 25 | time_t end = time(NULL); 26 | 27 | return is_health_metric_accessible(metric, start, end) ? health_service_sum_today(metric) : 0; 28 | } 29 | 30 | void Health_update(void) { 31 | HealthActivityMask mask = health_service_peek_current_activities(); 32 | 33 | // Sleep 34 | s_sleeping = (mask & HealthActivitySleep) || (mask & HealthActivityRestfulSleep); 35 | s_restfulSleeping = (mask & HealthActivityRestfulSleep); 36 | s_sleep_seconds = get_health_value_sum_today(HealthMetricSleepSeconds); 37 | s_restful_sleep_seconds = get_health_value_sum_today(HealthMetricSleepRestfulSeconds); 38 | 39 | if(s_sleeping) { 40 | s_endSleepTime = time(NULL); 41 | } 42 | 43 | // Steps 44 | s_distance_walked = get_health_value_sum_today(HealthMetricWalkedDistanceMeters); 45 | s_steps = get_health_value_sum_today(HealthMetricStepCount); 46 | s_active_seconds = get_health_value_sum_today(HealthMetricActiveSeconds); 47 | s_active_kCalories = get_health_value_sum_today(HealthMetricActiveKCalories); 48 | 49 | // Heart rate 50 | time_t now = time(NULL); 51 | if (is_health_metric_accessible(HealthMetricHeartRateBPM, now, now)) { 52 | s_heart_rate = health_service_peek_current_value(HealthMetricHeartRateBPM); 53 | } 54 | } 55 | 56 | bool Health_isUserSleeping(void) { 57 | return s_sleeping; 58 | } 59 | 60 | bool Health_isUserRestfulSleeping(void) { 61 | return s_restfulSleeping; 62 | } 63 | 64 | bool Health_sleepingToBeDisplayed(void) { 65 | // Sleep should be display during an half hour after wake up 66 | return s_sleeping || (s_endSleepTime + SECONDS_AFTER_WAKE_UP > time(NULL)); 67 | } 68 | 69 | HealthValue Health_getSleepSeconds(void) { 70 | return s_sleep_seconds; 71 | } 72 | 73 | HealthValue Health_getRestfulSleepSeconds(void) { 74 | return s_restful_sleep_seconds; 75 | } 76 | 77 | HealthValue Health_getDistanceWalked(void) { 78 | return s_distance_walked; 79 | } 80 | 81 | HealthValue Health_getSteps(void) { 82 | return s_steps; 83 | } 84 | 85 | HealthValue Health_getActiveSeconds(void) { 86 | return s_active_seconds; 87 | } 88 | 89 | HealthValue Health_getActiveKCalories(void) { 90 | return s_active_kCalories; 91 | } 92 | 93 | HealthValue Health_getHeartRate(void) { 94 | return s_heart_rate; 95 | } 96 | 97 | #endif // PBL_HEALTH 98 | -------------------------------------------------------------------------------- /src/c/health.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | void Health_update(void); 5 | bool Health_isUserSleeping(void); 6 | bool Health_isUserRestfulSleeping(void); 7 | bool Health_sleepingToBeDisplayed(void); 8 | HealthValue Health_getSleepSeconds(void); 9 | HealthValue Health_getRestfulSleepSeconds(void); 10 | HealthValue Health_getDistanceWalked(void); 11 | HealthValue Health_getSteps(void); 12 | HealthValue Health_getActiveSeconds(void); 13 | HealthValue Health_getActiveKCalories(void); 14 | HealthValue Health_getHeartRate(void); 15 | -------------------------------------------------------------------------------- /src/c/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "clock_area.h" 3 | #include "messaging.h" 4 | #include "settings.h" 5 | #include "weather.h" 6 | #include "sidebar.h" 7 | #include "util.h" 8 | #ifdef PBL_HEALTH 9 | #include "health.h" 10 | #endif 11 | #include "time_date.h" 12 | 13 | // windows and layers 14 | static Window* mainWindow; 15 | static Layer* windowLayer; 16 | 17 | // current bluetooth state 18 | static bool isPhoneConnected; 19 | 20 | // current time service subscription 21 | static bool updatingEverySecond; 22 | 23 | // try to randomize when watches call the weather API 24 | static uint8_t weatherRefreshMinute; 25 | 26 | static void update_screen(void) { 27 | time_date_update(); 28 | 29 | #ifdef PBL_HEALTH 30 | Health_update(); 31 | #endif 32 | 33 | // update the sidebar 34 | if(globalSettings.sidebarLocation != NONE) { 35 | Sidebar_redraw(); 36 | } 37 | 38 | ClockArea_redraw(); 39 | 40 | //APP_LOG(APP_LOG_LEVEL_DEBUG,"Avail RAM: %d", heap_bytes_free()); 41 | } 42 | 43 | static void tick_handler(struct tm *tick_time, TimeUnits units_changed) { 44 | // every 30 minutes, request new weather data 45 | if(!globalSettings.disableWeather) { 46 | if(tick_time->tm_min == weatherRefreshMinute && tick_time->tm_sec == 0) { 47 | messaging_requestNewWeatherData(); 48 | } 49 | } 50 | 51 | // every hour, if requested, vibrate 52 | if(!quiet_time_is_active() && tick_time->tm_sec == 0) { 53 | if(globalSettings.hourlyVibe == VIBE_EVERY_HOUR) { // hourly vibes only 54 | if(tick_time->tm_min == 0) { 55 | vibes_double_pulse(); 56 | } 57 | } else if(globalSettings.hourlyVibe == VIBE_EVERY_HALF_HOUR) { // hourly and half-hourly 58 | if(tick_time->tm_min == 0) { 59 | vibes_double_pulse(); 60 | } else if(tick_time->tm_min == 30) { 61 | vibes_short_pulse(); 62 | } 63 | } 64 | } 65 | 66 | update_screen(); 67 | } 68 | 69 | #ifndef PBL_ROUND 70 | static void unobstructed_area_will_change_handler(GRect final_unobstructed_screen_area, void *context) { 71 | // Get the full size of the screen 72 | GRect full_bounds = layer_get_bounds(windowLayer); 73 | if (!grect_equal(&full_bounds, &final_unobstructed_screen_area) && globalSettings.sidebarLocation == TOP) { 74 | // Screen is about to become obstructed, hide the bottom/top bar 75 | Sidebar_set_hidden(true); 76 | } 77 | } 78 | 79 | static void unobstructed_area_did_change_handler(void *context) { 80 | int obstruction_height = get_obstruction_height(windowLayer); 81 | 82 | if (obstruction_height == 0 && globalSettings.sidebarLocation == TOP) { 83 | Sidebar_set_hidden(false); 84 | } 85 | } 86 | #endif 87 | 88 | /* forces everything on screen to be redrawn -- perfect for keeping track of settings! */ 89 | static void redrawScreen() { 90 | 91 | // check if the tick handler frequency should be changed 92 | if(globalSettings.updateScreenEverySecond != updatingEverySecond) { 93 | tick_timer_service_unsubscribe(); 94 | 95 | if(globalSettings.updateScreenEverySecond) { 96 | tick_timer_service_subscribe(SECOND_UNIT, tick_handler); 97 | updatingEverySecond = true; 98 | } else { 99 | tick_timer_service_subscribe(MINUTE_UNIT, tick_handler); 100 | updatingEverySecond = false; 101 | } 102 | } 103 | 104 | #ifndef PBL_ROUND 105 | unobstructed_area_service_unsubscribe(); 106 | 107 | if(globalSettings.sidebarLocation == TOP) { 108 | UnobstructedAreaHandlers unobstructed_area_handlers = { 109 | .will_change = unobstructed_area_will_change_handler, 110 | .did_change = unobstructed_area_did_change_handler 111 | }; 112 | 113 | unobstructed_area_service_subscribe(unobstructed_area_handlers, NULL); 114 | } 115 | #endif 116 | 117 | window_set_background_color(mainWindow, globalSettings.timeBgColor); 118 | 119 | // maybe sidebar changed! 120 | Sidebar_set_layer(); 121 | 122 | // check if the fonts need to be switched 123 | ClockArea_update_fonts(); 124 | 125 | // Make sure display is refreshed from the start 126 | update_screen(); 127 | } 128 | 129 | static void main_window_load(Window *window) { 130 | // create the sidebar 131 | Sidebar_init(window); 132 | 133 | ClockArea_init(window); 134 | 135 | // Make sure the time is displayed from the start 136 | redrawScreen(); 137 | } 138 | 139 | static void main_window_unload(Window *window) { 140 | ClockArea_deinit(); 141 | Sidebar_deinit(); 142 | } 143 | 144 | static void bluetoothStateChanged(bool newConnectionState) { 145 | // if the phone was connected but isn't anymore and the user has opted in, 146 | // trigger a vibration 147 | if(!quiet_time_is_active() && isPhoneConnected && !newConnectionState && globalSettings.btVibe) { 148 | static uint32_t const segments[] = { 200, 100, 100, 100, 500 }; 149 | VibePattern pat = { 150 | .durations = segments, 151 | .num_segments = ARRAY_LENGTH(segments), 152 | }; 153 | vibes_enqueue_custom_pattern(pat); 154 | } 155 | 156 | // if the phone was disconnected and isn't anymore, update the data 157 | if(!globalSettings.disableWeather && !isPhoneConnected && newConnectionState) { 158 | messaging_requestNewWeatherData(); 159 | } 160 | 161 | isPhoneConnected = newConnectionState; 162 | 163 | if(globalSettings.sidebarLocation != NONE) { 164 | Sidebar_redraw(); 165 | } 166 | } 167 | 168 | // fixes for disappearing elements after notifications 169 | // (from http://codecorner.galanter.net/2016/01/08/solved-issue-with-pebble-framebuffer-after-notification-is-dismissed/) 170 | static void app_focus_changing(bool focusing) { 171 | if (focusing) { 172 | layer_set_hidden(windowLayer, true); 173 | } 174 | } 175 | 176 | static void app_focus_changed(bool focused) { 177 | if (focused) { 178 | layer_set_hidden(windowLayer, false); 179 | layer_mark_dirty(windowLayer); 180 | } 181 | } 182 | 183 | static void init(void) { 184 | setlocale(LC_ALL, ""); 185 | 186 | srand(time(NULL)); 187 | 188 | weatherRefreshMinute = rand() % 60; 189 | 190 | // init settings 191 | Settings_init(); 192 | 193 | // init weather system 194 | Weather_init(); 195 | 196 | // init the messaging thing 197 | messaging_init(redrawScreen); 198 | 199 | // Create main Window element and assign to pointer 200 | mainWindow = window_create(); 201 | 202 | // Set handlers to manage the elements inside the Window 203 | window_set_window_handlers(mainWindow, (WindowHandlers) { 204 | .load = main_window_load, 205 | .unload = main_window_unload 206 | }); 207 | 208 | // Show the Window on the watch, with animated=true 209 | window_stack_push(mainWindow, true); 210 | 211 | windowLayer = window_get_root_layer(mainWindow); 212 | 213 | // Register with TickTimerService 214 | if(globalSettings.updateScreenEverySecond) { 215 | tick_timer_service_subscribe(SECOND_UNIT, tick_handler); 216 | updatingEverySecond = true; 217 | } else { 218 | tick_timer_service_subscribe(MINUTE_UNIT, tick_handler); 219 | updatingEverySecond = false; 220 | } 221 | 222 | bool connected = bluetooth_connection_service_peek(); 223 | bluetoothStateChanged(connected); 224 | bluetooth_connection_service_subscribe(bluetoothStateChanged); 225 | 226 | // set up focus change handlers 227 | app_focus_service_subscribe_handlers((AppFocusHandlers){ 228 | .did_focus = app_focus_changed, 229 | .will_focus = app_focus_changing 230 | }); 231 | } 232 | 233 | static void deinit(void) { 234 | // Destroy Window 235 | window_destroy(mainWindow); 236 | 237 | // unload weather stuff 238 | Weather_deinit(); 239 | Settings_deinit(); 240 | 241 | tick_timer_service_unsubscribe(); 242 | bluetooth_connection_service_unsubscribe(); 243 | #ifndef PBL_ROUND 244 | unobstructed_area_service_unsubscribe(); 245 | #endif 246 | app_focus_service_unsubscribe(); 247 | } 248 | 249 | int main(void) { 250 | init(); 251 | app_event_loop(); 252 | deinit(); 253 | } 254 | 255 | -------------------------------------------------------------------------------- /src/c/messaging.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "weather.h" 3 | #include "settings.h" 4 | #include "messaging.h" 5 | 6 | static MessageProcessedCallback message_processed_callback; 7 | 8 | static void inbox_received_callback(DictionaryIterator *iterator, void *context) { 9 | // does this message contain current weather conditions? 10 | Tuple *weatherTemp_tuple = dict_find(iterator, MESSAGE_KEY_WeatherTemperature); 11 | Tuple *weatherConditions_tuple = dict_find(iterator, MESSAGE_KEY_WeatherCondition); 12 | 13 | if(weatherTemp_tuple != NULL && weatherConditions_tuple != NULL) { 14 | // now set the weather conditions properly 15 | Weather_weatherInfo.currentTemp = (int)weatherTemp_tuple->value->int32; 16 | 17 | Weather_setCurrentCondition(weatherConditions_tuple->value->int32); 18 | 19 | Weather_saveData(); 20 | } 21 | 22 | // does this message contain weather forecast information? 23 | Tuple *weatherForecastCondition_tuple = dict_find(iterator, MESSAGE_KEY_WeatherForecastCondition); 24 | Tuple *weatherForecastHigh_tuple = dict_find(iterator, MESSAGE_KEY_WeatherForecastHighTemp); 25 | Tuple *weatherForecastLow_tuple = dict_find(iterator, MESSAGE_KEY_WeatherForecastLowTemp); 26 | 27 | if(weatherForecastCondition_tuple != NULL && weatherForecastHigh_tuple != NULL 28 | && weatherForecastLow_tuple != NULL) { 29 | 30 | Weather_weatherForecast.highTemp = (int)weatherForecastHigh_tuple->value->int32; 31 | Weather_weatherForecast.lowTemp = (int)weatherForecastLow_tuple->value->int32; 32 | Weather_setForecastCondition(weatherForecastCondition_tuple->value->int32); 33 | 34 | Weather_saveData(); 35 | } 36 | 37 | // does this message contain new config information? 38 | Tuple *timeColor_tuple = dict_find(iterator, MESSAGE_KEY_SettingColorTime); 39 | Tuple *bgColor_tuple = dict_find(iterator, MESSAGE_KEY_SettingColorBG); 40 | Tuple *sidebarColor_tuple = dict_find(iterator, MESSAGE_KEY_SettingColorSidebar); 41 | Tuple *sidebarPos_tuple = dict_find(iterator, MESSAGE_KEY_SettingSidebarPosition); 42 | Tuple *sidebarTextColor_tuple = dict_find(iterator, MESSAGE_KEY_SettingSidebarTextColor); 43 | Tuple *useMetric_tuple = dict_find(iterator, MESSAGE_KEY_SettingUseMetric); 44 | Tuple *btVibe_tuple = dict_find(iterator, MESSAGE_KEY_SettingBluetoothVibe); 45 | Tuple *language_tuple = dict_find(iterator, MESSAGE_KEY_SettingLanguageID); 46 | Tuple *leadingZero_tuple = dict_find(iterator, MESSAGE_KEY_SettingShowLeadingZero); 47 | Tuple *centerTime_tuple = dict_find(iterator, MESSAGE_KEY_SettingCenterTime); 48 | Tuple *batteryPct_tuple = dict_find(iterator, MESSAGE_KEY_SettingShowBatteryPct); 49 | Tuple *disableWeather_tuple = dict_find(iterator, MESSAGE_KEY_SettingDisableWeather); 50 | Tuple *clockFont_tuple = dict_find(iterator, MESSAGE_KEY_SettingClockFontId); 51 | Tuple *hourlyVibe_tuple = dict_find(iterator, MESSAGE_KEY_SettingHourlyVibe); 52 | Tuple *useLargeFonts_tuple = dict_find(iterator, MESSAGE_KEY_SettingUseLargeFonts); 53 | 54 | Tuple *widget0Id_tuple = dict_find(iterator, MESSAGE_KEY_SettingWidget0ID); 55 | Tuple *widget1Id_tuple = dict_find(iterator, MESSAGE_KEY_SettingWidget1ID); 56 | Tuple *widget2Id_tuple = dict_find(iterator, MESSAGE_KEY_SettingWidget2ID); 57 | Tuple *widget3Id_tuple = dict_find(iterator, MESSAGE_KEY_SettingWidget3ID); 58 | 59 | Tuple *altclockName_tuple = dict_find(iterator, MESSAGE_KEY_SettingAltClockName); 60 | Tuple *altclockOffset_tuple = dict_find(iterator, MESSAGE_KEY_SettingAltClockOffset); 61 | 62 | Tuple *decimalSeparator_tuple = dict_find(iterator, MESSAGE_KEY_SettingDecimalSep); 63 | Tuple *healthActivityDisplay_tuple = dict_find(iterator, MESSAGE_KEY_SettingHealthActivityDisplay); 64 | Tuple *healthUseRestfulSleep_tuple = dict_find(iterator, MESSAGE_KEY_SettingHealthUseRestfulSleep); 65 | 66 | Tuple *autobattery_tuple = dict_find(iterator, MESSAGE_KEY_SettingDisableAutobattery); 67 | 68 | Tuple *activateDisconnectIcon_tuple = dict_find(iterator, MESSAGE_KEY_SettingDisconnectIcon); 69 | 70 | 71 | if(timeColor_tuple != NULL) { 72 | globalSettings.timeColor = GColorFromHEX(timeColor_tuple->value->int32); 73 | } 74 | 75 | if(bgColor_tuple != NULL) { 76 | globalSettings.timeBgColor = GColorFromHEX(bgColor_tuple->value->int32); 77 | } 78 | 79 | if(sidebarColor_tuple != NULL) { 80 | globalSettings.sidebarColor = GColorFromHEX(sidebarColor_tuple->value->int32); 81 | } 82 | 83 | if(sidebarTextColor_tuple != NULL) { 84 | // text can only be black or white, so we'll enforce that here 85 | globalSettings.sidebarTextColor = GColorFromHEX(sidebarTextColor_tuple->value->int32); 86 | } 87 | 88 | if(sidebarPos_tuple != NULL) { 89 | globalSettings.sidebarLocation = (BarLocationType)sidebarPos_tuple->value->int8; 90 | } 91 | 92 | if(useMetric_tuple != NULL) { 93 | globalSettings.useMetric = (bool)useMetric_tuple->value->int8; 94 | } 95 | 96 | if(btVibe_tuple != NULL) { 97 | globalSettings.btVibe = (bool)btVibe_tuple->value->int8; 98 | } 99 | 100 | if(leadingZero_tuple != NULL) { 101 | globalSettings.showLeadingZero = (bool)leadingZero_tuple->value->int8; 102 | } 103 | 104 | if(centerTime_tuple != NULL) { 105 | globalSettings.centerTime = (bool)centerTime_tuple->value->int8; 106 | } 107 | 108 | if(batteryPct_tuple != NULL) { 109 | globalSettings.showBatteryPct = (bool)batteryPct_tuple->value->int8; 110 | } 111 | 112 | if(autobattery_tuple != NULL) { 113 | globalSettings.disableAutobattery = (bool)autobattery_tuple->value->int8; 114 | } 115 | 116 | if(disableWeather_tuple != NULL) { 117 | globalSettings.disableWeather = (bool)disableWeather_tuple->value->int8; 118 | } 119 | 120 | if(clockFont_tuple != NULL) { 121 | globalSettings.clockFontId = clockFont_tuple->value->int8; 122 | } 123 | 124 | if(useLargeFonts_tuple != NULL) { 125 | globalSettings.useLargeFonts = (bool)useLargeFonts_tuple->value->int8; 126 | } 127 | 128 | if(hourlyVibe_tuple != NULL) { 129 | globalSettings.hourlyVibe = hourlyVibe_tuple->value->int8; 130 | } 131 | 132 | if(language_tuple != NULL) { 133 | globalSettings.languageId = language_tuple->value->int8; 134 | } 135 | 136 | if(widget0Id_tuple != NULL) { 137 | globalSettings.widgets[0] = widget0Id_tuple->value->int8; 138 | } 139 | 140 | if(widget1Id_tuple != NULL) { 141 | globalSettings.widgets[1] = widget1Id_tuple->value->int8; 142 | } 143 | 144 | if(widget2Id_tuple != NULL) { 145 | globalSettings.widgets[2] = widget2Id_tuple->value->int8; 146 | } 147 | 148 | if(widget3Id_tuple != NULL) { 149 | globalSettings.widgets[3] = widget3Id_tuple->value->int8; 150 | } 151 | 152 | if(altclockName_tuple != NULL) { 153 | strncpy(globalSettings.altclockName, altclockName_tuple->value->cstring, sizeof(globalSettings.altclockName)); 154 | } 155 | 156 | if(altclockOffset_tuple != NULL) { 157 | globalSettings.altclockOffset = altclockOffset_tuple->value->int8; 158 | } 159 | 160 | if(decimalSeparator_tuple != NULL) { 161 | globalSettings.decimalSeparator = (char)decimalSeparator_tuple->value->int8; 162 | } 163 | 164 | if(healthActivityDisplay_tuple != NULL) { 165 | globalSettings.healthActivityDisplay = (ActivityDisplayType)healthActivityDisplay_tuple->value->int8; 166 | } 167 | 168 | if(healthUseRestfulSleep_tuple != NULL) { 169 | globalSettings.healthUseRestfulSleep = (bool)healthUseRestfulSleep_tuple->value->int8; 170 | } 171 | 172 | if(activateDisconnectIcon_tuple != NULL) { 173 | globalSettings.activateDisconnectIcon = (bool)activateDisconnectIcon_tuple->value->int8; 174 | } 175 | 176 | // does this message contain new language information? 177 | Tuple *languageDayNames_tuple = dict_find(iterator, MESSAGE_KEY_SettingLanguageDayNames); 178 | Tuple *languageMonthNames_tuple = dict_find(iterator, MESSAGE_KEY_SettingLanguageMonthNames); 179 | Tuple *languageWordForWeek_tuple = dict_find(iterator, MESSAGE_KEY_SettingLanguageWordForWeek); 180 | 181 | if(languageDayNames_tuple != NULL) { 182 | for(int i = 0;i<7;i++){ 183 | strncpy(globalSettings.languageDayNames[i], languageDayNames_tuple->value->cstring, sizeof(globalSettings.languageDayNames[i])); 184 | languageDayNames_tuple = dict_find(iterator, MESSAGE_KEY_SettingLanguageDayNames + i + 1); 185 | } 186 | } 187 | 188 | if(languageMonthNames_tuple != NULL) { 189 | for(int i = 0;i<12;i++){ 190 | strncpy(globalSettings.languageMonthNames[i], languageMonthNames_tuple->value->cstring, sizeof(globalSettings.languageMonthNames[i])); 191 | languageMonthNames_tuple = dict_find(iterator, MESSAGE_KEY_SettingLanguageMonthNames + i + 1); 192 | } 193 | } 194 | 195 | if(languageWordForWeek_tuple != NULL) { 196 | strncpy(globalSettings.languageWordForWeek, languageWordForWeek_tuple->value->cstring, sizeof(globalSettings.languageWordForWeek)); 197 | } 198 | 199 | Settings_updateDynamicSettings(); 200 | 201 | // save the new settings to persistent storage 202 | Settings_saveToStorage(); 203 | 204 | // notify the main screen, in case something changed 205 | message_processed_callback(); 206 | } 207 | 208 | void messaging_requestNewWeatherData(void) { 209 | // just send an empty message for now 210 | DictionaryIterator *iter; 211 | app_message_outbox_begin(&iter); 212 | dict_write_uint32(iter, 0, 0); 213 | app_message_outbox_send(); 214 | } 215 | 216 | void messaging_init(MessageProcessedCallback processed_callback) { 217 | // register my custom callback 218 | message_processed_callback = processed_callback; 219 | 220 | // Register callbacks 221 | app_message_register_inbox_received(inbox_received_callback); 222 | 223 | // Open AppMessage 224 | app_message_open(305, 8); 225 | 226 | // APP_LOG(APP_LOG_LEVEL_DEBUG, "Watch messaging is started!"); 227 | } 228 | -------------------------------------------------------------------------------- /src/c/messaging.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | typedef void (*MessageProcessedCallback)(void); 5 | 6 | void messaging_requestNewWeatherData(void); 7 | void messaging_init(MessageProcessedCallback callback); -------------------------------------------------------------------------------- /src/c/settings.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "clock_area.h" 3 | #include "settings.h" 4 | 5 | Settings globalSettings; 6 | 7 | /* 8 | * Load defaults settings 9 | */ 10 | void Settings_loadDefaultsSettings(void) { 11 | // load the default colors 12 | #ifdef PBL_COLOR 13 | globalSettings.timeColor = GColorWhite; 14 | globalSettings.sidebarColor = GColorVividCerulean; 15 | #else 16 | globalSettings.timeColor = GColorWhite; 17 | globalSettings.sidebarColor = GColorLightGray; 18 | #endif 19 | globalSettings.timeBgColor = GColorBlack; 20 | globalSettings.sidebarTextColor = GColorBlack; 21 | 22 | globalSettings.languageId = LANGUAGE_EN; // English 23 | strncpy(globalSettings.languageDayNames[0], "SUN", sizeof(globalSettings.languageDayNames[0])); 24 | strncpy(globalSettings.languageDayNames[1], "MON", sizeof(globalSettings.languageDayNames[0])); 25 | strncpy(globalSettings.languageDayNames[2], "TUE", sizeof(globalSettings.languageDayNames[0])); 26 | strncpy(globalSettings.languageDayNames[3], "WED", sizeof(globalSettings.languageDayNames[0])); 27 | strncpy(globalSettings.languageDayNames[4], "THU", sizeof(globalSettings.languageDayNames[0])); 28 | strncpy(globalSettings.languageDayNames[5], "FRI", sizeof(globalSettings.languageDayNames[0])); 29 | strncpy(globalSettings.languageDayNames[6], "SAT", sizeof(globalSettings.languageDayNames[0])); 30 | strncpy(globalSettings.languageMonthNames[0], "JAN", sizeof(globalSettings.languageMonthNames[0])); 31 | strncpy(globalSettings.languageMonthNames[1], "FEB", sizeof(globalSettings.languageMonthNames[0])); 32 | strncpy(globalSettings.languageMonthNames[2], "MAR", sizeof(globalSettings.languageMonthNames[0])); 33 | strncpy(globalSettings.languageMonthNames[3], "APR", sizeof(globalSettings.languageMonthNames[0])); 34 | strncpy(globalSettings.languageMonthNames[4], "MAY", sizeof(globalSettings.languageMonthNames[0])); 35 | strncpy(globalSettings.languageMonthNames[5], "JUN", sizeof(globalSettings.languageMonthNames[0])); 36 | strncpy(globalSettings.languageMonthNames[6], "JUL", sizeof(globalSettings.languageMonthNames[0])); 37 | strncpy(globalSettings.languageMonthNames[7], "AUG", sizeof(globalSettings.languageMonthNames[0])); 38 | strncpy(globalSettings.languageMonthNames[8], "SEP", sizeof(globalSettings.languageMonthNames[0])); 39 | strncpy(globalSettings.languageMonthNames[9], "OCT", sizeof(globalSettings.languageMonthNames[0])); 40 | strncpy(globalSettings.languageMonthNames[10], "NOV", sizeof(globalSettings.languageMonthNames[0])); 41 | strncpy(globalSettings.languageMonthNames[11], "DEC", sizeof(globalSettings.languageMonthNames[0])); 42 | strncpy(globalSettings.languageWordForWeek, "Wk", sizeof(globalSettings.languageWordForWeek)); 43 | 44 | globalSettings.showLeadingZero = false; 45 | globalSettings.clockFontId = FONT_SETTING_DEFAULT; 46 | globalSettings.btVibe = false; 47 | globalSettings.hourlyVibe = NO_VIBE; 48 | globalSettings.sidebarLocation = BOTTOM; 49 | 50 | // set the default widgets 51 | globalSettings.widgets[0] = BATTERY_METER; 52 | globalSettings.widgets[1] = WEATHER_CURRENT; 53 | globalSettings.widgets[2] = PBL_IF_HEALTH_ELSE(HEALTH, BLUETOOTH_DISCONNECT); 54 | globalSettings.widgets[3] = WEEK_NUMBER; 55 | 56 | globalSettings.useLargeFonts = false; 57 | globalSettings.useMetric = true; 58 | globalSettings.showBatteryPct = true; 59 | globalSettings.disableAutobattery = false; 60 | globalSettings.healthActivityDisplay = STEPS; 61 | globalSettings.healthUseRestfulSleep = false; 62 | globalSettings.decimalSeparator = '.'; 63 | strncpy(globalSettings.altclockName, "ALT", sizeof(globalSettings.altclockName)); 64 | globalSettings.altclockOffset = 0; 65 | globalSettings.activateDisconnectIcon = true; 66 | globalSettings.centerTime = false; 67 | } 68 | 69 | /* 70 | * Load the saved color settings 71 | */ 72 | void Settings_loadFromStorage(void) { 73 | StoredSettings storedSettings; 74 | memset(&storedSettings,0,sizeof(StoredSettings)); 75 | // if previous version settings are used than only first part of settings would be overwritten, 76 | // all the other fields will left filled with zeroes 77 | persist_read_data(SETTING_VERSION6_AND_HIGHER, &storedSettings, sizeof(StoredSettings)); 78 | globalSettings.timeColor = storedSettings.timeColor; 79 | globalSettings.timeBgColor = storedSettings.timeBgColor; 80 | globalSettings.sidebarColor = storedSettings.sidebarColor; 81 | globalSettings.sidebarTextColor = storedSettings.sidebarTextColor; 82 | globalSettings.languageId = storedSettings.languageId; 83 | memcpy(globalSettings.languageDayNames, storedSettings.languageDayNames, sizeof(globalSettings.languageDayNames)); 84 | memcpy(globalSettings.languageMonthNames, storedSettings.languageMonthNames, sizeof(globalSettings.languageMonthNames)); 85 | memcpy(globalSettings.languageWordForWeek, storedSettings.languageWordForWeek, sizeof(globalSettings.languageWordForWeek)); 86 | globalSettings.showLeadingZero = storedSettings.showLeadingZero; 87 | globalSettings.clockFontId = storedSettings.clockFontId; 88 | globalSettings.btVibe = storedSettings.btVibe; 89 | globalSettings.hourlyVibe = storedSettings.hourlyVibe; 90 | globalSettings.sidebarLocation = storedSettings.sidebarLocation; 91 | globalSettings.widgets[0] = storedSettings.widgets[0]; 92 | globalSettings.widgets[1] = storedSettings.widgets[1]; 93 | globalSettings.widgets[2] = storedSettings.widgets[2]; 94 | globalSettings.widgets[3] = storedSettings.widgets[3]; 95 | globalSettings.useLargeFonts = storedSettings.useLargeFonts; 96 | globalSettings.useMetric = storedSettings.useMetric; 97 | globalSettings.showBatteryPct = storedSettings.showBatteryPct; 98 | globalSettings.disableAutobattery = storedSettings.disableAutobattery; 99 | globalSettings.healthActivityDisplay = storedSettings.healthActivityDisplay; 100 | globalSettings.healthUseRestfulSleep = storedSettings.healthUseRestfulSleep; 101 | globalSettings.decimalSeparator = storedSettings.decimalSeparator; 102 | memcpy(globalSettings.altclockName, storedSettings.altclockName, 8); 103 | globalSettings.altclockOffset = storedSettings.altclockOffset; 104 | globalSettings.activateDisconnectIcon = storedSettings.activateDisconnectIcon; 105 | globalSettings.centerTime = storedSettings.centerTime; 106 | } 107 | 108 | void Settings_saveToStorage(void) { 109 | // save settings to compressed structure and to persistent storage 110 | StoredSettings storedSettings; 111 | // if previous version settings are used than only first part of settings would be overwrited 112 | // all the other fields will left filled with zeroes 113 | storedSettings.timeColor = globalSettings.timeColor; 114 | storedSettings.timeBgColor = globalSettings.timeBgColor; 115 | storedSettings.sidebarColor = globalSettings.sidebarColor; 116 | storedSettings.sidebarTextColor = globalSettings.sidebarTextColor; 117 | storedSettings.languageId = globalSettings.languageId; 118 | memcpy(storedSettings.languageDayNames, globalSettings.languageDayNames, sizeof(globalSettings.languageDayNames)); 119 | memcpy(storedSettings.languageMonthNames, globalSettings.languageMonthNames, sizeof(globalSettings.languageMonthNames)); 120 | memcpy(storedSettings.languageWordForWeek, globalSettings.languageWordForWeek, sizeof(globalSettings.languageWordForWeek)); 121 | storedSettings.showLeadingZero = globalSettings.showLeadingZero; 122 | storedSettings.clockFontId = globalSettings.clockFontId; 123 | storedSettings.btVibe = globalSettings.btVibe; 124 | storedSettings.hourlyVibe = globalSettings.hourlyVibe; 125 | storedSettings.widgets[0] = globalSettings.widgets[0]; 126 | storedSettings.widgets[1] = globalSettings.widgets[1]; 127 | storedSettings.widgets[2] = globalSettings.widgets[2]; 128 | storedSettings.widgets[3] = globalSettings.widgets[3]; 129 | storedSettings.useLargeFonts = globalSettings.useLargeFonts; 130 | storedSettings.useMetric = globalSettings.useMetric; 131 | storedSettings.showBatteryPct = globalSettings.showBatteryPct; 132 | storedSettings.disableAutobattery = globalSettings.disableAutobattery; 133 | storedSettings.healthActivityDisplay = globalSettings.healthActivityDisplay; 134 | storedSettings.healthUseRestfulSleep = globalSettings.healthUseRestfulSleep; 135 | storedSettings.decimalSeparator = globalSettings.decimalSeparator; 136 | memcpy(storedSettings.altclockName, globalSettings.altclockName, 8); 137 | storedSettings.altclockOffset = globalSettings.altclockOffset; 138 | storedSettings.sidebarLocation = globalSettings.sidebarLocation; 139 | storedSettings.activateDisconnectIcon = globalSettings.activateDisconnectIcon; 140 | storedSettings.centerTime = globalSettings.centerTime; 141 | 142 | persist_write_data(SETTING_VERSION6_AND_HIGHER, &storedSettings, sizeof(StoredSettings)); 143 | persist_write_int(SETTINGS_VERSION_KEY, CURRENT_SETTINGS_VERSION); 144 | } 145 | 146 | void Settings_updateDynamicSettings(void) { 147 | globalSettings.disableWeather = true; 148 | globalSettings.updateScreenEverySecond = false; 149 | globalSettings.enableAutoBatteryWidget = true; 150 | globalSettings.enableBeats = false; 151 | globalSettings.enableAltTimeZone = false; 152 | 153 | for(int i = 0; i < 4; i++) { 154 | // if there are any weather widgets, enable weather checking 155 | if(globalSettings.widgets[i] == WEATHER_CURRENT || 156 | globalSettings.widgets[i] == WEATHER_FORECAST_TODAY) { 157 | globalSettings.disableWeather = false; 158 | } 159 | 160 | // if any widget is "seconds", we'll need to update the sidebar every second 161 | if(globalSettings.widgets[i] == SECONDS) { 162 | globalSettings.updateScreenEverySecond = true; 163 | } 164 | 165 | // if any widget is "battery", disable the automatic battery indication 166 | if(globalSettings.widgets[i] == BATTERY_METER) { 167 | globalSettings.enableAutoBatteryWidget = false; 168 | } 169 | 170 | // if any widget is "beats", enable the beats calculation 171 | if(globalSettings.widgets[i] == BEATS) { 172 | globalSettings.enableBeats = true; 173 | } 174 | 175 | // if any widget is "alt_time_zone", enable the alternative time calculation 176 | if(globalSettings.widgets[i] == ALT_TIME_ZONE) { 177 | globalSettings.enableAltTimeZone = true; 178 | } 179 | } 180 | 181 | // temp: if the sidebar is black, use inverted colors for icons 182 | if(gcolor_equal(globalSettings.sidebarColor, GColorBlack)) { 183 | globalSettings.iconFillColor = GColorBlack; 184 | globalSettings.iconStrokeColor = globalSettings.sidebarTextColor; // exciting 185 | } else { 186 | globalSettings.iconFillColor = GColorWhite; 187 | globalSettings.iconStrokeColor = GColorBlack; 188 | } 189 | } 190 | 191 | void Settings_init(void) { 192 | // first, check if we have any saved settings 193 | int current_settings_version = persist_exists(SETTINGS_VERSION_KEY) ? persist_read_int(SETTINGS_VERSION_KEY) : -1; 194 | APP_LOG(APP_LOG_LEVEL_DEBUG,"current_settings_version: %d", current_settings_version); 195 | if( current_settings_version < CURRENT_SETTINGS_VERSION ) { 196 | // load all settings 197 | Settings_loadDefaultsSettings(); 198 | } else { 199 | // load all settings 200 | Settings_loadFromStorage(); 201 | } 202 | Settings_updateDynamicSettings(); 203 | } 204 | 205 | void Settings_deinit(void) { 206 | // write all settings to storage 207 | Settings_saveToStorage(); 208 | } 209 | 210 | -------------------------------------------------------------------------------- /src/c/settings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "sidebar_widgets.h" 4 | 5 | #define SETTINGS_VERSION_KEY 4 6 | 7 | // settings "version" for app version 4.0 8 | #define CURRENT_SETTINGS_VERSION 8 9 | 10 | #define FIXED_WIDGET_HEIGHT 51 11 | 12 | #define LANGUAGE_EN 0 13 | #define LANGUAGE_FR 1 14 | #define LANGUAGE_DE 2 15 | #define LANGUAGE_ES 3 16 | #define LANGUAGE_IT 4 17 | #define LANGUAGE_NL 5 18 | #define LANGUAGE_TR 6 19 | #define LANGUAGE_CZ 7 20 | #define LANGUAGE_PT 8 21 | #define LANGUAGE_GK 9 22 | #define LANGUAGE_SE 10 23 | #define LANGUAGE_PL 11 24 | #define LANGUAGE_SK 12 25 | #define LANGUAGE_VN 13 26 | #define LANGUAGE_RO 14 27 | #define LANGUAGE_CA 15 28 | #define LANGUAGE_NO 16 29 | #define LANGUAGE_RU 17 30 | #define LANGUAGE_EE 18 31 | #define LANGUAGE_EU 19 32 | #define LANGUAGE_FI 20 33 | #define LANGUAGE_DA 21 34 | #define LANGUAGE_LT 22 35 | #define LANGUAGE_SL 23 36 | #define LANGUAGE_HU 24 37 | #define LANGUAGE_HR 25 38 | #define LANGUAGE_GA 26 39 | #define LANGUAGE_LV 27 40 | #define LANGUAGE_SR 28 41 | #define LANGUAGE_CN 29 42 | #define LANGUAGE_ID 30 43 | #define LANGUAGE_UK 31 44 | #define LANGUAGE_CY 32 // welsh 45 | #define LANGUAGE_GL 33 // gallacian 46 | #define LANGUAGE_JP 34 // japanese 47 | #define LANGUAGE_KR 35 // korean 48 | #define LANGUAGE_IW 36 // hebrew 49 | #define LANGUAGE_BG 37 // bulgarian 50 | 51 | typedef enum { 52 | NONE = 0, 53 | LEFT = 1, 54 | RIGHT = 2, 55 | BOTTOM = 3, 56 | TOP = 4 57 | } BarLocationType; 58 | 59 | typedef enum { 60 | NO_VIBE = 0, 61 | VIBE_EVERY_HOUR = 1, 62 | VIBE_EVERY_HALF_HOUR = 2 63 | } VibeIntervalType; 64 | 65 | typedef enum { 66 | STEPS = 0, 67 | DISTANCE = 1, 68 | DURATION = 2, 69 | KCALORIES = 3 70 | } ActivityDisplayType; 71 | 72 | typedef struct { 73 | // color settings 74 | GColor timeColor; 75 | GColor timeBgColor; 76 | GColor sidebarColor; 77 | GColor sidebarTextColor; 78 | 79 | // general settings 80 | uint8_t languageId; 81 | char languageDayNames[7][8]; 82 | char languageMonthNames[12][8]; 83 | char languageWordForWeek[12]; 84 | 85 | bool showLeadingZero; 86 | bool centerTime; 87 | uint8_t clockFontId; 88 | 89 | // vibration settings 90 | bool btVibe; 91 | VibeIntervalType hourlyVibe; 92 | 93 | // sidebar settings 94 | SidebarWidgetType widgets[4]; 95 | BarLocationType sidebarLocation; 96 | bool useLargeFonts; 97 | bool activateDisconnectIcon; 98 | 99 | // metric or imperial unit 100 | bool useMetric; 101 | 102 | // battery meter widget settings 103 | bool showBatteryPct; 104 | bool disableAutobattery; 105 | 106 | // alt tz widget settings 107 | char altclockName[8]; 108 | int altclockOffset; 109 | 110 | // health widget Settings 111 | ActivityDisplayType healthActivityDisplay; 112 | bool healthUseRestfulSleep; 113 | char decimalSeparator; 114 | 115 | // dynamic settings (calculated based the currently-selected widgets) 116 | bool disableWeather; 117 | bool updateScreenEverySecond; 118 | bool enableAutoBatteryWidget; 119 | bool enableBeats; 120 | bool enableAltTimeZone; 121 | 122 | // TODO: these shouldn't be dynamic 123 | GColor iconFillColor; 124 | GColor iconStrokeColor; 125 | } Settings; 126 | 127 | 128 | // !! all future settings should be added to the bottom of this structure 129 | // !! do not remove fields from this structure, it will lead to unexpected behaviour 130 | typedef struct { 131 | GColor timeColor; 132 | GColor timeBgColor; 133 | GColor sidebarColor; 134 | GColor sidebarTextColor; 135 | 136 | // general settings 137 | uint8_t languageId; 138 | uint8_t showLeadingZero:1; 139 | uint8_t clockFontId:7; 140 | 141 | // vibration settings 142 | uint8_t btVibe:1; 143 | int8_t hourlyVibe:7; 144 | 145 | // sidebar settings 146 | uint8_t widgets[4]; 147 | uint8_t useLargeFonts:1; 148 | 149 | // weather widget settings 150 | uint8_t useMetric:1; 151 | 152 | // battery meter widget settings 153 | uint8_t showBatteryPct:1; 154 | uint8_t disableAutobattery:1; 155 | 156 | // health widget Settings 157 | ActivityDisplayType healthActivityDisplay:2; 158 | uint8_t healthUseRestfulSleep:1; 159 | char decimalSeparator; 160 | 161 | // alt tz widget settings 162 | char altclockName[8]; 163 | int8_t altclockOffset; 164 | 165 | // sidebar location settings 166 | BarLocationType sidebarLocation:3; 167 | 168 | // bluetooth disconnection icon 169 | int8_t activateDisconnectIcon:1; 170 | 171 | int8_t centerTime:1; 172 | 173 | char languageDayNames[7][8]; 174 | char languageMonthNames[12][8]; 175 | char languageWordForWeek[12]; 176 | } StoredSettings; 177 | 178 | extern Settings globalSettings; 179 | 180 | // key for all the settings for versions 6 and higher 181 | #define SETTING_VERSION6_AND_HIGHER 100 182 | 183 | void Settings_init(void); 184 | void Settings_deinit(void); 185 | void Settings_saveToStorage(void); 186 | void Settings_updateDynamicSettings(void); 187 | -------------------------------------------------------------------------------- /src/c/sidebar.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "settings.h" 4 | #include "weather.h" 5 | #include "sidebar.h" 6 | #include "sidebar_widgets.h" 7 | #include "util.h" 8 | 9 | #define V_PADDING_DEFAULT 8 10 | #define V_PADDING_COMPACT 4 11 | 12 | #define H_PADDING_DEFAULT 4 13 | #define HORIZONTAL_BAR_HEIGHT FIXED_WIDGET_HEIGHT 14 | #define RECT_WIDGETS_XOFFSET ((ACTION_BAR_WIDTH - 30) / 2) 15 | 16 | static GRect screen_rect; 17 | static Layer* sidebarLayer; 18 | 19 | #ifdef PBL_ROUND 20 | static Layer* sidebarLayer2; 21 | #endif 22 | 23 | static bool isAutoBatteryShown(void) { 24 | if(!globalSettings.disableAutobattery) { 25 | BatteryChargeState chargeState = battery_state_service_peek(); 26 | 27 | if(globalSettings.enableAutoBatteryWidget) { 28 | if(chargeState.charge_percent <= 10 || chargeState.is_charging) { 29 | return true; 30 | } 31 | } 32 | } 33 | return false; 34 | } 35 | 36 | #ifdef PBL_ROUND 37 | // returns the best candidate widget for replacement by the auto battery 38 | // or the disconnection icon 39 | static int getReplacableWidget(void) { 40 | if(globalSettings.widgets[0] == EMPTY) { 41 | return 0; 42 | } else if(globalSettings.widgets[2] == EMPTY) { 43 | return 2; 44 | } 45 | 46 | if(globalSettings.widgets[0] == WEATHER_CURRENT || globalSettings.widgets[0] == WEATHER_FORECAST_TODAY) { 47 | return 0; 48 | } else if(globalSettings.widgets[2] == WEATHER_CURRENT || globalSettings.widgets[2] == WEATHER_FORECAST_TODAY) { 49 | return 2; 50 | } 51 | 52 | // if we don't have any of those things, just replace the left widget 53 | return 0; 54 | } 55 | #else 56 | // returns the best candidate widget for replacement by the auto battery 57 | // or the disconnection icon 58 | static int getReplacableWidget(void) { 59 | // if any widgets are empty, it's an obvious choice 60 | for(int i = 0; i < 3; i++) { 61 | if(globalSettings.widgets[i] == EMPTY) { 62 | return i; 63 | } 64 | } 65 | 66 | // use widget 4 only if bottom or top widget is used 67 | if(globalSettings.widgets[3] == EMPTY && 68 | (globalSettings.sidebarLocation == BOTTOM || globalSettings.sidebarLocation == TOP)) { 69 | return 3; 70 | } 71 | 72 | // are there any bluetooth-enabled widgets? if so, they're the second-best 73 | // candidates 74 | for(int i = 0; i < 4; i++) { 75 | if(globalSettings.widgets[i] == WEATHER_CURRENT || globalSettings.widgets[i] == WEATHER_FORECAST_TODAY) { 76 | return i; 77 | } 78 | } 79 | 80 | // if we don't have any of those things, just replace the middle widget 81 | return 1; 82 | } 83 | #endif 84 | 85 | #ifdef PBL_ROUND 86 | static SidebarWidget getRoundSidebarWidget(int widgetNumber) { 87 | bool showDisconnectIcon = !bluetooth_connection_service_peek(); 88 | bool showAutoBattery = isAutoBatteryShown(); 89 | 90 | SidebarWidgetType displayWidget = globalSettings.widgets[widgetNumber]; 91 | 92 | if((showAutoBattery || showDisconnectIcon) && getReplacableWidget() == widgetNumber) { 93 | if(showAutoBattery) { 94 | displayWidget = BATTERY_METER; 95 | } else if(showDisconnectIcon) { 96 | displayWidget = BLUETOOTH_DISCONNECT; 97 | } 98 | } 99 | 100 | return getSidebarWidgetByType(displayWidget); 101 | } 102 | 103 | static void drawRoundSidebar(GContext* ctx, GRect bgBounds, SidebarWidget widget, int widgetXPosition, int widgetYPosition, int widgetXOffset) { 104 | graphics_context_set_fill_color(ctx, globalSettings.sidebarColor); 105 | 106 | graphics_fill_radial(ctx, 107 | bgBounds, 108 | GOvalScaleModeFillCircle, 109 | 100, 110 | DEG_TO_TRIGANGLE(0), 111 | TRIG_MAX_ANGLE); 112 | 113 | graphics_context_set_text_color(ctx, globalSettings.sidebarTextColor); 114 | SidebarWidgets_xOffset = widgetXOffset; 115 | 116 | widget.draw(ctx, widgetXPosition, widgetYPosition); 117 | } 118 | 119 | static GRect getRoundSidebarBounds1(void) { 120 | if(globalSettings.sidebarLocation == RIGHT || globalSettings.sidebarLocation == LEFT) { 121 | return GRect(0, 0, 40, screen_rect.size.h); 122 | } else if(globalSettings.sidebarLocation == BOTTOM || globalSettings.sidebarLocation == TOP) { 123 | return GRect(0, 0, screen_rect.size.w, HORIZONTAL_BAR_HEIGHT ); 124 | }else { 125 | return GRect(0, 0, 0, 0); 126 | } 127 | } 128 | 129 | static GRect getRoundSidebarBounds2(void) { 130 | if(globalSettings.sidebarLocation == RIGHT || globalSettings.sidebarLocation == LEFT) { 131 | return GRect(screen_rect.size.w - 40, 0, 40, screen_rect.size.h); 132 | } else if(globalSettings.sidebarLocation == BOTTOM || globalSettings.sidebarLocation == TOP) { 133 | return GRect(0, screen_rect.size.h - HORIZONTAL_BAR_HEIGHT, screen_rect.size.w, HORIZONTAL_BAR_HEIGHT); 134 | }else { 135 | return GRect(0, 0, 0, 0); 136 | } 137 | } 138 | 139 | static void updateRoundSidebarRight(Layer *l, GContext* ctx) { 140 | GRect bounds = layer_get_bounds(l); 141 | GRect bgBounds = GRect(bounds.origin.x, bounds.size.h / -2, bounds.size.h * 2, bounds.size.h * 2); 142 | 143 | SidebarWidget widget = getRoundSidebarWidget(2); 144 | 145 | // calculate center position of the widget 146 | int widgetYPosition = bgBounds.size.h / 4 - widget.getHeight() / 2; 147 | 148 | drawRoundSidebar(ctx, bgBounds, widget, 0, widgetYPosition, 3); 149 | } 150 | 151 | static void updateRoundSidebarLeft(Layer *l, GContext* ctx) { 152 | GRect bounds = layer_get_bounds(l); 153 | GRect bgBounds = GRect(bounds.origin.x - bounds.size.h * 2 + bounds.size.w, bounds.size.h / -2, bounds.size.h * 2, bounds.size.h * 2); 154 | 155 | SidebarWidget widget = getRoundSidebarWidget(0); 156 | 157 | // calculate center position of the widget 158 | int widgetYPosition = bgBounds.size.h / 4 - widget.getHeight() / 2; 159 | 160 | drawRoundSidebar(ctx, bgBounds, widget, 0, widgetYPosition, 7); 161 | } 162 | 163 | static void updateRoundSidebarBottom(Layer *l, GContext* ctx) { 164 | GRect bounds = layer_get_bounds(l); 165 | GRect bgBounds = GRect(bounds.size.w / -2, bounds.origin.y, bounds.size.w * 2, bounds.size.w * 2); 166 | 167 | SidebarWidget widget = getRoundSidebarWidget(2); 168 | 169 | // use compact mode and fixed height for bottom and top widget 170 | SidebarWidgets_useCompactMode = true; 171 | SidebarWidgets_fixedHeight = true; 172 | 173 | // calculate center position of the widget 174 | int widgetXPosition = bgBounds.size.w / 4 - ACTION_BAR_WIDTH / 2; 175 | int widgetYPosition = (HORIZONTAL_BAR_HEIGHT - widget.getHeight()) / 2; 176 | 177 | drawRoundSidebar(ctx, bgBounds, widget, widgetXPosition, widgetYPosition, 5); 178 | } 179 | 180 | static void updateRoundSidebarTop(Layer *l, GContext* ctx) { 181 | GRect bounds = layer_get_bounds(l); 182 | GRect bgBounds = GRect(bounds.size.w / -2, bounds.origin.y - bounds.size.w * 2 + bounds.size.h, bounds.size.w * 2, bounds.size.w * 2); 183 | 184 | SidebarWidget widget = getRoundSidebarWidget(0); 185 | 186 | // use compact mode and fixed height for bottom and top widget 187 | SidebarWidgets_useCompactMode = true; 188 | SidebarWidgets_fixedHeight = true; 189 | 190 | // calculate center position of the widget 191 | int widgetXPosition = bgBounds.size.w / 4 - ACTION_BAR_WIDTH / 2; 192 | int widgetYPosition = (HORIZONTAL_BAR_HEIGHT - widget.getHeight()) / 2; 193 | 194 | drawRoundSidebar(ctx, bgBounds, widget, widgetXPosition, widgetYPosition, 5); 195 | } 196 | 197 | static void updateRoundSidebar1(Layer *l, GContext* ctx) { 198 | if(globalSettings.sidebarLocation == RIGHT || globalSettings.sidebarLocation == LEFT) { 199 | updateRoundSidebarLeft(l, ctx); 200 | } else if(globalSettings.sidebarLocation == BOTTOM || globalSettings.sidebarLocation == TOP) { 201 | updateRoundSidebarTop(l, ctx); 202 | } 203 | } 204 | 205 | static void updateRoundSidebar2(Layer *l, GContext* ctx) { 206 | if(globalSettings.sidebarLocation == RIGHT || globalSettings.sidebarLocation == LEFT) { 207 | updateRoundSidebarRight(l, ctx); 208 | } else if(globalSettings.sidebarLocation == BOTTOM || globalSettings.sidebarLocation == TOP) { 209 | updateRoundSidebarBottom(l, ctx); 210 | } 211 | } 212 | 213 | #else 214 | 215 | static GRect getRectSidebarBounds(void) { 216 | if(globalSettings.sidebarLocation == RIGHT) { 217 | return GRect(screen_rect.size.w - ACTION_BAR_WIDTH, 0, ACTION_BAR_WIDTH, screen_rect.size.h); 218 | } else if(globalSettings.sidebarLocation == LEFT) { 219 | return GRect(0, 0, ACTION_BAR_WIDTH, screen_rect.size.h); 220 | } else if(globalSettings.sidebarLocation == BOTTOM) { 221 | return GRect(0, screen_rect.size.h - HORIZONTAL_BAR_HEIGHT, screen_rect.size.w, HORIZONTAL_BAR_HEIGHT); 222 | } else if(globalSettings.sidebarLocation == TOP) { 223 | return GRect(0, 0, screen_rect.size.w, HORIZONTAL_BAR_HEIGHT); 224 | }else { 225 | return GRect(0, 0, 0, 0); 226 | } 227 | } 228 | 229 | static void updateRectSidebar(Layer *l, GContext* ctx) { 230 | GRect bounds = layer_get_bounds(l); 231 | 232 | // this ends up being zero on every rectangular platform besides emery 233 | SidebarWidgets_xOffset = RECT_WIDGETS_XOFFSET; 234 | 235 | graphics_context_set_fill_color(ctx, globalSettings.sidebarColor); 236 | graphics_fill_rect(ctx, bounds, 0, GCornerNone); 237 | 238 | graphics_context_set_text_color(ctx, globalSettings.sidebarTextColor); 239 | 240 | // if the pebble is disconnected, show the disconnect icon 241 | bool showDisconnectIcon = false; 242 | bool showAutoBattery = isAutoBatteryShown(); 243 | int widget_to_replace = -1; 244 | 245 | // if the pebble is disconnected and activated, show the disconnect icon 246 | if(globalSettings.activateDisconnectIcon) { 247 | showDisconnectIcon = !bluetooth_connection_service_peek(); 248 | } 249 | 250 | SidebarWidget displayWidgets[4]; 251 | 252 | displayWidgets[0] = getSidebarWidgetByType(globalSettings.widgets[0]); 253 | displayWidgets[1] = getSidebarWidgetByType(globalSettings.widgets[1]); 254 | displayWidgets[2] = getSidebarWidgetByType(globalSettings.widgets[2]); 255 | if(globalSettings.sidebarLocation == BOTTOM || globalSettings.sidebarLocation == TOP) { 256 | displayWidgets[3] = getSidebarWidgetByType(globalSettings.widgets[3]); 257 | } 258 | 259 | // do we need to replace a widget? 260 | // if so, determine which widget should be replaced 261 | if(showAutoBattery || showDisconnectIcon) { 262 | widget_to_replace = getReplacableWidget(); 263 | 264 | if(showAutoBattery) { 265 | displayWidgets[widget_to_replace] = getSidebarWidgetByType(BATTERY_METER); 266 | } else { // showDisconnectIcon 267 | displayWidgets[widget_to_replace] = getSidebarWidgetByType(BLUETOOTH_DISCONNECT); 268 | } 269 | } 270 | 271 | int v_padding; 272 | int middleWidgetPos; 273 | 274 | if(globalSettings.sidebarLocation == BOTTOM || globalSettings.sidebarLocation == TOP) { 275 | // calculate the three horizontal widget positions 276 | middleWidgetPos = (bounds.size.w - ACTION_BAR_WIDTH) / 2; 277 | int rightWidgetPos = bounds.size.w - H_PADDING_DEFAULT - ACTION_BAR_WIDTH; 278 | 279 | // use compact mode and fixed height for bottom and top widget 280 | SidebarWidgets_useCompactMode = true; 281 | SidebarWidgets_fixedHeight = true; 282 | 283 | // draw the widgets 284 | v_padding= (HORIZONTAL_BAR_HEIGHT - displayWidgets[0].getHeight()) / 2; 285 | displayWidgets[0].draw(ctx, H_PADDING_DEFAULT, v_padding); 286 | 287 | if(globalSettings.widgets[3] == EMPTY && widget_to_replace != 3) { 288 | v_padding = (HORIZONTAL_BAR_HEIGHT - displayWidgets[1].getHeight()) / 2; 289 | displayWidgets[1].draw(ctx, middleWidgetPos, v_padding); 290 | 291 | v_padding = (HORIZONTAL_BAR_HEIGHT - displayWidgets[2].getHeight()) / 2; 292 | displayWidgets[2].draw(ctx, rightWidgetPos, v_padding); 293 | }else if(globalSettings.widgets[2] == EMPTY) { 294 | v_padding = (HORIZONTAL_BAR_HEIGHT - displayWidgets[1].getHeight()) / 2; 295 | displayWidgets[1].draw(ctx, middleWidgetPos, v_padding); 296 | 297 | v_padding = (HORIZONTAL_BAR_HEIGHT - displayWidgets[3].getHeight()) / 2; 298 | displayWidgets[3].draw(ctx, rightWidgetPos, v_padding); 299 | }else if(globalSettings.widgets[1] == EMPTY) { 300 | v_padding = (HORIZONTAL_BAR_HEIGHT - displayWidgets[2].getHeight()) / 2; 301 | displayWidgets[2].draw(ctx, middleWidgetPos, v_padding); 302 | 303 | v_padding = (HORIZONTAL_BAR_HEIGHT - displayWidgets[3].getHeight()) / 2; 304 | displayWidgets[3].draw(ctx, rightWidgetPos, v_padding); 305 | } else { // we have 4 widgets 306 | 307 | // middle position 1 308 | middleWidgetPos = (bounds.size.w - 5 * H_PADDING_DEFAULT) / 4 + 2 * H_PADDING_DEFAULT; 309 | v_padding = (HORIZONTAL_BAR_HEIGHT - displayWidgets[1].getHeight()) / 2; 310 | displayWidgets[1].draw(ctx, middleWidgetPos, v_padding); 311 | 312 | // middle position 2 313 | middleWidgetPos = (bounds.size.w - 5 * H_PADDING_DEFAULT) / 2 + 3 * H_PADDING_DEFAULT; 314 | v_padding = (HORIZONTAL_BAR_HEIGHT - displayWidgets[2].getHeight()) / 2; 315 | displayWidgets[2].draw(ctx, middleWidgetPos, v_padding); 316 | 317 | v_padding = (HORIZONTAL_BAR_HEIGHT - displayWidgets[3].getHeight()) / 2; 318 | displayWidgets[3].draw(ctx, rightWidgetPos, v_padding); 319 | } 320 | } else if(globalSettings.sidebarLocation == LEFT || globalSettings.sidebarLocation == RIGHT) { 321 | GRect unobstructed_bounds = layer_get_unobstructed_bounds(l); 322 | 323 | // if the widgets are too tall, enable "compact mode" 324 | int compact_mode_threshold = unobstructed_bounds.size.h - V_PADDING_DEFAULT * 2 - 3; 325 | v_padding = V_PADDING_DEFAULT; 326 | 327 | SidebarWidgets_useCompactMode = false; // ensure that we compare the non-compacted heights 328 | SidebarWidgets_fixedHeight = false; 329 | int totalHeight = displayWidgets[0].getHeight() + displayWidgets[1].getHeight() + displayWidgets[2].getHeight(); 330 | SidebarWidgets_useCompactMode = (totalHeight > compact_mode_threshold); 331 | // printf("Total Height: %i, Threshold: %i", totalHeight, compact_mode_threshold); 332 | 333 | // now that they have been compacted, check if they fit a second time, 334 | // if they still don't fit, we can reduce padding 335 | totalHeight = displayWidgets[0].getHeight() + displayWidgets[1].getHeight() + displayWidgets[2].getHeight(); 336 | 337 | if(totalHeight > compact_mode_threshold) { 338 | v_padding = V_PADDING_COMPACT; 339 | } 340 | 341 | // draw the widgets 342 | int lowerWidgetPos = unobstructed_bounds.size.h - v_padding - displayWidgets[2].getHeight(); 343 | displayWidgets[0].draw(ctx, 0, v_padding); 344 | 345 | // vertically center the middle widget using MATH 346 | middleWidgetPos = ((lowerWidgetPos - displayWidgets[1].getHeight()) + (v_padding + displayWidgets[0].getHeight())) / 2; 347 | displayWidgets[1].draw(ctx, 0, middleWidgetPos); 348 | 349 | displayWidgets[2].draw(ctx, 0, lowerWidgetPos); 350 | } 351 | } 352 | #endif 353 | 354 | void Sidebar_init(Window* window) { 355 | // init the sidebar layer 356 | screen_rect = layer_get_bounds(window_get_root_layer(window)); 357 | GRect bounds; 358 | 359 | #ifdef PBL_ROUND 360 | GRect bounds2; 361 | 362 | bounds = getRoundSidebarBounds1(); 363 | bounds2 = getRoundSidebarBounds2(); 364 | #else 365 | bounds = getRectSidebarBounds(); 366 | #endif 367 | 368 | // init the widgets 369 | SidebarWidgets_init(); 370 | 371 | sidebarLayer = layer_create(bounds); 372 | layer_add_child(window_get_root_layer(window), sidebarLayer); 373 | 374 | #ifdef PBL_ROUND 375 | sidebarLayer2 = layer_create(bounds2); 376 | layer_add_child(window_get_root_layer(window), sidebarLayer2); 377 | 378 | layer_set_update_proc(sidebarLayer, updateRoundSidebar1); 379 | layer_set_update_proc(sidebarLayer2, updateRoundSidebar2); 380 | #else 381 | layer_set_update_proc(sidebarLayer, updateRectSidebar); 382 | #endif 383 | } 384 | 385 | void Sidebar_deinit(void) { 386 | layer_destroy(sidebarLayer); 387 | 388 | #ifdef PBL_ROUND 389 | layer_destroy(sidebarLayer2); 390 | #endif 391 | 392 | SidebarWidgets_deinit(); 393 | } 394 | 395 | void Sidebar_set_layer(void) { 396 | #ifdef PBL_ROUND 397 | // reposition the sidebar if needed 398 | layer_set_frame(sidebarLayer, getRoundSidebarBounds1()); 399 | layer_set_frame(sidebarLayer2, getRoundSidebarBounds2()); 400 | 401 | if(globalSettings.sidebarLocation == NONE) { 402 | layer_set_hidden(sidebarLayer, true); 403 | layer_set_hidden(sidebarLayer2, true); 404 | } else { 405 | layer_set_hidden(sidebarLayer, false); 406 | layer_set_hidden(sidebarLayer2, false); 407 | } 408 | #else 409 | // reposition the sidebar if needed 410 | layer_set_frame(sidebarLayer, getRectSidebarBounds()); 411 | 412 | if(globalSettings.sidebarLocation == NONE) { 413 | layer_set_hidden(sidebarLayer, true); 414 | } else { 415 | layer_set_hidden(sidebarLayer, false); 416 | } 417 | #endif 418 | 419 | SidebarWidgets_updateFonts(); 420 | } 421 | 422 | void Sidebar_redraw(void) { 423 | // redraw the layer 424 | layer_mark_dirty(sidebarLayer); 425 | 426 | #ifdef PBL_ROUND 427 | layer_mark_dirty(sidebarLayer2); 428 | #endif 429 | } 430 | 431 | #ifndef PBL_ROUND 432 | void Sidebar_set_hidden(bool hide) { 433 | layer_set_hidden(sidebarLayer, hide); 434 | } 435 | #endif 436 | -------------------------------------------------------------------------------- /src/c/sidebar.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | // "public" functions 5 | void Sidebar_init(Window* window); 6 | void Sidebar_deinit(void); 7 | void Sidebar_set_layer(void); 8 | void Sidebar_redraw(void); 9 | #ifndef PBL_ROUND 10 | void Sidebar_set_hidden(bool hide); 11 | #endif 12 | -------------------------------------------------------------------------------- /src/c/sidebar_widgets.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "settings.h" 4 | #include "weather.h" 5 | #include "util.h" 6 | #ifdef PBL_HEALTH 7 | #include "health.h" 8 | #endif 9 | #include "time_date.h" 10 | #include "sidebar_widgets.h" 11 | 12 | 13 | bool SidebarWidgets_useCompactMode = false; 14 | bool SidebarWidgets_fixedHeight = false; 15 | int SidebarWidgets_xOffset; 16 | 17 | // sidebar icons 18 | static GDrawCommandImage* dateImage; 19 | static GDrawCommandImage* disconnectImage; 20 | static GDrawCommandImage* batteryImage; 21 | static GDrawCommandImage* batteryChargeImage; 22 | 23 | // fonts 24 | static GFont smSidebarFont; 25 | static GFont mdSidebarFont; 26 | static GFont lgSidebarFont; 27 | static GFont currentSidebarFont; 28 | static GFont currentSidebarSmallFont; 29 | 30 | // the widgets 31 | static SidebarWidget batteryMeterWidget; 32 | static int BatteryMeter_getHeight(void); 33 | static void BatteryMeter_draw(GContext* ctx, int xPosition, int yPosition); 34 | 35 | static SidebarWidget emptyWidget; 36 | static int EmptyWidget_getHeight(void); 37 | static void EmptyWidget_draw(GContext* ctx, int xPosition, int yPosition); 38 | 39 | static SidebarWidget dateWidget; 40 | static int DateWidget_getHeight(void); 41 | static void DateWidget_draw(GContext* ctx, int xPosition, int yPosition); 42 | 43 | static SidebarWidget currentWeatherWidget; 44 | static int CurrentWeather_getHeight(void); 45 | static void CurrentWeather_draw(GContext* ctx, int xPosition, int yPosition); 46 | 47 | static SidebarWidget weatherForecastWidget; 48 | static int WeatherForecast_getHeight(void); 49 | static void WeatherForecast_draw(GContext* ctx, int xPosition, int yPosition); 50 | 51 | static SidebarWidget btDisconnectWidget; 52 | static int BTDisconnect_getHeight(void); 53 | static void BTDisconnect_draw(GContext* ctx, int xPosition, int yPosition); 54 | 55 | static SidebarWidget weekNumberWidget; 56 | static int WeekNumber_getHeight(void); 57 | static void WeekNumber_draw(GContext* ctx, int xPosition, int yPosition); 58 | 59 | static SidebarWidget secondsWidget; 60 | static int Seconds_getHeight(void); 61 | static void Seconds_draw(GContext* ctx, int xPosition, int yPosition); 62 | 63 | static SidebarWidget altTimeWidget; 64 | static int AltTime_getHeight(void); 65 | static void AltTime_draw(GContext* ctx, int xPosition, int yPosition); 66 | 67 | static SidebarWidget beatsWidget; 68 | static int Beats_getHeight(void); 69 | static void Beats_draw(GContext* ctx, int xPosition, int yPosition); 70 | 71 | #ifdef PBL_HEALTH 72 | static GDrawCommandImage* sleepImage; 73 | static GDrawCommandImage* stepsImage; 74 | static GDrawCommandImage* heartImage; 75 | 76 | static SidebarWidget healthWidget; 77 | static int Health_getHeight(void); 78 | static void Health_draw(GContext* ctx, int xPosition, int yPosition); 79 | static SidebarWidget sleepWidget; 80 | static int Sleep_getHeight(void); 81 | static void Sleep_draw(GContext* ctx, int xPosition, int yPosition); 82 | static SidebarWidget stepsWidget; 83 | static int Steps_getHeight(void); 84 | static void Steps_draw(GContext* ctx, int xPosition, int yPosition); 85 | 86 | static SidebarWidget heartRateWidget; 87 | static int HeartRate_getHeight(void); 88 | static void HeartRate_draw(GContext* ctx, int xPosition, int yPosition); 89 | #endif 90 | 91 | void SidebarWidgets_init(void) { 92 | // load fonts 93 | smSidebarFont = fonts_get_system_font(FONT_KEY_GOTHIC_14_BOLD); 94 | mdSidebarFont = fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD); 95 | lgSidebarFont = fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD); 96 | 97 | // load the sidebar graphics 98 | dateImage = gdraw_command_image_create_with_resource(RESOURCE_ID_DATE_BG); 99 | disconnectImage = gdraw_command_image_create_with_resource(RESOURCE_ID_DISCONNECTED); 100 | batteryImage = gdraw_command_image_create_with_resource(RESOURCE_ID_BATTERY_BG); 101 | batteryChargeImage = gdraw_command_image_create_with_resource(RESOURCE_ID_BATTERY_CHARGE); 102 | 103 | #ifdef PBL_HEALTH 104 | sleepImage = gdraw_command_image_create_with_resource(RESOURCE_ID_HEALTH_SLEEP); 105 | stepsImage = gdraw_command_image_create_with_resource(RESOURCE_ID_HEALTH_STEPS); 106 | heartImage = gdraw_command_image_create_with_resource(RESOURCE_ID_HEALTH_HEART); 107 | #endif 108 | 109 | // set up widgets' function pointers correctly 110 | batteryMeterWidget.getHeight = BatteryMeter_getHeight; 111 | batteryMeterWidget.draw = BatteryMeter_draw; 112 | 113 | emptyWidget.getHeight = EmptyWidget_getHeight; 114 | emptyWidget.draw = EmptyWidget_draw; 115 | 116 | dateWidget.getHeight = DateWidget_getHeight; 117 | dateWidget.draw = DateWidget_draw; 118 | 119 | currentWeatherWidget.getHeight = CurrentWeather_getHeight; 120 | currentWeatherWidget.draw = CurrentWeather_draw; 121 | 122 | weatherForecastWidget.getHeight = WeatherForecast_getHeight; 123 | weatherForecastWidget.draw = WeatherForecast_draw; 124 | 125 | btDisconnectWidget.getHeight = BTDisconnect_getHeight; 126 | btDisconnectWidget.draw = BTDisconnect_draw; 127 | 128 | weekNumberWidget.getHeight = WeekNumber_getHeight; 129 | weekNumberWidget.draw = WeekNumber_draw; 130 | 131 | secondsWidget.getHeight = Seconds_getHeight; 132 | secondsWidget.draw = Seconds_draw; 133 | 134 | altTimeWidget.getHeight = AltTime_getHeight; 135 | altTimeWidget.draw = AltTime_draw; 136 | 137 | #ifdef PBL_HEALTH 138 | healthWidget.getHeight = Health_getHeight; 139 | healthWidget.draw = Health_draw; 140 | 141 | sleepWidget.getHeight = Sleep_getHeight; 142 | sleepWidget.draw = Sleep_draw; 143 | 144 | stepsWidget.getHeight = Steps_getHeight; 145 | stepsWidget.draw = Steps_draw; 146 | 147 | heartRateWidget.getHeight = HeartRate_getHeight; 148 | heartRateWidget.draw = HeartRate_draw; 149 | #endif 150 | 151 | beatsWidget.getHeight = Beats_getHeight; 152 | beatsWidget.draw = Beats_draw; 153 | 154 | } 155 | 156 | void SidebarWidgets_deinit(void) { 157 | gdraw_command_image_destroy(dateImage); 158 | gdraw_command_image_destroy(disconnectImage); 159 | gdraw_command_image_destroy(batteryImage); 160 | gdraw_command_image_destroy(batteryChargeImage); 161 | 162 | #ifdef PBL_HEALTH 163 | gdraw_command_image_destroy(stepsImage); 164 | gdraw_command_image_destroy(sleepImage); 165 | gdraw_command_image_destroy(heartImage); 166 | #endif 167 | } 168 | 169 | void SidebarWidgets_updateFonts(void) { 170 | if(globalSettings.useLargeFonts) { 171 | currentSidebarFont = lgSidebarFont; 172 | currentSidebarSmallFont = mdSidebarFont; 173 | } else { 174 | currentSidebarFont = mdSidebarFont; 175 | currentSidebarSmallFont = smSidebarFont; 176 | } 177 | } 178 | 179 | /* Sidebar Widget Selection */ 180 | SidebarWidget getSidebarWidgetByType(SidebarWidgetType type) { 181 | switch(type) { 182 | case BATTERY_METER: 183 | return batteryMeterWidget; 184 | break; 185 | case BLUETOOTH_DISCONNECT: 186 | return btDisconnectWidget; 187 | break; 188 | case DATE: 189 | return dateWidget; 190 | break; 191 | case ALT_TIME_ZONE: 192 | return altTimeWidget; 193 | break; 194 | case SECONDS: 195 | return secondsWidget; 196 | break; 197 | case WEATHER_CURRENT: 198 | return currentWeatherWidget; 199 | break; 200 | case WEATHER_FORECAST_TODAY: 201 | return weatherForecastWidget; 202 | break; 203 | case WEEK_NUMBER: 204 | return weekNumberWidget; 205 | #ifdef PBL_HEALTH 206 | case HEALTH: 207 | return healthWidget; 208 | case SLEEP: 209 | return sleepWidget; 210 | case STEP: 211 | return stepsWidget; 212 | case HEARTRATE: 213 | return heartRateWidget; 214 | #endif 215 | case BEATS: 216 | return beatsWidget; 217 | default: 218 | return emptyWidget; 219 | break; 220 | } 221 | } 222 | 223 | /********** functions for the empty widget **********/ 224 | static int EmptyWidget_getHeight(void) { 225 | return 0; 226 | } 227 | 228 | static void EmptyWidget_draw(GContext* ctx, int xPosition, int yPosition) { 229 | return; 230 | } 231 | 232 | /********** functions for the battery meter widget **********/ 233 | 234 | static int BatteryMeter_getHeight(void) { 235 | BatteryChargeState chargeState = battery_state_service_peek(); 236 | 237 | if(SidebarWidgets_fixedHeight) { 238 | return FIXED_WIDGET_HEIGHT; 239 | } else if(chargeState.is_charging || !globalSettings.showBatteryPct) { 240 | return 14; // graphic only height 241 | } else { 242 | return (globalSettings.useLargeFonts) ? 33 : 27; // heights with text 243 | } 244 | } 245 | 246 | static void BatteryMeter_draw(GContext* ctx, int xPosition, int yPosition) { 247 | 248 | BatteryChargeState chargeState = battery_state_service_peek(); 249 | uint8_t battery_percent = (chargeState.charge_percent > 0) ? chargeState.charge_percent : 5; 250 | 251 | char batteryString[6]; 252 | int batteryPositionY = yPosition; 253 | 254 | if(SidebarWidgets_fixedHeight){ 255 | if(!globalSettings.showBatteryPct || chargeState.is_charging) { 256 | batteryPositionY += (FIXED_WIDGET_HEIGHT / 2) - 12; 257 | } else { 258 | batteryPositionY += 3; 259 | } 260 | } else { 261 | // correct for vertical empty space on battery icon 262 | batteryPositionY -= 5; 263 | } 264 | 265 | if (batteryImage) { 266 | util_image_draw(ctx, batteryImage, xPosition + 3 + SidebarWidgets_xOffset, batteryPositionY); 267 | } 268 | 269 | if(chargeState.is_charging) { 270 | if(batteryChargeImage) { 271 | // the charge "bolt" icon uses inverted colors 272 | util_image_draw_inverted_color(ctx, batteryChargeImage, xPosition + 3 + SidebarWidgets_xOffset, batteryPositionY); 273 | } 274 | } else { 275 | 276 | int width = roundf(18 * battery_percent / 100.0f); 277 | 278 | graphics_context_set_fill_color(ctx, globalSettings.iconStrokeColor); 279 | 280 | #ifdef PBL_COLOR 281 | if(battery_percent <= 20) { 282 | graphics_context_set_fill_color(ctx, GColorRed); 283 | } 284 | #endif 285 | 286 | graphics_fill_rect(ctx, GRect(xPosition + 6 + SidebarWidgets_xOffset, 8 + batteryPositionY, width, 8), 0, GCornerNone); 287 | } 288 | 289 | // never show battery % while charging, because of this issue: 290 | // https://github.com/freakified/TimeStylePebble/issues/11 291 | if(globalSettings.showBatteryPct && !chargeState.is_charging) { 292 | int textOffsetY; 293 | GFont batteryFont; 294 | 295 | if(!globalSettings.useLargeFonts) { 296 | batteryFont = smSidebarFont; 297 | if(SidebarWidgets_fixedHeight) { 298 | textOffsetY = 25; 299 | } else { 300 | textOffsetY = 18; 301 | } 302 | 303 | // put the percent sign on the opposite side if turkish 304 | snprintf(batteryString, sizeof(batteryString), 305 | (globalSettings.languageId == LANGUAGE_TR) ? "%%%d" : "%d%%", 306 | battery_percent); 307 | } else { 308 | batteryFont = lgSidebarFont; 309 | if(SidebarWidgets_fixedHeight) { 310 | textOffsetY = 18; 311 | } else { 312 | textOffsetY = 14; 313 | } 314 | 315 | snprintf(batteryString, sizeof(batteryString), "%d", battery_percent); 316 | } 317 | graphics_draw_text(ctx, 318 | batteryString, 319 | batteryFont, 320 | GRect(xPosition - 4 + SidebarWidgets_xOffset, textOffsetY + batteryPositionY, 38, 20), 321 | GTextOverflowModeFill, 322 | GTextAlignmentCenter, 323 | NULL); 324 | } 325 | } 326 | 327 | /********** current date widget **********/ 328 | 329 | static int DateWidget_getHeight(void) { 330 | if(globalSettings.useLargeFonts) { 331 | return (SidebarWidgets_useCompactMode) ? 42 : 62; 332 | } else { 333 | return (SidebarWidgets_useCompactMode) ? 41 : 58; 334 | } 335 | } 336 | 337 | static void DateWidget_draw(GContext* ctx, int xPosition, int yPosition) { 338 | // compensate for extra space that appears on the top of the date widget 339 | yPosition -= (globalSettings.useLargeFonts) ? 10 : 7; 340 | 341 | // first draw the day name 342 | graphics_draw_text(ctx, 343 | globalSettings.languageDayNames[time_date_currentDayName], 344 | currentSidebarFont, 345 | GRect(xPosition - 5 + SidebarWidgets_xOffset, yPosition, 40, 20), 346 | GTextOverflowModeFill, 347 | GTextAlignmentCenter, 348 | NULL); 349 | 350 | // next, draw the date background 351 | // (an image in normal mode, a rectangle in large font mode) 352 | if(!globalSettings.useLargeFonts) { 353 | if(dateImage) { 354 | util_image_draw(ctx, dateImage, xPosition + 3 + SidebarWidgets_xOffset, yPosition + 23); 355 | } 356 | } else { 357 | graphics_context_set_fill_color(ctx, globalSettings.iconStrokeColor); 358 | graphics_fill_rect(ctx, GRect(xPosition + 2 + SidebarWidgets_xOffset, yPosition + 30, 26, 22), 2, GCornersAll); 359 | 360 | graphics_context_set_fill_color(ctx, globalSettings.iconFillColor); 361 | graphics_fill_rect(ctx, GRect(xPosition + 4 + SidebarWidgets_xOffset, yPosition + 32, 22, 18), 0, GCornersAll); 362 | } 363 | 364 | // next, draw the date number 365 | graphics_context_set_text_color(ctx, globalSettings.iconStrokeColor); 366 | 367 | int yOffset = 0; 368 | yOffset = globalSettings.useLargeFonts ? 24 : 26; 369 | 370 | graphics_draw_text(ctx, 371 | time_date_currentDayNum, 372 | currentSidebarFont, 373 | GRect(xPosition - 5 + SidebarWidgets_xOffset, yPosition + yOffset, 40, 20), 374 | GTextOverflowModeFill, 375 | GTextAlignmentCenter, 376 | NULL); 377 | 378 | 379 | // switch back to normal color for the rest 380 | graphics_context_set_text_color(ctx, globalSettings.sidebarTextColor); 381 | 382 | // don't draw the month if we're in compact mode 383 | if(!SidebarWidgets_useCompactMode) { 384 | yOffset = globalSettings.useLargeFonts ? 48 : 47; 385 | 386 | graphics_draw_text(ctx, 387 | globalSettings.languageMonthNames[time_date_currentMonth], 388 | currentSidebarFont, 389 | GRect(xPosition - 5 + SidebarWidgets_xOffset, yPosition + yOffset, 40, 20), 390 | GTextOverflowModeFill, 391 | GTextAlignmentCenter, 392 | NULL); 393 | } 394 | 395 | 396 | } 397 | 398 | /********** current weather widget **********/ 399 | 400 | static int CurrentWeather_getHeight(void) { 401 | if(globalSettings.useLargeFonts) { 402 | return 44; 403 | } else { 404 | return 42; 405 | } 406 | } 407 | 408 | static void CurrentWeather_draw(GContext* ctx, int xPosition, int yPosition) { 409 | if (Weather_currentWeatherIcon) { 410 | util_image_draw(ctx, Weather_currentWeatherIcon, xPosition + 3 + SidebarWidgets_xOffset, yPosition); 411 | } 412 | 413 | // draw weather data only if it has been set 414 | if(Weather_weatherInfo.currentTemp != INT32_MIN) { 415 | 416 | int currentTemp = Weather_weatherInfo.currentTemp; 417 | 418 | if(!globalSettings.useMetric) { 419 | currentTemp = roundf(currentTemp * 1.8f + 32); 420 | } 421 | 422 | char tempString[8]; 423 | 424 | // in large font mode, omit the degree symbol and move the text 425 | if(!globalSettings.useLargeFonts) { 426 | snprintf(tempString, sizeof(tempString), " %d°", currentTemp); 427 | 428 | graphics_draw_text(ctx, 429 | tempString, 430 | currentSidebarFont, 431 | GRect(xPosition - 5 + SidebarWidgets_xOffset, yPosition + 24, 38, 20), 432 | GTextOverflowModeFill, 433 | GTextAlignmentCenter, 434 | NULL); 435 | } else { 436 | snprintf(tempString, sizeof(tempString), " %d", currentTemp); 437 | 438 | graphics_draw_text(ctx, 439 | tempString, 440 | currentSidebarFont, 441 | GRect(xPosition - 5 + SidebarWidgets_xOffset, yPosition + 20, 35, 20), 442 | GTextOverflowModeFill, 443 | GTextAlignmentCenter, 444 | NULL); 445 | } 446 | } else { 447 | // if the weather data isn't set, draw a loading indication 448 | graphics_draw_text(ctx, 449 | "...", 450 | currentSidebarFont, 451 | GRect(xPosition - 5 + SidebarWidgets_xOffset, yPosition, 38, 20), 452 | GTextOverflowModeFill, 453 | GTextAlignmentCenter, 454 | NULL); 455 | } 456 | } 457 | 458 | /***** Bluetooth Disconnection Widget *****/ 459 | 460 | static int BTDisconnect_getHeight(void) { 461 | return 22; 462 | } 463 | 464 | static void BTDisconnect_draw(GContext* ctx, int xPosition, int yPosition) { 465 | if(disconnectImage) { 466 | util_image_draw(ctx, disconnectImage, xPosition + 3 + SidebarWidgets_xOffset, yPosition); 467 | } 468 | } 469 | 470 | /***** Week Number Widget *****/ 471 | 472 | static int WeekNumber_getHeight(void) { 473 | if(SidebarWidgets_fixedHeight) { 474 | return FIXED_WIDGET_HEIGHT; 475 | } else { 476 | return (globalSettings.useLargeFonts) ? 31 : 26; 477 | } 478 | } 479 | 480 | static void WeekNumber_draw(GContext* ctx, int xPosition, int yPosition) { 481 | int yTextPosition = SidebarWidgets_fixedHeight ? yPosition + 6 : yPosition - 4; 482 | yTextPosition = globalSettings.useLargeFonts ? yTextPosition - 2 : yTextPosition; 483 | 484 | // note that it draws "above" the y position to correct for 485 | // the vertical padding 486 | graphics_draw_text(ctx, 487 | globalSettings.languageWordForWeek, 488 | currentSidebarSmallFont, 489 | GRect(xPosition - 4 + SidebarWidgets_xOffset, yTextPosition, 38, 20), 490 | GTextOverflowModeFill, 491 | GTextAlignmentCenter, 492 | NULL); 493 | 494 | yTextPosition = SidebarWidgets_fixedHeight ? yPosition + 15 : yPosition; 495 | yTextPosition = globalSettings.useLargeFonts ? yTextPosition + 6 : yTextPosition + 9; 496 | 497 | graphics_draw_text(ctx, 498 | time_date_currentWeekNum, 499 | currentSidebarFont, 500 | GRect(xPosition + SidebarWidgets_xOffset, yTextPosition, 30, 20), 501 | GTextOverflowModeFill, 502 | GTextAlignmentCenter, 503 | NULL); 504 | } 505 | 506 | /***** Seconds Widget *****/ 507 | 508 | static int Seconds_getHeight(void) { 509 | return 14; 510 | } 511 | 512 | static void Seconds_draw(GContext* ctx, int xPosition, int yPosition) { 513 | graphics_draw_text(ctx, 514 | time_date_currentSecondsNum, 515 | lgSidebarFont, 516 | GRect(xPosition + SidebarWidgets_xOffset, yPosition - 10, 30, 20), 517 | GTextOverflowModeFill, 518 | GTextAlignmentCenter, 519 | NULL); 520 | } 521 | 522 | /***** Weather Forecast Widget *****/ 523 | 524 | static int WeatherForecast_getHeight(void) { 525 | if(SidebarWidgets_fixedHeight) { 526 | return FIXED_WIDGET_HEIGHT; 527 | } else { 528 | return (globalSettings.useLargeFonts) ? 63 : 60; 529 | } 530 | } 531 | 532 | static void WeatherForecast_draw(GContext* ctx, int xPosition, int yPosition) { 533 | //srand(time(NULL)); 534 | //Weather_setForecastCondition(rand() % 12); 535 | 536 | if(Weather_forecastWeatherIcon) { 537 | util_image_draw(ctx, Weather_forecastWeatherIcon, xPosition + 3 + SidebarWidgets_xOffset, yPosition + 1); 538 | } 539 | 540 | // draw weather data only if it has been set 541 | if(Weather_weatherForecast.highTemp != INT32_MIN) { 542 | 543 | int highTemp = Weather_weatherForecast.highTemp; 544 | int lowTemp = Weather_weatherForecast.lowTemp; 545 | 546 | if(!globalSettings.useMetric) { 547 | highTemp = roundf(highTemp * 1.8f + 32); 548 | lowTemp = roundf(lowTemp * 1.8f + 32); 549 | } 550 | 551 | char tempString[8]; 552 | 553 | graphics_context_set_fill_color(ctx, globalSettings.sidebarTextColor); 554 | 555 | // in large font mode, omit the degree symbol and move the text 556 | if(!globalSettings.useLargeFonts) { 557 | snprintf(tempString, sizeof(tempString), " %d°", highTemp); 558 | 559 | graphics_draw_text(ctx, 560 | tempString, 561 | smSidebarFont, 562 | GRect(xPosition - 5 + SidebarWidgets_xOffset, yPosition + 23, 38, 20), 563 | GTextOverflowModeFill, 564 | GTextAlignmentCenter, 565 | NULL); 566 | 567 | graphics_fill_rect(ctx, GRect(xPosition + 6 + SidebarWidgets_xOffset, 8 + yPosition + 30, 18, 1), 0, GCornerNone); 568 | 569 | snprintf(tempString, sizeof(tempString), " %d°", lowTemp); 570 | 571 | graphics_draw_text(ctx, 572 | tempString, 573 | smSidebarFont, 574 | GRect(xPosition - 5 + SidebarWidgets_xOffset, yPosition + 35, 38, 20), 575 | GTextOverflowModeFill, 576 | GTextAlignmentCenter, 577 | NULL); 578 | } else { 579 | snprintf(tempString, sizeof(tempString), "%d", highTemp); 580 | 581 | graphics_draw_text(ctx, 582 | tempString, 583 | smSidebarFont, 584 | GRect(xPosition + SidebarWidgets_xOffset, yPosition + 23, 30, 20), 585 | GTextOverflowModeFill, 586 | GTextAlignmentCenter, 587 | NULL); 588 | 589 | graphics_fill_rect(ctx, GRect(xPosition + 6 + SidebarWidgets_xOffset, 8 + yPosition + 30, 18, 1), 0, GCornerNone); 590 | 591 | snprintf(tempString, sizeof(tempString), "%d", lowTemp); 592 | 593 | graphics_draw_text(ctx, 594 | tempString, 595 | smSidebarFont, 596 | GRect(xPosition + SidebarWidgets_xOffset, yPosition + 35, 30, 20), 597 | GTextOverflowModeFill, 598 | GTextAlignmentCenter, 599 | NULL); 600 | } 601 | } else { 602 | // if the weather data isn't set, draw a loading indication 603 | graphics_draw_text(ctx, 604 | "...", 605 | currentSidebarFont, 606 | GRect(xPosition - 5 + SidebarWidgets_xOffset, yPosition, 38, 20), 607 | GTextOverflowModeFill, 608 | GTextAlignmentCenter, 609 | NULL); 610 | } 611 | } 612 | 613 | /***** Alternate Time Zone Widget *****/ 614 | 615 | static int AltTime_getHeight(void) { 616 | if(SidebarWidgets_fixedHeight) { 617 | return FIXED_WIDGET_HEIGHT; 618 | } else { 619 | return (globalSettings.useLargeFonts) ? 31 : 26; 620 | } 621 | } 622 | 623 | static void AltTime_draw(GContext* ctx, int xPosition, int yPosition) { 624 | int yMod = SidebarWidgets_fixedHeight ? 6 : - 5; 625 | yMod = globalSettings.useLargeFonts ? yMod - 2 : yMod; 626 | 627 | graphics_draw_text(ctx, 628 | globalSettings.altclockName, 629 | currentSidebarSmallFont, 630 | GRect(xPosition + SidebarWidgets_xOffset, yPosition + yMod, 30, 20), 631 | GTextOverflowModeFill, 632 | GTextAlignmentCenter, 633 | NULL); 634 | 635 | yMod = SidebarWidgets_fixedHeight ? 16 : 0; 636 | yMod = (globalSettings.useLargeFonts) ? yMod + 5 : yMod + 8; 637 | 638 | graphics_draw_text(ctx, 639 | time_date_altClock, 640 | currentSidebarFont, 641 | GRect(xPosition - 1 + SidebarWidgets_xOffset, yPosition + yMod, 30, 20), 642 | GTextOverflowModeFill, 643 | GTextAlignmentCenter, 644 | NULL); 645 | } 646 | 647 | /***** Health Widget *****/ 648 | 649 | #ifdef PBL_HEALTH 650 | static int Health_getHeight(void) { 651 | if(Health_sleepingToBeDisplayed()) { 652 | return Sleep_getHeight(); 653 | } else { 654 | return Steps_getHeight(); 655 | } 656 | } 657 | 658 | static void Health_draw(GContext* ctx, int xPosition, int yPosition) { 659 | // check if we're showing the sleep data or step data 660 | 661 | if(Health_sleepingToBeDisplayed()) { 662 | Sleep_draw(ctx, xPosition, yPosition); 663 | } else { 664 | Steps_draw(ctx, xPosition, yPosition); 665 | } 666 | } 667 | 668 | static int Sleep_getHeight(void) { 669 | return 44; 670 | } 671 | 672 | static void Sleep_draw(GContext* ctx, int xPosition, int yPosition) { 673 | if(sleepImage) { 674 | util_image_draw(ctx, sleepImage, xPosition + 3 + SidebarWidgets_xOffset, yPosition - 7); 675 | } 676 | 677 | // get sleep in seconds 678 | HealthValue sleep_seconds = globalSettings.healthUseRestfulSleep ? Health_getRestfulSleepSeconds() : Health_getSleepSeconds(); 679 | 680 | char hours_text[4]; 681 | char minutes_text[4]; 682 | 683 | seconds_to_minutes_hours_text(sleep_seconds, hours_text, minutes_text); 684 | 685 | graphics_draw_text(ctx, 686 | hours_text, 687 | mdSidebarFont, 688 | GRect(xPosition - 2 + SidebarWidgets_xOffset, yPosition + 14, 34, 20), 689 | GTextOverflowModeFill, 690 | GTextAlignmentCenter, 691 | NULL); 692 | 693 | graphics_draw_text(ctx, 694 | minutes_text, 695 | smSidebarFont, 696 | GRect(xPosition - 2 + SidebarWidgets_xOffset, yPosition + 30, 34, 20), 697 | GTextOverflowModeFill, 698 | GTextAlignmentCenter, 699 | NULL); 700 | 701 | } 702 | 703 | static int Steps_getHeight(void) { 704 | if(SidebarWidgets_fixedHeight) { 705 | return FIXED_WIDGET_HEIGHT; 706 | } else { 707 | return 32; 708 | } 709 | } 710 | 711 | static void Steps_draw(GContext* ctx, int xPosition, int yPosition) { 712 | if(stepsImage) { 713 | int yIconPosition = SidebarWidgets_fixedHeight ? yPosition + 2 : yPosition - 7; 714 | 715 | util_image_draw(ctx, stepsImage, xPosition + 3 + SidebarWidgets_xOffset, yIconPosition); 716 | } 717 | 718 | char steps_text[8]; 719 | bool use_small_font = false; 720 | 721 | if(globalSettings.healthActivityDisplay == DISTANCE) { 722 | HealthValue distance = Health_getDistanceWalked(); 723 | MeasurementSystem unit_system = health_service_get_measurement_system_for_display(HealthMetricWalkedDistanceMeters); 724 | 725 | // format distance string 726 | if(unit_system == MeasurementSystemMetric) { 727 | distance_to_metric_text(distance, steps_text); 728 | 729 | if(distance > 9999) { 730 | use_small_font = true; 731 | } 732 | } else { 733 | distance_to_imperial_text(distance, steps_text); 734 | } 735 | } else if(globalSettings.healthActivityDisplay == STEPS) { 736 | HealthValue steps = Health_getSteps(); 737 | 738 | steps_to_text(steps, steps_text); 739 | } else if(globalSettings.healthActivityDisplay == DURATION) { 740 | HealthValue active_seconds = Health_getActiveSeconds(); 741 | 742 | seconds_to_text(active_seconds, steps_text); 743 | } else { // KCALORIES 744 | HealthValue active_kcalories = Health_getActiveKCalories(); 745 | 746 | kCalories_to_text(active_kcalories, steps_text); 747 | } 748 | 749 | int yTextPosition = yPosition; 750 | 751 | if(SidebarWidgets_fixedHeight) { 752 | if(globalSettings.useLargeFonts) { 753 | yTextPosition += 26; 754 | } else { 755 | yTextPosition += 24; 756 | } 757 | } else { 758 | yTextPosition += 13; 759 | } 760 | 761 | graphics_draw_text(ctx, 762 | steps_text, 763 | (use_small_font) ? smSidebarFont : mdSidebarFont, 764 | GRect(xPosition - 2 + SidebarWidgets_xOffset, yTextPosition, 35, 20), 765 | GTextOverflowModeFill, 766 | GTextAlignmentCenter, 767 | NULL); 768 | } 769 | 770 | static int HeartRate_getHeight(void) { 771 | if(SidebarWidgets_fixedHeight) { 772 | return FIXED_WIDGET_HEIGHT; 773 | } else if(globalSettings.useLargeFonts) { 774 | return 40; 775 | } else { 776 | return 38; 777 | } 778 | } 779 | 780 | static void HeartRate_draw(GContext* ctx, int xPosition, int yPosition) { 781 | if(heartImage) { 782 | int yIconPosition = SidebarWidgets_fixedHeight ? yPosition + 3 : yPosition; 783 | 784 | util_image_draw(ctx, heartImage, xPosition + 3 + SidebarWidgets_xOffset, yIconPosition); 785 | } 786 | 787 | int yOffset = globalSettings.useLargeFonts ? 17 : 20; 788 | 789 | if(SidebarWidgets_fixedHeight) { 790 | yOffset += 4; 791 | } 792 | 793 | HealthValue heart_rate = Health_getHeartRate(); 794 | char heart_rate_text[8]; 795 | 796 | snprintf(heart_rate_text, sizeof(heart_rate_text), "%li", heart_rate); 797 | 798 | graphics_draw_text(ctx, 799 | heart_rate_text, 800 | currentSidebarFont, 801 | GRect(xPosition - 5 + SidebarWidgets_xOffset, yPosition + yOffset, 38, 20), 802 | GTextOverflowModeFill, 803 | GTextAlignmentCenter, 804 | NULL); 805 | } 806 | 807 | #endif 808 | 809 | /***** Beats (Swatch Internet Time) widget *****/ 810 | 811 | static int Beats_getHeight(void) { 812 | if(SidebarWidgets_fixedHeight) { 813 | return FIXED_WIDGET_HEIGHT; 814 | } else { 815 | return (globalSettings.useLargeFonts) ? 31 : 26; 816 | } 817 | } 818 | 819 | static void Beats_draw(GContext* ctx, int xPosition, int yPosition) { 820 | int yMod = SidebarWidgets_fixedHeight ? 6 : - 5; 821 | yMod = globalSettings.useLargeFonts ? yMod - 2 : yMod; 822 | graphics_draw_text(ctx, 823 | "@", 824 | currentSidebarSmallFont, 825 | GRect(xPosition + SidebarWidgets_xOffset, yPosition + yMod, 30, 20), 826 | GTextOverflowModeFill, 827 | GTextAlignmentCenter, 828 | NULL); 829 | 830 | yMod = SidebarWidgets_fixedHeight ? 16 : 0; 831 | yMod = (globalSettings.useLargeFonts) ? yMod + 5 : yMod + 8; 832 | 833 | graphics_draw_text(ctx, 834 | time_date_currentBeats, 835 | currentSidebarFont, 836 | GRect(xPosition + SidebarWidgets_xOffset, yPosition + yMod, 30, 20), 837 | GTextOverflowModeFill, 838 | GTextAlignmentCenter, 839 | NULL); 840 | } 841 | -------------------------------------------------------------------------------- /src/c/sidebar_widgets.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | /* 5 | * "Compact Mode" is a global setting shared by all widgets, which determines 6 | * whether they should try to reduce their padding. Intended to allow larger 7 | * widgets to fit when vertical screen space is lacking 8 | */ 9 | extern bool SidebarWidgets_useCompactMode; 10 | 11 | /* 12 | * "Fixed Height" is a global setting shared by all widgets, which force 13 | * a fixed height for all widgets. It is used by bottom and top bar 14 | */ 15 | extern bool SidebarWidgets_fixedHeight; 16 | 17 | /* 18 | * A global x offset used for nudging the widgets left and right 19 | * Included for round support 20 | */ 21 | extern int SidebarWidgets_xOffset; 22 | 23 | /* 24 | * The different types of sidebar widgets: 25 | * we'll give them numbers so that we can index them in settings 26 | */ 27 | typedef enum { 28 | EMPTY = 0, 29 | BLUETOOTH_DISCONNECT = 1, 30 | BATTERY_METER = 2, 31 | ALT_TIME_ZONE = 3, 32 | DATE = 4, 33 | SECONDS = 5, 34 | WEEK_NUMBER = 6, 35 | WEATHER_CURRENT = 7, 36 | WEATHER_FORECAST_TODAY = 8, 37 | TIME_UNUSED = 9, 38 | HEALTH = 10, 39 | BEATS = 11, 40 | HEARTRATE = 12, 41 | SLEEP = 13, 42 | STEP = 14 43 | } SidebarWidgetType; 44 | 45 | typedef struct { 46 | /* 47 | * Returns the pixel height of the widget, taking into account all current 48 | * settings that would affect this, such as font size 49 | */ 50 | int (*getHeight)(); 51 | 52 | /* 53 | * Draws the widget using the provided graphics context 54 | */ 55 | void (*draw)(GContext* ctx, int xPosition, int yPosition); 56 | } SidebarWidget; 57 | 58 | void SidebarWidgets_init(void); 59 | void SidebarWidgets_deinit(void); 60 | SidebarWidget getSidebarWidgetByType(SidebarWidgetType type); 61 | void SidebarWidgets_updateFonts(void); 62 | -------------------------------------------------------------------------------- /src/c/time_date.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "time.h" 3 | #include "settings.h" 4 | #include "time_date.h" 5 | 6 | // the date and time strings 7 | char time_date_currentDayNum[3]; 8 | char time_date_currentWeekNum[3]; 9 | char time_date_currentSecondsNum[4]; 10 | char time_date_altClock[4]; 11 | char time_date_currentBeats[5]; 12 | char time_date_hours[3]; 13 | char time_date_minutes[3]; 14 | uint8_t time_date_currentDayName; 15 | uint8_t time_date_currentMonth; 16 | #ifndef PBL_ROUND 17 | bool time_date_isAmHour; 18 | #endif 19 | 20 | // c can't do true modulus on negative numbers, apparently 21 | // from http://stackoverflow.com/questions/11720656/modulo-operation-with-negative-numbers 22 | static int mod(int a, int b) { 23 | int r = a % b; 24 | return r < 0 ? r + b : r; 25 | } 26 | 27 | static int time_date_get_beats(const struct tm *tm) { 28 | // code from https://gist.github.com/insom/bf40b91fd25ae1d84764 29 | 30 | time_t t = mktime((struct tm *)tm); 31 | t = t + 3600; // Add an hour to make into BMT 32 | 33 | struct tm *bt = gmtime(&t); 34 | double sex = (bt->tm_hour * 3600) + (bt->tm_min * 60) + bt->tm_sec; 35 | int beats = (int)(10 * (sex / 864)) % 1000; 36 | 37 | return beats; 38 | } 39 | 40 | void time_date_update(void) { 41 | time_t rawTime; 42 | struct tm* time_info; 43 | 44 | time(&rawTime); 45 | time_info = localtime(&rawTime); 46 | 47 | if (clock_is_24h_style()) { 48 | strftime(time_date_hours, sizeof(time_date_hours), (globalSettings.showLeadingZero) ? "%H" : "%k", time_info); 49 | } else { 50 | strftime(time_date_hours, sizeof(time_date_hours), (globalSettings.showLeadingZero) ? "%I" : "%l", time_info); 51 | } 52 | 53 | if(time_date_hours[0] == ' ' && globalSettings.centerTime) { 54 | time_date_hours[0] = time_date_hours[1]; 55 | time_date_hours[1] = '\0'; 56 | } 57 | 58 | // minutes 59 | strftime(time_date_minutes, sizeof(time_date_minutes), "%M", time_info); 60 | 61 | // set all the date strings 62 | strftime(time_date_currentDayNum, 3, "%e", time_info); 63 | strftime(time_date_currentWeekNum, 3, "%V", time_info); 64 | 65 | // remove padding on date num, if needed 66 | if(time_date_currentDayNum[0] == ' ') { 67 | time_date_currentDayNum[0] = time_date_currentDayNum[1]; 68 | time_date_currentDayNum[1] = '\0'; 69 | } 70 | 71 | // set the seconds string 72 | strftime(time_date_currentSecondsNum, 4, ":%S", time_info); 73 | 74 | time_date_currentDayName = time_info->tm_wday; 75 | time_date_currentMonth = time_info->tm_mon; 76 | 77 | #ifndef PBL_ROUND 78 | time_date_isAmHour = time_info->tm_hour < 12; 79 | #endif // PBL_ROUND 80 | 81 | if(globalSettings.enableAltTimeZone) { 82 | // set the alternate time zone string 83 | int hour = time_info->tm_hour; 84 | 85 | // apply the configured offset value 86 | hour += globalSettings.altclockOffset; 87 | 88 | char am_pm; 89 | 90 | // format it 91 | if(clock_is_24h_style()) { 92 | hour = mod(hour, 24); 93 | am_pm = (char) 0; 94 | } else { 95 | hour = mod(hour, 12); 96 | if(hour == 0) { 97 | hour = 12; 98 | } 99 | am_pm = (mod(hour, 24) < 12) ? 'a' : 'p'; 100 | } 101 | 102 | if(globalSettings.showLeadingZero && hour < 10) { 103 | snprintf(time_date_altClock, sizeof(time_date_altClock), "0%i%c", hour, am_pm); 104 | } else { 105 | snprintf(time_date_altClock, sizeof(time_date_altClock), "%i%c", hour, am_pm); 106 | } 107 | } 108 | 109 | if(globalSettings.enableBeats) { 110 | // this must be last, because time_get_beats screws with the time structure 111 | int beats = 0; 112 | 113 | // set the swatch internet time beats 114 | beats = time_date_get_beats(time_info); 115 | 116 | snprintf(time_date_currentBeats, sizeof(time_date_currentBeats), "%i", beats); 117 | } 118 | } 119 | 120 | -------------------------------------------------------------------------------- /src/c/time_date.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | // the date and time strings 5 | extern char time_date_currentDayNum[3]; 6 | extern char time_date_currentWeekNum[3]; 7 | extern char time_date_currentSecondsNum[4]; 8 | extern char time_date_altClock[4]; 9 | extern char time_date_currentBeats[5]; 10 | extern char time_date_hours[3]; 11 | extern char time_date_minutes[3]; 12 | extern uint8_t time_date_currentDayName; 13 | extern uint8_t time_date_currentMonth; 14 | #ifndef PBL_ROUND 15 | extern bool time_date_isAmHour; 16 | #endif // PBL_ROUND 17 | 18 | void time_date_update(void); 19 | -------------------------------------------------------------------------------- /src/c/util.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "settings.h" 4 | #include "util.h" 5 | 6 | bool recolor_iterator_cb(GDrawCommand *command, uint32_t index, void *context) { 7 | GColor *colors = (GColor *)context; 8 | 9 | gdraw_command_set_fill_color(command, colors[0]); 10 | gdraw_command_set_stroke_color(command, colors[1]); 11 | 12 | return true; 13 | } 14 | 15 | /* 16 | * For the specified GDrawCommandImage, recolors it with 17 | * the specified fill and stroke colors 18 | */ 19 | void image_recolor(GDrawCommandImage *img, GColor fill_color, GColor stroke_color) { 20 | GColor colors[2]; 21 | colors[0] = fill_color; 22 | colors[1] = stroke_color; 23 | 24 | gdraw_command_list_iterate(gdraw_command_image_get_command_list(img), 25 | recolor_iterator_cb, &colors); 26 | } 27 | 28 | void util_image_draw(GContext* ctx, GDrawCommandImage *img, int xPosition, int yPosition) { 29 | image_recolor(img, globalSettings.iconFillColor, globalSettings.iconStrokeColor); 30 | gdraw_command_image_draw(ctx, img, GPoint(xPosition, yPosition)); 31 | } 32 | 33 | void util_image_draw_inverted_color(GContext* ctx, GDrawCommandImage *img, int xPosition, int yPosition) { 34 | image_recolor(img, globalSettings.iconStrokeColor, globalSettings.iconFillColor); 35 | gdraw_command_image_draw(ctx, img, GPoint(xPosition, yPosition)); 36 | } 37 | 38 | int16_t get_obstruction_height(Layer *s_window_layer) { 39 | GRect fullscreen = layer_get_bounds(s_window_layer); 40 | GRect unobstructed_bounds = layer_get_unobstructed_bounds(s_window_layer); 41 | 42 | return fullscreen.size.h - unobstructed_bounds.size.h; 43 | } 44 | 45 | void seconds_to_minutes_hours_text(HealthValue seconds, char * hours_text, char * minutes_text) { 46 | 47 | // convert to hours/minutes 48 | int minutes = seconds / 60; 49 | int hours = minutes / 60; 50 | 51 | // find minutes remainder 52 | minutes %= 60; 53 | 54 | snprintf(hours_text, sizeof(hours_text), "%ih", hours); 55 | snprintf(minutes_text, sizeof(minutes_text), "%im", minutes); 56 | } 57 | 58 | void seconds_to_text(HealthValue seconds, char * hours_minutes_text) { 59 | 60 | // convert to hours/minutes 61 | int minutes = seconds / 60; 62 | int hours = minutes / 60; 63 | 64 | // find minutes remainder 65 | minutes %= 60; 66 | 67 | snprintf(hours_minutes_text, sizeof(hours_minutes_text), "%ih%i", hours, minutes); 68 | } 69 | 70 | void distance_to_metric_text(HealthValue distance, char * metric_text) { 71 | if(distance < 100) { 72 | snprintf(metric_text, sizeof(metric_text), "%lim", distance); 73 | } else if(distance < 1000) { 74 | distance /= 100; // convert to tenths of km 75 | snprintf(metric_text, sizeof(metric_text), "%c%likm", globalSettings.decimalSeparator, distance); 76 | } else { 77 | distance /= 1000; // convert to km 78 | snprintf(metric_text, sizeof(metric_text), "%likm", distance); 79 | } 80 | } 81 | 82 | void distance_to_imperial_text(HealthValue distance, char * imperial_text) { 83 | int miles_tenths = distance * 10 / 1609 % 10; 84 | int miles_whole = (int)roundf(distance / 1609.0f); 85 | 86 | if(miles_whole > 0) { 87 | snprintf(imperial_text, sizeof(imperial_text), "%imi", miles_whole); 88 | } else { 89 | snprintf(imperial_text, sizeof(imperial_text), "%c%imi", globalSettings.decimalSeparator, miles_tenths); 90 | } 91 | } 92 | 93 | void steps_to_text(HealthValue steps, char * steps_text) { 94 | // format step string 95 | if(steps < 1000) { 96 | snprintf(steps_text, sizeof(steps_text), "%li", steps); 97 | } else { 98 | int steps_thousands = steps / 1000; 99 | int steps_hundreds = steps / 100 % 10; 100 | 101 | if (steps < 10000) { 102 | snprintf(steps_text, sizeof(steps_text), "%i%c%ik", steps_thousands, globalSettings.decimalSeparator, steps_hundreds); 103 | } else { 104 | snprintf(steps_text, sizeof(steps_text), "%ik", steps_thousands); 105 | } 106 | } 107 | } 108 | 109 | void kCalories_to_text(HealthValue kcalories, char * kcalories_text) { 110 | // format kcalories string 111 | if(kcalories < 1000) { 112 | snprintf(kcalories_text, sizeof(kcalories_text), "%likc", kcalories); 113 | } else { 114 | int kcalories_thousands = kcalories / 1000; 115 | int kcalories_hundreds = kcalories / 100 % 10; 116 | 117 | if (kcalories < 10000) { 118 | snprintf(kcalories_text, sizeof(kcalories_text), "%i%c%iMc", kcalories_thousands, globalSettings.decimalSeparator, kcalories_hundreds); 119 | } else { 120 | snprintf(kcalories_text, sizeof(kcalories_text), "%iMc", kcalories_thousands); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/c/util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | /* 5 | * Draw image at position with the specified fill and stroke colors 6 | */ 7 | void util_image_draw(GContext* ctx, GDrawCommandImage *img, int xPosition, int yPosition); 8 | 9 | /* 10 | * Draw image at position with the inverted fill and stroke colors 11 | */ 12 | void util_image_draw_inverted_color(GContext* ctx, GDrawCommandImage *img, int xPosition, int yPosition); 13 | 14 | /* 15 | * Get obstruction height of Timeline Quick View on the layer given as input 16 | */ 17 | int16_t get_obstruction_height(Layer *s_window_layer); 18 | 19 | /* 20 | * Convert number of seconds to minutes and hours text 21 | */ 22 | void seconds_to_minutes_hours_text(HealthValue seconds, char * hours_text, char * minutes_text); 23 | 24 | /* 25 | * Convert number of seconds to one minutes and hours text 26 | */ 27 | void seconds_to_text(HealthValue seconds, char * hours_minutes_text); 28 | 29 | /* 30 | * Convert distance to metric text 31 | */ 32 | void distance_to_metric_text(HealthValue distance, char * metric_text); 33 | 34 | /* 35 | * Convert distance to imperial unit text 36 | */ 37 | void distance_to_imperial_text(HealthValue distance, char * imperial_text); 38 | 39 | /* 40 | * Convert steps to text 41 | */ 42 | void steps_to_text(HealthValue steps, char * steps_text); 43 | 44 | /* 45 | * Convert kCalories to text 46 | */ 47 | void kCalories_to_text(HealthValue kcalories, char * kcalories_text); 48 | -------------------------------------------------------------------------------- /src/c/weather.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "weather.h" 3 | #include "settings.h" 4 | 5 | WeatherInfo Weather_weatherInfo; 6 | WeatherForecastInfo Weather_weatherForecast; 7 | 8 | GDrawCommandImage* Weather_currentWeatherIcon; 9 | GDrawCommandImage* Weather_forecastWeatherIcon; 10 | 11 | static uint32_t getConditionIcon(WeatherCondition conditionCode) { 12 | uint32_t iconToLoad; 13 | 14 | switch(conditionCode) { 15 | case CLEAR_DAY: 16 | iconToLoad = RESOURCE_ID_WEATHER_CLEAR_DAY; 17 | break; 18 | case CLEAR_NIGHT: 19 | iconToLoad = RESOURCE_ID_WEATHER_CLEAR_NIGHT; 20 | break; 21 | case CLOUDY_DAY: 22 | iconToLoad = RESOURCE_ID_WEATHER_CLOUDY; 23 | break; 24 | case HEAVY_RAIN: 25 | iconToLoad = RESOURCE_ID_WEATHER_HEAVY_RAIN; 26 | break; 27 | case HEAVY_SNOW: 28 | iconToLoad = RESOURCE_ID_WEATHER_HEAVY_SNOW; 29 | break; 30 | case LIGHT_RAIN: 31 | iconToLoad = RESOURCE_ID_WEATHER_LIGHT_RAIN; 32 | break; 33 | case LIGHT_SNOW: 34 | iconToLoad = RESOURCE_ID_WEATHER_LIGHT_SNOW; 35 | break; 36 | case PARTLY_CLOUDY_NIGHT: 37 | iconToLoad = RESOURCE_ID_WEATHER_PARTLY_CLOUDY_NIGHT; 38 | break; 39 | case PARTLY_CLOUDY: 40 | iconToLoad = RESOURCE_ID_WEATHER_PARTLY_CLOUDY; 41 | break; 42 | case RAINING_AND_SNOWING: 43 | iconToLoad = RESOURCE_ID_WEATHER_RAINING_AND_SNOWING; 44 | break; 45 | case THUNDERSTORM: 46 | iconToLoad = RESOURCE_ID_WEATHER_THUNDERSTORM; 47 | break; 48 | default: 49 | iconToLoad = RESOURCE_ID_WEATHER_GENERIC; 50 | break; 51 | } 52 | 53 | return iconToLoad; 54 | } 55 | 56 | void Weather_setCurrentCondition(int conditionCode) { 57 | 58 | uint32_t currentWeatherIcon = getConditionIcon(conditionCode); 59 | 60 | // ok, now load the new icon: 61 | gdraw_command_image_destroy(Weather_currentWeatherIcon); 62 | Weather_currentWeatherIcon = gdraw_command_image_create_with_resource(currentWeatherIcon); 63 | 64 | Weather_weatherInfo.currentIconResourceID = currentWeatherIcon; 65 | } 66 | 67 | void Weather_setForecastCondition(int conditionCode) { 68 | uint32_t forecastWeatherIcon = getConditionIcon(conditionCode); 69 | 70 | gdraw_command_image_destroy(Weather_forecastWeatherIcon); 71 | Weather_forecastWeatherIcon = gdraw_command_image_create_with_resource(forecastWeatherIcon); 72 | 73 | Weather_weatherForecast.forecastIconResourceID = forecastWeatherIcon; 74 | } 75 | 76 | void Weather_init(void) { 77 | // if possible, load weather data from persistent storage 78 | if (persist_exists(WEATHERINFO_PERSIST_KEY) && !globalSettings.disableWeather) { 79 | // printf("current key exists!"); 80 | WeatherInfo w; 81 | persist_read_data(WEATHERINFO_PERSIST_KEY, &w, sizeof(WeatherInfo)); 82 | 83 | Weather_weatherInfo = w; 84 | 85 | Weather_currentWeatherIcon = gdraw_command_image_create_with_resource(w.currentIconResourceID); 86 | 87 | } else { 88 | 89 | // printf("current key does not exist!"); 90 | // otherwise, use null data 91 | Weather_currentWeatherIcon = NULL; 92 | Weather_weatherInfo.currentTemp = INT32_MIN; 93 | } 94 | 95 | if (persist_exists(WEATHERFORECAST_PERSIST_KEY) && !globalSettings.disableWeather) { 96 | // printf("forecast key exists!"); 97 | WeatherForecastInfo w; 98 | persist_read_data(WEATHERFORECAST_PERSIST_KEY, &w, sizeof(WeatherForecastInfo)); 99 | 100 | Weather_weatherForecast = w; 101 | 102 | Weather_forecastWeatherIcon = gdraw_command_image_create_with_resource(w.forecastIconResourceID); 103 | 104 | } else { 105 | // printf("forecast key does not exist!"); 106 | 107 | Weather_forecastWeatherIcon = NULL; 108 | Weather_weatherForecast.highTemp = INT32_MIN; 109 | Weather_weatherForecast.lowTemp = INT32_MIN; 110 | } 111 | } 112 | 113 | void Weather_saveData(void) { 114 | // printf("saving data!"); 115 | persist_write_data(WEATHERINFO_PERSIST_KEY, &Weather_weatherInfo, sizeof(WeatherInfo)); 116 | persist_write_data(WEATHERFORECAST_PERSIST_KEY, &Weather_weatherForecast, sizeof(WeatherForecastInfo)); 117 | } 118 | 119 | void Weather_deinit(void) { 120 | if (!globalSettings.disableWeather) { 121 | // save weather data to persistent storage 122 | Weather_saveData(); 123 | } 124 | 125 | // free memory 126 | gdraw_command_image_destroy(Weather_currentWeatherIcon); 127 | gdraw_command_image_destroy(Weather_forecastWeatherIcon); 128 | } 129 | -------------------------------------------------------------------------------- /src/c/weather.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | // persistent storage 5 | #define WEATHERINFO_PERSIST_KEY 2 6 | #define WEATHERFORECAST_PERSIST_KEY 222 7 | 8 | typedef struct { 9 | int currentTemp; 10 | uint32_t currentIconResourceID; 11 | } WeatherInfo; 12 | 13 | typedef struct { 14 | int highTemp; 15 | int lowTemp; 16 | uint32_t forecastIconResourceID; 17 | } WeatherForecastInfo; 18 | 19 | typedef enum { 20 | CLEAR_DAY = 0, 21 | CLEAR_NIGHT = 1, 22 | CLOUDY_DAY = 2, 23 | HEAVY_RAIN = 3, 24 | HEAVY_SNOW = 4, 25 | LIGHT_RAIN = 5, 26 | LIGHT_SNOW = 6, 27 | PARTLY_CLOUDY_NIGHT = 7, 28 | PARTLY_CLOUDY = 8, 29 | RAINING_AND_SNOWING = 9, 30 | THUNDERSTORM = 10, 31 | WEATHER_GENERIC = 11 32 | } WeatherCondition; 33 | 34 | extern WeatherInfo Weather_weatherInfo; 35 | extern WeatherForecastInfo Weather_weatherForecast; 36 | 37 | extern GDrawCommandImage* Weather_currentWeatherIcon; 38 | extern GDrawCommandImage* Weather_forecastWeatherIcon; 39 | 40 | 41 | void Weather_setCurrentCondition(int conditionCode); 42 | void Weather_setForecastCondition(int conditionCode); 43 | void Weather_saveData(void); 44 | void Weather_init(void); 45 | void Weather_deinit(void); 46 | -------------------------------------------------------------------------------- /src/pkjs/index.js: -------------------------------------------------------------------------------- 1 | 2 | var weather = require('./weather'); 3 | var languages = require('./languages'); 4 | 5 | // Require the keys' numeric values. 6 | var keys = require('message_keys'); 7 | 8 | var CONFIG_VERSION = 11; 9 | // var BASE_CONFIG_URL = 'http://localhost:4000/'; 10 | var BASE_CONFIG_URL = 'http://plarus.github.io/TimeStyleBBPebble/'; 11 | 12 | // Listen for when the watchface is opened 13 | Pebble.addEventListener('ready', 14 | function(e) { 15 | console.log('JS component is now READY'); 16 | 17 | // if it has never been started, set the weather to disabled 18 | // this is because the weather defaults to "off" 19 | if(window.localStorage.getItem('disable_weather') === null) { 20 | window.localStorage.setItem('disable_weather', 'yes'); 21 | } 22 | 23 | console.log('the wdisabled value is: "' + window.localStorage.getItem('disable_weather') + '"'); 24 | // if applicable, get the weather data 25 | if(window.localStorage.getItem('disable_weather') != 'yes') { 26 | weather.updateWeather(); 27 | } 28 | } 29 | ); 30 | 31 | // Listen for incoming messages 32 | // when one is received, we simply assume that it is a request for new weather data 33 | Pebble.addEventListener('appmessage', 34 | function(msg) { 35 | console.log('Received message: ' + JSON.stringify(msg.payload)); 36 | 37 | // in the case of receiving this, we assume the watch does, in fact, need weather data 38 | window.localStorage.setItem('disable_weather', 'no'); 39 | weather.updateWeather(); 40 | } 41 | ); 42 | 43 | Pebble.addEventListener('showConfiguration', function(e) { 44 | var bwConfigURL = BASE_CONFIG_URL + 'config_bw.html'; 45 | var colorConfigURL = BASE_CONFIG_URL + 'config_color.html'; 46 | var roundConfigURL = BASE_CONFIG_URL + 'config_color_round.html'; 47 | var dioriteConfigURL = BASE_CONFIG_URL + 'config_diorite.html'; 48 | 49 | var versionString = '?appversion=' + CONFIG_VERSION; 50 | 51 | if(Pebble.getActiveWatchInfo) { 52 | try { 53 | watch = Pebble.getActiveWatchInfo(); 54 | } catch(err) { 55 | watch = { 56 | platform: "basalt" 57 | }; 58 | } 59 | } else { 60 | watch = { 61 | platform: "aplite" 62 | }; 63 | } 64 | 65 | if(watch.platform == "aplite"){ 66 | Pebble.openURL(bwConfigURL + versionString); 67 | } else if(watch.platform == "chalk") { 68 | Pebble.openURL(roundConfigURL + versionString); 69 | } else if(watch.platform == "diorite") { 70 | Pebble.openURL(dioriteConfigURL + versionString); 71 | } else { 72 | Pebble.openURL(colorConfigURL + versionString); 73 | } 74 | }); 75 | 76 | Pebble.addEventListener('webviewclosed', function(e) { 77 | var configData = decodeURIComponent(e.response); 78 | 79 | if(configData) { 80 | configData = JSON.parse(decodeURIComponent(e.response)); 81 | 82 | console.log("Config data received!" + JSON.stringify(configData)); 83 | 84 | // prepare a structure to hold everything we'll send to the watch 85 | var dict = {}; 86 | 87 | // color settings 88 | if(configData.color_bg) { 89 | dict.SettingColorBG = parseInt(configData.color_bg, 16); 90 | } 91 | 92 | if(configData.color_sidebar) { 93 | dict.SettingColorSidebar = parseInt(configData.color_sidebar, 16); 94 | } 95 | 96 | if(configData.color_time) { 97 | dict.SettingColorTime = parseInt(configData.color_time, 16); 98 | } 99 | 100 | if(configData.sidebar_text_color) { 101 | dict.SettingSidebarTextColor = parseInt(configData.sidebar_text_color, 16); 102 | } 103 | 104 | // general options 105 | if(configData.language_id !== undefined) { 106 | dict.SettingLanguageID = configData.language_id; 107 | } 108 | 109 | if(configData.leading_zero_setting) { 110 | if(configData.leading_zero_setting == 'yes') { 111 | dict.SettingShowLeadingZero = 1; 112 | } else { 113 | dict.SettingShowLeadingZero = 0; 114 | } 115 | } 116 | 117 | if(configData.center_time_setting) { 118 | if(configData.center_time_setting == 'yes') { 119 | dict.SettingCenterTime = 1; 120 | } else { 121 | dict.SettingCenterTime = 0; 122 | } 123 | } 124 | 125 | if(configData.clock_font_setting) { 126 | if(configData.clock_font_setting == 'default') { 127 | dict.SettingClockFontId = 0; 128 | } else if(configData.clock_font_setting == 'leco') { 129 | dict.SettingClockFontId = 1; 130 | } else if(configData.clock_font_setting == 'bold') { 131 | dict.SettingClockFontId = 2; 132 | } else if(configData.clock_font_setting == 'bold-h') { 133 | dict.SettingClockFontId = 3; 134 | } else if(configData.clock_font_setting == 'bold-m') { 135 | dict.SettingClockFontId = 4; 136 | } 137 | } 138 | 139 | // bluetooth settings 140 | if(configData.disconnect_icon_setting) { 141 | if(configData.disconnect_icon_setting == 'yes') { 142 | dict.SettingDisconnectIcon = 1; 143 | } else { 144 | dict.SettingDisconnectIcon = 0; 145 | } 146 | } 147 | 148 | if(configData.bluetooth_vibe_setting) { 149 | if(configData.bluetooth_vibe_setting == 'yes') { 150 | dict.SettingBluetoothVibe = 1; 151 | } else { 152 | dict.SettingBluetoothVibe = 0; 153 | } 154 | } 155 | 156 | // notification settings 157 | if(configData.hourly_vibe_setting) { 158 | if(configData.hourly_vibe_setting == 'yes') { 159 | dict.SettingHourlyVibe = 1; 160 | } else if (configData.hourly_vibe_setting == 'half') { 161 | dict.SettingHourlyVibe = 2; 162 | } else { 163 | dict.SettingHourlyVibe = 0; 164 | } 165 | } 166 | 167 | // sidebar settings 168 | dict.SettingWidget0ID = configData.widget_0_id; 169 | dict.SettingWidget1ID = configData.widget_1_id; 170 | dict.SettingWidget2ID = configData.widget_2_id; 171 | dict.SettingWidget3ID = configData.widget_3_id; 172 | 173 | if(configData.sidebar_position) { 174 | if(configData.sidebar_position == 'left') { 175 | dict.SettingSidebarPosition = 1; 176 | } else if(configData.sidebar_position == 'right') { 177 | dict.SettingSidebarPosition = 2; 178 | } else if(configData.sidebar_position == 'bottom') { 179 | dict.SettingSidebarPosition = 3; 180 | } else if(configData.sidebar_position == 'top') { 181 | dict.SettingSidebarPosition = 4; 182 | } else { // 'none' 183 | dict.SettingSidebarPosition = 0; 184 | } 185 | } 186 | 187 | if(configData.use_large_sidebar_font_setting) { 188 | if(configData.use_large_sidebar_font_setting == 'yes') { 189 | dict.SettingUseLargeFonts = 1; 190 | } else { 191 | dict.SettingUseLargeFonts = 0; 192 | } 193 | } 194 | 195 | // weather widget settings 196 | if(configData.units) { 197 | if(configData.units == 'c') { 198 | dict.SettingUseMetric = 1; 199 | } else { 200 | dict.SettingUseMetric = 0; 201 | } 202 | } 203 | 204 | // weather location/source configs are not the watch's concern 205 | 206 | if(configData.weather_loc !== undefined) { 207 | window.localStorage.setItem('weather_loc', configData.weather_loc); 208 | window.localStorage.setItem('weather_loc_lat', configData.weather_loc_lat); 209 | window.localStorage.setItem('weather_loc_lng', configData.weather_loc_lng); 210 | } 211 | 212 | if(configData.weather_datasource) { 213 | window.localStorage.setItem('weather_datasource', configData.weather_datasource); 214 | window.localStorage.setItem('weather_api_key', configData.weather_api_key); 215 | } 216 | 217 | // battery widget settings 218 | if(configData.battery_meter_setting) { 219 | if(configData.battery_meter_setting == 'icon-and-percent') { 220 | dict.SettingShowBatteryPct = 1; 221 | } else if(configData.battery_meter_setting == 'icon-only') { 222 | dict.SettingShowBatteryPct = 0; 223 | } 224 | } 225 | 226 | if(configData.autobattery_setting) { 227 | if(configData.autobattery_setting == 'on') { 228 | dict.SettingDisableAutobattery = 0; 229 | } else if(configData.autobattery_setting == 'off') { 230 | dict.SettingDisableAutobattery = 1; 231 | } 232 | } 233 | 234 | if(configData.altclock_name) { 235 | dict.SettingAltClockName = configData.altclock_name; 236 | } 237 | 238 | if(configData.altclock_offset !== null) { 239 | dict.SettingAltClockOffset = parseInt(configData.altclock_offset, 10); 240 | } 241 | 242 | if(watch.platform != "aplite"){ 243 | if(configData.decimal_separator) { 244 | dict.SettingDecimalSep = configData.decimal_separator; 245 | } 246 | 247 | if(configData.health_activity_display) { 248 | if(configData.health_activity_display == 'distance') { 249 | dict.SettingHealthActivityDisplay = 1; 250 | } else if(configData.health_activity_display == 'duration') { 251 | dict.SettingHealthActivityDisplay = 2; 252 | } else if(configData.health_activity_display == 'calories') { 253 | dict.SettingHealthActivityDisplay = 3; 254 | } else { // steps 255 | dict.SettingHealthActivityDisplay = 0; 256 | } 257 | } 258 | 259 | // heath settings 260 | if(configData.health_use_restful_sleep) { 261 | if(configData.health_use_restful_sleep == 'yes') { 262 | dict.SettingHealthUseRestfulSleep = 1; 263 | } else { 264 | dict.SettingHealthUseRestfulSleep = 0; 265 | } 266 | } 267 | } 268 | 269 | // determine whether or not the weather checking should be enabled 270 | var disableWeather; 271 | 272 | var widgetIDs = [configData.widget_0_id, configData.widget_1_id, configData.widget_2_id, configData.widget_3_id]; 273 | 274 | // if there is either a current conditions or a today's forecast widget, enable the weather 275 | if(widgetIDs.indexOf(7) != -1 || widgetIDs.indexOf(8) != -1) { 276 | disableWeather = 'no'; 277 | } else { 278 | disableWeather = 'yes'; 279 | } 280 | 281 | window.localStorage.setItem('disable_weather', disableWeather); 282 | 283 | var enableForecast; 284 | 285 | if(widgetIDs.indexOf(8) != -1) { 286 | enableForecast = 'yes'; 287 | } 288 | 289 | window.localStorage.setItem('enable_forecast', enableForecast); 290 | 291 | console.log('Preparing message: ', JSON.stringify(dict)); 292 | 293 | // Send settings to Pebble watchapp 294 | Pebble.sendAppMessage(dict, function(){ 295 | // Second part of data (send datas in 2 part in order to reduce buffer size on Pebble watch) 296 | if(configData.language_id !== undefined) { 297 | // reset the structure 298 | dict = {}; 299 | 300 | for (i = 0; i < 7; i++) { 301 | dict[keys.SettingLanguageDayNames + i] = languages.dayNames[configData.language_id][i]; 302 | } 303 | for (i = 0; i < 12; i++) { 304 | dict[keys.SettingLanguageMonthNames + i] = languages.monthNames[configData.language_id][i]; 305 | } 306 | dict.SettingLanguageWordForWeek = languages.wordForWeek[configData.language_id]; 307 | 308 | console.log('Preparing language message: ', JSON.stringify(dict)); 309 | 310 | // Send language information to Pebble watchapp 311 | Pebble.sendAppMessage(dict, function(){ 312 | console.log('Sent language data to Pebble, now trying to get weather'); 313 | 314 | if(window.localStorage.getItem('disable_weather') != 'yes') { 315 | // after sending config data, force a weather refresh in case that changed 316 | weather.updateWeather(true); 317 | } 318 | }, function() { 319 | console.log('Failed to send language data!'); 320 | }); 321 | } else { 322 | console.log('Sent config data to Pebble, now trying to get weather'); 323 | 324 | if(window.localStorage.getItem('disable_weather') != 'yes') { 325 | // after sending config data, force a weather refresh in case that changed 326 | weather.updateWeather(true); 327 | } 328 | } 329 | }, function() { 330 | console.log('Failed to send config data!'); 331 | }); 332 | } else { 333 | console.log("No settings changed!"); 334 | } 335 | 336 | }); 337 | -------------------------------------------------------------------------------- /src/pkjs/languages.js: -------------------------------------------------------------------------------- 1 | /* day names in many different languages! */ 2 | const dayNames = [ 3 | ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"], 4 | ["DIM", "LUN", "MAR", "MER", "JEU", "VEN", "SAM"], 5 | ["SO", "MO", "DI", "MI", "DO", "FR", "SA"], 6 | ["DOM", "LUN", "MAR", "MIÉ", "JUE", "VIE", "SÁB"], 7 | ["DOM", "LUN", "MAR", "MER", "GIO", "VEN", "SAB"], 8 | ["ZO", "MA", "DI", "WO", "DO", "VR", "ZA"], 9 | ["PAZ", "PTS", "SAL", "ÇAR", "PER", "CUM", "CTS"], 10 | ["NE", "PO", "ÚT", "ST", "ČT", "PÁ", "SO"], 11 | ["DOM", "SEG", "TER", "QUA", "QUI", "SEX", "SÁB"], 12 | ["ΚΥΡ", "ΔΕΥ", "ΤΡΙ", "ΤΕΤ", "ΠΕΜ", "ΠΑΡ", "ΣΑΒ"], 13 | ["SÖN", "MÅN", "TIS", "ONS", "TOR", "FRE", "LÖR"], 14 | ["NDZ", "PON", "WTO", "ŚRO", "CZW", "PIĄ", "SOB"], 15 | ["NE", "PO", "UT", "ST", "ŠT", "PI", "SO"], 16 | ["CN", "T2", "T3", "T4", "T5", "T6", "T7"], 17 | ["DUM", "LUN", "MAR", "MIE", "JOI", "VIN", "SÂM"], 18 | ["DG", "DL", "DT", "DC", "DJ", "DV", "DS"], 19 | ["SØN", "MAN", "TIR", "ONS", "TOR", "FRE", "LØR"], 20 | ["ВС", "ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ"], 21 | ["P", "E", "T", "K", "N", "R", "L"], 22 | ["IG", "AL", "AR", "AZ", "OG", "OL", "LR"], 23 | ["SU", "MA", "TI", "KE", "TO", "PE", "LA"], 24 | ["SØN", "MAN", "TIR", "ONS", "TOR", "FRE", "LØR"], 25 | ["SEK", "PIR", "ANT", "TRE", "KET", "PEN", "ŠEŠ"], 26 | ["NED", "PON", "TOR", "SRE", "ČET", "PET", "SOB"], 27 | ["VAS", "HÉT", "KED", "SZE", "CSÜ", "PÉN", "SZO"], 28 | ["NED", "PON", "UTO", "SRE", "ČET", "PET", "SUB"], 29 | ["DOM", "LUA", "MÁI", "CÉA", "DÉA", "AOI", "SAT"], 30 | ["SVĒ", "PIR", "OTR", "TRE", "CET", "PIE", "SES"], 31 | ["NE", "PO", "UT", "SR", "ČE", "PE", "SU"], 32 | ["日", "一", "二", "三", "四", "五", "六"], 33 | ["MIN", "SEN", "SEL", "RAB", "KAM", "JUM", "SAB"], 34 | ["НД", "ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ"], 35 | ["SUL", "LLN", "MAW", "MER", "IAU", "GWE", "SAD"], 36 | ["DOM", "LUN", "MAR", "MER", "XOV", "VEN", "SAB"], 37 | ["日", "月", "火", "水", "木", "金", "土"], 38 | ["일", "월", "화", "수", "목", "금", "토"], 39 | ["א", "ב", "ג", "ד", "ה", "ו", "ש"], 40 | ["НЕД","ПОН","ВТО","СРЯ","ЧЕТ","ПЕТ","СЪБ"] 41 | ]; 42 | 43 | /* month names in many different languages! */ 44 | const monthNames = [ 45 | ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"], 46 | ["JAN", "FÉV", "MAR", "AVR", "MAI", "JUI", "JUL", "AOÛ", "SEP", "OCT", "NOV", "DÉC"], 47 | ["JAN", "FEB", "MÄR", "APR", "MAI", "JUN", "JUL", "AUG", "SEP", "OKT", "NOV", "DEZ"], 48 | ["ENE", "FEB", "MAR", "ABR", "MAY", "JUN", "JUL", "AGO", "SEP", "OCT", "NOV", "DIC"], 49 | ["GEN", "FEB", "MAR", "APR", "MAG", "GIU", "LUG", "AGO", "SET", "OTT", "NOV", "DIC"], 50 | ["JAN", "FEB", "MRT", "APR", "MEI", "JUN", "JUL", "AUG", "SEP", "OKT", "NOV", "DEC"], 51 | ["OCA", "ŞUB", "MAR", "NİS", "MAY", "HAZ", "TEM", "AĞU", "EYL", "EKİ", "KAS", "ARA"], 52 | ["LED", "ÚNO", "BŘE", "DUB", "KVĚ", "ČRV", "ČVC", "SRP", "ZÁŘ", "ŘÍJ", "LIS", "PRO"], 53 | ["JAN", "FEV", "MAR", "ABR", "MAI", "JUN", "JUL", "AGO", "SET", "OUT", "NOV", "DEZ"], 54 | ["ΙΑΝ", "ΦΕΒ", "ΜΑΡ", "ΑΠΡ", "ΜΑΪ", "ΙΟΝ", "ΙΟΛ", "ΑΥΓ", "ΣΕΠ", "ΟΚΤ", "ΝΟΕ", "ΔΕΚ"], 55 | ["JAN", "FEB", "MAR", "APR", "MAJ", "JUN", "JUL", "AUG", "SEP", "OKT", "NOV", "DEC"], 56 | ["STY", "LUT", "MAR", "KWI", "MAJ", "CZE", "LIP", "SIE", "WRZ", "PAŹ", "LIS", "GRU"], 57 | ["JAN", "FEB", "MAR", "APR", "MÁJ", "JÚN", "JÚL", "AUG", "SEP", "OKT", "NOV", "DEC"], 58 | ["Th1", "Th2", "Th3", "Th4", "Th5", "Th6", "Th7", "Th8", "Th9", "T10", "T11", "T12"], 59 | ["IAN", "FEB", "MAR", "APR", "MAI", "IUN", "IUL", "AUG", "SEP", "OCT", "NOI", "DEC"], 60 | ["GEN", "FEB", "MAR", "ABR", "MAI", "JUN", "JUL", "AGO", "SET", "OCT", "NOV", "DES"], 61 | ["JAN", "FEB", "MAR", "APR", "MAI", "JUN", "JUL", "AUG", "SEP", "OKT", "NOV", "DES"], 62 | ["ЯНВ", "ФЕВ", "МАР", "АПР", "МАЙ", "ИЮН", "ИЮЛ", "АВГ", "СЕН", "ОКТ", "НОЯ", "ДЕК"], 63 | ["JAN", "VEB", "MÄR", "APR", "MAI", "JUN", "JUL", "AUG", "SEP", "OKT", "NOV", "DET"], 64 | ["URT", "OTS", "MAR", "API", "MAI", "EKA", "UZT", "ABU", "IRA", "URR", "AZA", "ABE"], 65 | ["TAM", "HEL", "MAA", "HUH", "TOU", "KES", "HEI", "ELO", "SYY", "LOK", "MAR", "JOU"], 66 | ["JAN", "FEB", "MAR", "APR", "MAJ", "JUN", "JUL", "AUG", "SEP", "OKT", "NOV", "DEC"], 67 | ["SAU", "VAS", "KOV", "BAL", "GEG", "BIR", "LIE", "RUG", "RGS", "SPA", "LAP", "GRU"], 68 | ["JAN", "FEB", "MAR", "APR", "MAJ", "JUN", "JUL", "AVG", "SEP", "OKT", "NOV", "DEC"], 69 | ["JAN", "FEB", "MÁR", "ÁPR", "MÁJ", "JÚN", "JÚL", "AUG", "SZE", "OKT", "NOV", "DEC"], 70 | ["SIJ", "VEL", "OŽU", "TRA", "SVI", "LIP", "SRP", "KOL", "RUJ", "LIS", "STU", "PRO"], 71 | ["EAN", "FEA", "MÁR", "AIB", "BEA", "MEI", "IÚI", "LÚN", "MFÓ", "DFÓ", "SAM", "NOL"], 72 | ["JAN", "FEB", "MAR", "APR", "MAI", "JŪN", "JŪL", "AUG", "SEP", "OKT", "NOV", "DEC"], 73 | ["JAN", "FEB", "MAR", "APR", "MAJ", "JUN", "JUL", "AVG", "SEP", "OKT", "NOV", "DEC"], 74 | ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], 75 | ["JAN", "FEB", "MAR", "APR", "MEI", "JUN", "JUL", "AGU", "SEP", "OKT", "NOV", "DES"], 76 | ["СІЧ", "ЛЮТ", "БЕР", "КВІ", "ТРА", "ЧЕР", "ЛИП", "СЕР", "ВЕР", "ЖОВ", "ЛИС", "ГРУ"], 77 | ["ION", "CHW", "MAW", "EBR", "MAI", "MEH", "GOR", "AWS", "MED", "HYD", "TCH", "RHA"], 78 | ["XAN", "FEB", "MAR", "ABR", "MAI", "XUÑ", "XUL", "AGO", "SET", "OUT", "NOV", "NAD"], 79 | ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"], 80 | ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"], 81 | ["ינו", "פבר", "מרץ", "אפר", "מאי", "יונ", "יול", "אוג", "ספט", "אוק", "נוב", "דצמ"], 82 | ["ЯНУ","ФЕВ","МАР","АПР","МАЙ","ЮНИ","ЮЛИ","АВГ","СЕП","ОКТ","НОЕ","ДЕК"] 83 | ]; 84 | 85 | // all of these are taken from: 86 | // http://www.unicode.org/cldr/charts/28/by_type/date_&_time.fields.html#521165cf49647551 87 | const wordForWeek = [ 88 | "Wk", 89 | "Sem", 90 | "W", 91 | "Sem", 92 | "Sett", 93 | "Wk", 94 | "Hf", 95 | "Týd", 96 | "Sem", 97 | "εβδ", 98 | "V", 99 | "Tydz", 100 | "Týž", 101 | "Tuần", 102 | "Săpt", 103 | "Setm", 104 | "Uke", 105 | "нед", 106 | "Näd", 107 | "Ast", 108 | "Vk", 109 | "Uge", 110 | "Sav", 111 | "Ted", 112 | "Hét", 113 | "Tj", 114 | "Scht", 115 | "Ned", 116 | "Ned", 117 | "周", 118 | "Ming", 119 | "Тиж", 120 | "Wnos", 121 | "Sem", 122 | "週", 123 | "주", 124 | "שב", 125 | "Сед" 126 | ]; 127 | 128 | module.exports.dayNames = dayNames; 129 | module.exports.monthNames = monthNames; 130 | module.exports.wordForWeek = wordForWeek; 131 | -------------------------------------------------------------------------------- /src/pkjs/secrets_example.js: -------------------------------------------------------------------------------- 1 | var OWM_APP_ID = 'YOUR OWM API KEY GOES HERE'; 2 | 3 | module.exports.OWM_APP_ID = OWM_APP_ID; 4 | -------------------------------------------------------------------------------- /src/pkjs/weather.js: -------------------------------------------------------------------------------- 1 | /* general utility stuff related to weather */ 2 | 3 | var weatherProviders = { 4 | 'owm' : require('./weather_owm'), 5 | // 'forecast' : require('weather_forecast'), 6 | 'wunderground' : require('./weather_wunderground') 7 | }; 8 | 9 | var DEFAULT_WEATHER_PROVIDER = 'owm'; 10 | 11 | // get new forecasts if 3 hours have elapsed 12 | var FORECAST_MAX_AGE = 3 * 60 * 60 * 1000; 13 | var MAX_FAILURES = 3; 14 | var currentFailures = 0; 15 | 16 | // icon codes for sending weather icons to pebble 17 | var WeatherIcons = { 18 | CLEAR_DAY : 0, 19 | CLEAR_NIGHT : 1, 20 | CLOUDY_DAY : 2, 21 | HEAVY_RAIN : 3, 22 | HEAVY_SNOW : 4, 23 | LIGHT_RAIN : 5, 24 | LIGHT_SNOW : 6, 25 | PARTLY_CLOUDY_NIGHT : 7, 26 | PARTLY_CLOUDY : 8, 27 | RAINING_AND_SNOWING : 9, 28 | THUNDERSTORM : 10, 29 | WEATHER_GENERIC : 11 30 | }; 31 | 32 | 33 | function getCurrentWeatherProvider() { 34 | var currentWeatherProvider = window.localStorage.getItem('weather_datasource'); 35 | 36 | if(weatherProviders[currentWeatherProvider] !== undefined ) { 37 | return weatherProviders[currentWeatherProvider]; 38 | } else { 39 | return weatherProviders[DEFAULT_WEATHER_PROVIDER]; 40 | } 41 | } 42 | 43 | function updateWeather(forceUpdate) { 44 | var weatherDisabled = window.localStorage.getItem('disable_weather'); 45 | 46 | console.log("Get weather function called! DisableWeather is '" + weatherDisabled + "'"); 47 | 48 | // if weather is not disabled... 49 | if(weatherDisabled !== "yes") { 50 | // in case "disable_weather" is empty or something weird, set it to "no" 51 | // since we already know it's not "yes" 52 | window.localStorage.setItem('disable_weather', 'no'); 53 | 54 | var weatherLoc = window.localStorage.getItem('weather_loc'); 55 | var storedLat = window.localStorage.getItem('weather_loc_lat'); 56 | var storedLng = window.localStorage.getItem('weather_loc_lng'); 57 | 58 | // console.log("Stored lat: " + storedLat + ", stored lng: " + storedLng); 59 | 60 | if(weatherLoc) { // do we have a stored location? 61 | // if so, we should check if we have valid LAT and LNG coords 62 | hasLocationCoords = (storedLat != undefined && storedLng != undefined) 63 | && (storedLat != '' && storedLng != ''); 64 | if(hasLocationCoords) { // do we have valid stored coordinates? 65 | // if we have valid coords, use them 66 | var pos = { 67 | coords : { 68 | latitude : storedLat, 69 | longitude : storedLng 70 | } 71 | }; 72 | 73 | getCurrentWeatherProvider().getWeatherFromCoords(pos); 74 | 75 | if(forceUpdate || isForecastNeeded()) { 76 | getCurrentWeatherProvider().getForecastFromCoords(pos); 77 | } 78 | } else { 79 | // otherwise, use the stored string (legacy, or google was blocked from running) 80 | getCurrentWeatherProvider().getWeather(weatherLoc); 81 | 82 | if(forceUpdate || isForecastNeeded()) { 83 | getCurrentWeatherProvider().getForecast(weatherLoc); 84 | } 85 | } 86 | } else { 87 | // if we don't have a stored location, get the GPS location 88 | getLocation(); 89 | } 90 | } 91 | } 92 | 93 | function getLocation() { 94 | navigator.geolocation.getCurrentPosition( 95 | locationSuccess, 96 | locationError, 97 | {timeout: 15000, maximumAge: 60000} 98 | ); 99 | } 100 | 101 | function locationError(err) { 102 | console.log('location error on the JS side! Failure #' + currentFailures); 103 | //if we fail, try using the cached location 104 | if(currentFailures <= MAX_FAILURES) { 105 | // reset cache time 106 | window.localStorage.setItem('weather_loc_cache_time', (new Date().getTime() / 1000)); 107 | 108 | currentFailures++; 109 | // try again 110 | updateWeather(); 111 | } else { 112 | // until we get too many failures, at which point give up 113 | currentFailures = 0; 114 | } 115 | } 116 | 117 | function locationSuccess(pos) { 118 | getCurrentWeatherProvider().getWeatherFromCoords(pos); 119 | 120 | if(isForecastNeeded()) { 121 | setTimeout(function() { getCurrentWeatherProvider().getForecastFromCoords(pos); }, 5000); 122 | } 123 | } 124 | 125 | function isForecastNeeded() { 126 | var enableForecast = window.localStorage.getItem('enable_forecast'); 127 | var lastForecastTime = window.localStorage.getItem('last_forecast_time'); 128 | var forecastAge = Date.now() - lastForecastTime; 129 | 130 | console.log("Forecast requested! Age is " + forecastAge); 131 | 132 | if(enableForecast === 'yes' && forecastAge > FORECAST_MAX_AGE) { 133 | return true; 134 | } else { 135 | return false; 136 | } 137 | } 138 | 139 | function sendWeatherToPebble(dictionary) { 140 | // Send to Pebble 141 | Pebble.sendAppMessage(dictionary, 142 | function(e) { 143 | console.log('Weather info sent to Pebble successfully!'); 144 | }, 145 | function(e) { 146 | // if we fail, wait a couple seconds, then try again 147 | if(currentFailures < failureRetryAmount) { 148 | // call it again somewhere between 3 and 10 seconds 149 | setTimeout(updateWeather, Math.floor(Math.random() * 10000) + 3000); 150 | 151 | currentFailures++; 152 | } else { 153 | currentFailures = 0; 154 | } 155 | 156 | console.log('Error sending weather info to Pebble! Count: #' + currentFailures); 157 | } 158 | ); 159 | } 160 | 161 | var xhrRequest = function (url, type, callback) { 162 | var xhr = new XMLHttpRequest(); 163 | xhr.onload = function () { 164 | callback(this.responseText); 165 | }; 166 | xhr.open(type, url); 167 | xhr.send(); 168 | }; 169 | 170 | // the individual weather providers need access to the weather icons 171 | module.exports.icons = WeatherIcons; 172 | 173 | // utility functions common to all weather providers 174 | module.exports.xhrRequest = xhrRequest; 175 | module.exports.sendWeatherToPebble = sendWeatherToPebble; 176 | 177 | // called by app.js 178 | // updates the weather if needed, respecting all provider settings in localStorage 179 | module.exports.updateWeather = updateWeather; 180 | -------------------------------------------------------------------------------- /src/pkjs/weather_owm.js: -------------------------------------------------------------------------------- 1 | // this contains our offical OWM weather key, hidden from prying eyes 2 | var secrets = require('./secrets'); 3 | var weatherCommon = require('./weather'); 4 | 5 | // "public" functions 6 | 7 | module.exports.getWeather = getWeather; 8 | module.exports.getWeatherFromCoords = getWeatherFromCoords; 9 | module.exports.getForecast = getForecast; 10 | module.exports.getForecastFromCoords = getForecastFromCoords; 11 | 12 | function getWeather(weatherLoc) { 13 | var url = 'http://api.openweathermap.org/data/2.5/weather?q=' + 14 | encodeURIComponent(weatherLoc) + '&units=metric&appid=' + secrets.OWM_APP_ID; 15 | 16 | getAndSendCurrentWeather(url); 17 | } 18 | 19 | function getWeatherFromCoords(pos) { 20 | // Construct URL 21 | var url = 'http://api.openweathermap.org/data/2.5/weather?lat=' + 22 | pos.coords.latitude + '&lon=' + pos.coords.longitude + '&units=metric&appid=' + secrets.OWM_APP_ID; 23 | console.log(url); 24 | 25 | getAndSendCurrentWeather(url); 26 | } 27 | 28 | function getForecast(weatherLoc) { 29 | var forecastURL = 'http://api.openweathermap.org/data/2.5/forecast?q=' + 30 | encodeURIComponent(weatherLoc) + '&cnt=8&units=metric&appid=' + secrets.OWM_APP_ID; 31 | 32 | getAndSendWeatherForecast(forecastURL); 33 | } 34 | 35 | function getForecastFromCoords(pos) { 36 | var forecastURL = 'http://api.openweathermap.org/data/2.5/forecast?lat=' + 37 | pos.coords.latitude + '&lon=' + pos.coords.longitude + '&cnt=8&units=metric&appid=' + secrets.OWM_APP_ID; 38 | 39 | getAndSendWeatherForecast(forecastURL); 40 | } 41 | 42 | // "private" functions 43 | 44 | // accepts an openweathermap url, gets weather data from it, and sends it to the watch 45 | function getAndSendCurrentWeather(url) { 46 | weatherCommon.xhrRequest(url, 'GET', 47 | function(responseText) { 48 | // responseText contains a JSON object with weather info 49 | var json = JSON.parse(responseText); 50 | 51 | if(json.cod == "200") { 52 | var temperature = Math.round(json.main.temp); 53 | console.log('Temperature is ' + temperature); 54 | 55 | // Conditions 56 | var conditionCode = json.weather[0].id; 57 | console.log('Condition code is ' + conditionCode); 58 | 59 | // night state 60 | var isNight = (json.weather[0].icon.slice(-1) == 'n') ? 1 : 0; 61 | 62 | var iconToLoad = getIconForConditionCode(conditionCode, isNight); 63 | 64 | // Assemble dictionary using our keys 65 | var dictionary = { 66 | 'WeatherTemperature': temperature, 67 | 'WeatherCondition': iconToLoad 68 | }; 69 | 70 | console.log(JSON.stringify(dictionary)); 71 | 72 | weatherCommon.sendWeatherToPebble(dictionary); 73 | } 74 | }); 75 | } 76 | 77 | function getAndSendWeatherForecast(url) { 78 | console.log(url); 79 | weatherCommon.xhrRequest(url, 'GET', 80 | function(responseText) { 81 | // responseText contains a JSON object with weather info 82 | var json = JSON.parse(responseText); 83 | 84 | if(json.cod == "200") { 85 | var forecast = extractFakeDailyForecast(json); 86 | 87 | console.log('Forecast high/low temps are ' + forecast.highTemp + '/' + forecast.lowTemp); 88 | 89 | // Conditions 90 | var conditionCode = forecast.condition; 91 | console.log('Forecast condition is ' + conditionCode); 92 | 93 | var iconToLoad = getIconForConditionCode(conditionCode, false); 94 | 95 | // Assemble dictionary using our keys 96 | var dictionary = { 97 | 'WeatherForecastCondition': iconToLoad, 98 | 'WeatherForecastHighTemp': forecast.highTemp, 99 | 'WeatherForecastLowTemp': forecast.lowTemp 100 | }; 101 | 102 | console.log(JSON.stringify(dictionary)); 103 | 104 | weatherCommon.sendWeatherToPebble(dictionary); 105 | } 106 | }); 107 | } 108 | 109 | function getIconForConditionCode(conditionCode, isNight) { 110 | var generalCondition = Math.floor(conditionCode / 100); 111 | 112 | // determine the correct icon 113 | switch(generalCondition) { 114 | case 2: //thunderstorm 115 | iconToLoad = weatherCommon.icons.THUNDERSTORM; 116 | break; 117 | case 3: //drizzle 118 | iconToLoad = weatherCommon.icons.LIGHT_RAIN; 119 | break; 120 | case 5: //rain 121 | if(conditionCode == 500) { 122 | iconToLoad = weatherCommon.icons.LIGHT_RAIN; 123 | } else if(conditionCode < 505) { 124 | iconToLoad = weatherCommon.icons.HEAVY_RAIN; 125 | } else if(conditionCode == 511) { 126 | iconToLoad = weatherCommon.icons.RAINING_AND_SNOWING; 127 | } else { 128 | iconToLoad = weatherCommon.icons.LIGHT_RAIN; 129 | } 130 | break; 131 | case 6: //snow 132 | if(conditionCode == 600 || conditionCode == 620) { 133 | iconToLoad = weatherCommon.icons.LIGHT_SNOW; 134 | } else if(conditionCode > 610 && conditionCode < 620) { 135 | iconToLoad = weatherCommon.icons.RAINING_AND_SNOWING; 136 | } else { 137 | iconToLoad = weatherCommon.icons.HEAVY_SNOW; 138 | } 139 | break; 140 | case 7: // fog, dust, etc 141 | iconToLoad = weatherCommon.icons.CLOUDY_DAY; 142 | break; 143 | case 8: // clouds 144 | if(conditionCode == 800) { 145 | iconToLoad = (!isNight) ? weatherCommon.icons.CLEAR_DAY : weatherCommon.icons.CLEAR_NIGHT; 146 | } else if(conditionCode < 803) { 147 | iconToLoad = (!isNight) ? weatherCommon.icons.PARTLY_CLOUDY : weatherCommon.icons.PARTLY_CLOUDY_NIGHT; 148 | } else { 149 | iconToLoad = weatherCommon.icons.CLOUDY_DAY; 150 | } 151 | break; 152 | default: 153 | iconToLoad = weatherCommon.icons.WEATHER_GENERIC; 154 | break; 155 | } 156 | 157 | return iconToLoad; 158 | } 159 | 160 | /* 161 | Attempts to approximate a daily forecast by interpolating the next 162 | 24 hours worth of 3 hour forecasts :-O 163 | */ 164 | function extractFakeDailyForecast(json) { 165 | var todaysForecast = {}; 166 | 167 | // find the max and min of those temperatures 168 | todaysForecast.highTemp = -Number.MAX_SAFE_INTEGER; 169 | todaysForecast.lowTemp = Number.MAX_SAFE_INTEGER; 170 | 171 | for(var i = 0; i < json.list.length; i++) { 172 | if(todaysForecast.highTemp < json.list[i].main.temp_max) { 173 | todaysForecast.highTemp = json.list[i].main.temp_max; 174 | } 175 | 176 | if(todaysForecast.lowTemp > json.list[i].main.temp_min) { 177 | todaysForecast.lowTemp = json.list[i].main.temp_min; 178 | } 179 | } 180 | 181 | // we can't really "average" conditions, so we'll just cheat and use...one of them :-O 182 | todaysForecast.condition = json.list[3].weather[0].id; 183 | 184 | return todaysForecast; 185 | } 186 | -------------------------------------------------------------------------------- /src/pkjs/weather_wunderground.js: -------------------------------------------------------------------------------- 1 | var weatherCommon = require('./weather'); 2 | 3 | // "public" functions 4 | 5 | module.exports.getWeather = getWeather; 6 | module.exports.getWeatherFromCoords = getWeatherFromCoords; 7 | module.exports.getForecast = getForecast; 8 | module.exports.getForecastFromCoords = getForecastFromCoords; 9 | 10 | function getWeather(weatherLoc) { 11 | var apiKey = window.localStorage.getItem('weather_api_key'); 12 | 13 | var url = 'http://api.wunderground.com/api/' + apiKey + 14 | '/conditions/q/' + encodeURIComponent(weatherLoc) + '.json'; 15 | 16 | getAndSendCurrentWeather(url); 17 | } 18 | 19 | function getWeatherFromCoords(pos) { 20 | var apiKey = window.localStorage.getItem('weather_api_key'); 21 | 22 | // Construct URL 23 | var url = 'http://api.wunderground.com/api/' + apiKey + 24 | '/conditions/q/' + pos.coords.latitude + ',' + pos.coords.longitude + '.json'; 25 | 26 | getAndSendCurrentWeather(url); 27 | } 28 | 29 | function getForecast(weatherLoc) { 30 | var apiKey = window.localStorage.getItem('weather_api_key'); 31 | 32 | var forecastURL = 'http://api.wunderground.com/api/' + apiKey + 33 | '/forecast/q/' + encodeURIComponent(weatherLoc) + '.json'; 34 | 35 | getAndSendWeatherForecast(forecastURL); 36 | } 37 | 38 | function getForecastFromCoords(pos) { 39 | var apiKey = window.localStorage.getItem('weather_api_key'); 40 | 41 | var forecastURL = 'http://api.wunderground.com/api/' + apiKey + 42 | '/forecast/q/' + pos.coords.latitude + ',' + pos.coords.longitude + '.json'; 43 | 44 | getAndSendWeatherForecast(forecastURL); 45 | } 46 | 47 | // "private" functions 48 | 49 | // accepts a wunderground conditions url, gets weather data from it, and sends it to the watch 50 | function getAndSendCurrentWeather(url) { 51 | weatherCommon.xhrRequest(url, 'GET', 52 | function(responseText) { 53 | // responseText contains a JSON object with weather info 54 | var json = JSON.parse(responseText); 55 | 56 | if(json.response.features.conditions == 1) { 57 | var temperature = Math.round(json.current_observation.temp_c); 58 | console.log('Temperature is ' + temperature); 59 | 60 | // Conditions 61 | var conditionCode = json.current_observation.icon; 62 | console.log('Condition icon is ' + conditionCode); 63 | 64 | // night state 65 | var isNight = false; 66 | 67 | if(json.current_observation.icon_url.indexOf('nt_') != -1) { 68 | isNight = true; 69 | } 70 | 71 | var iconToLoad = getIconForConditionCode(conditionCode, isNight); 72 | console.log('were loading this icon:' + iconToLoad); 73 | 74 | // Assemble dictionary using our keys 75 | var dictionary = { 76 | 'WeatherTemperature': temperature, 77 | 'WeatherCondition': iconToLoad 78 | }; 79 | 80 | console.log(JSON.stringify(dictionary)); 81 | 82 | weatherCommon.sendWeatherToPebble(dictionary); 83 | } 84 | }); 85 | } 86 | 87 | function getAndSendWeatherForecast(url) { 88 | console.log(url); 89 | weatherCommon.xhrRequest(url, 'GET', 90 | function(responseText) { 91 | // responseText contains a JSON object with weather info 92 | var json = JSON.parse(responseText); 93 | 94 | if(json.response.features.forecast == 1) { 95 | var todaysForecast = json.forecast.simpleforecast.forecastday[0]; 96 | 97 | var highTemp = parseInt(todaysForecast.high.celsius, 10); 98 | var lowTemp = parseInt(todaysForecast.low.celsius, 10); 99 | 100 | console.log('Forecast high/low temps are ' + highTemp + '/' + lowTemp); 101 | 102 | // Conditions 103 | var conditionCode = todaysForecast.icon; 104 | console.log('Forecast icon is ' + conditionCode); 105 | 106 | var iconToLoad = getIconForConditionCode(conditionCode, false); 107 | 108 | // Assemble dictionary using our keys 109 | var dictionary = { 110 | 'WeatherForecastCondition': iconToLoad, 111 | 'WeatherForecastHighTemp': highTemp, 112 | 'WeatherForecastLowTemp': lowTemp 113 | }; 114 | 115 | console.log(JSON.stringify(dictionary)); 116 | 117 | weatherCommon.sendWeatherToPebble(dictionary); 118 | } 119 | }); 120 | } 121 | 122 | function getIconForConditionCode(conditionCode, isNight) { 123 | 124 | // determine the correct icon 125 | switch(conditionCode) { 126 | case 'chanceflurries': 127 | iconToLoad = weatherCommon.icons.LIGHT_SNOW; 128 | break; 129 | case 'chancerain': 130 | iconToLoad = weatherCommon.icons.LIGHT_RAIN; 131 | break; 132 | case 'chancesleet': 133 | iconToLoad = weatherCommon.icons.RAINING_AND_SNOWING; 134 | break; 135 | case 'chancesnow': 136 | iconToLoad = weatherCommon.icons.LIGHT_SNOW; 137 | break; 138 | case 'chancetstorms': 139 | iconToLoad = weatherCommon.icons.THUNDERSTORM; 140 | break; 141 | case 'clear': 142 | iconToLoad = (!isNight) ? weatherCommon.icons.CLEAR_DAY : weatherCommon.icons.CLEAR_NIGHT; 143 | break; 144 | case 'cloudy': 145 | iconToLoad = weatherCommon.icons.CLOUDY_DAY; 146 | break; 147 | case 'flurries': 148 | iconToLoad = weatherCommon.icons.LIGHT_SNOW; 149 | break; 150 | case 'fog': 151 | case 'hazy': 152 | case 'mostlycloudy': 153 | iconToLoad = weatherCommon.icons.CLOUDY_DAY; 154 | break; 155 | case 'mostlysunny': 156 | iconToLoad = (!isNight) ? weatherCommon.icons.CLEAR_DAY : weatherCommon.icons.CLEAR_NIGHT; 157 | break; 158 | case 'partlycloudy': 159 | iconToLoad = (!isNight) ? weatherCommon.icons.PARTLY_CLOUDY : weatherCommon.icons.PARTLY_CLOUDY_NIGHT; 160 | break; 161 | case 'partlysunny': 162 | iconToLoad = (!isNight) ? weatherCommon.icons.PARTLY_CLOUDY : weatherCommon.icons.PARTLY_CLOUDY_NIGHT; 163 | break; 164 | case 'sleet': 165 | iconToLoad = weatherCommon.icons.RAINING_AND_SNOWING; 166 | break; 167 | case 'rain': 168 | iconToLoad = weatherCommon.icons.HEAVY_RAIN; 169 | break; 170 | case 'snow': 171 | iconToLoad = weatherCommon.icons.HEAVY_SNOW; 172 | break; 173 | case 'sunny': 174 | iconToLoad = (!isNight) ? weatherCommon.icons.CLEAR_DAY : weatherCommon.icons.CLEAR_NIGHT; 175 | break; 176 | case 'tstorms': 177 | iconToLoad = weatherCommon.icons.THUNDERSTORM; 178 | break; 179 | default: 180 | iconToLoad = weatherCommon.icons.WEATHER_GENERIC; 181 | console.log("Warning: No icon found for " + conditionCode); 182 | break; 183 | } 184 | 185 | return iconToLoad; 186 | } 187 | -------------------------------------------------------------------------------- /tools/fctx-compiler_regex.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Generate regex for fctx-compiler from languages.c data 3 | # fctx-compiler file -r [,.0-9:\;A-Za-z일ÁăầÂÅäÄĄČÇéÉĚĒĞÍİÑÓÖØŘŚŠŞÚÛÜŪýŹžŽאבגדהוטילמנספץצקרשΑΒβΓΔδΕεΙΪΚΛΜΝΟΠΡΣΤΥΦАБВГдДеЕжЖиИІЙКЛМнНОПРСТУФЧЮЯ一三二五六周四土日月木水火週金] 4 | # fctx-compiler file -r [0-9:\ ] 5 | # Version 0.1 - 29/12/2017 6 | 7 | input_file=$1 8 | tmp_file="/tmp/$$.tmp" 9 | output_file="./output.txt" 10 | 11 | cat $input_file | while read line ; do 12 | r=1 13 | tmp=$(echo $line | awk '{print substr($0,'"$r"',1)}') 14 | 15 | 16 | while [ "$r" -le "${#line}" ]; do 17 | echo "$tmp" >> $tmp_file 18 | r=$((r+1)) 19 | tmp=$(echo $line | awk '{print substr($0,'"$r"',1)}') 20 | done 21 | done 22 | sort -u $tmp_file | tr -d '\n' > $output_file 23 | echo >> $output_file 24 | rm $tmp_file 25 | -------------------------------------------------------------------------------- /tools/frankenpebble.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Command line example: python ./frankenpebble.py "./aplite.pbw" "./modern.pbw" "out.pbw" 4 | 5 | 6 | import zipfile 7 | import os 8 | import argparse 9 | import logging 10 | 11 | def copy_zip_file(filename, zip_from, zip_to): 12 | logging.info("Copying (%s --> %s): %s ", zip_from.filename, zip_to.filename, filename) 13 | zip_to.writestr(filename, zip_from.read(filename)) 14 | 15 | def copy_zip_directories(directories, zip_from, zip_to): 16 | """ 17 | Copy a list of directories from one zip to another 18 | :param directories: a list of directories to copy over 19 | :param zip_to: zip to copy to 20 | :param zip_from: zip to copy from 21 | :return: yields successfully copied filenames 22 | """ 23 | for f in zip_from.infolist(): 24 | # Copy the file if it is one level deep in a directory with one of the requested names 25 | if os.path.split(f.filename)[0] in directories: 26 | copy_zip_file(f.filename, zip_from, zip_to) 27 | yield f.filename 28 | 29 | def combine_zips(output_filename, zips_and_folders, default_filename): 30 | """ Copy folders from various zip files to one zip file. Then, 31 | for any filenames not copied in that step, copy remaining files 32 | from a default zipfile 33 | :param output: name of zip file to create 34 | :param pbws: map of 'zipfiles'->[foldernames] 35 | :param default: name of zip file to copy all 'other' files from 36 | """ 37 | with zipfile.ZipFile(output_filename, 'w') as output: 38 | copied_files = set() 39 | # Copy the requested folders from each zip in to the output zip 40 | for filename, folders in zips_and_folders.iteritems(): 41 | with zipfile.ZipFile(filename, 'r') as input: 42 | input.filename = filename 43 | # Remember which files we've copied 44 | copied_files.update(copy_zip_directories(folders, input, output)) 45 | 46 | # Then copy remaining files from the default zip to the output zip 47 | with zipfile.ZipFile(default_filename, 'r') as input: 48 | input.filename = default_filename 49 | for f in input.infolist(): 50 | if f.filename not in copied_files and '__MACOSX' not in f.filename: 51 | copy_zip_file(f.filename, input, output) 52 | 53 | def main(): 54 | parser = argparse.ArgumentParser(description="Combine two PBWs, one targeted toward Aplite, and one targeted toward everything else, into one franken-PBW") 55 | parser.add_argument('legacypbw', help='path to .pbw file for Aplite') 56 | parser.add_argument('modernpbw', help='path to .pbw file for Basalt, Chalk, and Diorite') 57 | parser.add_argument('out', help='filename for output file') 58 | parser.add_argument('-v', '--verbose', action="store_true", help="Output all copy operations") 59 | args = parser.parse_args() 60 | if args.verbose: 61 | logging.basicConfig(level=logging.DEBUG) 62 | 63 | combine_zips(args.out, { 64 | args.legacypbw: ['aplite'], 65 | args.modernpbw: ['basalt','chalk','diorite','emery'] 66 | }, args.modernpbw) 67 | 68 | if __name__ == '__main__': 69 | main() 70 | -------------------------------------------------------------------------------- /tools/localDates.py: -------------------------------------------------------------------------------- 1 | import locale 2 | import time 3 | 4 | locales = ['en_US', 5 | 'fr_FR', 6 | 'de_DE', 7 | 'es_ES', 8 | 'it_IT', 9 | 'nl_NL', 10 | 'pt_BR', 11 | 'tr_TR', 12 | 'cs_CZ', 13 | 'el_GR', 14 | 'sv_SE', 15 | 'pl_PL', 16 | 'sk_SK', 17 | 'ro_RO', 18 | 'ca_ES', 19 | 'no_NO', 20 | 'ru_RU', 21 | 'et_EE', 22 | 'eu_ES', 23 | 'fi_FI', 24 | 'da_DK', 25 | 'lt_LT', 26 | 'sl_SI', 27 | 'hu_HU', 28 | 'hr_HR', 29 | 'lv_LV']; 30 | 31 | timeData = list(time.localtime()); 32 | 33 | for l in locales: 34 | print(l) 35 | locale.setlocale(locale.LC_ALL, l) 36 | 37 | days = u'{'; 38 | for i in [6, 0, 1, 2, 3, 4, 5]: 39 | timeData[6] = i; 40 | days += time.strftime('"%a", ', tuple(timeData)).upper(); 41 | days += u'}'; 42 | print(days); 43 | 44 | months = u'{'; 45 | for i in range(1, 13): 46 | timeData[1] = i; 47 | months += time.strftime('"%b", ', tuple(timeData)).upper(); 48 | months += u'}'; 49 | print(months); 50 | -------------------------------------------------------------------------------- /wscript: -------------------------------------------------------------------------------- 1 | # 2 | # This file is the default set of rules to compile a Pebble application. 3 | # 4 | # Feel free to customize this to your needs. 5 | # 6 | import os.path 7 | 8 | top = '.' 9 | out = 'build' 10 | 11 | 12 | def options(ctx): 13 | ctx.load('pebble_sdk') 14 | 15 | 16 | def configure(ctx): 17 | """ 18 | This method is used to configure your build. ctx.load(`pebble_sdk`) automatically configures 19 | a build for each valid platform in `targetPlatforms`. Platform-specific configuration: add your 20 | change after calling ctx.load('pebble_sdk') and make sure to set the correct environment first. 21 | Universal configuration: add your change prior to calling ctx.load('pebble_sdk'). 22 | """ 23 | ctx.load('pebble_sdk') 24 | 25 | 26 | def build(ctx): 27 | ctx.load('pebble_sdk') 28 | 29 | build_worker = os.path.exists('worker_src') 30 | binaries = [] 31 | 32 | cached_env = ctx.env 33 | for platform in ctx.env.TARGET_PLATFORMS: 34 | ctx.env = ctx.all_envs[platform] 35 | ctx.set_group(ctx.env.PLATFORM_NAME) 36 | app_elf = '{}/pebble-app.elf'.format(ctx.env.BUILD_DIR) 37 | ctx.pbl_build(source=ctx.path.ant_glob('src/c/**/*.c'), target=app_elf, bin_type='app') 38 | 39 | if build_worker: 40 | worker_elf = '{}/pebble-worker.elf'.format(ctx.env.BUILD_DIR) 41 | binaries.append({'platform': platform, 'app_elf': app_elf, 'worker_elf': worker_elf}) 42 | ctx.pbl_build(source=ctx.path.ant_glob('worker_src/c/**/*.c'), 43 | target=worker_elf, 44 | bin_type='worker') 45 | else: 46 | binaries.append({'platform': platform, 'app_elf': app_elf}) 47 | ctx.env = cached_env 48 | 49 | ctx.set_group('bundle') 50 | ctx.pbl_bundle(binaries=binaries, 51 | js=ctx.path.ant_glob(['src/pkjs/**/*.js', 52 | 'src/pkjs/**/*.json', 53 | 'src/common/**/*.js']), 54 | js_entry_file='src/pkjs/index.js') 55 | --------------------------------------------------------------------------------