Merge pull request #116 from Ennea/main

Some frontend clean-up, additions, fixes
This commit is contained in:
Infi
2024-08-26 22:37:25 +02:00
committed by GitHub
5 changed files with 166 additions and 98 deletions
+2 -2
View File
@@ -50,7 +50,7 @@ public class Processing
if (chunk is IconChunk { } icon) if (chunk is IconChunk { } icon)
{ {
return IconUtil.GfdFileView.TryGetEntry((uint) icon.Icon, out _) return IconUtil.GfdFileView.TryGetEntry((uint) icon.Icon, out _)
? $"<span class=\"gfd-icon gfd-icon-hq-{(uint)icon.Icon}\" style=\"zoom:calc(16 * 4 / 3 / 40 * 1.4)\"></span>" ? $"<span class=\"gfd-icon gfd-icon-hq-{(uint)icon.Icon}\"></span>"
: ""; : "";
} }
@@ -63,7 +63,7 @@ public class Processing
// The emote name should be safe, it is checked against a list from BTTV. // The emote name should be safe, it is checked against a list from BTTV.
// Still sanitizing it for the extra safety. // Still sanitizing it for the extra safety.
if (image is { Failed: false }) if (image is { Failed: false })
return $"<span style=\"zoom:calc(16 * 4 / 3 / 40 * 1.4)\"><img class=\"emote-icon emote-icon-hq\" src=\"/emote/{Sanitizer.Sanitize(emotePayload.Code)}\"></span>"; return $"<span class=\"emote-icon\"><img src=\"/emote/{Sanitizer.Sanitize(emotePayload.Code)}\"></span>";
} }
var colour = text.Foreground; var colour = text.Foreground;
+104 -22
View File
@@ -38,7 +38,7 @@
* { * {
color: var(--fg); color: var(--fg);
font-family: Lodestone, 'Inter var', sans-serif; font-family: Lodestone, 'Inter var', sans-serif;
font-feature-settings: 'tnum'; font-feature-settings: 'tnum', 'calt' 0; /* calt appears to be on by default */
} }
html { html {
@@ -52,17 +52,58 @@ body {
background-color: var(--bg-input-hover); background-color: var(--bg-input-hover);
} }
body > main { body > main.chat {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: var(--bg); background-color: var(--bg);
border-radius: 20px; border-radius: 20px;
box-shadow: 0 0 50px 0 rgba(0, 0, 0, 0.5); box-shadow: 0 0 50px 0 rgba(0, 0, 0, 0.5);
} }
body > main.auth {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
width: 100%;
height: 100%;
h1 {
font-size: 1.5rem;
font-weight: 400;
}
input { width: 150px; }
input, button {
padding: 5px 20px;
font-size: 1rem;
border: 3px solid transparent;
border-radius: 20px;
background-color: var(--bg-input);
&:focus {
outline: 2px solid var(--focus-color);
}
}
button {
padding: 5px 15px;
border: 3px solid var(--bg-input);
background-image: var(--gradient-clickable);
cursor: pointer;
&:hover {
border-color: var(--bg-input-hover);
background-color: var(--bg-input-hover);
background-image: var(--gradient-clickable-hover);
}
}
}
/* message list */ /* message list */
section#messages { section#messages {
position: relative; position: relative;
@@ -84,6 +125,7 @@ section#messages {
width: calc(100% - 200px); width: calc(100% - 200px);
height: 200px; height: 200px;
background-image: radial-gradient(50% 20% at 50% 100%, #60a0ff40, transparent); background-image: radial-gradient(50% 20% at 50% 100%, #60a0ff40, transparent);
pointer-events: none;
} }
} }
@@ -101,6 +143,10 @@ section#messages {
color: var(--fg-faint); color: var(--fg-faint);
text-align: right; text-align: right;
} }
.message {
white-space: pre-wrap;
}
} }
} }
@@ -216,28 +262,64 @@ section#input {
} }
} }
/* icons, emotes */
.gfd-icon {
display: inline-block;
position: relative;
vertical-align: middle;
zoom: 0.75;
&::before {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
transform: translateY(-50%);
pointer-events: none;
}
}
.emote-icon {
display: inline-block;
position: relative;
width: 2rem;
vertical-align: middle;
img {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
width: 2rem;
transform: translateY(-50%);
pointer-events: none;
}
}
/*** mobile ***/ /*** mobile ***/
@media ((max-width: 600px) and (orientation: portrait)) or (max-width: 400px) { @media ((max-width: 600px) and (orientation: portrait)) or (max-width: 400px) {
body { body {
padding: 0; padding: 0;
}
body > main {
border-radius: 0;
box-shadow: none;
}
section#input {
button {
max-width: 0;
padding-left: 1.5rem;
padding-right: 1.5rem;
color: transparent;
} }
button::before { body > main {
left: 50%; border-radius: 0;
transform: translateX(-50%) translateY(-50%); box-shadow: none;
}
section#input {
button {
max-width: 0;
padding-left: 1.5rem;
padding-right: 1.5rem;
color: transparent;
}
button::before {
left: 50%;
transform: translateX(-50%) translateY(-50%);
}
} }
} }
}
+44 -61
View File
@@ -1,21 +1,20 @@
// websocket connection // websocket connection
class SSEConnection { class SSEConnection {
constructor() { constructor() {
this.socket = new EventSource('/sse', ); this.socket = new EventSource('/sse');
this.socket.addEventListener('close', (event) => { this.socket.addEventListener('close', () => {
console.log("Closing SSE connection.") console.log('Closing SSE connection.');
this.socket.close() this.socket.close();
}); });
this.socket.addEventListener('switch-channel', (event) => { this.socket.addEventListener('switch-channel', (event) => {
updateChannelHint(JSON.parse(event.data).channel) updateChannelHint(JSON.parse(event.data).channel);
}); });
// New messages that are able to be directly processed // New messages that are able to be directly processed
this.socket.addEventListener('new-message', (event) => { this.socket.addEventListener('new-message', (event) => {
for (let message of JSON.parse(event.data).messages) for (let message of JSON.parse(event.data).messages) {
{
addMessage(message); addMessage(message);
} }
}); });
@@ -23,15 +22,13 @@ class SSEConnection {
// New messages, that require a clean message list before processing // New messages, that require a clean message list before processing
this.socket.addEventListener('bulk-messages', (event) => { this.socket.addEventListener('bulk-messages', (event) => {
clearMessages(); clearMessages();
for (let message of JSON.parse(event.data).messages) {
for (let message of JSON.parse(event.data).messages)
{
addMessage(message); addMessage(message);
} }
}); });
this.socket.addEventListener('channel-list', (event) => { this.socket.addEventListener('channel-list', (event) => {
updateChannelOptions(JSON.parse(event.data).channels) updateChannelOptions(JSON.parse(event.data).channels);
}); });
} }
@@ -56,7 +53,7 @@ document.getElementById('channel-select').addEventListener('change', (event) =>
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({channel: event.target.value}) body: JSON.stringify({ channel: event.target.value })
}); });
const content = await rawResponse.json(); const content = await rawResponse.json();
@@ -71,30 +68,39 @@ function updateChannelOptions(channels) {
// clear existing channels // clear existing channels
select.innerHTML = ''; select.innerHTML = '';
for (let [ name, channel ] of Object.entries(channels)) for (const [ name, channel ] of Object.entries(channels)) {
{
let option = document.createElement('option'); let option = document.createElement('option');
option.text = name; option.text = name;
option.value = channel; option.value = channel;
select.appendChild(option) select.appendChild(option)
} }
} }
// functions for handling the message list // functions for handling the message list
function scrollMessagesToBottom() {
const messagesContainer = document.querySelector('#messages > .scroll-container');
messagesContainer.scrollTop = messagesContainer.scrollHeight - messagesContainer.offsetHeight;
}
function messagesContainerIsScrolledToBottom() { function messagesContainerIsScrolledToBottom() {
const messagesContainer = document.querySelector('#messages > .scroll-container'); const messagesContainer = document.querySelector('#messages > .scroll-container');
return messagesContainer.scrollTop == messagesContainer.scrollHeight - messagesContainer.offsetHeight; return messagesContainer.scrollTop >= messagesContainer.scrollHeight - messagesContainer.offsetHeight;
}
// calculate timestamp width
// to ensure that all timestamps have the same width. some typefaces have the same width across
// all number glyphs, others do not. the solution below is very rudimentary; at the very least,
// delaying it to account for font loading might make sense. perhaps there's an even better way?
let maxTimestampWidth = 0;
function calculateTimestampWidth(timestamp) {
const widthProbe = document.getElementById('timestamp-width-probe');
widthProbe.innerText = timestamp;
if (widthProbe.clientWidth > maxTimestampWidth) {
maxTimestampWidth = widthProbe.clientWidth;
document.body.style.setProperty('--timestamp-width', (Math.ceil(maxTimestampWidth) + 1) + 'px');
}
} }
function addMessage(messageData) { function addMessage(messageData) {
const scrolledToBottom = messagesContainerIsScrolledToBottom(); const scrolledToBottom = messagesContainerIsScrolledToBottom();
calculateTimestampWidth(messageData.timestamp);
const liMessage = document.createElement('li'); const liMessage = document.createElement('li');
const spanTimestamp = document.createElement('span'); const spanTimestamp = document.createElement('span');
@@ -102,16 +108,15 @@ function addMessage(messageData) {
const spanMessage = document.createElement('span'); const spanMessage = document.createElement('span');
spanMessage.classList.add('message'); spanMessage.classList.add('message');
// suggestions, update with actual data model
spanTimestamp.innerText = messageData.timestamp; spanTimestamp.innerText = messageData.timestamp;
spanMessage.innerHTML = messageData.messageHTML; // or build HTML in here spanMessage.innerHTML = messageData.messageHTML;
liMessage.appendChild(spanTimestamp); liMessage.appendChild(spanTimestamp);
liMessage.appendChild(spanMessage); liMessage.appendChild(spanMessage);
document.getElementById('messages-list').appendChild(liMessage); document.getElementById('messages-list').appendChild(liMessage);
if (scrolledToBottom) { if (scrolledToBottom) {
scrollMessagesToBottom(); liMessage.scrollIntoView();
} }
} }
@@ -119,7 +124,7 @@ function clearMessages() {
document.getElementById('messages-list').innerHTML = ''; document.getElementById('messages-list').innerHTML = '';
} }
// // add indicator signaling more messages
document.querySelector('#messages > .scroll-container').addEventListener('scroll', () => { document.querySelector('#messages > .scroll-container').addEventListener('scroll', () => {
const messagesContainer = document.querySelector('#messages > .scroll-container'); const messagesContainer = document.querySelector('#messages > .scroll-container');
if (!messagesContainerIsScrolledToBottom()) { if (!messagesContainerIsScrolledToBottom()) {
@@ -137,14 +142,6 @@ document.querySelector('#input > form').addEventListener('submit', (event) => {
const chatInput = document.getElementById('chat-input'); const chatInput = document.getElementById('chat-input');
const message = chatInput.value; const message = chatInput.value;
fetch('/send', {
method: 'POST',
body: message,
headers: {
'Content-type': 'application/txt; charset=UTF-8'
}
});
(async () => { (async () => {
const rawResponse = await fetch('/send', { const rawResponse = await fetch('/send', {
method: 'POST', method: 'POST',
@@ -152,7 +149,7 @@ document.querySelector('#input > form').addEventListener('submit', (event) => {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({message: message}) body: JSON.stringify({ message: message })
}); });
const content = await rawResponse.json(); const content = await rawResponse.json();
@@ -164,26 +161,14 @@ document.querySelector('#input > form').addEventListener('submit', (event) => {
}); });
// calculate timestamp width // from kizer, gfd icons
// to ensure that all timestamps have the same width. some typefaces have the same width across
// all number glyphs, others do not. the solution below is very rudimentary; at the very least,
// delaying it to account for font loading might make sense. perhaps there's an even better way?
window.setTimeout(() => {
const widthProbe = document.getElementById('timestamp-width-probe');
widthProbe.innerText = '88:88'; // assume 8 to be widest glyph
document.body.style.setProperty('--timestamp-width', Math.ceil(widthProbe.clientWidth) + 'px');
}, 100);
// From kizer
async function AddGfdStylesheet(gfdPath, texPath) { async function AddGfdStylesheet(gfdPath, texPath) {
const texPromise = LoadTexAsBlob(texPath); const texPromise = LoadTexAsBlob(texPath);
const gfdPromise = LoadGfd(gfdPath); const gfdPromise = LoadGfd(gfdPath);
const texUrl = URL.createObjectURL(await texPromise); const texUrl = URL.createObjectURL(await texPromise);
const gfd = await gfdPromise; const gfd = await gfdPromise;
let width = 0;
let height = 0;
let stylesheets = []; const stylesheets = [];
for (const entry of gfd) { for (const entry of gfd) {
if (entry.width * entry.height <= 0) if (entry.width * entry.height <= 0)
continue; continue;
@@ -202,32 +187,30 @@ async function AddGfdStylesheet(gfdPath, texPath) {
`background-position: -${entry.left}px -${entry.top}px`, `background-position: -${entry.left}px -${entry.top}px`,
`background-image: url('${texUrl}')`, `background-image: url('${texUrl}')`,
`width: ${entry.width}px`, `width: ${entry.width}px`,
`height: ${entry.height}px`, `height: ${entry.height}px`
`margin-bottom: -20px` ].join(';'),
].join(";"),
[ [
`background-position: -${entry.left * 2}px -${entry.top * 2 + 341}px`, `background-position: -${entry.left * 2}px -${entry.top * 2 + 341}px`,
`background-image: url('${texUrl}')`, `background-image: url('${texUrl}')`,
`width: ${entry.width * 2}px`, `width: ${entry.width * 2}px`,
`height: ${entry.height * 2}px`, `height: ${entry.height * 2}px`
`margin-bottom: -40px` ].join(';'),
].join(";") entry.width
]; ];
} }
let stylesheet = ".gfd-icon::before { content: ''; display: inline-block; overflow: hidden; vertical-align: top; height:0; }\n"; let stylesheet = '';
for (const entry of stylesheets) { for (const entry of stylesheets) {
if (!entry) if (!entry)
continue; continue;
stylesheet += `\n${entry[0].map(x => `.gfd-icon.gfd-icon-${x}::before`).join(', ')}{${entry[1]};}`; stylesheet += `\n${entry[0].map(x => `.gfd-icon.gfd-icon-${x}::before`).join(', ')}{${entry[1]};}`;
stylesheet += `\n${entry[0].map(x => `.gfd-icon.gfd-icon-hq-${x}::before`).join(', ')}{${entry[2]};}`; stylesheet += `\n${entry[0].map(x => `.gfd-icon.gfd-icon-hq-${x}::before`).join(', ')}{${entry[2]};}`;
stylesheet += `\n${entry[0].map(x => `.gfd-icon.gfd-icon-${x}`).join(', ')}{width:${entry[3]}px;}`;
stylesheet += `\n${entry[0].map(x => `.gfd-icon.gfd-icon-hq-${x}`).join(', ')}{width:${entry[3] * 2}px;}`;
} }
stylesheet += "\n.emote-icon { content: ''; display: inline-block; overflow: hidden; vertical-align: top; }";
stylesheet += `\n.emote-icon.emote-icon-hq {width: ${width * 2}px; height: ${height * 2}px; margin-bottom: -40px}`;
const styleNode = document.createElement("style"); const styleNode = document.createElement('style');
styleNode.type = "text/css";
styleNode.appendChild(document.createTextNode(stylesheet)); styleNode.appendChild(document.createTextNode(stylesheet));
document.head.appendChild(styleNode); document.head.appendChild(styleNode);
} }
@@ -235,7 +218,7 @@ async function AddGfdStylesheet(gfdPath, texPath) {
async function LoadTexAsBlob(path) { async function LoadTexAsBlob(path) {
const tex = ParseTex(await (await fetch(path)).arrayBuffer()); const tex = ParseTex(await (await fetch(path)).arrayBuffer());
if (tex.format !== 0x1450) // B8G8R8A8 if (tex.format !== 0x1450) // B8G8R8A8
throw "Not supported"; throw 'Not supported';
const dataArray = new Uint8ClampedArray(tex.buffer, tex.offsetToSurface[0], tex.width * tex.height * 4); const dataArray = new Uint8ClampedArray(tex.buffer, tex.offsetToSurface[0], tex.width * tex.height * 4);
for (let i = 0; i < dataArray.length; i += 4) { for (let i = 0; i < dataArray.length; i += 4) {
@@ -297,4 +280,4 @@ function ParseTex(arrayBuffer) {
}; };
} }
AddGfdStylesheet("/files/gfdata.gfd", "/files/fonticon_ps5.tex"); AddGfdStylesheet('/files/gfdata.gfd', '/files/fonticon_ps5.tex');
+14 -11
View File
@@ -3,17 +3,20 @@
<head> <head>
<title>Authentication</title> <title>Authentication</title>
<link rel="stylesheet" href="/static/start.css"> <meta charset="utf-8">
</head> <meta name="viewport" content="width=device-width, initial-scale=1">
<body>
<div style="text-align: center;">
<h3 style="color: #fff">Authcode</h3>
<form action="/auth" method="POST">
<input style="color: #fff; background: #121212" type="password" id="authcode" name="authcode">
<input style="color: #fff; background: #121212" type="submit" value="Submit">
</form>
<img src="/emote/Sure"> <link rel="stylesheet" href="static/start.css">
</div> </head>
<body>
<main class="auth">
<h1>Authcode</h1>
<form action="/auth" method="POST">
<input type="password" name="authcode">
<button type="submit">Submit</button>
</form>
<img src="/emote/Sure">
</main>
</body> </body>
</html> </html>
Binary file not shown.