Xteink-X4-crosspoint-reader/src/RecentBooksStore.cpp
Kenneth e548bfc0e1
My Library: Tab bar w/ Recent Books + File Browser (#250)
# Summary

This PR introduces a reusable Tab Bar component and combines the Recent
Books and File Browser into a unified tabbed page called "My Library"
accessible from the Home screen.

## Features
### New Tab Bar Component
A flexible, reusable tab bar component added to `ScreenComponents` that
can be used throughout the application.

### New Scroll Indicator Component
A page position indicator for lists that span multiple pages.
**Features:**
- Up/down arrow indicators
- Current page fraction display (e.g., "1/3")
- Only renders when content spans multiple pages

### My Library Activity
A new unified view combining Recent Books and File Browser into a single
tabbed page.

**Tabs:**
- **Recent** - Shows recently opened books
- **Files** - Browse SD card directory structure

**Navigation:**
- Up/Down or Left/Right: Navigate through list items
- Left/Right (when first item selected): Switch between tabs
- Confirm: Open selected book or enter directory
- Back: Go up directory (Files tab) or return home
- Long press Back: Jump to root directory (Files tab)

**UI Elements:**
- Tab bar with selection indicator
- Scroll/page indicator on right side
- Side button hints (up/down arrows)
- Dynamic bottom button labels ("BACK" in subdirectories, "HOME" at
root)

## Tab Bar Usage
The tab bar component is designed to be reusable across different
activities. Here's how to use it:

### Basic Example
```cpp
#include "ScreenComponents.h"
void MyActivity::render() const {
  renderer.clearScreen();
  
  // Define tabs with labels and selection state
  std::vector<TabInfo> tabs = {
    {"Tab One", currentTab == 0},   // Selected when currentTab is 0
    {"Tab Two", currentTab == 1},   // Selected when currentTab is 1
    {"Tab Three", currentTab == 2}  // Selected when currentTab is 2
  };
  
  // Draw tab bar at Y position 15, returns height of the tab bar
  int tabBarHeight = ScreenComponents::drawTabBar(renderer, 15, tabs);
  
  // Position your content below the tab bar
  int contentStartY = 15 + tabBarHeight + 10; // Add some padding
  
  // Draw content based on selected tab
  if (currentTab == 0) {
    renderTabOneContent(contentStartY);
  } else if (currentTab == 1) {
    renderTabTwoContent(contentStartY);
  } else {
    renderTabThreeContent(contentStartY);
  }
  
  renderer.displayBuffer();
}
```
Video Demo: https://share.cleanshot.com/P6NBncFS

<img width="250"
src="https://github.com/user-attachments/assets/07de4418-968e-4a88-9b42-ac5f53d8a832"
/>
<img width="250"
src="https://github.com/user-attachments/assets/e40201ed-dcc8-4568-b008-cd2bf13ebb2a"
/>
<img width="250"
src="https://github.com/user-attachments/assets/73db269f-e629-4696-b8ca-0b8443451a05"
/>

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-21 11:38:38 +00:00

87 lines
2.2 KiB
C++

#include "RecentBooksStore.h"
#include <HardwareSerial.h>
#include <SDCardManager.h>
#include <Serialization.h>
#include <algorithm>
namespace {
constexpr uint8_t RECENT_BOOKS_FILE_VERSION = 1;
constexpr char RECENT_BOOKS_FILE[] = "/.crosspoint/recent.bin";
constexpr int MAX_RECENT_BOOKS = 10;
} // namespace
RecentBooksStore RecentBooksStore::instance;
void RecentBooksStore::addBook(const std::string& path) {
// Remove existing entry if present
auto it = std::find(recentBooks.begin(), recentBooks.end(), path);
if (it != recentBooks.end()) {
recentBooks.erase(it);
}
// Add to front
recentBooks.insert(recentBooks.begin(), path);
// Trim to max size
if (recentBooks.size() > MAX_RECENT_BOOKS) {
recentBooks.resize(MAX_RECENT_BOOKS);
}
saveToFile();
}
bool RecentBooksStore::saveToFile() const {
// Make sure the directory exists
SdMan.mkdir("/.crosspoint");
FsFile outputFile;
if (!SdMan.openFileForWrite("RBS", RECENT_BOOKS_FILE, outputFile)) {
return false;
}
serialization::writePod(outputFile, RECENT_BOOKS_FILE_VERSION);
const uint8_t count = static_cast<uint8_t>(recentBooks.size());
serialization::writePod(outputFile, count);
for (const auto& book : recentBooks) {
serialization::writeString(outputFile, book);
}
outputFile.close();
Serial.printf("[%lu] [RBS] Recent books saved to file (%d entries)\n", millis(), count);
return true;
}
bool RecentBooksStore::loadFromFile() {
FsFile inputFile;
if (!SdMan.openFileForRead("RBS", RECENT_BOOKS_FILE, inputFile)) {
return false;
}
uint8_t version;
serialization::readPod(inputFile, version);
if (version != RECENT_BOOKS_FILE_VERSION) {
Serial.printf("[%lu] [RBS] Deserialization failed: Unknown version %u\n", millis(), version);
inputFile.close();
return false;
}
uint8_t count;
serialization::readPod(inputFile, count);
recentBooks.clear();
recentBooks.reserve(count);
for (uint8_t i = 0; i < count; i++) {
std::string path;
serialization::readString(inputFile, path);
recentBooks.push_back(path);
}
inputFile.close();
Serial.printf("[%lu] [RBS] Recent books loaded from file (%d entries)\n", millis(), count);
return true;
}