implemented search functionality with DuckDuckGo Lite

This commit is contained in:
pelgraine
2026-02-25 22:39:53 +11:00
parent 90b9045a90
commit db0fb1d4c6
2 changed files with 175 additions and 30 deletions

View File

@@ -1548,12 +1548,13 @@ void handleKeyboardInput() {
bool urlEdit = wr ? wr->isUrlEditing() : false;
bool passEdit = wr ? wr->isPasswordEntry() : false;
bool formEdit = wr ? wr->isFormFilling() : false;
if (wr && (urlEdit || passEdit || formEdit)) {
bool searchEdit = wr ? wr->isSearchEditing() : false;
if (wr && (urlEdit || passEdit || formEdit || searchEdit)) {
webReaderTextEntry = true; // Suppress ui_task.loop() in main loop
wr->handleInput(key); // Updates buffer instantly, no render
// Check if text entry ended (submitted, cancelled, etc.)
if (!wr->isUrlEditing() && !wr->isPasswordEntry() && !wr->isFormFilling()) {
if (!wr->isUrlEditing() && !wr->isPasswordEntry() && !wr->isFormFilling() && !wr->isSearchEditing()) {
// Text entry ended
webReaderTextEntry = false;
webReaderNeedsRefresh = false;
@@ -1570,7 +1571,7 @@ void handleKeyboardInput() {
webReaderTextEntry = false;
// Q from HOME mode exits the web reader entirely (like text reader)
if ((key == 'q' || key == 'Q') && wr && wr->isHome() && !wr->isUrlEditing()) {
if ((key == 'q' || key == 'Q') && wr && wr->isHome() && !wr->isUrlEditing() && !wr->isSearchEditing()) {
Serial.println("Exiting web reader");
ui_task.gotoHomeScreen();
return;
@@ -1786,22 +1787,14 @@ void handleKeyboardInput() {
// Export contacts to SD card (contacts screen only)
if (ui_task.isOnContactsScreen()) {
Serial.println("Contacts: Exporting to SD...");
// Show "working" toaster immediately (force e-ink render before blocking SD I/O)
ui_task.showAlert("Exporting to SD...", 10000);
ui_task.forceRefresh();
ui_task.loop(); // immediate render so user sees the popup
int exported = exportContactsToSD();
// Update toaster with result
if (exported >= 0) {
char alertBuf[48];
snprintf(alertBuf, sizeof(alertBuf), "Exported %d to SD", exported);
ui_task.showAlert(alertBuf, 2500);
ui_task.showAlert(alertBuf, 2000);
} else {
ui_task.showAlert("Export failed (no SD?)", 2500);
ui_task.showAlert("Export failed (no SD?)", 2000);
}
ui_task.forceRefresh();
}
break;
@@ -1809,15 +1802,9 @@ void handleKeyboardInput() {
// Import/merge contacts from SD backup (contacts screen only)
if (ui_task.isOnContactsScreen()) {
Serial.println("Contacts: Importing from SD...");
// Show "working" toaster immediately (force e-ink render before blocking SD I/O)
ui_task.showAlert("Importing from SD...", 10000);
ui_task.forceRefresh();
ui_task.loop(); // immediate render so user sees the popup
int added = importContactsFromSD();
// Update toaster with result
if (added > 0) {
// Invalidate the contacts screen cache so it rebuilds
ContactsScreen* cs2 = (ContactsScreen*)ui_task.getContactsScreen();
if (cs2) cs2->invalidateCache();
char alertBuf[48];
@@ -1825,11 +1812,10 @@ void handleKeyboardInput() {
added, (int)the_mesh.getNumContacts());
ui_task.showAlert(alertBuf, 2500);
} else if (added == 0) {
ui_task.showAlert("No new contacts to add", 2500);
ui_task.showAlert("No new contacts to add", 2000);
} else {
ui_task.showAlert("Import failed (no backup?)", 2500);
ui_task.showAlert("Import failed (no backup?)", 2000);
}
ui_task.forceRefresh();
}
break;

View File

@@ -1072,9 +1072,12 @@ private:
// Bookmarks & History
std::vector<String> _bookmarks;
std::vector<String> _history;
int _homeSelected; // Selected item in home view (0=URL bar, then bookmarks, then history)
int _homeSelected; // Selected item in home view (0=IRC, 1=URL, 2=Search, then bookmarks, then history)
int _homeScrollY; // Pixel scroll offset for home view
bool _urlEditing; // True when URL bar is active for text entry
bool _searchEditing; // True when search bar is active for text entry
char _searchBuffer[128]; // Search query text
int _searchLen;
// Link selection
int _linkInput; // Accumulated link number digits
@@ -2778,7 +2781,7 @@ private:
const int sectionH = listLineH; // Section header height
int maxChars = _charsPerLine - 2; // Account for "> " prefix
if (maxChars < 10) maxChars = 10;
int totalItems = 2 + (int)_bookmarks.size() + (int)_history.size();
int totalItems = 3 + (int)_bookmarks.size() + (int)_history.size();
// ---- Layout pass: compute virtual Y extent of each item ----
// We track: for each selectable item, its (virtualY, height).
@@ -2804,6 +2807,12 @@ private:
virtualY += urlBarH;
itemIdx++;
// Item 2: Search bar
int searchBarH = listLineH + 2;
if (itemIdx == _homeSelected) { selectedTop = virtualY; selectedBot = virtualY + searchBarH; }
virtualY += searchBarH;
itemIdx++;
// Bookmarks
if (_bookmarks.size() > 0) {
virtualY += sectionH; // "-- Bookmarks --" header
@@ -2924,6 +2933,39 @@ private:
itemIdx++;
}
// Item 2: Search bar
{
bool selected = (_homeSelected == itemIdx);
if (HOME_VISIBLE(y, searchBarH)) {
if (selected) {
display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, y + 5, display.width() - (needsScroll ? scrollbarW + 1 : 0), listLineH);
display.setColor(DisplayDriver::DARK);
} else {
display.setColor(DisplayDriver::LIGHT);
}
display.setCursor(0, y);
if (_searchEditing) {
char searchDisp[140];
int maxShow = maxChars - 8; // "Search: " prefix + cursor
int start = 0;
if (_searchLen > maxShow) start = _searchLen - maxShow;
snprintf(searchDisp, sizeof(searchDisp), "Search: %s_", _searchBuffer + start);
display.print(searchDisp);
} else if (_searchLen > 0) {
char searchDisp[140];
int maxShow = maxChars - 7;
snprintf(searchDisp, sizeof(searchDisp), "Search: %s",
_searchLen > maxShow ? (_searchBuffer + _searchLen - maxShow) : _searchBuffer);
display.print(searchDisp);
} else {
display.print("Search: [DuckDuckGo Lite]");
}
}
y += searchBarH;
itemIdx++;
}
// Bookmarks section
if (_bookmarks.size() > 0) {
// Section header
@@ -3056,15 +3098,43 @@ private:
display.setColor(DisplayDriver::YELLOW);
if (_urlEditing) {
display.print("Type URL Ent:Go");
} else if (_searchEditing) {
display.print("Type query Ent:Search");
} else {
char footerBuf[48];
bool hasData = (_cookieCount > 0 || !_history.empty());
if (hasData)
snprintf(footerBuf, sizeof(footerBuf), "Q:Bk W/S Ent:Go X:Clr");
bool onBookmark = (_homeSelected >= 3 && _homeSelected < 3 + (int)_bookmarks.size());
if (onBookmark && hasData)
snprintf(footerBuf, sizeof(footerBuf), "Ent:Go Del:Del Bkmk X:Clr Ckies");
else if (onBookmark)
snprintf(footerBuf, sizeof(footerBuf), "Q:Bk Ent:Go Del:Del Bkmk");
else if (hasData)
snprintf(footerBuf, sizeof(footerBuf), "Q:Bk W/S Ent:Go X:Clr Ckies");
else
snprintf(footerBuf, sizeof(footerBuf), "Q:Bk W/S:Nav Ent:Go");
display.print(footerBuf);
}
// Toast notification overlay (for bookmark deleted, etc.)
if (_toastMsg[0] && (millis() - _toastTime < 1500)) {
display.setTextSize(1);
int tw = display.getTextWidth(_toastMsg);
int bw = tw + 16;
int bh = 20;
int bx = (display.width() - bw) / 2;
int by = (display.height() - bh) / 2;
display.setColor(DisplayDriver::DARK);
display.fillRect(bx, by, bw, bh);
display.setColor(DisplayDriver::LIGHT);
display.drawRect(bx, by, bw, 1);
display.drawRect(bx, by + bh - 1, bw, 1);
display.drawRect(bx, by, 1, bh);
display.drawRect(bx + bw - 1, by, 1, bh);
display.setCursor(bx + 8, by + 5);
display.print(_toastMsg);
} else if (_toastMsg[0]) {
_toastMsg[0] = '\0';
}
}
void renderFetching(DisplayDriver& display) {
@@ -3409,7 +3479,7 @@ private:
}
bool handleHomeInput(char c) {
int totalItems = 2 + _bookmarks.size() + _history.size(); // IRC + URL + bookmarks + history
int totalItems = 3 + _bookmarks.size() + _history.size(); // IRC + URL + Search + bookmarks + history
if (_urlEditing) {
// URL text entry mode
@@ -3464,6 +3534,67 @@ private:
return true; // Consume all keys in editing mode
}
// Search text entry mode
if (_searchEditing) {
if (c == '\r' || c == 13) {
if (_searchLen > 0) {
_searchEditing = false;
// Build DuckDuckGo Lite search URL
// URL-encode the query: spaces become +, special chars become %XX
char encoded[256];
int ei = 0;
for (int i = 0; i < _searchLen && ei < (int)sizeof(encoded) - 4; i++) {
char ch = _searchBuffer[i];
if (ch == ' ') {
encoded[ei++] = '+';
} else if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') || ch == '-' || ch == '_' || ch == '.' || ch == '~') {
encoded[ei++] = ch;
} else {
if (ei < (int)sizeof(encoded) - 4) {
snprintf(encoded + ei, 4, "%%%02X", (unsigned char)ch);
ei += 3;
}
}
}
encoded[ei] = '\0';
snprintf(_urlBuffer, WEB_MAX_URL_LEN, "https://html.duckduckgo.com/lite/?q=%s", encoded);
_urlLen = strlen(_urlBuffer);
if (!isNetworkAvailable()) {
_mode = WIFI_SETUP;
if (!loadAndAutoConnect()) {
startWifiScan();
} else {
fetchWithSelfRef(_urlBuffer);
}
} else {
fetchWithSelfRef(_urlBuffer);
}
}
return true;
}
if (c == '\b' || c == 127) {
if (_searchLen > 0) {
_searchBuffer[--_searchLen] = '\0';
}
return true;
}
if (c == 'q' && _searchLen == 0) {
_searchEditing = false;
return true;
}
if (c == 0x1B) { // ESC
_searchEditing = false;
return true;
}
if (c >= 32 && c < 127 && _searchLen < (int)sizeof(_searchBuffer) - 1) {
_searchBuffer[_searchLen++] = c;
_searchBuffer[_searchLen] = '\0';
return true;
}
return true; // Consume all keys in search editing mode
}
// Normal navigation
if (c == 'w' || c == 'W' || c == 0xF2) {
if (_homeSelected > 0) _homeSelected--;
@@ -3494,9 +3625,14 @@ private:
_urlEditing = true;
return true;
}
// Bookmark or history item selected (offset by 2 for IRC + URL)
if (_homeSelected == 2) {
// Activate search editing
_searchEditing = true;
return true;
}
// Bookmark or history item selected (offset by 3 for IRC + URL + Search)
const char* selectedUrl = nullptr;
int bmIdx = _homeSelected - 2;
int bmIdx = _homeSelected - 3;
if (bmIdx < (int)_bookmarks.size()) {
selectedUrl = _bookmarks[bmIdx].c_str();
} else {
@@ -3522,6 +3658,25 @@ private:
return true;
}
// Delete/Backspace - remove selected bookmark
if (c == '\b' || c == 127) {
int bmIdx = _homeSelected - 3;
if (bmIdx >= 0 && bmIdx < (int)_bookmarks.size()) {
_bookmarks.erase(_bookmarks.begin() + bmIdx);
saveBookmarks();
// Adjust selection if we deleted the last item
int newTotal = 3 + _bookmarks.size() + _history.size();
if (_homeSelected >= newTotal && _homeSelected > 0) {
_homeSelected--;
}
strncpy(_toastMsg, "Bookmark deleted", sizeof(_toastMsg));
_toastTime = millis();
Serial.printf("WebReader: Deleted bookmark %d\n", bmIdx);
return true;
}
return false;
}
// X - clear all cookies
if (c == 'x' || c == 'X') {
bool hadData = (_cookieCount > 0 || !_history.empty());
@@ -4809,6 +4964,7 @@ public:
_textBuffer(nullptr), _textLen(0), _links(nullptr), _linkCount(0),
_currentPage(0), _totalPages(0),
_homeSelected(0), _homeScrollY(0), _urlEditing(false),
_searchEditing(false), _searchLen(0),
_linkInput(0), _linkInputActive(false),
_formCount(0), _forms(nullptr), _activeForm(-1), _activeField(0),
_formFieldEditing(false), _formEditLen(0), _formLastCharAt(0),
@@ -4825,6 +4981,7 @@ public:
_ircLastDataTime(0), _ircReconnectAt(0),
_ircDirty(false), _ircLastRender(0) {
_urlBuffer[0] = '\0';
_searchBuffer[0] = '\0';
_wifiPass[0] = '\0';
_pageTitle[0] = '\0';
_currentUrl[0] = '\0';
@@ -4963,6 +5120,7 @@ public:
_homeSelected = 0;
_homeScrollY = 0;
_urlEditing = false;
_searchEditing = false;
Serial.printf("WebReader: exitReader - heap after: %d, largest: %d\n",
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
@@ -4973,6 +5131,7 @@ public:
bool wantsTextReader() const { return _requestTextReader; }
void clearTextReaderRequest() { _requestTextReader = false; }
bool isUrlEditing() const { return _urlEditing && _mode == HOME; }
bool isSearchEditing() const { return _searchEditing && _mode == HOME; }
bool isWifiSetup() const { return _mode == WIFI_SETUP; }
bool isPasswordEntry() const {
return _mode == WIFI_SETUP && _wifiState == WIFI_ENTERING_PASS;