diff --git a/always-on-source/AlwaysOnDisplay.mc b/always-on-source/AlwaysOnDisplay.mc
index eb1f5093..d5be9952 100644
--- a/always-on-source/AlwaysOnDisplay.mc
+++ b/always-on-source/AlwaysOnDisplay.mc
@@ -1,201 +1,212 @@
-using Toybox.WatchUi as Ui;
-using Toybox.System as Sys;
-using Toybox.Application as App;
-using Toybox.Time;
-using Toybox.Time.Gregorian;
-
-// Draw time, line, date, battery.
-// Combine stripped down versions of ThickThinTime and DateLine.
-// Change vertical offset every minute to comply with burn-in protection requirements.
-class AlwaysOnDisplay extends Ui.Drawable {
-
- private var mBurnInYOffsets;
- private var mHoursFont, mMinutesFont, mSecondsFont, mDateFont, mBatteryFont;
-
- // Wide rectangle: time should be moved up slightly to centre within available space.
- private var mAdjustY = 0;
-
- private var mTimeY;
- private var mLineY;
- private var mLineWidth;
- private var mLineStroke;
- private var mDataY;
- private var mDataLeft;
-
- private var AM_PM_X_OFFSET = 2;
-
- private var mDayOfWeek;
- private var mDayOfWeekString;
-
- private var mMonth;
- private var mMonthString;
-
- function initialize(params) {
- Drawable.initialize(params);
-
- mBurnInYOffsets = params[:burnInYOffsets];
-
- if (params[:adjustY] != null) {
- mAdjustY = params[:adjustY];
- }
-
- if (params[:amPmOffset] != null) {
- AM_PM_X_OFFSET = params[:amPmOffset];
- }
-
- mTimeY = params[:timeY];
- mLineY = params[:lineY];
- mLineWidth = params[:lineWidth];
- //mLineStroke = params[:lineStroke];
- mDataY = params[:dataY];
- mDataLeft = params[:dataLeft];
-
- mHoursFont = Ui.loadResource(Rez.Fonts.AlwaysOnHoursFont);
- mMinutesFont = Ui.loadResource(Rez.Fonts.AlwaysOnMinutesFont);
- mSecondsFont = Ui.loadResource(Rez.Fonts.AlwaysOnSecondsFont);
- mBatteryFont = Ui.loadResource(Rez.Fonts.AlwaysOnBatteryFont);
-
- var rezFonts = Rez.Fonts;
- var resourceMap = {
- "ZHS" => rezFonts.AlwaysOnDateFontOverrideZHS,
- "ZHT" => rezFonts.AlwaysOnDateFontOverrideZHT,
- "RUS" => rezFonts.AlwaysOnDateFontOverrideRUS
- };
-
- // Unfortunate: because fonts can't be overridden based on locale, we have to read in current locale as manually-specified
- // string, then override font in code.
- var dateFontOverride = Ui.loadResource(Rez.Strings.DATE_FONT_OVERRIDE);
- var dateFont = (resourceMap.hasKey(dateFontOverride)) ? resourceMap[dateFontOverride] : rezFonts.AlwaysOnDateFont;
- mDateFont = Ui.loadResource(dateFont);
- }
-
- function draw(dc) {
-
- // TIME.
- var clockTime = Sys.getClockTime();
- var formattedTime = App.getApp().getFormattedTime(clockTime.hour, clockTime.min);
- formattedTime[:amPm] = formattedTime[:amPm].toUpper();
-
- // Change vertical offset every minute.
- var burnInYOffset = mBurnInYOffsets[clockTime.min % mBurnInYOffsets.size()] + (clockTime.min - 30);
-
- var hours = formattedTime[:hour];
- var minutes = formattedTime[:min];
- var amPmText = formattedTime[:amPm];
-
- var halfDCWidth = dc.getWidth() / 2;
-
- // Centre combined hours and minutes text (not the same as right-aligning hours and left-aligning minutes).
- // Font has tabular figures (monospaced numbers) even across different weights, so does not matter which of hours or
- // minutes font is used to calculate total width.
- var totalWidth = dc.getTextWidthInPixels(hours + minutes, mHoursFont);
- var x = halfDCWidth - (totalWidth / 2);
- var y = mTimeY + mAdjustY + burnInYOffset;
-
- dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
-
- // Hours.
- dc.drawText(
- x,
- y,
- mHoursFont,
- hours,
- Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER
- );
- x += dc.getTextWidthInPixels(hours, mHoursFont);
-
- // Minutes.
- dc.drawText(
- x,
- y,
- mMinutesFont,
- minutes,
- Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER
- );
-
- // If required, draw AM/PM after minutes, vertically centred.
- if (amPmText.length() > 0) {
- x += dc.getTextWidthInPixels(minutes, mMinutesFont);
- dc.drawText(
- x + AM_PM_X_OFFSET, // Breathing space between minutes and AM/PM.
- y,
- mSecondsFont,
- amPmText,
- Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER
- );
- }
-
- // LINE.
- y = mLineY + burnInYOffset;
- dc.setPenWidth(/* mLineStroke */ 2);
- dc.drawLine(halfDCWidth - (mLineWidth / 2), y, halfDCWidth + (mLineWidth / 2), y);
-
- // DATA.
- var rezStrings = Rez.Strings;
- var resourceArray;
-
- // Supply DOW/month strings ourselves, rather than relying on Time.FORMAT_MEDIUM, as latter is inconsistent e.g. returns
- // "Thurs" instead of "Thu".
- // Load strings just-in-time, to save memory. They rarely change, so worthwhile trade-off.
- var now = Gregorian.info(Time.now(), Time.FORMAT_SHORT);
-
- var dayOfWeek = now.day_of_week;
- if (dayOfWeek != mDayOfWeek) {
- mDayOfWeek = dayOfWeek;
-
- resourceArray = [
- rezStrings.Sun,
- rezStrings.Mon,
- rezStrings.Tue,
- rezStrings.Wed,
- rezStrings.Thu,
- rezStrings.Fri,
- rezStrings.Sat
- ];
- mDayOfWeekString = Ui.loadResource(resourceArray[mDayOfWeek - 1]).toUpper();
- }
-
- var month = now.month;
- if (month != mMonth) {
- mMonth = month;
-
- resourceArray = [
- rezStrings.Jan,
- rezStrings.Feb,
- rezStrings.Mar,
- rezStrings.Apr,
- rezStrings.May,
- rezStrings.Jun,
- rezStrings.Jul,
- rezStrings.Aug,
- rezStrings.Sep,
- rezStrings.Oct,
- rezStrings.Nov,
- rezStrings.Dec
- ];
- mMonthString = Ui.loadResource(resourceArray[mMonth - 1]).toUpper();
- }
-
- var day = now.day.format(INTEGER_FORMAT);
-
- // Date.
- y = mDataY + burnInYOffset;
- dc.drawText(
- mDataLeft,
- y,
- mDateFont,
- Lang.format("$1$ $2$ $3$", [mDayOfWeekString, day, mMonthString]),
- Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER
- );
-
- // Battery.
- var battery = Math.floor(Sys.getSystemStats().battery);
- dc.drawText(
- dc.getWidth() - mDataLeft,
- y,
- mBatteryFont,
- battery.format(INTEGER_FORMAT) + "%",
- Graphics.TEXT_JUSTIFY_RIGHT | Graphics.TEXT_JUSTIFY_VCENTER
- );
- }
+using Toybox.WatchUi as Ui;
+using Toybox.System as Sys;
+using Toybox.Application as App;
+using Toybox.Time;
+using Toybox.Time.Gregorian;
+
+// Draw time, line, date, battery.
+// Combine stripped down versions of ThickThinTime and DateLine.
+// Change vertical offset every minute to comply with burn-in protection requirements.
+class AlwaysOnDisplay extends Ui.Drawable {
+
+ private var mBurnInYOffsets;
+ private var mHoursFont, mMinutesFont, mSecondsFont, mDateFont, mBatteryFont;
+
+ // Wide rectangle: time should be moved up slightly to centre within available space.
+ private var mAdjustY = 0;
+
+ private var mTimeY;
+ private var mLineY;
+ private var mLineWidth;
+ private var mLineStroke;
+ private var mDataY;
+ private var mDataLeft;
+
+ private var AM_PM_X_OFFSET = 2;
+
+ private var mDayOfWeek;
+ private var mDayOfWeekString;
+
+ private var mMonth;
+ private var mMonthString;
+
+ function initialize(params) {
+ Drawable.initialize(params);
+
+ mBurnInYOffsets = params[:burnInYOffsets];
+
+ if (params[:adjustY] != null) {
+ mAdjustY = params[:adjustY];
+ }
+
+ if (params[:amPmOffset] != null) {
+ AM_PM_X_OFFSET = params[:amPmOffset];
+ }
+
+ mTimeY = params[:timeY];
+ mLineY = params[:lineY];
+ mLineWidth = params[:lineWidth];
+ //mLineStroke = params[:lineStroke];
+ mDataY = params[:dataY];
+ mDataLeft = params[:dataLeft];
+
+ mHoursFont = Ui.loadResource(Rez.Fonts.AlwaysOnHoursFont);
+ mMinutesFont = Ui.loadResource(Rez.Fonts.AlwaysOnMinutesFont);
+ mSecondsFont = Ui.loadResource(Rez.Fonts.AlwaysOnSecondsFont);
+ mBatteryFont = Ui.loadResource(Rez.Fonts.AlwaysOnBatteryFont);
+
+ var rezFonts = Rez.Fonts;
+ var resourceMap = {
+ "ZHS" => rezFonts.AlwaysOnDateFontOverrideZHS,
+ "ZHT" => rezFonts.AlwaysOnDateFontOverrideZHT,
+ "RUS" => rezFonts.AlwaysOnDateFontOverrideRUS
+ };
+
+ // Unfortunate: because fonts can't be overridden based on locale, we have to read in current locale as manually-specified
+ // string, then override font in code.
+ var dateFontOverride = Ui.loadResource(Rez.Strings.DATE_FONT_OVERRIDE);
+ var dateFont = (resourceMap.hasKey(dateFontOverride)) ? resourceMap[dateFontOverride] : rezFonts.AlwaysOnDateFont;
+ mDateFont = Ui.loadResource(dateFont);
+ }
+
+ function draw(dc) {
+
+ // TIME.
+ var clockTime = Sys.getClockTime();
+ var formattedTime = App.getApp().getFormattedTime(clockTime.hour, clockTime.min);
+ formattedTime[:amPm] = formattedTime[:amPm].toUpper();
+
+ // Change vertical offset every minute.
+ var burnInYOffset = mBurnInYOffsets[clockTime.min % mBurnInYOffsets.size()] + (clockTime.min - 30);
+
+ var hours = formattedTime[:hour];
+ var minutes = formattedTime[:min];
+ var amPmText = formattedTime[:amPm];
+ var colon = ":"; // SG Addition
+
+ var halfDCWidth = dc.getWidth() / 2;
+
+ // Centre combined hours and minutes text (not the same as right-aligning hours and left-aligning minutes).
+ // Font has tabular figures (monospaced numbers) even across different weights, so does not matter which of hours or
+ // minutes font is used to calculate total width.
+ var totalWidth = dc.getTextWidthInPixels(hours + colon + minutes, mHoursFont); // SG Added colon
+ var x = halfDCWidth - (totalWidth / 2);
+ var y = mTimeY + mAdjustY + burnInYOffset;
+
+ dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
+
+ // Hours.
+ dc.drawText(
+ x,
+ y,
+ mHoursFont,
+ hours,
+ Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER
+ );
+ x += dc.getTextWidthInPixels(hours, mHoursFont);
+
+ // SG Addition - Colon.
+ dc.drawText(
+ x,
+ y,
+ mHoursFont,
+ colon,
+ Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER
+ );
+ x += dc.getTextWidthInPixels(colon, mHoursFont);
+
+ // Minutes.
+ dc.drawText(
+ x,
+ y,
+ mMinutesFont,
+ minutes,
+ Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER
+ );
+
+ // If required, draw AM/PM after minutes, vertically centred.
+ if (amPmText.length() > 0) {
+ x += dc.getTextWidthInPixels(minutes, mMinutesFont);
+ dc.drawText(
+ x + AM_PM_X_OFFSET, // Breathing space between minutes and AM/PM.
+ y,
+ mSecondsFont,
+ amPmText,
+ Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER
+ );
+ }
+
+ // LINE.
+ y = mLineY + burnInYOffset;
+ dc.setPenWidth(/* mLineStroke */ 1); // SG From 2 to 1
+ dc.drawLine(halfDCWidth - (mLineWidth / 2), y, halfDCWidth + (mLineWidth / 2), y);
+
+ // DATA.
+ var rezStrings = Rez.Strings;
+ var resourceArray;
+
+ // Supply DOW/month strings ourselves, rather than relying on Time.FORMAT_MEDIUM, as latter is inconsistent e.g. returns
+ // "Thurs" instead of "Thu".
+ // Load strings just-in-time, to save memory. They rarely change, so worthwhile trade-off.
+ var now = Gregorian.info(Time.now(), Time.FORMAT_SHORT);
+
+ var dayOfWeek = now.day_of_week;
+ if (dayOfWeek != mDayOfWeek) {
+ mDayOfWeek = dayOfWeek;
+
+ resourceArray = [
+ rezStrings.Sun,
+ rezStrings.Mon,
+ rezStrings.Tue,
+ rezStrings.Wed,
+ rezStrings.Thu,
+ rezStrings.Fri,
+ rezStrings.Sat
+ ];
+ mDayOfWeekString = Ui.loadResource(resourceArray[mDayOfWeek - 1]).toUpper();
+ }
+
+ var month = now.month;
+ if (month != mMonth) {
+ mMonth = month;
+
+ resourceArray = [
+ rezStrings.Jan,
+ rezStrings.Feb,
+ rezStrings.Mar,
+ rezStrings.Apr,
+ rezStrings.May,
+ rezStrings.Jun,
+ rezStrings.Jul,
+ rezStrings.Aug,
+ rezStrings.Sep,
+ rezStrings.Oct,
+ rezStrings.Nov,
+ rezStrings.Dec
+ ];
+ mMonthString = Ui.loadResource(resourceArray[mMonth - 1]).toUpper();
+ }
+
+ var day = now.day.format(INTEGER_FORMAT);
+
+ // Date.
+ y = mDataY + burnInYOffset;
+ dc.drawText(
+ mDataLeft,
+ y,
+ mDateFont,
+ Lang.format("$1$ $2$ $3$", [mDayOfWeekString, day, mMonthString]),
+ Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER
+ );
+
+ // Battery.
+ var battery = Math.floor(Sys.getSystemStats().battery);
+ dc.drawText(
+ dc.getWidth() - mDataLeft,
+ y,
+ mBatteryFont,
+ battery.format(INTEGER_FORMAT) + "%",
+ Graphics.TEXT_JUSTIFY_RIGHT | Graphics.TEXT_JUSTIFY_VCENTER
+ );
+ }
}
\ No newline at end of file
diff --git a/resources-fre/strings/strings.xml b/resources-fre/strings/strings.xml
index 87dab35c..168ae70a 100644
--- a/resources-fre/strings/strings.xml
+++ b/resources-fre/strings/strings.xml
@@ -1,81 +1,81 @@
Crystal
- App Version
+ Version de l'appli
- Theme
- Hours Colour
- Minutes Colour
- Left Meter
- Right Meter
- Calories Goal (1-10,000kCal)
- Meter Style
- Meter Digits Style
- Add Local Time in City (Beta)
- Move Bar Style
- Hide Seconds
- Hide Hours Leading Zero
+ Thème
+ Couleur des heures
+ Couleur des minutes
+ Compteur de gauche
+ Compteur de droite
+ Objectif de calories (1-10,000kCal)
+ Apparence des compteurs
+ Apparence des champs numériques
+ Ajout de l'heure d'une ville (Beta)
+ Apparence de la barre d'activité
+ Masquer les secondes
+ Masquer le zéro avant l'heure
- Number Of Data Fields
- Data Field 1
- Data Field 2
- Data Field 3
+ Nombre de champs de données
+ Champs de données 1
+ Champs de données 2
+ Champs de données 3
- Number Of Indicators
- Indicator 1
- Indicator 2
- Indicator 3
+ Nombre d'indicateurs
+ Indicateur 1
+ Indicateur 2
+ Indicateur 3
- (From Theme)
- Mono Highlight
- Mono
+ (À partir du thème)
+ Monochrome brillant
+ Monochrome
- All Segments (Merged)
- Filled Segments (Merged)
- All Segments
- Filled Segments
- Hidden
- Current/Target
- Current
+ Tous les segments (fusionnés)
+ Segments remplis seulement (fusionnés)
+ Tous les segments
+ Segments remplis seulement
+ Masqué
+ Actuel/Objectif
+ Actuel
- Blue (Dark)
- Pink (Dark)
- Red (Dark)
- Green (Dark)
- Cornflower Blue (Dark)
- Lemon Cream (Dark)
- Vivid Yellow (Dark)
- Dayglo Orange (Dark)
- Mono (Dark)
- Mono (Light)
- Blue (Light)
- Red (Light)
- Green (Light)
- Dayglo Orange (Light)
- Corn Yellow (Dark)
+ Bleu (sur fond noir)
+ Rose (sur fond noir)
+ Rouge (sur fond noir)
+ Vert(sur fond noir)
+ Bleu pale (sur fond noir)
+ Crème (sur fond noir)
+ Jaune vif (sur fond noir)
+ Orange (sur fond noir)
+ Monochrome (sur fond noir)
+ Monochrome (sur fond blanc)
+ Bleu (sur fond blanc)
+ Rouge (sur fond blanc)
+ Vert (sur fond blanc)
+ Orange (sur fond blanc)
+ Jaune foncé (sur fond noir)
- Steps
- Floors Climbed
- Active Minutes (Weekly)
- Calories (Manual Goal)
- Off
+ Pas
+ Étages montées
+ Minutes actives (hebdo)
+ Calories (objectif manuel)
+ Masqué
- Heart Rate
- Heart Rate (Live 5s)
- Battery
- Battery (Hide Percentage)
+ Rythme cardiaque
+ Rythme cardiaque (actif 5s)
+ Pile
+ Pile (Pourcentage masqué)
Notifications
Calories
Distance
- Alarms
+ Alarmes
Altitude
- Thermometer
+ Thermomètre
Bluetooth
Bluetooth/Notifications
- Sunrise/Sunset
- Weather
- Humidity
- Pressure
+ Lever/Coucher du soleil
+ Météo
+ Humidité
+ Pression
@@ -89,16 +89,16 @@
Sam
Jan
- Fev
+ Fév
Mar
Avr
Mai
Juin
Juil
- Aout
+ Août
Sep
Oct
Nov
- Dec
+ Déc
diff --git a/resources-round-280x280/fonts/crystal-icons-large.fnt b/resources-round-280x280/fonts/crystal-icons-large.fnt
index 3fc6ac62..b82a0d52 100644
--- a/resources-round-280x280/fonts/crystal-icons-large.fnt
+++ b/resources-round-280x280/fonts/crystal-icons-large.fnt
@@ -1,7 +1,7 @@
info face="Crystal Icons" size=-28 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=1 aa=1 padding=0,0,0,0 spacing=1,1 outline=0
common lineHeight=28 base=28 scaleW=256 scaleH=256 pages=1 packed=0 alphaChnl=0 redChnl=0 greenChnl=0 blueChnl=0
page id=0 file="crystal-icons-large.png"
-chars count=17
+chars count=18
char id=48 x=0 y=0 width=25 height=28 xoffset=0 yoffset=0 xadvance=25 page=0 chnl=15
char id=49 x=25 y=0 width=28 height=28 xoffset=0 yoffset=0 xadvance=28 page=0 chnl=15
char id=50 x=53 y=0 width=28 height=28 xoffset=0 yoffset=0 xadvance=28 page=0 chnl=15
@@ -18,4 +18,5 @@ char id=61 x=42 y=28 width=7 height=7 xoffset=0 yoffset=10
char id=62 x=49 y=28 width=32 height=28 xoffset=0 yoffset=0 xadvance=32 page=0 chnl=15
char id=63 x=81 y=28 width=32 height=28 xoffset=0 yoffset=0 xadvance=32 page=0 chnl=15
char id=64 x=113 y=28 width=24 height=28 xoffset=0 yoffset=0 xadvance=24 page=0 chnl=15
-char id=65 x=137 y=28 width=19 height=28 xoffset=0 yoffset=0 xadvance=19 page=0 chnl=15
\ No newline at end of file
+char id=65 x=137 y=28 width=19 height=28 xoffset=0 yoffset=0 xadvance=19 page=0 chnl=15
+char id=66 x=158 y=32 width=28 height=25 xoffset=0 yoffset=0 xadvance=32 page=0 chnl=15
\ No newline at end of file
diff --git a/resources-round-280x280/fonts/crystal-icons-large.png b/resources-round-280x280/fonts/crystal-icons-large.png
index c8626ea6..db99c151 100644
Binary files a/resources-round-280x280/fonts/crystal-icons-large.png and b/resources-round-280x280/fonts/crystal-icons-large.png differ
diff --git a/resources-round-280x280/fonts/titillium-web-light-80-tall.bmfc b/resources-round-280x280/fonts/titillium-web-light-80-tall.bmfc
index 07c3eb7a..ac13b0e6 100644
--- a/resources-round-280x280/fonts/titillium-web-light-80-tall.bmfc
+++ b/resources-round-280x280/fonts/titillium-web-light-80-tall.bmfc
@@ -1,55 +1,55 @@
-# AngelCode Bitmap Font Generator configuration file
-fileVersion=1
-
-# font settings
-fontName=Titillium Web Light
-fontFile=
-charSet=0
-fontSize=-80
-aa=1
-scaleH=105
-useSmoothing=1
-isBold=0
-isItalic=0
-useUnicode=1
-disableBoxChars=1
-outputInvalidCharGlyph=0
-dontIncludeKerningPairs=0
-useHinting=1
-renderFromOutline=0
-useClearType=0
-
-# character alignment
-paddingDown=0
-paddingUp=0
-paddingRight=0
-paddingLeft=0
-spacingHoriz=1
-spacingVert=1
-useFixedHeight=0
-forceZero=0
-
-# output file
-outWidth=256
-outHeight=256
-outBitDepth=8
-fontDescFormat=0
-fourChnlPacked=0
-textureFormat=png
-textureCompression=0
-alphaChnl=1
-redChnl=0
-greenChnl=0
-blueChnl=0
-invA=0
-invR=0
-invG=0
-invB=0
-
-# outline
-outlineThickness=0
-
-# selected chars
-chars=48-57
-
-# imported icon images
+# AngelCode Bitmap Font Generator configuration file
+fileVersion=1
+
+# font settings
+fontName=Titillium Web Light
+fontFile=
+charSet=0
+fontSize=-80
+aa=1
+scaleH=105
+useSmoothing=1
+isBold=0
+isItalic=0
+useUnicode=1
+disableBoxChars=1
+outputInvalidCharGlyph=0
+dontIncludeKerningPairs=0
+useHinting=1
+renderFromOutline=0
+useClearType=0
+
+# character alignment
+paddingDown=0
+paddingUp=0
+paddingRight=0
+paddingLeft=0
+spacingHoriz=1
+spacingVert=1
+useFixedHeight=0
+forceZero=0
+
+# output file
+outWidth=256
+outHeight=256
+outBitDepth=8
+fontDescFormat=0
+fourChnlPacked=0
+textureFormat=png
+textureCompression=0
+alphaChnl=1
+redChnl=0
+greenChnl=0
+blueChnl=0
+invA=0
+invR=0
+invG=0
+invB=0
+
+# outline
+outlineThickness=0
+
+# selected chars
+chars=48-58
+
+# imported icon images
diff --git a/resources-round-280x280/fonts/titillium-web-light-80-tall.fnt b/resources-round-280x280/fonts/titillium-web-light-80-tall.fnt
index 049b1dfe..ec6b4a6a 100644
--- a/resources-round-280x280/fonts/titillium-web-light-80-tall.fnt
+++ b/resources-round-280x280/fonts/titillium-web-light-80-tall.fnt
@@ -12,3 +12,4 @@ char id=54 x=120 y=0 width=37 height=58 xoffset=4 yoffset=38
char id=55 x=36 y=59 width=33 height=57 xoffset=6 yoffset=39 xadvance=45 page=0 chnl=15
char id=56 x=0 y=0 width=40 height=58 xoffset=2 yoffset=38 xadvance=45 page=0 chnl=15
char id=57 x=81 y=0 width=38 height=58 xoffset=3 yoffset=38 xadvance=45 page=0 chnl=15
+char id=58 x=114 y=59 width=10 height=56 xoffset=0 yoffset=38 xadvance=10 page=0 chnl=15
\ No newline at end of file
diff --git a/resources-round-280x280/fonts/titillium-web-light-80-tall_0.png b/resources-round-280x280/fonts/titillium-web-light-80-tall_0.png
index c35991de..e1ff2fe8 100644
Binary files a/resources-round-280x280/fonts/titillium-web-light-80-tall_0.png and b/resources-round-280x280/fonts/titillium-web-light-80-tall_0.png differ
diff --git a/resources-round-390x390/fonts/crystal-icons-extra-large.bak b/resources-round-390x390/fonts/crystal-icons-extra-large.bak
new file mode 100644
index 00000000..1b0347a8
--- /dev/null
+++ b/resources-round-390x390/fonts/crystal-icons-extra-large.bak
@@ -0,0 +1,21 @@
+info face="Crystal Icons" size=-39 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=1 aa=1 padding=0,0,0,0 spacing=1,1 outline=0
+common lineHeight=39 base=39 scaleW=256 scaleH=256 pages=1 packed=0 alphaChnl=0 redChnl=0 greenChnl=0 blueChnl=0
+page id=0 file="crystal-icons-extra-large.png"
+chars count=17
+char id=48 x=0 y=0 width=34 height=39 xoffset=0 yoffset=0 xadvance=34 page=0 chnl=15
+char id=49 x=34 y=0 width=39 height=39 xoffset=0 yoffset=0 xadvance=39 page=0 chnl=15
+char id=50 x=73 y=0 width=39 height=39 xoffset=0 yoffset=0 xadvance=39 page=0 chnl=15
+char id=51 x=112 y=0 width=39 height=39 xoffset=0 yoffset=0 xadvance=39 page=0 chnl=15
+char id=53 x=151 y=0 width=39 height=39 xoffset=0 yoffset=0 xadvance=39 page=0 chnl=15
+char id=54 x=190 y=0 width=23 height=39 xoffset=8 yoffset=0 xadvance=39 page=0 chnl=15
+char id=55 x=213 y=0 width=35 height=39 xoffset=0 yoffset=0 xadvance=35 page=0 chnl=15
+char id=56 x=0 y=39 width=29 height=39 xoffset=0 yoffset=0 xadvance=29 page=0 chnl=15
+char id=57 x=30 y=39 width=39 height=39 xoffset=0 yoffset=0 xadvance=39 page=0 chnl=15
+char id=58 x=69 y=39 width=35 height=39 xoffset=0 yoffset=0 xadvance=35 page=0 chnl=15
+char id=59 x=104 y=39 width=39 height=39 xoffset=0 yoffset=0 xadvance=39 page=0 chnl=15
+char id=60 x=143 y=39 width=20 height=39 xoffset=0 yoffset=0 xadvance=20 page=0 chnl=15
+char id=61 x=163 y=39 width=9 height=9 xoffset=0 yoffset=13 xadvance=9 page=0 chnl=15
+char id=62 x=0 y=78 width=45 height=39 xoffset=0 yoffset=0 xadvance=45 page=0 chnl=15
+char id=63 x=45 y=78 width=45 height=39 xoffset=0 yoffset=0 xadvance=45 page=0 chnl=15
+char id=64 x=90 y=78 width=33 height=39 xoffset=0 yoffset=0 xadvance=33 page=0 chnl=15
+char id=65 x=123 y=78 width=27 height=39 xoffset=0 yoffset=0 xadvance=27 page=0 chnl=15
\ No newline at end of file
diff --git a/resources-round-390x390/fonts/crystal-icons-extra-large.fnt b/resources-round-390x390/fonts/crystal-icons-extra-large.fnt
index 1b0347a8..44129b8b 100644
--- a/resources-round-390x390/fonts/crystal-icons-extra-large.fnt
+++ b/resources-round-390x390/fonts/crystal-icons-extra-large.fnt
@@ -1,7 +1,7 @@
info face="Crystal Icons" size=-39 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=1 aa=1 padding=0,0,0,0 spacing=1,1 outline=0
common lineHeight=39 base=39 scaleW=256 scaleH=256 pages=1 packed=0 alphaChnl=0 redChnl=0 greenChnl=0 blueChnl=0
page id=0 file="crystal-icons-extra-large.png"
-chars count=17
+chars count=18
char id=48 x=0 y=0 width=34 height=39 xoffset=0 yoffset=0 xadvance=34 page=0 chnl=15
char id=49 x=34 y=0 width=39 height=39 xoffset=0 yoffset=0 xadvance=39 page=0 chnl=15
char id=50 x=73 y=0 width=39 height=39 xoffset=0 yoffset=0 xadvance=39 page=0 chnl=15
@@ -18,4 +18,5 @@ char id=61 x=163 y=39 width=9 height=9 xoffset=0 yoffset=13
char id=62 x=0 y=78 width=45 height=39 xoffset=0 yoffset=0 xadvance=45 page=0 chnl=15
char id=63 x=45 y=78 width=45 height=39 xoffset=0 yoffset=0 xadvance=45 page=0 chnl=15
char id=64 x=90 y=78 width=33 height=39 xoffset=0 yoffset=0 xadvance=33 page=0 chnl=15
-char id=65 x=123 y=78 width=27 height=39 xoffset=0 yoffset=0 xadvance=27 page=0 chnl=15
\ No newline at end of file
+char id=65 x=123 y=78 width=27 height=39 xoffset=0 yoffset=0 xadvance=27 page=0 chnl=15
+char id=66 x=153 y=78 width=43 height=39 xoffset=0 yoffset=0 xadvance=43 page=0 chnl=15
\ No newline at end of file
diff --git a/resources-round-390x390/fonts/crystal-icons-extra-large.png b/resources-round-390x390/fonts/crystal-icons-extra-large.png
index 3a9378f3..e988b595 100644
Binary files a/resources-round-390x390/fonts/crystal-icons-extra-large.png and b/resources-round-390x390/fonts/crystal-icons-extra-large.png differ
diff --git a/resources-round-390x390/fonts/fonts.xml b/resources-round-390x390/fonts/fonts.xml
index 5421baeb..52d7c21e 100644
--- a/resources-round-390x390/fonts/fonts.xml
+++ b/resources-round-390x390/fonts/fonts.xml
@@ -1,32 +1,32 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources-small-icons/fonts/crystal-icons-small.fnt b/resources-small-icons/fonts/crystal-icons-small.fnt
index 148876d9..8668a3db 100644
--- a/resources-small-icons/fonts/crystal-icons-small.fnt
+++ b/resources-small-icons/fonts/crystal-icons-small.fnt
@@ -1,7 +1,7 @@
info face="Crystal Icons" size=-20 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=1 aa=1 padding=0,0,0,0 spacing=1,1 outline=0
common lineHeight=20 base=20 scaleW=256 scaleH=256 pages=1 packed=0 alphaChnl=0 redChnl=0 greenChnl=0 blueChnl=0
page id=0 file="crystal-icons-small.png"
-chars count=17
+chars count=18
char id=48 x=0 y=0 width=20 height=20 xoffset=0 yoffset=0 xadvance=20 page=0 chnl=15
char id=49 x=21 y=0 width=20 height=20 xoffset=0 yoffset=0 xadvance=20 page=0 chnl=15
char id=50 x=42 y=0 width=20 height=20 xoffset=0 yoffset=0 xadvance=20 page=0 chnl=15
@@ -18,4 +18,5 @@ char id=61 x=250 y=0 width=6 height=6 xoffset=0 yoffset=7
char id=62 x=0 y=21 width=23 height=20 xoffset=0 yoffset=0 xadvance=23 page=0 chnl=15
char id=63 x=24 y=21 width=23 height=20 xoffset=0 yoffset=0 xadvance=23 page=0 chnl=15
char id=64 x=48 y=21 width=17 height=20 xoffset=0 yoffset=0 xadvance=17 page=0 chnl=15
-char id=65 x=66 y=21 width=14 height=20 xoffset=0 yoffset=0 xadvance=14 page=0 chnl=15
\ No newline at end of file
+char id=65 x=66 y=21 width=14 height=20 xoffset=0 yoffset=0 xadvance=14 page=0 chnl=15
+char id=66 x=82 y=23 width=20 height=18 xoffset=0 yoffset=0 xadvance=20 page=0 chnl=15
\ No newline at end of file
diff --git a/resources-small-icons/fonts/crystal-icons-small.png b/resources-small-icons/fonts/crystal-icons-small.png
index 351e80c2..5e3a0deb 100644
Binary files a/resources-small-icons/fonts/crystal-icons-small.png and b/resources-small-icons/fonts/crystal-icons-small.png differ
diff --git a/resources/fonts/crystal-icons.fnt b/resources/fonts/crystal-icons.fnt
index dd5b89a7..2b32067c 100644
--- a/resources/fonts/crystal-icons.fnt
+++ b/resources/fonts/crystal-icons.fnt
@@ -1,7 +1,7 @@
info face="Crystal Icons" size=-24 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=1 aa=1 padding=0,0,0,0 spacing=1,1 outline=0
common lineHeight=24 base=24 scaleW=256 scaleH=256 pages=1 packed=0 alphaChnl=0 redChnl=0 greenChnl=0 blueChnl=0
page id=0 file="crystal-icons.png"
-chars count=17
+chars count=18
char id=48 x=0 y=0 width=21 height=24 xoffset=1 yoffset=0 xadvance=24 page=0 chnl=15
char id=49 x=22 y=0 width=24 height=24 xoffset=0 yoffset=0 xadvance=24 page=0 chnl=15
char id=50 x=47 y=0 width=24 height=24 xoffset=0 yoffset=0 xadvance=24 page=0 chnl=15
@@ -18,4 +18,5 @@ char id=61 x=38 y=25 width=6 height=6 xoffset=0 yoffset=9
char id=62 x=47 y=25 width=28 height=24 xoffset=0 yoffset=0 xadvance=28 page=0 chnl=15
char id=63 x=76 y=25 width=28 height=24 xoffset=0 yoffset=0 xadvance=28 page=0 chnl=15
char id=64 x=105 y=25 width=20 height=24 xoffset=0 yoffset=0 xadvance=20 page=0 chnl=15
-char id=65 x=126 y=25 width=17 height=24 xoffset=0 yoffset=0 xadvance=17 page=0 chnl=15
\ No newline at end of file
+char id=65 x=126 y=25 width=17 height=24 xoffset=0 yoffset=0 xadvance=17 page=0 chnl=15
+char id=66 x=145 y=27 width=24 height=22 xoffset=0 yoffset=0 xadvance=24 page=0 chnl=15
\ No newline at end of file
diff --git a/resources/fonts/crystal-icons.png b/resources/fonts/crystal-icons.png
index 9a7b658c..2dbdfae9 100644
Binary files a/resources/fonts/crystal-icons.png and b/resources/fonts/crystal-icons.png differ
diff --git a/resources/settings/settings.xml b/resources/settings/settings.xml
index 3410e3fd..b1ccbd5b 100644
--- a/resources/settings/settings.xml
+++ b/resources/settings/settings.xml
@@ -88,6 +88,7 @@
@Strings.SunriseSunset
@Strings.Weather
@Strings.Humidity
+ @Strings.PulseOx
@@ -107,6 +108,7 @@
@Strings.SunriseSunset
@Strings.Weather
@Strings.Humidity
+ @Strings.PulseOx
@@ -126,6 +128,7 @@
@Strings.SunriseSunset
@Strings.Weather
@Strings.Humidity
+ @Strings.PulseOx
diff --git a/resources/strings/strings.bak b/resources/strings/strings.bak
new file mode 100644
index 00000000..f9512d17
--- /dev/null
+++ b/resources/strings/strings.bak
@@ -0,0 +1,104 @@
+
+
+ Crystal
+ App Version
+
+ Theme
+ Hours Colour
+ Minutes Colour
+ Left Meter
+ Right Meter
+ Calories Goal (1-10,000kCal)
+ Meter Style
+ Meter Digits Style
+ Add Local Time in City (Beta)
+ Move Bar Style
+ Hide Seconds
+ Hide Hours Leading Zero
+
+ Number Of Data Fields
+ Data Field 1
+ Data Field 2
+ Data Field 3
+
+ Number Of Indicators
+ Indicator 1
+ Indicator 2
+ Indicator 3
+
+ (From Theme)
+ Mono Highlight
+ Mono
+
+ All Segments (Merged)
+ Filled Segments (Merged)
+ All Segments
+ Filled Segments
+ Hidden
+ Current/Target
+ Current
+
+ Blue (Dark)
+ Pink (Dark)
+ Red (Dark)
+ Green (Dark)
+ Cornflower Blue (Dark)
+ Lemon Cream (Dark)
+ Vivid Yellow (Dark)
+ Dayglo Orange (Dark)
+ Mono (Dark)
+ Mono (Light)
+ Blue (Light)
+ Red (Light)
+ Green (Light)
+ Dayglo Orange (Light)
+ Corn Yellow (Dark)
+
+ Steps
+ Floors Climbed
+ Active Minutes (Weekly)
+ Calories (Manual Goal)
+ Off
+
+ Heart Rate
+ Heart Rate (Live 5s)
+ Battery
+ Battery (Hide Percentage)
+ Notifications
+ Calories
+ Distance
+ Alarms
+ Altitude
+ Thermometer
+ Bluetooth
+ Bluetooth/Notifications
+ Sunrise/Sunset
+ Weather
+ Humidity
+ Pressure
+
+
+
+
+ Sun
+ Mon
+ Tue
+ Wed
+ Thu
+ Fri
+ Sat
+
+ Jan
+ Feb
+ Mar
+ Apr
+ May
+ Jun
+ Jul
+ Aug
+ Sep
+ Oct
+ Nov
+ Dec
+
+
diff --git a/resources/strings/strings.xml b/resources/strings/strings.xml
index f9512d17..05378315 100644
--- a/resources/strings/strings.xml
+++ b/resources/strings/strings.xml
@@ -76,6 +76,7 @@
Weather
Humidity
Pressure
+ Pulse Ox
diff --git a/source/DataFields.bak b/source/DataFields.bak
new file mode 100644
index 00000000..2f2cf216
--- /dev/null
+++ b/source/DataFields.bak
@@ -0,0 +1,730 @@
+using Toybox.WatchUi as Ui;
+using Toybox.Graphics as Gfx;
+using Toybox.System as Sys;
+using Toybox.Application as App;
+using Toybox.Activity as Activity;
+using Toybox.ActivityMonitor as ActivityMonitor;
+using Toybox.SensorHistory as SensorHistory;
+
+using Toybox.Time;
+using Toybox.Time.Gregorian;
+
+enum /* FIELD_TYPES */ {
+ // Pseudo-fields.
+ FIELD_TYPE_SUNRISE = -1,
+ //FIELD_TYPE_SUNSET = -2,
+
+ // Real fields (used by properties).
+ FIELD_TYPE_HEART_RATE = 0,
+ FIELD_TYPE_BATTERY,
+ FIELD_TYPE_NOTIFICATIONS,
+ FIELD_TYPE_CALORIES,
+ FIELD_TYPE_DISTANCE,
+ FIELD_TYPE_ALARMS,
+ FIELD_TYPE_ALTITUDE,
+ FIELD_TYPE_TEMPERATURE,
+ FIELD_TYPE_BATTERY_HIDE_PERCENT,
+ FIELD_TYPE_HR_LIVE_5S,
+ FIELD_TYPE_SUNRISE_SUNSET,
+ FIELD_TYPE_WEATHER,
+ FIELD_TYPE_PRESSURE,
+ FIELD_TYPE_HUMIDITY
+}
+
+class DataFields extends Ui.Drawable {
+
+ private var mLeft;
+ private var mRight;
+ private var mTop;
+ private var mBottom;
+
+ private var mWeatherIconsFont;
+ private var mWeatherIconsSubset = null; // null, "d" for day subset, "n" for night subset.
+
+ private var mFieldCount;
+ private var mHasLiveHR = false; // Is a live HR field currently being shown?
+ private var mWasHRAvailable = false; // HR availability at last full draw (in high power mode).
+ private var mMaxFieldLength; // Maximum number of characters per field.
+ private var mBatteryWidth; // Width of battery meter.
+
+ // private const CM_PER_KM = 100000;
+ // private const MI_PER_KM = 0.621371;
+ // private const FT_PER_M = 3.28084;
+
+ function initialize(params) {
+ Drawable.initialize(params);
+
+ mLeft = params[:left];
+ mRight = params[:right];
+ mTop = params[:top];
+ mBottom = params[:bottom];
+
+ mBatteryWidth = params[:batteryWidth];
+
+ // Initialise mFieldCount and mMaxFieldLength.
+ onSettingsChanged();
+ }
+
+ // Cache FieldCount setting, and determine appropriate maximum field length.
+ function onSettingsChanged() {
+
+ // #123 Protect against null or unexpected type e.g. String.
+ mFieldCount = App.getApp().getIntProperty("FieldCount", 3);
+
+ /* switch (mFieldCount) {
+ case 3:
+ mMaxFieldLength = 4;
+ break;
+ case 2:
+ mMaxFieldLength = 6;
+ break;
+ case 1:
+ mMaxFieldLength = 8;
+ break;
+ } */
+
+ // #116 Handle FieldCount = 0 correctly.
+ mMaxFieldLength = [0, 8, 6, 4][mFieldCount];
+
+ mHasLiveHR = App.getApp().hasField(FIELD_TYPE_HR_LIVE_5S);
+
+ if (!App.getApp().hasField(FIELD_TYPE_WEATHER)) {
+ mWeatherIconsFont = null;
+ mWeatherIconsSubset = null;
+ }
+ }
+
+ function draw(dc) {
+ update(dc, /* isPartialUpdate */ false);
+ }
+
+ function update(dc, isPartialUpdate) {
+ if (isPartialUpdate && !mHasLiveHR) {
+ return;
+ }
+
+ var fieldTypes = App.getApp().mFieldTypes;
+
+ switch (mFieldCount) {
+ case 3:
+ drawDataField(dc, isPartialUpdate, fieldTypes[0], mLeft);
+ drawDataField(dc, isPartialUpdate, fieldTypes[1], (mRight + mLeft) / 2);
+ drawDataField(dc, isPartialUpdate, fieldTypes[2], mRight);
+ break;
+ case 2:
+ drawDataField(dc, isPartialUpdate, fieldTypes[0], mLeft + ((mRight - mLeft) * 0.15));
+ drawDataField(dc, isPartialUpdate, fieldTypes[1], mLeft + ((mRight - mLeft) * 0.85));
+ break;
+ case 1:
+ drawDataField(dc, isPartialUpdate, fieldTypes[0], (mRight + mLeft) / 2);
+ break;
+ /*
+ case 0:
+ break;
+ */
+ }
+ }
+
+ // Both regular and small icon fonts use same spot size for easier optimisation.
+ //private const LIVE_HR_SPOT_RADIUS = 3;
+
+ private function drawDataField(dc, isPartialUpdate, fieldType, x) {
+
+ // Assume we're only drawing live HR spot every 5 seconds; skip all other partial updates.
+ var isLiveHeartRate = (fieldType == FIELD_TYPE_HR_LIVE_5S);
+ var seconds = Sys.getClockTime().sec;
+ if (isPartialUpdate && (!isLiveHeartRate || (seconds % 5))) {
+ return;
+ }
+
+ // Decide whether spot should be shown or not, based on current seconds.
+ var showLiveHRSpot = false;
+ var isHeartRate = ((fieldType == FIELD_TYPE_HEART_RATE) || isLiveHeartRate);
+ if (isHeartRate) {
+
+ // High power mode: 0 on, 1 off, 2 on, etc.
+ if (!App.getApp().getView().isSleeping()) {
+ showLiveHRSpot = ((seconds % 2) == 0);
+
+ // Low power mode:
+ } else {
+
+ // Live HR: 0-4 on, 5-9 off, 10-14 on, etc.
+ if (isLiveHeartRate) {
+ showLiveHRSpot = (((seconds / 5) % 2) == 0);
+
+ // Normal HR: turn off spot when entering sleep.
+ } else {
+ showLiveHRSpot = false;
+ }
+ }
+ }
+
+ // 1. Value: draw first, as top of text overlaps icon.
+ var result = getValueForFieldType(fieldType);
+ var value = result["value"];
+
+ // Optimisation: if live HR remains unavailable, skip the rest of this partial update.
+ var isHRAvailable = isHeartRate && (value.length() != 0);
+ if (isPartialUpdate && !isHRAvailable && !mWasHRAvailable) {
+ return;
+ }
+
+ // #34 Clip live HR value.
+ // Optimisation: hard-code clip rect dimensions. Possible, as all watches use same label font.
+ dc.setColor(gMonoLightColour, gBackgroundColour);
+
+ if (isPartialUpdate) {
+ dc.setClip(
+ x - 11,
+ mBottom - 4,
+ 25,
+ 12);
+
+ dc.clear();
+ }
+
+ dc.drawText(
+ x,
+ mBottom,
+ gNormalFont,
+ value,
+ Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER
+ );
+
+ // 2. Icon.
+
+ // Grey out icon if no value was retrieved.
+ // #37 Do not grey out battery icon (getValueForFieldType() returns empty string).
+ var colour = (value.length() == 0) ? gMeterBackgroundColour : gThemeColour;
+
+ // Battery.
+ if ((fieldType == FIELD_TYPE_BATTERY) || (fieldType == FIELD_TYPE_BATTERY_HIDE_PERCENT)) {
+ drawBatteryMeter(dc, x, mTop, mBatteryWidth, mBatteryWidth / 2);
+
+ // #34 Live HR in low power mode.
+ } else if (isLiveHeartRate && isPartialUpdate) {
+
+ // If HR availability changes while in low power mode, then we unfortunately have to draw the full heart.
+ // HR availability was recorded during the last high power draw cycle.
+ if (isHRAvailable != mWasHRAvailable) {
+ mWasHRAvailable = isHRAvailable;
+
+ // Clip full heart, then draw.
+ var heartDims = dc.getTextDimensions("3", gIconsFont); // getIconFontCharForField(FIELD_TYPE_HR_LIVE_5S)
+ dc.setClip(
+ x - (heartDims[0] / 2),
+ mTop - (heartDims[1] / 2),
+ heartDims[0] + 1,
+ heartDims[1] + 1);
+ dc.setColor(colour, gBackgroundColour);
+ dc.drawText(
+ x,
+ mTop,
+ gIconsFont,
+ "3", // getIconFontCharForField(FIELD_TYPE_HR_LIVE_5S)
+ Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER
+ );
+ }
+
+ // Clip spot.
+ dc.setClip(
+ x - 3 /* LIVE_HR_SPOT_RADIUS */,
+ mTop - 3 /* LIVE_HR_SPOT_RADIUS */,
+ 7, // (2 * LIVE_HR_SPOT_RADIUS) + 1
+ 7); // (2 * LIVE_HR_SPOT_RADIUS) + 1
+
+ // Draw spot, if it should be shown.
+ // fillCircle() does not anti-aliase, so use font instead.
+ var spotChar;
+ if (showLiveHRSpot && (Activity.getActivityInfo().currentHeartRate != null)) {
+ dc.setColor(gBackgroundColour, Graphics.COLOR_TRANSPARENT);
+ spotChar = "="; // getIconFontCharForField(LIVE_HR_SPOT)
+
+ // Otherwise, fill in spot by drawing heart.
+ } else {
+ dc.setColor(colour, gBackgroundColour);
+ spotChar = "3"; // getIconFontCharForField(FIELD_TYPE_HR_LIVE_5S)
+ }
+ dc.drawText(
+ x,
+ mTop,
+ gIconsFont,
+ spotChar,
+ Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER
+ );
+
+ // Other icons.
+ } else {
+
+ // #19 Show sunrise icon instead of default sunset icon, if sunrise is next.
+ if ((fieldType == FIELD_TYPE_SUNRISE_SUNSET) && (result["isSunriseNext"] == true)) {
+ fieldType = FIELD_TYPE_SUNRISE;
+ }
+
+ var font;
+ var icon;
+ if (fieldType == FIELD_TYPE_WEATHER) {
+
+ // #83 Dynamic loading/unloading of day/night weather icons font, to save memory.
+ // If subset has changed since last draw, save new subset, and load appropriate font for it.
+ var weatherIconsSubset = result["weatherIcon"].substring(2, 3);
+ if (!weatherIconsSubset.equals(mWeatherIconsSubset)) {
+ mWeatherIconsSubset = weatherIconsSubset;
+ mWeatherIconsFont = Ui.loadResource((mWeatherIconsSubset.equals("d")) ?
+ Rez.Fonts.WeatherIconsFontDay : Rez.Fonts.WeatherIconsFontNight);
+ }
+ font = mWeatherIconsFont;
+
+ // #89 To avoid Unicode issues on real 735xt, rewrite char IDs as regular ASCII values, day icons starting from
+ // "A", night icons starting from "a" ("I" is shared). Also makes subsetting easier in fonts.xml.
+ // See https://openweathermap.org/weather-conditions.
+ icon = {
+ // Day icon Night icon Description
+ "01d" => "H" /* 61453 */, "01n" => "f" /* 61486 */, // clear sky
+ "02d" => "G" /* 61452 */, "02n" => "g" /* 61569 */, // few clouds
+ "03d" => "B" /* 61442 */, "03n" => "h" /* 61574 */, // scattered clouds
+ "04d" => "I" /* 61459 */, "04n" => "I" /* 61459 */, // broken clouds: day and night use same icon
+ "09d" => "E" /* 61449 */, "09n" => "d" /* 61481 */, // shower rain
+ "10d" => "D" /* 61448 */, "10n" => "c" /* 61480 */, // rain
+ "11d" => "C" /* 61445 */, "11n" => "b" /* 61477 */, // thunderstorm
+ "13d" => "F" /* 61450 */, "13n" => "e" /* 61482 */, // snow
+ "50d" => "A" /* 61441 */, "50n" => "a" /* 61475 */, // mist
+ }[result["weatherIcon"]];
+
+ } else {
+ font = gIconsFont;
+
+ // Map fieldType to icon font char.
+ icon = {
+ FIELD_TYPE_SUNRISE => ">",
+ // FIELD_TYPE_SUNSET => "?",
+
+ FIELD_TYPE_HEART_RATE => "3",
+ FIELD_TYPE_HR_LIVE_5S => "3",
+ // FIELD_TYPE_BATTERY => "4",
+ // FIELD_TYPE_BATTERY_HIDE_PERCENT => "4",
+ FIELD_TYPE_NOTIFICATIONS => "5",
+ FIELD_TYPE_CALORIES => "6",
+ FIELD_TYPE_DISTANCE => "7",
+ FIELD_TYPE_ALARMS => ":",
+ FIELD_TYPE_ALTITUDE => ";",
+ FIELD_TYPE_TEMPERATURE => "<",
+ // FIELD_TYPE_WEATHER => "<",
+ // LIVE_HR_SPOT => "=",
+
+ FIELD_TYPE_SUNRISE_SUNSET => "?",
+ FIELD_TYPE_PRESSURE => "@",
+ FIELD_TYPE_HUMIDITY => "A",
+ }[fieldType];
+ }
+
+ dc.setColor(colour, gBackgroundColour);
+ dc.drawText(
+ x,
+ mTop,
+ font,
+ icon,
+ Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER
+ );
+
+ if (isHeartRate) {
+
+ // #34 Save whether HR was available during this high power draw cycle.
+ mWasHRAvailable = isHRAvailable;
+
+ // #34 Live HR in high power mode.
+ if (showLiveHRSpot && (Activity.getActivityInfo().currentHeartRate != null)) {
+ dc.setColor(gBackgroundColour, Graphics.COLOR_TRANSPARENT);
+ dc.drawText(
+ x,
+ mTop,
+ gIconsFont,
+ "=", // getIconFontCharForField(LIVE_HR_SPOT)
+ Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER
+ );
+ }
+ }
+ }
+ }
+
+ // Return empty result["value"] string if value cannot be retrieved (e.g. unavailable, or unsupported).
+ // result["isSunriseNext"] indicates that sunrise icon should be shown for FIELD_TYPE_SUNRISE_SUNSET, rather than default
+ // sunset icon.
+ private function getValueForFieldType(type) {
+ var result = {};
+ var value = "";
+
+ var settings = Sys.getDeviceSettings();
+
+ var activityInfo;
+ var sample;
+ var altitude;
+ var pressure = null; // May never be initialised if no support for pressure (CIQ 1.x devices).
+ var temperature;
+ var weather;
+ var weatherValue;
+ var sunTimes;
+ var unit;
+
+ switch (type) {
+ case FIELD_TYPE_HEART_RATE:
+ case FIELD_TYPE_HR_LIVE_5S:
+ // #34 Try to retrieve live HR from Activity::Info, before falling back to historical HR from ActivityMonitor.
+ activityInfo = Activity.getActivityInfo();
+ sample = activityInfo.currentHeartRate;
+ if (sample != null) {
+ value = sample.format(INTEGER_FORMAT);
+ } else if (ActivityMonitor has :getHeartRateHistory) {
+ sample = ActivityMonitor.getHeartRateHistory(1, /* newestFirst */ true)
+ .next();
+ if ((sample != null) && (sample.heartRate != ActivityMonitor.INVALID_HR_SAMPLE)) {
+ value = sample.heartRate.format(INTEGER_FORMAT);
+ }
+ }
+ break;
+
+ case FIELD_TYPE_BATTERY:
+ // #8: battery returned as float. Use floor() to match native. Must match drawBatteryMeter().
+ value = Math.floor(Sys.getSystemStats().battery);
+ value = value.format(INTEGER_FORMAT) + "%";
+ break;
+
+ // #37 Return empty string. updateDataField() has special case so that battery icon is not greyed out.
+ // case FIELD_TYPE_BATTERY_HIDE_PERCENT:
+ // break;
+
+ case FIELD_TYPE_NOTIFICATIONS:
+ if (settings.notificationCount > 0) {
+ value = settings.notificationCount.format(INTEGER_FORMAT);
+ }
+ break;
+
+ case FIELD_TYPE_CALORIES:
+ activityInfo = ActivityMonitor.getInfo();
+ value = activityInfo.calories.format(INTEGER_FORMAT);
+ break;
+
+ case FIELD_TYPE_DISTANCE:
+ activityInfo = ActivityMonitor.getInfo();
+ value = activityInfo.distance.toFloat() / /* CM_PER_KM */ 100000; // #11: Ensure floating point division!
+
+ if (settings.distanceUnits == System.UNIT_METRIC) {
+ unit = "km";
+ } else {
+ value *= /* MI_PER_KM */ 0.621371;
+ unit = "mi";
+ }
+
+ value = value.format("%.1f");
+
+ // Show unit only if value plus unit fits within maximum field length.
+ if ((value.length() + unit.length()) <= mMaxFieldLength) {
+ value += unit;
+ }
+
+ break;
+
+ case FIELD_TYPE_ALARMS:
+ if (settings.alarmCount > 0) {
+ value = settings.alarmCount.format(INTEGER_FORMAT);
+ }
+ break;
+
+ case FIELD_TYPE_ALTITUDE:
+ // #67 Try to retrieve altitude from current activity, before falling back on elevation history.
+ // Note that Activity::Info.altitude is supported by CIQ 1.x, but elevation history only on select CIQ 2.x
+ // devices.
+ activityInfo = Activity.getActivityInfo();
+ altitude = activityInfo.altitude;
+ if ((altitude == null) && (Toybox has :SensorHistory) && (Toybox.SensorHistory has :getElevationHistory)) {
+ sample = SensorHistory.getElevationHistory({ :period => 1, :order => SensorHistory.ORDER_NEWEST_FIRST })
+ .next();
+ if ((sample != null) && (sample.data != null)) {
+ altitude = sample.data;
+ }
+ }
+ if (altitude != null) {
+
+ // Metres (no conversion necessary).
+ if (settings.elevationUnits == System.UNIT_METRIC) {
+ unit = "m";
+
+ // Feet.
+ } else {
+ altitude *= /* FT_PER_M */ 3.28084;
+ unit = "ft";
+ }
+
+ value = altitude.format(INTEGER_FORMAT);
+
+ // Show unit only if value plus unit fits within maximum field length.
+ if ((value.length() + unit.length()) <= mMaxFieldLength) {
+ value += unit;
+ }
+ }
+ break;
+
+ case FIELD_TYPE_TEMPERATURE:
+ if ((Toybox has :SensorHistory) && (Toybox.SensorHistory has :getTemperatureHistory)) {
+ sample = SensorHistory.getTemperatureHistory(null).next();
+ if ((sample != null) && (sample.data != null)) {
+ temperature = sample.data;
+
+ if (settings.temperatureUnits == System.UNIT_STATUTE) {
+ temperature = (temperature * (9.0 / 5)) + 32; // Convert to Farenheit: ensure floating point division.
+ }
+
+ value = temperature.format(INTEGER_FORMAT) + "°";
+ }
+ }
+ break;
+
+ case FIELD_TYPE_SUNRISE_SUNSET:
+
+ if (gLocationLat != null) {
+ var nextSunEvent = 0;
+ var now = Gregorian.info(Time.now(), Time.FORMAT_SHORT);
+
+ // Convert to same format as sunTimes, for easier comparison. Add a minute, so that e.g. if sun rises at
+ // 07:38:17, then 07:38 is already consided daytime (seconds not shown to user).
+ now = now.hour + ((now.min + 1) / 60.0);
+ //Sys.println(now);
+
+ // Get today's sunrise/sunset times in current time zone.
+ sunTimes = getSunTimes(gLocationLat, gLocationLng, null, /* tomorrow */ false);
+ //Sys.println(sunTimes);
+
+ // If sunrise/sunset happens today.
+ var sunriseSunsetToday = ((sunTimes[0] != null) && (sunTimes[1] != null));
+ if (sunriseSunsetToday) {
+
+ // Before sunrise today: today's sunrise is next.
+ if (now < sunTimes[0]) {
+ nextSunEvent = sunTimes[0];
+ result["isSunriseNext"] = true;
+
+ // After sunrise today, before sunset today: today's sunset is next.
+ } else if (now < sunTimes[1]) {
+ nextSunEvent = sunTimes[1];
+
+ // After sunset today: tomorrow's sunrise (if any) is next.
+ } else {
+ sunTimes = getSunTimes(gLocationLat, gLocationLng, null, /* tomorrow */ true);
+ nextSunEvent = sunTimes[0];
+ result["isSunriseNext"] = true;
+ }
+ }
+
+ // Sun never rises/sets today.
+ if (!sunriseSunsetToday) {
+ value = "---";
+
+ // Sun never rises: sunrise is next, but more than a day from now.
+ if (sunTimes[0] == null) {
+ result["isSunriseNext"] = true;
+ }
+
+ // We have a sunrise/sunset time.
+ } else {
+ var hour = Math.floor(nextSunEvent).toLong() % 24;
+ var min = Math.floor((nextSunEvent - Math.floor(nextSunEvent)) * 60); // Math.floor(fractional_part * 60)
+ value = App.getApp().getFormattedTime(hour, min);
+ value = value[:hour] + ":" + value[:min] + value[:amPm];
+ }
+
+ // Waiting for location.
+ } else {
+ value = "gps?";
+ }
+
+ break;
+
+ case FIELD_TYPE_WEATHER:
+ case FIELD_TYPE_HUMIDITY:
+
+ // Default = sunshine!
+ if (type == FIELD_TYPE_WEATHER) {
+ result["weatherIcon"] = "01d";
+ }
+
+ weather = App.getApp().getProperty("OpenWeatherMapCurrent");
+
+ // Awaiting location.
+ if (gLocationLat == null) {
+ value = "gps?";
+
+ // Stored weather data available.
+ } else if (weather != null) {
+
+ // FIELD_TYPE_WEATHER.
+ if (type == FIELD_TYPE_WEATHER) {
+ weatherValue = weather["temp"]; // Celcius.
+
+ if (settings.temperatureUnits == System.UNIT_STATUTE) {
+ weatherValue = (weatherValue * (9.0 / 5)) + 32; // Convert to Farenheit: ensure floating point division.
+ }
+
+ value = weatherValue.format(INTEGER_FORMAT) + "°";
+ result["weatherIcon"] = weather["icon"];
+
+ // FIELD_TYPE_HUMIDITY.
+ } else {
+ weatherValue = weather["humidity"];
+ value = weatherValue.format(INTEGER_FORMAT) + "%";
+ }
+
+ // Awaiting response.
+ } else if ((App.getApp().getProperty("PendingWebRequests") != null) &&
+ App.getApp().getProperty("PendingWebRequests")["OpenWeatherMapCurrent"]) {
+
+ value = "...";
+ }
+ break;
+
+ case FIELD_TYPE_PRESSURE:
+
+ // Avoid using ActivityInfo.ambientPressure, as this bypasses any manual pressure calibration e.g. on Fenix
+ // 5. Pressure is unlikely to change frequently, so there isn't the same concern with getting a "live" value,
+ // compared with HR. Use SensorHistory only.
+ if ((Toybox has :SensorHistory) && (Toybox.SensorHistory has :getPressureHistory)) {
+ sample = SensorHistory.getPressureHistory(null).next();
+ if ((sample != null) && (sample.data != null)) {
+ pressure = sample.data;
+ }
+ }
+
+ if (pressure != null) {
+ unit = "mb";
+ pressure = pressure / 100; // Pa --> mbar;
+ value = pressure.format("%.1f");
+
+ // If single decimal place doesn't fit, truncate to integer.
+ if (value.length() > mMaxFieldLength) {
+ value = pressure.format(INTEGER_FORMAT);
+
+ // Otherwise, if unit fits as well, add it.
+ } else if (value.length() + unit.length() <= mMaxFieldLength) {
+ value = value + unit;
+ }
+ }
+ break;
+ }
+
+ result["value"] = value;
+ return result;
+ }
+
+ /**
+ * With thanks to ruiokada. Adapted, then translated to Monkey C, from:
+ * https://gist.github.com/ruiokada/b28076d4911820ddcbbc
+ *
+ * Calculates sunrise and sunset in local time given latitude, longitude, and tz.
+ *
+ * Equations taken from:
+ * https://en.wikipedia.org/wiki/Julian_day#Converting_Julian_or_Gregorian_calendar_date_to_Julian_Day_Number
+ * https://en.wikipedia.org/wiki/Sunrise_equation#Complete_calculation_on_Earth
+ *
+ * @method getSunTimes
+ * @param {Float} lat Latitude of location (South is negative)
+ * @param {Float} lng Longitude of location (West is negative)
+ * @param {Integer || null} tz Timezone hour offset. e.g. Pacific/Los Angeles is -8 (Specify null for system timezone)
+ * @param {Boolean} tomorrow Calculate tomorrow's sunrise and sunset, instead of today's.
+ * @return {Array} Returns array of length 2 with sunrise and sunset as floats.
+ * Returns array with [null, -1] if the sun never rises, and [-1, null] if the sun never sets.
+ */
+ private function getSunTimes(lat, lng, tz, tomorrow) {
+
+ // Use double precision where possible, as floating point errors can affect result by minutes.
+ lat = lat.toDouble();
+ lng = lng.toDouble();
+
+ var now = Time.now();
+ if (tomorrow) {
+ now = now.add(new Time.Duration(24 * 60 * 60));
+ }
+ var d = Gregorian.info(Time.now(), Time.FORMAT_SHORT);
+ var rad = Math.PI / 180.0d;
+ var deg = 180.0d / Math.PI;
+
+ // Calculate Julian date from Gregorian.
+ var a = Math.floor((14 - d.month) / 12);
+ var y = d.year + 4800 - a;
+ var m = d.month + (12 * a) - 3;
+ var jDate = d.day
+ + Math.floor(((153 * m) + 2) / 5)
+ + (365 * y)
+ + Math.floor(y / 4)
+ - Math.floor(y / 100)
+ + Math.floor(y / 400)
+ - 32045;
+
+ // Number of days since Jan 1st, 2000 12:00.
+ var n = jDate - 2451545.0d + 0.0008d;
+ //Sys.println("n " + n);
+
+ // Mean solar noon.
+ var jStar = n - (lng / 360.0d);
+ //Sys.println("jStar " + jStar);
+
+ // Solar mean anomaly.
+ var M = 357.5291d + (0.98560028d * jStar);
+ var MFloor = Math.floor(M);
+ var MFrac = M - MFloor;
+ M = MFloor.toLong() % 360;
+ M += MFrac;
+ //Sys.println("M " + M);
+
+ // Equation of the centre.
+ var C = 1.9148d * Math.sin(M * rad)
+ + 0.02d * Math.sin(2 * M * rad)
+ + 0.0003d * Math.sin(3 * M * rad);
+ //Sys.println("C " + C);
+
+ // Ecliptic longitude.
+ var lambda = (M + C + 180 + 102.9372d);
+ var lambdaFloor = Math.floor(lambda);
+ var lambdaFrac = lambda - lambdaFloor;
+ lambda = lambdaFloor.toLong() % 360;
+ lambda += lambdaFrac;
+ //Sys.println("lambda " + lambda);
+
+ // Solar transit.
+ var jTransit = 2451545.5d + jStar
+ + 0.0053d * Math.sin(M * rad)
+ - 0.0069d * Math.sin(2 * lambda * rad);
+ //Sys.println("jTransit " + jTransit);
+
+ // Declination of the sun.
+ var delta = Math.asin(Math.sin(lambda * rad) * Math.sin(23.44d * rad));
+ //Sys.println("delta " + delta);
+
+ // Hour angle.
+ var cosOmega = (Math.sin(-0.83d * rad) - Math.sin(lat * rad) * Math.sin(delta))
+ / (Math.cos(lat * rad) * Math.cos(delta));
+ //Sys.println("cosOmega " + cosOmega);
+
+ // Sun never rises.
+ if (cosOmega > 1) {
+ return [null, -1];
+ }
+
+ // Sun never sets.
+ if (cosOmega < -1) {
+ return [-1, null];
+ }
+
+ // Calculate times from omega.
+ var omega = Math.acos(cosOmega) * deg;
+ var jSet = jTransit + (omega / 360.0);
+ var jRise = jTransit - (omega / 360.0);
+ var deltaJSet = jSet - jDate;
+ var deltaJRise = jRise - jDate;
+
+ var tzOffset = (tz == null) ? (Sys.getClockTime().timeZoneOffset / 3600) : tz;
+ return [
+ /* localRise */ (deltaJRise * 24) + tzOffset,
+ /* localSet */ (deltaJSet * 24) + tzOffset
+ ];
+ }
+}
diff --git a/source/DataFields.mc b/source/DataFields.mc
index 2f2cf216..89b525a8 100644
--- a/source/DataFields.mc
+++ b/source/DataFields.mc
@@ -28,7 +28,8 @@ enum /* FIELD_TYPES */ {
FIELD_TYPE_SUNRISE_SUNSET,
FIELD_TYPE_WEATHER,
FIELD_TYPE_PRESSURE,
- FIELD_TYPE_HUMIDITY
+ FIELD_TYPE_HUMIDITY,
+ FIELD_TYPE_PULSE_OX
}
class DataFields extends Ui.Drawable {
@@ -316,6 +317,7 @@ class DataFields extends Ui.Drawable {
FIELD_TYPE_SUNRISE_SUNSET => "?",
FIELD_TYPE_PRESSURE => "@",
FIELD_TYPE_HUMIDITY => "A",
+ FIELD_TYPE_PULSE_OX => "B", // SG Addition
}[fieldType];
}
@@ -368,6 +370,14 @@ class DataFields extends Ui.Drawable {
var unit;
switch (type) {
+ // SG Addition
+ case FIELD_TYPE_PULSE_OX:
+ activityInfo = Activity.getActivityInfo();
+ sample = activityInfo != null and activityInfo has :currentOxygenSaturation ? activityInfo.currentOxygenSaturation : null;
+ if (sample != null) {
+ value = sample.format(INTEGER_FORMAT);
+ }
+ break;
case FIELD_TYPE_HEART_RATE:
case FIELD_TYPE_HR_LIVE_5S:
// #34 Try to retrieve live HR from Activity::Info, before falling back to historical HR from ActivityMonitor.