From a8866e3000c4953365315c69329138fa8007d65c Mon Sep 17 00:00:00 2001
From: Dmitriy Lukovenko <mludima23@gmail.com>
Date: Tue, 4 Jun 2024 03:40:23 +0800
Subject: [PATCH] app changes

---
 .gitmodules                  |   3 --
 CMakeLists.txt               |  14 ++++++++-
 app.rc                       |   1 +
 favicon.ico                  | Bin 0 -> 1150 bytes
 include/exporter.hpp         |  10 ++++++
 include/exporters/txt.hpp    |  17 ++++++++++
 include/note.hpp             |   2 ++
 include/note_storage.hpp     |   4 ++-
 include/precompiled.hpp      |   4 ++-
 include/view_model.hpp       |   4 ++-
 src/exporters/txt.cpp        |  21 +++++++++++++
 src/main.cpp                 |   2 +-
 src/note_storage.cpp         |  59 ++++++++++++++++++++++++++++++-----
 src/view_model.cpp           |  25 ++++++++++++---
 thirdparty/libwebview        |   2 +-
 ui/dist/app.bundle.js        |  12 +++----
 ui/src/components/editor.vue |  30 ++++++++++++------
 ui/src/components/note.vue   |  21 ++++++++-----
 ui/src/views/app.vue         |  22 ++++++-------
 19 files changed, 197 insertions(+), 56 deletions(-)
 create mode 100644 app.rc
 create mode 100644 favicon.ico
 create mode 100644 include/exporter.hpp
 create mode 100644 include/exporters/txt.hpp
 create mode 100644 src/exporters/txt.cpp

diff --git a/.gitmodules b/.gitmodules
index 5704b2d..86bb40b 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -10,6 +10,3 @@
 [submodule "thirdparty/libwebview"]
 	path = thirdparty/libwebview
 	url = https://github.com/a3st/libwebview
