mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 14:47:37 +03:00
Add paragraph alignment setting (justify/left/center/right) (#191)
## Summary * **What is the goal of this PR?** Add a new user setting for paragraph alignment, instead of hard-coding full justification. * **What changes are included?** One new line in the settings screen, with 4 options (justify/left/center/right). Default is justified since that's what it was already. I personally only wanted to disable justification and use "left", but I included the other options for completeness since they were already supported. ## Additional Context Tested on my X4 and looks as expected for each alignment. Co-authored-by: Maeve Andrews <maeve@git.mail.maeveandrews.com>
This commit is contained in:
parent
00e83af4e8
commit
5e9626eb2a
@ -7,9 +7,9 @@
|
|||||||
#include "parsers/ChapterHtmlSlimParser.h"
|
#include "parsers/ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t SECTION_FILE_VERSION = 8;
|
constexpr uint8_t SECTION_FILE_VERSION = 9;
|
||||||
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint16_t) +
|
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
|
||||||
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint32_t);
|
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint32_t);
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||||
@ -30,19 +30,21 @@ uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||||
const uint16_t viewportWidth, const uint16_t viewportHeight) {
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||||
|
const uint16_t viewportHeight) {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
Serial.printf("[%lu] [SCT] File not open for writing header\n", millis());
|
Serial.printf("[%lu] [SCT] File not open for writing header\n", millis());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
|
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
|
||||||
sizeof(extraParagraphSpacing) + sizeof(viewportWidth) + sizeof(viewportHeight) +
|
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
|
||||||
sizeof(pageCount) + sizeof(uint32_t),
|
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(uint32_t),
|
||||||
"Header size mismatch");
|
"Header size mismatch");
|
||||||
serialization::writePod(file, SECTION_FILE_VERSION);
|
serialization::writePod(file, SECTION_FILE_VERSION);
|
||||||
serialization::writePod(file, fontId);
|
serialization::writePod(file, fontId);
|
||||||
serialization::writePod(file, lineCompression);
|
serialization::writePod(file, lineCompression);
|
||||||
serialization::writePod(file, extraParagraphSpacing);
|
serialization::writePod(file, extraParagraphSpacing);
|
||||||
|
serialization::writePod(file, paragraphAlignment);
|
||||||
serialization::writePod(file, viewportWidth);
|
serialization::writePod(file, viewportWidth);
|
||||||
serialization::writePod(file, viewportHeight);
|
serialization::writePod(file, viewportHeight);
|
||||||
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0 when written)
|
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0 when written)
|
||||||
@ -50,7 +52,8 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||||
const uint16_t viewportWidth, const uint16_t viewportHeight) {
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||||
|
const uint16_t viewportHeight) {
|
||||||
if (!SdMan.openFileForRead("SCT", filePath, file)) {
|
if (!SdMan.openFileForRead("SCT", filePath, file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -70,15 +73,17 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
|||||||
uint16_t fileViewportWidth, fileViewportHeight;
|
uint16_t fileViewportWidth, fileViewportHeight;
|
||||||
float fileLineCompression;
|
float fileLineCompression;
|
||||||
bool fileExtraParagraphSpacing;
|
bool fileExtraParagraphSpacing;
|
||||||
|
uint8_t fileParagraphAlignment;
|
||||||
serialization::readPod(file, fileFontId);
|
serialization::readPod(file, fileFontId);
|
||||||
serialization::readPod(file, fileLineCompression);
|
serialization::readPod(file, fileLineCompression);
|
||||||
serialization::readPod(file, fileExtraParagraphSpacing);
|
serialization::readPod(file, fileExtraParagraphSpacing);
|
||||||
|
serialization::readPod(file, fileParagraphAlignment);
|
||||||
serialization::readPod(file, fileViewportWidth);
|
serialization::readPod(file, fileViewportWidth);
|
||||||
serialization::readPod(file, fileViewportHeight);
|
serialization::readPod(file, fileViewportHeight);
|
||||||
|
|
||||||
if (fontId != fileFontId || lineCompression != fileLineCompression ||
|
if (fontId != fileFontId || lineCompression != fileLineCompression ||
|
||||||
extraParagraphSpacing != fileExtraParagraphSpacing || viewportWidth != fileViewportWidth ||
|
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
|
||||||
viewportHeight != fileViewportHeight) {
|
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight) {
|
||||||
file.close();
|
file.close();
|
||||||
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
|
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
|
||||||
clearCache();
|
clearCache();
|
||||||
@ -109,8 +114,8 @@ bool Section::clearCache() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||||
const uint16_t viewportWidth, const uint16_t viewportHeight,
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||||
const std::function<void()>& progressSetupFn,
|
const uint16_t viewportHeight, const std::function<void()>& progressSetupFn,
|
||||||
const std::function<void(int)>& progressFn) {
|
const std::function<void(int)>& progressFn) {
|
||||||
constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
|
constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
|
||||||
const auto localPath = epub->getSpineItem(spineIndex).href;
|
const auto localPath = epub->getSpineItem(spineIndex).href;
|
||||||
@ -166,11 +171,13 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
if (!SdMan.openFileForWrite("SCT", filePath, file)) {
|
if (!SdMan.openFileForWrite("SCT", filePath, file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, viewportWidth, viewportHeight);
|
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||||
|
viewportHeight);
|
||||||
std::vector<uint32_t> lut = {};
|
std::vector<uint32_t> lut = {};
|
||||||
|
|
||||||
ChapterHtmlSlimParser visitor(
|
ChapterHtmlSlimParser visitor(
|
||||||
tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, viewportWidth, viewportHeight,
|
tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||||
|
viewportHeight,
|
||||||
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
|
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
|
||||||
progressFn);
|
progressFn);
|
||||||
success = visitor.parseAndBuildPages();
|
success = visitor.parseAndBuildPages();
|
||||||
|
|||||||
@ -14,8 +14,8 @@ class Section {
|
|||||||
std::string filePath;
|
std::string filePath;
|
||||||
FsFile file;
|
FsFile file;
|
||||||
|
|
||||||
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint16_t viewportWidth,
|
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||||
uint16_t viewportHeight);
|
uint16_t viewportWidth, uint16_t viewportHeight);
|
||||||
uint32_t onPageComplete(std::unique_ptr<Page> page);
|
uint32_t onPageComplete(std::unique_ptr<Page> page);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@ -28,11 +28,12 @@ class Section {
|
|||||||
renderer(renderer),
|
renderer(renderer),
|
||||||
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
|
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
|
||||||
~Section() = default;
|
~Section() = default;
|
||||||
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint16_t viewportWidth,
|
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||||
uint16_t viewportHeight);
|
uint16_t viewportWidth, uint16_t viewportHeight);
|
||||||
bool clearCache() const;
|
bool clearCache() const;
|
||||||
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint16_t viewportWidth,
|
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||||
uint16_t viewportHeight, const std::function<void()>& progressSetupFn = nullptr,
|
uint16_t viewportWidth, uint16_t viewportHeight,
|
||||||
|
const std::function<void()>& progressSetupFn = nullptr,
|
||||||
const std::function<void(int)>& progressFn = nullptr);
|
const std::function<void(int)>& progressFn = nullptr);
|
||||||
std::unique_ptr<Page> loadPageFromSectionFile();
|
std::unique_ptr<Page> loadPageFromSectionFile();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -97,7 +97,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
if (strcmp(name, "br") == 0) {
|
if (strcmp(name, "br") == 0) {
|
||||||
self->startNewTextBlock(self->currentTextBlock->getStyle());
|
self->startNewTextBlock(self->currentTextBlock->getStyle());
|
||||||
} else {
|
} else {
|
||||||
self->startNewTextBlock(TextBlock::JUSTIFIED);
|
self->startNewTextBlock((TextBlock::Style)self->paragraphAlignment);
|
||||||
}
|
}
|
||||||
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
||||||
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
||||||
@ -221,7 +221,7 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||||
startNewTextBlock(TextBlock::JUSTIFIED);
|
startNewTextBlock((TextBlock::Style)this->paragraphAlignment);
|
||||||
|
|
||||||
const XML_Parser parser = XML_ParserCreate(nullptr);
|
const XML_Parser parser = XML_ParserCreate(nullptr);
|
||||||
int done;
|
int done;
|
||||||
|
|||||||
@ -33,6 +33,7 @@ class ChapterHtmlSlimParser {
|
|||||||
int fontId;
|
int fontId;
|
||||||
float lineCompression;
|
float lineCompression;
|
||||||
bool extraParagraphSpacing;
|
bool extraParagraphSpacing;
|
||||||
|
uint8_t paragraphAlignment;
|
||||||
uint16_t viewportWidth;
|
uint16_t viewportWidth;
|
||||||
uint16_t viewportHeight;
|
uint16_t viewportHeight;
|
||||||
|
|
||||||
@ -46,7 +47,8 @@ class ChapterHtmlSlimParser {
|
|||||||
public:
|
public:
|
||||||
explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId,
|
explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId,
|
||||||
const float lineCompression, const bool extraParagraphSpacing,
|
const float lineCompression, const bool extraParagraphSpacing,
|
||||||
const uint16_t viewportWidth, const uint16_t viewportHeight,
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||||
|
const uint16_t viewportHeight,
|
||||||
const std::function<void(std::unique_ptr<Page>)>& completePageFn,
|
const std::function<void(std::unique_ptr<Page>)>& completePageFn,
|
||||||
const std::function<void(int)>& progressFn = nullptr)
|
const std::function<void(int)>& progressFn = nullptr)
|
||||||
: filepath(filepath),
|
: filepath(filepath),
|
||||||
@ -54,6 +56,7 @@ class ChapterHtmlSlimParser {
|
|||||||
fontId(fontId),
|
fontId(fontId),
|
||||||
lineCompression(lineCompression),
|
lineCompression(lineCompression),
|
||||||
extraParagraphSpacing(extraParagraphSpacing),
|
extraParagraphSpacing(extraParagraphSpacing),
|
||||||
|
paragraphAlignment(paragraphAlignment),
|
||||||
viewportWidth(viewportWidth),
|
viewportWidth(viewportWidth),
|
||||||
viewportHeight(viewportHeight),
|
viewportHeight(viewportHeight),
|
||||||
completePageFn(completePageFn),
|
completePageFn(completePageFn),
|
||||||
|
|||||||
@ -12,7 +12,7 @@ CrossPointSettings CrossPointSettings::instance;
|
|||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
||||||
// Increment this when adding new persisted settings fields
|
// Increment this when adding new persisted settings fields
|
||||||
constexpr uint8_t SETTINGS_COUNT = 10;
|
constexpr uint8_t SETTINGS_COUNT = 11;
|
||||||
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
|
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@ -37,6 +37,7 @@ bool CrossPointSettings::saveToFile() const {
|
|||||||
serialization::writePod(outputFile, fontFamily);
|
serialization::writePod(outputFile, fontFamily);
|
||||||
serialization::writePod(outputFile, fontSize);
|
serialization::writePod(outputFile, fontSize);
|
||||||
serialization::writePod(outputFile, lineSpacing);
|
serialization::writePod(outputFile, lineSpacing);
|
||||||
|
serialization::writePod(outputFile, paragraphAlignment);
|
||||||
outputFile.close();
|
outputFile.close();
|
||||||
|
|
||||||
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
|
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
|
||||||
@ -83,6 +84,8 @@ bool CrossPointSettings::loadFromFile() {
|
|||||||
if (++settingsRead >= fileSettingsCount) break;
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
serialization::readPod(inputFile, lineSpacing);
|
serialization::readPod(inputFile, lineSpacing);
|
||||||
if (++settingsRead >= fileSettingsCount) break;
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
|
serialization::readPod(inputFile, paragraphAlignment);
|
||||||
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
} while (false);
|
} while (false);
|
||||||
|
|
||||||
inputFile.close();
|
inputFile.close();
|
||||||
|
|||||||
@ -43,6 +43,7 @@ class CrossPointSettings {
|
|||||||
// Font size options
|
// Font size options
|
||||||
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3 };
|
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3 };
|
||||||
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2 };
|
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2 };
|
||||||
|
enum PARAGRAPH_ALIGNMENT { JUSTIFIED = 0, LEFT_ALIGN = 1, CENTER_ALIGN = 2, RIGHT_ALIGN = 3 };
|
||||||
|
|
||||||
// Sleep screen settings
|
// Sleep screen settings
|
||||||
uint8_t sleepScreen = DARK;
|
uint8_t sleepScreen = DARK;
|
||||||
@ -62,6 +63,7 @@ class CrossPointSettings {
|
|||||||
uint8_t fontFamily = BOOKERLY;
|
uint8_t fontFamily = BOOKERLY;
|
||||||
uint8_t fontSize = MEDIUM;
|
uint8_t fontSize = MEDIUM;
|
||||||
uint8_t lineSpacing = NORMAL;
|
uint8_t lineSpacing = NORMAL;
|
||||||
|
uint8_t paragraphAlignment = JUSTIFIED;
|
||||||
|
|
||||||
~CrossPointSettings() = default;
|
~CrossPointSettings() = default;
|
||||||
|
|
||||||
|
|||||||
@ -267,7 +267,8 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
|
const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
|
||||||
|
|
||||||
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||||
SETTINGS.extraParagraphSpacing, viewportWidth, viewportHeight)) {
|
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||||
|
viewportHeight)) {
|
||||||
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
|
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
|
||||||
|
|
||||||
// Progress bar dimensions
|
// Progress bar dimensions
|
||||||
@ -311,8 +312,8 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||||
SETTINGS.extraParagraphSpacing, viewportWidth, viewportHeight, progressSetup,
|
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||||
progressCallback)) {
|
viewportHeight, progressSetup, progressCallback)) {
|
||||||
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
|
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
|
||||||
section.reset();
|
section.reset();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
// Define the static settings list
|
// Define the static settings list
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int settingsCount = 11;
|
constexpr int settingsCount = 12;
|
||||||
const SettingInfo settingsList[settingsCount] = {
|
const SettingInfo settingsList[settingsCount] = {
|
||||||
// Should match with SLEEP_SCREEN_MODE
|
// Should match with SLEEP_SCREEN_MODE
|
||||||
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}},
|
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}},
|
||||||
@ -34,6 +34,10 @@ const SettingInfo settingsList[settingsCount] = {
|
|||||||
{"Bookerly", "Noto Sans", "Open Dyslexic"}},
|
{"Bookerly", "Noto Sans", "Open Dyslexic"}},
|
||||||
{"Reader Font Size", SettingType::ENUM, &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}},
|
{"Reader Font Size", SettingType::ENUM, &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}},
|
||||||
{"Reader Line Spacing", SettingType::ENUM, &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}},
|
{"Reader Line Spacing", SettingType::ENUM, &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}},
|
||||||
|
{"Reader Paragraph Alignment",
|
||||||
|
SettingType::ENUM,
|
||||||
|
&CrossPointSettings::paragraphAlignment,
|
||||||
|
{"Justify", "Left", "Center", "Right"}},
|
||||||
{"Check for updates", SettingType::ACTION, nullptr, {}},
|
{"Check for updates", SettingType::ACTION, nullptr, {}},
|
||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user