commit 4df963cedf4598af3bf9a0f0cdc6de628aff2ae6 Author: Mark van Renswoude Date: Fri Nov 15 14:02:03 2024 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8af8434 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Resources required for the build +KittieCats/resources/drawables/background.jpg +KittieCats/resources/drawables/background_xmas.jpg + +# Krita source files for the drawables which are not used in the app and you do not need to supply +*.kra + +# Build artifacts +bin \ No newline at end of file diff --git a/GarminWatchfaces.code-workspace b/GarminWatchfaces.code-workspace new file mode 100644 index 0000000..1e6c2aa --- /dev/null +++ b/GarminWatchfaces.code-workspace @@ -0,0 +1,10 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "KittieCats" + } + ] +} \ No newline at end of file diff --git a/KittieCats/.vscode/launch.json b/KittieCats/.vscode/launch.json new file mode 100644 index 0000000..0819e8f --- /dev/null +++ b/KittieCats/.vscode/launch.json @@ -0,0 +1,31 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "monkeyc", + "request": "launch", + "name": "Run App", + "stopAtLaunch": false, + "device": "${command:GetTargetDevice}" + }, + { + "type": "monkeyc", + "request": "launch", + "name": "Run Tests", + "runTests": true, + "device": "${command:GetTargetDevice}" + }, + { + "type": "monkeyc", + "request": "launch", + "name": "Run Complication Apps", + "stopAtLaunch": false, + "complicationSubscriberFolder": "${command:GetComplicationSubscriberFolder}", + "complicationPublisherFolder": "${command:GetComplicationPublisherFolder}", + "device": "${command:GetTargetDevice}" + } + ] +} \ No newline at end of file diff --git a/KittieCats/manifest.xml b/KittieCats/manifest.xml new file mode 100644 index 0000000..d0e1780 --- /dev/null +++ b/KittieCats/manifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + dut + eng + + + + + \ No newline at end of file diff --git a/KittieCats/monkey.jungle b/KittieCats/monkey.jungle new file mode 100755 index 0000000..87796c7 --- /dev/null +++ b/KittieCats/monkey.jungle @@ -0,0 +1 @@ +project.manifest = manifest.xml diff --git a/KittieCats/resources-dut/strings/strings.xml b/KittieCats/resources-dut/strings/strings.xml new file mode 100644 index 0000000..e800193 --- /dev/null +++ b/KittieCats/resources-dut/strings/strings.xml @@ -0,0 +1,6 @@ + + KittieCats + + Instellingen + Thema + diff --git a/KittieCats/resources-dut/themes.json b/KittieCats/resources-dut/themes.json new file mode 100644 index 0000000..0281853 --- /dev/null +++ b/KittieCats/resources-dut/themes.json @@ -0,0 +1,10 @@ +[ + { + "id": "default", + "name": "Standaard" + }, + { + "id": "xmas", + "name": "Kerst" + } +] \ No newline at end of file diff --git a/KittieCats/resources-dut/themes.xml b/KittieCats/resources-dut/themes.xml new file mode 100644 index 0000000..bfc4c78 --- /dev/null +++ b/KittieCats/resources-dut/themes.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/KittieCats/resources/drawables/arm_hour.svg b/KittieCats/resources/drawables/arm_hour.svg new file mode 100644 index 0000000..6ccd3e8 --- /dev/null +++ b/KittieCats/resources/drawables/arm_hour.svg @@ -0,0 +1,7 @@ + + + + diff --git a/KittieCats/resources/drawables/arm_minute.svg b/KittieCats/resources/drawables/arm_minute.svg new file mode 100644 index 0000000..2895a27 --- /dev/null +++ b/KittieCats/resources/drawables/arm_minute.svg @@ -0,0 +1,7 @@ + + + + diff --git a/KittieCats/resources/drawables/arm_second.svg b/KittieCats/resources/drawables/arm_second.svg new file mode 100644 index 0000000..848d727 --- /dev/null +++ b/KittieCats/resources/drawables/arm_second.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/KittieCats/resources/drawables/drawables.xml b/KittieCats/resources/drawables/drawables.xml new file mode 100755 index 0000000..9cf24cf --- /dev/null +++ b/KittieCats/resources/drawables/drawables.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/KittieCats/resources/drawables/icons.png b/KittieCats/resources/drawables/icons.png new file mode 100644 index 0000000..8e921c8 Binary files /dev/null and b/KittieCats/resources/drawables/icons.png differ diff --git a/KittieCats/resources/drawables/launcher_icon.png b/KittieCats/resources/drawables/launcher_icon.png new file mode 100755 index 0000000..d3594e7 Binary files /dev/null and b/KittieCats/resources/drawables/launcher_icon.png differ diff --git a/KittieCats/resources/drawables/ticks.svg b/KittieCats/resources/drawables/ticks.svg new file mode 100644 index 0000000..659901d --- /dev/null +++ b/KittieCats/resources/drawables/ticks.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + diff --git a/KittieCats/resources/properties.xml b/KittieCats/resources/properties.xml new file mode 100644 index 0000000..c8eda03 --- /dev/null +++ b/KittieCats/resources/properties.xml @@ -0,0 +1,11 @@ + + + default + + + + + + + + diff --git a/KittieCats/resources/source/Icons.png b/KittieCats/resources/source/Icons.png new file mode 100644 index 0000000..281c074 Binary files /dev/null and b/KittieCats/resources/source/Icons.png differ diff --git a/KittieCats/resources/source/Layout.svg b/KittieCats/resources/source/Layout.svg new file mode 100644 index 0000000..20f28e6 --- /dev/null +++ b/KittieCats/resources/source/Layout.svg @@ -0,0 +1,98 @@ + + + + diff --git a/KittieCats/resources/source/LayoutClean.svg b/KittieCats/resources/source/LayoutClean.svg new file mode 100644 index 0000000..6f31d85 --- /dev/null +++ b/KittieCats/resources/source/LayoutClean.svg @@ -0,0 +1,19 @@ + + + + + + + diff --git a/KittieCats/resources/strings/strings.xml b/KittieCats/resources/strings/strings.xml new file mode 100644 index 0000000..b28aeb7 --- /dev/null +++ b/KittieCats/resources/strings/strings.xml @@ -0,0 +1,6 @@ + + KittieCats + + Settings + Theme + diff --git a/KittieCats/resources/themes.json b/KittieCats/resources/themes.json new file mode 100644 index 0000000..b03c7fc --- /dev/null +++ b/KittieCats/resources/themes.json @@ -0,0 +1,10 @@ +[ + { + "id": "default", + "name": "Default" + }, + { + "id": "xmas", + "name": "Christmas" + } +] \ No newline at end of file diff --git a/KittieCats/resources/themes.xml b/KittieCats/resources/themes.xml new file mode 100644 index 0000000..27a27a0 --- /dev/null +++ b/KittieCats/resources/themes.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/KittieCats/resources/themes/default.json b/KittieCats/resources/themes/default.json new file mode 100644 index 0000000..38125b3 --- /dev/null +++ b/KittieCats/resources/themes/default.json @@ -0,0 +1,6 @@ +{ + "ArmHourTintHi": 9708837, + "ArmHourTintLo": 14429228, + "ArmMinuteTintHi": 9708837, + "ArmMinuteTintLo": 14429228 +} \ No newline at end of file diff --git a/KittieCats/resources/themes/xmas.json b/KittieCats/resources/themes/xmas.json new file mode 100644 index 0000000..38125b3 --- /dev/null +++ b/KittieCats/resources/themes/xmas.json @@ -0,0 +1,6 @@ +{ + "ArmHourTintHi": 9708837, + "ArmHourTintLo": 14429228, + "ArmMinuteTintHi": 9708837, + "ArmMinuteTintLo": 14429228 +} \ No newline at end of file diff --git a/KittieCats/source/ComplicationSubscriber.mc b/KittieCats/source/ComplicationSubscriber.mc new file mode 100644 index 0000000..b2b3fe6 --- /dev/null +++ b/KittieCats/source/ComplicationSubscriber.mc @@ -0,0 +1,119 @@ +import Toybox.Complications; +import Toybox.Lang; + + +class ComplicationSnapshot +{ + public var HeartRate as Lang.Numeric; + public var Steps as Lang.Numeric; + public var Stress as Lang.Numeric; + public var BodyBattery as Lang.Numeric; + + + function initialize(heartRate, steps, stress, bodyBattery) + { + self.HeartRate = heartRate; + self.Steps = steps; + self.Stress = stress; + self.BodyBattery = bodyBattery; + } +} + + +class ComplicationSubscriber +{ + private static var instance = null; + private var heartRateComplicationId; + private var stepsComplicationId; + private var stressComplicationId; + private var bodyBatteryComplicationId; + + private var heartRate = null; + private var steps = null; + private var stress = null; + private var bodyBattery = null; + + + public static function subscribe() as Void + { + ComplicationSubscriber.getInstance().internalSubscribe(); + } + + + public static function getSnapshot() as ComplicationSnapshot + { + return ComplicationSubscriber.getInstance().internalGetSnapshot(); + } + + + + function internalSubscribe() as Void + { + self.heartRateComplicationId = new Id(Complications.COMPLICATION_TYPE_HEART_RATE); + self.stepsComplicationId = new Id(Complications.COMPLICATION_TYPE_STEPS); + self.stressComplicationId = new Id(Complications.COMPLICATION_TYPE_STRESS); + self.bodyBatteryComplicationId = new Id(Complications.COMPLICATION_TYPE_BODY_BATTERY); + + Complications.registerComplicationChangeCallback(self.method(:onComplicationChanged)); + + Complications.subscribeToUpdates(self.heartRateComplicationId); + Complications.subscribeToUpdates(self.stepsComplicationId); + Complications.subscribeToUpdates(self.stressComplicationId); + Complications.subscribeToUpdates(self.bodyBatteryComplicationId); + } + + + function internalGetSnapshot() as ComplicationSnapshot + { + return new ComplicationSnapshot( + self.heartRate, + self.steps, + self.stress, + self.bodyBattery + ); + } + + + function onComplicationChanged(complicationId as Complications.Id) as Void + { + var complicationValue = Complications.getComplication(complicationId).value; + + switch (complicationId) + { + case self.heartRateComplicationId: + { + self.heartRate = complicationValue; + break; + } + + case self.stepsComplicationId: + { + self.steps = complicationValue; + break; + } + + case self.stressComplicationId: + { + self.stress = complicationValue; + break; + } + + case self.bodyBatteryComplicationId: + { + self.bodyBattery = complicationValue; + break; + } + } + } + + + private static function getInstance() + { + if (instance == null) + { + instance = new ComplicationSubscriber(); + } + + return instance; + } +} \ No newline at end of file diff --git a/KittieCats/source/KittieCatsApp.mc b/KittieCats/source/KittieCatsApp.mc new file mode 100644 index 0000000..3665778 --- /dev/null +++ b/KittieCats/source/KittieCatsApp.mc @@ -0,0 +1,41 @@ +import Toybox.Application; +import Toybox.Lang; +import Toybox.WatchUi; + + +class KittieCatsApp extends Application.AppBase +{ + function initialize() + { + AppBase.initialize(); + } + + + function onStart(state as Dictionary?) as Void + { + ComplicationSubscriber.subscribe(); + } + + + function onStop(state as Dictionary?) as Void + { + } + + + function getInitialView() as [Views] or [Views, InputDelegates] + { + return [ new KittieCatsView() ]; + } + + + function getSettingsView() as [ Views ] or [ Views, InputDelegates ] or Null + { + return [ new KittieCatsSettingsView(), new KittieCatsSettingsMenuDelegate() ]; + } +} + + +function getApp() as KittieCatsApp +{ + return Application.getApp() as KittieCatsApp; +} \ No newline at end of file diff --git a/KittieCats/source/KittieCatsSettingsView.mc b/KittieCats/source/KittieCatsSettingsView.mc new file mode 100644 index 0000000..115a96b --- /dev/null +++ b/KittieCats/source/KittieCatsSettingsView.mc @@ -0,0 +1,128 @@ +import Toybox.Lang; +import Toybox.WatchUi; + + + +class KittieCatsSettingsView extends WatchUi.Menu2 +{ + private var themeMenuItem; + + + function initialize() + { + Menu2.initialize(null); + Menu2.setTitle(Rez.Strings.SettingsTitle); + + self.themeMenuItem = new WatchUi.MenuItem(Rez.Strings.SettingsTheme, "", "theme", null); + Menu2.addItem(self.themeMenuItem); + + // TODO add settings for low power mode + } + + + function onShow() + { + var currentTheme = ThemeManager.getInstance().getLocalizedCurrentTheme(); + self.themeMenuItem.setSubLabel(currentTheme.Name); + } +} + + +class KittieCatsSettingsMenuDelegate extends WatchUi.Menu2InputDelegate +{ + function initialize() + { + Menu2InputDelegate.initialize(); + } + + + function onSelect(item) + { + var id = item.getId(); + + switch (id) + { + case "theme": + { + var themes = ThemeManager.getInstance().getLocalizedThemes(); + var currentTheme = ThemeManager.getInstance().getLocalizedCurrentTheme(); + + // Not sure if I like the picker UI, but it'll do for now + WatchUi.pushView(new Picker({ + :title => new WatchUi.Text({ :text => Rez.Strings.SettingsTheme }), + :pattern => [new ThemePickerFactory(themes)], + :defaults => [themes.indexOf(currentTheme)] + }), new ThemePickerDelegate(), WatchUi.SLIDE_LEFT); + + break; + } + } + } + + + function onBack() + { + WatchUi.popView(WatchUi.SLIDE_IMMEDIATE); + } +} + + + +class ThemePickerFactory extends WatchUi.PickerFactory +{ + private var themes as Array; + + + function initialize(themes) + { + PickerFactory.initialize(); + + self.themes = themes; + } + + + function getDrawable(item, isSelected) + { + return new WatchUi.Text({ + :text => self.themes[item].Name, + :color => Graphics.COLOR_WHITE, + :font => Graphics.FONT_SMALL, + :justification => Graphics.TEXT_JUSTIFY_LEFT + }); + } + + + function getSize() + { + return self.themes.size(); + } + + + function getValue(item) + { + return self.themes[item].Id; + } +} + + +class ThemePickerDelegate extends WatchUi.PickerDelegate +{ + function initialize() + { + PickerDelegate.initialize(); + } + + + function onAccept(values) + { + ThemeManager.getInstance().setCurrentThemeId(values[0]); + + WatchUi.popView(WatchUi.SLIDE_DOWN); + return true; + } + + function onCancel() + { + return true; + } +} \ No newline at end of file diff --git a/KittieCats/source/KittieCatsView.mc b/KittieCats/source/KittieCatsView.mc new file mode 100644 index 0000000..3ebcb3f --- /dev/null +++ b/KittieCats/source/KittieCatsView.mc @@ -0,0 +1,203 @@ +import Toybox.Graphics; +import Toybox.Lang; +import Toybox.Math; +import Toybox.System; +import Toybox.WatchUi; + + + +class KittieCatsView extends WatchUi.WatchFace +{ + private var theme; + + private var backgroundHi; + private var backgroundLo; + + private var armHour; + private var armMinute; + private var armSecond; + + private var icons; + private var iconSize; + + private var sleepTime; + private var wakeTime; + + private var sleeping = false; + + + function initialize() + { + WatchFace.initialize(); + } + + + function onLayout(dc as Dc) as Void + { + } + + + function onShow() as Void + { + self.theme = ThemeManager.getInstance().getCurrentTheme(); + + self.backgroundHi = Application.loadResource(self.theme.BackgroundHi); + self.backgroundLo = Application.loadResource(self.theme.BackgroundLo); + + self.armHour = Application.loadResource(self.theme.ArmHour); + self.armMinute = Application.loadResource(self.theme.ArmMinute); + self.armSecond = Application.loadResource(self.theme.ArmSecond); + + + self.icons = Application.loadResource(Rez.Drawables.Icons); + self.iconSize = self.icons.getHeight(); + + + var profile = UserProfile.getProfile(); + self.sleepTime = profile.sleepTime; + self.wakeTime = profile.wakeTime; + } + + + function onHide() as Void + { + self.backgroundHi = null; + self.backgroundLo = null; + + self.armHour = null; + self.armMinute = null; + self.armSecond = null; + + self.icons = null; + } + + + + function onUpdate(dc as Dc) as Void + { + var highPowerMode = self.isHighPowerMode(); + + var clockTime = System.getClockTime(); + var complications = ComplicationSubscriber.getSnapshot(); + + + dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_BLACK); + dc.clear(); + dc.setAntiAlias(true); + + dc.drawBitmap(0, 0, highPowerMode ? self.backgroundHi : self.backgroundLo); + + + if (highPowerMode) + { + // --- Icons --- + self.drawIcon(dc, 0, 104, 145); + self.drawIcon(dc, 1, 170, 300); + self.drawIcon(dc, 2, 170, 335); + self.drawIcon(dc, 3, 237, 335); + } + + + // --- Arms --- + // Base the hour arm on minutes as well, to prevent the arm from staying on the + // current hour tick mark right up until the 59th minute + self.drawArm(dc, self.armHour, (clockTime.hour * 60) + clockTime.min, 720, highPowerMode ? self.theme.ArmHourTintHi : self.theme.ArmHourTintLo); + self.drawArm(dc, self.armMinute, clockTime.min, 60, highPowerMode ? self.theme.ArmMinuteTintHi : self.theme.ArmMinuteTintLo); + + if (highPowerMode) + { + self.drawArm(dc, self.armSecond, clockTime.sec, 60, null); + + + // --- Complications --- + self.drawShadowedText(dc, 116, 186, Graphics.FONT_XTINY, complications.HeartRate, Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER, + Graphics.COLOR_BLACK, Graphics.COLOR_WHITE, 2); + + self.drawShadowedText(dc, 195, 313, Graphics.FONT_XTINY, complications.Steps, Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER, + Graphics.COLOR_BLACK, Graphics.COLOR_WHITE, 2); + + self.drawShadowedText(dc, 195, 346, Graphics.FONT_XTINY, complications.BodyBattery, Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER, + Graphics.COLOR_BLACK, Graphics.COLOR_WHITE, 2); + + self.drawShadowedText(dc, 261, 346, Graphics.FONT_XTINY, complications.Stress, Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER, + Graphics.COLOR_BLACK, Graphics.COLOR_WHITE, 2); + } + } + + + function onExitSleep() as Void + { + self.sleeping = false; + } + + + function onEnterSleep() as Void + { + self.sleeping = true; + } + + + private function isHighPowerMode() as Boolean + { + if (self.sleeping) + { + return false; + } + + // Since isSleepMode is deprecated, base it on the wake and sleep times. Unfortunately this means + // the display will never be in high power mode between those times even if desired. + // Can't figure out how to check for the locked state either. + // Might want to check for DND mode instead if this is annoying. + var now = Time.now(); + var today = Time.today(); + + if (now.greaterThan(today.add(self.sleepTime)) && now.lessThan(today.add(self.wakeTime))) + { + return false; + } + + return true; + } + + + + private function drawArm(dc as Dc, bitmap as BitmapType, value as Lang.Numeric, max as Lang.Numeric, tintColor as Lang.Numeric or Null) as Void + { + var offsetX = Math.floor(bitmap.getWidth() / 2); + var offsetY = Math.floor(bitmap.getWidth() / 2); + + var rotation = new Graphics.AffineTransform(); + rotation.translate(offsetX, offsetY); + rotation.rotate(((Math.PI * 2) / max) * value); + rotation.translate(-offsetX, -offsetY); + + // NOTE: drawBitmap2 fails on the Forerunner 165 with an error "Source must be native color format" + dc.drawBitmap2(0, 0, bitmap, { + :tintColor => tintColor, + :filterMode => Graphics.FILTER_MODE_BILINEAR, + :transform => rotation + }); + } + + + private function drawShadowedText(dc as Dc, x as Lang.Numeric, y as Lang.Numeric, font as Graphics.FontType, + text as Lang.Object or Null, justification as Graphics.TextJustification or Lang.Number, + textColor as Graphics.ColorType, shadowColor as Graphics.ColorType, shadowOffset as Lang.Numeric) as Void + { + dc.setColor(shadowColor, Graphics.COLOR_TRANSPARENT); + dc.drawText(x + shadowOffset, y + shadowOffset, font, text, justification); + + dc.setColor(textColor, Graphics.COLOR_TRANSPARENT); + dc.drawText(x, y, font, text, justification); + } + + + private function drawIcon(dc as Dc, iconIndex as Lang.Numeric, x as Lang.Numeric, y as Lang.Numeric) + { + dc.drawBitmap2(x - (iconIndex * self.iconSize), y, self.icons, { + :bitmapX => iconIndex * self.iconSize, + :bitmapWidth => self.iconSize, + :bitmapHeight => self.iconSize + }); + } +} diff --git a/KittieCats/source/ThemeManager.mc b/KittieCats/source/ThemeManager.mc new file mode 100644 index 0000000..38a5b4a --- /dev/null +++ b/KittieCats/source/ThemeManager.mc @@ -0,0 +1,158 @@ +import Toybox.Lang; + + +class Theme +{ + public var BackgroundHi; + public var BackgroundLo; + + public var ArmHour; + public var ArmMinute; + public var ArmSecond; + + public var ArmHourTintHi; + public var ArmHourTintLo; + public var ArmMinuteTintHi; + public var ArmMinuteTintLo; + + + function initialize(backgroundHi, backgroundLo, armHour, armMinute, armSecond, variablesJsonResource) + { + self.BackgroundHi = backgroundHi; + self.BackgroundLo = backgroundLo; + + self.ArmHour = armHour; + self.ArmMinute = armMinute; + self.ArmSecond = armSecond; + + var variables = Application.loadResource(variablesJsonResource) as Dictionary; + self.ArmHourTintHi = variables["ArmHourTintHi"]; + self.ArmHourTintLo = variables["ArmHourTintLo"]; + self.ArmMinuteTintHi = variables["ArmMinuteTintHi"]; + self.ArmMinuteTintLo = variables["ArmMinuteTintLo"]; + } +} + + +class ThemeInfo +{ + public var Id; + public var Name; + + + function initialize(id, name) + { + self.Id = id; + self.Name = name; + } +} + + +class ThemeManager +{ + private static var instance = null; + + private var currentThemeId = "default"; + private var themes as Dictionary = {}; + private var localizedThemes as Array or Null = null; + + + public function initialize() + { + self.themes["default"] = new Theme( + Rez.Drawables.Background, + Rez.Drawables.Ticks, + + Rez.Drawables.ArmHour, + Rez.Drawables.ArmMinute, + Rez.Drawables.ArmSecond, + + Rez.JsonData.ThemeDefault + ); + + self.themes["xmas"] = new Theme( + Rez.Drawables.BackgroundXmas, + Rez.Drawables.Ticks, + + Rez.Drawables.ArmHour, + Rez.Drawables.ArmMinute, + Rez.Drawables.ArmSecond, + + Rez.JsonData.ThemeXMas + ); + + + var settingsThemeId = Application.Storage.getValue("theme"); + if (settingsThemeId != null && self.themes.hasKey(settingsThemeId)) + { + self.currentThemeId = settingsThemeId; + } + } + + + public function getCurrentThemeId() as Lang.String + { + return self.currentThemeId; + } + + + public function setCurrentThemeId(id as Lang.String) as Void + { + if (self.themes.hasKey(id)) + { + self.currentThemeId = id; + Application.Storage.setValue("theme", id); + } + } + + + public function getCurrentTheme() as Theme + { + return self.themes[self.getCurrentThemeId()]; + } + + + public function getLocalizedThemes() as Array + { + if (self.localizedThemes == null) + { + var themes = Application.loadResource(Rez.JsonData.ThemeInfo) as Array>; + self.localizedThemes = []; + + for (var i = 0; i < themes.size(); i++) + { + self.localizedThemes.add(new ThemeInfo(themes[i]["id"], themes[i]["name"])); + } + } + + return self.localizedThemes; + } + + + public function getLocalizedCurrentTheme() as ThemeInfo + { + var themes = self.getLocalizedThemes(); + var currentThemeId = self.getCurrentThemeId(); + + for (var i = 0; i < themes.size(); i++) + { + if (themes[i].Id.equals(currentThemeId)) + { + return themes[i]; + } + } + + return themes[0]; + } + + + public static function getInstance() + { + if (instance == null) + { + instance = new ThemeManager(); + } + + return instance; + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e106ecc --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# My Garmin Watchfaces + +This repository contains a few Garmin watchface apps I made for personal use. They are very specific and not very customizable, so I highly recommend the excellent [Watchface Builder](https://garmin.watchfacebuilder.com/) app instead. + +However, I wanted some customizations that the Watchface Builder could not provide, as well as some experience developing for the watch myself. I'm sharing the code as a starting point or reference for your own versions. + +If you want to use this code, do note that I did not include some of the assets used (such as background images) for privacy reasons. They are listed in .gitignore. + + +## Notes for NixOS + +I created the included shell.nix to be able to run the Garmin Connect SDK on NixOS. At the time of writing the most recent SDK is Connect IQ 7.3.1 (September 2024). Newer versions may require tweaks to the library dependencies. + +You will need to have ```programs.nix-ld.enable = true;``` in your configuration.nix somewhere. Run ```nix-shell``` in this folder to get a shell to be able to run the ```sdkmanager``` which you can download from the [Garmin developer site](https://developer.garmin.com/connect-iq/sdk/). + +Note: at least on my KDE Plasma 6 Wayland environment, the login form you need to fill when running for the first time does not size properly. With a bit of effort (I recommend just tabbing on the keyboard) you can get focus to the username and password fields. They were practically zero width for me so you can't see what you are entering and I recommend pasting the values, hoping for the best and pressing Enter. + + +For debugging I have not yet been able (nor really tried that hard) to get VS Code to start the simulator succesfully. An easy workaround is to open a separate nix-shell to run it beforehand, the VS Code debugger will connect to it just fine. + +```nix-shell --run ~/.Garmin/ConnectIQ/Sdks/connectiq-sdk-lin-7.3.1-2024-09-23-df7b5816a/bin/simulator``` \ No newline at end of file diff --git a/nix-run-simulator.sh b/nix-run-simulator.sh new file mode 100755 index 0000000..bc5f615 --- /dev/null +++ b/nix-run-simulator.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +CURRENT_SDK="$HOME/.Garmin/ConnectIQ/current-sdk.cfg" + +if [[ -f "$CURRENT_SDK" ]]; then + FOLDER_PATH=$(<"$CURRENT_SDK") + + if [[ -d "$FOLDER_PATH" ]]; then + nix-shell --run "$FOLDER_PATH/bin/simulator" + else + echo "Error: The folder specified in $CURRENT_SDK does not exist." + exit 1 + fi +else + echo "Error: no currect Garmin Connect IQ SDK in $CURRENT_SDK." + exit 1 +fi \ No newline at end of file diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..eb5f778 --- /dev/null +++ b/shell.nix @@ -0,0 +1,48 @@ +with import {}; + +let + zlib129 = stdenv.mkDerivation { + pname = "Zlib 1.2.9"; + version = "1.2.9"; + src = fetchurl { + url = "https://zlib.net/fossils/zlib-1.2.9.tar.gz"; + sha256 = "sha256-c6swLvMe0edIldKvVvUvWFPyawNw8+8hlUNHrOxeqiE="; + }; + }; +in + mkShell { + buildInputs = [ + zlib129 + ]; + + NIX_LD_LIBRARY_PATH = lib.makeLibraryPath [ + # For SDK Manager + at-spi2-atk + cairo + curl + expat + freetype + gdk-pixbuf + glib + glib-networking + gtk3 + libjpeg8 + libpng + libsecret + libsoup + pango + stdenv.cc.cc.lib + webkitgtk_4_0 + xorg.libSM + xorg.libX11 + xorg.libXext + xorg.libXxf86vm + zlib129 + + # For Simulator + systemdLibs + libusb1 + ]; + + GIO_EXTRA_MODULES = [ "${pkgs.glib-networking.out}/lib/gio/modules" ]; + } \ No newline at end of file