Compare commits

...

5 Commits

Author SHA1 Message Date
pablohc
5a152fc21f
Merge 3b9a37e075 into f67c544e16 2026-02-02 21:28:59 +11:00
Aaron Cunliffe
f67c544e16
fix: webserver folder creation regex change (#653)
Some checks failed
CI / build (push) Has been cancelled
## Summary

Resolves #562 

Implements regex change to support valid characters discussed by
@daveallie in issue
[here](https://github.com/crosspoint-reader/crosspoint-reader/issues/562#issuecomment-3830809156).

Also rejects `.` and `..` as folder names which are invalid in FAT32 and
exFAT filesystems

## Additional Context
- Unsure on the wording for the alert, it feels overly explicit, but
that might be a good thing. Happy to change.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< PARTIALLY >**_
2026-02-02 21:27:02 +11:00
pablohc
3b9a37e075 fix: remove unused variables in MyLibraryActivity::loop()
Fixes cppcheck warnings:
- [low:style] Variable 'itemCount' is assigned a value that is never used
- [low:style] Variable 'pageItems' is assigned a value that is never used
2026-01-31 01:55:45 +01:00
pablohc
01651b0385 fix: format code with clang-format
- Apply clang-format-fix to ensure consistent code style
- Minor formatting adjustments in function comments and spacing
- Maintains all functionality while improving readability
2026-01-31 01:43:46 +01:00
pablohc
38b17ec95d refactor: implement state machine for button press detection in MyLibraryActivity
Button Behavior Matrix:
┌─────────────────┬──────────────┬──────────────────┐
│ Action          │ Condition    │ Timing           │
├─────────────────┼──────────────┼──────────────────┤
│ Switch Tab      │ Long press   │ ≥450ms hold      │
│ Skip Page       │ Double press │ <120ms between   │
│                 │              │ 1st PRESS &      │
│                 │              │ 2nd RELEASE      │
│ Move Item       │ Short press  │ <450ms press     │
└─────────────────┴──────────────┴──────────────────┘

Key Fixes:
- Each action has clear, exclusive condition
2026-01-31 00:54:47 +01:00
4 changed files with 148 additions and 44 deletions

View File

@ -153,7 +153,7 @@ Click **File Manager** to access file management features.
1. Click the **+ Add** button in the top-right corner
2. Select **New Folder** from the dropdown menu
3. Enter a folder name (letters, numbers, underscores, and hyphens only)
3. Enter a folder name (must not contain characters \" * : < > ? / \\ | and must not be . or ..)
4. Click **Create Folder**
This is useful for organizing your ebooks by genre, author, or series.

View File

@ -20,9 +20,10 @@ constexpr int RECENTS_LINE_HEIGHT = 65; // Increased for two-line items
constexpr int LEFT_MARGIN = 20;
constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator
// Timing thresholds
constexpr int SKIP_PAGE_MS = 700;
constexpr unsigned long GO_HOME_MS = 1000;
// Timing thresholds for button behavior
constexpr int LONG_PRESS_MS = 450; // Long press: change tab
constexpr int DOUBLE_PRESS_MS = 120; // Double press: skip page
constexpr unsigned long GO_HOME_MS = 1000; // Long press back: go to root
void sortFileList(std::vector<std::string>& strs) {
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
@ -126,6 +127,51 @@ void MyLibraryActivity::taskTrampoline(void* param) {
self->displayTaskLoop();
}
// Action execution: Move one item (short press timeout)
void MyLibraryActivity::executeMoveItem(bool isPrevButton) {
const int itemCount = getCurrentItemCount();
if (itemCount > 0) {
if (isPrevButton) {
selectorIndex = (selectorIndex + itemCount - 1) % itemCount;
} else {
selectorIndex = (selectorIndex + 1) % itemCount;
}
}
}
// Action execution: Skip page (double press)
void MyLibraryActivity::executeSkipPage(bool isPrevButton) {
const int itemCount = getCurrentItemCount();
const int pageItems = getPageItems();
if (itemCount > 0) {
if (isPrevButton) {
int targetPage = (selectorIndex / pageItems) - 1;
if (targetPage < 0) {
targetPage = ((itemCount - 1) / pageItems);
}
selectorIndex = targetPage * pageItems;
} else {
int targetPage = (selectorIndex / pageItems) + 1;
int maxPage = (itemCount - 1) / pageItems;
if (targetPage > maxPage) {
targetPage = 0;
}
selectorIndex = targetPage * pageItems;
}
}
}
// Action execution: Switch tab (long press)
void MyLibraryActivity::executeSwitchTab(bool isPrevButton) {
if (isPrevButton && currentTab == Tab::Files) {
currentTab = Tab::Recent;
selectorIndex = 0;
} else if (!isPrevButton && currentTab == Tab::Recent) {
currentTab = Tab::Files;
selectorIndex = 0;
}
}
void MyLibraryActivity::onEnter() {
Activity::onEnter();
@ -163,8 +209,8 @@ void MyLibraryActivity::onExit() {
}
void MyLibraryActivity::loop() {
const int itemCount = getCurrentItemCount();
const int pageItems = getPageItems();
// Get current time for all timing operations
unsigned long currentTime = millis();
// Long press BACK (1s+) in Files tab goes to root folder
if (currentTab == Tab::Files && mappedInput.isPressed(MappedInputManager::Button::Back) &&
@ -178,13 +224,6 @@ void MyLibraryActivity::loop() {
return;
}
const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Up);
const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Down);
const bool leftReleased = mappedInput.wasReleased(MappedInputManager::Button::Left);
const bool rightReleased = mappedInput.wasReleased(MappedInputManager::Button::Right);
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
// Confirm button - open selected item
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (currentTab == Tab::Recent) {
@ -234,38 +273,84 @@ void MyLibraryActivity::loop() {
return;
}
// Tab switching: Left/Right always control tabs
if (leftReleased && currentTab == Tab::Files) {
currentTab = Tab::Recent;
selectorIndex = 0;
updateRequired = true;
return;
}
if (rightReleased && currentTab == Tab::Recent) {
currentTab = Tab::Files;
selectorIndex = 0;
updateRequired = true;
return;
}
// Navigation buttons (UP/LEFT and DOWN/RIGHT have same behavior)
const bool upPressed = mappedInput.isPressed(MappedInputManager::Button::Up);
const bool leftPressed = mappedInput.isPressed(MappedInputManager::Button::Left);
const bool downPressed = mappedInput.isPressed(MappedInputManager::Button::Down);
const bool rightPressed = mappedInput.isPressed(MappedInputManager::Button::Right);
const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Up);
const bool leftReleased = mappedInput.wasReleased(MappedInputManager::Button::Left);
const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Down);
const bool rightReleased = mappedInput.wasReleased(MappedInputManager::Button::Right);
// Navigation: Up/Down moves through items only
const bool prevReleased = upReleased;
const bool nextReleased = downReleased;
// Navigation: UP/LEFT move backward, DOWN/RIGHT move forward
const bool prevPressed = upPressed || leftPressed;
const bool nextPressed = downPressed || rightPressed;
const bool prevReleased = upReleased || leftReleased;
const bool nextReleased = downReleased || rightReleased;
if (prevReleased && itemCount > 0) {
if (skipPage) {
selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + itemCount) % itemCount;
} else {
selectorIndex = (selectorIndex + itemCount - 1) % itemCount;
// State machine for button press detection
// ==========================================
// IDLE: Wait for first press
if (buttonState == ButtonState::Idle) {
if (prevPressed || nextPressed) {
buttonState = ButtonState::FirstPress;
firstPressTime = currentTime;
isPrevButtonPressed = prevPressed;
}
updateRequired = true;
} else if (nextReleased && itemCount > 0) {
if (skipPage) {
selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % itemCount;
} else {
selectorIndex = (selectorIndex + 1) % itemCount;
}
// FIRST_PRESS: Button is held, check for long press or release
else if (buttonState == ButtonState::FirstPress) {
const unsigned long holdDuration = currentTime - firstPressTime;
// Check for long press (>=450ms) - switch tab
if (holdDuration >= LONG_PRESS_MS) {
executeSwitchTab(isPrevButtonPressed);
buttonState = ButtonState::WaitingForReleaseAfterLongPress;
updateRequired = true;
return;
}
// Check for release (<450ms) - transition to waiting for second press
if ((isPrevButtonPressed && prevReleased) || (!isPrevButtonPressed && nextReleased)) {
buttonState = ButtonState::WaitingForSecondPress;
firstReleaseTime = currentTime;
}
}
// WAITING_FOR_SECOND_PRESS: First button released, waiting for second press
else if (buttonState == ButtonState::WaitingForSecondPress) {
const unsigned long waitDuration = currentTime - firstReleaseTime;
// Timeout (>=120ms without second press) - execute move_item
if (waitDuration >= DOUBLE_PRESS_MS) {
executeMoveItem(isPrevButtonPressed);
buttonState = ButtonState::Idle;
updateRequired = true;
return;
}
// Second press detected (<120ms) - double press
if (prevPressed || nextPressed) {
buttonState = ButtonState::DoublePressDetected;
}
}
// DOUBLE_PRESS_DETECTED: Second press detected, wait for release
else if (buttonState == ButtonState::DoublePressDetected) {
// Wait for second button release
if (prevReleased || nextReleased) {
executeSkipPage(isPrevButtonPressed);
buttonState = ButtonState::Idle;
updateRequired = true;
return;
}
}
// WAITING_FOR_RELEASE_AFTER_LONG_PRESS: Ignore release after long press
else if (buttonState == ButtonState::WaitingForReleaseAfterLongPress) {
// Wait for button to be released, then go back to idle
if ((isPrevButtonPressed && prevReleased) || (!isPrevButtonPressed && nextReleased)) {
buttonState = ButtonState::Idle;
}
updateRequired = true;
}
}

View File

@ -22,6 +22,25 @@ class MyLibraryActivity final : public Activity {
int selectorIndex = 0;
bool updateRequired = false;
// State machine for button press detection
enum class ButtonState {
Idle,
FirstPress,
WaitingForSecondPress,
DoublePressDetected,
WaitingForReleaseAfterLongPress
};
ButtonState buttonState = ButtonState::Idle;
unsigned long firstPressTime = 0; // Time of first PRESS
unsigned long firstReleaseTime = 0; // Time of first RELEASE
bool isPrevButtonPressed = false; // Which button was pressed first
// Action execution functions
void executeMoveItem(bool isPrevButton);
void executeSkipPage(bool isPrevButton);
void executeSwitchTab(bool isPrevButton);
// Recent tab state
std::vector<RecentBook> recentBooks;

View File

@ -1146,10 +1146,10 @@ function retryAllFailedUploads() {
return;
}
// Validate folder name (no special characters except underscore and hyphen)
const validName = /^[a-zA-Z0-9_\-]+$/.test(folderName);
// Validate folder name
const validName = /^(?!\.{1,2}$)[^"*:<>?\/\\|]+$/.test(folderName);
if (!validName) {
alert('Folder name can only contain letters, numbers, underscores, and hyphens.');
alert('Folder name cannot contain \" * : < > ? / \\ | and must not be . or ..');
return;
}