-[submodule "thirdparty/cppcoro"]
-	path = thirdparty/cppcoro
-	url = https://github.com/lewissbaker/cppcoro
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0ac4ecf..97298bc 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -21,10 +21,22 @@ add_subdirectory(thirdparty/base64pp)
 endif()
 
 add_executable(Notes
+    src/exporters/txt.cpp
     src/main.cpp
     src/view_model.cpp
     src/note_storage.cpp
-    src/note.cpp)
+    src/note.cpp
+    app.rc)
+
+if(WIN32)
+set_target_properties(Notes
+    PROPERTIES
+        LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE"
+        LINK_FLAGS_RELEASE "/SUBSYSTEM:windows /ENTRY:mainCRTStartup"
+        LINK_FLAGS_RELWITHDEBINFO "/SUBSYSTEM:windows /ENTRY:mainCRTStartup"
+        LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:windows /ENTRY:mainCRTStartup"
+    )
+endif()
 
 target_include_directories(Notes PRIVATE 
     ${PROJECT_SOURCE_DIR}/src
diff --git a/app.rc b/app.rc
new file mode 100644
index 0000000..da3e7bc
--- /dev/null
+++ b/app.rc
@@ -0,0 +1 @@
+IDI_ICON1 ICON DISCARDABLE "favicon.ico"
\ No newline at end of file
diff --git a/favicon.ico b/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..484352859fd07ba8d67d663a4b074150bdfce1cd
GIT binary patch
literal 1150
zcmZQzU<5(|0R|wcz>vYhz#zuJz@P!dKp~(AL>x#lFaYHa^Fc6}4<src{r?BV|B1jK
z3U2++WK8?d<i7qtOgl(FF?#=3KK}nd{o?=EZtMQXxvcrm1f;=ckfJ~B{Qv(zeQ!W!
zxUPkmL56;?89@DUwoCsr5vw1huJHE%{~$M{p8Nkl{>1-R(MSGglA<4^78DL3TypRK
z|D@CZ-;t^xroQz4|NqHnhzom|9%On{|F8)^V)HLbK842rKL&PSHe?23MtGJci2(rp
CVm)^N

literal 0
HcmV?d00001

diff --git a/include/exporter.hpp b/include/exporter.hpp
new file mode 100644
index 0000000..d44eb66
--- /dev/null
+++ b/include/exporter.hpp
@@ -0,0 +1,10 @@
+#pragma once
+
+namespace notes
+{
+    class Exporter
+    {
+      public:
+        virtual bool saveFileAs(std::filesystem::path const& filePath) = 0;
+    };
+} // namespace notes
\ No newline at end of file
diff --git a/include/exporters/txt.hpp b/include/exporters/txt.hpp
new file mode 100644
index 0000000..d2e6aae
--- /dev/null
+++ b/include/exporters/txt.hpp
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "exporter.hpp"
+
+namespace notes::exporters
+{
+    class TxtExport : public Exporter
+    {
+      public:
+        TxtExport(std::string_view const text);
+
+        bool saveFileAs(std::filesystem::path const& filePath) override;
+
+      private:
+        std::string text;
+    };
+} // namespace notes::exporters
\ No newline at end of file
diff --git a/include/note.hpp b/include/note.hpp
index e327260..89b9ee9 100644
--- a/include/note.hpp
+++ b/include/note.hpp
@@ -4,6 +4,8 @@ namespace notes
 {
     struct Note
     {
+        Note() = default;
+
         Note(uint32_t const noteID, std::string_view const noteName, std::string_view const noteData);
 
         /*!
diff --git a/include/note_storage.hpp b/include/note_storage.hpp
index 719ef30..6bdbd76 100644
--- a/include/note_storage.hpp
+++ b/include/note_storage.hpp
@@ -12,10 +12,12 @@ namespace notes
 
         std::string getNotesData() const;
 
-        void saveNoteToDB(Note const& note);
+        int32_t saveNoteToDB(Note const& note);
 
         void removeNoteFromDB(uint32_t const ID);
 
+        bool exportNoteAsFile(uint32_t const ID, std::string_view const format, std::filesystem::path const& filePath);
+
       private:
         SQLite::Database database;
 
diff --git a/include/precompiled.hpp b/include/precompiled.hpp
index 11d8a54..dc910d8 100644
--- a/include/precompiled.hpp
+++ b/include/precompiled.hpp
@@ -3,4 +3,6 @@
 #include <iostream>
 #include <string>
 #include <string_view>
-#include <sstream>
\ No newline at end of file
+#include <sstream>
+#include <filesystem>
+#include <fstream>
\ No newline at end of file
diff --git a/include/view_model.hpp b/include/view_model.hpp
index 5c5083a..bfe3598 100644
--- a/include/view_model.hpp
+++ b/include/view_model.hpp
@@ -12,10 +12,12 @@ namespace notes
 
         std::string getNotes();
 
-        void saveNote(uint32_t ID, std::string noteName, std::string noteData);
+        int32_t saveNote(uint32_t ID, std::string noteName, std::string noteData);
 
         void removeNote(uint32_t ID);
 
+        void exportNote(uint32_t ID, std::string format);
+
       private:
         libwebview::App* app;
         NoteStorage noteStorage;
diff --git a/src/exporters/txt.cpp b/src/exporters/txt.cpp
new file mode 100644
index 0000000..6f8f40f
--- /dev/null
+++ b/src/exporters/txt.cpp
@@ -0,0 +1,21 @@
+#include "exporters/txt.hpp"
+#include "precompiled.hpp"
+
+namespace notes::exporters
+{
+    TxtExport::TxtExport(std::string_view const text) : text(text)
+    {
+    }
+
+    bool TxtExport::saveFileAs(std::filesystem::path const& filePath)
+    {
+        std::ofstream stream(filePath, std::ios::out);
+        if (!stream.is_open())
+        {
+            return false;
+        }
+
+        stream << text;
+        return true;
+    }
+} // namespace notes::exporters
\ No newline at end of file
diff --git a/src/main.cpp b/src/main.cpp
index ccbefec..659b1bb 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -16,7 +16,7 @@ int32_t main(int32_t argc, char** argv)
     }
     catch (std::exception e)
     {
-        std::cerr << e.what() << std::endl;
+        libwebview::showMessageDialog("Notes", e.what(), libwebview::MessageDialogType::Error);
         return EXIT_FAILURE;
     }
 }
\ No newline at end of file
diff --git a/src/note_storage.cpp b/src/note_storage.cpp
index 1f834bf..b3eef26 100644
--- a/src/note_storage.cpp
+++ b/src/note_storage.cpp
@@ -1,5 +1,8 @@
 #include "note_storage.hpp"
 #include "precompiled.hpp"
+#include <base64pp/base64pp.h>
+
+#include "exporters/txt.hpp"
 
 namespace notes
 {
@@ -55,7 +58,7 @@ namespace notes
         return true;
     }
 
-    void NoteStorage::saveNoteToDB(Note const& note)
+    int32_t NoteStorage::saveNoteToDB(Note const& note)
     {
         std::string const dbCheckNoteSQL = std::format("SELECT count(*) FROM `notes` WHERE `id`={}", note.noteID);
 
@@ -65,11 +68,7 @@ namespace notes
             std::string const dbAddNoteSQL = std::format(
                 "INSERT INTO `notes` (`name`, `data`) VALUES ('{}', '{}') RETURNING id", note.noteName, note.noteData);
 
-            int32_t const result = database.tryExec(dbAddNoteSQL);
-            if (result != SQLite::OK)
-            {
-                throw std::runtime_error("An error occurred when adding a row to the database");
-            }
+            return database.execAndGet(dbAddNoteSQL).getInt();
         }
         else
         {
@@ -81,6 +80,8 @@ namespace notes
             {
                 throw std::runtime_error("An error occurred when updating a row to the database");
             }
+
+            return note.noteID;
         }
     }
 
@@ -99,7 +100,7 @@ namespace notes
     {
         std::stringstream stream;
         stream << "{\"notes\":[";
-        
+
         bool isFirst = true;
         for (auto const& note : this->loadNotesFromDB())
         {
@@ -113,4 +114,48 @@ namespace notes
         stream << "]}";
         return stream.str();
     }
+
+    bool NoteStorage::exportNoteAsFile(uint32_t const ID, std::string_view const format,
+                                       std::filesystem::path const& filePath)
+    {
+        std::string const dbSelectSQL = std::format("SELECT `name`, `data` FROM `notes` WHERE `id`={}", ID);
+
+        SQLite::Statement query(database, dbSelectSQL);
+
+        Note note;
+        if (query.executeStep())
+        {
+            std::string const noteName = query.getColumn(0).getString();
+            std::string const noteData = query.getColumn(1).getString();
+
+            note.noteID = ID;
+            note.noteName = noteName;
+            note.noteData = noteData;
+        }
+
+        if (query.hasRow())
+        {
+            std::unique_ptr<Exporter> exporter;
+
+            std::string text;
+
+            auto result = base64pp::decode(note.noteData);
+            if (!result.has_value())
+            {
+                text = "";
+            }
+            else
+            {
+                auto decodedData = result.value();
+                text = std::string(reinterpret_cast<char const*>(decodedData.data()), decodedData.size());
+            }
+
+            if (format.compare("txt") == 0)
+            {
+                exporter = std::make_unique<exporters::TxtExport>(text);
+            }
+
+            return exporter->saveFileAs(filePath);
+        }
+    }
 } // namespace notes
\ No newline at end of file
diff --git a/src/view_model.cpp b/src/view_model.cpp
index 886e25b..33f4912 100644
--- a/src/view_model.cpp
+++ b/src/view_model.cpp
@@ -3,13 +3,14 @@
 
 namespace notes
 {
-    ViewModel::ViewModel(libwebview::App* app)
+    ViewModel::ViewModel(libwebview::App* app) : app(app)
     {
         app->bind("getNotes", [this]() -> std::string { return getNotes(); });
-        app->bind("saveNote", [this](uint32_t ID, std::string noteName, std::string noteData) -> void {
-            saveNote(ID, noteName, noteData);
+        app->bind("saveNote", [this](uint32_t ID, std::string noteName, std::string noteData) -> int32_t {
+            return saveNote(ID, noteName, noteData);
         });
         app->bind("removeNote", [this](uint32_t ID) -> void { removeNote(ID); });
+        app->bind("exportNote", [this](uint32_t ID, std::string format) -> void { exportNote(ID, format); });
     }
 
     std::string ViewModel::getNotes()
@@ -17,13 +18,27 @@ namespace notes
         return noteStorage.getNotesData();
     }
 
-    void ViewModel::saveNote(uint32_t ID, std::string noteName, std::string noteData)
+    int32_t ViewModel::saveNote(uint32_t ID, std::string noteName, std::string noteData)
     {
-        noteStorage.saveNoteToDB(Note(ID, noteName, noteData));
+        return noteStorage.saveNoteToDB(Note(ID, noteName, noteData));
     }
 
     void ViewModel::removeNote(uint32_t ID)
     {
         noteStorage.removeNoteFromDB(ID);
     }
+
+    void ViewModel::exportNote(uint32_t ID, std::string format)
+    {
+        auto result = app->showSaveDialog(std::filesystem::current_path(), "");
+        if (result.has_value())
+        {
+            if (!noteStorage.exportNoteAsFile(ID, format, result.value()))
+            {
+                libwebview::showMessageDialog("Notes",
+                                              "Произошла ошибка во время сохранения файла. Попробуйте еще раз!",
+                                              libwebview::MessageDialogType::Error);
+            }
+        }
+    }
 } // namespace notes
\ No newline at end of file
diff --git a/thirdparty/libwebview b/thirdparty/libwebview
index 6767356..6144e20 160000
--- a/thirdparty/libwebview
+++ b/thirdparty/libwebview
@@ -1 +1 @@
-Subproject commit 6767356e2dbe62a068715451652e091d080b5ff9
+Subproject commit 6144e20aaeea3c5ab7d34be89accbf0c85c0c5e5
diff --git a/ui/dist/app.bundle.js b/ui/dist/app.bundle.js
index e531414..5151714 100644
--- a/ui/dist/app.bundle.js
+++ b/ui/dist/app.bundle.js
@@ -16,7 +16,7 @@
 /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
 
 "use strict";
-eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var marked__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! marked */ \"./node_modules/marked/lib/marked.esm.js\");\n/* harmony import */ var ctxmenu__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ctxmenu */ \"./node_modules/ctxmenu/index.js\");\n/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\");\n/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(jquery__WEBPACK_IMPORTED_MODULE_2__);\n\n\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({\n  emits: ['save'],\n  data() {\n    return {\n      isOpenEditor: false,\n      isPreview: false,\n      content: \"\",\n      title: \"\",\n      id: -1\n    };\n  },\n  computed: {\n    previewText() {\n      return (0,marked__WEBPACK_IMPORTED_MODULE_0__.marked)(this.content);\n    }\n  },\n  mounted() {},\n  methods: {\n    onFocusInput(e) {\n      jquery__WEBPACK_IMPORTED_MODULE_2___default()('.header-input-container input').attr('placeholder', 'Заголовок');\n      this.isOpenEditor = true;\n    },\n    onCloseClick(e) {\n      jquery__WEBPACK_IMPORTED_MODULE_2___default()('.header-input-container input').attr('placeholder', 'Придумайте что написать...');\n      this.title = \"\";\n      this.content = \"\";\n      this.isOpenEditor = false;\n      this.isPreview = false;\n      this.id = -1;\n    },\n    onSaveClick(e) {\n      this.$emit('save', {\n        'id': this.id,\n        'title': this.title,\n        'content': this.content\n      });\n      this.onCloseClick();\n    },\n    onExportClick(e) {\n      e.stopPropagation();\n      ctxmenu__WEBPACK_IMPORTED_MODULE_1__.ctxmenu.show([{\n        text: \"Экспорт\"\n      }, {\n        text: \".TXT\",\n        action: () => alert(\"Hello World!\")\n      }, {\n        text: \".PDF\",\n        action: () => alert(\"Hello World!\")\n      }, {\n        text: \".DOCX\",\n        action: () => alert(\"Hello World!\")\n      }], e.target);\n    },\n    open(preview) {\n      this.isOpenEditor = true;\n      this.isPreview = preview;\n    },\n    isOpen() {\n      return this.isOpenEditor;\n    },\n    setID(id) {\n      this.id = id;\n    },\n    setTitle(title) {\n      this.title = title;\n    },\n    setContent(content) {\n      this.content = content;\n    },\n    onSwitchVisibleClick(e) {\n      this.isPreview = !this.isPreview;\n    },\n    onSwitchVisibleEditorClick(e) {\n      const containerElement = jquery__WEBPACK_IMPORTED_MODULE_2___default()('.markdown-container');\n      if (e.offsetX >= containerElement.width() - 15 || e.offsetY >= containerElement.height() - 15) {\n        return;\n      }\n      this.isPreview = false;\n    }\n  }\n});\n\n//# sourceURL=webpack://ui/./src/components/editor.vue?./node_modules/babel-loader/lib/index.js!./node_modules/vue-loader/dist/index.js??ruleSet%5B1%5D.rules%5B7%5D.use%5B0%5D");
+eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var marked__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! marked */ \"./node_modules/marked/lib/marked.esm.js\");\n/* harmony import */ var ctxmenu__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ctxmenu */ \"./node_modules/ctxmenu/index.js\");\n/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\");\n/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(jquery__WEBPACK_IMPORTED_MODULE_2__);\n\n\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({\n  emits: ['save'],\n  data() {\n    return {\n      isOpenEditor: false,\n      isPreview: false,\n      isChanged: false,\n      content: \"\",\n      title: \"\",\n      id: -1\n    };\n  },\n  computed: {\n    previewText() {\n      return (0,marked__WEBPACK_IMPORTED_MODULE_0__.marked)(this.content);\n    }\n  },\n  watch: {\n    content: {\n      handler(value, oldValue) {\n        this.isChanged = true;\n      }\n    },\n    title: {\n      handler(value, oldValue) {\n        this.isChanged = true;\n      }\n    }\n  },\n  methods: {\n    onFocusInput(e) {\n      jquery__WEBPACK_IMPORTED_MODULE_2___default()('.header-input-container input').attr('placeholder', 'Заголовок');\n      this.isOpenEditor = true;\n    },\n    onCloseClick(e) {\n      jquery__WEBPACK_IMPORTED_MODULE_2___default()('.header-input-container input').attr('placeholder', 'Придумайте что написать...');\n      this.title = \"\";\n      this.content = \"\";\n      this.isOpenEditor = false;\n      this.isPreview = false;\n      this.id = -1;\n    },\n    onSaveClick(e) {\n      webview.invoke('saveNote', this.id, this.title, btoa(unescape(encodeURIComponent(this.content)))).then(id => this.id = id);\n      this.$emit('save', e);\n      this.isChanged = false;\n    },\n    onExportClick(e) {\n      e.stopPropagation();\n      ctxmenu__WEBPACK_IMPORTED_MODULE_1__.ctxmenu.show([{\n        text: \"Экспорт\"\n      }, {\n        text: \".TXT\",\n        action: () => webview.invoke('exportNote', this.id, 'txt')\n      }], e.target);\n    },\n    open(preview) {\n      this.isOpenEditor = true;\n      this.isPreview = preview;\n    },\n    isOpen() {\n      return this.isOpenEditor;\n    },\n    setID(id) {\n      this.id = id;\n    },\n    setTitle(title) {\n      this.title = title;\n      this.isChanged = false;\n    },\n    setContent(content) {\n      this.content = content;\n      this.isChanged = false;\n    },\n    onSwitchVisibleClick(e) {\n      this.isPreview = !this.isPreview;\n    },\n    onSwitchVisibleEditorClick(e) {\n      const containerElement = jquery__WEBPACK_IMPORTED_MODULE_2___default()('.markdown-container');\n      if (e.offsetX >= containerElement.width() - 15 || e.offsetY >= containerElement.height() - 15) {\n        return;\n      }\n      this.isPreview = false;\n    }\n  }\n});\n\n//# sourceURL=webpack://ui/./src/components/editor.vue?./node_modules/babel-loader/lib/index.js!./node_modules/vue-loader/dist/index.js??ruleSet%5B1%5D.rules%5B7%5D.use%5B0%5D");
 
 /***/ }),
 
@@ -27,7 +27,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac
 /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
 
 "use strict";
-eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var marked__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! marked */ \"./node_modules/marked/lib/marked.esm.js\");\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({\n  emits: ['click', 'context'],\n  props: {\n    title: String,\n    data: String\n  },\n  computed: {\n    description() {\n      return (0,marked__WEBPACK_IMPORTED_MODULE_0__.marked)(this.data);\n    }\n  },\n  methods: {\n    onContextClick(e) {\n      this.$emit('context', e);\n    }\n  }\n});\n\n//# sourceURL=webpack://ui/./src/components/note.vue?./node_modules/babel-loader/lib/index.js!./node_modules/vue-loader/dist/index.js??ruleSet%5B1%5D.rules%5B7%5D.use%5B0%5D");
+eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var marked__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! marked */ \"./node_modules/marked/lib/marked.esm.js\");\n/* harmony import */ var ctxmenu__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ctxmenu */ \"./node_modules/ctxmenu/index.js\");\n\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({\n  emits: ['note-click', 'menu-click'],\n  props: {\n    id: Number,\n    title: String,\n    data: String\n  },\n  computed: {\n    description() {\n      return (0,marked__WEBPACK_IMPORTED_MODULE_0__.marked)(this.data);\n    }\n  },\n  methods: {\n    onMenuClick(e) {\n      e.stopPropagation();\n      ctxmenu__WEBPACK_IMPORTED_MODULE_1__.ctxmenu.show([{\n        text: \"Удалить\",\n        action: () => {\n          webview.invoke('removeNote', this.id).then(() => this.$emit('menu-click', e));\n        }\n      }], e.target);\n    }\n  }\n});\n\n//# sourceURL=webpack://ui/./src/components/note.vue?./node_modules/babel-loader/lib/index.js!./node_modules/vue-loader/dist/index.js??ruleSet%5B1%5D.rules%5B7%5D.use%5B0%5D");
 
 /***/ }),
 
