diff --git a/freeink-sdk b/freeink-sdk index fe50609e..e016d51e 160000 --- a/freeink-sdk +++ b/freeink-sdk @@ -1 +1 @@ -Subproject commit fe50609efcb6f1ae7743ab2369e1d209456d1d7c +Subproject commit e016d51e51ccb858dbbc8e25fca9894b4ed0913b diff --git a/lib/Epub/Epub/BookMetadataCache.cpp b/lib/Epub/Epub/BookMetadataCache.cpp index 149cc1d8..a71ab5cb 100644 --- a/lib/Epub/Epub/BookMetadataCache.cpp +++ b/lib/Epub/Epub/BookMetadataCache.cpp @@ -13,8 +13,47 @@ constexpr uint8_t BOOK_CACHE_VERSION = 7; constexpr char bookBinFile[] = "/book.bin"; constexpr char tmpSpineBinFile[] = "/spine.bin.tmp"; constexpr char tmpTocBinFile[] = "/toc.bin.tmp"; +constexpr uint32_t MAX_CACHE_STRING_LEN = 4096; + +bool readStringBounded(HalFile& file, std::string& out, const uint32_t maxLen = MAX_CACHE_STRING_LEN) { + uint32_t len = 0; + if (file.read(&len, sizeof(len)) != static_cast(sizeof(len))) { + return false; + } + if (len > maxLen || len > static_cast(file.available())) { + LOG_ERR("BMC", "Invalid cache string length: %lu (max=%lu available=%d)", static_cast(len), + static_cast(maxLen), file.available()); + return false; + } + out.clear(); + if (len == 0) { + return true; + } + out.resize(len); + return file.read(out.data(), len) == static_cast(len); +} + +class CacheIoLock { + public: + explicit CacheIoLock(SemaphoreHandle_t mutex) : mutex(mutex) { + if (mutex) xSemaphoreTakeRecursive(mutex, portMAX_DELAY); + } + ~CacheIoLock() { + if (mutex) xSemaphoreGiveRecursive(mutex); + } + + private: + SemaphoreHandle_t mutex; +}; } // namespace +BookMetadataCache::~BookMetadataCache() { + if (ioMutex) { + vSemaphoreDelete(ioMutex); + ioMutex = nullptr; + } +} + /* ============= WRITING / BUILDING FUNCTIONS ================ */ bool BookMetadataCache::beginWrite() { @@ -372,6 +411,7 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri /* ============= READING / LOADING FUNCTIONS ================ */ bool BookMetadataCache::load() { + CacheIoLock ioLock(ioMutex); if (!Storage.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) { return false; } @@ -389,11 +429,13 @@ bool BookMetadataCache::load() { serialization::readPod(bookFile, spineCount); serialization::readPod(bookFile, tocCount); - serialization::readString(bookFile, coreMetadata.title); - serialization::readString(bookFile, coreMetadata.author); - serialization::readString(bookFile, coreMetadata.language); - serialization::readString(bookFile, coreMetadata.coverItemHref); - serialization::readString(bookFile, coreMetadata.textReferenceHref); + if (!readStringBounded(bookFile, coreMetadata.title) || !readStringBounded(bookFile, coreMetadata.author) || + !readStringBounded(bookFile, coreMetadata.language) || !readStringBounded(bookFile, coreMetadata.coverItemHref) || + !readStringBounded(bookFile, coreMetadata.textReferenceHref)) { + LOG_ERR("BMC", "Invalid cache metadata strings"); + bookFile.close(); + return false; + } loaded = true; LOG_DBG("BMC", "Loaded cache data: %d spine, %d TOC entries", spineCount, tocCount); @@ -401,6 +443,7 @@ bool BookMetadataCache::load() { } BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) { + CacheIoLock ioLock(ioMutex); if (!loaded) { LOG_ERR("BMC", "getSpineEntry called but cache not loaded"); return {}; @@ -415,11 +458,16 @@ BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) bookFile.seek(lutOffset + sizeof(uint32_t) * index); uint32_t spineEntryPos; serialization::readPod(bookFile, spineEntryPos); + if (spineEntryPos >= bookFile.size()) { + LOG_ERR("BMC", "Spine entry offset out of range: %lu", static_cast(spineEntryPos)); + return {}; + } bookFile.seek(spineEntryPos); return readSpineEntry(bookFile); } BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) { + CacheIoLock ioLock(ioMutex); if (!loaded) { LOG_ERR("BMC", "getTocEntry called but cache not loaded"); return {}; @@ -434,24 +482,33 @@ BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) { bookFile.seek(lutOffset + sizeof(uint32_t) * spineCount + sizeof(uint32_t) * index); uint32_t tocEntryPos; serialization::readPod(bookFile, tocEntryPos); + if (tocEntryPos >= bookFile.size()) { + LOG_ERR("BMC", "TOC entry offset out of range: %lu", static_cast(tocEntryPos)); + return {}; + } bookFile.seek(tocEntryPos); return readTocEntry(bookFile); } BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(HalFile& file) const { SpineEntry entry; - serialization::readString(file, entry.href); - serialization::readPod(file, entry.cumulativeSize); - serialization::readPod(file, entry.tocIndex); + if (!readStringBounded(file, entry.href) || + file.read(&entry.cumulativeSize, sizeof(entry.cumulativeSize)) != static_cast(sizeof(entry.cumulativeSize)) || + file.read(&entry.tocIndex, sizeof(entry.tocIndex)) != static_cast(sizeof(entry.tocIndex))) { + LOG_ERR("BMC", "Invalid spine cache entry"); + return {}; + } return entry; } BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const { TocEntry entry; - serialization::readString(file, entry.title); - serialization::readString(file, entry.href); - serialization::readString(file, entry.anchor); - serialization::readPod(file, entry.level); - serialization::readPod(file, entry.spineIndex); + if (!readStringBounded(file, entry.title) || !readStringBounded(file, entry.href) || + !readStringBounded(file, entry.anchor) || + file.read(&entry.level, sizeof(entry.level)) != static_cast(sizeof(entry.level)) || + file.read(&entry.spineIndex, sizeof(entry.spineIndex)) != static_cast(sizeof(entry.spineIndex))) { + LOG_ERR("BMC", "Invalid TOC cache entry"); + return {}; + } return entry; } diff --git a/lib/Epub/Epub/BookMetadataCache.h b/lib/Epub/Epub/BookMetadataCache.h index 2114c664..0c4bd354 100644 --- a/lib/Epub/Epub/BookMetadataCache.h +++ b/lib/Epub/Epub/BookMetadataCache.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -54,6 +55,7 @@ class BookMetadataCache { // Temp file handles during build HalFile spineFile; HalFile tocFile; + SemaphoreHandle_t ioMutex; // Index for fast href→spineIndex lookup (used only for large EPUBs) struct SpineHrefIndexEntry { @@ -85,8 +87,14 @@ class BookMetadataCache { BookMetadata coreMetadata; explicit BookMetadataCache(std::string cachePath) - : cachePath(std::move(cachePath)), lutOffset(0), spineCount(0), tocCount(0), loaded(false), buildMode(false) {} - ~BookMetadataCache() = default; + : cachePath(std::move(cachePath)), + lutOffset(0), + spineCount(0), + tocCount(0), + loaded(false), + buildMode(false), + ioMutex(xSemaphoreCreateRecursiveMutex()) {} + ~BookMetadataCache(); // Building phase (stream to disk immediately) bool beginWrite(); diff --git a/lib/Logging/Logging.cpp b/lib/Logging/Logging.cpp index 1547a1b6..8f9298cd 100644 --- a/lib/Logging/Logging.cpp +++ b/lib/Logging/Logging.cpp @@ -1,9 +1,10 @@ -#include "Logging.h" - +#include #include #include +#include "Logging.h" + #define MAX_ENTRY_LEN 256 #define MAX_LOG_LINES 16 @@ -61,12 +62,13 @@ void logPrintf(const char* level, const char* origin, const char* format, ...) { } } va_end(args); -#if defined(CONFIG_IDF_TARGET_ESP32S3) - // ESP32-S3 over USB-Serial-JTAG (HWCDC): Serial's `operator bool` reads false under a - // host monitor (and HWCDC.write itself drops when it thinks it's disconnected), so the - // `if (logSerial)` path below silently swallows every line. esp_rom_printf writes to the - // always-on ROM/IDF console — the same channel that carries the boot banner and ARDUHAL - // logs — so output is actually visible. `buf` is already fully formatted; pass via %s. +#if FREEINK_LOG_TRANSPORT == FREEINK_LOG_TRANSPORT_USB_CDC_WRITE + // Native USB CDC can report false while PlatformIO monitor is attached on + // boards like LilyGo T5 S3, so write directly to the CDC object. + logSerial.write(reinterpret_cast(buf), strnlen(buf, sizeof(buf))); +#elif FREEINK_LOG_TRANSPORT == FREEINK_LOG_TRANSPORT_ROM_PRINTF + // IDF/ROM console path for boards whose monitor is attached there during + // bring-up, e.g. Sticky. esp_rom_printf("%s", buf); #else if (logSerial) { diff --git a/lib/hal/HalGPIO.cpp b/lib/hal/HalGPIO.cpp index 4169faca..ce2b3591 100644 --- a/lib/hal/HalGPIO.cpp +++ b/lib/hal/HalGPIO.cpp @@ -247,6 +247,10 @@ bool HalGPIO::wasTouchTap(float& nx, float& ny) const { return inputMgr.wasTouch bool HalGPIO::wasTouchDown(float& nx, float& ny) const { return inputMgr.wasTouchPressedAt(nx, ny); } +bool HalGPIO::isTouchTapCandidate(float& nx, float& ny, unsigned long& heldMs) const { + return inputMgr.isTouchTapCandidate(nx, ny, heldMs); +} + unsigned long HalGPIO::lastTouchHeldMs() const { return inputMgr.lastTouchHeldMs(); } bool HalGPIO::wasSwipe(float& nxStart, float& nyStart, float& nxEnd, float& nyEnd) const { diff --git a/lib/hal/HalGPIO.h b/lib/hal/HalGPIO.h index 493635ea..cdf9a0d9 100644 --- a/lib/hal/HalGPIO.h +++ b/lib/hal/HalGPIO.h @@ -86,6 +86,9 @@ class HalGPIO { // 0..1 (panel native). For showing the pressed/selected element before release. bool wasTouchDown(float& nx, float& ny) const; + // True while a touch remains within tap slop; writes touch-down position and held time. + bool isTouchTapCandidate(float& nx, float& ny, unsigned long& heldMs) const; + // Duration (ms) of the last touch contact, latched on release. Valid on the // release frame (alongside wasTouchTap). For tap-vs-long-press decisions. unsigned long lastTouchHeldMs() const; diff --git a/platformio.ini b/platformio.ini index dc38b9ab..5c94f4ac 100644 --- a/platformio.ini +++ b/platformio.ini @@ -117,6 +117,30 @@ build_flags = ; serial output is disabled in slim builds to save space -UENABLE_SERIAL_LOG +; --- LilyGo T5 S3 (ESP32-S3 N16R8, 4.7" ED047TC1 / raw-parallel EPD) --------- +; Uses FreeInk's LilyGo board-support library for the PCA9535/TPS65185 display +; power hooks and expander button. +; pio run -e lilygo_t5s3 -t upload --upload-port /dev/tty.usbmodemXXXX +[env:lilygo_t5s3] +extends = base +board = esp32-s3-devkitc1-n16r8 +board_build.mcu = esp32s3 +board_build.flash_mode = qio +board_build.arduino.memory_type = qio_opi +build_flags = + ${base.build_flags} + -DBOARD_HAS_PSRAM + -DFREEINK_DEVICE_LILYGO=1 + -DCROSSPOINT_VERSION=\"${crosspoint.version}-lilygo_t5s3\" + -DCROSSPOINT_SHOW_BUTTON_HINTS=0 + -DENABLE_SERIAL_LOG + -DLOG_LEVEL=2 + -DTOUCH_PROBE_DEBUG=1 +lib_deps = + ${base.lib_deps} + BoardT5S3=symlink://freeink-sdk/libs/hardware/BoardT5S3 + m5stack/M5GFX @ 0.2.20 + ; --- M5Paper v1.1 (classic ESP32-D0WDQ6, 4.7" 540x960 / IT8951E) ------------- ; Different MCU family than the C3 base: override board/mcu/flash, disable the ; C3-only native-USB CDC (logs over UART0), and select the M5Paper device. diff --git a/src/MappedInputManager.cpp b/src/MappedInputManager.cpp index ce0e524f..5049a8aa 100644 --- a/src/MappedInputManager.cpp +++ b/src/MappedInputManager.cpp @@ -63,8 +63,25 @@ bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint // Top-left corner fallback, as a fraction of the logical screen. Generous to hit. static constexpr float BACK_GESTURE_FRAC_X = 0.22f; static constexpr float BACK_GESTURE_FRAC_Y = 0.12f; +static constexpr float BOTTOM_EDGE_BACK_GESTURE_FRAC_Y = 0.14f; +static constexpr unsigned long TOUCH_DOWN_SELECT_DELAY_MS = 90; + +bool MappedInputManager::wasBottomEdgeSwipeUp() const { + float nxs = 0.0f, nys = 0.0f, nxe = 0.0f, nye = 0.0f; + if (!gpio.wasSwipe(nxs, nys, nxe, nye)) return false; + + int sx = 0, sy = 0, ex = 0, ey = 0; + renderer.tapToLogical(nxs, nys, sx, sy); + renderer.tapToLogical(nxe, nye, ex, ey); + + const int screenHeight = renderer.getScreenHeight(); + const int bottomEdgeTop = screenHeight - static_cast(screenHeight * BOTTOM_EDGE_BACK_GESTURE_FRAC_Y); + return sy >= bottomEdgeTop && ey < sy && std::abs(ey - sy) > std::abs(ex - sx); +} bool MappedInputManager::wasBackGesture() const { + if (wasBottomEdgeSwipeUp()) return true; + float nx = 0.0f, ny = 0.0f; if (!gpio.wasTouchTap(nx, ny)) return false; int lx = 0, ly = 0; @@ -87,10 +104,36 @@ bool MappedInputManager::wasItemTapped(int& id) const { bool MappedInputManager::wasItemTouchedDown(int& id) const { float nx = 0.0f, ny = 0.0f; - if (!gpio.wasTouchDown(nx, ny)) return false; - int lx = 0, ly = 0; - renderer.tapToLogical(nx, ny, lx, ly); - return TouchRegistry::getInstance().hitTest(lx, ly, TouchRegistry::Item, id); + unsigned long heldMs = 0; + if (!gpio.isTouchTapCandidate(nx, ny, heldMs)) { + touchSelectTracking = false; + touchSelectEmitted = false; + touchSelectId = -1; + return false; + } + + if (!touchSelectTracking) { + float downNx = 0.0f, downNy = 0.0f; + if (!gpio.wasTouchDown(downNx, downNy)) return false; + + int lx = 0, ly = 0; + renderer.tapToLogical(downNx, downNy, lx, ly); + int candidateId = -1; + if (!TouchRegistry::getInstance().hitTest(lx, ly, TouchRegistry::Item, candidateId)) { + touchSelectTracking = false; + touchSelectEmitted = false; + touchSelectId = -1; + return false; + } + touchSelectTracking = true; + touchSelectEmitted = false; + touchSelectId = candidateId; + } + + if (touchSelectEmitted || heldMs < TOUCH_DOWN_SELECT_DELAY_MS) return false; + touchSelectEmitted = true; + id = touchSelectId; + return true; } bool MappedInputManager::wasItemLongPressed(int& id) const { @@ -122,6 +165,7 @@ bool MappedInputManager::wasCoverTapped(int& id) const { bool MappedInputManager::wasListScroll(int& index, int count, int pageItems) const { if (count <= 0) return false; if (pageItems < 1) pageItems = 1; + if (wasBottomEdgeSwipeUp()) return false; const SwipeDir swipe = wasSwipe(); if (swipe == SwipeDir::Up) { index = std::min(index + pageItems, count - 1); @@ -135,6 +179,8 @@ bool MappedInputManager::wasListScroll(int& index, int count, int pageItems) con } MappedInputManager::SwipeDir MappedInputManager::wasSwipe() const { + if (wasBottomEdgeSwipeUp()) return SwipeDir::None; + float nxs = 0.0f, nys = 0.0f, nxe = 0.0f, nye = 0.0f; if (!gpio.wasSwipe(nxs, nys, nxe, nye)) return SwipeDir::None; // Map both endpoints into the logical frame so the direction follows what the diff --git a/src/MappedInputManager.h b/src/MappedInputManager.h index 03cbb5b7..d623a371 100644 --- a/src/MappedInputManager.h +++ b/src/MappedInputManager.h @@ -23,13 +23,14 @@ class MappedInputManager { bool wasReleased(Button button) const; bool isPressed(Button button) const; // Touch "back" gesture: a tap on the theme's header Back target, or in the - // top-left corner. Folded into Back's edges, so every screen gets it for free. + // top-left corner, or a swipe up from the visible bottom edge. Folded into + // Back's edges, so every screen gets it for free. bool wasBackGesture() const; // True (and writes the id) if a tap this frame hit a TouchRegistry item. // Activities treat the id as "select + activate". False on non-touch devices. bool wasItemTapped(int& id) const; - // Press-edge of wasItemTapped: fires on touch-DOWN over an item so the activity - // can show it selected before release. Mirrors button nav (move, then confirm). + // Stable touch-down candidate: fires once when a touch remains over an item + // briefly without crossing tap slop, so swipes do not show row selection. bool wasItemTouchedDown(int& id) const; // Subset of wasItemTapped's releases held past the long-press threshold (check // this first). Distinguishes tap vs press-and-hold. @@ -58,4 +59,9 @@ class MappedInputManager { GfxRenderer& renderer; bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const; + bool wasBottomEdgeSwipeUp() const; + + mutable bool touchSelectTracking = false; + mutable bool touchSelectEmitted = false; + mutable int touchSelectId = -1; }; diff --git a/src/activities/home/FileBrowserActivity.cpp b/src/activities/home/FileBrowserActivity.cpp index b8fef609..c04e5be0 100644 --- a/src/activities/home/FileBrowserActivity.cpp +++ b/src/activities/home/FileBrowserActivity.cpp @@ -214,8 +214,6 @@ void FileBrowserActivity::loop() { return; } - // Touch-down moves the selector to the pressed entry (shows selected state); release - // opens it below. int downId = -1; if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast(files.size())) { selectorIndex = downId; diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 9632da32..f23ec02d 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -181,7 +181,7 @@ void HomeActivity::loop() { // Tap a menu button to select + activate it. The button menu registers // menu-local ids (drawn with selectorIndex offset by recentBooks.size()), so map - // back into the global selector space. Touch-down shows it selected; release opens. + // back into the global selector space. int downId = -1; if (mappedInput.wasItemTouchedDown(downId)) { selectorIndex = static_cast(recentBooks.size()) + downId; @@ -274,8 +274,7 @@ void HomeActivity::render(RenderLock&&) { const int menuY = metrics.homeTopPadding + metrics.homeCoverTileHeight + metrics.homeMenuTopOffset; const int bottomReserve = (BaseTheme::showButtonHints() ? metrics.buttonHintsHeight : 0) + metrics.verticalSpacing; GUI.drawButtonMenu( - renderer, Rect{0, menuY, pageWidth, pageHeight - menuY - bottomReserve}, - static_cast(menuItems.size()), + renderer, Rect{0, menuY, pageWidth, pageHeight - menuY - bottomReserve}, static_cast(menuItems.size()), metrics.homeContinueReadingInMenu ? selectorIndex : selectorIndex - recentBooks.size(), [&menuItems](int index) { return std::string(menuItems[index]); }, [&menuIcons](int index) { return menuIcons[index]; }); diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index ae446f81..b1dd92db 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -417,7 +417,7 @@ void WifiSelectionActivity::loop() { requestUpdate(); return; } - // Touch: down-select highlights the pressed network, tap selects it (like Confirm). + // Touch: stable press highlights; tap selects it (like Confirm). Drag/scroll contacts do not preselect a row. int downId = -1; if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast(networks.size())) { selectedNetworkIndex = downId; diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index 22d92224..379afa61 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -3,11 +3,39 @@ #include #include +#include + #include "MappedInputManager.h" #include "components/UITheme.h" #include "fontIds.h" -int EpubReaderChapterSelectionActivity::getTotalItems() const { return epub->getTocItemsCount(); } +int EpubReaderChapterSelectionActivity::getTotalItems() const { return epub ? epub->getTocItemsCount() : 0; } + +void EpubReaderChapterSelectionActivity::cancelAndFinish() { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); +} + +void EpubReaderChapterSelectionActivity::activateSelection(int index) { + const int totalItems = getTotalItems(); + if (!epub || totalItems <= 0) { + cancelAndFinish(); + return; + } + + index = std::clamp(index, 0, totalItems - 1); + const auto tocItem = epub->getTocItem(index); + if (tocItem.spineIndex < 0 || tocItem.spineIndex >= epub->getSpineItemsCount()) { + cancelAndFinish(); + return; + } + + selectorIndex = index; + setResult(ChapterResult{tocItem.spineIndex, tocItem.anchor}); + finish(); +} void EpubReaderChapterSelectionActivity::onEnter() { Activity::onEnter(); @@ -30,6 +58,12 @@ void EpubReaderChapterSelectionActivity::onExit() { Activity::onExit(); } void EpubReaderChapterSelectionActivity::loop() { const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false); const int totalItems = getTotalItems(); + if (!epub || totalItems <= 0) { + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + cancelAndFinish(); + } + return; + } // Vertical swipe page-scrolls the list (touch nav without the side buttons). if (mappedInput.wasListScroll(selectorIndex, totalItems, pageItems)) { @@ -45,23 +79,17 @@ void EpubReaderChapterSelectionActivity::loop() { int tappedId = -1; const bool tapped = mappedInput.wasItemTapped(tappedId); - if (tapped && tappedId >= 0 && tappedId < totalItems) selectorIndex = tappedId; - if (tapped || mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - const auto tocItem = epub->getTocItem(selectorIndex); - if (tocItem.spineIndex == -1) { - ActivityResult result; - result.isCancelled = true; - setResult(std::move(result)); - finish(); - } else { - setResult(ChapterResult{tocItem.spineIndex, tocItem.anchor}); - finish(); - } + const bool validTap = tapped && tappedId >= 0 && tappedId < totalItems; + const bool confirm = mappedInput.wasReleased(MappedInputManager::Button::Confirm); + if (validTap) { + selectorIndex = tappedId; + } + if (validTap || confirm) { + activateSelection(selectorIndex); + return; } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - ActivityResult result; - result.isCancelled = true; - setResult(std::move(result)); - finish(); + cancelAndFinish(); + return; } buttonNavigator.onNextRelease([this, totalItems] { @@ -98,10 +126,12 @@ void EpubReaderChapterSelectionActivity::render(RenderLock&&) { const int contentHeight = screen.height - contentTop - metrics.verticalSpacing; const int totalItems = getTotalItems(); - GUI.drawList(renderer, Rect{screen.x, contentTop, screen.width, contentHeight}, totalItems, selectorIndex, + const int displayIndex = totalItems > 0 ? std::clamp(selectorIndex, 0, totalItems - 1) : 0; + GUI.drawList(renderer, Rect{screen.x, contentTop, screen.width, contentHeight}, totalItems, displayIndex, [this](int index) { auto item = epub->getTocItem(index); - std::string indent((item.level - 1) * 2, ' '); + const int level = item.level > 0 ? item.level - 1 : 0; + std::string indent(level * 2, ' '); return indent + item.title; }); diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.h b/src/activities/reader/EpubReaderChapterSelectionActivity.h index 9d593e30..7a91ed31 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.h +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.h @@ -19,6 +19,8 @@ class EpubReaderChapterSelectionActivity final : public Activity { // Total TOC items count int getTotalItems() const; + void cancelAndFinish(); + void activateSelection(int index); public: explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index dbe12247..0c952238 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -64,8 +64,6 @@ void EpubReaderMenuActivity::loop() { return; } - // Touch-down moves the selection to the pressed item (shows selected state), like - // moving with Up/Down; release activates it below. int downId = -1; if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast(menuItems.size())) { selectedIndex = downId; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index a7fbdcfd..80302410 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -133,15 +133,14 @@ void SettingsActivity::loop() { } else if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) { // Vertical swipe page-scrolls the list (touch nav without the side buttons). // count is settingsCount + 1 because row 0 is the category tab bar. - const int page = std::max( - 1, UITheme::getNumberOfItemsPerPage(renderer, true, true, BaseTheme::showButtonHints(), false)); + const int page = + std::max(1, UITheme::getNumberOfItemsPerPage(renderer, true, true, BaseTheme::showButtonHints(), false)); mappedInput.wasListScroll(selectedSettingIndex, settingsCount + 1, page); requestUpdate(); } // Tap a settings row to select + activate it. Row 0 is the tab bar, so the list - // is drawn at selectedSettingIndex - 1; map the tapped row back by +1. Touch-down - // shows it selected; release toggles/activates below. + // is drawn at selectedSettingIndex - 1; map the tapped row back by +1. int downId = -1; if (!swiped && mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < settingsCount) { selectedSettingIndex = downId + 1; diff --git a/src/main.cpp b/src/main.cpp index 7595cf30..077dd659 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -12,6 +12,10 @@ #include #include #include + +#if defined(FREEINK_DEVICE_LILYGO) && FREEINK_DEVICE_LILYGO +#include +#endif #include #include #include @@ -367,6 +371,12 @@ void setup() { #endif #endif +#if defined(FREEINK_DEVICE_LILYGO) && FREEINK_DEVICE_LILYGO + // LilyGo T5 S3 board-support owns the shared I2C setup plus non-reader + // peripherals that can otherwise contend with SD/display pins. + BoardT5S3::begin(); +#endif + HalSystem::begin(); // Read-and-clear so a panic later in setup() doesn't loop into silent reboot.