Preliminary new loading screen.

This commit is contained in:
flash 2025-04-11 21:25:40 +00:00
parent 2935d2307b
commit b92e6fcd1c
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
7 changed files with 275 additions and 72 deletions

View file

@ -0,0 +1,36 @@
.throbber {
display: flex;
justify-content: center;
flex-direction: column;
min-width: var(--throbber-container-width, calc(var(--throbber-size, 1) * 100px));
min-height: var(--throbber-container-height, calc(var(--throbber-size, 1) * 100px));
}
.throbber-inline {
display: inline-flex;
min-width: 0;
min-height: 0;
}
.throbber-frame {
display: flex;
justify-content: center;
flex: 0 0 auto;
}
.throbber-icon {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: var(--throbber-gap, calc(var(--throbber-size, 1) * 1px));
margin: var(--throbber-margin, calc(var(--throbber-size, 1) * 10px));
}
.throbber-icon-block {
background: var(--throbber-colour, currentColor);
width: var(--throbber-width, calc(var(--throbber-size, 1) * 10px));
height: var(--throbber-height, calc(var(--throbber-size, 1) * 10px));
}
.throbber-icon-block-hidden {
opacity: 0;
}

View file

@ -71,6 +71,7 @@ a:hover {
@include noscript.css;
@include controls/msgbox.css;
@include controls/throbber.css;
@include controls/views.css;
@include sound/sndtest.css;

View file

@ -1,22 +1,25 @@
.overlay {
font-family: Verdana, Tahoma, Geneva, Arial, Helvetica, sans-serif;
font-size: 15px;
line-height: 20px;
font-size: 16px;
line-height: 25px;
display: flex;
justify-content: flex-start;
align-items: flex-end;
color: #fff;
}
.overlay-filter {
backdrop-filter: blur(2px);
}
.overlay-wrapper {
display: flex;
justify-content: center;
align-items: center;
background-color: #222;
color: #ddd;
text-shadow: 0 0 5px #000;
text-align: center;
box-shadow: inset 0 0 1em #000;
flex: 1 1 auto;
background: #111;
}
.overlay__message {
margin-top: 10px
}
.overlay__status {
font-size: .8em;
color: #888
.overlay-message {
font-size: 1.5em;
line-height: 1.3em;
color: #fff;
}

View file

@ -0,0 +1,183 @@
const ThrobberIcon = function() {
const element = <div class="throbber-icon"/>;
for(let i = 0; i < 9; ++i)
element.appendChild(<div class="throbber-icon-block"/>);
// this is moderately cursed but it'll do
const blocks = [
element.children[3],
element.children[0],
element.children[1],
element.children[2],
element.children[5],
element.children[8],
element.children[7],
element.children[6],
];
let tsLastUpdate;
let counter = 0;
let playing = false;
let delay = 50;
let playResolve;
let pauseResolve;
const update = tsCurrent => {
try {
if(tsLastUpdate !== undefined && (tsCurrent - tsLastUpdate) < delay)
return;
tsLastUpdate = tsCurrent;
for(let i = 0; i < blocks.length; ++i)
blocks[(counter + i) % blocks.length].classList.toggle('throbber-icon-block-hidden', i < 3);
++counter;
} finally {
if(playResolve)
try {
playResolve();
} finally {
playResolve = undefined;
playing = true;
}
if(pauseResolve)
try {
pauseResolve();
} finally {
pauseResolve = undefined;
playing = false;
}
if(playing)
requestAnimationFrame(update);
}
};
const play = () => {
return new Promise(resolve => {
if(playing || playResolve) {
resolve();
return;
}
playResolve = resolve;
requestAnimationFrame(update);
});
};
const pause = () => {
return new Promise(resolve => {
if(!playing || pauseResolve) {
resolve();
return;
}
pauseResolve = resolve;
});
};
const stop = async () => {
await pause();
counter = 0;
};
const restart = async () => {
await stop();
await play();
};
const reverse = () => {
blocks.reverse();
};
const setBlock = (num, state=null) => {
element.children[num].classList.toggle('throbber-icon-block-hidden', !state);
};
const batsu = () => {
setBlock(0, true);setBlock(1, false);setBlock(2, true);
setBlock(3, false);setBlock(4, true);setBlock(5, false);
setBlock(6, true);setBlock(7, false);setBlock(8, true);
};
const maru = () => {
setBlock(0, true);setBlock(1, true);setBlock(2, true);
setBlock(3, true);setBlock(4, false);setBlock(5, true);
setBlock(6, true);setBlock(7, true);setBlock(8, true);
};
return {
get element() { return element; },
get playing() { return playing; },
get delay() { return delay; },
set delay(value) {
if(typeof value !== 'number')
value = parseFloat(value);
if(isNaN(value) || !isFinite(value))
return;
if(value < 0)
value = Math.abs(value);
delay = value;
},
play,
pause,
stop,
restart,
reverse,
batsu,
maru,
};
};
const Throbber = function(options=null) {
if(typeof options !== 'object')
throw 'options must be an object';
let {
element, size, colour,
width, height, inline,
containerWidth, containerHeight,
gap, margin, hidden, paused,
} = options ?? {};
if(typeof element === 'string')
element = document.querySelector(element);
if(!(element instanceof HTMLElement))
element = <div class="throbber"/>;
if(!element.classList.contains('throbber'))
element.classList.add('throbber');
if(inline)
element.classList.add('throbber-inline');
if(hidden)
element.classList.add('hidden');
if(typeof size === 'number' && size > 0)
element.style.setProperty('--throbber-size', size);
if(typeof containerWidth === 'string')
element.style.setProperty('--throbber-container-width', containerWidth);
if(typeof containerHeight === 'string')
element.style.setProperty('--throbber-container-height', containerHeight);
if(typeof gap === 'string')
element.style.setProperty('--throbber-gap', gap);
if(typeof margin === 'string')
element.style.setProperty('--throbber-margin', margin);
if(typeof width === 'string')
element.style.setProperty('--throbber-width', width);
if(typeof height === 'string')
element.style.setProperty('--throbber-height', height);
if(typeof colour === 'string')
element.style.setProperty('--throbber-colour', colour);
let icon;
if(element.childElementCount < 1) {
icon = new ThrobberIcon;
if(!paused) icon.play();
element.appendChild(<div class="throbber-frame">{icon}</div>);
}
return {
get element() { return element; },
get hasIcon() { return icon !== undefined; },
get icon() { return icon; },
get visible() { return !element.classList.contains('hidden'); },
set visible(state) { element.classList.toggle('hidden', !state); },
};
};

View file

@ -74,20 +74,17 @@ const MamiInit = async args => {
await ctx.views.push(loadingOverlay);
if(!('futami' in window)) {
loadingOverlay.message = 'Loading environment...';
try {
window.futami = await FutamiCommon.load();
} catch(ex) {
console.error('Failed to load common settings.', ex);
loadingOverlay.icon = 'cross';
loadingOverlay.header = 'Failed!';
loadingOverlay.message = 'Failed to load common settings.';
loadingOverlay.message = 'Could not load common settings.';
return;
}
}
if(!MamiMisuzuAuth.hasInfo()) {
loadingOverlay.message = 'Fetching credentials...';
try {
const auth = await MamiMisuzuAuth.update();
if(!auth.ok)
@ -108,8 +105,6 @@ const MamiInit = async args => {
}
loadingOverlay.message = 'Loading settings...';
const settings = new MamiSettings(args.settingsPrefix, ctx.events.scopeTo('settings'));
ctx.settings = settings;
@ -163,7 +158,6 @@ const MamiInit = async args => {
settings.define('notificationTriggers').default('').immutable(noNotifSupport).create();
loadingOverlay.message = 'Loading sounds...';
const soundCtx = new MamiSoundContext;
ctx.sound = soundCtx;
@ -229,7 +223,6 @@ const MamiInit = async args => {
// loading these asynchronously makes them not show up in the backlog
// revisit when emote reparsing is implemented
loadingOverlay.message = 'Loading emoticons...';
try {
MamiEmotes.load(await futami.getApiJson('/v1/emotes'));
} catch(ex) {
@ -252,8 +245,6 @@ const MamiInit = async args => {
});
loadingOverlay.message = 'Preparing UI...';
ctx.textTriggers = new MamiTextTriggers;
const sidebar = new MamiSidebar;
@ -407,8 +398,6 @@ const MamiInit = async args => {
});
loadingOverlay.message = 'Building menus...';
MamiCompat('Umi.Parser.SockChatBBcode.EmbedStub', { value: () => {} }); // intentionally a no-op
MamiCompat('Umi.UI.View.SetText', { value: text => { chatForm.input.setText(text); } });
@ -865,18 +854,17 @@ const MamiInit = async args => {
loadingOverlay.message = 'Connecting...';
const setLoadingOverlay = async (icon, header, message, optional) => {
const setLoadingOverlay = async (icon, message, optional) => {
const currentView = ctx.views.current;
if('icon' in currentView) {
currentView.icon = icon;
currentView.header = header;
currentView.message = message;
return currentView;
}
if(!optional) {
const loading = new Umi.UI.LoadingOverlay(icon, header, message);
const loading = new Umi.UI.LoadingOverlay(icon, message);
await ctx.views.push(loading);
}
};
@ -910,12 +898,12 @@ const MamiInit = async args => {
const reconManAttempt = ev => {
if(sockChatRestarting || ev.detail.delay > 2000)
setLoadingOverlay('spinner', 'Connecting...', 'Connecting to server...');
setLoadingOverlay('spinner', `Connecting${'.'.repeat(Math.max(3, ev.detail.attempt))}`);
};
const reconManFail = ev => {
// this is absolutely disgusting but i really don't care right now sorry
if(sockChatRestarting || ev.detail.delay > 2000)
setLoadingOverlay('unlink', sockChatRestarting ? 'Restarting...' : 'Disconnected', `Attempting to reconnect in ${(ev.detail.delay / 1000).toLocaleString()} seconds...<br><a href="javascript:void(0)" onclick="mami.conMan.force()">Retry now</a>`);
setLoadingOverlay('unlink', `Could not reconnect, retrying in ${(ev.detail.delay / 1000).toLocaleString()} seconds... [<a href="javascript:void(0)" onclick="mami.conMan.force()">Force?</a>]`);
};
const reconManSuccess = () => {
conMan.unwatch('success', reconManSuccess);
@ -939,11 +927,10 @@ const MamiInit = async args => {
sockChatHandlers.register();
const conManAttempt = ev => {
let message = ev.detail.attempt > 2 ? `Attempt ${ev.detail.attempt}...` : 'Connecting to server...';
setLoadingOverlay('spinner', 'Connecting...', message);
setLoadingOverlay('spinner', `Connecting${'.'.repeat(Math.max(3, ev.detail.attempt))}`);
};
const conManFail = ev => {
setLoadingOverlay('cross', 'Failed to connect', `Retrying in ${ev.detail.delay / 1000} seconds...`);
setLoadingOverlay('cross', `Could not connect, retrying in ${(ev.detail.delay / 1000).toLocaleString()} seconds... [<a href="javascript:void(0)" onclick="mami.conMan.force()">Force?</a>]`);
};
const conManSuccess = () => {
conMan.unwatch('success', conManSuccess);

View file

@ -35,7 +35,7 @@ const MamiSockChatHandlers = function(
handlers['conn:open'] = ev => {
if(dumpEvents) console.log('conn:open', ev.detail);
setLoadingOverlay('spinner', 'Connecting...', 'Authenticating...', true);
setLoadingOverlay('spinner', 'Authenticating...', true);
};
let legacyUmiConnectFired = false;
@ -106,7 +106,7 @@ const MamiSockChatHandlers = function(
sbUsers.createEntry(ev.detail.user);
if(ctx.views.count > 1)
ctx.views.pop();
ctx.views.pop();
};
handlers['session:fail'] = ev => {
if(dumpEvents) console.log('session:fail', ev.detail);
@ -121,7 +121,7 @@ const MamiSockChatHandlers = function(
return;
}
setLoadingOverlay('cross', 'Failed!', ev.detail.session.outOfConnections ? 'Too many active connections.' : 'Unspecified reason.');
setLoadingOverlay('cross', ev.detail.session.outOfConnections ? 'You have too many active connections.' : 'Could not authenticate.');
};
handlers['session:term'] = ev => {
if(dumpEvents) console.log('session:term', ev.detail);

View file

@ -1,60 +1,53 @@
Umi.UI.LoadingOverlay = function(icon, header, message) {
const icons = {
'spinner': 'fas fa-3x fa-fw fa-spinner fa-pulse',
'checkmark': 'fas fa-3x fa-fw fa-check-circle',
'cross': 'fas fa-3x fa-fw fa-times-circle',
'hammer': 'fas fa-3x fa-fw fa-gavel',
'bomb': 'fas fa-3x fa-fw fa-bomb',
'unlink': 'fas fa-3x fa-fw fa-unlink',
'reload': 'fas fa-3x fa-fw fa-sync fa-spin',
'warning': 'fas fa-exclamation-triangle fa-3x',
'question': 'fas fa-3x fa-question-circle',
'poop': 'fas fa-3x fa-poop',
};
#include controls/throbber.jsx
let iconElem, headerElem, messageElem;
const html = <div class="overlay">
<div class="overlay__inner">
{iconElem = <div class="fas fa-3x fa-question-circle" />}
{headerElem = <div class="overlay__message" />}
{messageElem = <div class="overlay__status" />}
</div>
Umi.UI.LoadingOverlay = function(icon, message) {
let throbber, wrapper, messageElem;
const html = <div class="overlay overlay-filter">
{wrapper = <div class="overlay-wrapper">
<div class="overlay-icon">
{throbber = <Throbber paused={true} size={2} inline={true} />}
</div>
<div class="overlay-message">
{messageElem = <div class="overlay-message-text" />}
</div>
</div>}
</div>;
const setIcon = name => {
name = (name || '').toString();
if(!(name in icons))
name = 'question';
iconElem.className = icons[name];
if(name === 'spinner') {
if(!throbber.icon.playing)
throbber.icon.restart();
} else
throbber.icon.stop().then(() => { throbber.icon.batsu(); });
};
const setHeader = text => headerElem.textContent = (text || '').toString();
const setMessage = text => messageElem.innerHTML = (text || '').toString();
setIcon(icon);
setHeader(header);
setMessage(message);
return {
get icon() { return iconElem.className; },
get icon() { return throbber.icon.playing ? 'spinner' : 'cross'; },
set icon(value) { setIcon(value); },
get header() { return headerElem.textContent; },
set header(value) { setHeader(value); },
get message() { return messageElem.innerHTML; },
set message(value) { setMessage(value); },
get element() { return html; },
getViewTransition: mode => {
getViewTransition(mode) {
if(mode === 'pop')
return ctx => MamiAnimate({
async: true,
duration: 200,
easing: 'inOutSine',
easing: 'inQuint',
start: () => {
ctx.fromElem.style.pointerEvents = 'none';
html.classList.remove('overlay-filter');
html.style.pointerEvents = 'none';
},
update: t => {
ctx.fromElem.style.transform = `scale(${1 + (.25 * t)})`;
ctx.fromElem.style.opacity = 1 - (1 * t).toString();
wrapper.style.bottom = `${0 - (104 * t)}px`;
wrapper.style.opacity = 1 - (1 * t).toString();
},
});
},