@@ -38,7 +38,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac
 /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
 
 "use strict";
-eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\");\n/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(jquery__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var ctxmenu__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ctxmenu */ \"./node_modules/ctxmenu/index.js\");\n/* harmony import */ var _components_note_vue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../components/note.vue */ \"./src/components/note.vue\");\n/* harmony import */ var _components_editor_vue__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../components/editor.vue */ \"./src/components/editor.vue\");\n\n\n\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({\n  components: {\n    'note': _components_note_vue__WEBPACK_IMPORTED_MODULE_2__[\"default\"],\n    'editor': _components_editor_vue__WEBPACK_IMPORTED_MODULE_3__[\"default\"]\n  },\n  data() {\n    return {\n      isReady: true,\n      notes: []\n    };\n  },\n  created() {\n    jquery__WEBPACK_IMPORTED_MODULE_0___default()(window).on('resize', e => {\n      this.updateNoteGroupHeight(e.target.outerHeight);\n    });\n  },\n  destroyed() {\n    jquery__WEBPACK_IMPORTED_MODULE_0___default()(window).off('resize');\n  },\n  mounted() {\n    this.updateNoteList();\n  },\n  methods: {\n    updateNoteGroupHeight(windowHeight) {\n      jquery__WEBPACK_IMPORTED_MODULE_0___default()('.notegr-container').css('height', windowHeight - (jquery__WEBPACK_IMPORTED_MODULE_0___default()('.editor-container').height() + 80));\n    },\n    updateNoteList() {\n      this.isReady = false;\n      this.notes.splice(0, this.notes.length);\n      webview.invoke('getNotes').then(data => {\n        for (const note of Object.values(data.notes)) {\n          const noteData = {\n            'id': note.id,\n            'title': note.name,\n            'content': decodeURIComponent(escape(atob(note.data)))\n          };\n          this.notes.push(noteData);\n        }\n        this.isReady = true;\n        this.$nextTick(() => {\n          this.updateNoteGroupHeight(jquery__WEBPACK_IMPORTED_MODULE_0___default()(window).outerHeight());\n        });\n      });\n    },\n    onNoteClick(e, index) {\n      const editor = this.$refs.editor;\n      if (editor.isOpen()) {\n        return;\n      }\n      editor.open(true);\n      editor.setID(this.notes[index].id);\n      editor.setTitle(this.notes[index].title);\n      editor.setContent(this.notes[index].content);\n    },\n    onNoteContextClick(e, index) {\n      e.stopPropagation();\n      ctxmenu__WEBPACK_IMPORTED_MODULE_1__.ctxmenu.show([{\n        text: \"Удалить\",\n        action: () => {\n          webview.invoke('removeNote', this.notes[index].id).then(() => {\n            this.updateNoteList();\n          });\n        }\n      }], e.target);\n    },\n    onEditorSaveClick(e) {\n      this.isReady = false;\n      webview.invoke('saveNote', e.id, e.title, btoa(unescape(encodeURIComponent(e.content)))).then(() => {\n        this.updateNoteList();\n      });\n    }\n  }\n});\n\n//# sourceURL=webpack://ui/./src/views/app.vue?./node_modules/babel-loader/lib/index.js!./node_modules/vue-loader/dist/index.js??ruleSet%5B1%5D.rules%5B7%5D.use%5B0%5D");
+eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\");\n/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(jquery__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _components_note_vue__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../components/note.vue */ \"./src/components/note.vue\");\n/* harmony import */ var _components_editor_vue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../components/editor.vue */ \"./src/components/editor.vue\");\n\n\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({\n  components: {\n    'note': _components_note_vue__WEBPACK_IMPORTED_MODULE_1__[\"default\"],\n    'editor': _components_editor_vue__WEBPACK_IMPORTED_MODULE_2__[\"default\"]\n  },\n  data() {\n    return {\n      isReady: true,\n      notes: []\n    };\n  },\n  created() {\n    jquery__WEBPACK_IMPORTED_MODULE_0___default()(window).on('resize', e => {\n      this.updateNoteGroupHeight(e.target.outerHeight);\n    });\n  },\n  destroyed() {\n    jquery__WEBPACK_IMPORTED_MODULE_0___default()(window).off('resize');\n  },\n  mounted() {\n    this.updateNoteList();\n    this.$nextTick(() => {\n      this.updateNoteGroupHeight(jquery__WEBPACK_IMPORTED_MODULE_0___default()(window).outerHeight());\n    });\n  },\n  methods: {\n    updateNoteGroupHeight(windowHeight) {\n      jquery__WEBPACK_IMPORTED_MODULE_0___default()('.notegr-container').css('height', windowHeight - (jquery__WEBPACK_IMPORTED_MODULE_0___default()('.editor-container').height() + 80));\n    },\n    updateNoteList() {\n      this.isReady = false;\n      this.notes.splice(0, this.notes.length);\n      webview.invoke('getNotes').then(data => {\n        for (const note of Object.values(data.notes)) {\n          const noteData = {\n            'id': note.id,\n            'title': note.name,\n            'content': decodeURIComponent(escape(atob(note.data)))\n          };\n          this.notes.push(noteData);\n        }\n        this.isReady = true;\n      });\n    },\n    onNoteClick(e, index) {\n      const editor = this.$refs.editor;\n      if (editor.isOpen()) {\n        return;\n      }\n      editor.open(true);\n      editor.setID(this.notes[index].id);\n      editor.setTitle(this.notes[index].title);\n      editor.setContent(this.notes[index].content);\n    },\n    onNoteMenuClick(e) {\n      this.updateNoteList();\n    },\n    onEditorSaveClick(e) {\n      this.updateNoteList();\n    }\n  }\n});\n\n//# sourceURL=webpack://ui/./src/views/app.vue?./node_modules/babel-loader/lib/index.js!./node_modules/vue-loader/dist/index.js??ruleSet%5B1%5D.rules%5B7%5D.use%5B0%5D");
 
 /***/ }),
 
