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,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 |