Merge branch 'feat-localoverride' of https://github.com/jpirnay/crosspoint-reader into mybuild
This commit is contained in:
@@ -303,6 +303,8 @@ bool JsonSettingsIO::saveRecentBooks(const RecentBooksStore& store, const char*
|
||||
obj["author"] = book.author;
|
||||
obj["series"] = book.series;
|
||||
obj["coverBmpPath"] = book.coverBmpPath;
|
||||
obj["embeddedStyleOverride"] = book.embeddedStyleOverride;
|
||||
obj["imageRenderingOverride"] = book.imageRenderingOverride;
|
||||
}
|
||||
|
||||
String json;
|
||||
@@ -320,6 +322,13 @@ bool JsonSettingsIO::loadRecentBooks(RecentBooksStore& store, const char* json)
|
||||
|
||||
store.recentBooks.clear();
|
||||
JsonArray arr = doc["books"].as<JsonArray>();
|
||||
auto clampInt8 = [](int value, int minValue, int maxValue, int8_t fallback) -> int8_t {
|
||||
if (value < minValue || value > maxValue) {
|
||||
return fallback;
|
||||
}
|
||||
return static_cast<int8_t>(value);
|
||||
};
|
||||
|
||||
for (JsonObject obj : arr) {
|
||||
if (store.getCount() >= 10) break;
|
||||
RecentBook book;
|
||||
@@ -328,6 +337,8 @@ bool JsonSettingsIO::loadRecentBooks(RecentBooksStore& store, const char* json)
|
||||
book.author = obj["author"] | std::string("");
|
||||
book.series = obj["series"] | std::string("");
|
||||
book.coverBmpPath = obj["coverBmpPath"] | std::string("");
|
||||
book.embeddedStyleOverride = clampInt8(obj["embeddedStyleOverride"] | -1, -1, 1, -1);
|
||||
book.imageRenderingOverride = clampInt8(obj["imageRenderingOverride"] | -1, -1, 2, -1);
|
||||
store.recentBooks.push_back(book);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,15 +22,23 @@ RecentBooksStore RecentBooksStore::instance;
|
||||
|
||||
void RecentBooksStore::addBook(const std::string& path, const std::string& title, const std::string& author,
|
||||
const std::string& series, const std::string& coverBmpPath) {
|
||||
const std::string& coverBmpPath) {
|
||||
int8_t embeddedStyleOverride = -1;
|
||||
int8_t imageRenderingOverride = -1;
|
||||
|
||||
// Remove existing entry if present
|
||||
auto it =
|
||||
std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; });
|
||||
if (it != recentBooks.end()) {
|
||||
embeddedStyleOverride = it->embeddedStyleOverride;
|
||||
imageRenderingOverride = it->imageRenderingOverride;
|
||||
recentBooks.erase(it);
|
||||
}
|
||||
|
||||
// Add to front
|
||||
recentBooks.insert(recentBooks.begin(), {path, title, author, series, coverBmpPath});
|
||||
recentBooks.insert(recentBooks.begin(),
|
||||
{path, title, author, coverBmpPath, embeddedStyleOverride, imageRenderingOverride});
|
||||
|
||||
// Trim to max size
|
||||
if (recentBooks.size() > MAX_RECENT_BOOKS) {
|
||||
@@ -54,6 +62,28 @@ void RecentBooksStore::updateBook(const std::string& path, const std::string& ti
|
||||
}
|
||||
}
|
||||
|
||||
RecentBook RecentBooksStore::getBookByPath(const std::string& path) const {
|
||||
auto it =
|
||||
std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; });
|
||||
if (it != recentBooks.end()) {
|
||||
return *it;
|
||||
}
|
||||
return RecentBook{};
|
||||
}
|
||||
|
||||
bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t embeddedStyleOverride,
|
||||
const int8_t imageRenderingOverride) {
|
||||
auto it =
|
||||
std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; });
|
||||
if (it == recentBooks.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
it->embeddedStyleOverride = embeddedStyleOverride;
|
||||
it->imageRenderingOverride = imageRenderingOverride;
|
||||
return saveToFile();
|
||||
}
|
||||
|
||||
bool RecentBooksStore::saveToFile() const {
|
||||
Storage.mkdir("/.crosspoint");
|
||||
return JsonSettingsIO::saveRecentBooks(*this, RECENT_BOOKS_FILE_JSON);
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
||||
struct RecentBook {
|
||||
std::string path;
|
||||
std::string title;
|
||||
std::string author;
|
||||
std::string series;
|
||||
std::string coverBmpPath;
|
||||
// -1 = use global setting, otherwise explicit per-book override.
|
||||
int8_t embeddedStyleOverride = -1;
|
||||
// -1 = use global setting, otherwise CrossPointSettings::IMAGE_RENDERING value.
|
||||
int8_t imageRenderingOverride = -1;
|
||||
|
||||
bool operator==(const RecentBook& other) const { return path == other.path; }
|
||||
};
|
||||
@@ -48,6 +54,8 @@ class RecentBooksStore {
|
||||
|
||||
bool loadFromFile();
|
||||
RecentBook getDataFromBook(std::string path) const;
|
||||
RecentBook getBookByPath(const std::string& path) const;
|
||||
bool setReaderOverrides(const std::string& path, int8_t embeddedStyleOverride, int8_t imageRenderingOverride);
|
||||
|
||||
private:
|
||||
bool loadFromBinaryFile();
|
||||
|
||||
@@ -21,6 +21,8 @@ struct MenuResult {
|
||||
int action = -1;
|
||||
uint8_t orientation = 0;
|
||||
uint8_t pageTurnOption = 0;
|
||||
int8_t embeddedStyleOverride = -1;
|
||||
int8_t imageRenderingOverride = -1;
|
||||
};
|
||||
|
||||
struct ChapterResult {
|
||||
|
||||
@@ -86,9 +86,10 @@ void EpubReaderActivity::onEnter() {
|
||||
// Save current epub as last opened epub and add to recent books
|
||||
APP_STATE.openEpubPath = epub->getPath();
|
||||
APP_STATE.saveToFile();
|
||||
std::string series = epub->getSeries();
|
||||
if (!series.empty() && !epub->getSeriesIndex().empty()) series += " #" + epub->getSeriesIndex();
|
||||
RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), series, epub->getThumbBmpPath());
|
||||
RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), epub->getThumbBmpPath());
|
||||
const RecentBook currentBook = RECENT_BOOKS.getBookByPath(epub->getPath());
|
||||
bookEmbeddedStyleOverride = currentBook.embeddedStyleOverride;
|
||||
bookImageRenderingOverride = currentBook.imageRenderingOverride;
|
||||
|
||||
// Trigger first update
|
||||
requestUpdate();
|
||||
@@ -149,18 +150,20 @@ void EpubReaderActivity::loop() {
|
||||
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
||||
}
|
||||
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
||||
startActivityForResult(std::make_unique<EpubReaderMenuActivity>(
|
||||
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
||||
SETTINGS.orientation, !currentPageFootnotes.empty()),
|
||||
[this](const ActivityResult& result) {
|
||||
// Always apply orientation change even if the menu was cancelled
|
||||
const auto& menu = std::get<MenuResult>(result.data);
|
||||
applyOrientation(menu.orientation);
|
||||
toggleAutoPageTurn(menu.pageTurnOption);
|
||||
if (!result.isCancelled) {
|
||||
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
|
||||
}
|
||||
});
|
||||
startActivityForResult(
|
||||
std::make_unique<EpubReaderMenuActivity>(
|
||||
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, SETTINGS.orientation,
|
||||
!currentPageFootnotes.empty(), bookEmbeddedStyleOverride, bookImageRenderingOverride),
|
||||
[this](const ActivityResult& result) {
|
||||
// Always apply orientation change even if the menu was cancelled
|
||||
const auto& menu = std::get<MenuResult>(result.data);
|
||||
applyOrientation(menu.orientation);
|
||||
toggleAutoPageTurn(menu.pageTurnOption);
|
||||
applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride);
|
||||
if (!result.isCancelled) {
|
||||
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Long press BACK (1s+) goes to file selection
|
||||
@@ -460,6 +463,43 @@ void EpubReaderActivity::toggleAutoPageTurn(const uint8_t selectedPageTurnOption
|
||||
}
|
||||
}
|
||||
|
||||
void EpubReaderActivity::applyBookReaderOverrides(const int8_t embeddedStyleOverride,
|
||||
const int8_t imageRenderingOverride) {
|
||||
if (!epub) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bookEmbeddedStyleOverride == embeddedStyleOverride && bookImageRenderingOverride == imageRenderingOverride) {
|
||||
return;
|
||||
}
|
||||
|
||||
bookEmbeddedStyleOverride = embeddedStyleOverride;
|
||||
bookImageRenderingOverride = imageRenderingOverride;
|
||||
RECENT_BOOKS.setReaderOverrides(epub->getPath(), bookEmbeddedStyleOverride, bookImageRenderingOverride);
|
||||
|
||||
RenderLock lock(*this);
|
||||
if (section) {
|
||||
cachedSpineIndex = currentSpineIndex;
|
||||
cachedChapterTotalPageCount = section->pageCount;
|
||||
nextPageNumber = section->currentPage;
|
||||
}
|
||||
section.reset();
|
||||
}
|
||||
|
||||
bool EpubReaderActivity::getEffectiveEmbeddedStyle() const {
|
||||
if (bookEmbeddedStyleOverride >= 0) {
|
||||
return bookEmbeddedStyleOverride != 0;
|
||||
}
|
||||
return SETTINGS.embeddedStyle != 0;
|
||||
}
|
||||
|
||||
uint8_t EpubReaderActivity::getEffectiveImageRendering() const {
|
||||
if (bookImageRenderingOverride >= 0) {
|
||||
return static_cast<uint8_t>(bookImageRenderingOverride);
|
||||
}
|
||||
return SETTINGS.imageRendering;
|
||||
}
|
||||
|
||||
void EpubReaderActivity::pageTurn(bool isForwardTurn) {
|
||||
if (isForwardTurn) {
|
||||
if (section->currentPage < section->pageCount - 1) {
|
||||
@@ -538,22 +578,23 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
|
||||
|
||||
if (!section) {
|
||||
const bool embeddedStyle = getEffectiveEmbeddedStyle();
|
||||
const uint8_t imageRendering = getEffectiveImageRendering();
|
||||
const auto filepath = epub->getSpineItem(currentSpineIndex).href;
|
||||
LOG_DBG("ERS", "Loading file: %s, index: %d", filepath.c_str(), currentSpineIndex);
|
||||
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
|
||||
|
||||
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering)) {
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) {
|
||||
LOG_DBG("ERS", "Cache not found, building...");
|
||||
|
||||
const auto popupFn = [this]() { GUI.drawPopup(renderer, tr(STR_INDEXING)); };
|
||||
|
||||
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering, popupFn)) {
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering,
|
||||
popupFn)) {
|
||||
LOG_ERR("ERS", "Failed to persist page data to SD");
|
||||
section.reset();
|
||||
return;
|
||||
@@ -663,19 +704,20 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW
|
||||
return;
|
||||
}
|
||||
|
||||
const bool embeddedStyle = getEffectiveEmbeddedStyle();
|
||||
const uint8_t imageRendering = getEffectiveImageRendering();
|
||||
|
||||
Section nextSection(epub, nextSpineIndex, renderer);
|
||||
if (nextSection.loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering)) {
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DBG("ERS", "Silently indexing next chapter: %d", nextSpineIndex);
|
||||
if (!nextSection.createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering)) {
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, embeddedStyle, imageRendering)) {
|
||||
LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpineIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ class EpubReaderActivity final : public Activity {
|
||||
bool pendingScreenshot = false;
|
||||
bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit
|
||||
bool automaticPageTurnActive = false;
|
||||
// -1 means use global SETTINGS value.
|
||||
int8_t bookEmbeddedStyleOverride = -1;
|
||||
int8_t bookImageRenderingOverride = -1;
|
||||
|
||||
// Footnote support
|
||||
std::vector<FootnoteEntry> currentPageFootnotes;
|
||||
@@ -48,6 +51,9 @@ class EpubReaderActivity final : public Activity {
|
||||
void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action);
|
||||
void applyOrientation(uint8_t orientation);
|
||||
void toggleAutoPageTurn(uint8_t selectedPageTurnOption);
|
||||
void applyBookReaderOverrides(int8_t embeddedStyleOverride, int8_t imageRenderingOverride);
|
||||
bool getEffectiveEmbeddedStyle() const;
|
||||
uint8_t getEffectiveImageRendering() const;
|
||||
void pageTurn(bool isForwardTurn);
|
||||
|
||||
// Footnote navigation
|
||||
|
||||
@@ -10,11 +10,14 @@
|
||||
EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::string& title, const int currentPage, const int totalPages,
|
||||
const int bookProgressPercent, const uint8_t currentOrientation,
|
||||
const bool hasFootnotes)
|
||||
const bool hasFootnotes, const int8_t initialEmbeddedStyleOverride,
|
||||
const int8_t initialImageRenderingOverride)
|
||||
: Activity("EpubReaderMenu", renderer, mappedInput),
|
||||
menuItems(buildMenuItems(hasFootnotes)),
|
||||
title(title),
|
||||
pendingOrientation(currentOrientation),
|
||||
pendingEmbeddedStyleOverride(initialEmbeddedStyleOverride),
|
||||
pendingImageRenderingOverride(initialImageRenderingOverride),
|
||||
currentPage(currentPage),
|
||||
totalPages(totalPages),
|
||||
bookProgressPercent(bookProgressPercent) {}
|
||||
@@ -26,6 +29,8 @@ std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuI
|
||||
if (hasFootnotes) {
|
||||
items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES});
|
||||
}
|
||||
items.push_back({MenuAction::EMBEDDED_STYLE, StrId::STR_EMBEDDED_STYLE});
|
||||
items.push_back({MenuAction::IMAGE_RENDERING, StrId::STR_IMAGES});
|
||||
items.push_back({MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION});
|
||||
items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN});
|
||||
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
|
||||
@@ -71,13 +76,41 @@ void EpubReaderMenuActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption});
|
||||
if (selectedAction == MenuAction::EMBEDDED_STYLE) {
|
||||
// Cycle per-book override: default -> ON -> OFF -> default.
|
||||
if (pendingEmbeddedStyleOverride < 0) {
|
||||
pendingEmbeddedStyleOverride = 1;
|
||||
} else if (pendingEmbeddedStyleOverride > 0) {
|
||||
pendingEmbeddedStyleOverride = 0;
|
||||
} else {
|
||||
pendingEmbeddedStyleOverride = -1;
|
||||
}
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedAction == MenuAction::IMAGE_RENDERING) {
|
||||
// Cycle per-book override: default -> display -> placeholder -> suppress -> default.
|
||||
if (pendingImageRenderingOverride < 0) {
|
||||
pendingImageRenderingOverride = 0;
|
||||
} else if (pendingImageRenderingOverride >= 2) {
|
||||
pendingImageRenderingOverride = -1;
|
||||
} else {
|
||||
pendingImageRenderingOverride++;
|
||||
}
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption,
|
||||
pendingEmbeddedStyleOverride, pendingImageRenderingOverride});
|
||||
finish();
|
||||
return;
|
||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
result.data = MenuResult{-1, pendingOrientation, selectedPageTurnOption};
|
||||
result.data = MenuResult{-1, pendingOrientation, selectedPageTurnOption, pendingEmbeddedStyleOverride,
|
||||
pendingImageRenderingOverride};
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
return;
|
||||
@@ -134,6 +167,26 @@ void EpubReaderMenuActivity::render(RenderLock&&) {
|
||||
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
|
||||
renderer.drawText(UI_10_FONT_ID, contentRect.x + contentRect.width - 20 - width, displayY, value, !isSelected);
|
||||
}
|
||||
|
||||
if (menuItems[i].action == MenuAction::EMBEDDED_STYLE) {
|
||||
const char* value = tr(STR_DEFAULT_VALUE);
|
||||
if (pendingEmbeddedStyleOverride == 1) {
|
||||
value = tr(STR_STATE_ON);
|
||||
} else if (pendingEmbeddedStyleOverride == 0) {
|
||||
value = tr(STR_STATE_OFF);
|
||||
}
|
||||
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
|
||||
renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected);
|
||||
}
|
||||
|
||||
if (menuItems[i].action == MenuAction::IMAGE_RENDERING) {
|
||||
const char* value = tr(STR_DEFAULT_VALUE);
|
||||
if (pendingImageRenderingOverride >= 0 && pendingImageRenderingOverride < imageRenderingLabels.size()) {
|
||||
value = I18N.get(imageRenderingLabels[pendingImageRenderingOverride]);
|
||||
}
|
||||
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
|
||||
renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected);
|
||||
}
|
||||
}
|
||||
|
||||
// Footer / Hints
|
||||
|
||||
@@ -14,6 +14,8 @@ class EpubReaderMenuActivity final : public Activity {
|
||||
enum class MenuAction {
|
||||
SELECT_CHAPTER,
|
||||
FOOTNOTES,
|
||||
EMBEDDED_STYLE,
|
||||
IMAGE_RENDERING,
|
||||
GO_TO_PERCENT,
|
||||
AUTO_PAGE_TURN,
|
||||
ROTATE_SCREEN,
|
||||
@@ -26,7 +28,9 @@ class EpubReaderMenuActivity final : public Activity {
|
||||
|
||||
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
|
||||
const int currentPage, const int totalPages, const int bookProgressPercent,
|
||||
const uint8_t currentOrientation, const bool hasFootnotes);
|
||||
const uint8_t currentOrientation, const bool hasFootnotes,
|
||||
const int8_t initialEmbeddedStyleOverride,
|
||||
const int8_t initialImageRenderingOverride);
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
@@ -50,8 +54,12 @@ class EpubReaderMenuActivity final : public Activity {
|
||||
std::string title = "Reader Menu";
|
||||
uint8_t pendingOrientation = 0;
|
||||
uint8_t selectedPageTurnOption = 0;
|
||||
int8_t pendingEmbeddedStyleOverride = -1;
|
||||
int8_t pendingImageRenderingOverride = -1;
|
||||
const std::vector<StrId> orientationLabels = {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED,
|
||||
StrId::STR_LANDSCAPE_CCW};
|
||||
const std::vector<StrId> imageRenderingLabels = {StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER,
|
||||
StrId::STR_IMAGES_SUPPRESS};
|
||||
const std::vector<const char*> pageTurnLabels = {I18N.get(StrId::STR_STATE_OFF), "1", "3", "6", "12"};
|
||||
int currentPage = 0;
|
||||
int totalPages = 0;
|
||||
|
||||
Reference in New Issue
Block a user