@@ -49,7 +49,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac
 /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
 
 "use strict";
-eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   render: () => (/* binding */ render)\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm-browser.js\");\n\nconst _hoisted_1 = {\n  class: \"editor-container\"\n};\nconst _hoisted_2 = {\n  class: \"header-input-container\"\n};\nconst _hoisted_3 = {\n  class: \"body-input-container\"\n};\nconst _hoisted_4 = {\n  key: 0,\n  style: {\n    \"display\": \"flex\",\n    \"padding\": \"10px\",\n    \"height\": \"calc(100% - 80px)\"\n  }\n};\nconst _hoisted_5 = [\"innerHTML\"];\nconst _hoisted_6 = {\n  key: 1,\n  style: {\n    \"display\": \"flex\",\n    \"padding\": \"10px\",\n    \"height\": \"calc(100% - 80px)\"\n  }\n};\nconst _hoisted_7 = {\n  class: \"footer-input-container\"\n};\nconst _hoisted_8 = {\n  style: {\n    \"display\": \"flex\",\n    \"flex-direction\": \"row\",\n    \"align-items\": \"center\",\n    \"width\": \"100%\"\n  }\n};\nconst _hoisted_9 = {\n  style: {\n    \"display\": \"flex\",\n    \"flex-direction\": \"row\",\n    \"gap\": \"10px\"\n  }\n};\nconst _hoisted_10 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"object\", {\n  style: {\n    \"pointer-events\": \"none\"\n  },\n  data: \"images/eye.svg\",\n  width: \"20\",\n  height: \"20\"\n}, null, -1 /* HOISTED */);\nconst _hoisted_11 = [_hoisted_10];\nconst _hoisted_12 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"object\", {\n  style: {\n    \"pointer-events\": \"none\"\n  },\n  data: \"images/file-export.svg\",\n  width: \"20\",\n  height: \"20\"\n}, null, -1 /* HOISTED */);\nconst _hoisted_13 = [_hoisted_12];\nconst _hoisted_14 = {\n  style: {\n    \"display\": \"flex\",\n    \"flex-direction\": \"row\",\n    \"margin-left\": \"auto\"\n  }\n};\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n  return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_1, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_2, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n    type: \"text\",\n    placeholder: \"Придумайте что написать...\",\n    \"onUpdate:modelValue\": _cache[0] || (_cache[0] = $event => $data.title = $event),\n    onFocus: _cache[1] || (_cache[1] = $event => $options.onFocusInput($event))\n  }, null, 544 /* NEED_HYDRATION, NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $data.title]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_3, [$data.isPreview ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_4, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", {\n    class: \"markdown-container\",\n    innerHTML: $options.previewText,\n    onMousedown: _cache[2] || (_cache[2] = $event => $options.onSwitchVisibleEditorClick($event))\n  }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_5)])) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_6, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"textarea\", {\n    placeholder: \"Заметка...\",\n    \"onUpdate:modelValue\": _cache[3] || (_cache[3] = $event => $data.content = $event)\n  }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $data.content]])])), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_7, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_8, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_9, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n    title: \"Предпросмотр\",\n    class: \"btn-icon circle\",\n    style: {\n      \"width\": \"36px\",\n      \"height\": \"36px\"\n    },\n    onClick: _cache[4] || (_cache[4] = $event => $options.onSwitchVisibleClick($event))\n  }, [..._hoisted_11]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n    id: \"export-btn\",\n    title: \"Экспорт\",\n    class: \"btn-icon circle\",\n    style: {\n      \"width\": \"36px\",\n      \"height\": \"36px\"\n    },\n    onClick: _cache[5] || (_cache[5] = $event => $options.onExportClick($event))\n  }, [..._hoisted_13])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_14, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n    class: \"btn-text\",\n    onClick: _cache[6] || (_cache[6] = $event => $options.onSaveClick($event)),\n    style: {\n      \"font-weight\": \"bold\"\n    }\n  }, \"Сохранить\", 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vShow, this.title.length > 0]]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n    class: \"btn-text\",\n    onClick: _cache[7] || (_cache[7] = $event => $options.onCloseClick($event)),\n    style: {\n      \"font-weight\": \"bold\"\n    }\n  }, \"Закрыть\")])])])], 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vShow, $data.isOpenEditor]])]);\n}\n\n//# sourceURL=webpack://ui/./src/components/editor.vue?./node_modules/babel-loader/lib/index.js!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B2%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B1%5D.rules%5B7%5D.use%5B0%5D");
+eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   render: () => (/* binding */ render)\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm-browser.js\");\n\nconst _hoisted_1 = {\n  class: \"editor-container\"\n};\nconst _hoisted_2 = {\n  class: \"header-input-container\"\n};\nconst _hoisted_3 = {\n  class: \"body-input-container\"\n};\nconst _hoisted_4 = {\n  key: 0,\n  style: {\n    \"display\": \"flex\",\n    \"padding\": \"10px\",\n    \"height\": \"calc(100% - 80px)\"\n  }\n};\nconst _hoisted_5 = [\"innerHTML\"];\nconst _hoisted_6 = {\n  key: 1,\n  style: {\n    \"display\": \"flex\",\n    \"padding\": \"10px\",\n    \"height\": \"calc(100% - 80px)\"\n  }\n};\nconst _hoisted_7 = {\n  class: \"footer-input-container\"\n};\nconst _hoisted_8 = {\n  style: {\n    \"display\": \"flex\",\n    \"flex-direction\": \"row\",\n    \"align-items\": \"center\",\n    \"width\": \"100%\"\n  }\n};\nconst _hoisted_9 = {\n  style: {\n    \"display\": \"flex\",\n    \"flex-direction\": \"row\",\n    \"gap\": \"10px\"\n  }\n};\nconst _hoisted_10 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"object\", {\n  style: {\n    \"pointer-events\": \"none\"\n  },\n  data: \"images/eye.svg\",\n  width: \"20\",\n  height: \"20\"\n}, null, -1 /* HOISTED */);\nconst _hoisted_11 = [_hoisted_10];\nconst _hoisted_12 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"object\", {\n  style: {\n    \"pointer-events\": \"none\"\n  },\n  data: \"images/file-export.svg\",\n  width: \"20\",\n  height: \"20\"\n}, null, -1 /* HOISTED */);\nconst _hoisted_13 = [_hoisted_12];\nconst _hoisted_14 = {\n  style: {\n    \"display\": \"flex\",\n    \"flex-direction\": \"row\",\n    \"margin-left\": \"auto\"\n  }\n};\nconst _hoisted_15 = [\"disabled\"];\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n  return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_1, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_2, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n    type: \"text\",\n    placeholder: \"Придумайте что написать...\",\n    \"onUpdate:modelValue\": _cache[0] || (_cache[0] = $event => $data.title = $event),\n    onFocus: _cache[1] || (_cache[1] = $event => $options.onFocusInput($event))\n  }, null, 544 /* NEED_HYDRATION, NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $data.title]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_3, [$data.isPreview ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_4, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", {\n    class: \"markdown-container\",\n    innerHTML: $options.previewText,\n    onMousedown: _cache[2] || (_cache[2] = $event => $options.onSwitchVisibleEditorClick($event))\n  }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_5)])) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_6, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"textarea\", {\n    placeholder: \"Заметка...\",\n    \"onUpdate:modelValue\": _cache[3] || (_cache[3] = $event => $data.content = $event)\n  }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $data.content]])])), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_7, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_8, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_9, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n    title: \"Предпросмотр\",\n    class: \"btn-icon circle\",\n    style: {\n      \"width\": \"36px\",\n      \"height\": \"36px\"\n    },\n    onClick: _cache[4] || (_cache[4] = $event => $options.onSwitchVisibleClick($event))\n  }, [..._hoisted_11]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n    id: \"export-btn\",\n    title: \"Экспорт\",\n    class: \"btn-icon circle\",\n    style: {\n      \"width\": \"36px\",\n      \"height\": \"36px\"\n    },\n    onClick: _cache[5] || (_cache[5] = $event => $options.onExportClick($event))\n  }, [..._hoisted_13], 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vShow, this.id != -1]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_14, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n    class: \"btn-text\",\n    onClick: _cache[6] || (_cache[6] = $event => $options.onSaveClick($event)),\n    style: {\n      \"font-weight\": \"bold\"\n    },\n    disabled: !$data.isChanged || this.title.length == 0\n  }, \"Сохранить\", 8 /* PROPS */, _hoisted_15), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n    class: \"btn-text\",\n    onClick: _cache[7] || (_cache[7] = $event => $options.onCloseClick($event)),\n    style: {\n      \"font-weight\": \"bold\"\n    }\n  }, \"Закрыть\")])])])], 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vShow, $data.isOpenEditor]])]);\n}\n\n//# sourceURL=webpack://ui/./src/components/editor.vue?./node_modules/babel-loader/lib/index.js!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B2%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B1%5D.rules%5B7%5D.use%5B0%5D");
 
 /***/ }),
 
