#include "DictionarySearchActivity.h" #include #include #include #include "DictionaryMargins.h" #include "DictionaryResultActivity.h" #include "MappedInputManager.h" #include "activities/util/KeyboardEntryActivity.h" #include "fontIds.h" namespace { // Dictionary path on SD card constexpr const char* DICT_BASE_PATH = "/dictionaries/dict-data"; // Global dictionary instance (lazy initialized) StarDict* g_dictionary = nullptr; StarDict& getDictionary() { if (!g_dictionary) { g_dictionary = new StarDict(DICT_BASE_PATH); if (!g_dictionary->begin()) { Serial.printf("[%lu] [DICT] Failed to initialize dictionary\n", millis()); } } return *g_dictionary; } } // namespace void DictionarySearchActivity::taskTrampoline(void* param) { auto* self = static_cast(param); self->displayTaskLoop(); } void DictionarySearchActivity::onEnter() { ActivityWithSubactivity::onEnter(); renderingMutex = xSemaphoreCreateMutex(); isSearching = false; keyboardShown = false; searchStatus = ""; updateRequired = true; xTaskCreate(&DictionarySearchActivity::taskTrampoline, "DictSearchTask", 4096, // Stack size (needs more for dictionary operations) this, // Parameters 1, // Priority &displayTaskHandle // Task handle ); // If no initial word provided, show keyboard if (searchWord.empty()) { xSemaphoreTake(renderingMutex, portMAX_DELAY); keyboardShown = true; enterNewActivity(new KeyboardEntryActivity( renderer, mappedInput, "Enter Word", "", 10, 64, // maxLength false, // not password [this](const std::string& word) { // User entered a word exitActivity(); searchWord = word; keyboardShown = false; if (!word.empty()) { performSearch(word); } else { onBack(); } }, [this]() { // User cancelled keyboard exitActivity(); keyboardShown = false; onBack(); })); xSemaphoreGive(renderingMutex); } else { // Perform search with provided word performSearch(searchWord); } } void DictionarySearchActivity::onExit() { ActivityWithSubactivity::onExit(); xSemaphoreTake(renderingMutex, portMAX_DELAY); if (displayTaskHandle) { vTaskDelete(displayTaskHandle); displayTaskHandle = nullptr; vTaskDelay(10 / portTICK_PERIOD_MS); // Let idle task free stack } vSemaphoreDelete(renderingMutex); renderingMutex = nullptr; } void DictionarySearchActivity::loop() { if (subActivity) { subActivity->loop(); return; } // Handle back button - use wasReleased to consume the full button event if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { onBack(); return; } } void DictionarySearchActivity::performSearch(const std::string& word) { isSearching = true; searchStatus = "Searching..."; updateRequired = true; // Small delay to allow render vTaskDelay(50 / portTICK_PERIOD_MS); // Initialize dictionary if needed StarDict& dict = getDictionary(); if (!dict.isReady()) { searchStatus = "Dictionary not found"; isSearching = false; updateRequired = true; return; } // Perform lookup const auto result = dict.lookup(word); if (result.found) { showResult(result.word, result.definition); } else { showNotFound(word); } } void DictionarySearchActivity::showResult(const std::string& word, const std::string& definition) { xSemaphoreTake(renderingMutex, portMAX_DELAY); isSearching = false; exitActivity(); enterNewActivity(new DictionaryResultActivity( renderer, mappedInput, word, definition, [this]() { // Back from result exitActivity(); onBack(); }, [this]() { // Search another word exitActivity(); searchWord = ""; keyboardShown = true; enterNewActivity(new KeyboardEntryActivity( renderer, mappedInput, "Enter Word", "", 10, 64, false, [this](const std::string& newWord) { exitActivity(); keyboardShown = false; if (!newWord.empty()) { performSearch(newWord); } else { onBack(); } }, [this]() { exitActivity(); keyboardShown = false; onBack(); })); })); xSemaphoreGive(renderingMutex); } void DictionarySearchActivity::showNotFound(const std::string& word) { xSemaphoreTake(renderingMutex, portMAX_DELAY); isSearching = false; exitActivity(); enterNewActivity(new DictionaryResultActivity( renderer, mappedInput, word, "", // Empty definition = not found [this]() { // Back from result exitActivity(); onBack(); }, [this]() { // Search another word exitActivity(); searchWord = ""; keyboardShown = true; enterNewActivity(new KeyboardEntryActivity( renderer, mappedInput, "Enter Word", "", 10, 64, false, [this](const std::string& newWord) { exitActivity(); keyboardShown = false; if (!newWord.empty()) { performSearch(newWord); } else { onBack(); } }, [this]() { exitActivity(); keyboardShown = false; onBack(); })); })); xSemaphoreGive(renderingMutex); } void DictionarySearchActivity::displayTaskLoop() { int animationCounter = 0; constexpr int ANIMATION_INTERVAL = 30; // ~300ms at 10ms per tick while (true) { // Handle animation updates when searching if (isSearching && !subActivity) { animationCounter++; if (animationCounter >= ANIMATION_INTERVAL) { animationCounter = 0; animationFrame = (animationFrame + 1) % 3; updateRequired = true; } } if (updateRequired && !subActivity) { updateRequired = false; xSemaphoreTake(renderingMutex, portMAX_DELAY); render(); xSemaphoreGive(renderingMutex); } vTaskDelay(10 / portTICK_PERIOD_MS); } } void DictionarySearchActivity::render() const { renderer.clearScreen(); // Get margins with button hint space for all orientations int marginTop, marginRight, marginBottom, marginLeft; getDictionaryContentMargins(renderer, &marginTop, &marginRight, &marginBottom, &marginLeft); const auto pageHeight = renderer.getScreenHeight(); // Draw header renderer.drawCenteredText(UI_12_FONT_ID, marginTop + 5, "Dictionary", true, EpdFontFamily::BOLD); if (isSearching) { // Show searching status with word and animated ellipsis // Center in content area (accounting for margins) const int centerY = marginTop + (pageHeight - marginTop - marginBottom) / 2; // Build animated ellipsis const char* dots = (animationFrame == 0) ? "." : (animationFrame == 1) ? ".." : "..."; // Show "Searching for 'word'..." char statusText[128]; snprintf(statusText, sizeof(statusText), "Searching for '%s'%s", searchWord.c_str(), dots); renderer.drawCenteredText(UI_10_FONT_ID, centerY, statusText); } renderer.displayBuffer(); }