├── .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 |
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 |
--------------------------------------------------------------------------------