@@ -60,7 +60,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac
 /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
 
 "use strict";
-eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   render: () => (/* binding */ render)\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm-browser.js\");\n\nconst _hoisted_1 = {\n  class: \"note-container\"\n};\nconst _hoisted_2 = {\n  class: \"note-header-container\"\n};\nconst _hoisted_3 = {\n  style: {\n    \"display\": \"flex\",\n    \"flex-direction\": \"row\",\n    \"margin-left\": \"auto\"\n  }\n};\nconst _hoisted_4 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"object\", {\n  style: {\n    \"pointer-events\": \"none\"\n  },\n  data: \"images/ellipsis.svg\",\n  width: \"16\",\n  height: \"16\"\n}, null, -1 /* HOISTED */);\nconst _hoisted_5 = [_hoisted_4];\nconst _hoisted_6 = [\"innerHTML\"];\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n  return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_1, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_2, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n    class: \"note-title\",\n    onMousedown: _cache[0] || (_cache[0] = $event => _ctx.$emit('click', $event))\n  }, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($props.title), 33 /* TEXT, NEED_HYDRATION */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_3, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n    class: \"btn-icon\",\n    onClick: _cache[1] || (_cache[1] = $event => $options.onContextClick($event))\n  }, [..._hoisted_5])])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", {\n    class: \"note-body-container\",\n    innerHTML: $options.description,\n    onMousedown: _cache[2] || (_cache[2] = $event => _ctx.$emit('click', $event))\n  }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_6)]);\n}\n\n//# sourceURL=webpack://ui/./src/components/note.vue?./node_modules/babel-loader/lib/index.js!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B2%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B1%5D.rules%5B7%5D.use%5B0%5D");
+eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   render: () => (/* binding */ render)\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm-browser.js\");\n\nconst _hoisted_1 = {\n  class: \"note-container\"\n};\nconst _hoisted_2 = {\n  class: \"note-header-container\"\n};\nconst _hoisted_3 = {\n  style: {\n    \"display\": \"flex\",\n    \"flex-direction\": \"row\",\n    \"margin-left\": \"auto\"\n  }\n};\nconst _hoisted_4 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"object\", {\n  style: {\n    \"pointer-events\": \"none\"\n  },\n  data: \"images/ellipsis.svg\",\n  width: \"16\",\n  height: \"16\"\n}, null, -1 /* HOISTED */);\nconst _hoisted_5 = [_hoisted_4];\nconst _hoisted_6 = [\"innerHTML\"];\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n  return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_1, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_2, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n    class: \"note-title\",\n    onMousedown: _cache[0] || (_cache[0] = $event => _ctx.$emit('note-click', $event))\n  }, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($props.title), 33 /* TEXT, NEED_HYDRATION */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_3, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n    class: \"btn-icon\",\n    onClick: _cache[1] || (_cache[1] = $event => $options.onMenuClick($event))\n  }, [..._hoisted_5])])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", {\n    class: \"note-body-container\",\n    innerHTML: $options.description,\n    onMousedown: _cache[2] || (_cache[2] = $event => _ctx.$emit('note-click', $event))\n  }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_6)]);\n}\n\n//# sourceURL=webpack://ui/./src/components/note.vue?./node_modules/babel-loader/lib/index.js!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B2%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B1%5D.rules%5B7%5D.use%5B0%5D");
 
 /***/ }),
 
