diff --git a/doc/gettingStarted/Applications.md b/doc/gettingStarted/Applications.md index 8ca2b25287..3e98099e89 100644 --- a/doc/gettingStarted/Applications.md +++ b/doc/gettingStarted/Applications.md @@ -97,3 +97,9 @@ InfiniTime has 13 apps on the `main` branch at the time of writing. ![Weather UI](/doc/gettingStarted/AppsScreenshots/Weather.png) - This app shows weather info. - Please note that this app is not very useful without a device connected. + +### Tally +![Tally UI](/doc/gettingStarted/AppsScreenshots/Tally.png) +- Tap to count, or enable shake-to-count. +- Vibrates when the counter increases. +- Enable keep awake to have your count always visible. diff --git a/doc/gettingStarted/AppsScreenshots/Tally.png b/doc/gettingStarted/AppsScreenshots/Tally.png new file mode 100644 index 0000000000..8366abe0e4 Binary files /dev/null and b/doc/gettingStarted/AppsScreenshots/Tally.png differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5cd2e656a4..a326c43888 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -398,6 +398,7 @@ list(APPEND SOURCE_FILES displayapp/screens/Alarm.cpp displayapp/screens/Styles.cpp displayapp/screens/WeatherSymbols.cpp + displayapp/screens/Tally.cpp displayapp/Colors.cpp displayapp/widgets/Counter.cpp displayapp/widgets/PageIndicator.cpp @@ -615,6 +616,7 @@ set(INCLUDE_FILES displayapp/screens/Timer.h displayapp/screens/Dice.h displayapp/screens/Alarm.h + displayapp/screens/Tally.h displayapp/Colors.h displayapp/widgets/Counter.h displayapp/widgets/PageIndicator.h diff --git a/src/displayapp/UserApps.h b/src/displayapp/UserApps.h index 8dc114429f..3f38d740de 100644 --- a/src/displayapp/UserApps.h +++ b/src/displayapp/UserApps.h @@ -7,6 +7,7 @@ #include "displayapp/screens/Timer.h" #include "displayapp/screens/Twos.h" #include "displayapp/screens/Tile.h" +#include "displayapp/screens/Tally.h" #include "displayapp/screens/ApplicationList.h" #include "displayapp/screens/WatchFaceDigital.h" #include "displayapp/screens/WatchFaceAnalog.h" diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index f6feeb7b6d..6b439c0438 100644 --- a/src/displayapp/apps/Apps.h.in +++ b/src/displayapp/apps/Apps.h.in @@ -43,6 +43,7 @@ namespace Pinetime { SettingChimes, SettingShakeThreshold, SettingBluetooth, + Tally, Error }; diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt index 93196ed6a0..043c9e537e 100644 --- a/src/displayapp/apps/CMakeLists.txt +++ b/src/displayapp/apps/CMakeLists.txt @@ -15,6 +15,7 @@ else () set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Navigation") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Calculator") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Weather") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Tally") #set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Motion") set(USERAPP_TYPES "${DEFAULT_USER_APP_TYPES}" CACHE STRING "List of user apps to build into the firmware") endif () diff --git a/src/displayapp/fonts/AdwaitaMono-Bold.ttf b/src/displayapp/fonts/AdwaitaMono-Bold.ttf new file mode 100644 index 0000000000..08f874f43c Binary files /dev/null and b/src/displayapp/fonts/AdwaitaMono-Bold.ttf differ diff --git a/src/displayapp/fonts/fonts.json b/src/displayapp/fonts/fonts.json index fea3160572..03a05eee37 100644 --- a/src/displayapp/fonts/fonts.json +++ b/src/displayapp/fonts/fonts.json @@ -7,7 +7,11 @@ }, { "file": "FontAwesome5-Solid+Brands+Regular.woff", - "range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c, 0xf0f3, 0xf522, 0xf743, 0xf1ec, 0xf55a" + "range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c, 0xf0f3, 0xf522, 0xf743, 0xf1ec, 0xf55a, 0xf0dc, 0xf0e2" + }, + { + "file": "AdwaitaMono-Bold.ttf", + "range": "0x1D378" } ], "bpp": 1, diff --git a/src/displayapp/screens/Symbols.h b/src/displayapp/screens/Symbols.h index 40699b3d65..e42d981986 100644 --- a/src/displayapp/screens/Symbols.h +++ b/src/displayapp/screens/Symbols.h @@ -41,6 +41,9 @@ namespace Pinetime { static constexpr const char* sleep = "\xEE\xBD\x84"; static constexpr const char* calculator = "\xEF\x87\xAC"; static constexpr const char* backspace = "\xEF\x95\x9A"; + static constexpr const char* tally = "\xF0\x9D\x8D\xB8"; + static constexpr const char* sort = "\xEF\x83\x9C"; + static constexpr const char* undo = "\xEF\x83\xA2"; // fontawesome_weathericons.c // static constexpr const char* sun = "\xEF\x86\x85"; diff --git a/src/displayapp/screens/Tally.cpp b/src/displayapp/screens/Tally.cpp new file mode 100644 index 0000000000..fde7c42a6a --- /dev/null +++ b/src/displayapp/screens/Tally.cpp @@ -0,0 +1,189 @@ +#include "displayapp/screens/Tally.h" +#include "displayapp/screens/Screen.h" +#include "displayapp/screens/Symbols.h" +#include "components/settings/Settings.h" +#include "components/motion/MotionController.h" +#include "components/motor/MotorController.h" + +using namespace Pinetime::Applications::Screens; + +namespace { + void CountButtonEventHandler(lv_obj_t* obj, lv_event_t event) { + auto* screen = static_cast(obj->user_data); + if (event == LV_EVENT_CLICKED) { + screen->Increment(); + } + } + + void ResetButtonEventHandler(lv_obj_t* obj, lv_event_t event) { + auto* screen = static_cast(obj->user_data); + if (event == LV_EVENT_CLICKED) { + screen->Reset(); + } + } + + void ShakeButtonEventHandler(lv_obj_t* obj, lv_event_t event) { + auto* screen = static_cast(obj->user_data); + if (event == LV_EVENT_CLICKED) { + screen->ToggleShakeToCount(); + } + } + + void AwakeButtonEventHandler(lv_obj_t* obj, lv_event_t event) { + auto* screen = static_cast(obj->user_data); + if (event == LV_EVENT_CLICKED) { + screen->ToggleKeepAwake(); + } + } +} + +Tally::Tally(Controllers::MotionController& motionController, + Controllers::MotorController& motorController, + Controllers::Settings& settingsController, + System::SystemTask& systemTask) + : motionController {motionController}, motorController {motorController}, settingsController {settingsController}, wakeLock(systemTask) { + + // the count is actually a button (it was the easiest way to detect taps) + countButton = lv_btn_create(lv_scr_act(), nullptr); + countButton->user_data = this; + lv_obj_set_event_cb(countButton, CountButtonEventHandler); + lv_obj_set_size(countButton, 240, 180); + lv_obj_set_style_local_bg_color(countButton, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK); + lv_obj_align(countButton, nullptr, LV_ALIGN_IN_TOP_MID, 0, 0); + countLabel = lv_label_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_text_font(countLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &open_sans_light); + lv_obj_align(countLabel, nullptr, LV_ALIGN_IN_TOP_MID, 0, 25); + + messageLabel = lv_label_create(lv_scr_act(), nullptr); + lv_obj_align(messageLabel, nullptr, LV_ALIGN_IN_BOTTOM_MID, 0, -75); + SetMessage("Tap to count"); + + resetButton = lv_btn_create(lv_scr_act(), nullptr); + resetButton->user_data = this; + lv_obj_set_event_cb(resetButton, ResetButtonEventHandler); + lv_obj_set_size(resetButton, 76, 60); + lv_obj_align(resetButton, nullptr, LV_ALIGN_IN_BOTTOM_LEFT, 0, 0); + resetLabel = lv_label_create(resetButton, nullptr); + lv_label_set_text_static(resetLabel, Symbols::undo); + + shakeButton = lv_btn_create(lv_scr_act(), nullptr); + shakeButton->user_data = this; + lv_obj_set_event_cb(shakeButton, ShakeButtonEventHandler); + lv_obj_set_size(shakeButton, 76, 60); + lv_obj_set_style_local_bg_color(shakeButton, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_RED); + lv_obj_align(shakeButton, nullptr, LV_ALIGN_IN_BOTTOM_MID, 0, 0); + shakeLabel = lv_label_create(shakeButton, nullptr); + lv_label_set_text_static(shakeLabel, Symbols::sort); + + awakeButton = lv_btn_create(lv_scr_act(), nullptr); + awakeButton->user_data = this; + lv_obj_set_event_cb(awakeButton, AwakeButtonEventHandler); + lv_obj_set_size(awakeButton, 76, 60); + lv_obj_set_style_local_bg_color(awakeButton, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_RED); + lv_obj_align(awakeButton, nullptr, LV_ALIGN_IN_BOTTOM_RIGHT, 0, 0); + awakeLabel = lv_label_create(awakeButton, nullptr); + lv_label_set_text_static(awakeLabel, Symbols::eye); + + UpdateCount(); + + refreshTask = lv_task_create(RefreshTaskCallback, period, LV_TASK_PRIO_MID, this); +} + +Tally::~Tally() { + ShakeToWakeDisable(); + lv_task_del(refreshTask); + lv_obj_clean(lv_scr_act()); +} + +void Tally::Refresh() { + if (shakeToCountEnabled) { + if (motionController.CurrentShakeSpeed() >= settingsController.GetShakeThreshold()) { + if (shakeToCountDelay == 0) { + shakeToCountDelay = shakeDelayTime / period; + Increment(); + } + } else if (shakeToCountDelay > 0) { + shakeToCountDelay--; + } + } + + if (incrementDelay > 0) { + incrementDelay--; + } + + if (messageTimer > 0) { + messageTimer--; + if (messageTimer == 0) { + lv_label_set_text_static(messageLabel, ""); + } + } +} + +void Tally::Increment() { + if (incrementDelay <= 0) { + incrementDelay = incrementDelayTime / period; + count++; + if (count == 100) { + lv_obj_set_style_local_text_font(countLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_76); + } + motorController.RunForDuration(80); + UpdateCount(); + } +} + +void Tally::Reset() { + if (count >= 100) { + lv_obj_set_style_local_text_font(countLabel, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &open_sans_light); + } + count = 0; + UpdateCount(); +} + +void Tally::ToggleShakeToCount() { + shakeToCountEnabled = !shakeToCountEnabled; + if (shakeToCountEnabled) { + ShakeToWakeEnable(); + } else { + ShakeToWakeDisable(); + } + SetMessage(shakeToCountEnabled ? "Shake-to-count on" : "Shake-to-count off"); + lv_obj_set_style_local_bg_color(shakeButton, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, shakeToCountEnabled ? LV_COLOR_GREEN : LV_COLOR_RED); +} + +void Tally::ToggleKeepAwake() { + keepAwakeEnabled = !keepAwakeEnabled; + if (keepAwakeEnabled) { + wakeLock.Lock(); + } else { + wakeLock.Release(); + } + SetMessage(keepAwakeEnabled ? "Keep awake on" : "Keep awake off"); + lv_obj_set_style_local_bg_color(awakeButton, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, keepAwakeEnabled ? LV_COLOR_GREEN : LV_COLOR_RED); +} + +void Tally::UpdateCount() { + lv_label_set_text_fmt(countLabel, "%d", count); + lv_obj_realign(countLabel); +} + +void Tally::ShakeToWakeEnable() { + // if shake-to-wake is not enabled, enable it while this app is open + shakeToWakeTempEnable = !settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::Shake); + if (shakeToWakeTempEnable) { + settingsController.setWakeUpMode(Pinetime::Controllers::Settings::WakeUpMode::Shake, true); + } +} + +void Tally::ShakeToWakeDisable() { + // if shake-to-wake was not enabled before, disable it again + if (shakeToWakeTempEnable) { + settingsController.setWakeUpMode(Pinetime::Controllers::Settings::WakeUpMode::Shake, false); + shakeToWakeTempEnable = false; + } +} + +void Tally::SetMessage(const char* text) { + lv_label_set_text_static(messageLabel, text); + lv_obj_realign(messageLabel); + messageTimer = messageTime / period; +} diff --git a/src/displayapp/screens/Tally.h b/src/displayapp/screens/Tally.h new file mode 100644 index 0000000000..e7d8416f5e --- /dev/null +++ b/src/displayapp/screens/Tally.h @@ -0,0 +1,73 @@ +#pragma once + +#include "displayapp/apps/Apps.h" +#include "displayapp/screens/Screen.h" +#include "displayapp/Controllers.h" +#include "systemtask/SystemTask.h" +#include "systemtask/WakeLock.h" +#include "Symbols.h" + +namespace Pinetime { + namespace Applications { + namespace Screens { + class Tally : public Screen { + public: + Tally(Controllers::MotionController& motionController, + Controllers::MotorController& motorController, + Controllers::Settings& settingsController, + System::SystemTask& systemTask); + ~Tally() override; + void Refresh() override; + void Increment(); + void Reset(); + void ToggleShakeToCount(); + void ToggleKeepAwake(); + + private: + static constexpr uint16_t period = 100; // milliseconds + static constexpr uint16_t incrementDelayTime = 100; // milliseconds + static constexpr uint16_t shakeDelayTime = 200; // milliseconds + static constexpr uint16_t messageTime = 3000; // milliseconds + lv_obj_t* countButton; + lv_obj_t* countLabel; + lv_obj_t* messageLabel; + lv_obj_t* resetButton; + lv_obj_t* resetLabel; + lv_obj_t* shakeButton; + lv_obj_t* shakeLabel; + lv_obj_t* awakeButton; + lv_obj_t* awakeLabel; + uint16_t count = 0; + bool shakeToCountEnabled = false; + bool shakeToWakeTempEnable = false; + bool keepAwakeEnabled = false; + uint16_t incrementDelay = 0; + uint16_t shakeToCountDelay = 0; + uint16_t messageTimer = 0; + void UpdateCount(); + void ShakeToWakeEnable(); + void ShakeToWakeDisable(); + void SetMessage(const char* text); + + Controllers::MotionController& motionController; + Controllers::MotorController& motorController; + Controllers::Settings& settingsController; + Pinetime::System::WakeLock wakeLock; + lv_task_t* refreshTask; + }; + } + + template <> + struct AppTraits { + static constexpr Apps app = Apps::Tally; + static constexpr const char* icon = Screens::Symbols::tally; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Tally(controllers.motionController, + controllers.motorController, + controllers.settingsController, + *controllers.systemTask); + }; + }; + } +}