#include "Library.h" #include #include #include #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& 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(file.size()) > config::kMaxBookBytes) { file.close(); return false; } const size_t size = static_cast(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(path.length()) && path[slash + 1] == '.'; } bool listDirectories(const String& path, std::vector& 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& chapters, std::vector& 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& 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(choices.size()); const int offset = direction < 0 ? -1 : 1; const int next = (static_cast(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(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); }