@@ -71,7 +71,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac
 /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
 
 "use strict";
-eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   render: () => (/* binding */ render)\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm-browser.js\");\n\nconst _hoisted_1 = {\n  class: \"wrapper-container\"\n};\nconst _hoisted_2 = {\n  key: 0,\n  class: \"notegr-container\"\n};\nconst _hoisted_3 = {\n  key: 1,\n  class: \"notegr-loading-container\"\n};\nconst _hoisted_4 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", {\n  class: \"loader\"\n}, null, -1 /* HOISTED */);\nconst _hoisted_5 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"Загрузка заметок...\", -1 /* HOISTED */);\nconst _hoisted_6 = [_hoisted_4, _hoisted_5];\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n  const _component_editor = (0,vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent)(\"editor\");\n  const _component_note = (0,vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent)(\"note\");\n  return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_1, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)(_component_editor, {\n    ref: \"editor\",\n    onSave: _cache[0] || (_cache[0] = $event => $options.onEditorSaveClick($event))\n  }, null, 512 /* NEED_PATCH */), $data.isReady ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_2, [((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.renderList)($data.notes, (note, index) => {\n    return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)(_component_note, {\n      title: note.title,\n      data: note.content,\n      onClick: $event => $options.onNoteClick($event, index),\n      onContext: $event => $options.onNoteContextClick($event, index)\n    }, null, 8 /* PROPS */, [\"title\", \"data\", \"onClick\", \"onContext\"]);\n  }), 256 /* UNKEYED_FRAGMENT */))])) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_3, [..._hoisted_6]))]);\n}\n\n//# sourceURL=webpack://ui/./src/views/app.vue?./node_modules/babel-loader/lib/index.js!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B2%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B1%5D.rules%5B7%5D.use%5B0%5D");
+eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   render: () => (/* binding */ render)\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm-browser.js\");\n\nconst _hoisted_1 = {\n  class: \"wrapper-container\"\n};\nconst _hoisted_2 = {\n  key: 0,\n  class: \"notegr-container\"\n};\nconst _hoisted_3 = {\n  key: 1,\n  class: \"notegr-loading-container\"\n};\nconst _hoisted_4 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", {\n  class: \"loader\"\n}, null, -1 /* HOISTED */);\nconst _hoisted_5 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"Загрузка заметок...\", -1 /* HOISTED */);\nconst _hoisted_6 = [_hoisted_4, _hoisted_5];\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n  const _component_editor = (0,vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent)(\"editor\");\n  const _component_note = (0,vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent)(\"note\");\n  return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_1, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)(_component_editor, {\n    ref: \"editor\",\n    onSave: _cache[0] || (_cache[0] = $event => $options.onEditorSaveClick($event))\n  }, null, 512 /* NEED_PATCH */), $data.isReady ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_2, [((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.renderList)($data.notes, (note, index) => {\n    return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)(_component_note, {\n      id: note.id,\n      title: note.title,\n      data: note.content,\n      onNoteClick: $event => $options.onNoteClick($event, index),\n      onMenuClick: _cache[1] || (_cache[1] = $event => $options.onNoteMenuClick($event))\n    }, null, 8 /* PROPS */, [\"id\", \"title\", \"data\", \"onNoteClick\"]);\n  }), 256 /* UNKEYED_FRAGMENT */))])) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_3, [..._hoisted_6]))]);\n}\n\n//# sourceURL=webpack://ui/./src/views/app.vue?./node_modules/babel-loader/lib/index.js!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B2%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B1%5D.rules%5B7%5D.use%5B0%5D");
 
 /***/ }),
 
