Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions lib/I18n/translations/english.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,30 @@ STR_WEATHER_SHOWERS: "Showers"
STR_WEATHER_THUNDERSTORM: "Thunderstorm"
STR_LAST_UPDATED: "Updated"

STR_TASKS: "To-Do List"
STR_TASKS_FETCHING: "Fetching tasks..."
STR_TASKS_NO_TASKS: "Nothing to do"
STR_TASKS_OFFLINE: "WiFi required"
STR_TASKS_FETCH_FAILED: "Failed to load tasks"
STR_TASKS_TOKEN_SETUP_HINT: "Edit /.crosspoint/tasks.json on the SD card and set todoistApiToken"
STR_TASKS_UPDATED: "Updated"
STR_PROVIDER_TODOIST: "Todoist"
STR_TASKS_PROVIDER: "Provider"
STR_TASKS_DESIGN_MODE: "Design"
STR_TASKS_MINIMAL: "Minimal"
STR_TASKS_DAILY: "Daily"
STR_TASKS_DATE_FILTER: "Date filter"
STR_TASKS_OVERDUE_FILTER: "Overdue filter"
STR_TASKS_FILTER_NONE: "None"
STR_TASKS_FILTER_TODAY: "Today"
STR_TASKS_FILTER_THIS_WEEK: "This week"
STR_TASKS_FILTER_THIS_MONTH: "This month"
STR_TASKS_FILTER_LAST_7: "Last 7 days"
STR_TASKS_FILTER_ALL: "All"
STR_TASKS_DATE_FORMAT: "Date format"
STR_TASKS_ORIENTATION: "Orientation"
STR_TASKS_FORGET: "Forget"
STR_TASKS_FORGET_PROMPT: "Are you sure?"

