tds3p-rsvp-reader/src/Library.cpp

287 lines
8.8 KiB
C++

#include "Library.h"
#include <algorithm>
#include <SD.h>
#include <vector>
#include "Config.h"
#include "Settings.h"
namespace {
bool isChapterPath(const String& path) {
const String prefix = String(config::kBooksDirectory) + "/";
if (!path.startsWith(prefix) || !path.endsWith(".txt")) return false;
const String relativePath = path.substring(prefix.length());
const int separator = relativePath.indexOf('/');
return separator > 0 && separator == relativePath.lastIndexOf('/');
}
} // namespace
bool Library::loadLastOrFirst(String& path, String& contents) const {
Settings settings;
settings.begin();
const SavedState state = settings.load();
if (isChapterPath(state.bookPath) && isListedChapter(state.bookPath) &&
loadFile(state.bookPath, contents)) {
path = state.bookPath;
return true;
}
return firstBook(path) && loadFile(path, contents);
}
bool Library::load(const String& path, String& contents) const {
return isChapterPath(path) && isListedChapter(path) && loadFile(path, contents);
}
bool Library::loadSidecar(const String& chapterPath,
std::vector<uint8_t>& bytes) const {
bytes.clear();
if (!isChapterPath(chapterPath)) return false;
String sidecarPath = chapterPath;
sidecarPath.remove(sidecarPath.length() - 4);
sidecarPath += ".rsvp";
File file = SD.open(sidecarPath, FILE_READ);
if (!file || file.isDirectory() || file.size() == 0 ||
static_cast<size_t>(file.size()) > config::kMaxBookBytes) {
file.close();
return false;
}
const size_t size = static_cast<size_t>(file.size());
bytes.resize(size);
const size_t read = file.read(bytes.data(), size);
file.close();
if (read != size) {
bytes.clear();
return false;
}
return true;
}
namespace {
String baseName(const String& path) {
const int slash = path.lastIndexOf('/');
return slash >= 0 ? path.substring(slash + 1) : path;
}
String parentPath(const String& path) {
const int slash = path.lastIndexOf('/');
return slash > 0 ? path.substring(0, slash) : String();
}
String childPath(const String& parent, const String& name) {
return name.startsWith("/") ? name : parent + "/" + name;
}
bool isHiddenPath(const String& path) {
const int slash = path.lastIndexOf('/');
return slash + 1 < static_cast<int>(path.length()) &&
path[slash + 1] == '.';
}
bool listDirectories(const String& path, std::vector<String>& directories) {
File directory = SD.open(path);
if (!directory || !directory.isDirectory()) return false;
for (File entry = directory.openNextFile(); entry;
entry = directory.openNextFile()) {
const String entryPath = childPath(path, entry.name());
if (entry.isDirectory() && !isHiddenPath(entryPath)) {
directories.push_back(entryPath);
}
entry.close();
}
directory.close();
std::sort(directories.begin(), directories.end());
return !directories.empty();
}
bool listChapters(const String& path, std::vector<String>& chapters,
std::vector<String>& titles) {
File toc = SD.open(path + "/TOC.txt", FILE_READ);
if (!toc || toc.isDirectory()) return false;
while (toc.available()) {
String title = toc.readStringUntil('\n');
title.replace("\r", "");
title.trim();
if (title.isEmpty() || title.indexOf('/') >= 0 || title.indexOf('\\') >= 0) continue;
const String chapterPath = path + "/" + title + ".txt";
File chapter = SD.open(chapterPath, FILE_READ);
const bool validChapter = chapter && !chapter.isDirectory();
chapter.close();
if (!validChapter) continue;
chapters.push_back(chapterPath);
titles.push_back(title);
}
toc.close();
return !chapters.empty();
}
bool firstListedChapter(const String& path, String& chapterPath) {
File toc = SD.open(path + "/TOC.txt", FILE_READ);
if (!toc || toc.isDirectory()) return false;
while (toc.available()) {
String title = toc.readStringUntil('\n');
title.replace("\r", "");
title.trim();
if (title.isEmpty() || title.indexOf('/') >= 0 || title.indexOf('\\') >= 0) {
continue;
}
const String candidate = path + "/" + title + ".txt";
File chapter = SD.open(candidate, FILE_READ);
const bool validChapter = chapter && !chapter.isDirectory();
chapter.close();
if (validChapter) {
chapterPath = candidate;
toc.close();
return true;
}
}
toc.close();
return false;
}
bool cycle(const std::vector<String>& choices, const String& current,
int direction, String& choice) {
if (choices.empty()) return false;
size_t index = 0;
for (; index < choices.size(); ++index) {
if (choices[index] == current) break;
}
if (index == choices.size()) {
choice = choices.front();
return true;
}
const int count = static_cast<int>(choices.size());
const int offset = direction < 0 ? -1 : 1;
const int next = (static_cast<int>(index) + offset + count) % count;
choice = choices[next];
return true;
}
} // namespace
bool Library::selectBook(const String& currentPath, int direction,
String& chapterPath) const {
if (!ensureBooks()) return false;
String selectedDirectory;
if (!cycle(books_, parentPath(currentPath), direction, selectedDirectory)) return false;
return firstChapter(selectedDirectory, chapterPath);
}
bool Library::selectChapter(const String& currentPath, int direction,
String& chapterPath) const {
const String directoryPath = parentPath(currentPath);
if (directoryPath.isEmpty() || !ensureChapters(directoryPath)) return false;
return cycle(chapters_, currentPath, direction, chapterPath);
}
bool Library::previousChapter(const String& currentPath, String& chapterPath) const {
const String directoryPath = parentPath(currentPath);
if (directoryPath.isEmpty() || !ensureChapters(directoryPath)) return false;
for (size_t index = 1; index < chapters_.size(); ++index) {
if (chapters_[index] == currentPath) {
chapterPath = chapters_[index - 1];
return true;
}
}
return false;
}
bool Library::nextChapter(const String& currentPath, String& chapterPath) const {
const String directoryPath = parentPath(currentPath);
if (directoryPath.isEmpty() || !ensureChapters(directoryPath)) return false;
for (size_t index = 0; index + 1 < chapters_.size(); ++index) {
if (chapters_[index] == currentPath) {
chapterPath = chapters_[index + 1];
return true;
}
}
return false;
}
String Library::bookName(const String& chapterPath) const {
return baseName(parentPath(chapterPath));
}
String Library::chapterName(const String& chapterPath) const {
const String directoryPath = parentPath(chapterPath);
if (chapterDirectory_ == directoryPath && !chapters_.empty()) {
for (size_t index = 0; index < chapters_.size(); ++index) {
if (chapters_[index] == chapterPath) return chapterTitles_[index];
}
}
String name = baseName(chapterPath);
if (name.endsWith(".txt")) name.remove(name.length() - 4);
return name;
}
bool Library::loadFile(const String& path, String& contents) const {
File file = SD.open(path, FILE_READ);
if (!file || file.isDirectory() || file.size() == 0 ||
static_cast<size_t>(file.size()) > config::kMaxBookBytes) {
return false;
}
contents = file.readString();
file.close();
return !contents.isEmpty();
}
bool Library::ensureBooks() const {
if (booksCached_) return !books_.empty();
booksCached_ = true;
books_.clear();
firstChapters_.clear();
if (!listDirectories(config::kBooksDirectory, books_)) return false;
firstChapters_.resize(books_.size());
return true;
}
bool Library::ensureChapters(const String& directoryPath) const {
if (chapterDirectory_ == directoryPath) return !chapters_.empty();
chapterDirectory_ = directoryPath;
chapters_.clear();
chapterTitles_.clear();
return listChapters(chapterDirectory_, chapters_, chapterTitles_);
}
bool Library::isListedChapter(const String& chapterPath) const {
const String directoryPath = parentPath(chapterPath);
if (directoryPath.isEmpty() || !ensureChapters(directoryPath)) return false;
for (const String& path : chapters_) {
if (path == chapterPath) return true;
}
return false;
}
bool Library::firstChapter(const String& directoryPath, String& chapterPath) const {
size_t bookIndex = books_.size();
for (size_t index = 0; index < books_.size(); ++index) {
if (books_[index] == directoryPath) {
bookIndex = index;
break;
}
}
if (bookIndex < firstChapters_.size() && !firstChapters_[bookIndex].isEmpty()) {
chapterPath = firstChapters_[bookIndex];
return true;
}
if (!firstListedChapter(directoryPath, chapterPath)) return false;
if (bookIndex < firstChapters_.size()) firstChapters_[bookIndex] = chapterPath;
return true;
}
bool Library::firstBook(String& path) const {
return ensureBooks() && firstChapter(books_.front(), path);
}