Sergidev commited on
Commit
9982ad3
·
verified ·
1 Parent(s): db5fcc7
Files changed (11) hide show
  1. Cargo.toml +12 -0
  2. Dockerfile +12 -0
  3. src/app.rs +26 -0
  4. src/database.rs +59 -0
  5. src/main.rs +26 -0
  6. src/websocket.rs +53 -0
  7. src/whiteboard.rs +31 -0
  8. static/index.html +18 -0
  9. static/script.js +121 -0
  10. static/style.css +23 -0
  11. whiteboard.db +0 -0
Cargo.toml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "weboard"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [dependencies]
7
+ tokio = { version = "1.28", features = ["full"] }
8
+ warp = "0.3"
9
+ serde = { version = "1.0", features = ["derive"] }
10
+ serde_json = "1.0"
11
+ futures = "0.3"
12
+ uuid = { version = "1.3", features = ["v4"] }
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM rust:1.70 as builder
2
+ WORKDIR /usr/src/weboard
3
+ COPY . .
4
+ RUN cargo build --release
5
+
6
+ FROM debian:bullseye-slim
7
+ RUN apt-get update && apt-get install -y libssl1.1 ca-certificates && rm -rf /var/lib/apt/lists/*
8
+ COPY --from=builder /usr/src/weboard/target/release/weboard /usr/local/bin/weboard
9
+ COPY --from=builder /usr/src/weboard/static /usr/local/bin/static
10
+ WORKDIR /usr/local/bin
11
+ EXPOSE 7860
12
+ CMD ["weboard"]
src/app.rs ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use serde::{Deserialize, Serialize};
2
+
3
+ #[derive(Debug, Clone, Serialize, Deserialize)]
4
+ pub struct Point {
5
+ pub x: f32,
6
+ pub y: f32,
7
+ }
8
+
9
+ #[derive(Debug, Clone, Serialize, Deserialize)]
10
+ pub struct DrawAction {
11
+ pub color: String,
12
+ pub size: f32,
13
+ pub points: Vec<Point>,
14
+ }
15
+
16
+ #[derive(Debug, Clone, Serialize, Deserialize)]
17
+ pub enum ClientMessage {
18
+ Draw(DrawAction),
19
+ Clear,
20
+ }
21
+
22
+ #[derive(Debug, Clone, Serialize, Deserialize)]
23
+ pub enum ServerMessage {
24
+ Update(Vec<DrawAction>),
25
+ Clear,
26
+ }
src/database.rs ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use rusqlite::{Connection, Result};
2
+ use crate::whiteboard::DrawAction;
3
+ use std::sync::Mutex;
4
+
5
+ pub struct Database {
6
+ conn: Mutex<Connection>,
7
+ }
8
+
9
+ impl Database {
10
+ pub fn new(conn: Connection) -> Self {
11
+ Database { conn: Mutex::new(conn) }
12
+ }
13
+
14
+ pub fn save_action(&self, action: &DrawAction) -> Result<()> {
15
+ let conn = self.conn.lock().unwrap();
16
+ conn.execute(
17
+ "INSERT INTO actions (x, y, color, is_eraser, pen_size) VALUES (?1, ?2, ?3, ?4, ?5)",
18
+ (action.x, action.y, &action.color, action.is_eraser, action.pen_size),
19
+ )?;
20
+ Ok(())
21
+ }
22
+
23
+ pub fn get_current_state(&self) -> Result<Vec<DrawAction>> {
24
+ let conn = self.conn.lock().unwrap();
25
+ let mut stmt = conn.prepare("SELECT x, y, color, is_eraser, pen_size FROM actions ORDER BY id")?;
26
+ let actions = stmt.query_map([], |row| {
27
+ Ok(DrawAction {
28
+ x: row.get(0)?,
29
+ y: row.get(1)?,
30
+ color: row.get(2)?,
31
+ is_eraser: row.get(3)?,
32
+ pen_size: row.get(4)?,
33
+ })
34
+ })?;
35
+ actions.collect()
36
+ }
37
+
38
+ pub fn clear_whiteboard(&self) -> Result<()> {
39
+ let conn = self.conn.lock().unwrap();
40
+ conn.execute("DELETE FROM actions", [])?;
41
+ Ok(())
42
+ }
43
+ }
44
+
45
+ pub fn init_db() -> Result<Database> {
46
+ let conn = Connection::open("whiteboard.db")?;
47
+ conn.execute(
48
+ "CREATE TABLE IF NOT EXISTS actions (
49
+ id INTEGER PRIMARY KEY,
50
+ x REAL NOT NULL,
51
+ y REAL NOT NULL,
52
+ color TEXT NOT NULL,
53
+ is_eraser BOOLEAN NOT NULL,
54
+ pen_size REAL NOT NULL
55
+ )",
56
+ [],
57
+ )?;
58
+ Ok(Database::new(conn))
59
+ }
src/main.rs ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ mod app;
2
+ mod whiteboard;
3
+ mod websocket;
4
+
5
+ use warp::Filter;
6
+
7
+ #[tokio::main]
8
+ async fn main() {
9
+ let whiteboard = whiteboard::Whiteboard::new();
10
+ let whiteboard = std::sync::Arc::new(tokio::sync::Mutex::new(whiteboard));
11
+
12
+ let websocket_route = warp::path("ws")
13
+ .and(warp::ws())
14
+ .and(warp::any().map(move || whiteboard.clone()))
15
+ .and_then(websocket::ws_handler);
16
+
17
+ let static_files = warp::fs::dir("static");
18
+ let index = warp::get().and(warp::path::end()).and(warp::fs::file("static/index.html"));
19
+
20
+ let routes = websocket_route
21
+ .or(static_files)
22
+ .or(index);
23
+
24
+ println!("Server starting on http://0.0.0.0:7860");
25
+ warp::serve(routes).run(([0, 0, 0, 0], 7860)).await;
26
+ }
src/websocket.rs ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::app::{ClientMessage, ServerMessage};
2
+ use crate::whiteboard::Whiteboard;
3
+ use futures::{FutureExt, SinkExt, StreamExt};
4
+ use std::sync::Arc;
5
+ use tokio::sync::Mutex;
6
+ use warp::ws::{Message, WebSocket};
7
+
8
+ pub async fn ws_handler(
9
+ ws: warp::ws::Ws,
10
+ whiteboard: Arc<Mutex<Whiteboard>>,
11
+ ) -> Result<impl warp::Reply, warp::Rejection> {
12
+ Ok(ws.on_upgrade(move |socket| client_connection(socket, whiteboard)))
13
+ }
14
+
15
+ async fn client_connection(ws: WebSocket, whiteboard: Arc<Mutex<Whiteboard>>) {
16
+ let (mut client_ws_sender, mut client_ws_rcv) = ws.split();
17
+
18
+ let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(69));
19
+
20
+ loop {
21
+ tokio::select! {
22
+ msg = client_ws_rcv.next().fuse() => {
23
+ match msg {
24
+ Some(Ok(msg)) => {
25
+ if let Ok(text) = msg.to_str() {
26
+ if let Ok(client_msg) = serde_json::from_str::<ClientMessage>(text) {
27
+ let mut wb = whiteboard.lock().await;
28
+ match client_msg {
29
+ ClientMessage::Draw(action) => {
30
+ wb.add_action(action);
31
+ }
32
+ ClientMessage::Clear => {
33
+ wb.clear();
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ _ => break,
40
+ }
41
+ }
42
+ _ = interval.tick() => {
43
+ let wb = whiteboard.lock().await;
44
+ let actions = wb.get_actions();
45
+ let server_msg = ServerMessage::Update(actions);
46
+ let msg = serde_json::to_string(&server_msg).unwrap();
47
+ if let Err(_) = client_ws_sender.send(Message::text(msg)).await {
48
+ break;
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
src/whiteboard.rs ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::app::{DrawAction};
2
+ use std::collections::VecDeque;
3
+
4
+ pub struct Whiteboard {
5
+ actions: VecDeque<DrawAction>,
6
+ max_actions: usize,
7
+ }
8
+
9
+ impl Whiteboard {
10
+ pub fn new() -> Self {
11
+ Whiteboard {
12
+ actions: VecDeque::new(),
13
+ max_actions: 1000,
14
+ }
15
+ }
16
+
17
+ pub fn add_action(&mut self, action: DrawAction) {
18
+ if self.actions.len() >= self.max_actions {
19
+ self.actions.pop_front();
20
+ }
21
+ self.actions.push_back(action);
22
+ }
23
+
24
+ pub fn clear(&mut self) {
25
+ self.actions.clear();
26
+ }
27
+
28
+ pub fn get_actions(&self) -> Vec<DrawAction> {
29
+ self.actions.iter().cloned().collect()
30
+ }
31
+ }
static/index.html ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Weboard - Collaborative Whiteboard</title>
7
+ <link rel="stylesheet" href="/style.css" />
8
+ </head>
9
+ <body>
10
+ <canvas id="whiteboard"></canvas>
11
+ <div id="toolbar">
12
+ <input type="color" id="color-picker" value="#000000" />
13
+ <input type="range" id="size-slider" min="1" max="20" value="5" />
14
+ <button id="clear-btn">Clear</button>
15
+ </div>
16
+ <script src="/script.js"></script>
17
+ </body>
18
+ </html>
static/script.js ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const canvas = document.getElementById("whiteboard");
2
+ const ctx = canvas.getContext("2d");
3
+ const colorPicker = document.getElementById("color-picker");
4
+ const sizeSlider = document.getElementById("size-slider");
5
+ const clearBtn = document.getElementById("clear-btn");
6
+
7
+ let isDrawing = false;
8
+ let currentPath = [];
9
+
10
+ canvas.width = window.innerWidth;
11
+ canvas.height = window.innerHeight - 50;
12
+
13
+ const ws = new WebSocket(`ws://${window.location.host}/ws`);
14
+
15
+ ws.onmessage = (event) => {
16
+ const message = JSON.parse(event.data);
17
+ if (message.Update) {
18
+ redrawWhiteboard(message.Update);
19
+ } else if (message.Clear) {
20
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
21
+ }
22
+ };
23
+
24
+ function redrawWhiteboard(actions) {
25
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
26
+ for (const action of actions) {
27
+ drawPath(action);
28
+ }
29
+ }
30
+
31
+ function drawPath(action) {
32
+ ctx.beginPath();
33
+ ctx.strokeStyle = action.color;
34
+ ctx.lineWidth = action.size;
35
+ ctx.lineCap = "round";
36
+ ctx.lineJoin = "round";
37
+ for (let i = 0; i < action.points.length; i++) {
38
+ const point = action.points[i];
39
+ if (i === 0) {
40
+ ctx.moveTo(point.x, point.y);
41
+ } else {
42
+ ctx.lineTo(point.x, point.y);
43
+ }
44
+ }
45
+ ctx.stroke();
46
+ }
47
+
48
+ canvas.addEventListener("mousedown", startDrawing);
49
+ canvas.addEventListener("mousemove", draw);
50
+ canvas.addEventListener("mouseup", stopDrawing);
51
+ canvas.addEventListener("mouseout", stopDrawing);
52
+
53
+ function startDrawing(e) {
54
+ isDrawing = true;
55
+ currentPath = [];
56
+ const point = getPoint(e);
57
+ currentPath.push(point);
58
+ ctx.beginPath();
59
+ ctx.moveTo(point.x, point.y);
60
+ }
61
+
62
+ function draw(e) {
63
+ if (!isDrawing) return;
64
+
65
+ const point = getPoint(e);
66
+ currentPath.push(point);
67
+
68
+ // Clear the canvas and redraw the current path
69
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
70
+ redrawWhiteboard([]); // Redraw all previous actions
71
+ drawCurrentPath();
72
+ }
73
+
74
+ function drawCurrentPath() {
75
+ ctx.beginPath();
76
+ ctx.strokeStyle = colorPicker.value;
77
+ ctx.lineWidth = sizeSlider.value;
78
+ ctx.lineCap = "round";
79
+ ctx.lineJoin = "round";
80
+ for (let i = 0; i < currentPath.length; i++) {
81
+ const point = currentPath[i];
82
+ if (i === 0) {
83
+ ctx.moveTo(point.x, point.y);
84
+ } else {
85
+ ctx.lineTo(point.x, point.y);
86
+ }
87
+ }
88
+ ctx.stroke();
89
+ }
90
+
91
+ function stopDrawing() {
92
+ if (!isDrawing) return;
93
+ isDrawing = false;
94
+
95
+ const action = {
96
+ color: colorPicker.value,
97
+ size: parseFloat(sizeSlider.value),
98
+ points: currentPath,
99
+ };
100
+
101
+ ws.send(JSON.stringify({ Draw: action }));
102
+ currentPath = [];
103
+ }
104
+
105
+ function getPoint(e) {
106
+ const rect = canvas.getBoundingClientRect();
107
+ return {
108
+ x: e.clientX - rect.left,
109
+ y: e.clientY - rect.top,
110
+ };
111
+ }
112
+
113
+ clearBtn.addEventListener("click", () => {
114
+ ws.send(JSON.stringify({ Clear: null }));
115
+ });
116
+
117
+ window.addEventListener("resize", () => {
118
+ canvas.width = window.innerWidth;
119
+ canvas.height = window.innerHeight - 50;
120
+ redrawWhiteboard([]);
121
+ });
static/style.css ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ margin: 0;
3
+ padding: 0;
4
+ display: flex;
5
+ flex-direction: column;
6
+ height: 100vh;
7
+ }
8
+
9
+ #whiteboard {
10
+ flex-grow: 1;
11
+ }
12
+
13
+ #toolbar {
14
+ display: flex;
15
+ justify-content: center;
16
+ align-items: center;
17
+ padding: 10px;
18
+ background-color: #f0f0f0;
19
+ }
20
+
21
+ #toolbar > * {
22
+ margin: 0 10px;
23
+ }
whiteboard.db ADDED
Binary file (20.5 kB). View file