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
105 lines
2.7 KiB
C++
105 lines
2.7 KiB
C++
#include "StringUtils.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
|
|
namespace StringUtils {
|
|
|
|
std::string sanitizeFilename(const std::string& name, size_t maxLength) {
|
|
std::string result;
|
|
result.reserve(name.size());
|
|
|
|
for (char c : name) {
|
|
// Replace invalid filename characters with underscore
|
|
if (c == '/' || c == '\\' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|') {
|
|
result += '_';
|
|
} else if (c >= 32 && c < 127) {
|
|
// Keep printable ASCII characters
|
|
result += c;
|
|
}
|
|
// Skip non-printable characters
|
|
}
|
|
|
|
// Trim leading/trailing spaces and dots
|
|
size_t start = result.find_first_not_of(" .");
|
|
if (start == std::string::npos) {
|
|
return "book"; // Fallback if name is all invalid characters
|
|
}
|
|
size_t end = result.find_last_not_of(" .");
|
|
result = result.substr(start, end - start + 1);
|
|
|
|
// Limit filename length
|
|
if (result.length() > maxLength) {
|
|
result.resize(maxLength);
|
|
}
|
|
|
|
return result.empty() ? "book" : result;
|
|
}
|
|
|
|
bool checkFileExtension(const std::string& fileName, const char* extension) {
|
|
if (fileName.length() < strlen(extension)) {
|
|
return false;
|
|
}
|
|
|
|
const std::string fileExt = fileName.substr(fileName.length() - strlen(extension));
|
|
for (size_t i = 0; i < fileExt.length(); i++) {
|
|
if (tolower(fileExt[i]) != tolower(extension[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool checkFileExtension(const String& fileName, const char* extension) {
|
|
if (fileName.length() < strlen(extension)) {
|
|
return false;
|
|
}
|
|
|
|
String localFile(fileName);
|
|
String localExtension(extension);
|
|
localFile.toLowerCase();
|
|
localExtension.toLowerCase();
|
|
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
|