[ini]
Christian Fraß

Christian Fraß commited on 2021-11-18 23:54:20
Zeige 7 geänderte Dateien mit 510 Einfügungen und 0 Löschungen.

... ...
@@ -0,0 +1 @@
1
+build/
... ...
@@ -0,0 +1,16 @@
1
+{
2
+	"backend": {
3
+		"scheme": "http",
4
+		"host": "localhost",
5
+		"port": 7979
6
+	},
7
+	"settings": {
8
+		"poll_interval_in_milliseconds": 2000
9
+	},
10
+	"irc": {
11
+		"server": "irc.abtreff.de",
12
+		"predefined_channel": "#ab",
13
+		"predefined_nickname_prefix": "wichtel_"
14
+	}
15
+}
16
+
... ...
@@ -0,0 +1,41 @@
1
+<!DOCTYPE html>
2
+<html>
3
+	<head>
4
+		<meta charset="utf-8"/>
5
+		<title>web-irc</title>
6
+		<link rel="stylesheet" type="text/css" href="style.css"/>
7
+		<script type="text/javascript" src="logic.js"></script>
8
+		<script type="text/javascript">init();</script>
9
+	</head>
10
+	<body class="offline">
11
+		<div id="connect">
12
+			<form action="#">
13
+				<div class="field">
14
+					<label>nickname</label>
15
+					<input type="text" id="nickname" placeholder="nickname"/>
16
+				</div>
17
+				<div class="field">
18
+					<label>channel</label>
19
+					<input type="text" id="channel" placeholder="channel"/>
20
+				</div>
21
+				<input type="submit" value="connect"/>
22
+			</form>
23
+		</div>
24
+		<div id="wait">
25
+			<span>loading …</span>
26
+		</div>
27
+		<div id="main">
28
+			<div id="head">
29
+				<button id="disconnect">exit</button>
30
+			</div>
31
+			<div id="middle">
32
+				<ul class="pane" id="history"></ul>
33
+				<ul class="pane" id="users"></ul>
34
+			</div>
35
+			<form action="#">
36
+				<input type="text" id="message" placeholder="…"/>
37
+				<input type="submit" value="send"/>
38
+			</form>
39
+		</template>
40
+	</body>
41
+</html>
... ...
@@ -0,0 +1,236 @@
1
+type int = number;
2
+type float = number;
3
+
4
+var _conf: any = null;
5
+var _state: (null | string) = null;
6
+var _channel: (null | string) = null;
7
+var _nickname: (null | string) = null;
8
+var _userhash: (null | string) = null;
9
+
10
+function get_timestamp(): int
11
+{
12
+	return Math.floor(Date.now()/1000);
13
+}
14
+
15
+function hash_string_to_unit(x: string): float
16
+{
17
+	return (x.split("").reduce((x, y) => ((x + y.charCodeAt(0)) % 32), 0) / 32);
18
+}
19
+
20
+function get_usercolor(name: string): string
21
+{
22
+	const hue: float = hash_string_to_unit(name);
23
+	return `hsl(${(hue*360).toFixed(2)},50%,75%)`;
24
+}
25
+
26
+async function backend_call(action: string, data: any): Promise<any>
27
+{
28
+	const response = await fetch(
29
+		`${_conf.backend.scheme}://${_conf.backend.host}:${_conf.backend.port.toFixed(0)}`,
30
+		{
31
+			"method": "POST",
32
+			"body": JSON.stringify({"action": action, "data": data}),
33
+		}
34
+	);
35
+	if (response.ok) {
36
+		return response.json();
37
+	}
38
+	else {
39
+		console.error(response.text());
40
+		return Promise.reject<any>(new Error("backend call failed"));
41
+	}
42
+}
43
+
44
+function update_state(): void
45
+{
46
+	document.querySelector("body").setAttribute("class", _state);
47
+}
48
+
49
+function update_history(events): void
50
+{
51
+	let dom_history: HTMLUListElement = document.querySelector("#history");
52
+	for (const event of events) {
53
+		const timestring: string = (new Date(event["timestamp"]*1000)).toISOString().slice(11, 19);
54
+		let dom_event: HTMLLIElement = document.createElement("li");
55
+		dom_event.classList.add("event");
56
+		switch (event["kind"]) {
57
+			default:
58
+				dom_event.textContent = ("-- unhandled event: " + JSON.stringify(event));
59
+				break;
60
+			case "channel_message":
61
+				{
62
+					let dom_time: HTMLDivElement = document.createElement("div");
63
+					dom_time.classList.add("event_time");
64
+					dom_time.textContent = timestring;
65
+					dom_event.appendChild(dom_time);
66
+				}
67
+				{
68
+					let dom_sender: HTMLDivElement = document.createElement("div");
69
+					dom_sender.classList.add("event_sender");
70
+					dom_sender.style.color = get_usercolor(event["data"]["from"] ?? "");
71
+					dom_sender.textContent = event["data"]["from"];
72
+					dom_event.appendChild(dom_sender);
73
+				}
74
+				{
75
+					let dom_message: HTMLDivElement = document.createElement("div");
76
+					dom_message.classList.add("event_message");
77
+					dom_message.textContent = event["data"]["message"];
78
+					dom_event.appendChild(dom_message);
79
+				}
80
+				break;
81
+		}
82
+		dom_history.appendChild(dom_event);
83
+	}
84
+	dom_history.scrollTo(0, dom_history["scrollTopMax"]);
85
+}
86
+
87
+function update_users(users: Array<{name: string; role: string;}>): void
88
+{
89
+	let dom_users: HTMLUListElement = document.querySelector("#users");
90
+	dom_users.textContent = "";
91
+	const users_sorted: Array<{name: string; role: string;}> = users.sort(
92
+		(x, y) => (
93
+			(x.role >= y.role)
94
+			? -1
95
+			: (
96
+				(x.role === y.role)
97
+				? ((x.name < y.name) ? -1 : +1)
98
+				: +1
99
+			)
100
+		)
101
+	);
102
+	for (const user of users_sorted) {
103
+		let dom_user: HTMLLIElement = document.createElement("li");
104
+		dom_user.classList.add("user");
105
+		{
106
+			let dom_role: HTMLSpanElement = document.createElement("span");
107
+			dom_role.textContent = user.role;
108
+			dom_user.appendChild(dom_role);
109
+		}
110
+		{
111
+			let dom_name: HTMLSpanElement = document.createElement("span");
112
+			dom_name.textContent = user.name;
113
+			dom_name.style.color = get_usercolor(user.name);
114
+			dom_user.appendChild(dom_name);
115
+		}
116
+		// dom_user.textContent = `${user.role}${user.name}`;
117
+		dom_users.appendChild(dom_user);
118
+	}
119
+}
120
+
121
+function set_state(state: string): void
122
+{
123
+	_state = state;
124
+	update_state();
125
+}
126
+
127
+function setup_view(): void
128
+{
129
+	document.querySelector<HTMLInputElement>("#channel").value = _conf["irc"]["predefined_channel"];
130
+	document.querySelector<HTMLInputElement>("#nickname").value = (_conf["irc"]["predefined_nickname_prefix"] + (Math.random()*100).toFixed(0));
131
+	setInterval(
132
+		async () => {
133
+			switch (_state) {
134
+				case "offline":
135
+					// do nothing
136
+					break;
137
+				case "checking":
138
+					const ready: boolean = await backend_call("check", null);
139
+					if (ready) {
140
+						set_state("online");
141
+					}
142
+					break;
143
+				case "online":
144
+					const stuff: any = await backend_call("fetch", null);
145
+					update_history(stuff["events"]);
146
+					const userhash: string = btoa(JSON.stringify(stuff["users"]));
147
+					if (_userhash !== userhash) {
148
+						_userhash = userhash;
149
+						update_users(stuff["users"]);
150
+					}
151
+					break;
152
+			}
153
+		},
154
+		_conf["settings"]["poll_interval_in_milliseconds"]
155
+	);
156
+	set_state("offline");
157
+}
158
+
159
+function setup_control(): void
160
+{
161
+	document.querySelector("#connect > form").addEventListener(
162
+		"submit",
163
+		async (event) => {
164
+			let dom_nickname: HTMLInputElement = document.querySelector<HTMLInputElement>("#nickname");
165
+			let dom_channel: HTMLInputElement = document.querySelector<HTMLInputElement>("#channel");
166
+			const nickname: string = dom_nickname.value;
167
+			const channel: string = dom_channel.value;
168
+			await backend_call(
169
+				"connect",
170
+				{
171
+					"server": _conf["irc"]["server"],
172
+					"channels": [channel],
173
+					"nickname": nickname,
174
+				}
175
+			);
176
+			_channel = channel;
177
+			_nickname = nickname;
178
+			set_state("checking");
179
+		}
180
+	);
181
+	document.querySelector("#disconnect").addEventListener(
182
+		"click",
183
+		async (event) => {
184
+			await backend_call(
185
+				"disconnect",
186
+				null
187
+			);
188
+			set_state("offline");
189
+		}
190
+	);
191
+	document.querySelector("#main > form").addEventListener(
192
+		"submit",
193
+		async (event) => {
194
+			event.preventDefault();
195
+			let dom_message: HTMLInputElement = document.querySelector<HTMLInputElement>("#message");
196
+			const message: string = dom_message.value;
197
+			dom_message.value = "";
198
+			dom_message.focus();
199
+			const event_: any = {
200
+				"timestamp": get_timestamp(),
201
+				"kind": "channel_message",
202
+				"data": {
203
+					"from": _nickname,
204
+					"to": _channel,
205
+					"message": message,
206
+				}
207
+			};
208
+			update_history([event_]);
209
+			await backend_call(
210
+				"say",
211
+				{
212
+					"channel": _channel,
213
+					"message": message,
214
+				}
215
+			);
216
+		}
217
+	);
218
+}
219
+
220
+async function main(): Promise<void>
221
+{
222
+	_conf = await fetch("conf.json").then(x => x.json());
223
+	setup_view();
224
+	setup_control();
225
+}
226
+
227
+function init(): void
228
+{
229
+	document.addEventListener(
230
+		"DOMContentLoaded",
231
+		(event) => {
232
+			main();
233
+		}
234
+	);
235
+}
236
+
... ...
@@ -0,0 +1,160 @@
1
+@hue: 120;
2
+
3
+body
4
+{
5
+	background-color: hsl(@hue, 0%, 6.125%);
6
+	color: hsl(@hue, 0%, 93.75%);
7
+	
8
+	font-family: monospace;
9
+	font-size: 1.25em;
10
+}
11
+
12
+input,textarea,button
13
+{
14
+	font-size: 1.25em;
15
+}
16
+
17
+button, input[type="submit"]
18
+{
19
+	background-color: hsl(@hue, 50%, 37.5%);
20
+	color: hsl(@hue, 0%, 100%);
21
+	
22
+	border: none;
23
+	border-radius: 4px;
24
+	
25
+	padding: 8px;
26
+	
27
+	cursor: pointer;
28
+}
29
+
30
+label
31
+{
32
+	display: block;
33
+	font-size: 1.0em;
34
+	font-weight: bold;
35
+	text-transform: capitalize;
36
+}
37
+
38
+.field
39
+{
40
+	display: block;
41
+	margin: 16px 0;
42
+}
43
+
44
+.event > *
45
+{
46
+	display: inline-block;
47
+}
48
+
49
+.event_time
50
+{
51
+	margin: 0 2px;
52
+	
53
+	color: hsl(@hue, 0%, 75%);
54
+	
55
+	&:before
56
+	{
57
+		content: "<";
58
+	}
59
+	
60
+	&:after
61
+	{
62
+		content: ">";
63
+	}
64
+}
65
+
66
+.event_sender
67
+{
68
+	margin: 0 2px;
69
+	
70
+	font-weight: bold;
71
+	
72
+	&:before
73
+	{
74
+		content: "[";
75
+	}
76
+	
77
+	&:after
78
+	{
79
+		content: "]";
80
+	}
81
+}
82
+
83
+.pane
84
+{
85
+	background-color: hsl(@hue, 0%, 12.5%);
86
+	
87
+	height: 75vh;
88
+	overflow-y: auto;
89
+	
90
+	list-style-type: none;
91
+	
92
+	border: 1px solid hsl(@hue, 0%, 50%);
93
+	margin: 4px;
94
+	padding: 8px;
95
+	
96
+	& > li
97
+	{
98
+		margin: 4px 0;
99
+	}
100
+}
101
+
102
+#head
103
+{
104
+	text-align: right;
105
+}
106
+
107
+#middle
108
+{
109
+	display: flex;
110
+	flex-direction: row;
111
+	
112
+	& #history {flex: 4;}	
113
+	& #users {flex: 1;}
114
+}
115
+
116
+#message
117
+{
118
+	// height: 40px;
119
+	width: 80%;
120
+	
121
+	border: none;
122
+	
123
+	background-color: hsl(@hue, 0%, 25%);
124
+	color: hsl(@hue, 0%, 100%);
125
+	
126
+	padding: 8px;
127
+	margin: 4px;
128
+}
129
+
130
+body
131
+{
132
+	&:not(.offline):not(.checking):not(.online)
133
+	{
134
+		& #connect {display: none;}
135
+		& #wait {display: none;}
136
+		& #main {display: none;}
137
+	}
138
+	
139
+	&.offline
140
+	{
141
+		& #connect {}
142
+		& #wait {display: none;}
143
+		& #main {display: none;}
144
+	}
145
+	
146
+	&.checking
147
+	{
148
+		& #connect {display: none;}
149
+		& #wait {}
150
+		& #main {display: none;}
151
+	}
152
+	
153
+	&.online
154
+	{
155
+		& #connect {display: none;}
156
+		& #wait {display: none;}
157
+		& #main {}
158
+	}
159
+}
160
+
... ...
@@ -0,0 +1,4 @@
1
+#!/usr/bin/env sh
2
+
3
+make -f tools/makefile
4
+
... ...
@@ -0,0 +1,52 @@
1
+## directories
2
+
3
+dir_source := source
4
+dir_build := build
5
+
6
+
7
+## commands
8
+
9
+cmd_log := echo "--"
10
+cmd_tsc := tsc --lib es2015,dom
11
+cmd_cp := cp
12
+cmd_lessc := lessc
13
+cmd_mkdir := mkdir -p
14
+
15
+
16
+## rules
17
+
18
+all: structure logic style conf
19
+.PHONY: all
20
+
21
+structure: ${dir_build}/index.html
22
+.PHONY: structure
23
+
24
+${dir_build}/index.html: ${dir_source}/index.html
25
+	@ ${cmd_log} "structure …"
26
+	@ ${cmd_mkdir} $(dir $@)
27
+	@ ${cmd_cp} -ru $^ $@
28
+
29
+logic: ${dir_build}/logic.js
30
+.PHONY: logic
31
+
32
+${dir_build}/logic.js: ${dir_source}/logic.ts
33
+	@ ${cmd_log} "logic …"
34
+	@ ${cmd_mkdir} $(dir $@)
35
+	@ ${cmd_tsc} $^ --outFile $@
36
+
37
+style: ${dir_build}/style.css
38
+.PHONY: style
39
+
40
+${dir_build}/style.css: ${dir_source}/style.less
41
+	@ ${cmd_log} "style …"
42
+	@ ${cmd_mkdir} $(dir $@)
43
+	@ ${cmd_lessc} $^ > $@
44
+
45
+conf: ${dir_build}/conf.json
46
+.PHONY: conf
47
+
48
+${dir_build}/conf.json: ${dir_source}/conf.json
49
+	@ ${cmd_log} "conf …"
50
+	@ ${cmd_mkdir} $(dir $@)
51
+	@ ${cmd_cp} -ru $^ $@
52
+
0 53