diff --git a/ui/src/components/editor.vue b/ui/src/components/editor.vue
index 8fd6d03..86837e5 100644
--- a/ui/src/components/editor.vue
+++ b/ui/src/components/editor.vue
@@ -19,12 +19,12 @@
                             <object style="pointer-events: none;" data="images/eye.svg" width="20" height="20"></object>
                         </button>
 
-                        <button id="export-btn" title="Экспорт" class="btn-icon circle" style="width: 36px; height: 36px;" @click="onExportClick($event)">
+                        <button v-show="this.id != -1" id="export-btn" title="Экспорт" class="btn-icon circle" style="width: 36px; height: 36px;" @click="onExportClick($event)">
                             <object style="pointer-events: none;" data="images/file-export.svg" width="20" height="20"></object>
                         </button>
                     </div>
                     <div style="display: flex; flex-direction: row; margin-left: auto;">
-                        <button v-show="this.title.length > 0" class="btn-text" @click="onSaveClick($event)" style="font-weight: bold;">Сохранить</button>
+                        <button class="btn-text" @click="onSaveClick($event)" style="font-weight: bold;" v-bind:disabled="!isChanged || this.title.length == 0">Сохранить</button>
                         <button class="btn-text" @click="onCloseClick($event)" style="font-weight: bold;">Закрыть</button>
                     </div>
                 </div>
