mirror of
https://github.com/arabine/open-story-teller.git
synced 2025-12-06 17:09:06 +01:00
(WIP library store manager)
This commit is contained in:
parent
3669841045
commit
001034db61
9 changed files with 244 additions and 42 deletions
2
.github/workflows/story_editor.yml
vendored
2
.github/workflows/story_editor.yml
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
name: BuildStoryEditor-Linux
|
name: build-story-editor
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -79,3 +79,5 @@ story-editor/buildxcode/
|
||||||
story-editor/cmake-build-debug/
|
story-editor/cmake-build-debug/
|
||||||
|
|
||||||
story-editor/build-win32/
|
story-editor/build-win32/
|
||||||
|
|
||||||
|
build-story-editor-Desktop_Qt_GCC_64bit-Debug/
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,24 @@ endif()
|
||||||
|
|
||||||
find_package(OpenGL REQUIRED)
|
find_package(OpenGL REQUIRED)
|
||||||
|
|
||||||
|
find_package(OpenSSL REQUIRED)
|
||||||
|
|
||||||
set(IMGUI_VERSION 1.90)
|
set(IMGUI_VERSION 1.90)
|
||||||
|
|
||||||
include(FetchContent)
|
include(FetchContent)
|
||||||
|
|
||||||
|
# =========================================================================================================================
|
||||||
|
# CURL
|
||||||
|
# =========================================================================================================================
|
||||||
|
FetchContent_Declare(curl
|
||||||
|
URL https://github.com/curl/curl/archive/refs/tags/curl-8_6_0.zip
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
set(BUILD_CURL_EXE FALSE)
|
||||||
|
set(BUILD_STATIC_LIBS TRUE)
|
||||||
|
FetchContent_MakeAvailable(curl)
|
||||||
|
|
||||||
# =========================================================================================================================
|
# =========================================================================================================================
|
||||||
# IMGUI and plugins
|
# IMGUI and plugins
|
||||||
# =========================================================================================================================
|
# =========================================================================================================================
|
||||||
|
|
@ -183,7 +197,6 @@ else()
|
||||||
add_executable(${STORY_EDITOR_PROJECT}
|
add_executable(${STORY_EDITOR_PROJECT}
|
||||||
${SRCS}
|
${SRCS}
|
||||||
|
|
||||||
|
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
@ -195,6 +208,8 @@ target_include_directories(${STORY_EDITOR_PROJECT} PUBLIC
|
||||||
libs/ImGuiFileDialog
|
libs/ImGuiFileDialog
|
||||||
libs/imgui-node-editor
|
libs/imgui-node-editor
|
||||||
|
|
||||||
|
${curl_INCLUDE_DIR}
|
||||||
|
|
||||||
../software/library/
|
../software/library/
|
||||||
../software/chip32/
|
../software/chip32/
|
||||||
../software/common
|
../software/common
|
||||||
|
|
@ -228,7 +243,7 @@ target_compile_definitions(${STORY_EDITOR_PROJECT} PUBLIC cimg_display=0)
|
||||||
|
|
||||||
target_compile_definitions(${STORY_EDITOR_PROJECT} PUBLIC "$<$<CONFIG:DEBUG>:DEBUG>")
|
target_compile_definitions(${STORY_EDITOR_PROJECT} PUBLIC "$<$<CONFIG:DEBUG>:DEBUG>")
|
||||||
|
|
||||||
target_link_directories(${STORY_EDITOR_PROJECT} PUBLIC ${sdl2_BINARY_DIR})
|
target_link_directories(${STORY_EDITOR_PROJECT} PUBLIC ${sdl2_BINARY_DIR} ${curl_BINARY_DIR})
|
||||||
message(${sdl2_BINARY_DIR})
|
message(${sdl2_BINARY_DIR})
|
||||||
set(SDL2_BIN_DIR ${sdl2_BINARY_DIR})
|
set(SDL2_BIN_DIR ${sdl2_BINARY_DIR})
|
||||||
|
|
||||||
|
|
@ -238,6 +253,8 @@ if(UNIX)
|
||||||
OpenGL::GL
|
OpenGL::GL
|
||||||
dl
|
dl
|
||||||
SDL2
|
SDL2
|
||||||
|
libcurl_static
|
||||||
|
OpenSSL::SSL OpenSSL::Crypto
|
||||||
)
|
)
|
||||||
elseif(WIN32)
|
elseif(WIN32)
|
||||||
target_link_libraries(${STORY_EDITOR_PROJECT}
|
target_link_libraries(${STORY_EDITOR_PROJECT}
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,20 @@ struct Connection
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
~Connection() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
unsigned int outNodeId{0};
|
unsigned int outNodeId{0};
|
||||||
unsigned int outPortIndex{0};
|
unsigned int outPortIndex{0};
|
||||||
unsigned int inNodeId{0};
|
unsigned int inNodeId{0};
|
||||||
unsigned int inPortIndex{0};
|
unsigned int inPortIndex{0};
|
||||||
|
|
||||||
|
Connection(const Connection &other){
|
||||||
|
*this = other;
|
||||||
|
}
|
||||||
|
|
||||||
Connection& operator=(const Connection& other) {
|
Connection& operator=(const Connection& other) {
|
||||||
this->outNodeId = other.outNodeId;
|
this->outNodeId = other.outNodeId;
|
||||||
this->outPortIndex = other.outPortIndex;
|
this->outPortIndex = other.outPortIndex;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ your use of the corresponding standard functions.
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
#include "imgui_impl_sdl2.h"
|
#include "imgui_impl_sdl2.h"
|
||||||
#include "imgui_impl_sdlrenderer2.h"
|
#include "imgui_impl_sdlrenderer2.h"
|
||||||
|
|
@ -121,7 +122,7 @@ std::string GetDirectory (const std::string& path)
|
||||||
|
|
||||||
Gui::Gui()
|
Gui::Gui()
|
||||||
{
|
{
|
||||||
m_executablePath = GetDirectory(pf::getExecutablePath());
|
m_executablePath = std::filesystem::current_path();
|
||||||
std::cout << "PATH: " << m_executablePath << std::endl;
|
std::cout << "PATH: " << m_executablePath << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,62 @@
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include "IconsMaterialDesignIcons.h"
|
#include "IconsMaterialDesignIcons.h"
|
||||||
|
|
||||||
|
#define CPPHTTPLIB_OPENSSL_SUPPORT
|
||||||
|
#include "httplib.h"
|
||||||
|
#define CA_CERT_FILE "./ca-bundle.crt"
|
||||||
|
|
||||||
|
#include <curl/curl.h>
|
||||||
|
|
||||||
|
int xfer_callback(void *clientp, curl_off_t dltotal, curl_off_t dlnow,
|
||||||
|
curl_off_t ultotal, curl_off_t ulnow)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void download_file(const std::string &output_file = "pi.txt")
|
||||||
|
{
|
||||||
|
|
||||||
|
CURL *curl_download;
|
||||||
|
FILE *fp;
|
||||||
|
CURLcode res;
|
||||||
|
std::string url = "http://www.gecif.net/articles/mathematiques/pi/pi_1_million.txt";
|
||||||
|
|
||||||
|
curl_download = curl_easy_init();
|
||||||
|
|
||||||
|
if (curl_download)
|
||||||
|
{
|
||||||
|
//SetConsoleTextAttribute(hConsole, 11);
|
||||||
|
fp = fopen(output_file.c_str(),"wb");
|
||||||
|
|
||||||
|
curl_easy_setopt(curl_download, CURLOPT_URL, url.c_str());
|
||||||
|
curl_easy_setopt(curl_download, CURLOPT_WRITEFUNCTION, NULL);
|
||||||
|
curl_easy_setopt(curl_download, CURLOPT_WRITEDATA, fp);
|
||||||
|
curl_easy_setopt(curl_download, CURLOPT_NOPROGRESS, 0);
|
||||||
|
//progress_bar : the fonction for the progress bar
|
||||||
|
curl_easy_setopt(curl_download, CURLOPT_XFERINFOFUNCTION, xfer_callback);
|
||||||
|
|
||||||
|
|
||||||
|
std::cout<<" Start download"<<std::endl<<std::endl;
|
||||||
|
|
||||||
|
res = curl_easy_perform(curl_download);
|
||||||
|
|
||||||
|
fclose(fp);
|
||||||
|
if(res == CURLE_OK)
|
||||||
|
{
|
||||||
|
|
||||||
|
std::cout<< std::endl<< std::endl<<" The file was download with succes"<< std::endl;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
std::cout<< std::endl << std::endl<<" Error"<< std::endl;
|
||||||
|
}
|
||||||
|
curl_easy_cleanup(curl_download);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
LibraryWindow::LibraryWindow(IStoryManager &project, LibraryManager &library)
|
LibraryWindow::LibraryWindow(IStoryManager &project, LibraryManager &library)
|
||||||
: WindowBase("Library Manager")
|
: WindowBase("Library Manager")
|
||||||
|
|
@ -11,6 +67,51 @@ LibraryWindow::LibraryWindow(IStoryManager &project, LibraryManager &library)
|
||||||
, m_libraryManager(library)
|
, m_libraryManager(library)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
download_file();
|
||||||
|
|
||||||
|
httplib::SSLClient cli("gist.githubusercontent.com", 443);
|
||||||
|
cli.set_ca_cert_path(CA_CERT_FILE);
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
|
|
||||||
|
if (auto res = cli.Get("/UnofficialStories/32702fb104aebfe650d4ef8d440092c1/raw/luniicreations.json")) {
|
||||||
|
std::cout << res->status << std::endl;
|
||||||
|
std::cout << res->get_header_value("Content-Type") << std::endl;
|
||||||
|
m_storeRawJson = res->body;
|
||||||
|
ParseStoreData();
|
||||||
|
//std::cout << res->body << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cout << "error code: " << res.error() << std::endl;
|
||||||
|
|
||||||
|
auto result = cli.get_openssl_verify_result();
|
||||||
|
if (result) {
|
||||||
|
std::cout << "verify error: " << X509_verify_cert_error_string(result) <<std:: endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void LibraryWindow::ParseStoreData()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
|
||||||
|
nlohmann::json j = nlohmann::json::parse(m_storeRawJson);
|
||||||
|
|
||||||
|
m_store.clear();
|
||||||
|
for (const auto &obj : j)
|
||||||
|
{
|
||||||
|
StoryInf s;
|
||||||
|
|
||||||
|
s.title = obj["title"].get<std::string>();
|
||||||
|
s.age = obj["age"].get<int>();
|
||||||
|
|
||||||
|
m_store.push_back(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(std::exception &e)
|
||||||
|
{
|
||||||
|
std::cout << e.what() << std::endl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LibraryWindow::Initialize()
|
void LibraryWindow::Initialize()
|
||||||
|
|
@ -42,74 +143,127 @@ void LibraryWindow::Draw()
|
||||||
WindowBase::BeginDraw();
|
WindowBase::BeginDraw();
|
||||||
ImGui::SetWindowSize(ImVec2(626, 744), ImGuiCond_FirstUseEver);
|
ImGui::SetWindowSize(ImVec2(626, 744), ImGuiCond_FirstUseEver);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (ImGui::Button( ICON_MDI_FOLDER " Select directory"))
|
if (ImGui::Button( ICON_MDI_FOLDER " Select directory"))
|
||||||
{
|
{
|
||||||
ImGuiFileDialog::Instance()->OpenDialog("ChooseLibraryDirDialog", "Choose a library directory", nullptr, ".", 1, nullptr, ImGuiFileDialogFlags_Modal);
|
ImGuiFileDialog::Instance()->OpenDialog("ChooseLibraryDirDialog", "Choose a library directory", nullptr, ".", 1, nullptr, ImGuiFileDialogFlags_Modal);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
if (!m_libraryManager.IsInitialized())
|
if (!m_libraryManager.IsInitialized())
|
||||||
{
|
{
|
||||||
|
ImGui::SameLine();
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255,0,0,255));
|
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255,0,0,255));
|
||||||
ImGui::Text("No any library directory set. Please select one where stories will be located.");
|
ImGui::Text("No any library directory set. Please select one where stories will be located.");
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
ImGui::SameLine();
|
||||||
ImGui::Text("Library path: %s", m_libraryManager.LibraryPath().c_str());
|
ImGui::Text("Library path: %s", m_libraryManager.LibraryPath().c_str());
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui::Button("Scan library"))
|
|
||||||
{
|
|
||||||
m_libraryManager.Scan();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SameLine();
|
static ImGuiTabBarFlags tab_bar_flags = ImGuiTabBarFlags_Reorderable;
|
||||||
|
if (ImGui::BeginTabBar("LibraryTabBar", tab_bar_flags))
|
||||||
if (ImGui::Button("Import story"))
|
|
||||||
{
|
|
||||||
ImGuiFileDialog::Instance()->OpenDialogWithPane("ImportStoryDlgKey", "Import story", "", "", InfosPane);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_libraryManager.IsInitialized())
|
|
||||||
{
|
|
||||||
static ImGuiTableFlags tableFlags =
|
|
||||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable
|
|
||||||
| ImGuiTableFlags_Sortable | ImGuiTableFlags_SortMulti
|
|
||||||
| ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders | ImGuiTableFlags_NoBordersInBody
|
|
||||||
| ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY
|
|
||||||
| ImGuiTableFlags_SizingFixedFit;
|
|
||||||
|
|
||||||
if (ImGui::BeginTable("library_table", 2, tableFlags))
|
|
||||||
{
|
{
|
||||||
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed);
|
static ImGuiTableFlags tableFlags =
|
||||||
|
ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable
|
||||||
|
| ImGuiTableFlags_Sortable | ImGuiTableFlags_SortMulti
|
||||||
|
| ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders | ImGuiTableFlags_NoBordersInBody
|
||||||
|
| ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY
|
||||||
|
| ImGuiTableFlags_SizingFixedFit;
|
||||||
|
|
||||||
ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed);
|
// ============================================================================
|
||||||
|
// LOCAL TABLE
|
||||||
ImGui::TableHeadersRow();
|
// ============================================================================
|
||||||
|
if (ImGui::BeginTabItem("Local library ##LocalTabBar", nullptr, ImGuiTabItemFlags_None))
|
||||||
for (auto &p : m_libraryManager)
|
|
||||||
{
|
{
|
||||||
ImGui::TableNextColumn();
|
|
||||||
ImGui::Text("%s", p->GetName().c_str());
|
|
||||||
|
|
||||||
ImGui::TableNextColumn();
|
|
||||||
|
|
||||||
if (ImGui::SmallButton("Load"))
|
if (ImGui::Button("Scan library"))
|
||||||
{
|
{
|
||||||
m_storyManager.OpenProject(p->GetUuid());
|
m_libraryManager.Scan();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
|
||||||
if (ImGui::SmallButton("Remove"))
|
if (ImGui::Button("Import story"))
|
||||||
{
|
{
|
||||||
|
ImGuiFileDialog::Instance()->OpenDialogWithPane("ImportStoryDlgKey", "Import story", "", "", InfosPane);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (ImGui::BeginTable("library_table", 2, tableFlags))
|
||||||
|
{
|
||||||
|
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed);
|
||||||
|
|
||||||
|
ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed);
|
||||||
|
|
||||||
|
ImGui::TableHeadersRow();
|
||||||
|
|
||||||
|
for (auto &p : m_libraryManager)
|
||||||
|
{
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::Text("%s", p->GetName().c_str());
|
||||||
|
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
|
||||||
|
if (ImGui::SmallButton("Load"))
|
||||||
|
{
|
||||||
|
m_storyManager.OpenProject(p->GetUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
if (ImGui::SmallButton("Remove"))
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndTabItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::EndTable();
|
// ============================================================================
|
||||||
|
// LOCAL TABLE
|
||||||
|
// ============================================================================
|
||||||
|
if (ImGui::BeginTabItem("Remote Store##StoreTabBar", nullptr, ImGuiTabItemFlags_None))
|
||||||
|
{
|
||||||
|
if (ImGui::BeginTable("store_table", 3, tableFlags))
|
||||||
|
{
|
||||||
|
ImGui::TableSetupColumn("Title", ImGuiTableColumnFlags_WidthFixed);
|
||||||
|
ImGui::TableSetupColumn("Age", ImGuiTableColumnFlags_WidthFixed);
|
||||||
|
ImGui::TableSetupColumn("Download", ImGuiTableColumnFlags_WidthFixed);
|
||||||
|
|
||||||
|
ImGui::TableHeadersRow();
|
||||||
|
|
||||||
|
for (const auto &obj : m_store)
|
||||||
|
{
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::Text("%s", obj.title.c_str());
|
||||||
|
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::Text("%d", obj.age);
|
||||||
|
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
if (ImGui::SmallButton("Download"))
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndTabBar();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@
|
||||||
#include "library_manager.h"
|
#include "library_manager.h"
|
||||||
#include "i_story_manager.h"
|
#include "i_story_manager.h"
|
||||||
|
|
||||||
|
struct StoryInf {
|
||||||
|
int age;
|
||||||
|
std::string title;
|
||||||
|
};
|
||||||
|
|
||||||
class LibraryWindow : public WindowBase
|
class LibraryWindow : public WindowBase
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|
@ -16,5 +21,9 @@ private:
|
||||||
IStoryManager &m_storyManager;
|
IStoryManager &m_storyManager;
|
||||||
LibraryManager &m_libraryManager;
|
LibraryManager &m_libraryManager;
|
||||||
|
|
||||||
|
std::vector<StoryInf> m_store;
|
||||||
|
|
||||||
|
std::string m_storeRawJson;
|
||||||
|
void ParseStoreData();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,8 @@ void NodeEditorWindow::Load(const nlohmann::json &model)
|
||||||
GetInputPin(model.inNodeId, model.inPortIndex),
|
GetInputPin(model.inNodeId, model.inPortIndex),
|
||||||
GetOutputPin(model.outNodeId, model.outPortIndex));
|
GetOutputPin(model.outNodeId, model.outPortIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -485,6 +487,12 @@ void NodeEditorWindow::Draw()
|
||||||
ImGui::EndPopup();
|
ImGui::EndPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_loaded)
|
||||||
|
{
|
||||||
|
ed::NavigateToContent();
|
||||||
|
m_loaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
ed::Resume();
|
ed::Resume();
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,8 @@ private:
|
||||||
|
|
||||||
ed::EditorContext* m_context = nullptr;
|
ed::EditorContext* m_context = nullptr;
|
||||||
|
|
||||||
|
bool m_loaded{false};
|
||||||
|
|
||||||
// key: Id
|
// key: Id
|
||||||
std::list<std::shared_ptr<BaseNode>> m_nodes;
|
std::list<std::shared_ptr<BaseNode>> m_nodes;
|
||||||
std::list<std::shared_ptr<LinkInfo>> m_links; // List of live links. It is dynamic unless you want to create read-only view over nodes.
|
std::list<std::shared_ptr<LinkInfo>> m_links; // List of live links. It is dynamic unless you want to create read-only view over nodes.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue