feat: add directory picker for OPDS downloads with per-server default path

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
This commit is contained in:
cottongin
2026-03-02 04:28:57 -05:00
parent 2aa13ea2de
commit 42011d5977
20 changed files with 363 additions and 72 deletions

View File

@@ -1,5 +1,6 @@
#include "StringUtils.h"
#include <algorithm>
#include <cstring>
namespace StringUtils {
@@ -61,4 +62,43 @@ bool checkFileExtension(const String& fileName, const char* extension) {
return localFile.endsWith(localExtension);
}
void sortFileList(std::vector<std::string>& entries) {
std::sort(begin(entries), end(entries), [](const std::string& str1, const std::string& str2) {
bool isDir1 = str1.back() == '/';
bool isDir2 = str2.back() == '/';
if (isDir1 != isDir2) return isDir1;
const char* s1 = str1.c_str();
const char* s2 = str2.c_str();
while (*s1 && *s2) {
if (isdigit(*s1) && isdigit(*s2)) {
while (*s1 == '0') s1++;
while (*s2 == '0') s2++;
int len1 = 0, len2 = 0;
while (isdigit(s1[len1])) len1++;
while (isdigit(s2[len2])) len2++;
if (len1 != len2) return len1 < len2;
for (int i = 0; i < len1; i++) {
if (s1[i] != s2[i]) return s1[i] < s2[i];
}
s1 += len1;
s2 += len2;
} else {
char c1 = tolower(*s1);
char c2 = tolower(*s2);
if (c1 != c2) return c1 < c2;
s1++;
s2++;
}
}
return *s1 == '\0' && *s2 != '\0';
});
}
} // namespace StringUtils

View File

@@ -3,6 +3,7 @@
#include <WString.h>
#include <string>
#include <vector>
namespace StringUtils {
@@ -19,4 +20,10 @@ std::string sanitizeFilename(const std::string& name, size_t maxLength = 100);
bool checkFileExtension(const std::string& fileName, const char* extension);
bool checkFileExtension(const String& fileName, const char* extension);
/**
* Sort a file/directory list with directories first, using case-insensitive natural sort.
* Directory entries are identified by a trailing '/'.
*/
void sortFileList(std::vector<std::string>& entries);
} // namespace StringUtils