Compare commits
13 Commits
4dadea1a03
...
0e2440aea8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e2440aea8
|
||
|
|
39ef1e6d78
|
||
|
|
3cc127d658
|
||
|
|
98146f2545
|
||
|
|
f5b708424d
|
||
|
|
1c19899aa3
|
||
|
|
390f10f30d
|
||
|
|
49471e36f1
|
||
|
|
c44ac0272a
|
||
|
|
29954a3683
|
||
|
|
3eddb07a1a
|
||
|
|
f443f5dde0
|
||
|
|
3d51dfeeb7
|
@@ -398,6 +398,23 @@ enum class StrId : uint16_t {
|
|||||||
STR_INDEXING_POPUP,
|
STR_INDEXING_POPUP,
|
||||||
STR_INDEXING_STATUS_TEXT,
|
STR_INDEXING_STATUS_TEXT,
|
||||||
STR_INDEXING_STATUS_ICON,
|
STR_INDEXING_STATUS_ICON,
|
||||||
|
STR_SYNC_CLOCK,
|
||||||
|
STR_TIME_SYNCED,
|
||||||
|
STR_MANAGE_BOOK,
|
||||||
|
STR_ARCHIVE_BOOK,
|
||||||
|
STR_UNARCHIVE_BOOK,
|
||||||
|
STR_DELETE_BOOK,
|
||||||
|
STR_DELETE_CACHE_ONLY,
|
||||||
|
STR_REINDEX_BOOK,
|
||||||
|
STR_BROWSE_ARCHIVE,
|
||||||
|
STR_BOOK_ARCHIVED,
|
||||||
|
STR_BOOK_UNARCHIVED,
|
||||||
|
STR_BOOK_DELETED,
|
||||||
|
STR_CACHE_DELETED,
|
||||||
|
STR_BOOK_REINDEXED,
|
||||||
|
STR_ACTION_FAILED,
|
||||||
|
STR_BACK_TO_BEGINNING,
|
||||||
|
STR_CLOSE_MENU,
|
||||||
// Sentinel - must be last
|
// Sentinel - must be last
|
||||||
_COUNT
|
_COUNT
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Zobrazení indexování"
|
|||||||
STR_INDEXING_POPUP: "Popup"
|
STR_INDEXING_POPUP: "Popup"
|
||||||
STR_INDEXING_STATUS_TEXT: "Text stavového řádku"
|
STR_INDEXING_STATUS_TEXT: "Text stavového řádku"
|
||||||
STR_INDEXING_STATUS_ICON: "Ikona stavového řádku"
|
STR_INDEXING_STATUS_ICON: "Ikona stavového řádku"
|
||||||
|
STR_SYNC_CLOCK: "Sync Clock"
|
||||||
|
STR_TIME_SYNCED: "Time synced!"
|
||||||
|
|||||||
@@ -362,3 +362,20 @@ STR_INDEXING_DISPLAY: "Indexing Display"
|
|||||||
STR_INDEXING_POPUP: "Popup"
|
STR_INDEXING_POPUP: "Popup"
|
||||||
STR_INDEXING_STATUS_TEXT: "Status Bar Text"
|
STR_INDEXING_STATUS_TEXT: "Status Bar Text"
|
||||||
STR_INDEXING_STATUS_ICON: "Status Bar Icon"
|
STR_INDEXING_STATUS_ICON: "Status Bar Icon"
|
||||||
|
STR_SYNC_CLOCK: "Sync Clock"
|
||||||
|
STR_TIME_SYNCED: "Time synced!"
|
||||||
|
STR_MANAGE_BOOK: "Manage Book"
|
||||||
|
STR_ARCHIVE_BOOK: "Archive Book"
|
||||||
|
STR_UNARCHIVE_BOOK: "Unarchive Book"
|
||||||
|
STR_DELETE_BOOK: "Delete Book"
|
||||||
|
STR_DELETE_CACHE_ONLY: "Delete Cache Only"
|
||||||
|
STR_REINDEX_BOOK: "Reindex Book"
|
||||||
|
STR_BROWSE_ARCHIVE: "Browse Archive"
|
||||||
|
STR_BOOK_ARCHIVED: "Book archived"
|
||||||
|
STR_BOOK_UNARCHIVED: "Book unarchived"
|
||||||
|
STR_BOOK_DELETED: "Book deleted"
|
||||||
|
STR_CACHE_DELETED: "Cache deleted"
|
||||||
|
STR_BOOK_REINDEXED: "Book reindexed"
|
||||||
|
STR_ACTION_FAILED: "Action failed"
|
||||||
|
STR_BACK_TO_BEGINNING: "Back to Beginning"
|
||||||
|
STR_CLOSE_MENU: "Close Menu"
|
||||||
|
|||||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Affichage indexation"
|
|||||||
STR_INDEXING_POPUP: "Popup"
|
STR_INDEXING_POPUP: "Popup"
|
||||||
STR_INDEXING_STATUS_TEXT: "Texte barre d'état"
|
STR_INDEXING_STATUS_TEXT: "Texte barre d'état"
|
||||||
STR_INDEXING_STATUS_ICON: "Icône barre d'état"
|
STR_INDEXING_STATUS_ICON: "Icône barre d'état"
|
||||||
|
STR_SYNC_CLOCK: "Sync Clock"
|
||||||
|
STR_TIME_SYNCED: "Time synced!"
|
||||||
|
|||||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Indexierungsanzeige"
|
|||||||
STR_INDEXING_POPUP: "Popup"
|
STR_INDEXING_POPUP: "Popup"
|
||||||
STR_INDEXING_STATUS_TEXT: "Statusleistentext"
|
STR_INDEXING_STATUS_TEXT: "Statusleistentext"
|
||||||
STR_INDEXING_STATUS_ICON: "Statusleistensymbol"
|
STR_INDEXING_STATUS_ICON: "Statusleistensymbol"
|
||||||
|
STR_SYNC_CLOCK: "Sync Clock"
|
||||||
|
STR_TIME_SYNCED: "Time synced!"
|
||||||
|
|||||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Exibição de indexação"
|
|||||||
STR_INDEXING_POPUP: "Popup"
|
STR_INDEXING_POPUP: "Popup"
|
||||||
STR_INDEXING_STATUS_TEXT: "Texto da barra"
|
STR_INDEXING_STATUS_TEXT: "Texto da barra"
|
||||||
STR_INDEXING_STATUS_ICON: "Ícone da barra"
|
STR_INDEXING_STATUS_ICON: "Ícone da barra"
|
||||||
|
STR_SYNC_CLOCK: "Sync Clock"
|
||||||
|
STR_TIME_SYNCED: "Time synced!"
|
||||||
|
|||||||
@@ -316,3 +316,5 @@ STR_UPLOAD: "Încărcare"
|
|||||||
STR_BOOK_S_STYLE: "Stilul cărţii"
|
STR_BOOK_S_STYLE: "Stilul cărţii"
|
||||||
STR_EMBEDDED_STYLE: "Stil încorporat"
|
STR_EMBEDDED_STYLE: "Stil încorporat"
|
||||||
STR_OPDS_SERVER_URL: "URL server OPDS"
|
STR_OPDS_SERVER_URL: "URL server OPDS"
|
||||||
|
STR_SYNC_CLOCK: "Sync Clock"
|
||||||
|
STR_TIME_SYNCED: "Time synced!"
|
||||||
|
|||||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Отображение индексации"
|
|||||||
STR_INDEXING_POPUP: "Popup"
|
STR_INDEXING_POPUP: "Popup"
|
||||||
STR_INDEXING_STATUS_TEXT: "Текст в строке"
|
STR_INDEXING_STATUS_TEXT: "Текст в строке"
|
||||||
STR_INDEXING_STATUS_ICON: "Иконка в строке"
|
STR_INDEXING_STATUS_ICON: "Иконка в строке"
|
||||||
|
STR_SYNC_CLOCK: "Sync Clock"
|
||||||
|
STR_TIME_SYNCED: "Time synced!"
|
||||||
|
|||||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Mostrar indexación"
|
|||||||
STR_INDEXING_POPUP: "Popup"
|
STR_INDEXING_POPUP: "Popup"
|
||||||
STR_INDEXING_STATUS_TEXT: "Texto barra estado"
|
STR_INDEXING_STATUS_TEXT: "Texto barra estado"
|
||||||
STR_INDEXING_STATUS_ICON: "Icono barra estado"
|
STR_INDEXING_STATUS_ICON: "Icono barra estado"
|
||||||
|
STR_SYNC_CLOCK: "Sync Clock"
|
||||||
|
STR_TIME_SYNCED: "Time synced!"
|
||||||
|
|||||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Indexeringsvisning"
|
|||||||
STR_INDEXING_POPUP: "Popup"
|
STR_INDEXING_POPUP: "Popup"
|
||||||
STR_INDEXING_STATUS_TEXT: "Statusfältstext"
|
STR_INDEXING_STATUS_TEXT: "Statusfältstext"
|
||||||
STR_INDEXING_STATUS_ICON: "Statusfältsikon"
|
STR_INDEXING_STATUS_ICON: "Statusfältsikon"
|
||||||
|
STR_SYNC_CLOCK: "Sync Clock"
|
||||||
|
STR_TIME_SYNCED: "Time synced!"
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ bool HalStorage::remove(const char* path) { return SDCard.remove(path); }
|
|||||||
|
|
||||||
bool HalStorage::rmdir(const char* path) { return SDCard.rmdir(path); }
|
bool HalStorage::rmdir(const char* path) { return SDCard.rmdir(path); }
|
||||||
|
|
||||||
|
bool HalStorage::rename(const char* path, const char* newPath) { return SDCard.rename(path, newPath); }
|
||||||
|
|
||||||
bool HalStorage::openFileForRead(const char* moduleName, const char* path, FsFile& file) {
|
bool HalStorage::openFileForRead(const char* moduleName, const char* path, FsFile& file) {
|
||||||
return SDCard.openFileForRead(moduleName, path, file);
|
return SDCard.openFileForRead(moduleName, path, file);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class HalStorage {
|
|||||||
bool exists(const char* path);
|
bool exists(const char* path);
|
||||||
bool remove(const char* path);
|
bool remove(const char* path);
|
||||||
bool rmdir(const char* path);
|
bool rmdir(const char* path);
|
||||||
|
bool rename(const char* path, const char* newPath);
|
||||||
|
|
||||||
bool openFileForRead(const char* moduleName, const char* path, FsFile& file);
|
bool openFileForRead(const char* moduleName, const char* path, FsFile& file);
|
||||||
bool openFileForRead(const char* moduleName, const std::string& path, FsFile& file);
|
bool openFileForRead(const char* moduleName, const std::string& path, FsFile& file);
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ void RecentBooksStore::removeBook(const std::string& path) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RecentBooksStore::clear() {
|
||||||
|
recentBooks.clear();
|
||||||
|
saveToFile();
|
||||||
|
}
|
||||||
|
|
||||||
void RecentBooksStore::updateBook(const std::string& path, const std::string& title, const std::string& author,
|
void RecentBooksStore::updateBook(const std::string& path, const std::string& title, const std::string& author,
|
||||||
const std::string& coverBmpPath) {
|
const std::string& coverBmpPath) {
|
||||||
auto it =
|
auto it =
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ class RecentBooksStore {
|
|||||||
// Remove a book from the recent list by path
|
// Remove a book from the recent list by path
|
||||||
void removeBook(const std::string& path);
|
void removeBook(const std::string& path);
|
||||||
|
|
||||||
|
// Clear all recent books
|
||||||
|
void clear();
|
||||||
|
|
||||||
// Get the list of recent books (most recent first)
|
// Get the list of recent books (most recent first)
|
||||||
const std::vector<RecentBook>& getBooks() const { return recentBooks; }
|
const std::vector<RecentBook>& getBooks() const { return recentBooks; }
|
||||||
|
|
||||||
|
|||||||
115
src/activities/home/BookManageMenuActivity.cpp
Normal file
115
src/activities/home/BookManageMenuActivity.cpp
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
#include "BookManageMenuActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <I18n.h>
|
||||||
|
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "components/UITheme.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
void BookManageMenuActivity::buildMenuItems() {
|
||||||
|
menuItems.clear();
|
||||||
|
if (archived) {
|
||||||
|
menuItems.push_back({Action::UNARCHIVE, StrId::STR_UNARCHIVE_BOOK});
|
||||||
|
} else {
|
||||||
|
menuItems.push_back({Action::ARCHIVE, StrId::STR_ARCHIVE_BOOK});
|
||||||
|
}
|
||||||
|
menuItems.push_back({Action::DELETE, StrId::STR_DELETE_BOOK});
|
||||||
|
menuItems.push_back({Action::DELETE_CACHE, StrId::STR_DELETE_CACHE_ONLY});
|
||||||
|
menuItems.push_back({Action::REINDEX, StrId::STR_REINDEX_BOOK});
|
||||||
|
}
|
||||||
|
|
||||||
|
void BookManageMenuActivity::onEnter() {
|
||||||
|
Activity::onEnter();
|
||||||
|
selectedIndex = 0;
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BookManageMenuActivity::onExit() { Activity::onExit(); }
|
||||||
|
|
||||||
|
void BookManageMenuActivity::loop() {
|
||||||
|
// Long-press detection: REINDEX_FULL when long-pressing on the Reindex item
|
||||||
|
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= LONG_PRESS_MS) {
|
||||||
|
if (!ignoreNextConfirmRelease && selectedIndex < static_cast<int>(menuItems.size()) &&
|
||||||
|
menuItems[selectedIndex].action == Action::REINDEX) {
|
||||||
|
ignoreNextConfirmRelease = true;
|
||||||
|
auto cb = onAction;
|
||||||
|
cb(Action::REINDEX_FULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonNavigator.onNext([this] {
|
||||||
|
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
||||||
|
requestUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPrevious([this] {
|
||||||
|
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
||||||
|
requestUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
|
if (ignoreNextConfirmRelease) {
|
||||||
|
ignoreNextConfirmRelease = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedIndex < static_cast<int>(menuItems.size())) {
|
||||||
|
auto cb = onAction;
|
||||||
|
cb(menuItems[selectedIndex].action);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
|
auto cb = onCancel;
|
||||||
|
cb();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BookManageMenuActivity::render(Activity::RenderLock&&) {
|
||||||
|
renderer.clearScreen();
|
||||||
|
|
||||||
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
constexpr int popupMargin = 20;
|
||||||
|
constexpr int lineHeight = 30;
|
||||||
|
constexpr int titleHeight = 40;
|
||||||
|
const int optionCount = static_cast<int>(menuItems.size());
|
||||||
|
const int popupH = titleHeight + popupMargin + lineHeight * optionCount + popupMargin;
|
||||||
|
const int popupW = pageWidth - 60;
|
||||||
|
const int popupX = (pageWidth - popupW) / 2;
|
||||||
|
const int popupY = (pageHeight - popupH) / 2;
|
||||||
|
|
||||||
|
// Popup border and background
|
||||||
|
renderer.fillRect(popupX - 2, popupY - 2, popupW + 4, popupH + 4, true);
|
||||||
|
renderer.fillRect(popupX, popupY, popupW, popupH, false);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
renderer.drawText(UI_12_FONT_ID, popupX + popupMargin, popupY + 8, tr(STR_MANAGE_BOOK), true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
|
// Divider line
|
||||||
|
const int dividerY = popupY + titleHeight;
|
||||||
|
renderer.fillRect(popupX + 4, dividerY, popupW - 8, 1, true);
|
||||||
|
|
||||||
|
// Menu items
|
||||||
|
const int startY = dividerY + popupMargin / 2;
|
||||||
|
for (int i = 0; i < optionCount; ++i) {
|
||||||
|
const int itemY = startY + i * lineHeight;
|
||||||
|
const bool isSelected = (i == selectedIndex);
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
renderer.fillRect(popupX + 2, itemY, popupW - 4, lineHeight, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.drawText(UI_10_FONT_ID, popupX + popupMargin, itemY, I18N.get(menuItems[i].labelId), !isSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button hints
|
||||||
|
const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
||||||
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
61
src/activities/home/BookManageMenuActivity.h
Normal file
61
src/activities/home/BookManageMenuActivity.h
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <I18n.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Activity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
|
class BookManageMenuActivity final : public Activity {
|
||||||
|
public:
|
||||||
|
enum class Action {
|
||||||
|
ARCHIVE,
|
||||||
|
UNARCHIVE,
|
||||||
|
DELETE,
|
||||||
|
DELETE_CACHE,
|
||||||
|
REINDEX,
|
||||||
|
REINDEX_FULL,
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit BookManageMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
|
const std::string& bookPath, bool isArchived,
|
||||||
|
const std::function<void(Action)>& onAction,
|
||||||
|
const std::function<void()>& onCancel,
|
||||||
|
bool initialSkipRelease = false)
|
||||||
|
: Activity("BookManageMenu", renderer, mappedInput),
|
||||||
|
bookPath(bookPath),
|
||||||
|
archived(isArchived),
|
||||||
|
ignoreNextConfirmRelease(initialSkipRelease),
|
||||||
|
onAction(onAction),
|
||||||
|
onCancel(onCancel) {
|
||||||
|
buildMenuItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
void render(Activity::RenderLock&&) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct MenuItem {
|
||||||
|
Action action;
|
||||||
|
StrId labelId;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string bookPath;
|
||||||
|
bool archived;
|
||||||
|
std::vector<MenuItem> menuItems;
|
||||||
|
int selectedIndex = 0;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
|
|
||||||
|
bool ignoreNextConfirmRelease;
|
||||||
|
static constexpr unsigned long LONG_PRESS_MS = 700;
|
||||||
|
|
||||||
|
const std::function<void(Action)> onAction;
|
||||||
|
const std::function<void()> onCancel;
|
||||||
|
|
||||||
|
void buildMenuItems();
|
||||||
|
};
|
||||||
@@ -13,12 +13,14 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "BookManageMenuActivity.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "util/BookManager.h"
|
||||||
#include "util/StringUtils.h"
|
#include "util/StringUtils.h"
|
||||||
|
|
||||||
int HomeActivity::getMenuItemCount() const {
|
int HomeActivity::getMenuItemCount() const {
|
||||||
@@ -124,7 +126,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void HomeActivity::onEnter() {
|
void HomeActivity::onEnter() {
|
||||||
Activity::onEnter();
|
ActivityWithSubactivity::onEnter();
|
||||||
|
|
||||||
// Check if OPDS browser URL is configured
|
// Check if OPDS browser URL is configured
|
||||||
hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0;
|
hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0;
|
||||||
@@ -139,7 +141,7 @@ void HomeActivity::onEnter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void HomeActivity::onExit() {
|
void HomeActivity::onExit() {
|
||||||
Activity::onExit();
|
ActivityWithSubactivity::onExit();
|
||||||
|
|
||||||
// Free the stored cover buffer if any
|
// Free the stored cover buffer if any
|
||||||
freeCoverBuffer();
|
freeCoverBuffer();
|
||||||
@@ -188,6 +190,11 @@ void HomeActivity::freeCoverBuffer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void HomeActivity::loop() {
|
void HomeActivity::loop() {
|
||||||
|
if (subActivity) {
|
||||||
|
subActivity->loop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const int menuCount = getMenuItemCount();
|
const int menuCount = getMenuItemCount();
|
||||||
|
|
||||||
buttonNavigator.onNext([this, menuCount] {
|
buttonNavigator.onNext([this, menuCount] {
|
||||||
@@ -200,7 +207,32 @@ void HomeActivity::loop() {
|
|||||||
requestUpdate();
|
requestUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Long-press Confirm: manage menu for recent books, or browse archive for Browse Files
|
||||||
|
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= LONG_PRESS_MS &&
|
||||||
|
!ignoreNextConfirmRelease) {
|
||||||
|
if (selectorIndex < static_cast<int>(recentBooks.size())) {
|
||||||
|
// Long-press on a recent book → manage menu
|
||||||
|
ignoreNextConfirmRelease = true;
|
||||||
|
openManageMenu(recentBooks[selectorIndex].path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Browse Files is selected
|
||||||
|
const int menuSelectedIndex = selectorIndex - static_cast<int>(recentBooks.size());
|
||||||
|
if (menuSelectedIndex == 0) {
|
||||||
|
// Long-press on Browse Files → go to archive folder
|
||||||
|
ignoreNextConfirmRelease = true;
|
||||||
|
onMyLibraryOpenWithPath("/.archive", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
|
if (ignoreNextConfirmRelease) {
|
||||||
|
ignoreNextConfirmRelease = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate dynamic indices based on which options are available
|
// Calculate dynamic indices based on which options are available
|
||||||
int idx = 0;
|
int idx = 0;
|
||||||
int menuSelectedIndex = selectorIndex - static_cast<int>(recentBooks.size());
|
int menuSelectedIndex = selectorIndex - static_cast<int>(recentBooks.size());
|
||||||
@@ -210,7 +242,7 @@ void HomeActivity::loop() {
|
|||||||
const int fileTransferIdx = idx++;
|
const int fileTransferIdx = idx++;
|
||||||
const int settingsIdx = idx;
|
const int settingsIdx = idx;
|
||||||
|
|
||||||
if (selectorIndex < recentBooks.size()) {
|
if (selectorIndex < static_cast<int>(recentBooks.size())) {
|
||||||
onSelectBook(recentBooks[selectorIndex].path);
|
onSelectBook(recentBooks[selectorIndex].path);
|
||||||
} else if (menuSelectedIndex == myLibraryIdx) {
|
} else if (menuSelectedIndex == myLibraryIdx) {
|
||||||
onMyLibraryOpen();
|
onMyLibraryOpen();
|
||||||
@@ -273,3 +305,53 @@ void HomeActivity::render(Activity::RenderLock&&) {
|
|||||||
loadRecentCovers(metrics.homeCoverHeight);
|
loadRecentCovers(metrics.homeCoverHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void HomeActivity::openManageMenu(const std::string& bookPath) {
|
||||||
|
const bool isArchived = BookManager::isArchived(bookPath);
|
||||||
|
const std::string capturedPath = bookPath;
|
||||||
|
enterNewActivity(new BookManageMenuActivity(
|
||||||
|
renderer, mappedInput, capturedPath, isArchived,
|
||||||
|
[this, capturedPath](BookManageMenuActivity::Action action) {
|
||||||
|
exitActivity();
|
||||||
|
bool success = false;
|
||||||
|
switch (action) {
|
||||||
|
case BookManageMenuActivity::Action::ARCHIVE:
|
||||||
|
success = BookManager::archiveBook(capturedPath);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::UNARCHIVE:
|
||||||
|
success = BookManager::unarchiveBook(capturedPath);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::DELETE:
|
||||||
|
success = BookManager::deleteBook(capturedPath);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::DELETE_CACHE:
|
||||||
|
success = BookManager::deleteBookCache(capturedPath);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::REINDEX:
|
||||||
|
success = BookManager::reindexBook(capturedPath, false);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::REINDEX_FULL:
|
||||||
|
success = BookManager::reindexBook(capturedPath, true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
RenderLock lock(*this);
|
||||||
|
GUI.drawPopup(renderer, success ? tr(STR_DONE) : tr(STR_ACTION_FAILED));
|
||||||
|
}
|
||||||
|
requestUpdateAndWait();
|
||||||
|
// Fully reset recent books state so the home screen reloads cleanly
|
||||||
|
recentBooks.clear();
|
||||||
|
recentsLoaded = false;
|
||||||
|
recentsLoading = false;
|
||||||
|
coverRendered = false;
|
||||||
|
freeCoverBuffer();
|
||||||
|
selectorIndex = 0;
|
||||||
|
firstRenderDone = false;
|
||||||
|
requestUpdate();
|
||||||
|
},
|
||||||
|
[this] {
|
||||||
|
exitActivity();
|
||||||
|
requestUpdate();
|
||||||
|
},
|
||||||
|
true));
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../ActivityWithSubactivity.h"
|
||||||
#include "./MyLibraryActivity.h"
|
#include "./MyLibraryActivity.h"
|
||||||
#include "util/ButtonNavigator.h"
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
struct RecentBook;
|
struct RecentBook;
|
||||||
struct Rect;
|
struct Rect;
|
||||||
|
|
||||||
class HomeActivity final : public Activity {
|
class HomeActivity final : public ActivityWithSubactivity {
|
||||||
ButtonNavigator buttonNavigator;
|
ButtonNavigator buttonNavigator;
|
||||||
int selectorIndex = 0;
|
int selectorIndex = 0;
|
||||||
bool recentsLoading = false;
|
bool recentsLoading = false;
|
||||||
@@ -20,8 +20,14 @@ class HomeActivity final : public Activity {
|
|||||||
bool coverBufferStored = false; // Track if cover buffer is stored
|
bool coverBufferStored = false; // Track if cover buffer is stored
|
||||||
uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image
|
uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image
|
||||||
std::vector<RecentBook> recentBooks;
|
std::vector<RecentBook> recentBooks;
|
||||||
|
|
||||||
|
// Long-press state
|
||||||
|
bool ignoreNextConfirmRelease = false;
|
||||||
|
static constexpr unsigned long LONG_PRESS_MS = 700;
|
||||||
|
|
||||||
const std::function<void(const std::string& path)> onSelectBook;
|
const std::function<void(const std::string& path)> onSelectBook;
|
||||||
const std::function<void()> onMyLibraryOpen;
|
const std::function<void()> onMyLibraryOpen;
|
||||||
|
const std::function<void(const std::string& path, bool initialSkipRelease)> onMyLibraryOpenWithPath;
|
||||||
const std::function<void()> onRecentsOpen;
|
const std::function<void()> onRecentsOpen;
|
||||||
const std::function<void()> onSettingsOpen;
|
const std::function<void()> onSettingsOpen;
|
||||||
const std::function<void()> onFileTransferOpen;
|
const std::function<void()> onFileTransferOpen;
|
||||||
@@ -33,16 +39,20 @@ class HomeActivity final : public Activity {
|
|||||||
void freeCoverBuffer(); // Free the stored cover buffer
|
void freeCoverBuffer(); // Free the stored cover buffer
|
||||||
void loadRecentBooks(int maxBooks);
|
void loadRecentBooks(int maxBooks);
|
||||||
void loadRecentCovers(int coverHeight);
|
void loadRecentCovers(int coverHeight);
|
||||||
|
void openManageMenu(const std::string& bookPath);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
const std::function<void(const std::string& path)>& onSelectBook,
|
const std::function<void(const std::string& path)>& onSelectBook,
|
||||||
const std::function<void()>& onMyLibraryOpen, const std::function<void()>& onRecentsOpen,
|
const std::function<void()>& onMyLibraryOpen,
|
||||||
|
const std::function<void(const std::string& path, bool initialSkipRelease)>& onMyLibraryOpenWithPath,
|
||||||
|
const std::function<void()>& onRecentsOpen,
|
||||||
const std::function<void()>& onSettingsOpen, const std::function<void()>& onFileTransferOpen,
|
const std::function<void()>& onSettingsOpen, const std::function<void()>& onFileTransferOpen,
|
||||||
const std::function<void()>& onOpdsBrowserOpen)
|
const std::function<void()>& onOpdsBrowserOpen)
|
||||||
: Activity("Home", renderer, mappedInput),
|
: ActivityWithSubactivity("Home", renderer, mappedInput),
|
||||||
onSelectBook(onSelectBook),
|
onSelectBook(onSelectBook),
|
||||||
onMyLibraryOpen(onMyLibraryOpen),
|
onMyLibraryOpen(onMyLibraryOpen),
|
||||||
|
onMyLibraryOpenWithPath(onMyLibraryOpenWithPath),
|
||||||
onRecentsOpen(onRecentsOpen),
|
onRecentsOpen(onRecentsOpen),
|
||||||
onSettingsOpen(onSettingsOpen),
|
onSettingsOpen(onSettingsOpen),
|
||||||
onFileTransferOpen(onFileTransferOpen),
|
onFileTransferOpen(onFileTransferOpen),
|
||||||
|
|||||||
@@ -6,9 +6,11 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "BookManageMenuActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "util/BookManager.h"
|
||||||
#include "util/StringUtils.h"
|
#include "util/StringUtils.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -103,7 +105,7 @@ void MyLibraryActivity::loadFiles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MyLibraryActivity::onEnter() {
|
void MyLibraryActivity::onEnter() {
|
||||||
Activity::onEnter();
|
ActivityWithSubactivity::onEnter();
|
||||||
|
|
||||||
loadFiles();
|
loadFiles();
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
@@ -112,11 +114,26 @@ void MyLibraryActivity::onEnter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MyLibraryActivity::onExit() {
|
void MyLibraryActivity::onExit() {
|
||||||
Activity::onExit();
|
ActivityWithSubactivity::onExit();
|
||||||
files.clear();
|
files.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MyLibraryActivity::loop() {
|
void MyLibraryActivity::loop() {
|
||||||
|
if (subActivity) {
|
||||||
|
subActivity->loop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deferred open: wait for Confirm release before navigating to avoid stale event in reader
|
||||||
|
if (!pendingOpenPath.empty()) {
|
||||||
|
if (!mappedInput.isPressed(MappedInputManager::Button::Confirm)) {
|
||||||
|
std::string path = std::move(pendingOpenPath);
|
||||||
|
pendingOpenPath.clear();
|
||||||
|
onSelectBook(path);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Long press BACK (1s+) goes to root folder
|
// Long press BACK (1s+) goes to root folder
|
||||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS &&
|
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS &&
|
||||||
basepath != "/") {
|
basepath != "/") {
|
||||||
@@ -128,7 +145,28 @@ void MyLibraryActivity::loop() {
|
|||||||
|
|
||||||
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
|
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
|
||||||
|
|
||||||
|
// In archive context: long-press = unarchive+open, short-press = manage menu
|
||||||
|
// Outside archive: long-press = manage menu, short-press = open
|
||||||
|
const bool inArchive = isInArchive();
|
||||||
|
const bool isBookFile = !files.empty() && selectorIndex < files.size() && files[selectorIndex].back() != '/';
|
||||||
|
|
||||||
|
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= LONG_PRESS_MS &&
|
||||||
|
!ignoreNextConfirmRelease && isBookFile) {
|
||||||
|
ignoreNextConfirmRelease = true;
|
||||||
|
const std::string fullPath = (basepath.back() == '/' ? basepath : basepath + "/") + files[selectorIndex];
|
||||||
|
if (inArchive) {
|
||||||
|
unarchiveAndOpen(fullPath);
|
||||||
|
} else {
|
||||||
|
openManageMenu(fullPath);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
|
if (ignoreNextConfirmRelease) {
|
||||||
|
ignoreNextConfirmRelease = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (files.empty()) {
|
if (files.empty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -139,6 +177,9 @@ void MyLibraryActivity::loop() {
|
|||||||
loadFiles();
|
loadFiles();
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
|
} else if (inArchive) {
|
||||||
|
const std::string fullPath = basepath + files[selectorIndex];
|
||||||
|
openManageMenu(fullPath);
|
||||||
} else {
|
} else {
|
||||||
onSelectBook(basepath + files[selectorIndex]);
|
onSelectBook(basepath + files[selectorIndex]);
|
||||||
return;
|
return;
|
||||||
@@ -235,6 +276,94 @@ void MyLibraryActivity::render(Activity::RenderLock&&) {
|
|||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MyLibraryActivity::openManageMenu(const std::string& bookPath) {
|
||||||
|
const bool isArchived = BookManager::isArchived(bookPath);
|
||||||
|
const bool fromLongPress = !isInArchive();
|
||||||
|
const std::string capturedPath = bookPath;
|
||||||
|
enterNewActivity(new BookManageMenuActivity(
|
||||||
|
renderer, mappedInput, capturedPath, isArchived,
|
||||||
|
[this, capturedPath](BookManageMenuActivity::Action action) {
|
||||||
|
exitActivity();
|
||||||
|
bool success = false;
|
||||||
|
switch (action) {
|
||||||
|
case BookManageMenuActivity::Action::ARCHIVE:
|
||||||
|
success = BookManager::archiveBook(capturedPath);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::UNARCHIVE:
|
||||||
|
success = BookManager::unarchiveBook(capturedPath);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::DELETE:
|
||||||
|
success = BookManager::deleteBook(capturedPath);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::DELETE_CACHE:
|
||||||
|
success = BookManager::deleteBookCache(capturedPath);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::REINDEX:
|
||||||
|
success = BookManager::reindexBook(capturedPath, false);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::REINDEX_FULL:
|
||||||
|
success = BookManager::reindexBook(capturedPath, true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (success && BookManager::isArchived(capturedPath) &&
|
||||||
|
(action == BookManageMenuActivity::Action::UNARCHIVE ||
|
||||||
|
action == BookManageMenuActivity::Action::DELETE)) {
|
||||||
|
BookManager::cleanupEmptyArchiveDirs(capturedPath);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
RenderLock lock(*this);
|
||||||
|
GUI.drawPopup(renderer, success ? tr(STR_DONE) : tr(STR_ACTION_FAILED));
|
||||||
|
}
|
||||||
|
requestUpdateAndWait();
|
||||||
|
loadFiles();
|
||||||
|
if (files.empty() && isInArchive() && basepath != "/.archive") {
|
||||||
|
// Current directory was removed; navigate up to nearest existing ancestor
|
||||||
|
while (basepath.length() > std::string("/.archive").length()) {
|
||||||
|
auto slash = basepath.find_last_of('/');
|
||||||
|
if (slash == std::string::npos || slash == 0) break;
|
||||||
|
basepath = basepath.substr(0, slash);
|
||||||
|
loadFiles();
|
||||||
|
if (!files.empty() || basepath == "/.archive") break;
|
||||||
|
}
|
||||||
|
selectorIndex = 0;
|
||||||
|
} else if (selectorIndex >= files.size() && !files.empty()) {
|
||||||
|
selectorIndex = files.size() - 1;
|
||||||
|
}
|
||||||
|
requestUpdate();
|
||||||
|
},
|
||||||
|
[this] {
|
||||||
|
exitActivity();
|
||||||
|
requestUpdate();
|
||||||
|
},
|
||||||
|
fromLongPress));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MyLibraryActivity::isInArchive() const { return basepath.rfind("/.archive", 0) == 0; }
|
||||||
|
|
||||||
|
void MyLibraryActivity::unarchiveAndOpen(const std::string& bookPath) {
|
||||||
|
std::string unarchivedPath;
|
||||||
|
if (BookManager::unarchiveBook(bookPath, &unarchivedPath)) {
|
||||||
|
BookManager::cleanupEmptyArchiveDirs(bookPath);
|
||||||
|
{
|
||||||
|
RenderLock lock(*this);
|
||||||
|
GUI.drawPopup(renderer, tr(STR_BOOK_UNARCHIVED));
|
||||||
|
}
|
||||||
|
requestUpdateAndWait();
|
||||||
|
pendingOpenPath = unarchivedPath;
|
||||||
|
} else {
|
||||||
|
{
|
||||||
|
RenderLock lock(*this);
|
||||||
|
GUI.drawPopup(renderer, tr(STR_ACTION_FAILED));
|
||||||
|
}
|
||||||
|
requestUpdateAndWait();
|
||||||
|
loadFiles();
|
||||||
|
if (selectorIndex >= files.size() && !files.empty()) {
|
||||||
|
selectorIndex = files.size() - 1;
|
||||||
|
}
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
size_t MyLibraryActivity::findEntry(const std::string& name) const {
|
size_t MyLibraryActivity::findEntry(const std::string& name) const {
|
||||||
for (size_t i = 0; i < files.size(); i++)
|
for (size_t i = 0; i < files.size(); i++)
|
||||||
if (files[i] == name) return i;
|
if (files[i] == name) return i;
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../ActivityWithSubactivity.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "util/ButtonNavigator.h"
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class MyLibraryActivity final : public Activity {
|
class MyLibraryActivity final : public ActivityWithSubactivity {
|
||||||
private:
|
private:
|
||||||
ButtonNavigator buttonNavigator;
|
ButtonNavigator buttonNavigator;
|
||||||
|
|
||||||
@@ -17,6 +17,13 @@ class MyLibraryActivity final : public Activity {
|
|||||||
std::string basepath = "/";
|
std::string basepath = "/";
|
||||||
std::vector<std::string> files;
|
std::vector<std::string> files;
|
||||||
|
|
||||||
|
// Long-press state
|
||||||
|
bool ignoreNextConfirmRelease = false;
|
||||||
|
static constexpr unsigned long LONG_PRESS_MS = 700;
|
||||||
|
|
||||||
|
// Deferred open: wait for Confirm release before navigating to book
|
||||||
|
std::string pendingOpenPath;
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
const std::function<void(const std::string& path)> onSelectBook;
|
const std::function<void(const std::string& path)> onSelectBook;
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
@@ -24,14 +31,19 @@ class MyLibraryActivity final : public Activity {
|
|||||||
// Data loading
|
// Data loading
|
||||||
void loadFiles();
|
void loadFiles();
|
||||||
size_t findEntry(const std::string& name) const;
|
size_t findEntry(const std::string& name) const;
|
||||||
|
bool isInArchive() const;
|
||||||
|
|
||||||
|
void openManageMenu(const std::string& bookPath);
|
||||||
|
void unarchiveAndOpen(const std::string& bookPath);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
const std::function<void()>& onGoHome,
|
const std::function<void()>& onGoHome,
|
||||||
const std::function<void(const std::string& path)>& onSelectBook,
|
const std::function<void(const std::string& path)>& onSelectBook,
|
||||||
std::string initialPath = "/")
|
std::string initialPath = "/", bool initialSkipRelease = false)
|
||||||
: Activity("MyLibrary", renderer, mappedInput),
|
: ActivityWithSubactivity("MyLibrary", renderer, mappedInput),
|
||||||
basepath(initialPath.empty() ? "/" : std::move(initialPath)),
|
basepath(initialPath.empty() ? "/" : std::move(initialPath)),
|
||||||
|
ignoreNextConfirmRelease(initialSkipRelease),
|
||||||
onSelectBook(onSelectBook),
|
onSelectBook(onSelectBook),
|
||||||
onGoHome(onGoHome) {}
|
onGoHome(onGoHome) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
|
|||||||
@@ -6,10 +6,12 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "BookManageMenuActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "util/BookManager.h"
|
||||||
#include "util/StringUtils.h"
|
#include "util/StringUtils.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -31,7 +33,7 @@ void RecentBooksActivity::loadRecentBooks() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RecentBooksActivity::onEnter() {
|
void RecentBooksActivity::onEnter() {
|
||||||
Activity::onEnter();
|
ActivityWithSubactivity::onEnter();
|
||||||
|
|
||||||
// Load data
|
// Load data
|
||||||
loadRecentBooks();
|
loadRecentBooks();
|
||||||
@@ -41,14 +43,33 @@ void RecentBooksActivity::onEnter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RecentBooksActivity::onExit() {
|
void RecentBooksActivity::onExit() {
|
||||||
Activity::onExit();
|
ActivityWithSubactivity::onExit();
|
||||||
recentBooks.clear();
|
recentBooks.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RecentBooksActivity::loop() {
|
void RecentBooksActivity::loop() {
|
||||||
|
if (subActivity) {
|
||||||
|
subActivity->loop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, true);
|
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, true);
|
||||||
|
|
||||||
|
// Long-press Confirm: open manage menu
|
||||||
|
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= LONG_PRESS_MS &&
|
||||||
|
!ignoreNextConfirmRelease) {
|
||||||
|
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
|
||||||
|
ignoreNextConfirmRelease = true;
|
||||||
|
openManageMenu(recentBooks[selectorIndex].path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
|
if (ignoreNextConfirmRelease) {
|
||||||
|
ignoreNextConfirmRelease = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
|
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
|
||||||
LOG_DBG("RBA", "Selected recent book: %s", recentBooks[selectorIndex].path.c_str());
|
LOG_DBG("RBA", "Selected recent book: %s", recentBooks[selectorIndex].path.c_str());
|
||||||
onSelectBook(recentBooks[selectorIndex].path);
|
onSelectBook(recentBooks[selectorIndex].path);
|
||||||
@@ -111,3 +132,49 @@ void RecentBooksActivity::render(Activity::RenderLock&&) {
|
|||||||
|
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RecentBooksActivity::openManageMenu(const std::string& bookPath) {
|
||||||
|
const bool isArchived = BookManager::isArchived(bookPath);
|
||||||
|
const std::string capturedPath = bookPath;
|
||||||
|
enterNewActivity(new BookManageMenuActivity(
|
||||||
|
renderer, mappedInput, capturedPath, isArchived,
|
||||||
|
[this, capturedPath](BookManageMenuActivity::Action action) {
|
||||||
|
exitActivity();
|
||||||
|
bool success = false;
|
||||||
|
switch (action) {
|
||||||
|
case BookManageMenuActivity::Action::ARCHIVE:
|
||||||
|
success = BookManager::archiveBook(capturedPath);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::UNARCHIVE:
|
||||||
|
success = BookManager::unarchiveBook(capturedPath);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::DELETE:
|
||||||
|
success = BookManager::deleteBook(capturedPath);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::DELETE_CACHE:
|
||||||
|
success = BookManager::deleteBookCache(capturedPath);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::REINDEX:
|
||||||
|
success = BookManager::reindexBook(capturedPath, false);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::REINDEX_FULL:
|
||||||
|
success = BookManager::reindexBook(capturedPath, true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
RenderLock lock(*this);
|
||||||
|
GUI.drawPopup(renderer, success ? tr(STR_DONE) : tr(STR_ACTION_FAILED));
|
||||||
|
}
|
||||||
|
requestUpdateAndWait();
|
||||||
|
loadRecentBooks();
|
||||||
|
if (selectorIndex >= static_cast<int>(recentBooks.size()) && !recentBooks.empty()) {
|
||||||
|
selectorIndex = recentBooks.size() - 1;
|
||||||
|
}
|
||||||
|
requestUpdate();
|
||||||
|
},
|
||||||
|
[this] {
|
||||||
|
exitActivity();
|
||||||
|
requestUpdate();
|
||||||
|
},
|
||||||
|
true));
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../ActivityWithSubactivity.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "util/ButtonNavigator.h"
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class RecentBooksActivity final : public Activity {
|
class RecentBooksActivity final : public ActivityWithSubactivity {
|
||||||
private:
|
private:
|
||||||
ButtonNavigator buttonNavigator;
|
ButtonNavigator buttonNavigator;
|
||||||
|
|
||||||
@@ -18,18 +18,23 @@ class RecentBooksActivity final : public Activity {
|
|||||||
// Recent tab state
|
// Recent tab state
|
||||||
std::vector<RecentBook> recentBooks;
|
std::vector<RecentBook> recentBooks;
|
||||||
|
|
||||||
|
// Long-press state
|
||||||
|
bool ignoreNextConfirmRelease = false;
|
||||||
|
static constexpr unsigned long LONG_PRESS_MS = 700;
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
const std::function<void(const std::string& path)> onSelectBook;
|
const std::function<void(const std::string& path)> onSelectBook;
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
|
|
||||||
// Data loading
|
// Data loading
|
||||||
void loadRecentBooks();
|
void loadRecentBooks();
|
||||||
|
void openManageMenu(const std::string& bookPath);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit RecentBooksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
explicit RecentBooksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
const std::function<void()>& onGoHome,
|
const std::function<void()>& onGoHome,
|
||||||
const std::function<void(const std::string& path)>& onSelectBook)
|
const std::function<void(const std::string& path)>& onSelectBook)
|
||||||
: Activity("RecentBooks", renderer, mappedInput), onSelectBook(onSelectBook), onGoHome(onGoHome) {}
|
: ActivityWithSubactivity("RecentBooks", renderer, mappedInput), onSelectBook(onSelectBook), onGoHome(onGoHome) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|||||||
74
src/activities/reader/EndOfBookMenuActivity.cpp
Normal file
74
src/activities/reader/EndOfBookMenuActivity.cpp
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
#include "EndOfBookMenuActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <I18n.h>
|
||||||
|
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "components/UITheme.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
void EndOfBookMenuActivity::buildMenuItems() {
|
||||||
|
menuItems.clear();
|
||||||
|
menuItems.push_back({Action::ARCHIVE, StrId::STR_ARCHIVE_BOOK});
|
||||||
|
menuItems.push_back({Action::DELETE, StrId::STR_DELETE_BOOK});
|
||||||
|
menuItems.push_back({Action::TABLE_OF_CONTENTS, StrId::STR_TABLE_OF_CONTENTS});
|
||||||
|
menuItems.push_back({Action::BACK_TO_BEGINNING, StrId::STR_BACK_TO_BEGINNING});
|
||||||
|
menuItems.push_back({Action::CLOSE_BOOK, StrId::STR_CLOSE_BOOK});
|
||||||
|
menuItems.push_back({Action::CLOSE_MENU, StrId::STR_CLOSE_MENU});
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndOfBookMenuActivity::onEnter() {
|
||||||
|
Activity::onEnter();
|
||||||
|
selectedIndex = 0;
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndOfBookMenuActivity::onExit() { Activity::onExit(); }
|
||||||
|
|
||||||
|
void EndOfBookMenuActivity::loop() {
|
||||||
|
buttonNavigator.onNext([this] {
|
||||||
|
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
||||||
|
requestUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPrevious([this] {
|
||||||
|
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
||||||
|
requestUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
|
if (selectedIndex < static_cast<int>(menuItems.size())) {
|
||||||
|
auto cb = onAction;
|
||||||
|
cb(menuItems[selectedIndex].action);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
|
auto cb = onAction;
|
||||||
|
cb(Action::CLOSE_MENU);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndOfBookMenuActivity::render(Activity::RenderLock&&) {
|
||||||
|
renderer.clearScreen();
|
||||||
|
|
||||||
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
auto metrics = UITheme::getInstance().getMetrics();
|
||||||
|
|
||||||
|
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_END_OF_BOOK));
|
||||||
|
|
||||||
|
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
|
||||||
|
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing;
|
||||||
|
|
||||||
|
GUI.drawList(
|
||||||
|
renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast<int>(menuItems.size()), selectedIndex,
|
||||||
|
[this](int index) { return std::string(I18N.get(menuItems[index].labelId)); });
|
||||||
|
|
||||||
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
||||||
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
47
src/activities/reader/EndOfBookMenuActivity.h
Normal file
47
src/activities/reader/EndOfBookMenuActivity.h
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <I18n.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Activity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
|
class EndOfBookMenuActivity final : public Activity {
|
||||||
|
public:
|
||||||
|
enum class Action {
|
||||||
|
ARCHIVE,
|
||||||
|
DELETE,
|
||||||
|
TABLE_OF_CONTENTS,
|
||||||
|
BACK_TO_BEGINNING,
|
||||||
|
CLOSE_BOOK,
|
||||||
|
CLOSE_MENU,
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit EndOfBookMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& bookPath,
|
||||||
|
const std::function<void(Action)>& onAction)
|
||||||
|
: Activity("EndOfBookMenu", renderer, mappedInput), bookPath(bookPath), onAction(onAction) {
|
||||||
|
buildMenuItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
void render(Activity::RenderLock&&) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct MenuItem {
|
||||||
|
Action action;
|
||||||
|
StrId labelId;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string bookPath;
|
||||||
|
std::vector<MenuItem> menuItems;
|
||||||
|
int selectedIndex = 0;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
|
const std::function<void(Action)> onAction;
|
||||||
|
|
||||||
|
void buildMenuItems();
|
||||||
|
};
|
||||||
@@ -13,12 +13,14 @@
|
|||||||
#include "EpubReaderBookmarkSelectionActivity.h"
|
#include "EpubReaderBookmarkSelectionActivity.h"
|
||||||
#include "EpubReaderChapterSelectionActivity.h"
|
#include "EpubReaderChapterSelectionActivity.h"
|
||||||
#include "EpubReaderPercentSelectionActivity.h"
|
#include "EpubReaderPercentSelectionActivity.h"
|
||||||
|
#include "EndOfBookMenuActivity.h"
|
||||||
#include "KOReaderCredentialStore.h"
|
#include "KOReaderCredentialStore.h"
|
||||||
#include "KOReaderSyncActivity.h"
|
#include "KOReaderSyncActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "util/BookManager.h"
|
||||||
#include "util/BookmarkStore.h"
|
#include "util/BookmarkStore.h"
|
||||||
#include "util/Dictionary.h"
|
#include "util/Dictionary.h"
|
||||||
|
|
||||||
@@ -220,6 +222,52 @@ void EpubReaderActivity::loop() {
|
|||||||
return; // Don't access 'this' after callback
|
return; // Don't access 'this' after callback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deferred end-of-book menu (set in render() to avoid deadlock)
|
||||||
|
if (pendingEndOfBookMenu) {
|
||||||
|
pendingEndOfBookMenu = false;
|
||||||
|
endOfBookMenuOpened = true;
|
||||||
|
const std::string path = epub->getPath();
|
||||||
|
enterNewActivity(new EndOfBookMenuActivity(
|
||||||
|
renderer, mappedInput, path, [this](EndOfBookMenuActivity::Action action) {
|
||||||
|
exitActivity();
|
||||||
|
switch (action) {
|
||||||
|
case EndOfBookMenuActivity::Action::ARCHIVE:
|
||||||
|
if (epub) BookManager::archiveBook(epub->getPath());
|
||||||
|
pendingGoHome = true;
|
||||||
|
break;
|
||||||
|
case EndOfBookMenuActivity::Action::DELETE:
|
||||||
|
if (epub) BookManager::deleteBook(epub->getPath());
|
||||||
|
pendingGoHome = true;
|
||||||
|
break;
|
||||||
|
case EndOfBookMenuActivity::Action::TABLE_OF_CONTENTS:
|
||||||
|
endOfBookMenuOpened = false;
|
||||||
|
currentSpineIndex = epub->getSpineItemsCount() - 1;
|
||||||
|
nextPageNumber = UINT16_MAX;
|
||||||
|
section.reset();
|
||||||
|
openChapterSelection();
|
||||||
|
break;
|
||||||
|
case EndOfBookMenuActivity::Action::BACK_TO_BEGINNING:
|
||||||
|
currentSpineIndex = 0;
|
||||||
|
nextPageNumber = 0;
|
||||||
|
section.reset();
|
||||||
|
endOfBookMenuOpened = false;
|
||||||
|
requestUpdate();
|
||||||
|
break;
|
||||||
|
case EndOfBookMenuActivity::Action::CLOSE_BOOK:
|
||||||
|
pendingGoHome = true;
|
||||||
|
break;
|
||||||
|
case EndOfBookMenuActivity::Action::CLOSE_MENU:
|
||||||
|
currentSpineIndex = epub->getSpineItemsCount() - 1;
|
||||||
|
nextPageNumber = UINT16_MAX;
|
||||||
|
section.reset();
|
||||||
|
endOfBookMenuOpened = false;
|
||||||
|
requestUpdate();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip button processing after returning from subactivity
|
// Skip button processing after returning from subactivity
|
||||||
// This prevents stale button release events from triggering actions
|
// This prevents stale button release events from triggering actions
|
||||||
// We wait until: (1) all relevant buttons are released, AND (2) wasReleased events have been cleared
|
// We wait until: (1) all relevant buttons are released, AND (2) wasReleased events have been cleared
|
||||||
@@ -264,7 +312,7 @@ void EpubReaderActivity::loop() {
|
|||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new EpubReaderMenuActivity(
|
enterNewActivity(new EpubReaderMenuActivity(
|
||||||
this->renderer, this->mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
this->renderer, this->mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
||||||
SETTINGS.orientation, SETTINGS.fontSize, hasDictionary, isBookmarked, epub->getCachePath(),
|
SETTINGS.orientation, SETTINGS.fontSize, hasDictionary, isBookmarked, epub->getCachePath(), epub->getPath(),
|
||||||
[this](const uint8_t orientation, const uint8_t fontSize) { onReaderMenuBack(orientation, fontSize); },
|
[this](const uint8_t orientation, const uint8_t fontSize) { onReaderMenuBack(orientation, fontSize); },
|
||||||
[this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); }));
|
[this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); }));
|
||||||
}
|
}
|
||||||
@@ -299,10 +347,11 @@ void EpubReaderActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// any botton press when at end of the book goes back to the last page
|
// any button press when at end of the book goes back to the last page
|
||||||
if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) {
|
if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) {
|
||||||
currentSpineIndex = epub->getSpineItemsCount() - 1;
|
currentSpineIndex = epub->getSpineItemsCount() - 1;
|
||||||
nextPageNumber = UINT16_MAX;
|
nextPageNumber = UINT16_MAX;
|
||||||
|
endOfBookMenuOpened = false;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -712,6 +761,36 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
|||||||
pendingGoHome = true;
|
pendingGoHome = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case EpubReaderMenuActivity::MenuAction::ARCHIVE_BOOK: {
|
||||||
|
if (epub) {
|
||||||
|
BookManager::archiveBook(epub->getPath());
|
||||||
|
}
|
||||||
|
pendingGoHome = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EpubReaderMenuActivity::MenuAction::DELETE_BOOK: {
|
||||||
|
if (epub) {
|
||||||
|
BookManager::deleteBook(epub->getPath());
|
||||||
|
}
|
||||||
|
pendingGoHome = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EpubReaderMenuActivity::MenuAction::MANAGE_BOOK:
|
||||||
|
break;
|
||||||
|
case EpubReaderMenuActivity::MenuAction::REINDEX_BOOK: {
|
||||||
|
if (epub) {
|
||||||
|
BookManager::reindexBook(epub->getPath(), false);
|
||||||
|
}
|
||||||
|
pendingGoHome = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EpubReaderMenuActivity::MenuAction::REINDEX_BOOK_FULL: {
|
||||||
|
if (epub) {
|
||||||
|
BookManager::reindexBook(epub->getPath(), true);
|
||||||
|
}
|
||||||
|
pendingGoHome = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case EpubReaderMenuActivity::MenuAction::SYNC: {
|
case EpubReaderMenuActivity::MenuAction::SYNC: {
|
||||||
if (KOREADER_STORE.hasCredentials()) {
|
if (KOREADER_STORE.hasCredentials()) {
|
||||||
const int currentPage = section ? section->currentPage : 0;
|
const int currentPage = section ? section->currentPage : 0;
|
||||||
@@ -807,11 +886,11 @@ void EpubReaderActivity::render(Activity::RenderLock&& lock) {
|
|||||||
currentSpineIndex = epub->getSpineItemsCount();
|
currentSpineIndex = epub->getSpineItemsCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show end of book screen
|
// End of book — defer menu creation to loop() to avoid deadlock (render holds the lock)
|
||||||
if (currentSpineIndex == epub->getSpineItemsCount()) {
|
if (currentSpineIndex == epub->getSpineItemsCount()) {
|
||||||
renderer.clearScreen();
|
if (!endOfBookMenuOpened) {
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
|
pendingEndOfBookMenu = true;
|
||||||
renderer.displayBuffer();
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
|
|||||||
volatile bool loadingSection = false; // True during the entire !section block (read from main loop)
|
volatile bool loadingSection = false; // True during the entire !section block (read from main loop)
|
||||||
bool silentIndexingActive = false; // True while silently pre-indexing the next chapter
|
bool silentIndexingActive = false; // True while silently pre-indexing the next chapter
|
||||||
int preIndexedNextSpine = -1; // Spine index already pre-indexed (prevents re-render loop)
|
int preIndexedNextSpine = -1; // Spine index already pre-indexed (prevents re-render loop)
|
||||||
|
bool endOfBookMenuOpened = false; // Guard to prevent repeated opening of EndOfBookMenuActivity
|
||||||
|
bool pendingEndOfBookMenu = false; // Deferred: open EndOfBookMenuActivity from loop(), not render()
|
||||||
const std::function<void()> onGoBack;
|
const std::function<void()> onGoBack;
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
|
|
||||||
|
#include "../home/BookManageMenuActivity.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "util/BookManager.h"
|
||||||
|
|
||||||
void EpubReaderMenuActivity::onEnter() {
|
void EpubReaderMenuActivity::onEnter() {
|
||||||
ActivityWithSubactivity::onEnter();
|
ActivityWithSubactivity::onEnter();
|
||||||
@@ -116,6 +118,42 @@ void EpubReaderMenuActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedAction == MenuAction::MANAGE_BOOK) {
|
||||||
|
const bool isArchived = BookManager::isArchived(bookFilePath);
|
||||||
|
enterNewActivity(new BookManageMenuActivity(
|
||||||
|
renderer, mappedInput, bookFilePath, isArchived,
|
||||||
|
[this](BookManageMenuActivity::Action action) {
|
||||||
|
exitActivity();
|
||||||
|
auto cb = onAction;
|
||||||
|
switch (action) {
|
||||||
|
case BookManageMenuActivity::Action::ARCHIVE:
|
||||||
|
cb(MenuAction::ARCHIVE_BOOK);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::DELETE:
|
||||||
|
cb(MenuAction::DELETE_BOOK);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::DELETE_CACHE:
|
||||||
|
cb(MenuAction::DELETE_CACHE);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::REINDEX:
|
||||||
|
cb(MenuAction::REINDEX_BOOK);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::REINDEX_FULL:
|
||||||
|
cb(MenuAction::REINDEX_BOOK_FULL);
|
||||||
|
break;
|
||||||
|
case BookManageMenuActivity::Action::UNARCHIVE:
|
||||||
|
// Unarchive from within reader is unusual but handle gracefully
|
||||||
|
cb(MenuAction::GO_HOME);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[this] {
|
||||||
|
exitActivity();
|
||||||
|
requestUpdate();
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Capture the callback and action locally
|
// 1. Capture the callback and action locally
|
||||||
auto actionCallback = onAction;
|
auto actionCallback = onAction;
|
||||||
|
|
||||||
|
|||||||
@@ -28,12 +28,18 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
|||||||
GO_HOME,
|
GO_HOME,
|
||||||
SYNC,
|
SYNC,
|
||||||
DELETE_CACHE,
|
DELETE_CACHE,
|
||||||
|
MANAGE_BOOK,
|
||||||
|
ARCHIVE_BOOK,
|
||||||
|
DELETE_BOOK,
|
||||||
|
REINDEX_BOOK,
|
||||||
|
REINDEX_BOOK_FULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
|
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
|
||||||
const int currentPage, const int totalPages, const int bookProgressPercent,
|
const int currentPage, const int totalPages, const int bookProgressPercent,
|
||||||
const uint8_t currentOrientation, const uint8_t currentFontSize,
|
const uint8_t currentOrientation, const uint8_t currentFontSize,
|
||||||
const bool hasDictionary, const bool isBookmarked, const std::string& bookCachePath,
|
const bool hasDictionary, const bool isBookmarked, const std::string& bookCachePath,
|
||||||
|
const std::string& bookFilePath,
|
||||||
const std::function<void(uint8_t, uint8_t)>& onBack,
|
const std::function<void(uint8_t, uint8_t)>& onBack,
|
||||||
const std::function<void(MenuAction)>& onAction)
|
const std::function<void(MenuAction)>& onAction)
|
||||||
: ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput),
|
: ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput),
|
||||||
@@ -42,6 +48,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
|||||||
pendingOrientation(currentOrientation),
|
pendingOrientation(currentOrientation),
|
||||||
pendingFontSize(currentFontSize),
|
pendingFontSize(currentFontSize),
|
||||||
bookCachePath(bookCachePath),
|
bookCachePath(bookCachePath),
|
||||||
|
bookFilePath(bookFilePath),
|
||||||
currentPage(currentPage),
|
currentPage(currentPage),
|
||||||
totalPages(totalPages),
|
totalPages(totalPages),
|
||||||
bookProgressPercent(bookProgressPercent),
|
bookProgressPercent(bookProgressPercent),
|
||||||
@@ -75,6 +82,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
|||||||
StrId::STR_LANDSCAPE_CCW};
|
StrId::STR_LANDSCAPE_CCW};
|
||||||
const std::vector<StrId> fontSizeLabels = {StrId::STR_SMALL, StrId::STR_MEDIUM, StrId::STR_LARGE, StrId::STR_X_LARGE};
|
const std::vector<StrId> fontSizeLabels = {StrId::STR_SMALL, StrId::STR_MEDIUM, StrId::STR_LARGE, StrId::STR_X_LARGE};
|
||||||
std::string bookCachePath;
|
std::string bookCachePath;
|
||||||
|
std::string bookFilePath;
|
||||||
// Letterbox fill override: 0xFF = Default (use global), 0 = Dithered, 1 = Solid, 2 = None
|
// Letterbox fill override: 0xFF = Default (use global), 0 = Dithered, 1 = Solid, 2 = None
|
||||||
uint8_t pendingLetterboxFill = BookSettings::USE_GLOBAL;
|
uint8_t pendingLetterboxFill = BookSettings::USE_GLOBAL;
|
||||||
static constexpr int LETTERBOX_FILL_OPTION_COUNT = 4; // Default + 3 modes
|
static constexpr int LETTERBOX_FILL_OPTION_COUNT = 4; // Default + 3 modes
|
||||||
@@ -132,7 +140,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
|||||||
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
|
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
|
||||||
items.push_back({MenuAction::GO_HOME, StrId::STR_CLOSE_BOOK});
|
items.push_back({MenuAction::GO_HOME, StrId::STR_CLOSE_BOOK});
|
||||||
items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS});
|
items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS});
|
||||||
items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE});
|
items.push_back({MenuAction::MANAGE_BOOK, StrId::STR_MANAGE_BOOK});
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,10 +9,12 @@
|
|||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
|
#include "EndOfBookMenuActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "util/BookManager.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr unsigned long goHomeMs = 1000;
|
constexpr unsigned long goHomeMs = 1000;
|
||||||
@@ -153,10 +155,45 @@ void TxtReaderActivity::loop() {
|
|||||||
|
|
||||||
if (prevTriggered && currentPage > 0) {
|
if (prevTriggered && currentPage > 0) {
|
||||||
currentPage--;
|
currentPage--;
|
||||||
|
endOfBookMenuOpened = false;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
} else if (nextTriggered && currentPage < totalPages - 1) {
|
} else if (nextTriggered && currentPage < totalPages - 1) {
|
||||||
currentPage++;
|
currentPage++;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
|
} else if (nextTriggered && currentPage == totalPages - 1 && !endOfBookMenuOpened) {
|
||||||
|
// At last page and trying to advance → show end of book menu
|
||||||
|
endOfBookMenuOpened = true;
|
||||||
|
const std::string path = txt->getPath();
|
||||||
|
enterNewActivity(new EndOfBookMenuActivity(
|
||||||
|
renderer, mappedInput, path, [this](EndOfBookMenuActivity::Action action) {
|
||||||
|
exitActivity();
|
||||||
|
switch (action) {
|
||||||
|
case EndOfBookMenuActivity::Action::ARCHIVE:
|
||||||
|
if (txt) BookManager::archiveBook(txt->getPath());
|
||||||
|
if (onGoHome) onGoHome();
|
||||||
|
return;
|
||||||
|
case EndOfBookMenuActivity::Action::DELETE:
|
||||||
|
if (txt) BookManager::deleteBook(txt->getPath());
|
||||||
|
if (onGoHome) onGoHome();
|
||||||
|
return;
|
||||||
|
case EndOfBookMenuActivity::Action::TABLE_OF_CONTENTS:
|
||||||
|
endOfBookMenuOpened = false;
|
||||||
|
requestUpdate();
|
||||||
|
break;
|
||||||
|
case EndOfBookMenuActivity::Action::BACK_TO_BEGINNING:
|
||||||
|
currentPage = 0;
|
||||||
|
endOfBookMenuOpened = false;
|
||||||
|
requestUpdate();
|
||||||
|
break;
|
||||||
|
case EndOfBookMenuActivity::Action::CLOSE_BOOK:
|
||||||
|
if (onGoHome) onGoHome();
|
||||||
|
return;
|
||||||
|
case EndOfBookMenuActivity::Action::CLOSE_MENU:
|
||||||
|
endOfBookMenuOpened = false;
|
||||||
|
requestUpdate();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class TxtReaderActivity final : public ActivityWithSubactivity {
|
|||||||
int totalPages = 1;
|
int totalPages = 1;
|
||||||
int pagesUntilFullRefresh = 0;
|
int pagesUntilFullRefresh = 0;
|
||||||
|
|
||||||
|
bool endOfBookMenuOpened = false;
|
||||||
const std::function<void()> onGoBack;
|
const std::function<void()> onGoBack;
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,13 @@
|
|||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
|
#include "EndOfBookMenuActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "XtcReaderChapterSelectionActivity.h"
|
#include "XtcReaderChapterSelectionActivity.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "util/BookManager.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr unsigned long skipPageMs = 700;
|
constexpr unsigned long skipPageMs = 700;
|
||||||
@@ -104,6 +106,60 @@ void XtcReaderActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deferred end-of-book menu (set in render() to avoid deadlock)
|
||||||
|
if (pendingEndOfBookMenu) {
|
||||||
|
pendingEndOfBookMenu = false;
|
||||||
|
endOfBookMenuOpened = true;
|
||||||
|
const std::string path = xtc->getPath();
|
||||||
|
enterNewActivity(new EndOfBookMenuActivity(
|
||||||
|
renderer, mappedInput, path, [this](EndOfBookMenuActivity::Action action) {
|
||||||
|
exitActivity();
|
||||||
|
switch (action) {
|
||||||
|
case EndOfBookMenuActivity::Action::ARCHIVE:
|
||||||
|
if (xtc) BookManager::archiveBook(xtc->getPath());
|
||||||
|
if (onGoHome) onGoHome();
|
||||||
|
break;
|
||||||
|
case EndOfBookMenuActivity::Action::DELETE:
|
||||||
|
if (xtc) BookManager::deleteBook(xtc->getPath());
|
||||||
|
if (onGoHome) onGoHome();
|
||||||
|
break;
|
||||||
|
case EndOfBookMenuActivity::Action::TABLE_OF_CONTENTS:
|
||||||
|
endOfBookMenuOpened = false;
|
||||||
|
currentPage = xtc->getPageCount() - 1;
|
||||||
|
if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) {
|
||||||
|
enterNewActivity(new XtcReaderChapterSelectionActivity(
|
||||||
|
renderer, mappedInput, xtc, currentPage,
|
||||||
|
[this] {
|
||||||
|
exitActivity();
|
||||||
|
requestUpdate();
|
||||||
|
},
|
||||||
|
[this](const uint32_t newPage) {
|
||||||
|
currentPage = newPage;
|
||||||
|
exitActivity();
|
||||||
|
requestUpdate();
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case EndOfBookMenuActivity::Action::BACK_TO_BEGINNING:
|
||||||
|
currentPage = 0;
|
||||||
|
endOfBookMenuOpened = false;
|
||||||
|
requestUpdate();
|
||||||
|
break;
|
||||||
|
case EndOfBookMenuActivity::Action::CLOSE_BOOK:
|
||||||
|
if (onGoHome) onGoHome();
|
||||||
|
break;
|
||||||
|
case EndOfBookMenuActivity::Action::CLOSE_MENU:
|
||||||
|
currentPage = xtc->getPageCount() - 1;
|
||||||
|
endOfBookMenuOpened = false;
|
||||||
|
requestUpdate();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Enter chapter selection activity
|
// Enter chapter selection activity
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) {
|
if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) {
|
||||||
@@ -155,6 +211,7 @@ void XtcReaderActivity::loop() {
|
|||||||
// Handle end of book
|
// Handle end of book
|
||||||
if (currentPage >= xtc->getPageCount()) {
|
if (currentPage >= xtc->getPageCount()) {
|
||||||
currentPage = xtc->getPageCount() - 1;
|
currentPage = xtc->getPageCount() - 1;
|
||||||
|
endOfBookMenuOpened = false;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -183,12 +240,11 @@ void XtcReaderActivity::render(Activity::RenderLock&&) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bounds check
|
// End of book — defer menu creation to loop() to avoid deadlock (render holds the lock)
|
||||||
if (currentPage >= xtc->getPageCount()) {
|
if (currentPage >= xtc->getPageCount()) {
|
||||||
// Show end of book screen
|
if (!endOfBookMenuOpened) {
|
||||||
renderer.clearScreen();
|
pendingEndOfBookMenu = true;
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
|
}
|
||||||
renderer.displayBuffer();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ class XtcReaderActivity final : public ActivityWithSubactivity {
|
|||||||
uint32_t currentPage = 0;
|
uint32_t currentPage = 0;
|
||||||
int pagesUntilFullRefresh = 0;
|
int pagesUntilFullRefresh = 0;
|
||||||
|
|
||||||
|
bool endOfBookMenuOpened = false;
|
||||||
|
bool pendingEndOfBookMenu = false;
|
||||||
const std::function<void()> onGoBack;
|
const std::function<void()> onGoBack;
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
|
#include "RecentBooksStore.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
@@ -94,8 +95,8 @@ void ClearCacheActivity::clearCache() {
|
|||||||
file.getName(name, sizeof(name));
|
file.getName(name, sizeof(name));
|
||||||
String itemName(name);
|
String itemName(name);
|
||||||
|
|
||||||
// Only delete directories starting with epub_ or xtc_
|
if (file.isDirectory() &&
|
||||||
if (file.isDirectory() && (itemName.startsWith("epub_") || itemName.startsWith("xtc_"))) {
|
(itemName.startsWith("epub_") || itemName.startsWith("xtc_") || itemName.startsWith("txt_"))) {
|
||||||
String fullPath = "/.crosspoint/" + itemName;
|
String fullPath = "/.crosspoint/" + itemName;
|
||||||
LOG_DBG("CLEAR_CACHE", "Removing cache: %s", fullPath.c_str());
|
LOG_DBG("CLEAR_CACHE", "Removing cache: %s", fullPath.c_str());
|
||||||
|
|
||||||
@@ -113,6 +114,9 @@ void ClearCacheActivity::clearCache() {
|
|||||||
}
|
}
|
||||||
root.close();
|
root.close();
|
||||||
|
|
||||||
|
// Clear recents since all cached data (covers, progress) is gone
|
||||||
|
RECENT_BOOKS.clear();
|
||||||
|
|
||||||
LOG_DBG("CLEAR_CACHE", "Cache cleared: %d removed, %d failed", clearedCount, failedCount);
|
LOG_DBG("CLEAR_CACHE", "Cache cleared: %d removed, %d failed", clearedCount, failedCount);
|
||||||
|
|
||||||
state = SUCCESS;
|
state = SUCCESS;
|
||||||
|
|||||||
150
src/activities/settings/NtpSyncActivity.cpp
Normal file
150
src/activities/settings/NtpSyncActivity.cpp
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
#include "NtpSyncActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <I18n.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
|
||||||
|
#include "CrossPointSettings.h"
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "activities/network/WifiSelectionActivity.h"
|
||||||
|
#include "components/UITheme.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
#include "util/TimeSync.h"
|
||||||
|
|
||||||
|
static constexpr unsigned long AUTO_DISMISS_MS = 5000;
|
||||||
|
|
||||||
|
void NtpSyncActivity::onWifiSelectionComplete(const bool success) {
|
||||||
|
exitActivity();
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
LOG_ERR("NTP", "WiFi connection failed, exiting");
|
||||||
|
goBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("NTP", "WiFi connected, starting NTP sync");
|
||||||
|
|
||||||
|
{
|
||||||
|
RenderLock lock(*this);
|
||||||
|
state = SYNCING;
|
||||||
|
}
|
||||||
|
requestUpdateAndWait();
|
||||||
|
|
||||||
|
const bool synced = TimeSync::waitForNtpSync(8000);
|
||||||
|
|
||||||
|
{
|
||||||
|
RenderLock lock(*this);
|
||||||
|
state = synced ? SUCCESS : FAILED;
|
||||||
|
if (synced) {
|
||||||
|
successTimestamp = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestUpdate();
|
||||||
|
|
||||||
|
if (synced) {
|
||||||
|
LOG_DBG("NTP", "Time synced successfully");
|
||||||
|
} else {
|
||||||
|
LOG_ERR("NTP", "NTP sync timed out");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NtpSyncActivity::onEnter() {
|
||||||
|
ActivityWithSubactivity::onEnter();
|
||||||
|
|
||||||
|
LOG_DBG("NTP", "Turning on WiFi...");
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
|
||||||
|
LOG_DBG("NTP", "Launching WifiSelectionActivity...");
|
||||||
|
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
||||||
|
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
||||||
|
}
|
||||||
|
|
||||||
|
void NtpSyncActivity::onExit() {
|
||||||
|
ActivityWithSubactivity::onExit();
|
||||||
|
|
||||||
|
TimeSync::stopNtpSync();
|
||||||
|
WiFi.disconnect(false);
|
||||||
|
delay(100);
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
delay(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NtpSyncActivity::render(Activity::RenderLock&&) {
|
||||||
|
if (subActivity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto metrics = UITheme::getInstance().getMetrics();
|
||||||
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
renderer.clearScreen();
|
||||||
|
|
||||||
|
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_SYNC_CLOCK));
|
||||||
|
|
||||||
|
const auto lineHeight = renderer.getLineHeight(UI_10_FONT_ID);
|
||||||
|
const auto centerY = (pageHeight - lineHeight) / 2;
|
||||||
|
|
||||||
|
if (state == SYNCING) {
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_SYNCING_TIME));
|
||||||
|
} else if (state == SUCCESS) {
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_TIME_SYNCED), true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
|
time_t now = time(nullptr);
|
||||||
|
struct tm* t = localtime(&now);
|
||||||
|
if (t != nullptr && t->tm_year > 100) {
|
||||||
|
char timeBuf[32];
|
||||||
|
if (SETTINGS.clockFormat == CrossPointSettings::CLOCK_24H) {
|
||||||
|
snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", t->tm_hour, t->tm_min);
|
||||||
|
} else {
|
||||||
|
int hour12 = t->tm_hour % 12;
|
||||||
|
if (hour12 == 0) hour12 = 12;
|
||||||
|
snprintf(timeBuf, sizeof(timeBuf), "%d:%02d %s", hour12, t->tm_min, t->tm_hour >= 12 ? "PM" : "AM");
|
||||||
|
}
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, centerY + lineHeight + metrics.verticalSpacing, timeBuf);
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsigned long elapsed = millis() - successTimestamp;
|
||||||
|
const int remaining = static_cast<int>((AUTO_DISMISS_MS - elapsed + 999) / 1000);
|
||||||
|
char backLabel[32];
|
||||||
|
snprintf(backLabel, sizeof(backLabel), "%s (%d)", tr(STR_BACK), remaining > 0 ? remaining : 1);
|
||||||
|
const auto labels = mappedInput.mapLabels(backLabel, "", "", "");
|
||||||
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
} else if (state == FAILED) {
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_SYNC_FAILED_MSG), true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||||
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NtpSyncActivity::loop() {
|
||||||
|
if (subActivity) {
|
||||||
|
subActivity->loop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == SUCCESS) {
|
||||||
|
const unsigned long elapsed = millis() - successTimestamp;
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back) || elapsed >= AUTO_DISMISS_MS) {
|
||||||
|
goBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int currentSecond = static_cast<int>(elapsed / 1000);
|
||||||
|
if (currentSecond != lastCountdownSecond) {
|
||||||
|
lastCountdownSecond = currentSecond;
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == FAILED) {
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
|
goBack();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/activities/settings/NtpSyncActivity.h
Normal file
24
src/activities/settings/NtpSyncActivity.h
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
|
||||||
|
class NtpSyncActivity : public ActivityWithSubactivity {
|
||||||
|
enum State { WIFI_SELECTION, SYNCING, SUCCESS, FAILED };
|
||||||
|
|
||||||
|
const std::function<void()> goBack;
|
||||||
|
State state = WIFI_SELECTION;
|
||||||
|
unsigned long successTimestamp = 0;
|
||||||
|
int lastCountdownSecond = -1;
|
||||||
|
|
||||||
|
void onWifiSelectionComplete(bool success);
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit NtpSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
|
const std::function<void()>& goBack)
|
||||||
|
: ActivityWithSubactivity("NtpSync", renderer, mappedInput), goBack(goBack) {}
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
void render(Activity::RenderLock&&) override;
|
||||||
|
bool preventAutoSleep() override { return state == SYNCING; }
|
||||||
|
};
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "KOReaderSettingsActivity.h"
|
#include "KOReaderSettingsActivity.h"
|
||||||
#include "LanguageSelectActivity.h"
|
#include "LanguageSelectActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
|
#include "NtpSyncActivity.h"
|
||||||
#include "OtaUpdateActivity.h"
|
#include "OtaUpdateActivity.h"
|
||||||
#include "SetTimeActivity.h"
|
#include "SetTimeActivity.h"
|
||||||
#include "SetTimezoneOffsetActivity.h"
|
#include "SetTimezoneOffsetActivity.h"
|
||||||
@@ -221,6 +222,9 @@ void SettingsActivity::toggleCurrentSetting() {
|
|||||||
case SettingAction::SetTimezoneOffset:
|
case SettingAction::SetTimezoneOffset:
|
||||||
enterSubActivity(new SetTimezoneOffsetActivity(renderer, mappedInput, onComplete));
|
enterSubActivity(new SetTimezoneOffsetActivity(renderer, mappedInput, onComplete));
|
||||||
break;
|
break;
|
||||||
|
case SettingAction::SyncClock:
|
||||||
|
enterSubActivity(new NtpSyncActivity(renderer, mappedInput, onComplete));
|
||||||
|
break;
|
||||||
case SettingAction::None:
|
case SettingAction::None:
|
||||||
// Do nothing
|
// Do nothing
|
||||||
break;
|
break;
|
||||||
@@ -245,7 +249,8 @@ void SettingsActivity::rebuildClockActions() {
|
|||||||
[](const SettingInfo& s) { return s.type == SettingType::ACTION; }),
|
[](const SettingInfo& s) { return s.type == SettingType::ACTION; }),
|
||||||
clockSettings.end());
|
clockSettings.end());
|
||||||
|
|
||||||
// Always add Set Time
|
// Always add Sync Clock and Set Time
|
||||||
|
clockSettings.push_back(SettingInfo::Action(StrId::STR_SYNC_CLOCK, SettingAction::SyncClock));
|
||||||
clockSettings.push_back(SettingInfo::Action(StrId::STR_SET_TIME, SettingAction::SetTime));
|
clockSettings.push_back(SettingInfo::Action(StrId::STR_SET_TIME, SettingAction::SetTime));
|
||||||
|
|
||||||
// Only add Set UTC Offset when timezone is set to Custom
|
// Only add Set UTC Offset when timezone is set to Custom
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ enum class SettingAction {
|
|||||||
Language,
|
Language,
|
||||||
SetTime,
|
SetTime,
|
||||||
SetTimezoneOffset,
|
SetTimezoneOffset,
|
||||||
|
SyncClock,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SettingInfo {
|
struct SettingInfo {
|
||||||
|
|||||||
14
src/main.cpp
14
src/main.cpp
@@ -224,12 +224,13 @@ void enterDeepSleep() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onGoHome();
|
void onGoHome();
|
||||||
void onGoToMyLibraryWithPath(const std::string& path);
|
void onGoToMyLibraryWithPath(const std::string& path, bool initialSkipRelease = false);
|
||||||
void onGoToRecentBooks();
|
void onGoToRecentBooks();
|
||||||
void onGoToReader(const std::string& initialEpubPath) {
|
void onGoToReader(const std::string& initialEpubPath) {
|
||||||
const std::string bookPath = initialEpubPath; // Copy before exitActivity() invalidates the reference
|
const std::string bookPath = initialEpubPath; // Copy before exitActivity() invalidates the reference
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, bookPath, onGoHome, onGoToMyLibraryWithPath));
|
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, bookPath, onGoHome,
|
||||||
|
[](const std::string& p) { onGoToMyLibraryWithPath(p); }));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onGoToFileTransfer() {
|
void onGoToFileTransfer() {
|
||||||
@@ -252,9 +253,9 @@ void onGoToRecentBooks() {
|
|||||||
enterNewActivity(new RecentBooksActivity(renderer, mappedInputManager, onGoHome, onGoToReader));
|
enterNewActivity(new RecentBooksActivity(renderer, mappedInputManager, onGoHome, onGoToReader));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onGoToMyLibraryWithPath(const std::string& path) {
|
void onGoToMyLibraryWithPath(const std::string& path, bool initialSkipRelease) {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, path));
|
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, path, initialSkipRelease));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onGoToBrowser() {
|
void onGoToBrowser() {
|
||||||
@@ -264,8 +265,9 @@ void onGoToBrowser() {
|
|||||||
|
|
||||||
void onGoHome() {
|
void onGoHome() {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new HomeActivity(renderer, mappedInputManager, onGoToReader, onGoToMyLibrary, onGoToRecentBooks,
|
enterNewActivity(new HomeActivity(renderer, mappedInputManager, onGoToReader, onGoToMyLibrary,
|
||||||
onGoToSettings, onGoToFileTransfer, onGoToBrowser));
|
onGoToMyLibraryWithPath, onGoToRecentBooks, onGoToSettings, onGoToFileTransfer,
|
||||||
|
onGoToBrowser));
|
||||||
}
|
}
|
||||||
|
|
||||||
void setupDisplayAndFonts() {
|
void setupDisplayAndFonts() {
|
||||||
|
|||||||
255
src/util/BookManager.cpp
Normal file
255
src/util/BookManager.cpp
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
#include "BookManager.h"
|
||||||
|
|
||||||
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#include "RecentBooksStore.h"
|
||||||
|
#include "StringUtils.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr char ARCHIVE_ROOT[] = "/.archive";
|
||||||
|
constexpr char CACHE_ROOT[] = "/.crosspoint";
|
||||||
|
|
||||||
|
std::string getCachePrefix(const std::string& bookPath) {
|
||||||
|
if (StringUtils::checkFileExtension(bookPath, ".epub")) return "epub_";
|
||||||
|
if (StringUtils::checkFileExtension(bookPath, ".xtc") || StringUtils::checkFileExtension(bookPath, ".xtch"))
|
||||||
|
return "xtc_";
|
||||||
|
if (StringUtils::checkFileExtension(bookPath, ".txt") || StringUtils::checkFileExtension(bookPath, ".md"))
|
||||||
|
return "txt_";
|
||||||
|
return "epub_";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string computeCachePath(const std::string& bookPath) {
|
||||||
|
const auto prefix = getCachePrefix(bookPath);
|
||||||
|
return std::string(CACHE_ROOT) + "/" + prefix + std::to_string(std::hash<std::string>{}(bookPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure all parent directories of a path exist.
|
||||||
|
void ensureParentDirs(const std::string& path) {
|
||||||
|
for (size_t i = 1; i < path.length(); i++) {
|
||||||
|
if (path[i] == '/') {
|
||||||
|
Storage.mkdir(path.substr(0, i).c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete cover and thumbnail BMP files from a cache directory.
|
||||||
|
void deleteCoverFiles(const std::string& cachePath) {
|
||||||
|
auto dir = Storage.open(cachePath.c_str());
|
||||||
|
if (!dir || !dir.isDirectory()) {
|
||||||
|
if (dir) dir.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dir.rewindDirectory();
|
||||||
|
|
||||||
|
char name[256];
|
||||||
|
for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) {
|
||||||
|
file.getName(name, sizeof(name));
|
||||||
|
const std::string fname(name);
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
const bool isCover = (fname == "cover.bmp" || fname == "cover_crop.bmp");
|
||||||
|
const bool isThumb = (fname.rfind("thumb_", 0) == 0 && StringUtils::checkFileExtension(fname, ".bmp"));
|
||||||
|
|
||||||
|
if (isCover || isThumb) {
|
||||||
|
const std::string fullPath = cachePath + "/" + fname;
|
||||||
|
Storage.remove(fullPath.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dir.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace BookManager {
|
||||||
|
|
||||||
|
std::string getBookCachePath(const std::string& bookPath) { return computeCachePath(bookPath); }
|
||||||
|
|
||||||
|
bool isArchived(const std::string& bookPath) {
|
||||||
|
return bookPath.rfind(std::string(ARCHIVE_ROOT) + "/", 0) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool archiveBook(const std::string& bookPath) {
|
||||||
|
if (isArchived(bookPath)) {
|
||||||
|
LOG_ERR("BKMGR", "Book is already archived: %s", bookPath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string destPath = std::string(ARCHIVE_ROOT) + bookPath;
|
||||||
|
ensureParentDirs(destPath);
|
||||||
|
|
||||||
|
if (!Storage.rename(bookPath.c_str(), destPath.c_str())) {
|
||||||
|
LOG_ERR("BKMGR", "Failed to move book to archive: %s -> %s", bookPath.c_str(), destPath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename cache directory to match the new book path hash
|
||||||
|
const std::string oldCache = computeCachePath(bookPath);
|
||||||
|
const std::string newCache = computeCachePath(destPath);
|
||||||
|
if (oldCache != newCache && Storage.exists(oldCache.c_str())) {
|
||||||
|
if (!Storage.rename(oldCache.c_str(), newCache.c_str())) {
|
||||||
|
LOG_ERR("BKMGR", "Failed to rename cache dir: %s -> %s", oldCache.c_str(), newCache.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RECENT_BOOKS.removeBook(bookPath);
|
||||||
|
LOG_DBG("BKMGR", "Archived: %s -> %s", bookPath.c_str(), destPath.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool unarchiveBook(const std::string& archivePath, std::string* unarchivedPath) {
|
||||||
|
if (!isArchived(archivePath)) {
|
||||||
|
LOG_ERR("BKMGR", "Book is not in archive: %s", archivePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip "/.archive" prefix to get original path
|
||||||
|
std::string destPath = archivePath.substr(strlen(ARCHIVE_ROOT));
|
||||||
|
if (destPath.empty() || destPath[0] != '/') {
|
||||||
|
destPath = "/" + destPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if original parent directory exists, fall back to root
|
||||||
|
const auto lastSlash = destPath.find_last_of('/');
|
||||||
|
std::string parentDir = (lastSlash != std::string::npos && lastSlash > 0) ? destPath.substr(0, lastSlash) : "/";
|
||||||
|
if (!Storage.exists(parentDir.c_str())) {
|
||||||
|
const auto filename = destPath.substr(lastSlash + 1);
|
||||||
|
destPath = "/" + filename;
|
||||||
|
LOG_DBG("BKMGR", "Original dir gone, unarchiving to root: %s", destPath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureParentDirs(destPath);
|
||||||
|
|
||||||
|
if (!Storage.rename(archivePath.c_str(), destPath.c_str())) {
|
||||||
|
LOG_ERR("BKMGR", "Failed to move book from archive: %s -> %s", archivePath.c_str(), destPath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename cache directory
|
||||||
|
const std::string oldCache = computeCachePath(archivePath);
|
||||||
|
const std::string newCache = computeCachePath(destPath);
|
||||||
|
if (oldCache != newCache && Storage.exists(oldCache.c_str())) {
|
||||||
|
if (!Storage.rename(oldCache.c_str(), newCache.c_str())) {
|
||||||
|
LOG_ERR("BKMGR", "Failed to rename cache dir: %s -> %s", oldCache.c_str(), newCache.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RECENT_BOOKS.removeBook(archivePath);
|
||||||
|
LOG_DBG("BKMGR", "Unarchived: %s -> %s", archivePath.c_str(), destPath.c_str());
|
||||||
|
if (unarchivedPath) *unarchivedPath = destPath;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool deleteBook(const std::string& bookPath) {
|
||||||
|
// Delete the book file
|
||||||
|
if (Storage.exists(bookPath.c_str())) {
|
||||||
|
if (!Storage.remove(bookPath.c_str())) {
|
||||||
|
LOG_ERR("BKMGR", "Failed to delete book file: %s", bookPath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete cache directory
|
||||||
|
const std::string cachePath = computeCachePath(bookPath);
|
||||||
|
if (Storage.exists(cachePath.c_str())) {
|
||||||
|
Storage.removeDir(cachePath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
RECENT_BOOKS.removeBook(bookPath);
|
||||||
|
LOG_DBG("BKMGR", "Deleted book: %s", bookPath.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool deleteBookCache(const std::string& bookPath) {
|
||||||
|
const std::string cachePath = computeCachePath(bookPath);
|
||||||
|
if (Storage.exists(cachePath.c_str())) {
|
||||||
|
if (!Storage.removeDir(cachePath.c_str())) {
|
||||||
|
LOG_ERR("BKMGR", "Failed to delete cache: %s", cachePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RECENT_BOOKS.removeBook(bookPath);
|
||||||
|
LOG_DBG("BKMGR", "Deleted cache for: %s", bookPath.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool reindexBook(const std::string& bookPath, bool alsoRegenerateCovers) {
|
||||||
|
const std::string cachePath = computeCachePath(bookPath);
|
||||||
|
if (!Storage.exists(cachePath.c_str())) {
|
||||||
|
LOG_DBG("BKMGR", "No cache to reindex for: %s", bookPath.c_str());
|
||||||
|
RECENT_BOOKS.removeBook(bookPath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto prefix = getCachePrefix(bookPath);
|
||||||
|
|
||||||
|
if (prefix == "epub_") {
|
||||||
|
// Delete sections directory
|
||||||
|
const std::string sectionsPath = cachePath + "/sections";
|
||||||
|
if (Storage.exists(sectionsPath.c_str())) {
|
||||||
|
Storage.removeDir(sectionsPath.c_str());
|
||||||
|
}
|
||||||
|
// Delete book.bin (spine/TOC metadata)
|
||||||
|
const std::string bookBin = cachePath + "/book.bin";
|
||||||
|
if (Storage.exists(bookBin.c_str())) {
|
||||||
|
Storage.remove(bookBin.c_str());
|
||||||
|
}
|
||||||
|
// Delete CSS cache
|
||||||
|
const std::string cssCache = cachePath + "/css_rules.cache";
|
||||||
|
if (Storage.exists(cssCache.c_str())) {
|
||||||
|
Storage.remove(cssCache.c_str());
|
||||||
|
}
|
||||||
|
} else if (prefix == "txt_") {
|
||||||
|
// Delete page index
|
||||||
|
const std::string indexBin = cachePath + "/index.bin";
|
||||||
|
if (Storage.exists(indexBin.c_str())) {
|
||||||
|
Storage.remove(indexBin.c_str());
|
||||||
|
}
|
||||||
|
} else if (prefix == "xtc_") {
|
||||||
|
// XTC is pre-indexed; only covers/thumbs are cached
|
||||||
|
// Nothing to delete for sections
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alsoRegenerateCovers) {
|
||||||
|
deleteCoverFiles(cachePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
RECENT_BOOKS.removeBook(bookPath);
|
||||||
|
LOG_DBG("BKMGR", "Reindexed (covers=%d): %s", alsoRegenerateCovers, bookPath.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cleanupEmptyArchiveDirs(const std::string& bookPath) {
|
||||||
|
if (!isArchived(bookPath)) return;
|
||||||
|
|
||||||
|
// Walk up from the book's parent directory, removing empty dirs
|
||||||
|
std::string dir = bookPath.substr(0, bookPath.find_last_of('/'));
|
||||||
|
const std::string archiveRoot(ARCHIVE_ROOT);
|
||||||
|
|
||||||
|
while (dir.length() > archiveRoot.length()) {
|
||||||
|
auto d = Storage.open(dir.c_str());
|
||||||
|
if (!d || !d.isDirectory()) {
|
||||||
|
if (d) d.close();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
auto child = d.openNextFile();
|
||||||
|
const bool empty = !child;
|
||||||
|
if (child) child.close();
|
||||||
|
d.close();
|
||||||
|
|
||||||
|
if (!empty) break;
|
||||||
|
|
||||||
|
Storage.rmdir(dir.c_str());
|
||||||
|
LOG_DBG("BKMGR", "Removed empty archive dir: %s", dir.c_str());
|
||||||
|
|
||||||
|
auto slash = dir.find_last_of('/');
|
||||||
|
if (slash == std::string::npos || slash == 0) break;
|
||||||
|
dir = dir.substr(0, slash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace BookManager
|
||||||
39
src/util/BookManager.h
Normal file
39
src/util/BookManager.h
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace BookManager {
|
||||||
|
|
||||||
|
// Compute the cache directory path for a book (e.g. "/.crosspoint/epub_12345")
|
||||||
|
std::string getBookCachePath(const std::string& bookPath);
|
||||||
|
|
||||||
|
// Move a book to /.archive/ preserving directory structure.
|
||||||
|
// Renames the cache dir to match the new path hash. Removes from recents.
|
||||||
|
// Returns true on success.
|
||||||
|
bool archiveBook(const std::string& bookPath);
|
||||||
|
|
||||||
|
// Move a book from /.archive/ back to its original location.
|
||||||
|
// Falls back to "/" if the original directory no longer exists.
|
||||||
|
// Renames the cache dir to match the restored path hash. Returns true on success.
|
||||||
|
// If unarchivedPath is non-null, stores the destination path on success.
|
||||||
|
bool unarchiveBook(const std::string& archivePath, std::string* unarchivedPath = nullptr);
|
||||||
|
|
||||||
|
// Delete a book file, its cache directory, and remove from recents.
|
||||||
|
bool deleteBook(const std::string& bookPath);
|
||||||
|
|
||||||
|
// Delete only the cache directory for a book and remove from recents.
|
||||||
|
bool deleteBookCache(const std::string& bookPath);
|
||||||
|
|
||||||
|
// Clear indexed data from cache, preserving progress.
|
||||||
|
// If alsoRegenerateCovers is true, also deletes cover/thumbnail BMPs.
|
||||||
|
// Removes from recents.
|
||||||
|
bool reindexBook(const std::string& bookPath, bool alsoRegenerateCovers);
|
||||||
|
|
||||||
|
// Returns true if the book path is inside the /.archive/ folder.
|
||||||
|
bool isArchived(const std::string& bookPath);
|
||||||
|
|
||||||
|
// Remove empty directories under /.archive/ walking up from the book's parent.
|
||||||
|
// Stops at /.archive itself (never removes it).
|
||||||
|
void cleanupEmptyArchiveDirs(const std::string& bookPath);
|
||||||
|
|
||||||
|
} // namespace BookManager
|
||||||
Reference in New Issue
Block a user