Add opds image download
This commit is contained in:
@@ -197,6 +197,10 @@ void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, cons
|
|||||||
}
|
}
|
||||||
self->currentEntry.acquisitionLinks.push_back(acquisition);
|
self->currentEntry.acquisitionLinks.push_back(acquisition);
|
||||||
}
|
}
|
||||||
|
} else if (rel && type && strstr(rel, "opds-spec.org/image") != nullptr &&
|
||||||
|
strstr(rel, "thumbnail") == nullptr && strstr(type, "image/jpeg") != nullptr &&
|
||||||
|
self->currentEntry.imageHref.empty()) {
|
||||||
|
self->currentEntry.imageHref = href;
|
||||||
} else if (type && strstr(type, "application/atom+xml") != nullptr) {
|
} else if (type && strstr(type, "application/atom+xml") != nullptr) {
|
||||||
if (self->currentEntry.type != OpdsEntryType::BOOK) {
|
if (self->currentEntry.type != OpdsEntryType::BOOK) {
|
||||||
self->currentEntry.type = OpdsEntryType::NAVIGATION;
|
self->currentEntry.type = OpdsEntryType::NAVIGATION;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ struct OpdsEntry {
|
|||||||
std::string href; // Navigation URL or epub download URL
|
std::string href; // Navigation URL or epub download URL
|
||||||
std::string id;
|
std::string id;
|
||||||
std::vector<OpdsAcquisitionLink> acquisitionLinks;
|
std::vector<OpdsAcquisitionLink> acquisitionLinks;
|
||||||
|
std::string imageHref; // Cover image URL (rel="http://opds-spec.org/image"), books only
|
||||||
};
|
};
|
||||||
|
|
||||||
// Legacy alias for backward compatibility
|
// Legacy alias for backward compatibility
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ int ButtonEventManager::buttonToIndex(const Button button) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ButtonEventManager::hasDoubleAction(const Button button) {
|
bool ButtonEventManager::hasDoubleAction(const Button button) const {
|
||||||
|
if (forcedDoubleMask & (1 << static_cast<int>(button))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
using BA = CrossPointSettings::BUTTON_ACTION;
|
using BA = CrossPointSettings::BUTTON_ACTION;
|
||||||
switch (button) {
|
switch (button) {
|
||||||
case Button::Back:
|
case Button::Back:
|
||||||
|
|||||||
@@ -49,6 +49,16 @@ class ButtonEventManager {
|
|||||||
// Reset all per-button FSMs. Call on activity transitions to prevent bleed-through.
|
// Reset all per-button FSMs. Call on activity transitions to prevent bleed-through.
|
||||||
void drain();
|
void drain();
|
||||||
|
|
||||||
|
// Temporarily force double-click detection for a button (adds latency to Short press).
|
||||||
|
// Call this in the Activity's transition setup or loop.
|
||||||
|
void forceDoubleAction(Button button, bool enable = true) {
|
||||||
|
if (enable) {
|
||||||
|
forcedDoubleMask |= (1 << static_cast<int>(button));
|
||||||
|
} else {
|
||||||
|
forcedDoubleMask &= ~(1 << static_cast<int>(button));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Preserve a default event for activity processing after main loop dispatch.
|
// Preserve a default event for activity processing after main loop dispatch.
|
||||||
// This is used when the configured action is BTN_DEFAULT.
|
// This is used when the configured action is BTN_DEFAULT.
|
||||||
void pushEventFront(Button button, PressType type);
|
void pushEventFront(Button button, PressType type);
|
||||||
@@ -59,14 +69,17 @@ class ButtonEventManager {
|
|||||||
|
|
||||||
// Returns true if a double-click action is configured for this button.
|
// Returns true if a double-click action is configured for this button.
|
||||||
// ButtonEventManager queries CrossPointSettings internally.
|
// ButtonEventManager queries CrossPointSettings internally.
|
||||||
static bool hasDoubleAction(Button button);
|
bool hasDoubleAction(Button button) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr int NUM_BUTTONS = 7;
|
static constexpr int NUM_BUTTONS = 9;
|
||||||
static constexpr Button ALL_BUTTONS[NUM_BUTTONS] = {
|
static constexpr Button ALL_BUTTONS[NUM_BUTTONS] = {
|
||||||
Button::Back, Button::Confirm, Button::Left, Button::Right, Button::PageBack, Button::PageForward, Button::Power,
|
Button::Back, Button::Confirm, Button::Left, Button::Right, Button::Up,
|
||||||
|
Button::Down, Button::PageBack, Button::PageForward, Button::Power,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
uint32_t forcedDoubleMask = 0;
|
||||||
|
|
||||||
enum class State { Idle, Pressed, ReleasedOnce, DoublePressed };
|
enum class State { Idle, Pressed, ReleasedOnce, DoublePressed };
|
||||||
|
|
||||||
struct PerButton {
|
struct PerButton {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
#include "ButtonEventManager.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "OpdsFormatLabel.h"
|
#include "OpdsFormatLabel.h"
|
||||||
#include "activities/network/WifiSelectionActivity.h"
|
#include "activities/network/WifiSelectionActivity.h"
|
||||||
@@ -63,6 +64,7 @@ void writeEntryToCache(HalFile& f, const OpdsEntry& entry) {
|
|||||||
writeString(f, entry.author);
|
writeString(f, entry.author);
|
||||||
writeString(f, entry.href);
|
writeString(f, entry.href);
|
||||||
writeString(f, entry.id);
|
writeString(f, entry.id);
|
||||||
|
writeString(f, entry.imageHref);
|
||||||
uint16_t numLinks = entry.acquisitionLinks.size();
|
uint16_t numLinks = entry.acquisitionLinks.size();
|
||||||
f.write(reinterpret_cast<const uint8_t*>(&numLinks), sizeof(numLinks));
|
f.write(reinterpret_cast<const uint8_t*>(&numLinks), sizeof(numLinks));
|
||||||
for (const auto& link : entry.acquisitionLinks) {
|
for (const auto& link : entry.acquisitionLinks) {
|
||||||
@@ -83,6 +85,7 @@ OpdsEntry readEntryFromCache(HalFile& f) {
|
|||||||
entry.author = readString(f);
|
entry.author = readString(f);
|
||||||
entry.href = readString(f);
|
entry.href = readString(f);
|
||||||
entry.id = readString(f);
|
entry.id = readString(f);
|
||||||
|
entry.imageHref = readString(f);
|
||||||
uint16_t numLinks = 0;
|
uint16_t numLinks = 0;
|
||||||
if (f.read(&numLinks, sizeof(numLinks)) == sizeof(numLinks)) {
|
if (f.read(&numLinks, sizeof(numLinks)) == sizeof(numLinks)) {
|
||||||
for (uint16_t i = 0; i < numLinks; ++i) {
|
for (uint16_t i = 0; i < numLinks; ++i) {
|
||||||
@@ -110,6 +113,9 @@ OpdsEntry OpdsBookBrowserActivity::getEntry(size_t index) const {
|
|||||||
void OpdsBookBrowserActivity::onEnter() {
|
void OpdsBookBrowserActivity::onEnter() {
|
||||||
Activity::onEnter();
|
Activity::onEnter();
|
||||||
|
|
||||||
|
globalButtonEvents().forceDoubleAction(ButtonEventManager::Button::Up, true);
|
||||||
|
globalButtonEvents().forceDoubleAction(ButtonEventManager::Button::Down, true);
|
||||||
|
|
||||||
state = BrowserState::CHECK_WIFI;
|
state = BrowserState::CHECK_WIFI;
|
||||||
entryOffsets.clear();
|
entryOffsets.clear();
|
||||||
navigationHistory.clear();
|
navigationHistory.clear();
|
||||||
@@ -131,6 +137,9 @@ void OpdsBookBrowserActivity::onEnter() {
|
|||||||
void OpdsBookBrowserActivity::onExit() {
|
void OpdsBookBrowserActivity::onExit() {
|
||||||
Activity::onExit();
|
Activity::onExit();
|
||||||
|
|
||||||
|
globalButtonEvents().forceDoubleAction(ButtonEventManager::Button::Up, false);
|
||||||
|
globalButtonEvents().forceDoubleAction(ButtonEventManager::Button::Down, false);
|
||||||
|
|
||||||
HalClock::wifiOff();
|
HalClock::wifiOff();
|
||||||
|
|
||||||
entryOffsets.clear();
|
entryOffsets.clear();
|
||||||
@@ -231,17 +240,29 @@ void OpdsBookBrowserActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!entryOffsets.empty()) {
|
if (!entryOffsets.empty()) {
|
||||||
// Navigator is restricted to Up/Down so a Left release used to launch
|
ButtonEventManager::ButtonEvent extEvent;
|
||||||
// search (line above) cannot also be consumed here as a previous-item
|
while (globalButtonEvents().consumeEvent(extEvent)) {
|
||||||
// step on the same tick.
|
if (extEvent.type == ButtonEventManager::PressType::Double) {
|
||||||
buttonNavigator.onRelease({MappedInputManager::Button::Down}, [this] {
|
if (extEvent.button == ButtonEventManager::Button::Down) {
|
||||||
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entryOffsets.size());
|
selectorIndex = (selectorIndex + 9) % entryOffsets.size();
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
});
|
} else if (extEvent.button == ButtonEventManager::Button::Up) {
|
||||||
buttonNavigator.onRelease({MappedInputManager::Button::Up}, [this] {
|
int size = entryOffsets.size();
|
||||||
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entryOffsets.size());
|
selectorIndex = (selectorIndex - 9 + size) % size;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
});
|
}
|
||||||
|
} else if (extEvent.type == ButtonEventManager::PressType::Short ||
|
||||||
|
extEvent.type == ButtonEventManager::PressType::Long) {
|
||||||
|
if (extEvent.button == ButtonEventManager::Button::Down) {
|
||||||
|
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entryOffsets.size());
|
||||||
|
requestUpdate();
|
||||||
|
} else if (extEvent.button == ButtonEventManager::Button::Up) {
|
||||||
|
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entryOffsets.size());
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buttonNavigator.onContinuous({MappedInputManager::Button::Down}, [this] {
|
buttonNavigator.onContinuous({MappedInputManager::Button::Down}, [this] {
|
||||||
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entryOffsets.size(), PAGE_ITEMS);
|
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entryOffsets.size(), PAGE_ITEMS);
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
@@ -537,7 +558,37 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book, const OpdsAcqu
|
|||||||
// Clear any existing cache for this book just in case it's a redownload of
|
// Clear any existing cache for this book just in case it's a redownload of
|
||||||
// a previously opened book.
|
// a previously opened book.
|
||||||
if (acquisition.mimeType == "application/epub+zip") {
|
if (acquisition.mimeType == "application/epub+zip") {
|
||||||
Epub(filename, "/.crosspoint").clearCache();
|
if (!book.imageHref.empty()) {
|
||||||
|
const std::string coverUrl =
|
||||||
|
(book.imageHref.rfind("http", 0) == 0) ? book.imageHref : UrlUtils::buildUrl(server.url, book.imageHref);
|
||||||
|
|
||||||
|
std::string baseFilename = filename;
|
||||||
|
size_t dotPos = baseFilename.find_last_of('.');
|
||||||
|
if (dotPos != std::string::npos) {
|
||||||
|
baseFilename = baseFilename.substr(0, dotPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ext = ".jpg";
|
||||||
|
if (book.imageHref.length() >= 4) {
|
||||||
|
std::string lowerHref = book.imageHref.substr(book.imageHref.length() - 4);
|
||||||
|
std::transform(lowerHref.begin(), lowerHref.end(), lowerHref.begin(), ::tolower);
|
||||||
|
if (lowerHref == ".png") {
|
||||||
|
ext = ".png";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::string sidecarPath = baseFilename + ext;
|
||||||
|
|
||||||
|
const auto coverDlResult = HttpDownloader::downloadToFile(
|
||||||
|
coverUrl, sidecarPath, [this](const size_t, const size_t) { requestUpdate(true); }, server.username,
|
||||||
|
server.password);
|
||||||
|
if (coverDlResult != HttpDownloader::OK) {
|
||||||
|
LOG_ERR("OPDS", "Failed to download cover from %s (err %d)", coverUrl.c_str(), (int)coverDlResult);
|
||||||
|
Storage.remove(sidecarPath.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Epub epub(filename, "/.crosspoint");
|
||||||
|
epub.clearCache();
|
||||||
} else if (acquisition.formatKey == "xtc" || acquisition.formatKey == "xtch") {
|
} else if (acquisition.formatKey == "xtc" || acquisition.formatKey == "xtch") {
|
||||||
Xtc(filename, "/.crosspoint").clearCache();
|
Xtc(filename, "/.crosspoint").clearCache();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -328,16 +328,16 @@ void EpubReaderActivity::loop() {
|
|||||||
|
|
||||||
if (ev.type == ButtonEventManager::PressType::Short) {
|
if (ev.type == ButtonEventManager::PressType::Short) {
|
||||||
if ((ev.button == MappedInputManager::Button::PageBack && SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
|
if ((ev.button == MappedInputManager::Button::PageBack && SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
|
||||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack)) ||
|
globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageBack)) ||
|
||||||
(ev.button == MappedInputManager::Button::Left && SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
|
(ev.button == MappedInputManager::Button::Left && SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
|
||||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left))) {
|
globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Left))) {
|
||||||
delayedPrevTurn = true;
|
delayedPrevTurn = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ((ev.button == MappedInputManager::Button::PageForward && SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
|
if ((ev.button == MappedInputManager::Button::PageForward && SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
|
||||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward)) ||
|
globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageForward)) ||
|
||||||
(ev.button == MappedInputManager::Button::Right && SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
|
(ev.button == MappedInputManager::Button::Right && SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
|
||||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right))) {
|
globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Right))) {
|
||||||
delayedNextTurn = true;
|
delayedNextTurn = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,16 +95,16 @@ inline PageTurnResult detectPageTurn(const MappedInputManager& input) {
|
|||||||
// because the button event system delays short events until the double-click window expires.
|
// because the button event system delays short events until the double-click window expires.
|
||||||
using BA = CrossPointSettings::BUTTON_ACTION;
|
using BA = CrossPointSettings::BUTTON_ACTION;
|
||||||
const bool prev = (SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
|
const bool prev = (SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
|
||||||
!ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack) &&
|
!globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageBack) &&
|
||||||
input.wasReleased(MappedInputManager::Button::PageBack)) ||
|
input.wasReleased(MappedInputManager::Button::PageBack)) ||
|
||||||
(SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
|
(SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
|
||||||
!ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left) &&
|
!globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Left) &&
|
||||||
input.wasReleased(MappedInputManager::Button::Left));
|
input.wasReleased(MappedInputManager::Button::Left));
|
||||||
const bool next = (SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
|
const bool next = (SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
|
||||||
!ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward) &&
|
!globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageForward) &&
|
||||||
input.wasReleased(MappedInputManager::Button::PageForward)) ||
|
input.wasReleased(MappedInputManager::Button::PageForward)) ||
|
||||||
(SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
|
(SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
|
||||||
!ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right) &&
|
!globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Right) &&
|
||||||
input.wasReleased(MappedInputManager::Button::Right));
|
input.wasReleased(MappedInputManager::Button::Right));
|
||||||
return {prev, next};
|
return {prev, next};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,16 +102,16 @@ void XtcReaderActivity::loop() {
|
|||||||
|
|
||||||
if (ev.type == ButtonEventManager::PressType::Short) {
|
if (ev.type == ButtonEventManager::PressType::Short) {
|
||||||
if ((ev.button == MappedInputManager::Button::PageBack && SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
|
if ((ev.button == MappedInputManager::Button::PageBack && SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
|
||||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack)) ||
|
globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageBack)) ||
|
||||||
(ev.button == MappedInputManager::Button::Left && SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
|
(ev.button == MappedInputManager::Button::Left && SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
|
||||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left))) {
|
globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Left))) {
|
||||||
delayedPrevTurn = true;
|
delayedPrevTurn = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ((ev.button == MappedInputManager::Button::PageForward && SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
|
if ((ev.button == MappedInputManager::Button::PageForward && SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
|
||||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward)) ||
|
globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageForward)) ||
|
||||||
(ev.button == MappedInputManager::Button::Right && SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
|
(ev.button == MappedInputManager::Button::Right && SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
|
||||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right))) {
|
globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Right))) {
|
||||||
delayedNextTurn = true;
|
delayedNextTurn = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user