STR_POMO_FOCUS_LABEL: "Focus:"
STR_POMO_SHORT_BREAK: "Short break:"
Expand Down
2 changes: 2 additions & 0 deletions src/CrossPetSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ bool CrossPetSettings::saveToFile() const {
doc["appFlashcard"] = appFlashcard;
doc["flashcardNewPerDay"] = flashcardNewPerDay;
doc["flashcardMaxReviewPerDay"] = flashcardMaxReviewPerDay;
doc["appTasks"] = appTasks;

String json;
serializeJson(doc, json);
Expand Down Expand Up @@ -82,6 +83,7 @@ bool CrossPetSettings::loadFromFile() {
appFlashcard = doc["appFlashcard"] | (uint8_t)1;
flashcardNewPerDay = doc["flashcardNewPerDay"] | (uint8_t)10;
flashcardMaxReviewPerDay = doc["flashcardMaxReviewPerDay"] | (uint8_t)250;
appTasks = doc["appTasks"] | (uint8_t)1;
LOG_DBG("CPS", "CrossPet settings loaded from file");
return true;
}
Expand Down
1 change: 1 addition & 0 deletions src/CrossPetSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class CrossPetSettings {
uint8_t appFlashcard = 1; // Per-app visibility toggle (1=show, 0=hide)
uint8_t flashcardNewPerDay = 10; // New cards per day limit
uint8_t flashcardMaxReviewPerDay = 250; // Max reviews per day (capped at 255 for uint8_t)
uint8_t appTasks = 1; // To-Do List app visibility (1=show, 0=hide)

// Ghost Mode — ctOS-style security toolkit home screen
uint8_t ghostMode = 0;
Expand Down
1 change: 1 addition & 0 deletions src/activities/settings/SettingsActivity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ void SettingsActivity::onEnter() {
{StrId::STR_SLEEP_IMAGE_PICKER, &CrossPetSettings::appSleepImagePicker, "appSleepImagePicker"},
{StrId::STR_GAMES, &CrossPetSettings::appGames, "appGames"},
{StrId::STR_FLASHCARD, &CrossPetSettings::appFlashcard, "appFlashcard"},
{StrId::STR_TASKS, &CrossPetSettings::appTasks, "appTasks"},
};
for (const auto& t : appToggles) {
auto field = t.field;
Expand Down
226 changes: 226 additions & 0 deletions src/activities/settings/TasksSettingsActivity.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
#include "TasksSettingsActivity.h"

#include <GfxRenderer.h>
#include <I18n.h>

#include "MappedInputManager.h"
#include "activities/util/ConfirmationActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "tasks/TasksConfig.h"
#include "tasks/TaskProvider.h"

namespace {

constexpr int kIdxProvider = 0;
constexpr int kIdxDesignMode = 1;
constexpr int kIdxDateFilter = 2;
constexpr int kIdxOverdueFilter = 3;
constexpr int kIdxDateFormat = 4;
constexpr int kIdxOrientation = 5;
constexpr int kIdxForget = 6;

const char* designModeLabel(tasks::DesignMode m) {
switch (m) {
case tasks::DesignMode::Minimal: return tr(STR_TASKS_MINIMAL);
case tasks::DesignMode::Daily: return tr(STR_TASKS_DAILY);
}
return tr(STR_TASKS_MINIMAL);
}

const char* dateFilterLabel(tasks::DateFilter f) {
switch (f) {
case tasks::DateFilter::None: return tr(STR_TASKS_FILTER_NONE);
case tasks::DateFilter::Today: return tr(STR_TASKS_FILTER_TODAY);
case tasks::DateFilter::ThisWeek: return tr(STR_TASKS_FILTER_THIS_WEEK);
case tasks::DateFilter::ThisMonth: return tr(STR_TASKS_FILTER_THIS_MONTH);
}
return tr(STR_TASKS_FILTER_TODAY);
}

const char* overdueFilterLabel(tasks::OverdueFilter f) {
switch (f) {
case tasks::OverdueFilter::None: return tr(STR_TASKS_FILTER_NONE);
case tasks::OverdueFilter::Last7Days: return tr(STR_TASKS_FILTER_LAST_7);
case tasks::OverdueFilter::All: return tr(STR_TASKS_FILTER_ALL);
}
return tr(STR_TASKS_FILTER_LAST_7);
}

const char* orientationLabel(GfxRenderer::Orientation o) {
switch (o) {
case GfxRenderer::Orientation::Portrait: return tr(STR_PORTRAIT);
case GfxRenderer::Orientation::PortraitInverted: return tr(STR_INVERTED);
case GfxRenderer::Orientation::LandscapeClockwise: return tr(STR_LANDSCAPE_CW);
case GfxRenderer::Orientation::LandscapeCounterClockwise: return tr(STR_LANDSCAPE_CCW);
}
return tr(STR_PORTRAIT);
}

GfxRenderer::Orientation nextOrientation(GfxRenderer::Orientation o) {
switch (o) {
case GfxRenderer::Orientation::Portrait: return GfxRenderer::Orientation::PortraitInverted;
case GfxRenderer::Orientation::PortraitInverted: return GfxRenderer::Orientation::LandscapeClockwise;
case GfxRenderer::Orientation::LandscapeClockwise: return GfxRenderer::Orientation::LandscapeCounterClockwise;
case GfxRenderer::Orientation::LandscapeCounterClockwise: return GfxRenderer::Orientation::Portrait;
}
return GfxRenderer::Orientation::Portrait;
}

StrId menuLabelStrId(int idx) {
switch (idx) {
case kIdxProvider: return StrId::STR_TASKS_PROVIDER;
case kIdxDesignMode: return StrId::STR_TASKS_DESIGN_MODE;
case kIdxDateFilter: return StrId::STR_TASKS_DATE_FILTER;
case kIdxOverdueFilter: return StrId::STR_TASKS_OVERDUE_FILTER;
case kIdxDateFormat: return StrId::STR_TASKS_DATE_FORMAT;
case kIdxOrientation: return StrId::STR_TASKS_ORIENTATION;
case kIdxForget: return StrId::STR_TASKS_FORGET;
}
return StrId::STR_TASKS;
}

} // namespace

void TasksSettingsActivity::onEnter() {
Activity::onEnter();
selectedIndex = 0;
requestUpdate();
}

void TasksSettingsActivity::onExit() {
Activity::onExit();
}

void TasksSettingsActivity::loop() {
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
finish();
return;
}
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
handleSelection();
return;
}
buttonNavigator.onNext([this] {
selectedIndex = (selectedIndex + 1) % kItemCount;
requestUpdate();
});
buttonNavigator.onPrevious([this] {
selectedIndex = (selectedIndex + kItemCount - 1) % kItemCount;
requestUpdate();
});
}

void TasksSettingsActivity::handleSelection() {
switch (static_cast<int>(selectedIndex)) {
case kIdxProvider:
// Only one provider exists today. The row stays for forward
// compatibility; cycling becomes a real action when a 2nd lands.
break;
case kIdxDesignMode: {
tasks::DesignMode next = (TASKS_CONFIG.getDesignMode() == tasks::DesignMode::Minimal)
? tasks::DesignMode::Daily
: tasks::DesignMode::Minimal;
TASKS_CONFIG.setDesignMode(next);
break;
}
case kIdxDateFilter: {
using DF = tasks::DateFilter;
DF cur = TASKS_CONFIG.getDateFilter();
DF next = (cur == DF::None) ? DF::Today
: (cur == DF::Today) ? DF::ThisWeek
: (cur == DF::ThisWeek) ? DF::ThisMonth
: DF::None;
TASKS_CONFIG.setDateFilter(next);
break;
}
case kIdxOverdueFilter: {
using OF = tasks::OverdueFilter;
OF cur = TASKS_CONFIG.getOverdueFilter();
OF next = (cur == OF::None) ? OF::Last7Days
: (cur == OF::Last7Days) ? OF::All
: OF::None;
TASKS_CONFIG.setOverdueFilter(next);
break;
}
case kIdxDateFormat: {
uint8_t raw = static_cast<uint8_t>(TASKS_CONFIG.getDateFormat());
raw = (raw + 1) % 6;
TASKS_CONFIG.setDateFormat(static_cast<tasks::DateFormat>(raw));
break;
}
case kIdxOrientation: {
auto next = nextOrientation(TASKS_CONFIG.getActivityOrientation());
TASKS_CONFIG.setActivityOrientation(next);
break;
}
case kIdxForget: {
startActivityForResult(
std::make_unique<ConfirmationActivity>(renderer, mappedInput,
tr(STR_TASKS_FORGET),
tr(STR_TASKS_FORGET_PROMPT)),
[this](const ActivityResult& result) {
if (!result.isCancelled) {
TASKS_CONFIG.forget();
// Close settings so the user lands directly on the setup screen.
finish();
return;
}
requestUpdate();
});
return;
}
}
requestUpdate();
}

void TasksSettingsActivity::render(RenderLock&&) {
renderer.clearScreen();

const auto& metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();

// In landscape, the hint strip lands on a side edge (left for CW, right for
// CCW) instead of the bottom — so reserve horizontal space rather than
// trimming vertical content.
const auto orientation = renderer.getOrientation();
const bool isLandscape = (orientation == GfxRenderer::LandscapeClockwise ||
orientation == GfxRenderer::LandscapeCounterClockwise);
const bool hintOnLeft = (orientation == GfxRenderer::LandscapeClockwise);
const int hintReserve = metrics.buttonHintsHeight + metrics.verticalSpacing * 2;
const int hintLeftReserve = (isLandscape && hintOnLeft) ? hintReserve : 0;
const int hintRightReserve = (isLandscape && !hintOnLeft) ? hintReserve : 0;
const int contentWidth = pageWidth - hintLeftReserve - hintRightReserve;

GUI.drawHeader(renderer, Rect{hintLeftReserve, metrics.topPadding, contentWidth, metrics.headerHeight}, tr(STR_TASKS));

const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
const int contentHeight = pageHeight - contentTop - (isLandscape ? 0 : (metrics.buttonHintsHeight + metrics.verticalSpacing * 2));

GUI.drawList(
renderer, Rect{hintLeftReserve, contentTop, contentWidth, contentHeight}, kItemCount,
static_cast<int>(selectedIndex),
[](int index) {
return std::string(I18N.get(menuLabelStrId(index)));
},
nullptr, nullptr,
[](int index) -> std::string {
switch (index) {
case kIdxProvider: return std::string(TASKS_CONFIG.getActiveProvider().displayName());
case kIdxDesignMode: return std::string(designModeLabel(TASKS_CONFIG.getDesignMode()));
case kIdxDateFilter: return std::string(dateFilterLabel(TASKS_CONFIG.getDateFilter()));
case kIdxOverdueFilter: return std::string(overdueFilterLabel(TASKS_CONFIG.getOverdueFilter()));
case kIdxDateFormat: return std::string(tasks::dateFormatToString(TASKS_CONFIG.getDateFormat()));
case kIdxOrientation: return std::string(orientationLabel(TASKS_CONFIG.getActivityOrientation()));
case kIdxForget: return std::string(); // action — no value column
}
return std::string();
},
true);

const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);

renderer.displayBuffer();
}
28 changes: 28 additions & 0 deletions src/activities/settings/TasksSettingsActivity.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#pragma once

#include "activities/Activity.h"
#include "util/ButtonNavigator.h"

#include <GfxRenderer.h>

// Per-app settings for the Tasks (To-Do List) activity. Launched from
// within TasksActivity via the Left or Right front button — no entry in
// the main SettingsActivity, mirroring crosspet's WeatherActivity
// pattern of keeping app preferences inside the owning activity.
class TasksSettingsActivity final : public Activity {
public:
TasksSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("TasksSettings", renderer, mappedInput) {}

void onEnter() override;
void onExit() override;
void loop() override;
void render(RenderLock&&) override;

private:
void handleSelection();

ButtonNavigator buttonNavigator;
size_t selectedIndex = 0;
static constexpr int kItemCount = 7;
};
Loading
Loading