Some fixes
This commit is contained in:
@@ -53,7 +53,10 @@ void ReadingSessionTracker::markFinished() {
|
|||||||
if (!active) return;
|
if (!active) return;
|
||||||
const int64_t walltime = HalClock::isSynced() ? static_cast<int64_t>(HalClock::now()) : 0;
|
const int64_t walltime = HalClock::isSynced() ? static_cast<int64_t>(HalClock::now()) : 0;
|
||||||
READING_STATS.markFinished(docId, title, author, static_cast<time_t>(walltime));
|
READING_STATS.markFinished(docId, title, author, static_cast<time_t>(walltime));
|
||||||
READING_STATS.saveToFile();
|
if (!READING_STATS.saveToFile()) {
|
||||||
|
LOG_ERR("RST", "saveToFile failed (markFinished) doc=%s title=%s author=%s wall=%lld", docId.c_str(), title.c_str(),
|
||||||
|
author.c_str(), (long long)walltime);
|
||||||
|
}
|
||||||
LOG_DBG("RST", "Marked finished doc=%s wall=%lld", docId.c_str(), (long long)walltime);
|
LOG_DBG("RST", "Marked finished doc=%s wall=%lld", docId.c_str(), (long long)walltime);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +80,10 @@ void ReadingSessionTracker::end() {
|
|||||||
if (seconds > 0 && !docId.empty()) {
|
if (seconds > 0 && !docId.empty()) {
|
||||||
READING_STATS.recordSession(docId, title, author, seconds, pagesTurnedThisSession, lastKnownProgress,
|
READING_STATS.recordSession(docId, title, author, seconds, pagesTurnedThisSession, lastKnownProgress,
|
||||||
static_cast<time_t>(walltime));
|
static_cast<time_t>(walltime));
|
||||||
READING_STATS.saveToFile();
|
if (!READING_STATS.saveToFile()) {
|
||||||
|
LOG_ERR("RST", "saveToFile failed (session end) doc=%s title=%s author=%s secs=%u pages=%u wall=%lld",
|
||||||
|
docId.c_str(), title.c_str(), author.c_str(), seconds, pagesTurnedThisSession, (long long)walltime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
active = false;
|
active = false;
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ uint16_t ReadingStatsStore::computeCurrentStreak(uint16_t today) const {
|
|||||||
// yesterday. After that the chain is broken.
|
// yesterday. After that the chain is broken.
|
||||||
uint16_t anchor = today;
|
uint16_t anchor = today;
|
||||||
if (getSecondsForDay(anchor) == 0) {
|
if (getSecondsForDay(anchor) == 0) {
|
||||||
if (anchor == 0) return 0;
|
|
||||||
anchor -= 1;
|
anchor -= 1;
|
||||||
if (getSecondsForDay(anchor) == 0) return 0;
|
if (getSecondsForDay(anchor) == 0) return 0;
|
||||||
}
|
}
|
||||||
@@ -170,11 +169,8 @@ void ReadingStatsStore::markFinished(const std::string& docId, const std::string
|
|||||||
}
|
}
|
||||||
|
|
||||||
size_t ReadingStatsStore::getFinishedBookCount() const {
|
size_t ReadingStatsStore::getFinishedBookCount() const {
|
||||||
size_t n = 0;
|
return static_cast<size_t>(
|
||||||
for (const auto& b : books) {
|
std::count_if(books.begin(), books.end(), [](const BookReadingStats& b) { return b.finishedCount > 0; }));
|
||||||
if (b.finishedCount > 0) ++n;
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const BookReadingStats* ReadingStatsStore::findBook(const std::string& docId) const {
|
const BookReadingStats* ReadingStatsStore::findBook(const std::string& docId) const {
|
||||||
|
|||||||
@@ -905,23 +905,27 @@ void TxtReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION a
|
|||||||
case BA::BTN_PAGE_FORWARD:
|
case BA::BTN_PAGE_FORWARD:
|
||||||
if (currentPage < totalPages - 1) {
|
if (currentPage < totalPages - 1) {
|
||||||
currentPage++;
|
currentPage++;
|
||||||
|
globalReadingSessionTracker().onPageTurn();
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case BA::BTN_PAGE_BACK:
|
case BA::BTN_PAGE_BACK:
|
||||||
if (currentPage > 0) {
|
if (currentPage > 0) {
|
||||||
currentPage--;
|
currentPage--;
|
||||||
|
globalReadingSessionTracker().onPageTurn();
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case BA::BTN_PAGE_FORWARD_10:
|
case BA::BTN_PAGE_FORWARD_10:
|
||||||
currentPage += 10;
|
currentPage += 10;
|
||||||
clampPage();
|
clampPage();
|
||||||
|
globalReadingSessionTracker().onPageTurn();
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
break;
|
break;
|
||||||
case BA::BTN_PAGE_BACK_10:
|
case BA::BTN_PAGE_BACK_10:
|
||||||
currentPage -= 10;
|
currentPage -= 10;
|
||||||
clampPage();
|
clampPage();
|
||||||
|
globalReadingSessionTracker().onPageTurn();
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
break;
|
break;
|
||||||
case BA::BTN_STAR_PAGE:
|
case BA::BTN_STAR_PAGE:
|
||||||
|
|||||||
@@ -523,21 +523,25 @@ void XtcReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION a
|
|||||||
case BA::BTN_PAGE_FORWARD:
|
case BA::BTN_PAGE_FORWARD:
|
||||||
if (currentPage + 1 < pageCount) {
|
if (currentPage + 1 < pageCount) {
|
||||||
currentPage++;
|
currentPage++;
|
||||||
|
globalReadingSessionTracker().onPageTurn();
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case BA::BTN_PAGE_BACK:
|
case BA::BTN_PAGE_BACK:
|
||||||
if (currentPage > 0) {
|
if (currentPage > 0) {
|
||||||
currentPage--;
|
currentPage--;
|
||||||
|
globalReadingSessionTracker().onPageTurn();
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case BA::BTN_PAGE_FORWARD_10:
|
case BA::BTN_PAGE_FORWARD_10:
|
||||||
currentPage = (currentPage + 10 < pageCount) ? currentPage + 10 : pageCount - 1;
|
currentPage = (currentPage + 10 < pageCount) ? currentPage + 10 : pageCount - 1;
|
||||||
|
globalReadingSessionTracker().onPageTurn();
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
break;
|
break;
|
||||||
case BA::BTN_PAGE_BACK_10:
|
case BA::BTN_PAGE_BACK_10:
|
||||||
currentPage = (currentPage >= 10) ? currentPage - 10 : 0;
|
currentPage = (currentPage >= 10) ? currentPage - 10 : 0;
|
||||||
|
globalReadingSessionTracker().onPageTurn();
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
break;
|
break;
|
||||||
case BA::BTN_NEXT_SECTION:
|
case BA::BTN_NEXT_SECTION:
|
||||||
@@ -547,6 +551,7 @@ void XtcReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION a
|
|||||||
[this](const auto& ch) { return ch.startPage > currentPage; });
|
[this](const auto& ch) { return ch.startPage > currentPage; });
|
||||||
if (it != chapters.end()) {
|
if (it != chapters.end()) {
|
||||||
currentPage = it->startPage;
|
currentPage = it->startPage;
|
||||||
|
globalReadingSessionTracker().onPageTurn();
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -559,6 +564,7 @@ void XtcReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION a
|
|||||||
|
|
||||||
if (prevChapter != chapters.rend()) {
|
if (prevChapter != chapters.rend()) {
|
||||||
currentPage = prevChapter->startPage;
|
currentPage = prevChapter->startPage;
|
||||||
|
globalReadingSessionTracker().onPageTurn();
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
#include <Arduino.h> // millis()
|
#include <Arduino.h> // millis()
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
|
#include <Utf8.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <iterator>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -178,7 +180,8 @@ void ReadingStatsActivity::render(RenderLock&&) {
|
|||||||
if (!store.getBooks().empty()) {
|
if (!store.getBooks().empty()) {
|
||||||
std::vector<const BookReadingStats*> sorted;
|
std::vector<const BookReadingStats*> sorted;
|
||||||
sorted.reserve(store.getBooks().size());
|
sorted.reserve(store.getBooks().size());
|
||||||
for (const auto& b : store.getBooks()) sorted.push_back(&b);
|
std::transform(store.getBooks().begin(), store.getBooks().end(), std::back_inserter(sorted),
|
||||||
|
[](const BookReadingStats& b) { return &b; });
|
||||||
std::sort(sorted.begin(), sorted.end(),
|
std::sort(sorted.begin(), sorted.end(),
|
||||||
[](const BookReadingStats* a, const BookReadingStats* b) { return a->totalSeconds > b->totalSeconds; });
|
[](const BookReadingStats* a, const BookReadingStats* b) { return a->totalSeconds > b->totalSeconds; });
|
||||||
|
|
||||||
@@ -199,7 +202,7 @@ void ReadingStatsActivity::render(RenderLock&&) {
|
|||||||
if (maxLabelWidth > 0 && renderer.getTextWidth(UI_10_FONT_ID, label.c_str()) > maxLabelWidth) {
|
if (maxLabelWidth > 0 && renderer.getTextWidth(UI_10_FONT_ID, label.c_str()) > maxLabelWidth) {
|
||||||
while (!label.empty() &&
|
while (!label.empty() &&
|
||||||
renderer.getTextWidth(UI_10_FONT_ID, label.c_str()) + ellipsisWidth > maxLabelWidth) {
|
renderer.getTextWidth(UI_10_FONT_ID, label.c_str()) + ellipsisWidth > maxLabelWidth) {
|
||||||
label.pop_back();
|
utf8RemoveLastChar(label);
|
||||||
}
|
}
|
||||||
label += "…";
|
label += "…";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HalClock.h>
|
#include <HalClock.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
|
#include <Utf8.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
@@ -107,9 +108,19 @@ void ReadingStatsBookDetailActivity::render(RenderLock&&) {
|
|||||||
// Header — title (truncated) and author. Title fallback to docId so we
|
// Header — title (truncated) and author. Title fallback to docId so we
|
||||||
// still produce a usable screen if the book's metadata was never recorded.
|
// still produce a usable screen if the book's metadata was never recorded.
|
||||||
std::string headerTitle = (book && !book->title.empty()) ? book->title : docId;
|
std::string headerTitle = (book && !book->title.empty()) ? book->title : docId;
|
||||||
if (headerTitle.size() > 28) {
|
{
|
||||||
headerTitle.resize(28);
|
constexpr size_t kMaxChars = 28;
|
||||||
headerTitle += "…";
|
const auto* p = reinterpret_cast<const unsigned char*>(headerTitle.c_str());
|
||||||
|
const auto* start = p;
|
||||||
|
size_t chars = 0;
|
||||||
|
while (*p != 0 && chars < kMaxChars) {
|
||||||
|
utf8NextCodepoint(&p);
|
||||||
|
++chars;
|
||||||
|
}
|
||||||
|
if (*p != 0) {
|
||||||
|
headerTitle.resize(static_cast<size_t>(p - start));
|
||||||
|
headerTitle += "…";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
GUI.drawHeader(renderer,
|
GUI.drawHeader(renderer,
|
||||||
Rect{contentRect.x, contentRect.y + metrics.topPadding, contentRect.width, metrics.headerHeight},
|
Rect{contentRect.x, contentRect.y + metrics.topPadding, contentRect.width, metrics.headerHeight},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <iterator>
|
||||||
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "ReadingStatsBookDetailActivity.h"
|
#include "ReadingStatsBookDetailActivity.h"
|
||||||
@@ -35,9 +36,9 @@ std::string formatDuration(uint32_t totalSeconds) {
|
|||||||
|
|
||||||
void ReadingStatsBookListActivity::rebuildSortedBooks() {
|
void ReadingStatsBookListActivity::rebuildSortedBooks() {
|
||||||
sortedBooks.clear();
|
sortedBooks.clear();
|
||||||
for (const auto& b : READING_STATS.getBooks()) {
|
sortedBooks.reserve(READING_STATS.getBooks().size());
|
||||||
sortedBooks.push_back(&b);
|
std::transform(READING_STATS.getBooks().begin(), READING_STATS.getBooks().end(), std::back_inserter(sortedBooks),
|
||||||
}
|
[](const BookReadingStats& b) { return &b; });
|
||||||
std::sort(sortedBooks.begin(), sortedBooks.end(),
|
std::sort(sortedBooks.begin(), sortedBooks.end(),
|
||||||
[](const BookReadingStats* a, const BookReadingStats* b) { return a->totalSeconds > b->totalSeconds; });
|
[](const BookReadingStats* a, const BookReadingStats* b) { return a->totalSeconds > b->totalSeconds; });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
CardLayout::CardLayout(GfxRenderer& renderer, Rect contentRect, int startY, CardLayoutConfig cfg)
|
CardLayout::CardLayout(GfxRenderer& renderer, Rect contentRect, int startY, const CardLayoutConfig& cfg)
|
||||||
: renderer_(renderer), contentRect_(contentRect), cfg_(cfg), y_(startY) {
|
: renderer_(renderer), contentRect_(contentRect), cfg_(cfg), y_(startY) {
|
||||||
cardLeft_ = contentRect.x + cfg_.outerMarginX;
|
cardLeft_ = contentRect.x + cfg_.outerMarginX;
|
||||||
cardWidth_ = contentRect.width - cfg_.outerMarginX * 2;
|
cardWidth_ = contentRect.width - cfg_.outerMarginX * 2;
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class CardLayout {
|
|||||||
void centeredMessage(const char* msg);
|
void centeredMessage(const char* msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
CardLayout(GfxRenderer& renderer, Rect contentRect, int startY, CardLayoutConfig cfg = {});
|
CardLayout(GfxRenderer& renderer, Rect contentRect, int startY, const CardLayoutConfig& cfg = {});
|
||||||
|
|
||||||
// Render a single card. `bodyFn` receives a `Body&` and may call its
|
// Render a single card. `bodyFn` receives a `Body&` and may call its
|
||||||
// helpers in any order; the card auto-sizes to whatever the body draws.
|
// helpers in any order; the card auto-sizes to whatever the body draws.
|
||||||
|
|||||||
@@ -108,6 +108,7 @@
|
|||||||
.nav-links {
|
.nav-links {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,7 @@
|
|||||||
.nav-links {
|
.nav-links {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
.nav-links {
|
.nav-links {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
.nav-links a {
|
.nav-links a {
|
||||||
|
|||||||
@@ -310,8 +310,10 @@
|
|||||||
// seconds.
|
// seconds.
|
||||||
function formatEta(seconds) {
|
function formatEta(seconds) {
|
||||||
if (!seconds || seconds <= 0) return "—";
|
if (!seconds || seconds <= 0) return "—";
|
||||||
const h = Math.floor(seconds / 3600);
|
let h = Math.floor(seconds / 3600);
|
||||||
const m = Math.round((seconds % 3600) / 60);
|
let m = Math.round((seconds % 3600) / 60);
|
||||||
|
// Rounding can push m to 60 (e.g. 3570s → 0h 60m); carry into hours.
|
||||||
|
if (m === 60) { h += 1; m = 0; }
|
||||||
if (h > 0) return `${h}h ${String(m).padStart(2, "0")}m`;
|
if (h > 0) return `${h}h ${String(m).padStart(2, "0")}m`;
|
||||||
if (m > 0) return `${m}m`;
|
if (m > 0) return `${m}m`;
|
||||||
return "<1m";
|
return "<1m";
|
||||||
|
|||||||
Reference in New Issue
Block a user