287 lines
8.8 KiB
C++
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);
|
|
}
|