Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- View a selected book's reading stats from its Library or File Browser action menu.
- Library replaces Recent Books with a searchable book list, and adds various book metadata sort options.
- Reset a book's reader settings from the in-reader Settings tab.
- The OPDS browser asks before downloading a book that is already on the SD card, showing the existing file's size and date. Cancel is selected by default.
- Assign actions to upward and downward slides along either screen edge on touch devices.
- Automatic light sleep is available on the X3/X4 and Sticky and in the X4 Pro light-sleep firmware profiles, reducing idle power while retaining normal button, touch, and frontlight behavior. USB serial stays available while a computer is connected, because the device skips light sleep for as long as a USB host is attached. USB Drive also works in these profiles and keeps the device awake while it is open.
- Add the continuous **IncreMENTAL** EPUB indexing method for background chapter indexing.
Expand Down
1 change: 1 addition & 0 deletions lib/I18n/translations/english.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ STR_RESTARTING_HINT: "Restarting... If device does not restart, hold the power b
STR_NO_ENTRIES: "No entries found"
STR_DOWNLOADING: "Downloading..."
STR_DOWNLOAD_FAILED: "Download failed"
STR_BOOK_EXISTS_OVERWRITE: "Book already on SD card. Overwrite?"
STR_ERROR_MSG: "Error:"
STR_UNNAMED: "Unnamed"
STR_NO_SERVER_URL: "No server URL configured"
Expand Down
100 changes: 90 additions & 10 deletions src/activities/browser/OpdsBookBrowserActivity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "SdCardFontSystem.h"
#include "SilentRestart.h"
#include "activities/network/WifiSelectionActivity.h"
#include "activities/util/ConfirmationActivity.h"
#include "activities/util/KeyboardEntryActivity.h"
#include "components/TouchHeaderBackButton.h"
#include "components/UIScale.h"
Expand All @@ -33,6 +34,7 @@
#include "fontIds.h"
#include "network/HttpDownloader.h"
#include "util/BookCacheUtils.h"
#include "util/DaylightSaving.h"
#include "util/StringUtils.h"
#include "util/UrlUtils.h"

Expand Down Expand Up @@ -63,6 +65,52 @@ std::string buildBookFilenameBase(const OpdsEntry& book, const OpdsFilenameForma
return book.author + " - " + book.title;
}

// SD path a book downloads to: the configured folder (or the root) plus the
// sanitized "<author> - <title>.epub" name.
std::string bookDownloadPath(const OpdsEntry& book, const OpdsFilenameFormat format) {
const char* downloadFolder = SETTINGS.opdsDownloadFolder;
std::string path;
path.reserve(96);
if (downloadFolder[0] != '\0') path += downloadFolder;
path += '/';
path += StringUtils::sanitizeFilename(buildBookFilenameBase(book, format));
path += ".epub";
return path;
}

// "12.3 MB", "456 KB" or "789 B", with integer math only.
void formatFileSize(const uint64_t bytes, char* out, const size_t outSize) {
constexpr uint64_t KB = 1024;
constexpr uint64_t MB = KB * KB;
const uint64_t roundedKb = (bytes + KB / 2) / KB;
if (roundedKb >= KB) {
const uint64_t tenths = (bytes * 10 + MB / 2) / MB;
snprintf(out, outSize, "%lu.%lu MB", static_cast<unsigned long>(tenths / 10),
static_cast<unsigned long>(tenths % 10));
} else if (bytes >= KB) {
snprintf(out, outSize, "%lu KB", static_cast<unsigned long>(roundedKb));
} else {
snprintf(out, outSize, "%lu B", static_cast<unsigned long>(bytes));
}
}

// Packed FAT date/time (HalFile::modificationTime) as "YYYY-MM-DD HH:MM",
// decoded by hand rather than through strftime. False when the file has none.
bool formatFatDateTime(const uint32_t packed, char* out, const size_t outSize) {
if (packed == 0) return false;
const uint16_t date = static_cast<uint16_t>(packed >> 16);
const uint16_t time = static_cast<uint16_t>(packed & 0xFFFF);
const uint16_t year = static_cast<uint16_t>(1980 + (date >> 9));
const uint8_t month = static_cast<uint8_t>((date >> 5) & 0x0F);
const uint8_t day = static_cast<uint8_t>(date & 0x1F);
const unsigned hour = time >> 11;
const unsigned minute = (time >> 5) & 0x3F;
if (!DaylightSaving::isValidDate(year, month, day) || hour > 23 || minute > 59) return false;
snprintf(out, outSize, "%04u-%02u-%02u %02u:%02u", static_cast<unsigned>(year), static_cast<unsigned>(month),
static_cast<unsigned>(day), hour, minute);
return true;
}

