summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRoland Reichwein <mail@reichwein.it>2023-01-27 19:42:08 +0100
committerRoland Reichwein <mail@reichwein.it>2023-01-27 19:42:08 +0100
commitf44d36b05e43cabde31aeaba5d25fded140345a1 (patch)
tree1024a76cb1ae671c9445dcc379cb9eddd26922aa
parent789e5555ab4c44a1ae779eccf6ccf8340602cf22 (diff)
Added diff.cpp
-rwxr-xr-xMakefile3
-rw-r--r--common.mk8
-rw-r--r--connectionregistry.cpp2
-rw-r--r--connectionregistry.h2
-rw-r--r--debian/changelog4
-rw-r--r--diff.cpp125
-rw-r--r--diff.h24
-rw-r--r--html/index.html6
-rw-r--r--html/whiteboard.css13
-rw-r--r--html/whiteboard.js65
-rw-r--r--tests/Makefile5
-rw-r--r--whiteboard.cpp55
-rw-r--r--whiteboard.h3
13 files changed, 283 insertions, 32 deletions
diff --git a/Makefile b/Makefile
index 658e764..449e915 100755
--- a/Makefile
+++ b/Makefile
@@ -9,11 +9,10 @@ include common.mk
PROJECTNAME=whiteboard
DISTROS=base debian11 ubuntu2210
-VERSION=$(shell dpkg-parsechangelog --show-field Version)
TGZNAME=$(PROJECTNAME)-$(VERSION).tar.xz
INCLUDES=-I.
-HEADERS=config.h qrcode.h storage.h whiteboard.h compiledsql.h connectionregistry.h
+HEADERS=config.h qrcode.h storage.h whiteboard.h compiledsql.h connectionregistry.h diff.h
SOURCES=$(HEADERS:.h=.cpp)
OBJECTS=$(HEADERS:.h=.o)
TARGETS=whiteboard
diff --git a/common.mk b/common.mk
index 1f4c2df..5f4c77c 100644
--- a/common.mk
+++ b/common.mk
@@ -61,3 +61,11 @@ LIBS+=-lboost_filesystem -lpthread
LIBS+=-lSQLiteCpp $(shell pkg-config --libs qrcodegencpp GraphicsMagick++ fmt sqlite3)
LIBS+=-lreichwein
+SRC_ROOT=$(shell echo $(MAKEFILE_LIST) | tr " " "\n" | grep common.mk | sed -e 's/\([^ ]*\)common.mk/\1/g')
+ifeq ($(SRC_ROOT),)
+SRC_ROOT=.
+endif
+
+VERSION=$(shell dpkg-parsechangelog --show-field Version --file $(SRC_ROOT)/debian/changelog)
+CXXFLAGS+=-DWHITEBOARD_VERSION=\"$(VERSION)\"
+
diff --git a/connectionregistry.cpp b/connectionregistry.cpp
index 1e48a96..11a538b 100644
--- a/connectionregistry.cpp
+++ b/connectionregistry.cpp
@@ -61,7 +61,7 @@ std::unordered_set<ConnectionRegistry::connection>::iterator ConnectionRegistry:
return m_ids.at(id).end();
}
-void ConnectionRegistry::dump()
+void ConnectionRegistry::dump() const
{
std::cout << "Connection Registry:" << std::endl;
diff --git a/connectionregistry.h b/connectionregistry.h
index 2b14553..cdd30d9 100644
--- a/connectionregistry.h
+++ b/connectionregistry.h
@@ -30,7 +30,7 @@ public:
std::unordered_set<connection>::iterator begin(const std::string& id);
std::unordered_set<connection>::iterator end(const std::string& id);
- void dump();
+ void dump() const;
class RegistryGuard
{
diff --git a/debian/changelog b/debian/changelog
index 8373568..2c990d1 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,6 +1,10 @@
whiteboard (1.5) UNRELEASED; urgency=medium
* Move from FCGI to websocket interface
+ * Position changes w/o file transmit
+ * Print version on main page
+ * Add reconnect button
+ * Add diff handling
-- Roland Reichwein <mail@reichwein.it> Sat, 21 Jan 2023 18:18:37 +0100
diff --git a/diff.cpp b/diff.cpp
new file mode 100644
index 0000000..b3ed5ce
--- /dev/null
+++ b/diff.cpp
@@ -0,0 +1,125 @@
+#include "diff.h"
+
+#include <algorithm>
+#include <sstream>
+
+#include <boost/property_tree/xml_parser.hpp>
+
+namespace pt = boost::property_tree;
+
+Diff::Diff()
+{
+}
+
+Diff::Diff(const std::string& old_version, const std::string& new_version)
+{
+ create(old_version, new_version);
+}
+
+std::string Diff::apply(const std::string& old_version) const
+{
+ std::string result{old_version};
+
+ result.erase(m_pos0, m_pos1 - m_pos0);
+
+ result.insert(m_pos0, m_data);
+
+ return result;
+}
+
+void Diff::create(const std::string& old_version, const std::string& new_version)
+{
+ auto front_mismatch{std::mismatch(old_version.cbegin(), old_version.cend(), new_version.cbegin(), new_version.cend())};
+ std::string::difference_type old_pos0 {front_mismatch.first - old_version.cbegin()};
+ auto& new_pos0 {old_pos0};
+
+ // equal
+ if (old_pos0 == old_version.size() && new_pos0 == new_version.size()) {
+ m_pos0 = 0;
+ m_pos1 = 0;
+ m_data.clear();
+ return;
+ }
+
+ // append at end
+ if (old_pos0 == old_version.size()) {
+ m_pos0 = old_pos0;
+ m_pos1 = old_pos0;
+ m_data = new_version.substr(new_pos0);
+ return;
+ }
+
+ // remove from end
+ if (new_pos0 == new_version.size()) {
+ m_pos0 = old_pos0;
+ m_pos1 = old_version.size();
+ m_data.clear();
+ return;
+ }
+
+ auto back_mismatch{std::mismatch(old_version.crbegin(), old_version.crend(), new_version.crbegin(), new_version.crend())};
+ // i.e. the indices starting from which we can assume equality
+ size_t old_pos1 {old_version.size() - (back_mismatch.first - old_version.crbegin())};
+ size_t new_pos1 {new_version.size() - (back_mismatch.second - new_version.crbegin())};
+
+ // complete equality is already handled above
+
+ // insert at start
+ if (old_pos1 == 0) {
+ m_pos0 = 0;
+ m_pos1 = 0;
+ m_data = new_version.substr(0, new_pos1);
+ return;
+ }
+
+ // remove from start
+ if (new_pos1 == 0) {
+ m_pos0 = 0;
+ m_pos1 = old_pos1;
+ m_data.clear();
+ return;
+ }
+
+ // insert in the middle
+ if (old_pos0 == old_pos1) {
+ m_pos0 = old_pos0;
+ m_pos1 = old_pos0;
+ m_data = new_version.substr(new_pos0, new_pos1 - new_pos0);
+ return;
+ }
+
+ // remove from the middle
+ if (new_pos0 == new_pos1) {
+ m_pos0 = old_pos0;
+ m_pos1 = old_pos1;
+ m_data.clear();
+ return;
+ }
+
+ // last resort: remove and add in the middle
+ m_pos0 = old_pos0;
+ m_pos1 = old_pos1;
+ m_data = new_version.substr(old_pos0, new_pos1 - new_pos0);
+}
+
+boost::property_tree::ptree Diff::get_structure() const
+{
+ pt::ptree ptree;
+ ptree.put("diff.chunk.start", std::to_string(m_pos0));
+ ptree.put("diff.chunk.end", std::to_string(m_pos1));
+ ptree.put("diff.chunk.data", m_data);
+
+ return ptree;
+}
+
+std::string Diff::get_xml() const
+{
+ pt::ptree ptree{get_structure()};
+
+ std::ostringstream oss;
+ // write_xml_element instead of write_xml to omit <!xml...> header
+ //pt::xml_parser::write_xml(oss, xml);
+ pt::xml_parser::write_xml_element(oss, {}, ptree, -1, boost::property_tree::xml_writer_settings<pt::ptree::key_type>{});
+ return oss.str();
+}
+
diff --git a/diff.h b/diff.h
new file mode 100644
index 0000000..0193238
--- /dev/null
+++ b/diff.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <string>
+
+#include <boost/property_tree/ptree.hpp>
+
+class Diff
+{
+public:
+ Diff();
+ Diff(const std::string& old_version, const std::string& new_version);
+
+ std::string apply(const std::string& old_version) const;
+ void create(const std::string& old_version, const std::string& new_version);
+
+ boost::property_tree::ptree get_structure() const;
+ std::string get_xml() const;
+
+private:
+ // diff replaces space from m_pos0 (inclusive) to m_pos1 (exclusive) with m_data
+ size_t m_pos0{};
+ size_t m_pos1{};
+ std::string m_data;
+};
diff --git a/html/index.html b/html/index.html
index f06ea01..4d1fb2a 100644
--- a/html/index.html
+++ b/html/index.html
@@ -19,10 +19,12 @@
<br/>
<br/>
<button class="button" onclick="on_new_page();">New page</button>
- <button class="button" onclick="on_qrcode();">QR code</button>
+ <button class="button" onclick="on_qrcode_click();">QR code</button>
+ <span id="connecting">Connecting...</span>
+ <button class="buttonred" id="reconnect" onclick="on_reconnect_click();" hidden>Reconnect</button>
<br/>
<br/>
- Reichwein.IT Whiteboard by <a href="https://www.reichwein.it">https://www.reichwein.it</a><br/>
+ Reichwein.IT Whiteboard <span id="version"></span> by <a href="https://www.reichwein.it">https://www.reichwein.it</a><br/>
</div>
<a id="download-a" hidden></a>
diff --git a/html/whiteboard.css b/html/whiteboard.css
index 55e68cf..4de9b46 100644
--- a/html/whiteboard.css
+++ b/html/whiteboard.css
@@ -1,5 +1,5 @@
body {
- font-family: "sans-serif";
+ font-family: sans-serif;
}
figcaption {
@@ -100,6 +100,17 @@ img.banner {
cursor: pointer;
}
+.buttonred {
+ color:#FFFFFF;
+ background-color:#B05050;
+ text-decoration: none;
+ padding: 15px 20px;
+ font-size: 16px;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+}
+
@media only screen and (min-width: 1px) and (max-width: 630px) {
.qrwindow {
diff --git a/html/whiteboard.js b/html/whiteboard.js
index 9610468..83601b8 100644
--- a/html/whiteboard.js
+++ b/html/whiteboard.js
@@ -30,8 +30,13 @@ function on_getfile(data, rev, pos)
if (board.value != data) {
board.value = data;
}
- textAreaSetPos("board", pos);
revision = rev;
+ textAreaSetPos("board", pos);
+}
+
+function on_getpos(pos)
+{
+ textAreaSetPos("board", pos);
}
function on_newid(id)
@@ -42,13 +47,17 @@ function on_newid(id)
function on_qrcode(png)
{
- var blob = new Blob([png], {type: 'image/png'});
- var url = URL.createObjectURL(blob);
+ var url = "data:image/png;base64," + png;
var img = document.getElementById("qrcode");
img.src = url;
showQRWindow();
}
+function on_version(version)
+{
+ document.getElementById("version").textContent = version;
+}
+
function on_modify_ack(rev)
{
revision = rev;
@@ -63,13 +72,17 @@ function on_message(e) {
if (type == "getfile") {
on_getfile(xmlDocument.getElementsByTagName("data")[0].textContent,
parseInt(xmlDocument.getElementsByTagName("revision")[0].textContent),
- parseInt(xmlDocument.getElementsByTagName("cursorpos")[0].textContent));
+ parseInt(xmlDocument.getElementsByTagName("pos")[0].textContent));
+ } else if (type == "getpos") {
+ on_getpos(parseInt(xmlDocument.getElementsByTagName("pos")[0].textContent));
} else if (type == "modify") {
on_modify_ack(parseInt(xmlDocument.getElementsByTagName("revision")[0].textContent));
} else if (type == "newid") {
on_newid(xmlDocument.getElementsByTagName("id")[0].textContent);
} else if (type == "qrcode") {
on_qrcode(xmlDocument.getElementsByTagName("png")[0].textContent);
+ } else if (type == "version") {
+ on_version(xmlDocument.getElementsByTagName("version")[0].textContent);
} else if (type == "error") {
alert(xmlDocument.getElementsByTagName("message")[0].textContent);
} else {
@@ -88,12 +101,15 @@ function handleSelection() {
}
}
-function init_board() {
+function connect_websocket() {
+ document.getElementById("reconnect").style.display = 'none';
+ document.getElementById("connecting").style.display = 'block';
var newlocation = location.origin + location.pathname;
newlocation = newlocation.replace(/^http/, 'ws');
if (newlocation.slice(-1) != "/")
newlocation += "/";
newlocation += "websocket";
+
websocket = new WebSocket(newlocation);
websocket.onmessage = function(e) { on_message(e); };
@@ -105,24 +121,37 @@ function init_board() {
return;
}
- var board = document.getElementById("board");
- board.addEventListener("input", function() {on_input(); });
- // Need this workaround (different from direct on_selectionchange) for Chrome.
- // Otherwise, callback will not be called on Chrome.
- document.addEventListener("selectionchange", handleSelection);
- //board.addEventListener("selectionchange", function() {on_selectionchange(); });
-
+ websocket.send("<request><command>getversion</command></request>");
websocket.send("<request><command>getfile</command><id>" + get_id() + "</id></request>");
+ document.getElementById("connecting").style.display = 'none';
};
websocket.onclose = function(e) {
alert("Server connection closed.");
+ document.getElementById("reconnect").style.display = 'inline';
};
websocket.onerror = function(e) {
alert("Error: Server connection closed.");
+ document.getElementById("reconnect").style.display = 'inline';
};
+}
+
+function on_reconnect_click() {
+ connect_websocket();
+}
+
+function init_board() {
+ connect_websocket();
+
+ var board = document.getElementById("board");
+ board.addEventListener("input", function() {on_input(); });
+ // Need this workaround (different from direct on_selectionchange) for Chrome.
+ // Otherwise, callback will not be called on Chrome.
+ document.addEventListener("selectionchange", handleSelection);
+ //board.addEventListener("selectionchange", function() {on_selectionchange(); });
+
document.getElementById("qrwindow").onclick = function() {
hideQRWindow();
}
@@ -173,6 +202,10 @@ function on_input()
dataElement.appendChild(document.createTextNode(document.getElementById("board").value));
requestElement.appendChild(dataElement);
+ var posElement = xmlDocument.createElement("pos");
+ posElement.appendChild(document.createTextNode(document.getElementById("board").selectionStart));
+ requestElement.appendChild(posElement);
+
websocket.send(new XMLSerializer().serializeToString(xmlDocument));
}
@@ -192,9 +225,9 @@ function on_selectionchange(pos)
idElement.appendChild(document.createTextNode(get_id()));
requestElement.appendChild(idElement);
- var dataElement = xmlDocument.createElement("pos");
- dataElement.appendChild(document.createTextNode(pos));
- requestElement.appendChild(dataElement);
+ var posElement = xmlDocument.createElement("pos");
+ posElement.appendChild(document.createTextNode(pos));
+ requestElement.appendChild(posElement);
websocket.send(new XMLSerializer().serializeToString(xmlDocument));
}
@@ -209,7 +242,7 @@ function textAreaSetPos(id, pos)
}
// HTML button
-function on_qrcode()
+function on_qrcode_click()
{
var parser = new DOMParser();
var xmlDocument = parser.parseFromString("<request></request>", "text/xml");
diff --git a/tests/Makefile b/tests/Makefile
index 15e4106..c4109d5 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -11,7 +11,7 @@ CXXFLAGS+=--coverage
LDFLAGS+=--coverage
endif
-UNITS=storage.cpp config.cpp compiledsql.cpp qrcode.cpp whiteboard.cpp connectionregistry.cpp
+UNITS=storage.cpp config.cpp compiledsql.cpp qrcode.cpp whiteboard.cpp connectionregistry.cpp diff.cpp
UNITTESTS=test-config.cpp \
test-storage.cpp \
@@ -52,6 +52,9 @@ config.o: ../config.cpp
connectionregistry.o: ../connectionregistry.cpp
$(CXX) $(CXXFLAGS) -o $@ -c $<
+diff.o: ../diff.cpp
+ $(CXX) $(CXXFLAGS) -o $@ -c $<
+
storage.o: ../storage.cpp
$(CXX) $(CXXFLAGS) -o $@ -c $<
diff --git a/whiteboard.cpp b/whiteboard.cpp
index b15ebbe..df18242 100644
--- a/whiteboard.cpp
+++ b/whiteboard.cpp
@@ -34,6 +34,7 @@
#include <fmt/core.h>
+#include "libreichwein/base64.h"
#include "libreichwein/file.h"
#include "config.h"
@@ -89,11 +90,12 @@ std::string make_xml(const std::initializer_list<std::pair<std::string, std::str
std::ostringstream oss;
// write_xml_element instead of write_xml to omit <!xml...> header
+ //pt::xml_parser::write_xml(oss, xml);
pt::xml_parser::write_xml_element(oss, {}, xml, -1, boost::property_tree::xml_writer_settings<pt::ptree::key_type>{});
return oss.str();
}
-void Whiteboard::notify_other_connections(Whiteboard::connection& c, const std::string& id)
+void Whiteboard::notify_other_connections_file(Whiteboard::connection& c, const std::string& id)
{
std::for_each(m_registry.begin(id), m_registry.end(id), [&](const Whiteboard::connection& ci)
{
@@ -103,13 +105,34 @@ void Whiteboard::notify_other_connections(Whiteboard::connection& c, const std::
{"type", "getfile"},
{"data", m_storage->getDocument(id)},
{"revision", std::to_string(m_storage->getRevision(id)) },
- {"cursorpos", std::to_string(m_storage->getCursorPos(id)) }
+ {"pos", std::to_string(m_storage->getCursorPos(id)) }
});
std::lock_guard<std::mutex> lock(m_websocket_mutex);
try {
ci->write(buffer.data());
} catch (const std::exception& ex) {
- std::cerr << "Warning: Notify write for " << ci << " not possible, id " << id << std::endl;
+ std::cerr << "Warning: Notify getfile write for " << ci << " not possible, id " << id << std::endl;
+ m_registry.dump();
+ }
+ }
+ });
+}
+
+void Whiteboard::notify_other_connections_pos(Whiteboard::connection& c, const std::string& id)
+{
+ std::for_each(m_registry.begin(id), m_registry.end(id), [&](const Whiteboard::connection& ci)
+ {
+ if (c != ci) {
+ boost::beast::flat_buffer buffer;
+ boost::beast::ostream(buffer) << make_xml({
+ {"type", "getpos"},
+ {"pos", std::to_string(m_storage->getCursorPos(id)) }
+ });
+ std::lock_guard<std::mutex> lock(m_websocket_mutex);
+ try {
+ ci->write(buffer.data());
+ } catch (const std::exception& ex) {
+ std::cerr << "Warning: Notify getpos write for " << ci << " not possible, id " << id << std::endl;
m_registry.dump();
}
}
@@ -135,7 +158,13 @@ std::string Whiteboard::handle_request(Whiteboard::connection& c, const std::str
if (m_storage->getDocument(id) != data) {
m_storage->setDocument(id, data);
m_registry.setId(c, id);
- notify_other_connections(c, id);
+ notify_other_connections_file(c, id);
+
+ int pos {xml.get<int>("request.pos")};
+ if (m_storage->getCursorPos(id) != pos) {
+ m_storage->setCursorPos(id, pos);
+ notify_other_connections_pos(c, id);
+ }
return make_xml({{"type", "modify"}, {"revision", std::to_string(m_storage->getRevision(id)) }});
}
return {};
@@ -144,7 +173,7 @@ std::string Whiteboard::handle_request(Whiteboard::connection& c, const std::str
int pos {xml.get<int>("request.pos")};
if (m_storage->getCursorPos(id) != pos) {
m_storage->setCursorPos(id, pos);
- notify_other_connections(c, id);
+ notify_other_connections_pos(c, id);
}
return {};
} else if (command == "getfile") {
@@ -165,7 +194,14 @@ std::string Whiteboard::handle_request(Whiteboard::connection& c, const std::str
{"type", "getfile"},
{"data", filedata},
{"revision", std::to_string(m_storage->getRevision(id)) },
- {"cursorpos", std::to_string(m_storage->getCursorPos(id)) }
+ {"pos", std::to_string(m_storage->getCursorPos(id)) }
+ });
+ } else if (command == "getpos") {
+ std::string id {xml.get<std::string>("request.id")};
+
+ return make_xml({
+ {"type", "getpos"},
+ {"pos", std::to_string(m_storage->getCursorPos(id)) }
});
} else if (command == "newid") {
return make_xml({{"type", "newid"}, {"id", m_storage->generate_id()}});
@@ -177,7 +213,12 @@ std::string Whiteboard::handle_request(Whiteboard::connection& c, const std::str
std::string pngdata {QRCode::getQRCode(url)};
- return make_xml({{"type", "qrcode"}, {"png", pngdata}});
+ return make_xml({{"type", "qrcode"}, {"png", Reichwein::Base64::encode64(pngdata)}});
+ } else if (command == "getversion") {
+ return make_xml({
+ {"type", "version"},
+ {"version", WHITEBOARD_VERSION }
+ });
} else {
throw std::runtime_error("Bad command: "s + command);
}
diff --git a/whiteboard.h b/whiteboard.h
index e39b94e..818fcfe 100644
--- a/whiteboard.h
+++ b/whiteboard.h
@@ -27,7 +27,8 @@ private:
using connection = std::shared_ptr<boost::beast::websocket::stream<boost::asio::ip::tcp::socket>>;
std::string handle_request(connection& c, const std::string& request);
- void notify_other_connections(connection& c, const std::string& id); // notify all other id-related connections about changes
+ void notify_other_connections_file(connection& c, const std::string& id); // notify all other id-related connections about changes
+ void notify_other_connections_pos(connection& c, const std::string& id); // notify all other id-related connections about changes
void do_session(boost::asio::ip::tcp::socket socket);
void storage_cleanup();
};