diff --git a/apps/reader/Makefile b/apps/reader/Makefile new file mode 100644 index 000000000..71adec9e0 --- /dev/null +++ b/apps/reader/Makefile @@ -0,0 +1,18 @@ +apps += reader::App +app_headers += apps/reader/app.h + +app_sreader_src = $(addprefix apps/reader/,\ + app.cpp \ + list_book_controller.cpp \ + utility.cpp \ + read_book_controller \ + word_wrap_view.cpp \ +) + +apps_src += $(app_sreader_src) + +app_images += apps/reader/reader_icon.png + +i18n_files += $(call i18n_without_universal_for,reader/base) + +$(eval $(call depends_on_image,apps/reader/app.cpp,apps/reader/reader_icon.png)) \ No newline at end of file diff --git a/apps/reader/app.cpp b/apps/reader/app.cpp new file mode 100644 index 000000000..18ae33da0 --- /dev/null +++ b/apps/reader/app.cpp @@ -0,0 +1,39 @@ +#include "app.h" +#include "reader_icon.h" +#include "apps/apps_container.h" +#include "apps/i18n.h" + + +namespace reader { + +I18n::Message App::Descriptor::name() { + return I18n::Message::ReaderApp; +} + +I18n::Message App::Descriptor::upperName() { + return I18n::Message::ReaderAppCapital; +} + +const Image * App::Descriptor::icon() { + return ImageStore::ReaderIcon; +} + + +App * App::Snapshot::unpack(Container * container) { + return new (container->currentAppBuffer()) App(this); +} + +App::Descriptor * App::Snapshot::descriptor() { + static Descriptor descriptor; + return &descriptor; +} + + +App::App(Snapshot * snapshot) : + ::App(snapshot, &m_stackViewController), + m_listBookController(&m_stackViewController), + m_stackViewController(nullptr, &m_listBookController) +{ +} + +} diff --git a/apps/reader/app.h b/apps/reader/app.h new file mode 100644 index 000000000..1204862c4 --- /dev/null +++ b/apps/reader/app.h @@ -0,0 +1,30 @@ +#ifndef READER_H +#define READER_H + +#include +#include "list_book_controller.h" + +namespace reader { + +class App : public ::App { +public: + class Descriptor : public ::App::Descriptor { + public: + I18n::Message name() override; + I18n::Message upperName() override; + const Image * icon() override; + }; + class Snapshot : public ::App::Snapshot { + public: + App * unpack(Container * container) override; + Descriptor * descriptor() override; + }; +private: + App(Snapshot * snapshot); + ListBookController m_listBookController; + StackViewController m_stackViewController; +}; + +} + +#endif \ No newline at end of file diff --git a/apps/reader/base.de.i18n b/apps/reader/base.de.i18n new file mode 100644 index 000000000..68a64986d --- /dev/null +++ b/apps/reader/base.de.i18n @@ -0,0 +1,3 @@ +ReaderApp = "Reader" +ReaderAppCapital = "READER" +NoFileToDisplay = "Keine Dateien zum Anzeigen!" \ No newline at end of file diff --git a/apps/reader/base.en.i18n b/apps/reader/base.en.i18n new file mode 100644 index 000000000..0101ebe2a --- /dev/null +++ b/apps/reader/base.en.i18n @@ -0,0 +1,3 @@ +ReaderApp = "Reader" +ReaderAppCapital = "READER" +NoFileToDisplay = "No file to display!" \ No newline at end of file diff --git a/apps/reader/base.es.i18n b/apps/reader/base.es.i18n new file mode 100644 index 000000000..51be1eaf4 --- /dev/null +++ b/apps/reader/base.es.i18n @@ -0,0 +1,3 @@ +ReaderApp = "Reader" +ReaderAppCapital = "READER" +NoFileToDisplay ="No hay archivos para mostrar!" \ No newline at end of file diff --git a/apps/reader/base.fr.i18n b/apps/reader/base.fr.i18n new file mode 100644 index 000000000..220c36fa1 --- /dev/null +++ b/apps/reader/base.fr.i18n @@ -0,0 +1,3 @@ +ReaderApp = "Reader" +ReaderAppCapital = "READER" +NoFileToDisplay = "Aucun fichier à afficher!" \ No newline at end of file diff --git a/apps/reader/base.hu.i18n b/apps/reader/base.hu.i18n new file mode 100644 index 000000000..264d6e878 --- /dev/null +++ b/apps/reader/base.hu.i18n @@ -0,0 +1,3 @@ +ReaderApp = "Reader" +ReaderAppCapital = "READER" +NoFileToDisplay ="Nincs megjeleníthető fájl" \ No newline at end of file diff --git a/apps/reader/base.it.i18n b/apps/reader/base.it.i18n new file mode 100644 index 000000000..4464ff077 --- /dev/null +++ b/apps/reader/base.it.i18n @@ -0,0 +1,3 @@ +ReaderApp = "Reader" +ReaderAppCapital = "READER" +NoFileToDisplay ="essun file da visualizzare" \ No newline at end of file diff --git a/apps/reader/base.nl.i18n b/apps/reader/base.nl.i18n new file mode 100644 index 000000000..55693de3e --- /dev/null +++ b/apps/reader/base.nl.i18n @@ -0,0 +1,3 @@ +ReaderApp = "Reader" +ReaderAppCapital = "READER" +NoFileToDisplay ="Geen bestanden om weer te geven" \ No newline at end of file diff --git a/apps/reader/base.pt.i18n b/apps/reader/base.pt.i18n new file mode 100644 index 000000000..886d4930c --- /dev/null +++ b/apps/reader/base.pt.i18n @@ -0,0 +1,3 @@ +ReaderApp = "Reader" +ReaderAppCapital = "READER" +NoFileToDisplay ="Nenhum arquivo para exibir" \ No newline at end of file diff --git a/apps/reader/list_book_controller.cpp b/apps/reader/list_book_controller.cpp new file mode 100644 index 000000000..d2635129c --- /dev/null +++ b/apps/reader/list_book_controller.cpp @@ -0,0 +1,102 @@ +#include "list_book_controller.h" +#include "utility.h" +#include "apps/i18n.h" + +namespace reader +{ + +View* ListBookController::view() +{ + return &m_tableView; +} + +ListBookController::ListBookController(Responder * parentResponder): + ViewController(parentResponder), + m_tableView(this, this, this), + m_readBookController(this) +{ + m_nbFiles = filesWithExtension(".txt", m_files, NB_FILES); + cleanRemovedBookRecord(); +} + +int ListBookController::numberOfRows() const +{ + return m_nbFiles; +} + +KDCoordinate ListBookController::cellHeight() +{ + return 50; +} + +HighlightCell * ListBookController::reusableCell(int index) +{ + return &m_cells[index]; +} + +int ListBookController::reusableCellCount() const +{ + return NB_CELLS; +} + +void ListBookController::willDisplayCellForIndex(HighlightCell * cell, int index) +{ + MessageTableCell* myTextCell = static_cast(cell); + MessageTextView* textView = static_cast(myTextCell->labelView()); + textView->setText(m_files[index].name); + myTextCell->setMessageFont(KDFont::LargeFont); +} + +void ListBookController::didBecomeFirstResponder() +{ + if (selectedRow() < 0) { + selectCellAtLocation(0, 0); + } + Container::activeApp()->setFirstResponder(&m_tableView); + if(m_nbFiles == 0) + { + Container::activeApp()->displayWarning(I18n::Message::NoFileToDisplay); + } +} + +bool ListBookController::handleEvent(Ion::Events::Event event) +{ + if (event == Ion::Events::OK || event == Ion::Events::EXE || event == Ion::Events::Right) + { + + m_readBookController.setBook(m_files[selectedRow()]); + static_cast(parentResponder())->push(&m_readBookController); + Container::activeApp()->setFirstResponder(&m_readBookController); + return true; + } + + return false; +} + + +bool ListBookController::hasBook(const char* filename) const +{ + for(int i=0;inumberOfRecordsWithExtension("txt"); + for(int i=0; irecordWithExtensionAtIndex("txt", i); + if(!hasBook(r.fullName())) + { + r.destroy(); + } + } +} + +} \ No newline at end of file diff --git a/apps/reader/list_book_controller.h b/apps/reader/list_book_controller.h new file mode 100644 index 000000000..82eecf207 --- /dev/null +++ b/apps/reader/list_book_controller.h @@ -0,0 +1,42 @@ +#ifndef __LIST_BOOK_CONTROLLER_H__ +#define __LIST_BOOK_CONTROLLER_H__ + +#include +#include + +#include "read_book_controller.h" + +namespace reader +{ + +class ListBookController : public ViewController, public SimpleListViewDataSource, public SelectableTableViewDataSource +{ +public: + ListBookController(Responder * parentResponder); + View* view() override; + + int numberOfRows() const override; + KDCoordinate cellHeight() override; + HighlightCell * reusableCell(int index) override; + int reusableCellCount() const override; + void willDisplayCellForIndex(HighlightCell * cell, int index) override; + void didBecomeFirstResponder() override; + bool handleEvent(Ion::Events::Event event) override; + bool hasBook(const char* filename) const; + void cleanRemovedBookRecord(); +private: + SelectableTableView m_tableView; + + static const int NB_FILES = 20; + External::Archive::File m_files[NB_FILES]; + int m_nbFiles = 0; + + static const int NB_CELLS = 6; + MessageTableCell m_cells[NB_CELLS]; + + ReadBookController m_readBookController; +}; + +} + +#endif \ No newline at end of file diff --git a/apps/reader/normalize.py b/apps/reader/normalize.py new file mode 100644 index 000000000..fff638b33 --- /dev/null +++ b/apps/reader/normalize.py @@ -0,0 +1,18 @@ +import sys +import unicodedata +import argparse +import io +import shutil + +filename = sys.argv[1] + +print("Normalization of "+filename) + +output = open(filename+".tmp", "wb") + +with io.open(filename, "r", encoding='utf-8') as file: + for line in file: + unicodeLine = unicodedata.normalize("NFKD", line) + output.write(unicodeLine.encode("UTF-8")) +output.close() +shutil.move(filename+".tmp",filename) diff --git a/apps/reader/read_book_controller.cpp b/apps/reader/read_book_controller.cpp new file mode 100644 index 000000000..7bdd9380e --- /dev/null +++ b/apps/reader/read_book_controller.cpp @@ -0,0 +1,69 @@ +#include "read_book_controller.h" + +namespace reader +{ + +ReadBookController::ReadBookController(Responder * parentResponder) : + ViewController(parentResponder) +{ +} + +View * ReadBookController::view() +{ + return &m_readerView; +} + +void ReadBookController::setBook(const External::Archive::File& file) +{ + m_file = &file; + loadPosition(); + m_readerView.setText(reinterpret_cast(file.data), file.dataLength); +} + +bool ReadBookController::handleEvent(Ion::Events::Event event) +{ + if(event == Ion::Events::Down) + { + m_readerView.nextPage(); + return true; + } + if(event == Ion::Events::Up) + { + m_readerView.previousPage(); + return true; + } + if(event == Ion::Events::Back || event == Ion::Events::Home) + { + savePosition(); + } + return false; +} + +void ReadBookController::savePosition() const +{ + int pageOffset = m_readerView.getPageOffset(); + Ion::Storage::Record::ErrorStatus status = Ion::Storage::sharedStorage()->createRecordWithFullName(m_file->name, &pageOffset, sizeof(pageOffset)); + if(Ion::Storage::Record::ErrorStatus::NameTaken == status) + { + Ion::Storage::Record::Data data; + data.buffer = &pageOffset; + data.size = sizeof(pageOffset); + status = Ion::Storage::sharedStorage()->recordNamed(m_file->name).setValue(data); + } +} + +void ReadBookController::loadPosition() +{ + Ion::Storage::Record r = Ion::Storage::sharedStorage()->recordNamed(m_file->name); + if(Ion::Storage::sharedStorage()->hasRecord(r)) + { + int pageOffset = *(static_cast(r.value().buffer)); + m_readerView.setPageOffset(pageOffset); + } + else + { + m_readerView.setPageOffset(0); + } +} + +} \ No newline at end of file diff --git a/apps/reader/read_book_controller.h b/apps/reader/read_book_controller.h new file mode 100644 index 000000000..c4349be4e --- /dev/null +++ b/apps/reader/read_book_controller.h @@ -0,0 +1,27 @@ +#ifndef _READ_BOOK_CONTROLLER_H_ +#define _READ_BOOK_CONTROLLER_H_ + +#include +#include "apps/external/archive.h" +#include "word_wrap_view.h" + +namespace reader { + +class ReadBookController : public ViewController { +public: + ReadBookController(Responder * parentResponder); + View * view() override; + + void setBook(const External::Archive::File& file); + bool handleEvent(Ion::Events::Event event) override; + + void savePosition() const; + void loadPosition(); +private: + WordWrapTextView m_readerView; + const External::Archive::File* m_file; +}; + +} + +#endif \ No newline at end of file diff --git a/apps/reader/reader_icon.png b/apps/reader/reader_icon.png new file mode 100644 index 000000000..78e23e139 Binary files /dev/null and b/apps/reader/reader_icon.png differ diff --git a/apps/reader/utility.cpp b/apps/reader/utility.cpp new file mode 100644 index 000000000..2f82b61f3 --- /dev/null +++ b/apps/reader/utility.cpp @@ -0,0 +1,122 @@ +#include "utility.h" + +#include + +#ifndef DEVICE +#include +#include +#include +#endif + +namespace reader +{ + +bool stringEndsWith(const char* str, const char* pattern) +{ + int strLength = strlen(str); + int patternLength = strlen(pattern); + if (patternLength > strLength) + return false; + + const char* strIter = str + strlen(str); + const char* patternIter = pattern + strlen(pattern); + + while(*strIter == *patternIter) + { + if(patternIter == pattern) + return true; + strIter--; + patternIter--; + } + return false; +} + +void stringNCopy(char* dest, int max, const char* src, int len) +{ + while(len>0 && max >1 && *src) + { + *dest = *src; + dest++; + src++; + len--; + max--; + } + *dest=0; +} + + +#ifdef DEVICE + +int filesWithExtension(const char* extension, External::Archive::File* files, int filesSize) +{ + size_t nbTotalFiles = External::Archive::numberOfFiles(); + int nbFiles = 0; + for(size_t i=0; i < nbTotalFiles; ++i) + { + External::Archive::File file; + External::Archive::fileAtIndex(i, file); + if(stringEndsWith(file.name, ".txt")) + { + files[nbFiles] = file; + nbFiles++; + if(nbFiles == filesSize) + break; + } + } + return nbFiles; +} +#else + +static void fillFileData(External::Archive::File& file) +{ + file.data = nullptr; + file.dataLength = 0; + + struct stat info; + if (stat(file.name, &info) != 0) + { + return; + } + + unsigned char* content = new unsigned char[info.st_size]; + if (content == NULL) + { + return ; + } + FILE *fp = fopen(file.name, "rb"); + if (fp == NULL) + { + return ; + } + + fread(content, info.st_size, 1, fp); + fclose(fp); + file.data = content; + file.dataLength = info.st_size; +} + +int filesWithExtension(const char* extension, External::Archive::File* files, int filesSize) +{ + dirent *file; + DIR *d = opendir("."); + int nb = 0; + if (d) + { + while ((file = readdir(d)) != NULL) + { + if(stringEndsWith(file->d_name, extension)) + { + files[nb].name = strdup(file->d_name);//will probably leak + fillFileData(files[nb]); + nb++; + if(nb == filesSize) + break; + } + } + closedir(d); + } + return nb; +} +#endif + +} \ No newline at end of file diff --git a/apps/reader/utility.h b/apps/reader/utility.h new file mode 100644 index 000000000..6893ab95c --- /dev/null +++ b/apps/reader/utility.h @@ -0,0 +1,14 @@ +#ifndef __UTILITY_H__ +#define __UTILITY_H__ + +#include + +namespace reader +{ + +bool stringEndsWith(const char* str, const char* end); +int filesWithExtension(const char* extension, External::Archive::File* files, int filesSize) ; +void stringNCopy(char* dest, int max, const char* src, int len); + +} +#endif \ No newline at end of file diff --git a/apps/reader/word_wrap_view.cpp b/apps/reader/word_wrap_view.cpp new file mode 100644 index 000000000..9e4cccd62 --- /dev/null +++ b/apps/reader/word_wrap_view.cpp @@ -0,0 +1,156 @@ +#include "word_wrap_view.h" + +#include "utility.h" + +namespace reader +{ + +void WordWrapTextView::nextPage() +{ + if(m_nextPageOffset >= m_length) + return; + m_pageOffset = m_nextPageOffset; + markRectAsDirty(bounds()); +} + +void WordWrapTextView::setText(const char* text, int length) +{ + PointerTextView::setText(text); + m_length = length; +} + +void WordWrapTextView::previousPage() +{ + if(m_pageOffset <= 0) + return; + + const int spaceWidth = m_font->stringSize(" ").width(); + + const char * endOfWord = text() + m_pageOffset; + const char * startOfWord = UTF8Helper::BeginningOfWord(text(), endOfWord); + + KDPoint textPosition(m_frame.width() - margin, m_frame.height() - margin); + + while(startOfWord>=text()) + { + endOfWord = UTF8Helper::EndOfWord(startOfWord); + KDSize textSize = m_font->stringSizeUntil(startOfWord, endOfWord); + KDPoint previousTextPosition = KDPoint(textPosition.x()-textSize.width(), textPosition.y()); + + if(previousTextPosition.x() < margin) + { + textPosition = KDPoint(m_frame.width() - margin, textPosition.y() - textSize.height()); + previousTextPosition = KDPoint(textPosition.x() - textSize.width(), textPosition.y()); + } + if(textPosition.y() - textSize.height() < margin) + { + break; + } + + + --startOfWord; + while(startOfWord >= text() && *startOfWord == ' ') + { + previousTextPosition = KDPoint(previousTextPosition.x() - spaceWidth, previousTextPosition.y()); + --startOfWord; + } + if(previousTextPosition.x() < margin) + { + previousTextPosition = KDPoint(m_frame.width() - margin, previousTextPosition.y() - textSize.height()); + } + + + while(startOfWord >= text() && *startOfWord == '\n') + { + previousTextPosition = KDPoint(m_frame.width() - margin, previousTextPosition.y() - textSize.height()); + --startOfWord; + } + + if(previousTextPosition.y() - textSize.height() < margin) + { + break; + } + + textPosition = previousTextPosition; + endOfWord = startOfWord; + startOfWord = UTF8Helper::BeginningOfWord(text(), endOfWord); + } + if(startOfWord == text()) + m_pageOffset = 0; + else + m_pageOffset = UTF8Helper::EndOfWord(startOfWord) - text() + 1; + markRectAsDirty(bounds()); +} + +void WordWrapTextView::drawRect(KDContext * ctx, KDRect rect) const +{ + ctx->fillRect(KDRect(0, 0, bounds().width(), bounds().height()), m_backgroundColor); + + const char * endOfFile = text() + m_length; + const char * startOfWord = text() + m_pageOffset; + const char * endOfWord = UTF8Helper::EndOfWord(startOfWord); + KDPoint textPosition(margin, margin); + + const int wordMaxLength = 128; + char word[wordMaxLength]; + + const int spaceWidth = m_font->stringSize(" ").width(); + + while(startOfWord < endOfFile) + { + + KDSize textSize = m_font->stringSizeUntil(startOfWord, endOfWord); + KDPoint nextTextPosition = KDPoint(textPosition.x()+textSize.width(), textPosition.y()); + + if(nextTextPosition.x() > m_frame.width() - margin) + { + textPosition = KDPoint(margin, textPosition.y() + textSize.height()); + nextTextPosition = KDPoint(margin + textSize.width(), textPosition.y()); + } + if(textPosition.y() + textSize.height() > m_frame.height() - margin) + { + break; + } + + stringNCopy(word, wordMaxLength, startOfWord, endOfWord-startOfWord); + ctx->drawString(word, textPosition, m_font, m_textColor, m_backgroundColor); + + while(*endOfWord == ' ') + { + nextTextPosition = KDPoint(nextTextPosition.x() + spaceWidth, nextTextPosition.y()); + ++endOfWord; + } + if(nextTextPosition.x() > m_frame.width() - margin) + { + nextTextPosition = KDPoint(margin, nextTextPosition.y() + textSize.height()); + } + + while(*endOfWord == '\n') + { + nextTextPosition = KDPoint(margin, nextTextPosition.y() + textSize.height()); + ++endOfWord; + } + + if(nextTextPosition.y() + textSize.height() > m_frame.height() - margin) + { + break; + } + + textPosition = nextTextPosition; + startOfWord = endOfWord; + endOfWord = UTF8Helper::EndOfWord(startOfWord); + } + m_nextPageOffset = startOfWord - text(); +} + +int WordWrapTextView::getPageOffset() const +{ + return m_pageOffset; +} + +void WordWrapTextView::setPageOffset(int o) +{ + m_pageOffset = o; +} + +} \ No newline at end of file diff --git a/apps/reader/word_wrap_view.h b/apps/reader/word_wrap_view.h new file mode 100644 index 000000000..4075ff4e2 --- /dev/null +++ b/apps/reader/word_wrap_view.h @@ -0,0 +1,27 @@ +#ifndef _WORD_WRAP_VIEW_H_ +#define _WORD_WRAP_VIEW_H_ + +#include + +namespace reader +{ + +class WordWrapTextView : public PointerTextView { +public: + void drawRect(KDContext * ctx, KDRect rect) const override; + void setText(const char*, int length); + void nextPage(); + void previousPage(); + int getPageOffset() const; + void setPageOffset(int o); +protected: + int m_pageOffset = 0; + mutable int m_nextPageOffset = 0; + int m_length = 0; + + static const int margin = 10; +}; + +} + +#endif \ No newline at end of file diff --git a/build/config.mak b/build/config.mak index ae8e0dfae..97caa8945 100644 --- a/build/config.mak +++ b/build/config.mak @@ -8,7 +8,7 @@ EPSILON_VERSION ?= 15.5.0 OMEGA_VERSION ?= 1.22.1 # OMEGA_USERNAME ?= N/A OMEGA_STATE ?= public -EPSILON_APPS ?= calculation rpn graph code statistics probability solver atomic sequence regression settings external +EPSILON_APPS ?= calculation rpn graph code statistics probability solver atomic sequence regression reader settings external SUBMODULES_APPS = atomic rpn EPSILON_I18N ?= en fr nl pt it de es hu EPSILON_COUNTRIES ?= WW CA DE ES FR GB IT NL PT US diff --git a/themes/icons.json b/themes/icons.json index 3dc1ae52d..ee6985c86 100644 --- a/themes/icons.json +++ b/themes/icons.json @@ -9,6 +9,7 @@ "apps/graph/graph_icon.png" : "apps/graph_icon.png", "apps/probability/probability_icon.png" : "apps/probability_icon.png", "apps/regression/regression_icon.png" : "apps/regression_icon.png", + "apps/reader/reader_icon.png" : "apps/reader_icon.png", "apps/rpn/rpn_icon.png" : "apps/rpn_icon.png", "apps/sequence/sequence_icon.png" : "apps/sequence_icon.png", "apps/settings/settings_icon.png" : "apps/settings_icon.png",