// Mayberry prefixes folder titles with U+1F4C1 (file folder), which the UI
// fonts lack; show "/name" instead.
void replaceFolderEmoji(std::string& title) {
Expand Down Expand Up @@ -154,7 +202,7 @@ void OpdsBookBrowserActivity::activateSelected() {
if (!entries || entryCount == 0 || selectorIndex < 0 || selectorIndex >= static_cast<int>(entryCount)) return;
const auto& entry = entries[selectorIndex];
if (entry.type == OpdsEntryType::BOOK) {
downloadBook(entry);
requestDownload(entry);
return;
}
const bool pageLink =
Expand Down Expand Up @@ -752,7 +800,46 @@ void OpdsBookBrowserActivity::navigateBack() {
}
}

void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
void OpdsBookBrowserActivity::requestDownload(const OpdsEntry& book) {
// Read-only open: one directory lookup for existence, size and date, and no
// SD write unless the user chooses to overwrite.
std::string path = bookDownloadPath(book, server.filenameFormat);
HalFile existing = Storage.open(path.c_str());
if (!existing || existing.isDirectory()) {
if (existing) existing.close();
downloadBook(book, path);
return;
}
char sizeLabel[16];
formatFileSize(existing.fileSize64(), sizeLabel, sizeof(sizeLabel));
char dateLabel[20];
const bool hasDate = formatFatDateTime(existing.modificationTime(), dateLabel, sizeof(dateLabel));
existing.close();

// ConfirmationActivity appends "<size>, <date>" to the question in one
// popup title; Cancel is the focused option.
std::string details = sizeLabel;
if (hasDate) {
details += ", ";
details += dateLabel;
}
LOG_INF("OPDS", "Already on SD: %s (%s)", path.c_str(), details.c_str());

auto dialog = makeUniqueNoThrow<ConfirmationActivity>(renderer, mappedInput, tr(STR_BOOK_EXISTS_OVERWRITE), details);
if (!dialog) {
LOG_ERR("OPDS", "Cannot allocate overwrite dialog");
return;
}
// entries and selectorIndex stay put while the dialog is on top (this
// activity's loop does not run), so the index is enough to find the book.
const int bookIndex = selectorIndex;
startActivityForResult(std::move(dialog), [this, bookIndex, path = std::move(path)](const ActivityResult& result) {
if (result.isCancelled || !entries || bookIndex < 0 || bookIndex >= static_cast<int>(entryCount)) return;
downloadBook(entries[bookIndex], path);
});
}

void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book, const std::string& filename) {
state = BrowserState::DOWNLOADING;
statusMessage = book.title;
downloadProgress = downloadTotal = 0;
Expand All @@ -777,21 +864,14 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
// DownloadOptions borrows it to avoid copying the server URL per transfer.
const std::string authorizationOrigin = UrlUtils::ensureProtocol(server.url);
const char* downloadFolder = SETTINGS.opdsDownloadFolder;
bool useDownloadFolder = downloadFolder[0] != '\0';
if (useDownloadFolder && !Storage.exists(downloadFolder) && !Storage.mkdir(downloadFolder)) {
if (downloadFolder[0] != '\0' && !Storage.exists(downloadFolder) && !Storage.mkdir(downloadFolder)) {
LOG_ERR("OPDS", "Could not create download folder %s", downloadFolder);
state = BrowserState::ERROR;
errorMessage = tr(STR_DOWNLOAD_FAILED);
requestUpdate();
return;
}

std::string filename;
filename.reserve(96);
if (useDownloadFolder) filename += downloadFolder;
filename += '/';
filename += StringUtils::sanitizeFilename(buildBookFilenameBase(book, server.filenameFormat));
filename += ".epub";
LOG_DBG("OPDS", "Downloading: %s -> %s", downloadUrl.c_str(), filename.c_str());

bool cancelRequested = false;
Expand Down
6 changes: 5 additions & 1 deletion src/activities/browser/OpdsBookBrowserActivity.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,11 @@ class OpdsBookBrowserActivity final : public Activity {
// listing instead of pushing it onto the Back history.
void navigateToEntry(const OpdsEntry& entry, bool pageLink);
void navigateBack();
void downloadBook(const OpdsEntry& book);
// Asks before replacing a book already on SD (showing its size and date),
// otherwise downloads straight away.
void requestDownload(const OpdsEntry& book);
// filename: the SD destination from requestDownload.
void downloadBook(const OpdsEntry& book, const std::string& filename);
void launchSearch();
void performSearch(const std::string& query);
bool preventAutoSleep() override;
Expand Down
Loading