disable inter's on-by-default ligatures, redo icon/emote CSS
also: * proper styling for auth page * clean-up of html, css, js * fix scroll indicator eating mouse events * fix scroll indicator on (mobile?) chrome * re-calculate timestamp width on each new message, to account for unknown/unexpected formats like the inclusion of AM/PM
This commit is contained in:
@@ -50,7 +50,7 @@ public class Processing
|
||||
if (chunk is IconChunk { } icon)
|
||||
{
|
||||
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.
|
||||
// Still sanitizing it for the extra safety.
|
||||
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;
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
* {
|
||||
color: var(--fg);
|
||||
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 {
|
||||
@@ -52,17 +52,58 @@ body {
|
||||
background-color: var(--bg-input-hover);
|
||||
}
|
||||
|
||||
body > main {
|
||||
body > main.chat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--bg);
|
||||
border-radius: 20px;
|
||||
|
||||
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 */
|
||||
section#messages {
|
||||
position: relative;
|
||||
@@ -84,6 +125,7 @@ section#messages {
|
||||
width: calc(100% - 200px);
|
||||
height: 200px;
|
||||
background-image: radial-gradient(50% 20% at 50% 100%, #60a0ff40, transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +143,10 @@ section#messages {
|
||||
color: var(--fg-faint);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.message {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,18 +262,54 @@ 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 ***/
|
||||
@media ((max-width: 600px) and (orientation: portrait)) or (max-width: 400px) {
|
||||
body {
|
||||
body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
body > main {
|
||||
body > main {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
section#input {
|
||||
section#input {
|
||||
button {
|
||||
max-width: 0;
|
||||
padding-left: 1.5rem;
|
||||
@@ -239,5 +321,5 @@ section#input {
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
// websocket connection
|
||||
class SSEConnection {
|
||||
constructor() {
|
||||
this.socket = new EventSource('/sse', );
|
||||
this.socket = new EventSource('/sse');
|
||||
|
||||
this.socket.addEventListener('close', (event) => {
|
||||
console.log("Closing SSE connection.")
|
||||
this.socket.close()
|
||||
this.socket.addEventListener('close', () => {
|
||||
console.log('Closing SSE connection.');
|
||||
this.socket.close();
|
||||
});
|
||||
|
||||
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
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -23,15 +22,13 @@ class SSEConnection {
|
||||
// New messages, that require a clean message list before processing
|
||||
this.socket.addEventListener('bulk-messages', (event) => {
|
||||
clearMessages();
|
||||
|
||||
for (let message of JSON.parse(event.data).messages)
|
||||
{
|
||||
for (let message of JSON.parse(event.data).messages) {
|
||||
addMessage(message);
|
||||
}
|
||||
});
|
||||
|
||||
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',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({channel: event.target.value})
|
||||
body: JSON.stringify({ channel: event.target.value })
|
||||
});
|
||||
const content = await rawResponse.json();
|
||||
|
||||
@@ -71,30 +68,39 @@ function updateChannelOptions(channels) {
|
||||
// clear existing channels
|
||||
select.innerHTML = '';
|
||||
|
||||
for (let [ name, channel ] of Object.entries(channels))
|
||||
{
|
||||
for (const [ name, channel ] of Object.entries(channels)) {
|
||||
let option = document.createElement('option');
|
||||
option.text = name;
|
||||
option.value = channel;
|
||||
|
||||
select.appendChild(option)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// functions for handling the message list
|
||||
function scrollMessagesToBottom() {
|
||||
const messagesContainer = document.querySelector('#messages > .scroll-container');
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight - messagesContainer.offsetHeight;
|
||||
}
|
||||
|
||||
function messagesContainerIsScrolledToBottom() {
|
||||
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) {
|
||||
const scrolledToBottom = messagesContainerIsScrolledToBottom();
|
||||
calculateTimestampWidth(messageData.timestamp);
|
||||
|
||||
const liMessage = document.createElement('li');
|
||||
const spanTimestamp = document.createElement('span');
|
||||
@@ -102,16 +108,15 @@ function addMessage(messageData) {
|
||||
const spanMessage = document.createElement('span');
|
||||
spanMessage.classList.add('message');
|
||||
|
||||
// suggestions, update with actual data model
|
||||
spanTimestamp.innerText = messageData.timestamp;
|
||||
spanMessage.innerHTML = messageData.messageHTML; // or build HTML in here
|
||||
spanMessage.innerHTML = messageData.messageHTML;
|
||||
|
||||
liMessage.appendChild(spanTimestamp);
|
||||
liMessage.appendChild(spanMessage);
|
||||
document.getElementById('messages-list').appendChild(liMessage);
|
||||
|
||||
if (scrolledToBottom) {
|
||||
scrollMessagesToBottom();
|
||||
liMessage.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +124,7 @@ function clearMessages() {
|
||||
document.getElementById('messages-list').innerHTML = '';
|
||||
}
|
||||
|
||||
//
|
||||
// add indicator signaling more messages
|
||||
document.querySelector('#messages > .scroll-container').addEventListener('scroll', () => {
|
||||
const messagesContainer = document.querySelector('#messages > .scroll-container');
|
||||
if (!messagesContainerIsScrolledToBottom()) {
|
||||
@@ -137,14 +142,6 @@ document.querySelector('#input > form').addEventListener('submit', (event) => {
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const message = chatInput.value;
|
||||
|
||||
fetch('/send', {
|
||||
method: 'POST',
|
||||
body: message,
|
||||
headers: {
|
||||
'Content-type': 'application/txt; charset=UTF-8'
|
||||
}
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const rawResponse = await fetch('/send', {
|
||||
method: 'POST',
|
||||
@@ -152,7 +149,7 @@ document.querySelector('#input > form').addEventListener('submit', (event) => {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({message: message})
|
||||
body: JSON.stringify({ message: message })
|
||||
});
|
||||
const content = await rawResponse.json();
|
||||
|
||||
@@ -164,26 +161,14 @@ document.querySelector('#input > form').addEventListener('submit', (event) => {
|
||||
});
|
||||
|
||||
|
||||
// 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?
|
||||
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
|
||||
// from kizer, gfd icons
|
||||
async function AddGfdStylesheet(gfdPath, texPath) {
|
||||
const texPromise = LoadTexAsBlob(texPath);
|
||||
const gfdPromise = LoadGfd(gfdPath);
|
||||
const texUrl = URL.createObjectURL(await texPromise);
|
||||
const gfd = await gfdPromise;
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
|
||||
let stylesheets = [];
|
||||
const stylesheets = [];
|
||||
for (const entry of gfd) {
|
||||
if (entry.width * entry.height <= 0)
|
||||
continue;
|
||||
@@ -202,32 +187,30 @@ async function AddGfdStylesheet(gfdPath, texPath) {
|
||||
`background-position: -${entry.left}px -${entry.top}px`,
|
||||
`background-image: url('${texUrl}')`,
|
||||
`width: ${entry.width}px`,
|
||||
`height: ${entry.height}px`,
|
||||
`margin-bottom: -20px`
|
||||
].join(";"),
|
||||
`height: ${entry.height}px`
|
||||
].join(';'),
|
||||
[
|
||||
`background-position: -${entry.left * 2}px -${entry.top * 2 + 341}px`,
|
||||
`background-image: url('${texUrl}')`,
|
||||
`width: ${entry.width * 2}px`,
|
||||
`height: ${entry.height * 2}px`,
|
||||
`margin-bottom: -40px`
|
||||
].join(";")
|
||||
`height: ${entry.height * 2}px`
|
||||
].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) {
|
||||
if (!entry)
|
||||
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-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");
|
||||
styleNode.type = "text/css";
|
||||
const styleNode = document.createElement('style');
|
||||
styleNode.appendChild(document.createTextNode(stylesheet));
|
||||
document.head.appendChild(styleNode);
|
||||
}
|
||||
@@ -235,7 +218,7 @@ async function AddGfdStylesheet(gfdPath, texPath) {
|
||||
async function LoadTexAsBlob(path) {
|
||||
const tex = ParseTex(await (await fetch(path)).arrayBuffer());
|
||||
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);
|
||||
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');
|
||||
|
||||
@@ -3,17 +3,20 @@
|
||||
<head>
|
||||
<title>Authentication</title>
|
||||
|
||||
<link rel="stylesheet" href="/static/start.css">
|
||||
</head>
|
||||
<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>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="stylesheet" href="static/start.css">
|
||||
</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">
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
Reference in New Issue
Block a user