~aleteoryx/muditaos

5d757a6dc0ca3e3fcded6b7b915239f7093a468d — Piotr Tanski 5 years ago 3a924e7
[EGD-4553] Notes search engine implemented. (#1112)

M changelog.md => changelog.md +1 -0
@@ 4,6 4,7 @@

### Added
* `[notes]` Notes application implemented.
* `[notes]` Notes search engine implemented.

### Changed


M image/assets/lang/English.json => image/assets/lang/English.json +1 -0
@@ 130,6 130,7 @@
  "app_notes_delete_note": "Delete",
  "app_notes_note_delete_confirmation": "Do you really want to delete this note?",
  "app_notes_no_notes": "There are no notes yet.\nPress Left arrow to add new.",
  "app_notes_search_no_results": "No notes found.",

  "app_calllog_title_main": "Calls",
  "app_calllog_new_note": "New Note",

M module-apps/application-notes/ApplicationNotes.cpp => module-apps/application-notes/ApplicationNotes.cpp +20 -7
@@ 7,6 7,8 @@
#include "windows/NoteMainWindow.hpp"
#include "windows/NotePreviewWindow.hpp"
#include "windows/NoteEditWindow.hpp"
#include "windows/SearchEngineWindow.hpp"
#include "windows/SearchResultsWindow.hpp"

#include <service-db/DBMessage.hpp>
#include <service-db/QueryMessage.hpp>


@@ 80,22 82,33 @@ namespace app

    void ApplicationNotes::createUserInterface()
    {
        windowsFactory.attach(gui::name::window::main_window, [this](Application *app, const std::string &name) {
            auto notesRepository = std::make_unique<notes::NotesDBRepository>(this);
            auto notesProvider   = std::make_shared<notes::NotesListModel>(this, std::move(notesRepository));
        windowsFactory.attach(gui::name::window::main_window, [](Application *app, const std::string &name) {
            auto notesRepository = std::make_unique<notes::NotesDBRepository>(app);
            auto notesProvider   = std::make_shared<notes::NotesListModel>(app, std::move(notesRepository));
            auto presenter       = std::make_unique<notes::NotesMainWindowPresenter>(notesProvider);
            return std::make_unique<notes::NoteMainWindow>(app, std::move(presenter));
        });
        windowsFactory.attach(gui::name::window::note_preview, [this](Application *app, const std::string &name) {
            auto notesRepository = std::make_unique<notes::NotesDBRepository>(this);
        windowsFactory.attach(gui::name::window::note_preview, [](Application *app, const std::string &name) {
            auto notesRepository = std::make_unique<notes::NotesDBRepository>(app);
            auto presenter       = std::make_unique<notes::NotePreviewWindowPresenter>(std::move(notesRepository));
            return std::make_unique<notes::NotePreviewWindow>(app, std::move(presenter));
        });
        windowsFactory.attach(gui::name::window::note_edit, [this](Application *app, const std::string &name) {
            auto notesRepository = std::make_unique<notes::NotesDBRepository>(this);
        windowsFactory.attach(gui::name::window::note_edit, [](Application *app, const std::string &name) {
            auto notesRepository = std::make_unique<notes::NotesDBRepository>(app);
            auto presenter       = std::make_unique<notes::NoteEditWindowPresenter>(std::move(notesRepository));
            return std::make_unique<notes::NoteEditWindow>(app, std::move(presenter));
        });
        windowsFactory.attach(gui::name::window::notes_search, [](Application *app, const std::string &name) {
            auto notesRepository = std::make_unique<notes::NotesDBRepository>(app);
            auto presenter       = std::make_unique<notes::SearchEngineWindowPresenter>(std::move(notesRepository));
            return std::make_unique<notes::SearchEngineWindow>(app, std::move(presenter));
        });
        windowsFactory.attach(gui::name::window::notes_search_result, [](Application *app, const std::string &name) {
            return std::make_unique<notes::SearchResultsWindow>(app);
        });
        windowsFactory.attach(gui::name::window::note_dialog, [](Application *app, const std::string &name) {
            return std::make_unique<gui::Dialog>(app, name);
        });
        windowsFactory.attach(gui::name::window::note_confirm_dialog, [](Application *app, const std::string &name) {
            return std::make_unique<gui::DialogYesNo>(app, name);
        });

M module-apps/application-notes/ApplicationNotes.hpp => module-apps/application-notes/ApplicationNotes.hpp +3 -0
@@ 10,6 10,9 @@ namespace gui::name::window
{
    inline constexpr auto note_preview        = "NotePreview";
    inline constexpr auto note_edit           = "NoteEdit";
    inline constexpr auto notes_search        = "NotesSearch";
    inline constexpr auto notes_search_result = "NotesSearchResult";
    inline constexpr auto note_dialog         = "Dialog";
    inline constexpr auto note_confirm_dialog = "ConfirmDialog";
} // namespace gui::name::window


M module-apps/application-notes/CMakeLists.txt => module-apps/application-notes/CMakeLists.txt +10 -0
@@ 13,29 13,39 @@ target_sources( ${PROJECT_NAME}
	PRIVATE
		"${CMAKE_CURRENT_LIST_DIR}/ApplicationNotes.cpp"
		"${CMAKE_CURRENT_LIST_DIR}/model/NotesListModel.cpp"
		"${CMAKE_CURRENT_LIST_DIR}/model/SearchResultsListModel.cpp"
		"${CMAKE_CURRENT_LIST_DIR}/model/NotesRepository.cpp"
		"${CMAKE_CURRENT_LIST_DIR}/presenter/NotesMainWindowPresenter.cpp"
		"${CMAKE_CURRENT_LIST_DIR}/presenter/NotePreviewWindowPresenter.cpp"
		"${CMAKE_CURRENT_LIST_DIR}/presenter/NoteEditWindowPresenter.cpp"
		"${CMAKE_CURRENT_LIST_DIR}/presenter/SearchEngineWindowPresenter.cpp"
		"${CMAKE_CURRENT_LIST_DIR}/widgets/NotesItem.cpp"
		"${CMAKE_CURRENT_LIST_DIR}/windows/NoteMainWindow.cpp"
		"${CMAKE_CURRENT_LIST_DIR}/windows/NotePreviewWindow.cpp"
		"${CMAKE_CURRENT_LIST_DIR}/windows/NoteEditWindow.cpp"
		"${CMAKE_CURRENT_LIST_DIR}/windows/NotesOptions.cpp"
		"${CMAKE_CURRENT_LIST_DIR}/windows/SearchEngineWindow.cpp"
		"${CMAKE_CURRENT_LIST_DIR}/windows/SearchResultsWindow.cpp"
		"${CMAKE_CURRENT_LIST_DIR}/data/NoteSwitchData.cpp"
		"${CMAKE_CURRENT_LIST_DIR}/data/NotesFoundData.cpp"
	PUBLIC
		"${CMAKE_CURRENT_LIST_DIR}/ApplicationNotes.hpp"
		"${CMAKE_CURRENT_LIST_DIR}/model/NotesListModel.hpp"
		"${CMAKE_CURRENT_LIST_DIR}/model/SearchResultsListModel.hpp"
		"${CMAKE_CURRENT_LIST_DIR}/model/NotesRepository.hpp"
		"${CMAKE_CURRENT_LIST_DIR}/presenter/NotesMainWindowPresenter.hpp"
		"${CMAKE_CURRENT_LIST_DIR}/presenter/NotePreviewWindowPresenter.hpp"
		"${CMAKE_CURRENT_LIST_DIR}/presenter/NoteEditWindowPresenter.hpp"
		"${CMAKE_CURRENT_LIST_DIR}/presenter/SearchEngineWindowPresenter.hpp"
		"${CMAKE_CURRENT_LIST_DIR}/widgets/NotesItem.hpp"
		"${CMAKE_CURRENT_LIST_DIR}/windows/NoteMainWindow.hpp"
		"${CMAKE_CURRENT_LIST_DIR}/windows/NotePreviewWindow.hpp"
		"${CMAKE_CURRENT_LIST_DIR}/windows/NoteEditWindow.hpp"
		"${CMAKE_CURRENT_LIST_DIR}/windows/SearchEngineWindow.hpp"
		"${CMAKE_CURRENT_LIST_DIR}/windows/SearchResultsWindow.hpp"
		"${CMAKE_CURRENT_LIST_DIR}/windows/NotesOptions.hpp"
		"${CMAKE_CURRENT_LIST_DIR}/data/NoteSwitchData.hpp"
		"${CMAKE_CURRENT_LIST_DIR}/data/NotesFoundData.hpp"
)
target_include_directories(${PROJECT_NAME}
    PRIVATE

A module-apps/application-notes/data/NotesFoundData.cpp => module-apps/application-notes/data/NotesFoundData.cpp +22 -0
@@ 0,0 1,22 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "NotesFoundData.hpp"

namespace app::notes
{
    NotesFoundData::NotesFoundData(std::string searchText, std::vector<NotesRecord> notes)
        : gui::SwitchData(std::string{"NotesFoundData"}), searchText{std::move(searchText)}, recordsFound{
                                                                                                 std::move(notes)}
    {}

    const std::string &NotesFoundData::getSearchText() const noexcept
    {
        return searchText;
    }

    const std::vector<NotesRecord> &NotesFoundData::getFoundRecords() const noexcept
    {
        return recordsFound;
    }
} // namespace app::notes

A module-apps/application-notes/data/NotesFoundData.hpp => module-apps/application-notes/data/NotesFoundData.hpp +25 -0
@@ 0,0 1,25 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <string>

#include <module-db/Interface/NotesRecord.hpp>
#include <module-gui/gui/SwitchData.hpp>

namespace app::notes
{
    class NotesFoundData : public gui::SwitchData
    {
      public:
        NotesFoundData(std::string searchText, std::vector<NotesRecord> notes);

        const std::string &getSearchText() const noexcept;
        const std::vector<NotesRecord> &getFoundRecords() const noexcept;

      private:
        std::string searchText;
        std::vector<NotesRecord> recordsFound;
    };
} // namespace app::notes

M module-apps/application-notes/model/NotesRepository.cpp => module-apps/application-notes/model/NotesRepository.cpp +17 -0
@@ 4,6 4,7 @@
#include "NotesRepository.hpp"

#include <module-db/queries/notes/QueryNotesGet.hpp>
#include <module-db/queries/notes/QueryNotesGetByText.hpp>
#include <module-db/queries/notes/QueryNoteStore.hpp>
#include <module-db/queries/notes/QueryNoteRemove.hpp>



@@ 30,6 31,22 @@ namespace app::notes
        DBServiceAPI::GetQuery(application, db::Interface::Name::Notes, std::move(query));
    }

    void NotesDBRepository::getByText(const std::string &text, const OnFilteredCallback &callback)
    {
        auto query = std::make_unique<db::query::QueryNotesGetByText>(text);
        query->setQueryListener(db::QueryCallback::fromFunction([callback](auto response) {
            auto result = dynamic_cast<db::query::NotesGetByTextResult *>(response);
            if (result == nullptr) {
                return false;
            }
            if (callback) {
                callback(result->getRecords());
            }
            return true;
        }));
        DBServiceAPI::GetQuery(application, db::Interface::Name::Notes, std::move(query));
    }

    void NotesDBRepository::save(const NotesRecord &note, const OnResultCallback &callback)
    {
        auto query = std::make_unique<db::query::QueryNoteStore>(note);

M module-apps/application-notes/model/NotesRepository.hpp => module-apps/application-notes/model/NotesRepository.hpp +5 -2
@@ 14,12 14,14 @@ namespace app::notes
    class AbstractNotesRepository
    {
      public:
        using OnGetCallback    = std::function<bool(const std::vector<NotesRecord> &, unsigned int)>;
        using OnResultCallback = std::function<void(bool)>;
        using OnGetCallback      = std::function<bool(const std::vector<NotesRecord> &, unsigned int)>;
        using OnFilteredCallback = std::function<void(const std::vector<NotesRecord> &)>;
        using OnResultCallback   = std::function<void(bool)>;

        virtual ~AbstractNotesRepository() noexcept = default;

        virtual void get(std::uint32_t offset, std::uint32_t limit, const OnGetCallback &callback) = 0;
        virtual void getByText(const std::string &text, const OnFilteredCallback &callback)        = 0;
        virtual void save(const NotesRecord &note, const OnResultCallback &callback)               = 0;
        virtual void remove(const NotesRecord &note, const OnResultCallback &callback)             = 0;
    };


@@ 30,6 32,7 @@ namespace app::notes
        explicit NotesDBRepository(Application *application);

        void get(std::uint32_t offset, std::uint32_t limit, const OnGetCallback &callback) override;
        void getByText(const std::string &text, const OnFilteredCallback &callback) override;
        void save(const NotesRecord &note, const OnResultCallback &callback) override;
        void remove(const NotesRecord &note, const OnResultCallback &callback) override;


A module-apps/application-notes/model/SearchResultsListModel.cpp => module-apps/application-notes/model/SearchResultsListModel.cpp +59 -0
@@ 0,0 1,59 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "SearchResultsListModel.hpp"

#include "module-apps/application-notes/style/NotesListStyle.hpp"
#include "module-apps/application-notes/data/NoteSwitchData.hpp"

#include <module-gui/gui/widgets/ListView.hpp>
#include <module-apps/application-notes/ApplicationNotes.hpp>

namespace app::notes
{
    SearchResultsListModel::SearchResultsListModel(Application *application) : application{application}
    {}

    void SearchResultsListModel::setResults(const std::vector<NotesRecord> &results)
    {
        eraseInternalData();
        for (const auto &note : results) {
            auto item = createItem(note);
            internalData.push_back(std::move(item));
        }
    }

    gui::NotesItem *SearchResultsListModel::createItem(const NotesRecord &record) const
    {
        auto item               = new gui::NotesItem(std::make_shared<NotesRecord>(record));
        item->deleteByList      = false;
        item->activatedCallback = [this, record](gui::Item &) {
            auto data                        = std::make_unique<NoteSwitchData>(record);
            data->ignoreCurrentWindowOnStack = true;
            application->switchWindow(gui::name::window::note_preview, std::move(data));
            return true;
        };
        return item;
    }

    void SearchResultsListModel::requestRecords(std::uint32_t offset, std::uint32_t limit)
    {
        setupModel(offset, limit);
        list->onProviderDataUpdate();
    }

    unsigned int SearchResultsListModel::requestRecordsCount()
    {
        return internalData.size();
    }

    gui::ListItem *SearchResultsListModel::getItem(gui::Order order)
    {
        return getRecord(order);
    }

    unsigned int SearchResultsListModel::getMinimalItemHeight() const
    {
        return style::list::item::Height;
    }
} // namespace app::notes

A module-apps/application-notes/model/SearchResultsListModel.hpp => module-apps/application-notes/model/SearchResultsListModel.hpp +34 -0
@@ 0,0 1,34 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <cstdint>
#include <vector>

#include <module-db/Interface/NotesRecord.hpp>

#include <module-apps/InternalModel.hpp>
#include "module-apps/application-notes/widgets/NotesItem.hpp"
#include <module-gui/gui/widgets/ListItemProvider.hpp>

namespace app::notes
{
    class SearchResultsListModel : public InternalModel<gui::NotesItem *>, public gui::ListItemProvider
    {
      public:
        explicit SearchResultsListModel(Application *application);

        void setResults(const std::vector<NotesRecord> &results);

        void requestRecords(std::uint32_t offset, std::uint32_t limit) override;
        unsigned int requestRecordsCount() override;
        unsigned int getMinimalItemHeight() const override;
        gui::ListItem *getItem(gui::Order order) override;

      private:
        gui::NotesItem *createItem(const NotesRecord &record) const;

        Application *application;
    };
} // namespace app::notes

A module-apps/application-notes/presenter/SearchEngineWindowPresenter.cpp => module-apps/application-notes/presenter/SearchEngineWindowPresenter.cpp +21 -0
@@ 0,0 1,21 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "SearchEngineWindowPresenter.hpp"

namespace app::notes
{
    SearchEngineWindowPresenter::SearchEngineWindowPresenter(std::unique_ptr<AbstractNotesRepository> &&notesRepository)
        : notesRepository{std::move(notesRepository)}
    {}

    void SearchEngineWindowPresenter::searchFor(const std::string &searchText)
    {
        if (searchText.empty()) {
            getView()->notesFound({}, searchText);
            return;
        }
        notesRepository->getByText(
            searchText, [searchText, this](const auto &records) { getView()->notesFound(records, searchText); });
    }
} // namespace app::notes

A module-apps/application-notes/presenter/SearchEngineWindowPresenter.hpp => module-apps/application-notes/presenter/SearchEngineWindowPresenter.hpp +41 -0
@@ 0,0 1,41 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "BasePresenter.hpp"

#include <module-apps/application-notes/model/NotesRepository.hpp>

namespace app::notes
{
    class SearchEngineWindowContract
    {
      public:
        class View
        {
          public:
            virtual ~View() noexcept = default;

            virtual void notesFound(const std::vector<NotesRecord> &notes, const std::string &searchText) = 0;
        };
        class Presenter : public BasePresenter<SearchEngineWindowContract::View>
        {
          public:
            virtual ~Presenter() noexcept = default;

            virtual void searchFor(const std::string &searchText) = 0;
        };
    };

    class SearchEngineWindowPresenter : public SearchEngineWindowContract::Presenter
    {
      public:
        explicit SearchEngineWindowPresenter(std::unique_ptr<AbstractNotesRepository> &&notesRepository);

        void searchFor(const std::string &searchText) override;

      private:
        std::unique_ptr<AbstractNotesRepository> notesRepository;
    };
} // namespace app::notes

M module-apps/application-notes/windows/NoteMainWindow.cpp => module-apps/application-notes/windows/NoteMainWindow.cpp +3 -0
@@ 167,6 167,9 @@ namespace app::notes
                application->switchWindow(gui::name::window::note_edit,
                                          std::make_unique<NoteSwitchData>(NotesRecord{}));
            }
            else if (inputEvent.is(gui::KeyCode::KEY_RIGHT)) {
                application->switchWindow(gui::name::window::notes_search);
            }
        }
        return AppWindow::onInput(inputEvent);
    }

A module-apps/application-notes/windows/SearchEngineWindow.cpp => module-apps/application-notes/windows/SearchEngineWindow.cpp +61 -0
@@ 0,0 1,61 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "SearchEngineWindow.hpp"

#include <module-apps/application-notes/ApplicationNotes.hpp>
#include <module-apps/application-notes/data/NotesFoundData.hpp>
#include <module-apps/widgets/InputBox.hpp>

#include <module-utils/i18n/i18n.hpp>

namespace app::notes
{
    SearchEngineWindow::SearchEngineWindow(Application *application,
                                           std::unique_ptr<SearchEngineWindowContract::Presenter> &&windowPresenter)
        : gui::AppWindow{application, gui::name::window::notes_search}, presenter{std::move(windowPresenter)}
    {
        presenter->attach(this);
        buildInterface();
    }

    SearchEngineWindow::~SearchEngineWindow() noexcept
    {
        destroyInterface();
    }

    void SearchEngineWindow::buildInterface()
    {
        AppWindow::buildInterface();
        setTitle(utils::localize.get("app_notes_title_main"));

        bottomBar->setActive(gui::BottomBar::Side::CENTER, true);
        bottomBar->setText(gui::BottomBar::Side::CENTER, utils::localize.get(style::strings::common::search));
        bottomBar->setActive(gui::BottomBar::Side::RIGHT, true);
        bottomBar->setText(gui::BottomBar::Side::RIGHT, utils::localize.get(style::strings::common::back));

        input = gui::inputBox(this, utils::localize.get("common_search_uc"), "search");
        setFocusItem(input);
    }

    void SearchEngineWindow::destroyInterface()
    {
        erase();
        input = nullptr;
    }

    bool SearchEngineWindow::onInput(const gui::InputEvent &inputEvent)
    {
        if (inputEvent.isShortPress() && inputEvent.is(gui::KeyCode::KEY_ENTER)) {
            presenter->searchFor(input->getText());
            return true;
        }
        return AppWindow::onInput(inputEvent);
    }

    void SearchEngineWindow::notesFound(const std::vector<NotesRecord> &notes, const std::string &searchText)
    {
        application->switchWindow(gui::name::window::notes_search_result,
                                  std::make_unique<NotesFoundData>(searchText, notes));
    }
} // namespace app::notes

A module-apps/application-notes/windows/SearchEngineWindow.hpp => module-apps/application-notes/windows/SearchEngineWindow.hpp +33 -0
@@ 0,0 1,33 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "AppWindow.hpp"

#include <module-apps/Application.hpp>
#include <module-apps/application-notes/presenter/SearchEngineWindowPresenter.hpp>

#include <module-gui/gui/input/InputEvent.hpp>
#include <module-gui/gui/widgets/Text.hpp>

namespace app::notes
{
    class SearchEngineWindow : public gui::AppWindow, public SearchEngineWindowContract::View
    {
      public:
        SearchEngineWindow(Application *application,
                           std::unique_ptr<SearchEngineWindowContract::Presenter> &&windowPresenter);
        ~SearchEngineWindow() noexcept override;

        void buildInterface() override;
        void destroyInterface() override;
        bool onInput(const gui::InputEvent &inputEvent) override;

        void notesFound(const std::vector<NotesRecord> &notes, const std::string &searchText) override;

      private:
        std::unique_ptr<SearchEngineWindowContract::Presenter> presenter;
        gui::Text *input = nullptr;
    };
} // namespace app::notes

A module-apps/application-notes/windows/SearchResultsWindow.cpp => module-apps/application-notes/windows/SearchResultsWindow.cpp +76 -0
@@ 0,0 1,76 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "SearchResultsWindow.hpp"

#include <module-apps/application-notes/ApplicationNotes.hpp>
#include <module-apps/application-notes/data/NotesFoundData.hpp>
#include <module-apps/application-notes/style/NotesListStyle.hpp>
#include <module-apps/windows/DialogMetadata.hpp>
#include <module-apps/messages/DialogMetadataMessage.hpp>

namespace app::notes
{
    SearchResultsWindow::SearchResultsWindow(Application *application)
        : AppWindow(application, gui::name::window::notes_search_result), listModel{
                                                                              std::make_shared<SearchResultsListModel>(
                                                                                  application)}
    {
        buildInterface();
    }

    SearchResultsWindow::~SearchResultsWindow() noexcept
    {
        destroyInterface();
    }

    void SearchResultsWindow::buildInterface()
    {
        AppWindow::buildInterface();
        setTitle(utils::localize.get("app_notes_title_main"));

        bottomBar->setActive(gui::BottomBar::Side::CENTER, true);
        bottomBar->setText(gui::BottomBar::Side::CENTER, utils::localize.get(::style::strings::common::open));
        bottomBar->setActive(gui::BottomBar::Side::RIGHT, true);
        bottomBar->setText(gui::BottomBar::Side::RIGHT, utils::localize.get(::style::strings::common::back));

        list =
            new gui::ListView(this, style::list::X, style::list::Y, style::list::Width, style::list::Height, listModel);
        list->setScrollTopMargin(::style::margins::small);
        setFocusItem(list);
    }

    void SearchResultsWindow::destroyInterface()
    {
        erase();
        list = nullptr;
    }

    void SearchResultsWindow::onBeforeShow(gui::ShowMode mode, gui::SwitchData *data)
    {
        auto foundNotesData = dynamic_cast<NotesFoundData *>(data);
        if (foundNotesData == nullptr) {
            onNothingFound();
            return;
        }

        if (const auto &records = foundNotesData->getFoundRecords(); !records.empty()) {
            listModel->setResults(records);
            list->rebuildList();
            setFocusItem(list);
        }
        else {
            onNothingFound(foundNotesData->getSearchText());
        }
    }

    void SearchResultsWindow::onNothingFound(const std::string &searchText)
    {
        gui::DialogMetadata meta{utils::localize.get("common_results_prefix") + searchText,
                                 "search_big",
                                 utils::localize.get("app_notes_search_no_results")};
        auto data                        = std::make_unique<gui::DialogMetadataMessage>(meta);
        data->ignoreCurrentWindowOnStack = true;
        application->switchWindow(gui::name::window::note_dialog, std::move(data));
    }
} // namespace app::notes

A module-apps/application-notes/windows/SearchResultsWindow.hpp => module-apps/application-notes/windows/SearchResultsWindow.hpp +34 -0
@@ 0,0 1,34 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <memory>

#include "AppWindow.hpp"

#include <module-apps/Application.hpp>
#include <module-apps/application-notes/model/SearchResultsListModel.hpp>

#include <module-gui/gui/widgets/ListView.hpp>

namespace app::notes
{
    class SearchResultsWindow : public gui::AppWindow
    {
      public:
        explicit SearchResultsWindow(Application *application);
        ~SearchResultsWindow() noexcept override;

        void buildInterface() override;
        void destroyInterface() override;

        void onBeforeShow(gui::ShowMode mode, gui::SwitchData *data) override;

      private:
        void onNothingFound(const std::string &searchText = {});

        std::shared_ptr<SearchResultsListModel> listModel;
        gui::ListView *list = nullptr;
    };
} // namespace app::notes

M module-db/CMakeLists.txt => module-db/CMakeLists.txt +1 -0
@@ 88,6 88,7 @@ set(SOURCES
        queries/messages/threads/QueryThreadRemove.cpp
        queries/messages/threads/QueryThreadMarkAsRead.cpp
        queries/notes/QueryNotesGet.cpp
        queries/notes/QueryNotesGetByText.cpp
        queries/notes/QueryNoteStore.cpp
        queries/notes/QueryNoteRemove.cpp
        queries/calllog/QueryCalllogSetAllRead.cpp

M module-db/Interface/NotesRecord.cpp => module-db/Interface/NotesRecord.cpp +27 -3
@@ 4,6 4,7 @@
#include "NotesRecord.hpp"

#include "queries/notes/QueryNotesGet.hpp"
#include "queries/notes/QueryNotesGetByText.hpp"
#include "queries/notes/QueryNoteStore.hpp"
#include "queries/notes/QueryNoteRemove.hpp"



@@ 96,9 97,20 @@ NotesRecord NotesRecordInterface::GetByID(std::uint32_t id)
std::vector<NotesRecord> NotesRecordInterface::getNotes(std::uint32_t offset, std::uint32_t limit) const
{
    std::vector<NotesRecord> records;
    const auto notes = notesDB->notes.getLimitOffset(offset, limit);
    for (const auto &w : notes) {
        NotesRecord record{w.ID, w.date, w.snippet};
    const auto &notes = notesDB->notes.getLimitOffset(offset, limit);
    for (const auto &note : notes) {
        NotesRecord record{note.ID, note.date, note.snippet};
        records.push_back(std::move(record));
    }
    return records;
}

std::vector<NotesRecord> NotesRecordInterface::getNotesByText(const std::string &text) const
{
    std::vector<NotesRecord> records;
    const auto &notes = notesDB->notes.getByText(text);
    for (const auto &note : notes) {
        NotesRecord record{note.ID, note.date, note.snippet};
        records.push_back(std::move(record));
    }
    return records;


@@ 109,6 121,9 @@ std::unique_ptr<db::QueryResult> NotesRecordInterface::runQuery(std::shared_ptr<
    if (typeid(*query) == typeid(db::query::QueryNotesGet)) {
        return getQuery(query);
    }
    if (typeid(*query) == typeid(db::query::QueryNotesGetByText)) {
        return getByTextQuery(query);
    }
    if (typeid(*query) == typeid(db::query::QueryNoteStore)) {
        return storeQuery(query);
    }


@@ 127,6 142,15 @@ std::unique_ptr<db::QueryResult> NotesRecordInterface::getQuery(const std::share
    return response;
}

std::unique_ptr<db::QueryResult> NotesRecordInterface::getByTextQuery(const std::shared_ptr<db::Query> &query)
{
    const auto localQuery = static_cast<db::query::QueryNotesGetByText *>(query.get());
    const auto &records   = getNotesByText(localQuery->getText());
    auto response         = std::make_unique<db::query::NotesGetByTextResult>(records);
    response->setRequestQuery(query);
    return response;
}

std::unique_ptr<db::QueryResult> NotesRecordInterface::storeQuery(const std::shared_ptr<db::Query> &query)
{
    const auto localQuery = static_cast<db::query::QueryNoteStore *>(query.get());

M module-db/Interface/NotesRecord.hpp => module-db/Interface/NotesRecord.hpp +2 -0
@@ 47,8 47,10 @@ class NotesRecordInterface : public RecordInterface<NotesRecord, NotesRecordFiel

  private:
    std::vector<NotesRecord> getNotes(std::uint32_t offset, std::uint32_t limit) const;
    std::vector<NotesRecord> getNotesByText(const std::string &text) const;

    std::unique_ptr<db::QueryResult> getQuery(const std::shared_ptr<db::Query> &query);
    std::unique_ptr<db::QueryResult> getByTextQuery(const std::shared_ptr<db::Query> &query);
    std::unique_ptr<db::QueryResult> storeQuery(const std::shared_ptr<db::Query> &query);
    std::unique_ptr<db::QueryResult> removeQuery(const std::shared_ptr<db::Query> &query);


M module-db/Tables/NotesTable.cpp => module-db/Tables/NotesTable.cpp +19 -0
@@ 117,6 117,25 @@ std::vector<NotesTableRow> NotesTable::getLimitOffsetByField(std::uint32_t offse
    return ret;
}

std::vector<NotesTableRow> NotesTable::getByText(const std::string &text)
{
    auto retQuery = db->query("SELECT *, INSTR(snippet,'%s') pos FROM notes WHERE pos > 0;", text.c_str());
    if (retQuery == nullptr || retQuery->getRowCount() == 0) {
        return {};
    }

    std::vector<NotesTableRow> records;
    do {
        NotesTableRow row{
            (*retQuery)[0].getUInt32(), // ID
            (*retQuery)[1].getUInt32(), // date
            (*retQuery)[2].getString()  // snippet
        };
        records.push_back(std::move(row));
    } while (retQuery->nextRow());
    return records;
}

std::uint32_t NotesTable::count()
{
    auto queryRet = db->query("SELECT COUNT(*) FROM notes;");

M module-db/Tables/NotesTable.hpp => module-db/Tables/NotesTable.hpp +1 -0
@@ 41,6 41,7 @@ class NotesTable : public Table<NotesTableRow, NotesTableFields>
                                                     std::uint32_t limit,
                                                     NotesTableFields field,
                                                     const char *str) override;
    std::vector<NotesTableRow> getByText(const std::string &text);

    std::uint32_t count() override;
    std::uint32_t countByFieldId(const char *field, std::uint32_t id) override;

A module-db/queries/notes/QueryNotesGetByText.cpp => module-db/queries/notes/QueryNotesGetByText.cpp +33 -0
@@ 0,0 1,33 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "QueryNotesGetByText.hpp"

namespace db::query
{
    QueryNotesGetByText::QueryNotesGetByText(std::string text) : Query(Query::Type::Read), text{std::move(text)}
    {}

    const std::string &QueryNotesGetByText::getText() const noexcept
    {
        return text;
    }

    std::string QueryNotesGetByText::debugInfo() const
    {
        return {"QueryNotesGetByText"};
    }

    NotesGetByTextResult::NotesGetByTextResult(std::vector<NotesRecord> notes) : records{std::move(notes)}
    {}

    const std::vector<NotesRecord> &NotesGetByTextResult::getRecords() const noexcept
    {
        return records;
    }

    auto NotesGetByTextResult::debugInfo() const -> std::string
    {
        return {"NotesGetByTextResult"};
    }
} // namespace db::query

A module-db/queries/notes/QueryNotesGetByText.hpp => module-db/queries/notes/QueryNotesGetByText.hpp +36 -0
@@ 0,0 1,36 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <string>

#include <Common/Query.hpp>
#include "Interface/NotesRecord.hpp"

namespace db::query
{
    class QueryNotesGetByText : public Query
    {
      public:
        explicit QueryNotesGetByText(std::string text);

        [[nodiscard]] const std::string &getText() const noexcept;
        [[nodiscard]] std::string debugInfo() const override;

      private:
        std::string text;
    };

    class NotesGetByTextResult : public QueryResult
    {
      public:
        explicit NotesGetByTextResult(std::vector<NotesRecord> notes);

        [[nodiscard]] const std::vector<NotesRecord> &getRecords() const noexcept;
        [[nodiscard]] std::string debugInfo() const override;

      private:
        std::vector<NotesRecord> records;
    };
} // namespace db::query

M module-db/tests/NotesRecord_tests.cpp => module-db/tests/NotesRecord_tests.cpp +12 -0
@@ 5,6 5,7 @@

#include <Interface/NotesRecord.hpp>
#include <queries/notes/QueryNotesGet.hpp>
#include <queries/notes/QueryNotesGetByText.hpp>
#include <queries/notes/QueryNoteRemove.hpp>
#include <queries/notes/QueryNoteStore.hpp>



@@ 40,6 41,17 @@ TEST_CASE("Notes Record tests")
        REQUIRE(getResult->getRecords()[0].snippet == testSnippet);
    }

    SECTION("Get notes by text query")
    {
        constexpr auto testSearch = "TEST";
        auto query                = std::make_unique<db::query::QueryNotesGetByText>(testSearch);
        auto response             = notesRecordInterface.runQuery(std::move(query));
        auto getResult            = static_cast<db::query::NotesGetByTextResult *>(response.get());

        REQUIRE(getResult->getRecords().size() == 1);
        REQUIRE(getResult->getRecords()[0].snippet == testSnippet);
    }

    SECTION("Add a note")
    {
        NotesRecord record;

M module-db/tests/NotesTable_tests.cpp => module-db/tests/NotesTable_tests.cpp +8 -0
@@ 33,6 33,14 @@ TEST_CASE("Notes Table tests")
        REQUIRE(records[0].snippet == testSnippet);
    }

    SECTION("Get notes by text query")
    {
        constexpr auto testSearch = "TEST";
        const auto &records       = table.getByText(testSearch);
        REQUIRE(records.size() == 1);
        REQUIRE(records[0].snippet == testSnippet);
    }

    SECTION("Add a note")
    {
        NotesTableRow row;