When downloading a book via OPDS, a directory picker now lets the user choose the save location instead of always saving to the SD root. Each OPDS server has a configurable default download path (persisted in opds.json) that the picker opens to. Falls back to "/" if the saved path no longer exists on disk. - Add DirectoryPickerActivity (browse-only directory view with "Save Here") - Add PICKING_DIRECTORY state to OpdsBookBrowserActivity - Add downloadPath field to OpdsServer with JSON serialization - Add Download Path setting to OPDS server edit screen - Extract sortFileList() to StringUtils for shared use - Add i18n strings: STR_SAVE_HERE, STR_SELECT_FOLDER, STR_DOWNLOAD_PATH Made-with: Cursor
167 lines
5.0 KiB
C++
167 lines
5.0 KiB
C++
#include "DirectoryPickerActivity.h"
|
|
|
|
#include <HalStorage.h>
|
|
#include <I18n.h>
|
|
|
|
#include <cstring>
|
|
|
|
#include "MappedInputManager.h"
|
|
#include "components/UITheme.h"
|
|
#include "fontIds.h"
|
|
#include "util/StringUtils.h"
|
|
|
|
void DirectoryPickerActivity::onEnter() {
|
|
Activity::onEnter();
|
|
|
|
basepath = initialPath;
|
|
if (basepath.empty()) basepath = "/";
|
|
|
|
// Validate the initial path exists; fall back to root if not
|
|
auto dir = Storage.open(basepath.c_str());
|
|
if (!dir || !dir.isDirectory()) {
|
|
if (dir) dir.close();
|
|
basepath = "/";
|
|
} else {
|
|
dir.close();
|
|
}
|
|
|
|
selectorIndex = 0;
|
|
loadDirectories();
|
|
requestUpdate();
|
|
}
|
|
|
|
void DirectoryPickerActivity::onExit() {
|
|
directories.clear();
|
|
Activity::onExit();
|
|
}
|
|
|
|
void DirectoryPickerActivity::loadDirectories() {
|
|
directories.clear();
|
|
|
|
auto root = Storage.open(basepath.c_str());
|
|
if (!root || !root.isDirectory()) {
|
|
if (root) root.close();
|
|
return;
|
|
}
|
|
|
|
root.rewindDirectory();
|
|
|
|
char name[256];
|
|
for (auto file = root.openNextFile(); file; file = root.openNextFile()) {
|
|
file.getName(name, sizeof(name));
|
|
if (name[0] == '.' || strcmp(name, "System Volume Information") == 0) {
|
|
file.close();
|
|
continue;
|
|
}
|
|
|
|
if (file.isDirectory()) {
|
|
directories.emplace_back(std::string(name) + "/");
|
|
}
|
|
file.close();
|
|
}
|
|
root.close();
|
|
StringUtils::sortFileList(directories);
|
|
}
|
|
|
|
void DirectoryPickerActivity::loop() {
|
|
// Absorb the Confirm release from the parent activity that launched us
|
|
if (waitForConfirmRelease) {
|
|
if (!mappedInput.isPressed(MappedInputManager::Button::Confirm)) {
|
|
waitForConfirmRelease = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Index 0 = "Save Here", indices 1..N = directory entries
|
|
const int totalItems = 1 + static_cast<int>(directories.size());
|
|
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
if (selectorIndex == 0) {
|
|
onSelect(basepath);
|
|
} else {
|
|
const auto& dirName = directories[selectorIndex - 1];
|
|
// Strip trailing '/'
|
|
std::string folderName = dirName.substr(0, dirName.length() - 1);
|
|
basepath = (basepath.back() == '/' ? basepath : basepath + "/") + folderName;
|
|
selectorIndex = 0;
|
|
loadDirectories();
|
|
requestUpdate();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
|
if (basepath == "/") {
|
|
onCancel();
|
|
} else {
|
|
auto slash = basepath.find_last_of('/');
|
|
basepath = (slash == 0) ? "/" : basepath.substr(0, slash);
|
|
selectorIndex = 0;
|
|
loadDirectories();
|
|
requestUpdate();
|
|
}
|
|
return;
|
|
}
|
|
|
|
buttonNavigator.onNextRelease([this, totalItems] {
|
|
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
|
|
requestUpdate();
|
|
});
|
|
|
|
buttonNavigator.onPreviousRelease([this, totalItems] {
|
|
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
|
|
requestUpdate();
|
|
});
|
|
|
|
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
|
|
|
|
buttonNavigator.onNextContinuous([this, totalItems, pageItems] {
|
|
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems);
|
|
requestUpdate();
|
|
});
|
|
|
|
buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] {
|
|
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems);
|
|
requestUpdate();
|
|
});
|
|
}
|
|
|
|
void DirectoryPickerActivity::render(Activity::RenderLock&&) {
|
|
renderer.clearScreen();
|
|
|
|
const auto& metrics = UITheme::getInstance().getMetrics();
|
|
const auto pageWidth = renderer.getScreenWidth();
|
|
const auto pageHeight = renderer.getScreenHeight();
|
|
|
|
std::string folderName = (basepath == "/") ? tr(STR_SD_CARD) : basepath.substr(basepath.rfind('/') + 1);
|
|
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_SELECT_FOLDER));
|
|
|
|
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
|
|
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing;
|
|
|
|
const int totalItems = 1 + static_cast<int>(directories.size());
|
|
|
|
GUI.drawList(
|
|
renderer, Rect{0, contentTop, pageWidth, contentHeight}, totalItems, selectorIndex,
|
|
[this](int index) -> std::string {
|
|
if (index == 0) {
|
|
std::string label = std::string(tr(STR_SAVE_HERE)) + " (" + basepath + ")";
|
|
return label;
|
|
}
|
|
// Strip trailing '/' for display
|
|
const auto& dir = directories[index - 1];
|
|
return dir.substr(0, dir.length() - 1);
|
|
},
|
|
nullptr,
|
|
[this](int index) -> UIIcon {
|
|
return (index == 0) ? UIIcon::File : UIIcon::Folder;
|
|
});
|
|
|
|
const char* backLabel = (basepath == "/") ? tr(STR_CANCEL) : tr(STR_BACK);
|
|
const char* confirmLabel = (selectorIndex == 0) ? tr(STR_SAVE_HERE) : tr(STR_OPEN);
|
|
const auto labels = mappedInput.mapLabels(backLabel, confirmLabel, tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
|
|
renderer.displayBuffer();
|
|
}
|