@@ -44,6 +44,7 @@ export default {
         return {
             isOpenEditor: false,
             isPreview: false,
+            isChanged: false,
             content: "",
             title: "",
             id: -1
@@ -54,8 +55,17 @@ export default {
             return marked(this.content);
         }
     },
-    mounted() {
-        
+    watch: {
+        content: {
+            handler(value, oldValue) {
+                this.isChanged = true;
+            }
+        },
+        title: {
+            handler(value, oldValue) {
+                this.isChanged = true;
+            }
+        }
     },
     methods: {
         onFocusInput(e) {
@@ -74,16 +84,16 @@ export default {
             this.id = -1;
         },
         onSaveClick(e) {
-            this.$emit('save', { 'id': this.id, 'title': this.title, 'content': this.content });
-            this.onCloseClick();
+            webview.invoke('saveNote', this.id, this.title, btoa(unescape(encodeURIComponent(this.content)))).then(
+                id => this.id = id);
+            this.$emit('save', e);
+            this.isChanged = false;
         },
         onExportClick(e) {
             e.stopPropagation();
             ctxmenu.show([
                 { text: "Экспорт" }, 
-                { text: ".TXT", action: () => alert("Hello World!") }, 
-                { text: ".PDF", action: () => alert("Hello World!") },
-                { text: ".DOCX", action: () => alert("Hello World!") },
+                { text: ".TXT", action: () => webview.invoke('exportNote', this.id, 'txt') }
             ], e.target);
         },
         open(preview) {
@@ -98,9 +108,11 @@ export default {
         },
         setTitle(title) {
             this.title = title;
+            this.isChanged = false;
         },
         setContent(content) {
             this.content = content;
+            this.isChanged = false;
         },
         onSwitchVisibleClick(e) {
             this.isPreview = !this.isPreview;
diff --git a/ui/src/components/note.vue b/ui/src/components/note.vue
index 65092b2..66183e8 100644
--- a/ui/src/components/note.vue
+++ b/ui/src/components/note.vue
@@ -1,25 +1,27 @@
 <template>
     <div class="note-container">
         <div class="note-header-container">
-            <span class="note-title" @mousedown="$emit('click', $event)">{{ title }}</span>
+            <span class="note-title" @mousedown="$emit('note-click', $event)">{{ title }}</span>
 
             <div style="display: flex; flex-direction: row; margin-left: auto;">
-                <button class="btn-icon" @click="onContextClick($event)">
+                <button class="btn-icon" @click="onMenuClick($event)">
                     <object style="pointer-events: none;" data="images/ellipsis.svg" width="16" height="16"></object>
                 </button>
             </div>
         </div>
 
-        <div class="note-body-container" v-html="description" @mousedown="$emit('click', $event)"></div>
+        <div class="note-body-container" v-html="description" @mousedown="$emit('note-click', $event)"></div>
     </div>
 </template>
 
 <script>
-import {marked} from 'marked'
+import {marked} from 'marked';
+import { ctxmenu } from 'ctxmenu';
 
 export default {
-    emits: ['click', 'context'],
+    emits: ['note-click', 'menu-click'],
     props: {
+        id: Number,
         title: String,
         data: String
     },
@@ -29,9 +31,12 @@ export default {
         }
     },
     methods: {
-        onContextClick(e) {
-            this.$emit('context', e);
-        }
+        onMenuClick(e) {
+            e.stopPropagation();
+            ctxmenu.show([
+                { text: "Удалить", action: () => {
+                    webview.invoke('removeNote', this.id).then(() => this.$emit('menu-click', e)); } }], e.target);
+        },
     }
 }
 </script>
diff --git a/ui/src/views/app.vue b/ui/src/views/app.vue
index 45f8905..de7d7ac 100644
--- a/ui/src/views/app.vue
+++ b/ui/src/views/app.vue
@@ -4,7 +4,12 @@
 
         <div v-if="isReady" class="notegr-container">
             <note v-for="(note, index) in notes" 
-                v-bind:title="note.title" v-bind:data="note.content" @click="onNoteClick($event, index)" @context="onNoteContextClick($event, index)"></note>
+                v-bind:id="note.id" 
+                v-bind:title="note.title" 
+                v-bind:data="note.content" 
+                @note-click="onNoteClick($event, index)" 
+                @menu-click="onNoteMenuClick($event)">
+            </note>
         </div>
 
         <div v-else class="notegr-loading-container">
@@ -16,7 +21,6 @@
 
 <script>
 import $ from 'jquery';
-import { ctxmenu } from 'ctxmenu';
 
 import NoteComponent from '../components/note.vue';
 import EditorComponent from '../components/editor.vue';
@@ -40,6 +44,7 @@ export default {
     },
     mounted() {
         this.updateNoteList();
+        this.$nextTick(() => { this.updateNoteGroupHeight($(window).outerHeight()); });
     },
     methods: {
         updateNoteGroupHeight(windowHeight) {
@@ -60,7 +65,6 @@ export default {
                     this.notes.push(noteData);
                 }
                 this.isReady = true;
-                this.$nextTick(() => { this.updateNoteGroupHeight($(window).outerHeight()); });
             });
         },
         onNoteClick(e, index) {
@@ -76,17 +80,11 @@ export default {
             editor.setTitle(this.notes[index].title);
             editor.setContent(this.notes[index].content);
         },
-        onNoteContextClick(e, index) {
-            e.stopPropagation();
-            ctxmenu.show([
-                { text: "Удалить", action: () => {
-                    webview.invoke('removeNote', this.notes[index].id).then(() => { this.updateNoteList(); }); } }], e.target);
+        onNoteMenuClick(e) {
+            this.updateNoteList();
         },
         onEditorSaveClick(e) {
-            this.isReady = false;
-
-            webview.invoke('saveNote', e.id, e.title, btoa(unescape(encodeURIComponent(e.content))))
-                .then(() => { this.updateNoteList(); });
+            this.updateNoteList();
         }
     }
 }