Preliminary new loading screen.
This commit is contained in:
parent
2935d2307b
commit
b92e6fcd1c
7 changed files with 275 additions and 72 deletions
src
mami.css
mami.js
36
src/mami.css/controls/throbber.css
Normal file
36
src/mami.css/controls/throbber.css
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
183
src/mami.js/controls/throbber.jsx
Normal file
183
src/mami.js/controls/throbber.jsx
Normal 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); },
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue