Compare commits

...
Sign in to create a new pull request.

76 commits

Author SHA1 Message Date
a3debba954
Fixed accidental checkbox behaviour change in messages. 2025-06-18 00:47:43 +00:00
a2cdedb03c
Fixed some detections. 2025-06-18 00:37:21 +00:00
9a28ec0a77
Cleaned up the code for the remaining macros. 2025-06-18 00:32:34 +00:00
c681e2699e
Switched to correct auth data fetcher. 2025-06-17 16:12:54 +00:00
c0a838773d
why was this using a macro????? 2025-06-17 15:18:20 +00:00
ce9272b935
Replaced all forum macros with includes. 2025-06-17 15:15:08 +00:00
0babeb9390
First batch of Twig macro reduction, forum ones will probably take as long as these combined. 2025-06-17 01:20:14 +00:00
8ef46fe475
Minor spacing fixes. 2025-06-16 20:59:05 +00:00
c8db915142
Increased base font size and switched to Inter, Victor Mono and Zen Kaku Gothic New.
You'll likely run into some jank with this update, and this is also the first in a series of visual overhauls to come.

I've been wanting to increase the base font size from 12px to 16px for a while now but never actually got around to it until getting really tired of the limits imposed on me by Verdana.
It will require some getting used to, even for me, though!
The only thing that really stands out is that big walls of text like news and forum posts are a bit bigger and actually have a non-claustrophobic amount of spacing inbetween them.

My efforts to avoid shoving too much spacing into things have severely backfired into making me afraid to introduce any in the first place.
Personally, I think this reads a lot nicer!

Verdana won't be gone for good though.
Once enough of the API is ready it will be featured on the Flashii Fundamentals for Legacy PCs subsite!!
2025-06-16 20:52:25 +00:00
a0e719c2d1
Updated libraries. 2025-06-12 20:17:54 +00:00
d8dbfaa7ab
Added leeway to the device code token polling process. 2025-05-18 15:55:06 +00:00
4e66852db1
Fixed tag script, potentially. 2025-05-13 17:28:26 +00:00
40a96029ff
Switched to the railgun/jwt library. 2025-05-13 17:27:04 +00:00
dd8ec7c8dd
Cleaned up forum templates. 2025-04-24 19:17:38 +00:00
5ec7dddd0e
Fixed categories without topics not being browseable. 2025-04-24 18:40:41 +00:00
af476491b2
Made OAuth2 IDs snowflakes and also removed the client_id field. 2025-04-24 00:17:35 +00:00
c91fa69362
Altered default token lifetimes and also made them configurable. 2025-04-23 21:56:33 +00:00
093266eb1e
Allow access and refresh tokens to not be associated with an app. 2025-04-23 21:24:02 +00:00
d9cf9674f3
Check for empty array. 2025-04-22 22:20:13 +00:00
e1155158d9
Should probably also bump the version. 2025-04-22 21:58:24 +00:00
86578c9ff4
Forgot to update the migration, oops. 2025-04-22 21:58:07 +00:00
3bd556fba9
Added chat server selection endpoints. 2025-04-22 21:55:51 +00:00
52632b42eb
Fixed oversights. 2025-04-22 21:10:47 +00:00
ca2c192330
Added new token/login endpoints for chat. 2025-04-22 20:16:55 +00:00
85c71f604a
Use URI instead of URL in the API.
URL equivalent of a field can still be requested using the fields parameter for the existing routes.
2025-04-22 18:33:47 +00:00
1d57fc3b45
Added kaomoji list to the database. 2025-04-21 01:25:02 +00:00
45635ddc5b
Updated libraries. 2025-04-21 00:52:00 +00:00
d675f1410f
Ported removeChild util. 2025-04-13 00:46:51 +00:00
a6b3cef7b4
Fixed incorrect path being used for CORS credentials check. 2025-04-12 21:15:14 +00:00
4b4ae1c820
Added Cache-Control rules for existing APIs. 2025-04-12 20:33:44 +00:00
d15e074674
Use --env-file-if-exists instead. 2025-04-11 19:28:57 +00:00
c7dc5de306
That should've been install, not update. 2025-04-11 19:02:37 +00:00
e8dcd5b878
Removed push. 2025-04-11 18:59:15 +00:00
4da099ca74
Use make for calling various scripts correctly. 2025-04-11 18:57:13 +00:00
0d8283495d
Allow filtering to a specific type of asset task. 2025-04-11 18:15:50 +00:00
99e3e3111a
Share .env file with the NodeJS build script. 2025-04-11 18:02:38 +00:00
35dce01323
Fixed authentication oversights. 2025-04-03 20:35:57 +00:00
277afa5b19
Removed SameSite=Strict. 2025-04-03 20:26:04 +00:00
55dc011df6
Also check folder permissions for the EEPROM scripts. 2025-04-03 20:09:18 +00:00
9d70505ad8
Added utility to nuke the templating cache directory. 2025-04-03 20:01:27 +00:00
155b301405
Fixed this thing being a layer up too high. 2025-04-03 19:42:43 +00:00
83068a4183
Replaced timings in the footer with a visual for the Server-Timing header. 2025-04-03 19:40:10 +00:00
ceb6bece09
Improved templating engine wrapping. 2025-04-03 14:37:19 +00:00
e4c3e4c052
Authn/z rework. 2025-03-31 15:35:24 +00:00
28be4f16c2
Added optional alphaless RGB field. 2025-03-30 19:26:54 +00:00
16a7e20441
Logging cleanup. 2025-03-28 20:19:54 +00:00
d1fad37022
Added new tables to data exports. 2025-03-28 02:05:40 +00:00
a1398fb179
Added field filtering to API endpoints and store colour presets in the database. 2025-03-28 01:47:24 +00:00
12d40e69a5
Tighter client side integrations for uploads. 2025-03-27 21:27:49 +00:00
41e27cdffa
Ensure storage folder exists. 2025-03-27 00:57:53 +00:00
104bf379b0
One day I won't forgot to bump VERSION. 2025-03-27 00:45:46 +00:00
5f6133c007
Shoved EEPROM into Misuzu. 2025-03-27 00:44:42 +00:00
0b7031959b
Removed RPCii dependency. 2025-03-25 14:28:04 +00:00
6c50a582cb
:))) 2025-03-25 14:22:28 +00:00
238bb3f48e
Updated leftovers from rebase. 2025-03-25 13:51:39 +00:00
5ba8b30047 Updated to latest Index as well as some minor bug fixes. 2025-03-25 14:27:48 +01:00
ad89d45cf0
Fixed oversight. 2025-03-25 12:23:40 +00:00
e3707bf2b4
Moved /v1/me over. 2025-03-24 22:15:52 +00:00
649968814a
Forgot the VERSION bump again (its gonna get confusing). 2025-03-24 19:04:16 +00:00
477210411d
Moved /v1/emotes. 2025-03-24 19:03:05 +00:00
b6f5a4e0cc
Styled landing, Bluesky and Fedi redirect pages. 2025-02-27 00:17:00 +00:00
7e75a71bd2
Fixed oversight. 2025-02-26 22:06:00 +00:00
61ab586df8
Bumped VERSION. 2025-02-26 22:03:55 +00:00
17251cf750
PHPStan fixes. 2025-02-26 22:03:13 +00:00
446c675613
JWT out of the OpenID namespace and merged the remainder with the OAuth namespace. 2025-02-26 21:01:32 +00:00
b2f8347526
OAuth metadata endpoints. 2025-02-26 18:06:46 +00:00
e5a947e973
Moved user agent string creation thingy into a method. 2025-02-26 15:14:30 +00:00
676e3fb217
Switched Xrpc client to Guzzle. 2025-02-26 14:57:19 +00:00
d3bdd8f3a2
Removed Awaki from the source code list.
(Excuse to test GPG signing)
2025-02-25 19:34:40 +00:00
b1d9f88549 Don't use X-Accel-Redirect for the maintenance page, it overrides the status code. 2025-02-25 15:14:17 +00:00
aaba24894c Added OpenID Connect. 2025-02-25 02:30:24 +00:00
24d93a5dbf Slight adjustment to style assignment. 2025-02-20 19:17:17 +00:00
55fca7b945 Adjusted fragment creation. 2025-02-20 18:21:36 +00:00
eb72573121 Reverted to the old removeChildren implementation. 2025-02-20 18:09:17 +00:00
604771aac3 Bumped VERSION. 2025-02-20 02:23:25 +00:00
7353553de7 Rewrote the comments system. ()
old one basically bitrotted to death, may it rinse in prosciutto

Reviewed-on: 
Co-authored-by: flashwave <me@flash.moe>
Co-committed-by: flashwave <me@flash.moe>
2025-02-20 02:19:32 +00:00
467 changed files with 16611 additions and 8004 deletions

View file

@ -6,3 +6,6 @@ insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4
[Makefile]
indent_style = tab

View file

@ -1,3 +1,7 @@
# Enable debug mode
# Useful for debugging
MSZ_DEBUG=1
# Database connection setting
# This uses the Index library's DSN syntax
# Currently Misuzu only supports MariaDB, or Null of course since that does nothing
@ -23,3 +27,15 @@ DATABASE_DSN="null:"
# But to make things more understandable, the value for Flashii is also included
#DOMAIN_ROLES="flashii.net=main; fii.moe=redirect"
DOMAIN_ROLES="localhost=main,redirect:/go"
# Local storage path
# This determines where uploaded files are stored. If left unset, the storage folder in the project tree will be used.
#STORAGE_PATH_LOCAL="/path/to/storage"
# Remote storage path
# Path on which the storage folder is exposed to the web for NGINX.
#STORAGE_PATH_REMOTE="/_storage"
# Template cache directory
# Writeable directory path to which template files are cached.
#TEMPLATE_CACHE="/tmp/msz-tpl-cache"

3
.gitignore vendored
View file

@ -16,10 +16,13 @@
/config/github.cfg
/config/config.ini
/config/github.ini
/config/keys/*.pem
/.debug
/.migrating
# Storage
/storage/*
!/storage/.gitkeep
/store
# OS specific

58
Makefile Normal file
View file

@ -0,0 +1,58 @@
PHP_EXE := $(shell which php8.4)
COMPOSER_EXE := $(shell which composer)
NODE_EXE := $(shell which node)
NPM_EXE := $(shell which npm)
SUDO_EXE := $(shell which sudo)
WWW_USER := www-data
BUILD_EXE := build.js
ENV_FILE := .env
TOOLS_DIR := tools
VENDOR_DIR := vendor
all: install migrate clear-cache build cron
install: composer_install npm_install
composer_install:
${PHP_EXE} ${COMPOSER_EXE} install
npm_install:
${NPM_EXE} ci
update: composer_update npm_update
composer_update:
${PHP_EXE} ${COMPOSER_EXE} update
npm_update:
${NPM_EXE} update --save
migrate:
${SUDO_EXE} -u ${WWW_USER} ${PHP_EXE} ${TOOLS_DIR}/migrate
clear-cache:
${SUDO_EXE} -u ${WWW_USER} ${PHP_EXE} ${TOOLS_DIR}/nuke-tpl-cache
cron:
${SUDO_EXE} -u ${WWW_USER} ${PHP_EXE} ${TOOLS_DIR}/cron slow
build:
${NODE_EXE} --env-file-if-exists ${ENV_FILE} ${BUILD_EXE}
rebuild-css:
${NODE_EXE} --env-file-if-exists ${ENV_FILE} ${BUILD_EXE} css
rebuild-js:
${NODE_EXE} --env-file-if-exists ${ENV_FILE} ${BUILD_EXE} js
rebuild-twig:
${NODE_EXE} --env-file-if-exists ${ENV_FILE} ${BUILD_EXE} twig
analyse:
${PHP_EXE} ${VENDOR_DIR}/bin/phpstan
tag: analyse
${PHP_EXE} ${TOOLS_DIR}/create-tag
.PHONY: all update composer npm build rebuild-css rebuild-js rebuild-twig migrate clear-cache cron analyse tag

View file

@ -1 +1 @@
20250210.1
20250617.1

View file

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

View file

@ -17,6 +17,17 @@ html, body {
}
:root {
--font-regular: Verdana, Geneva, 'Dejavu Sans', Arial, Helvetica, sans-serif;
--font-monospace: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
--font-regular: Inter, 'Zen Kaku Gothic New', sans-serif;
--font-monospace: 'Victor Mono', monospace;
font-feature-settings: 'calt' 1, 'dlig' 1, 'ss01' 1;
font-optical-sizing: auto;
}
@supports (font-variation-settings: normal) {
:root {
--font-regular: InterVariable, 'Zen Kaku Gothic New', sans-serif;
}
}
@include loading.css;
@include perf.css;

101
assets/common.css/perf.css Normal file
View file

@ -0,0 +1,101 @@
.msz-perfs {
position: fixed;
bottom: 4px;
left: 4px;
display: flex;
gap: 2px;
flex-direction: column-reverse;
align-items: flex-start;
opacity: .5;
}
.msz-perfs-right {
left: initial;
right: 4px;
}
.msz-perfs:hover {
opacity: 1;
}
.msz-perf {
background-color: #111d;
color: #fff;
font-family: var(--font-monospace);
font-feature-settings: 'ss07' 1;
font-size: 12px;
line-height: 20px;
padding: 4px 8px;
border-radius: 6px;
}
.msz-perfs:hover .msz-perf {
backdrop-filter: blur(10px);
}
.msz-perf-number {
color: #fff;
}
.msz-perf-unit {
color: #888;
}
.msz-perf-header {
display: flex;
flex: 0 0 auto;
gap: 4px;
}
.msz-perf:hover .msz-perf-header {
border-bottom: 1px solid #888;
margin-bottom: 2px;
}
.msz-perf-type {
flex: 0 0 auto;
font-weight: 700;
min-width: 60px;
}
.msz-perf-type-navigation {
color: #f0f;
}
.msz-perf-type-other {
color: #0ff;
}
.msz-perf-target {
flex: 1 0 auto;
min-width: 200px;
}
.msz-perf-target-host,
.msz-perf-target-path,
.msz-perf-target-query {
display: inline-block;
}
.msz-perf-target-host,
.msz-perf-target-query {
color: #888;
}
.msz-perf-total {
flex: 0 0 auto;
min-width: 80px;
text-align: right;
}
.msz-perf-timings {
display: none;
border-collapse: collapse;
width: 100%;
}
.msz-perf:hover .msz-perf-timings {
display: table;
}
.msz-perf-timing-name {
font-weight: 700;
min-width: 60px;
}
.msz-perf-timing-comment {
color: #888;
}
.msz-perf-timing-duration {
min-width: 80px;
text-align: right;
}

View file

@ -3,129 +3,155 @@ const $query = document.querySelector.bind(document);
const $queryAll = document.querySelectorAll.bind(document);
const $text = document.createTextNode.bind(document);
const $insertBefore = function(ref, elem) {
ref.parentNode.insertBefore(elem, ref);
const $insertBefore = function(target, element) {
target.parentNode.insertBefore(element, target);
};
const $appendChild = function(element, child) {
switch(typeof child) {
case 'undefined':
break;
case 'string':
element.appendChild($text(child));
break;
case 'function':
$appendChild(element, child());
break;
case 'object':
if(child === null)
break;
if(child instanceof Node)
element.appendChild(child);
else if(child?.element instanceof Node)
element.appendChild(child.element);
else if(typeof child?.toString === 'function')
element.appendChild($text(child.toString()));
break;
default:
element.appendChild($text(child.toString()));
break;
}
};
const $appendChildren = function(element, ...children) {
for(const child of children)
$appendChild(element, child);
};
const $removeChild = function(element, child) {
switch(typeof child) {
case 'function':
$removeChild(element, child());
break;
case 'object':
if(child === null)
break;
if(child instanceof Node)
element.removeChild(child);
else if(child?.element instanceof Node)
element.removeChild(child.element);
break;
}
};
const $removeChildren = function(element) {
while(element.firstChild)
element.firstChild.remove();
while(element.lastChild)
element.removeChild(element.lastChild);
};
const $jsx = (type, props, ...children) => $create({ tag: type, attrs: props, child: children });
const $fragment = function(props, ...children) {
const fragment = document.createDocumentFragment();
$appendChildren(fragment, ...children);
return fragment;
};
const $create = function(info, attrs, child, created) {
info = info || {};
const $element = function(type, props, ...children) {
if(typeof type === 'function')
return new type(props ?? {}, ...children);
if(typeof info === 'string') {
info = {tag: info};
if(attrs)
info.attrs = attrs;
if(child)
info.child = child;
if(created)
info.created = created;
}
const element = document.createElement(type ?? 'div');
const elem = document.createElement(info.tag || 'div');
if(info.attrs) {
const attrs = info.attrs;
for(let key in attrs) {
const attr = attrs[key];
if(attr === undefined || attr === null)
if(props)
for(let key in props) {
const prop = props[key];
if(prop === undefined || prop === null)
continue;
switch(typeof attr) {
switch(typeof prop) {
case 'function':
if(key.substring(0, 2) === 'on')
key = key.substring(2).toLowerCase();
elem.addEventListener(key, attr);
element.addEventListener(key, prop);
break;
case 'object':
if(attr instanceof Array) {
if(prop instanceof Array) {
if(key === 'class')
key = 'classList';
const prop = elem[key];
const attr = element[key];
let addFunc = null;
if(prop instanceof Array)
addFunc = prop.push.bind(prop);
else if(prop instanceof DOMTokenList)
addFunc = prop.add.bind(prop);
if(attr instanceof Array)
addFunc = attr.push.bind(attr);
else if(attr instanceof DOMTokenList)
addFunc = attr.add.bind(attr);
if(addFunc !== null) {
for(let j = 0; j < attr.length; ++j)
addFunc(attr[j]);
for(let j = 0; j < prop.length; ++j)
addFunc(prop[j]);
} else {
if(key === 'classList')
key = 'class';
elem.setAttribute(key, attr.toString());
element.setAttribute(key, prop.toString());
}
} else {
for(const attrKey in attr)
elem[key][attrKey] = attr[attrKey];
if(key === 'class' || key === 'className')
key = 'classList';
let setFunc = null;
if(element[key] instanceof DOMTokenList)
setFunc = (ak, av) => { if(av) element[key].add(ak); };
else if(element[key] instanceof CSSStyleDeclaration)
setFunc = (ak, av) => {
if(ak.includes('-'))
element[key].setProperty(ak, av);
else
element[key][ak] = av;
};
else
setFunc = (ak, av) => { element[key][ak] = av; };
for(const attrKey in prop) {
const attrValue = prop[attrKey];
if(attrValue)
setFunc(attrKey, attrValue);
}
}
break;
case 'boolean':
if(attr)
elem.setAttribute(key, '');
if(prop)
element.setAttribute(key, '');
break;
default:
if(key === 'className')
key = 'class';
elem.setAttribute(key, attr.toString());
element.setAttribute(key, prop.toString());
break;
}
}
}
if(info.child) {
let children = info.child;
$appendChildren(element, ...children);
if(!Array.isArray(children))
children = [children];
for(const child of children) {
switch(typeof child) {
case 'string':
elem.appendChild(document.createTextNode(child));
break;
case 'object':
if(child instanceof Element) {
elem.appendChild(child);
} else if('element' in child) {
const childElem = child.element;
if(childElem instanceof Element)
elem.appendChild(childElem);
else
elem.appendChild($create(child));
} else if('getElement' in child) {
const childElem = child.getElement();
if(childElem instanceof Element)
elem.appendChild(childElem);
else
elem.appendChild($create(child));
} else {
elem.appendChild($create(child));
}
break;
default:
elem.appendChild(document.createTextNode(child.toString()));
break;
}
}
}
if(info.created)
info.created(elem);
return elem;
return element;
};

View file

@ -0,0 +1,183 @@
const MszLoadingIcon = function() {
const element = <div class="msz-loading-icon"/>;
for(let i = 0; i < 9; ++i)
element.appendChild(<div class="msz-loading-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('msz-loading-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('msz-loading-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 MszLoading = 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,
} = options ?? {};
if(typeof element === 'string')
element = document.querySelector(element);
if(!(element instanceof HTMLElement))
element = <div class="msz-loading"/>;
if(!element.classList.contains('msz-loading'))
element.classList.add('msz-loading');
if(inline)
element.classList.add('msz-loading-inline');
if(hidden)
element.classList.add('hidden');
if(typeof size === 'number' && size > 0)
element.style.setProperty('--msz-loading-size', size);
if(typeof containerWidth === 'string')
element.style.setProperty('--msz-loading-container-width', containerWidth);
if(typeof containerHeight === 'string')
element.style.setProperty('--msz-loading-container-height', containerHeight);
if(typeof gap === 'string')
element.style.setProperty('--msz-loading-gap', gap);
if(typeof margin === 'string')
element.style.setProperty('--msz-loading-margin', margin);
if(typeof width === 'string')
element.style.setProperty('--msz-loading-width', width);
if(typeof height === 'string')
element.style.setProperty('--msz-loading-height', height);
if(typeof colour === 'string')
element.style.setProperty('--msz-loading-colour', colour);
let icon;
if(element.childElementCount < 1) {
icon = new MszLoadingIcon;
icon.play();
element.appendChild(<div class="msz-loading-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

@ -1,5 +1,9 @@
#include array.js
#include csrf.js
#include html.js
#include meta.js
#include perf.jsx
#include uniqstr.js
#include xhr.js
#include loading.jsx

36
assets/common.js/meta.js Normal file
View file

@ -0,0 +1,36 @@
#include html.js
const $meta = (() => {
return {
get(name, prefixed=true) {
if(!name) return;
if(prefixed) name = `msz-${name}`;
const elem = $query(`meta[name="${name}"]`);
if(elem instanceof HTMLMetaElement && typeof elem.content === 'string')
return elem.content;
return null;
},
set(name, value, prefixed=true) {
if(!name) return;
if(prefixed) name = `msz-${name}`;
let elem = $query(`meta[name="${name}"]`);
if(elem instanceof HTMLMetaElement) {
if(typeof value === 'string')
elem.content = value;
else
elem.remove();
} else {
if(typeof value !== 'string')
return;
elem = document.createElement('meta');
elem.name = name;
elem.content = value;
document.head.appendChild(elem);
}
},
};
})();

72
assets/common.js/perf.jsx Normal file
View file

@ -0,0 +1,72 @@
#include html.js
(() => {
const perfs = <div class="msz-perfs"/>;
perfs.ondblclick = () => {
perfs.classList.toggle('msz-perfs-right');
};
const appendReal = elem => {
$appendChild(perfs, elem);
};
let append = elem => {
append = appendReal;
$appendChild(document.body, perfs);
appendReal(elem);
};
(new PerformanceObserver(list => {
for(const entry of list.getEntries()) {
if(entry.serverTiming.length < 1)
break;
const url = new URL(entry.name);
let total = 0;
let queries = -1;
const timings = <table class="msz-perf-timings"/>;
for(const timing of entry.serverTiming) {
if(timing.name === 'msz-queries') {
queries = Math.ceil(timing.duration);
continue;
}
total += timing.duration;
$appendChild(timings, <tr class="msz-perf-timing">
<td class="msz-perf-timing-name">{timing.name}</td>
<td class="msz-perf-timing-comment">{decodeURIComponent(timing.description)}</td>
<td class="msz-perf-timing-duration">
<span class="msz-perf-number">{timing.duration}</span>
<span class="msz-perf-unit">ms</span>
</td>
</tr>);
}
append(<div class="msz-perf">
<div class="msz-perf-header">
<div class={`msz-perf-type msz-perf-type-${entry instanceof PerformanceNavigationTiming ? 'navigation' : 'other'}`}>
{entry instanceof PerformanceNavigationTiming ? entry.type : (
entry.initiatorType === 'xmlhttprequest' ? 'xhr' : entry.initiatorType
)}
</div>
<div class="msz-perf-target">
{url.host !== location.host ? <div class="msz-perf-target-host">{url.host}</div> : null}
<div class="msz-perf-target-path">{url.pathname}</div>
{url.search !== '' ? <div class="msz-perf-target-query">{url.search}</div> : null}
</div>
{queries > 0 ? <div class="msz-perf-total">
<span class="msz-perf-number">{queries}</span>
{' '}
<span class="msz-perf-unit">{queries === 1 ? 'query' : 'queries'}</span>
</div> : null}
<div class="msz-perf-total">
<span class="msz-perf-number">{total.toFixed(5)}</span>
<span class="msz-perf-unit">ms</span>
</div>
</div>
{timings}
</div>);
}
})).observe({ entryTypes: ['navigation', 'resource'] });
})();

View file

@ -80,7 +80,7 @@ const $xhr = (function() {
return headers;
})(xhr.getAllResponseHeaders());
if(options.csrf && headers.has('x-csrf-token'))
if(headers.has('x-csrf-token'))
$csrf.token = headers.get('x-csrf-token');
resolve({

View file

@ -20,7 +20,9 @@ body {
color: #fff;
font-size: 16px;
line-height: 25px;
font-family: Verdana, Geneva, 'Dejavu Sans', Arial, Helvetica, sans-serif;
font-family: Inter, 'Zen Kaku Gothic New', sans-serif;
font-feature-settings: 'calt' 1, 'dlig' 1, 'ss01' 1;
font-optical-sizing: auto;
background-color: #8559a5;
background-color: var(--error-colour, #8559a5);
background-image: url('/images/clouds.png');
@ -30,6 +32,12 @@ body {
flex-direction: column;
}
@supports (font-variation-settings: normal) {
body {
--font-regular: InterVariable, 'Zen Kaku Gothic New', sans-serif;
}
}
.error {
display: flex;
flex-direction: column;
@ -93,8 +101,8 @@ body {
}
.error-home {
font-size: 1.5em;
line-height: 1.4em;
font-size: 1.5rem;
line-height: 1.375rem;
}
.error-nav {
@ -112,21 +120,24 @@ body {
justify-content: center;
}
.error-title {
line-height: 3rem;
}
.error-icon {
font-size: 4em;
line-height: 1.4em;
font-size: 4rem;
line-height: 1.5rem;
}
.error-blerb {
margin: 10px auto;
font-size: .9em;
line-height: 1.4em;
font-size: .875rem;
line-height: 1rem;
}
.error-footer {
text-align: center;
font-size: 12px;
line-height: 1.4em;
font-size: .75rem;
line-height: 1.375em;
color: #888;
border-top: 1px solid #444;
padding-top: 4px;

View file

@ -1,9 +1,7 @@
.input__button {
background-color: var(--background-colour);
font-family: var(--font-regular);
font-size: 1.2em;
line-height: 1.4em;
padding: 5px 10px;
padding: 4px 12px;
min-width: 80px;
text-align: center;
cursor: pointer;
@ -15,6 +13,8 @@
align-items: center;
justify-content: center;
text-decoration: none;
font-size: inherit;
line-height: inherit;
}
.input__button:hover, .input__button:active,
.input__button:focus, .input__button:checked,

View file

@ -49,6 +49,8 @@
.input__checkbox__text {
display: inline-block;
margin-left: 4px;
font-size: .875em;
line-height: 1.375em;
}
.input__checkbox--disabled {
opacity: .5;

View file

@ -4,10 +4,12 @@
background: #222;
color: #fff;
min-width: 150px;
font-size: 1.2em;
border-radius: 2px;
box-shadow: inset 0 0 4px #111;
transition: border-color .2s;
font-family: inherit;
font-size: 1em;
line-height: 1.25em;
}
.input__select:focus {
border-color: var(--accent-colour);

View file

@ -1,5 +1,4 @@
.input__text {
font-size: 1.2em;
border: 1px solid #222;
padding: 5px 10px;
background: #222;
@ -7,8 +6,11 @@
border-radius: 2px;
box-shadow: inset 0 0 4px #111;
transition: border-color .2s;
font-family: inherit;
font-size: 1em;
line-height: 1.25em;
}
.input__text:focus { border-color: var(--accent-colour); }
.input__text--readonly { color: #888; }
.input__text--monospace { font-family: var(--font-monospace); }
.input__text--monospace { font-family: var(--font-monospace); font-feature-settings: 'ss07' 1; }
.input__text--centre { text-align: center; }

View file

@ -1,11 +1,11 @@
.input__textarea {
font-size: 1.2em;
border: 1px solid #222;
padding: 5px 10px;
padding: 4px 12px;
vertical-align: bottom;
background: #222;
color: #fff;
font-family: var(--font-monospace);
font-feature-settings: 'ss07' 1;
border-radius: 2px;
box-shadow: inset 0 0 4px #111;
transition: border-color .2s;

View file

@ -10,9 +10,8 @@
}
.input__upload__selection {
text-align: center;
font-size: 1.2em;
border: 1px solid #222;
padding: 5px 10px;
padding: 4px 12px;
background: #222;
color: #fff;
border-radius: 2px;

View file

@ -43,7 +43,7 @@
.changelog__change__username {
color: inherit;
font-size: 1.4em;
font-size: 1.375em;
line-height: 1.5em;
text-decoration: none;
}
@ -52,7 +52,7 @@
}
.changelog__change__userrole {
font-size: .9em;
font-size: .875em;
line-height: 1.5em;
color: inherit;
text-decoration: none;
@ -64,15 +64,13 @@
.changelog__change__date {
color: inherit;
text-decoration: none;
font-size: 1.1em;
line-height: 1.5em;
}
.changelog__change__date:hover {
text-decoration: underline;
}
.changelog__change__text {
line-height: 1.2em;
line-height: 1.25em;
flex: 1 1 auto;
word-wrap: break-word;
overflow: hidden;

View file

@ -1,6 +1,8 @@
.changelog__entry {
display: flex;
margin: 5px;
font-size: .875em;
line-height: 1.5em;
}
.changelog__entry__info { display: flex; }
@ -68,7 +70,7 @@
.changelog__entry__tags {
display: flex;
flex-wrap: wrap;
font-size: .9em;
font-size: .75em;
line-height: 1.5em;
}

View file

@ -6,8 +6,6 @@
text-decoration: none;
padding: 1px 3px;
color: var(--accent-colour);
font-size: 1.2em;
line-height: 1.5em;
}
.changelog__listing__date:hover {
text-decoration: underline;

View file

@ -21,8 +21,8 @@
}
.changelog__log__text {
padding: 8px 12px;
font-size: 1.5em;
line-height: 1.3em;
font-size: 1.375em;
line-height: 1.25em;
align-self: center;
flex: 1 1 auto;
overflow: hidden;

View file

@ -1,159 +0,0 @@
.comment {
margin: 10px;
}
.comment__reply-toggle {
display: none;
}
.comment__reply-toggle:checked ~ .comment--reply {
display: block;
}
.comment--reply {
display: none;
}
.comment--deleted > .comment__container {
opacity: .5;
transition: opacity .2s;
}
.comment--deleted > .comment__container:hover {
opacity: .9;
}
.comment__container {
display: flex;
margin-bottom: 3px;
}
.comment__mention {
color: var(--user-colour);
text-decoration: none;
font-weight: 700;
}
.comment__mention:hover {
text-decoration: underline;
}
.comment__actions {
list-style: none;
display: flex;
font-size: .9em;
align-items: center;
}
.comment__action {
color: inherit;
text-decoration: none;
vertical-align: middle;
cursor: pointer;
}
.comment__action:not(:last-child) {
margin-right: 6px;
}
.comment__action--link:hover {
text-decoration: underline;
}
.comment__action--post {
margin-left: auto;
}
.comment__action--button {
cursor: pointer;
font: 12px/20px var(--font-regular);
padding: 0 10px;
}
.comment__action--hide {
opacity: 0;
transition: opacity .2s;
}
.comment__action--voted {
font-weight: 700;
}
.comment__action__checkbox {
vertical-align: text-top;
margin-right: 2px;
}
.comment__replies .comment--indent-1,
.comment__replies .comment--indent-2,
.comment__replies .comment--indent-3,
.comment__replies .comment--indent-4,
.comment__replies .comment--indent-5 {
margin-left: 20px;
}
.comment__avatar {
flex: 0 0 auto;
height: 50px;
width: 50px;
margin-right: 5px;
}
.comment__replies .comment__avatar {
width: 40px;
height: 40px;
}
.comment__content {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: hidden;
word-wrap: break-word;
padding-left: 5px;
}
.comment__content:hover .comment__action--hide {
opacity: 1;
}
.comment__info {
display: inline-flex;
}
.comment__text {
margin-right: 2px;
}
.comment__text--input {
min-width: 100%;
max-width: 100%;
min-height: 50px;
font: 12px/20px var(--font-regular);
margin-right: 1px;
}
.comment__user {
color: var(--user-colour);
text-decoration: none;
}
.comment__user--link:hover {
text-decoration: underline;
}
.comment__date,
.comment__pin {
color: #666;
font-size: .9em;
margin-left: 8px;
}
.comment__link {
color: #666;
display: inline-flex;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.comment__pin {
margin-left: 4px;
}
.comment__pin:before {
content: "-";
padding-right: 4px;
}

View file

@ -1,36 +0,0 @@
.comments {
--comments-max-height: 600px;
margin: 1px;
overflow: hidden;
word-wrap: break-word;
}
.comments__listing {
overflow-y: auto;
}
.comments__listing--limit {
max-height: var(--comments-max-height);
}
/*.comments__input,*/
.comments__javascript,
.comments__notice--staff {
border-bottom: 1px solid var(--accent-colour);
padding-bottom: 1px;
margin-bottom: 1px;
}
.comments__none,
.comments__javascript,
.comments__notice {
padding: 10px;
font-size: 1.2em;
text-align: center;
}
.comments__notice__link {
color: var(--accent-colour);
text-decoration: none;
}
.comments__notice__link:hover {
text-decoration: underline;
}

View file

@ -0,0 +1,121 @@
.comments-entry-main {
display: grid;
grid-template-columns: 46px 1fr;
gap: 2px;
}
.comments-entry-root {
padding-bottom: 2px;
border-top: 1px solid var(--accent-colour);
}
.comments-entry-replies {
margin-left: 25px;
}
.comments-entry-avatar {
flex: 0 0 auto;
padding: 4px;
}
.comments-entry-wrap {
flex: 0 1 auto;
display: flex;
flex-direction: column;
gap: 2px;
}
.comments-entry-meta {
display: flex;
gap: 4px;
margin-top: 4px;
}
.comments-entry-user {
display: flex;
}
.comments-entry-user-link {
text-decoration: none;
}
.comments-entry-user-link:hover,
.comments-entry-user-link:focus {
text-decoration: underline solid var(--user-colour, var(--text-colour, #fff));
}
.comments-entry-user-dead {
text-decoration: line-through;
}
.comments-entry-time {
display: flex;
gap: 6px;
}
.comments-entry-time-edited,
.comments-entry-time-pinned,
.comments-entry-time-deleted {
margin-left: 6px;
}
.comments-entry-time-pinned .comments-entry-time-icon {
rotate: 45deg;
}
.comments-entry-time-link {
color: inherit;
text-decoration: none;
}
.comments-entry-time-link:hover,
.comments-entry-time-link:focus {
text-decoration: underline;
}
.comments-entry-actions {
display: flex;
gap: 2px;
margin-top: 2px;
}
.comments-entry-actions-group {
display: flex;
border-radius: 3px;
padding: 1px;
gap: 1px;
transition: opacity .1s;
}
.comments-entry-actions-group-votes,
.comments-entry-actions-group-replies {
border: 1px solid var(--accent-colour);
}
.comments-entry-actions-group-disabled {
opacity: .5;
}
.comments-entry-action {
background: transparent;
border-width: 0;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 3px 6px;
cursor: pointer;
transition: background-color .1s;
min-width: 24px;
min-height: 22px;
color: inherit;
}
.comments-entry-action:not([disabled]):hover,
.comments-entry-action:not([disabled]):focus {
background: var(--comments-entry-action-background-hover, #fff4);
}
.comments-entry-action-reply-active {
background: #fff2;
}
.comments-entry-action-vote-like.comments-entry-action-vote-cast {
background: #0808;
}
.comments-entry-action-vote-like {
--comments-entry-action-background-hover: #0804;
}
.comments-entry-action-vote-dislike.comments-entry-action-vote-cast {
background: #c008;
}
.comments-entry-action-vote-dislike {
--comments-entry-action-background-hover: #c004;
}

View file

@ -0,0 +1,73 @@
.comments-form {
border: 1px solid var(--accent-colour);
border-radius: 3px;
margin: 2px 0;
display: grid;
grid-template-columns: 46px 1fr;
transition: opacity .1s;
}
.comments-form-root {
margin: 2px;
}
.comments-form-disabled {
opacity: .5;
}
.comments-form-avatar {
flex: 0 0 auto;
padding: 3px;
}
.comments-form-wrap {
display: grid;
grid-template-rows: 1fr 35px;
gap: 2px;
margin: 3px;
margin-left: 0;
overflow: hidden;
}
.comments-form-input {
overflow: hidden;
}
.comments-form-input textarea {
min-width: 100%;
max-width: 100%;
width: 100%;
min-height: 40px;
height: 0;
}
.comments-form-root .comments-form-input textarea {
min-height: 60px;
}
.comments-form-actions {
display: flex;
align-items: center;
overflow: hidden;
gap: 6px;
}
.comments-form-status {
flex: 1 1 auto;
padding: 0 6px;
overflow: hidden;
transition: color .2s;
}
.comments-form-status-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.comments-form-status-error {
color: #c00;
}
.comments-form-pin {
flex: 0 0 auto;
font-size: 1.25em;
line-height: 1.375em;
}
.comments-form-post {
flex: 0 0 auto;
}

View file

@ -0,0 +1,8 @@
.comments-listing {
display: flex;
flex-direction: column;
gap: 2px;
}
.comments-listing-root {
margin: 2px;
}

View file

@ -0,0 +1,5 @@
@include comments/form.css;
@include comments/entry.css;
@include comments/listing.css;
@include comments/notice.css;
@include comments/options.css;

View file

@ -0,0 +1,14 @@
.comments-notice {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
font-size: 1.25em;
line-height: 1.375em;
gap: 6px;
padding: 12px;
margin: 2px;
}
.comments-notice-inner {
flex: 0 1 auto;
}

View file

@ -0,0 +1,32 @@
.comments-options {
display: flex;
justify-content: flex-end;
margin: 2px;
padding: 6px;
gap: 6px;
}
.comments-options-actions {
display: flex;
gap: 6px;
}
.comments-options-action {
color: inherit;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 4px 8px;
background-color: transparent;
border-width: 0;
border-radius: 4px;
transition: background-color .1s, opacity .1s;
}
.comments-options-action[disabled] {
opacity: .5;
}
.comments-options-action:not([disabled]):hover,
.comments-options-action:not([disabled]):focus {
background-color: #fff4;
}

View file

@ -13,10 +13,12 @@
overflow: hidden;
}
.container__title__text {
font-size: 1.5em;
font-size: 1.125rem;
line-height: 1.5em;
padding: 8px 10px;
word-wrap: break-word;
font-weight: 500;
min-height: 44px;
}
.container__title__link {
color: inherit;
@ -30,7 +32,6 @@
width: 100%;
height: 100%;
mask-image: linear-gradient(0deg, transparent 10%, var(--background-colour) 100%);
-webkit-mask-image: linear-gradient(0deg, transparent 10%, var(--background-colour) 100%);
background: var(--background-pattern);
background-color: var(--accent-colour);
background-blend-mode: multiply;

View file

@ -21,7 +21,7 @@
visibility: hidden;
}
.eeprom-widget-form-text {
font-size: 1.4em;
font-size: 1.325em;
line-height: 1.5em;
}
.eeprom-widget-form:focus,
@ -70,8 +70,8 @@
text-decoration: underline;
}
.eeprom-widget-file-progress {
font-size: .9em;
line-height: 1.4em;
font-size: .875em;
line-height: 1.325em;
text-align: right;
padding: 0 2px;
white-space: nowrap;

View file

@ -88,30 +88,26 @@
margin-left: 5px;
}
.embedph-info-title {
font-size: 2em !important;
font-weight: 400 !important;
line-height: 1.2em !important;
margin-bottom: 5px;
font-size: 1.5rem !important;
font-weight: 500 !important;
line-height: 1.5rem !important;
margin: 5px 0;
word-break: break-word;
margin: 0 !important;
padding: 0 !important;
border-width: 0 !important;
}
.embedph-info-desc {
line-height: 1.4em;
margin: .5em 0;
font-size: .875rem;
line-height: 1.5rem;
margin: .25rem 0;
word-break: break-word;
}
.embedph-info-site {
font-size: .9em;
line-height: 1.2em;
font-size: .75rem;
}
@media (max-width: 640px) {
.embedph-info-title {
font-size: 1.5em;
line-height: 1.2em;
}
.embedph-info-desc {
display: none;
}
@ -135,9 +131,7 @@
margin-bottom: 20px;
}
.embedph-play-external {
padding: 5px 10px;
font-size: 1.2em;
line-height: 1.5em;
padding: 4px 8px;
text-decoration: none !important;
color: var(--text-colour) !important;
background-color: var(--background-colour-translucent-6);
@ -225,6 +219,7 @@
height: 70px;
border-radius: 5px;
margin: 5px;
font-size: .75rem;
}
.aembedph:hover .aembedph-play,
.aembedph:active .aembedph-play,
@ -293,9 +288,9 @@
padding: 0 5px;
}
.aembedph-info-title {
font-size: 1.4em !important;
font-weight: 400 !important;
line-height: 1.2em !important;
font-size: 1.25em !important;
font-weight: 500 !important;
line-height: 1.25em !important;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@ -309,10 +304,10 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 700;
font-weight: 600;
}
.aembedph-info-album {
line-height: 1.4em;
line-height: 1.5em;
word-break: break-word;
white-space: nowrap;
overflow: hidden;
@ -321,7 +316,7 @@
}
.aembedph-info-site {
font-size: .9em;
line-height: 1.2em;
line-height: 1.25em;
}
.aembedph-play {

View file

@ -13,8 +13,8 @@
max-width: var(--site-max-width);
margin: 0 auto;
text-align: center;
font-size: .9em;
line-height: 1.5em;
font-size: .875rem;
line-height: 1.5rem;
padding: 1em 0;
}
.footer__background {

View file

@ -4,8 +4,6 @@
overflow: auto;
}
.forum__categories__empty {
font-size: 1.2em;
line-height: 1.5em;
text-align: center;
padding: 10px;
}
@ -63,8 +61,8 @@
background-size: 80px 80px;
background-image: radial-gradient(ellipse at center, rgba(255, 255, 255, .2) 0%, rgba(0, 0, 0, .4) 100%);
box-shadow: 0 1px 4px #111;
font-size: 2em;
line-height: 1.5em;
font-size: 1.5em;
line-height: 1.25em;
color: #fff;
display: flex;
justify-content: center;
@ -82,22 +80,25 @@
justify-content: center;
flex-direction: column;
line-height: 1.5em;
gap: 2px;
}
.forum__category__title {
font-size: 1.3em;
font-size: 1.125em;
line-height: 1.25em;
}
.forum__category__description,
.forum__category__subforums {
font-size: .9em;
font-size: .875em;
line-height: 1.25em;
}
.forum__category__subforums {
display: flex;
gap: 6px;
}
.forum__category__subforum {
padding: 2px;
pointer-events: initial;
color: var(--accent-colour);
text-decoration: none;
@ -125,21 +126,23 @@
}
.forum__category__stat {
font-size: .9em;
line-height: 1.3em;
font-size: .875em;
line-height: 1.25em;
opacity: .7;
pointer-events: auto;
font-feature-settings: 'ss01' 1, 'tnum' 1;
}
.forum__category__stat:first-child {
font-size: 1.5em;
font-size: 1.25em;
opacity: 1;
}
.forum__category__activity {
text-align: right;
min-width: 270px;
line-height: 1.4em;
min-width: 300px;
font-size: .875em;
line-height: 1.375em;
}
.forum__category__activity__none,
.forum__category__activity__details {
@ -160,7 +163,7 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
max-width: 230px;
}
.forum__category__activity__post:hover,
.forum__category__activity__post:focus {
@ -194,7 +197,7 @@
flex-wrap: wrap;
}
.forum__category__details {
flex-basis: calc(100% - 100px);
flex-basis: calc(100% - 8em); /* god knows what this does */
}
.forum__category__stats {
min-width: initial;

View file

@ -7,8 +7,8 @@
}
.forum__header__title {
font-size: 2em;
line-height: 1.5em;
font-size: 1.5em;
line-height: 1.25em;
color: inherit;
text-decoration: none;
padding: 0 5px;
@ -28,19 +28,18 @@
margin: 0;
box-shadow: initial;
font-size: 1em;
font-family: inherit;
font-family: var(--font-regular);
}
.forum__header__breadcrumbs {
display: flex;
font-size: 1.1em;
line-height: 1.5em;
align-items: center;
}
.forum__header__breadcrumb {
color: var(--accent-colour);
text-decoration: none;
padding: 2px 5px;
padding: 2px 6px;
}
.forum__header__breadcrumb:hover {
text-decoration: underline;
@ -48,7 +47,7 @@
.forum__header__breadcrumb__separator {
color: var(--accent-colour);
margin: 0 4px;
font-size: .9em;
font-size: .875em;
}
.forum__header__actions {

View file

@ -36,14 +36,13 @@
.forum__leaderboard__user {
margin: 2px 0;
font-size: 1.2em;
}
.forum__leaderboard__user--rank-1 {
font-size: 1.6em;
font-size: 1.5em;
}
.forum__leaderboard__user--rank-2,
.forum__leaderboard__user--rank-3 {
font-size: 1.4em;
font-size: 1.25em;
}
.forum__leaderboard__user__background {
@ -69,6 +68,7 @@
justify-content: center;
font-weight: 700;
flex: 0 0 auto;
font-feature-settings: 'ss01' 1, 'tnum' 1;
}
.forum__leaderboard__user__rank:before {
content: "#";

View file

@ -22,8 +22,8 @@
}
.forum__post__details {
font-size: .9em;
line-height: 1.7em;
font-size: .875em;
line-height: 1.75em;
padding: 0 2px;
display: flex;
justify-content: space-between;
@ -36,6 +36,9 @@
color: inherit;
text-decoration: none;
}
.forum__post__id {
font-feature-settings: 'ss01' 1, 'tnum' 1;
}
.forum__post__datetime:hover,
.forum__post__datetime:focus,
.forum__post__id:hover,
@ -47,7 +50,7 @@
.forum__post__text {
padding: 2px;
line-height: 1.4em;
line-height: 1.5em;
flex: 1 1 auto;
}
.forum__post__text--edit {
@ -97,14 +100,14 @@
}
.forum__post__posts-count {
font-size: .9em;
margin-left: 4px;
font-size: .75em;
margin-left: 8px;
}
.forum__post__joined {
flex: 1 1 auto;
max-width: 170px;
font-size: .9em;
font-size: .75em;
justify-self: flex-end;
}
@ -117,7 +120,7 @@
.forum__post__username {
color: inherit;
font-size: 1.4em;
font-size: 1.25em;
line-height: 2em;
text-decoration: none;
}
@ -127,8 +130,8 @@
}
.forum__post__usertitle {
font-size: .9em;
line-height: 1.5em;
font-size: .75em;
line-height: 1.25em;
margin-bottom: 4px;
}
@ -187,12 +190,13 @@
.forum__post__badge {
background-color: var(--accent-colour);
border-radius: 12px;
border-radius: 24px;
width: 100%;
padding: 2px;
box-shadow: 0 2px 3px #000A;
margin: 4px;
overflow: hidden;
font-size: .875em;
}
.forum__post__badge__desktop {
display: block;
@ -207,13 +211,11 @@
}
.forum__post__text {
margin: 4px;
font-size: 1.2em;
line-height: 1.3em;
}
.forum__post__info {
flex-direction: row;
margin: 0;
padding: 5px;
padding: 4px;
}
.forum__post__info__content {
width: 100%;
@ -252,8 +254,8 @@
padding: 2px 10px;
margin: 0;
align-self: flex-start;
margin-left: 5px;
font-size: .9em;
margin-left: 10px;
font-size: .875em;
}
.forum__post__badge__desktop {
display: none;

View file

@ -12,7 +12,7 @@
display: flex;
justify-content: center;
align-items: center;
font-size: 2em;
font-size: 1.5em;
padding-bottom: 1px;
}
.forum__status__icon__background {

View file

@ -4,8 +4,6 @@
overflow: auto;
}
.forum__topics__empty {
font-size: 1.2em;
line-height: 1.5em;
text-align: center;
padding: 10px;
}
@ -76,7 +74,7 @@
background-size: 60px 60px;
background-image: radial-gradient(ellipse at center, rgba(255, 255, 255, .2) 0%, rgba(0, 0, 0, .4) 100%);
box-shadow: 0 1px 4px #111;
font-size: 1.5em;
font-size: 1.125em;
line-height: 1.5em;
color: #fff;
display: flex;
@ -114,14 +112,15 @@
}
.forum__topic__title {
font-size: 1.3em;
font-size: 1.125em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.forum__topic__info {
font-size: .9em;
font-size: .875em;
line-height: 1.25em;
}
.forum__topic__stats,
@ -137,14 +136,15 @@
}
.forum__topic__stat {
font-size: .9em;
line-height: 1.3em;
font-size: .875em;
line-height: 1.25em;
opacity: .7;
pointer-events: auto;
cursor: default;
font-feature-settings: 'ss01' 1, 'tnum' 1;
}
.forum__topic__stat:first-child {
font-size: 1.4em;
font-size: 1.25em;
opacity: 1;
}
@ -153,7 +153,7 @@
align-items: center;
text-align: right;
min-width: 200px;
line-height: 1.5em;
line-height: 1.25em;
}
.forum__topic__activity__details {
display: flex;
@ -198,8 +198,9 @@
.forum__topic__pagination {
display: flex;
align-items: center;
font-size: .9em;
line-height: 1.2em;
font-size: .875em;
line-height: 1.25em;
font-feature-settings: 'ss01' 1, 'tnum' 1;
}
.forum__topic__pagination__separator {
margin: 0 8px;
@ -209,7 +210,7 @@
text-decoration: none;
pointer-events: initial;
margin: 0 1px;
padding: 2px 4px;
padding: 3px;
border-radius: 2px;
min-width: 25px;
height: 25px;
@ -262,8 +263,8 @@
.forum__topic__pagination__item {
min-width: 30px;
height: 30px;
line-height: 26px;
font-size: 1.2em;
line-height: 1.375em;
font-size: 1.25em;
}
}

View file

@ -69,8 +69,7 @@
}
.header__desktop__menu__link {
margin: var(--header-link-margin) 0;
font-size: 1.2em;
padding: 6px 10px;
padding: 4px 10px;
text-align: center;
}
@ -86,6 +85,8 @@
}
.header__desktop__submenu__link {
margin: 5px;
font-size: .875em;
line-height: 1.5em;
}
.header__desktop__submenu__background {
background: var(--header-accent-colour);
@ -128,8 +129,8 @@
margin: 2px;
color: inherit;
text-decoration: none;
font-size: 1.5em;
line-height: 32px;
font-size: 1.125rem;
line-height: 2rem;
width: 32px;
height: 32px;
transition: background-color .2s;
@ -149,8 +150,8 @@
top: -5px;
right: -3px;
z-index: 1;
font-size: .5em;
line-height: 1.4em;
font-size: .625rem;
line-height: 1.375rem;
text-align: right;
padding: 2px 2px 0;
border-radius: 4px;
@ -158,7 +159,8 @@
opacity: .9;
border-radius: 4px;
line-height: 12px;
padding: 2px 4px;
padding: 2px 3px;
font-feature-settings: 'ss01' 1, 'tnum' 1;
}
/** MOBILE HEADER **/
@ -186,18 +188,12 @@
.header__mobile__icon {
flex: 0 0 auto;
cursor: pointer;
font-size: 32px;
font-size: 1.5rem;
width: var(--header-icon-px);
height: var(--header-icon-px);
display: flex;
justify-content: center;
align-items: center;
-webkit-touch-callout: none !important;
-webkit-user-select: none !important;
-khtml-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
user-select: none !important;
}
@ -227,7 +223,7 @@
background: var(--background-pattern);
background-color: var(--header-accent-colour);
background-blend-mode: multiply;
transition: max-height .2s;
transition: max-height .3s;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
max-height: 0;
overflow: hidden;
@ -237,7 +233,7 @@
}
.header__mobile__toggle:checked ~ .header__mobile__menu {
max-height: 600px;
max-height: 100vh;
}
.header__mobile__user {
@ -255,24 +251,23 @@
color: inherit;
text-decoration: none;
display: block;
padding: 8px;
padding-left: 20px;
padding: 6px;
padding-left: 24px;
cursor: pointer;
border-radius: 2px;
transition: background-color .2s, margin .1s, opacity .1s;
font-size: 1.2em;
}
.header__mobile__link:not(:last-child) {
margin-bottom: 2px;
}
.header__mobile__link--primary {
font-size: 1.5em;
padding: 10px;
font-size: 1.25rem;
padding: 8px;
}
.header__mobile__link--user {
margin: 2px;
font-size: 1.5em;
padding: 10px;
font-size: 1.25rem;
padding: 8px;
}
.header__mobile__link:hover,
.header__mobile__link:focus {

View file

@ -2,8 +2,8 @@ pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em;
font-size: 1.2em;
font-family: var(--font-monospace);
font-feature-settings: 'ss07' 1;
}
code.hljs {

View file

@ -46,8 +46,8 @@
.landingv2-footer-copyright {
text-align: right;
line-height: 1.8em;
font-size: .9em;
line-height: 1.5em;
font-size: .875em;
align-self: flex-end;
}

View file

@ -52,8 +52,8 @@
display: flex;
align-items: center;
min-height: 70px;
font-size: 1.4em;
padding: 10px 16px;
font-size: 1.5em;
padding: 8px 16px;
grid-column: 1;
}
.landingv2-header-menu-link:hover,

View file

@ -13,7 +13,6 @@
display: flex;
align-items: center;
margin: 10px;
text-shadow: 0 1px 4px #000;
}
.landingv2-stat-icon {
font-size: 4em;
@ -25,6 +24,7 @@
}
.landingv2-stat-value-num {
font-weight: 700;
font-feature-settings: 'ss01' 1, 'tnum' 1;
}
.landingv2-forum {
@ -37,7 +37,6 @@
.landingv2-forum-topics {
background-color: var(--container-colour);
box-shadow: 0 1px 2px #0009;
text-shadow: 0 1px 4px #000;
overflow: hidden;
word-wrap: break-word;
}
@ -95,7 +94,7 @@
background-size: 60px 60px;
background-image: radial-gradient(ellipse at center, rgba(255, 255, 255, .2) 0%, rgba(0, 0, 0, .4) 100%);
box-shadow: 0 1px 4px #111;
font-size: 1.5em;
font-size: 1.25em;
line-height: 1.5em;
color: #fff;
display: flex;
@ -110,34 +109,35 @@
display: flex;
justify-content: center;
flex-direction: column;
line-height: 1.6em;
line-height: 1.625em;
overflow: hidden;
}
.landingv2-forum-topic-info-details-title {
font-size: 1.3em;
font-size: 1.25em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.landingv2-forum-topic-info-stats {
font-size: .9em;
font-size: .875em;
display: flex;
flex: 0 0 auto;
text-align: center;
min-width: 60px;
flex-direction: column;
font-feature-settings: 'ss01' 1, 'tnum' 1;
}
.landingv2-forum-topic-info-stats-posts,
.landingv2-forum-topic-info-stats-views {
font-size: .9em;
line-height: 1.3em;
font-size: .875em;
line-height: 1.25em;
opacity: .7;
pointer-events: auto;
cursor: default;
}
.landingv2-forum-topic-info-stats-posts {
font-size: 1.4em;
font-size: 1.25em;
opacity: 1;
}
@ -150,7 +150,6 @@
.landingv2-news-post {
background-color: var(--container-colour);
box-shadow: 0 1px 2px #0009;
text-shadow: 0 1px 4px #000;
overflow: hidden;
word-wrap: break-word;
padding: 0 10px 10px 10px;
@ -167,7 +166,6 @@
.landingv2-online {
background-color: var(--container-colour);
box-shadow: 0 1px 2px #0009;
text-shadow: 0 1px 4px #000;
margin: 4px 0;
}
.landingv2-online-users {

View file

@ -37,7 +37,6 @@
height: 30px;
line-height: 29px;
text-align: center;
font-size: 1.5em;
background-color: #222d;
display: block;
color: var(--text-colour);

View file

@ -57,13 +57,10 @@
width: 45%;
padding: 4px 0;
}
.landing__statistic__name {
font-size: 1.3em;
line-height: 2em;
}
.landing__statistic__value {
font-size: 1.5em;
line-height: 1.5em;
font-size: 1.25em;
line-height: 1.625em;
font-feature-settings: 'ss01' 1, 'tnum' 1;
}
.landing__latest {
@ -91,13 +88,13 @@
padding-left: 8px;
}
.landing__latest__username {
font-size: 1.5em;
line-height: 1.4em;
font-size: 1.25em;
line-height: 1.375em;
color: var(--user-colour);
}
.landing__latest__joined {
font-size: .9em;
line-height: 1.2em;
font-size: .875em;
line-height: 1.25em;
}
@media (max-width: 800px) {

View file

@ -1,6 +1,6 @@
:root {
--font-size: 12px;
--line-height: 20px;
--font-size: 16px;
--line-height: 25px;
--site-max-width: 1200px;
--site-mobile-width: 800px;
@ -121,8 +121,7 @@ html {
@include changelog/log.css;
@include changelog/pagination.css;
@include comments/comment.css;
@include comments/comments.css;
@include comments/main.css;
@include forum/actions.css;
@include forum/categories.css;

View file

@ -9,10 +9,10 @@
flex: 1 1 auto;
}
.manage__description {
font-size: .9em;
font-size: .875em;
margin: 1px 2px;
border-bottom: 1px solid var(--accent-colour);
padding: 2px 5px;
padding: 2px 4px;
}
@media (max-width: 800px) {

View file

@ -4,13 +4,13 @@
}
.manage__ban__title {
font-size: 1.4em;
font-size: 1.25em;
line-height: 1.5em;
padding: 0 4px;
}
.manage__ban__desc {
font-size: .9em;
font-size: .875em;
line-height: 1.5em;
font-style: italic;
border-bottom: 1px solid var(--accent-colour);

View file

@ -107,7 +107,7 @@
border-top: 1px solid var(--accent-colour);
}
.manage__bans__item__reason__title {
font-size: .9em;
font-size: .875em;
line-height: 1.5em;
font-style: italic;
}
@ -117,6 +117,6 @@
}
.manage__bans__item__noreason {
font-size: .9em;
font-size: .875em;
font-style: italic;
}

View file

@ -13,7 +13,8 @@
.manage__blacklist__textarea {
margin: 0;
padding: 5px 10px;
font-family: monospace;
font-family: var(--font-monospace);
font-feature-settings: 'ss07' 1;
width: 100%;
min-width: 100%;
max-width: 100%;

View file

@ -4,13 +4,11 @@
.manage__navigation__links {
display: flex;
flex-direction: column;
font-size: 1.2em;
}
.manage__navigation__link {
color: inherit;
text-decoration: none;
padding: 2px 5px;
margin-bottom: 2px;
padding: 2px 8px;
}
.manage__navigation__link:hover {
text-decoration: underline;

View file

@ -17,11 +17,11 @@
.manage__note__title {
flex-grow: 1;
flex-shrink: 1;
font-size: 1.4em;
line-height: 1.3em;
font-size: 1.375em;
line-height: 1.25em;
}
.manage__note__title__text {
padding: 2px 5px;
padding: 2px 6px;
}
.manage__note__title input {
width: 100%;
@ -73,7 +73,7 @@
.manage__note__nobody {
text-align: center;
font-size: .9em;
font-size: .875em;
font-style: italic;
}

View file

@ -24,9 +24,9 @@
.manage__notes__item__title {
flex-grow: 1;
flex-shrink: 1;
font-size: 1.4em;
line-height: 1.3em;
padding: 2px 5px;
font-size: 1.25em;
line-height: 1.25em;
padding: 2px 6px;
}
.manage__notes__item__title a {
color: inherit;
@ -96,7 +96,7 @@
.manage__notes__item__nobody {
text-align: center;
font-size: .9em;
font-size: .875em;
font-style: italic;
}

View file

@ -49,7 +49,7 @@
justify-content: center;
width: 100%;
height: 100%;
font-size: 1.5em;
font-size: 1.25em;
transition: background-color .2s;
}
.manage__role-item__icon__background {
@ -73,14 +73,9 @@
flex: 1 1 auto;
}
.manage__role-item__name {
font-size: 1.4em;
line-height: 1.4em;
}
.manage__role-item__details {
font-size: .9em;
line-height: 1.3em;
font-size: .875em;
line-height: 1.25em;
display: inline-flex;
align-items: center;
padding: 1px 0;
@ -90,11 +85,11 @@
border-radius: 10px;
background-color: var(--accent-colour);
box-shadow: 0 1px 4px #111;
padding: 2px 5px;
padding: 2px 6px;
}
.manage__role-item__title {
padding: 2px 5px;
padding: 2px 6px;
}
.manage__role-item__actions {

View file

@ -27,6 +27,7 @@
}
.manage-list-setting-key-text {
font-family: var(--font-monospace);
font-feature-settings: 'ss07' 1;
}
.manage-list-setting-type {
@ -53,7 +54,7 @@
border-radius: 5px;
font-weight: 700;
padding: 0 5px;
font-size: .9em;
font-size: .75em;
display: inline-block;
}
@ -63,6 +64,7 @@
}
.manage-list-setting-value-text {
font-family: var(--font-monospace);
font-feature-settings: 'ss07' 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

View file

@ -1,20 +1,21 @@
.manage__statistic {
border: 1px solid var(--accent-colour);
border-radius: 2px;
padding: 2px 5px;
padding: 2px 6px;
}
.manage__statistic__name {
font-size: 1.1em;
font-size: 1.125em;
line-height: 1.5em;
}
.manage__statistic__value {
text-align: right;
font-size: 1.4em;
font-size: 1.375em;
line-height: 1.5em;
font-feature-settings: 'ss01' 1, 'tnum' 1;
}
.manage__statistic__updated {
text-align: right;
font-size: .9em;
font-size: .75em;
font-style: italic;
line-height: 1.5em;
}

View file

@ -57,25 +57,26 @@
}
.manage__user-item__name {
font-size: 1.4em;
line-height: 1.4em;
font-size: 1.125em;
line-height: 1.5em;
max-width: 600px; /* whatever */
}
.manage__user-item__details {
font-size: .9em;
line-height: 1.3em;
font-size: .875em;
line-height: 1.25em;
display: inline-flex;
align-items: center;
}
.manage__user-item__detail {
border-radius: 10px;
border-radius: 16px;
background-color: var(--accent-colour);
box-shadow: 0 1px 4px #111;
padding: 3px 8px;
padding: 2px 8px;
pointer-events: initial;
margin: 2px;
font-feature-settings: 'ss01' 1, 'tnum' 1;
}
.manage__user-item__actions {
@ -88,7 +89,7 @@
width: 32px;
height: 32px;
line-height: 32px;
font-size: 1.5em;
font-size: 1.25em;
border-radius: 2px;
margin: 5px;
margin-right: 0;

View file

@ -4,13 +4,13 @@
}
.manage__warning__title {
font-size: 1.4em;
font-size: 1.25em;
line-height: 1.5em;
padding: 0 4px;
}
.manage__warning__desc {
font-size: .9em;
font-size: .875em;
line-height: 1.5em;
font-style: italic;
border-bottom: 1px solid var(--accent-colour);

View file

@ -1,5 +1,5 @@
.markdown {
line-height: 1.7em;
line-height: 1.5em;
}
.markdown a {
@ -78,8 +78,7 @@
.markdown h1, .markdown h2,
.markdown h3, .markdown h4,
.markdown h5, .markdown h6 {
margin-top: calc(var(--font-size) * 1.2);
margin-bottom: var(--font-size);
margin: calc(var(--font-size) * 0.875) 0;
font-weight: 700;
line-height: 1em;
}

View file

@ -3,8 +3,6 @@
align-items: center;
height: 30px;
margin: 1px;
font-size: 1.3em;
line-height: 1.4em;
color: #fff;
text-decoration: none;
transition: background-color .1s;
@ -12,6 +10,7 @@
border: 0;
background-color: inherit;
text-align: left;
font-size: 1rem;
}
.messages-actions-item:hover,
.messages-actions-item:focus {

View file

@ -10,8 +10,6 @@
}
.messages-entry-header {
display: flex;
font-size: 1.1em;
line-height: 1.6em;
border-bottom: 2px solid #9999;
gap: 2px;
}
@ -60,12 +58,12 @@
align-self: flex-end;
}
.messages-entry-subject {
line-height: 1.4em;
line-height: 1.5em;
color: #fff;
overflow: hidden;
}
.messages-entry-preview {
line-height: 1.4em;
line-height: 1.5em;
color: #888;
overflow: hidden;
}

View file

@ -25,7 +25,7 @@
margin: 10px;
}
.messages-folder-notice-text {
font-size: 1.4em;
font-size: 1.375em;
line-height: 1.5em;
}
.messages-folder .pagination {

View file

@ -6,7 +6,7 @@
}
.messages-message-snippet {
cursor: pointer;
font-size: .9em;
font-size: .875em;
line-height: 1.5em;
color: #888;
gap: 5px;
@ -117,7 +117,7 @@
}
.messages-message-body {
line-height: 1.4em;
line-height: 1.5em;
}
.messages-message-body p:first-child {
margin-top: 0 !important;
@ -131,5 +131,5 @@
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4em;
line-height: 1.5em;
}

View file

@ -9,7 +9,7 @@
display: flex;
color: inherit;
text-decoration: none;
font-size: 1.5em;
font-size: 1.25em;
line-height: 32px;
height: 32px;
transition: background-color .2s;

View file

@ -44,7 +44,7 @@
.news__post__username {
color: inherit;
font-size: 1.4em;
font-size: 1.25em;
line-height: 1.5em;
text-decoration: none;
}
@ -53,14 +53,14 @@
}
.news__post__date {
font-size: 1.1em;
font-size: .875em;
line-height: 1.5em;
}
.news__post__category {
color: inherit;
text-decoration: none;
font-size: 1.1em;
font-size: 1.125em;
line-height: 1.5em;
margin: 6px 0;
}
@ -69,7 +69,6 @@
}
.news__post__text {
line-height: 1.2em;
flex: 1 1 auto;
word-wrap: break-word;
overflow: hidden;
@ -101,4 +100,4 @@
width: 50px;
height: 50px;
}
}
}

View file

@ -23,8 +23,8 @@
}
.news__preview__title h1 {
font-size: 2em;
line-height: 1.5em;
font-weight: 700;
line-height: 1.375em;
}
.news__preview__attrs {
@ -32,7 +32,7 @@
flex-shrink: 0;
display: flex;
gap: 4px;
font-size: .9em;
font-size: .875em;
}
.news__preview__attr {
@ -62,7 +62,7 @@
}
.news__preview__content {
line-height: 1.4em;
line-height: 1.5em;
word-wrap: break-word;
overflow: hidden;
}
@ -73,5 +73,5 @@
}
.news__preview__link {
font-size: .9em;
font-size: .875em;
}

View file

@ -23,9 +23,7 @@
.pagination__link {
display: flex;
min-width: 40px;
font-size: 1.2em;
line-height: 1.5em;
padding: 3px 10px 4px;
min-height: 36px;
text-align: center;
text-decoration: none;
background-color: var(--background-colour);
@ -37,13 +35,17 @@
align-items: center;
justify-content: center;
flex: 1 0 auto;
font-feature-settings: 'ss01' 1, 'tnum' 1;
}
.pagination__link:not(:last-child) { margin-right: 1px; }
.pagination__link--disabled { --accent-colour: #555; }
.pagination__link--first, .pagination__link--last,
.pagination__link--next, .pagination__link--prev {
padding-top: 5px;
.pagination__link:not(:last-child) {
margin-right: 1px;
}
.pagination__link--disabled {
--accent-colour: #555;
}
.pagination__link--current,
.pagination__link:not(.pagination__link--disabled):hover,
.pagination__link:not(.pagination__link--disabled):active,
@ -53,5 +55,7 @@
}
@media (max-width: 800px) {
.pagination__section--pages { display: none; }
.pagination__section--pages {
display: none;
}
}

View file

@ -6,12 +6,12 @@
.permissions__line {
display: flex;
font-size: .9em;
line-height: 1.7em;
font-size: .8rem;
line-height: 1.5rem;
}
.permissions__line--header {
font-size: 1.2em;
line-height: 1.4em;
font-size: 1rem;
line-height: 1.25rem;
border-bottom: 1px solid rgba(255, 255, 255, .1);
padding-bottom: 1px;
font-weight: 700;

View file

@ -1,30 +1,26 @@
.profile__accounts__content {
display: flex;
flex-direction: column;
padding: 2px 5px;
padding: 2px 6px;
}
.profile__accounts__item {
padding-bottom: 5px;
padding-bottom: 4px;
}
.profile__accounts__item:not(:last-child) {
border-bottom: 1px solid #222;
}
.profile__accounts__notice {
font-size: 1.2em;
line-height: 1.5em;
text-align: center;
padding: 10px;
}
.profile__accounts__title {
font-size: .9em;
line-height: 1.8em;
font-size: .875em;
line-height: 1.5em;
}
.profile__accounts__value {
font-size: 1.2em;
line-height: 1.2em;
color: inherit;
text-decoration: none;
}

View file

@ -1,5 +1,5 @@
.profile__birthdate__content {
padding: 2px 5px;
padding: 2px 6px;
}
.profile__birthdate__select {
min-width: auto;
@ -8,6 +8,6 @@
display: inline-block;
}
.profile__birthdate__title {
font-size: .9em;
line-height: 1.8em;
font-size: .875em;
line-height: 1.75em;
}

View file

@ -7,14 +7,14 @@
}
.profile__forum-activity__leader {
font-size: .9em;
margin: 0 5px;
font-size: .875em;
margin: 0 4px;
}
.profile__forum-activity .forum__category__icon {
width: 30px;
height: 30px;
font-size: 1.5em;
font-size: 1.25em;
line-height: 1.5em;
flex: 0 0 30px;
}

View file

@ -14,20 +14,18 @@
padding: 1px;
}
.profile__guidelines__line--header {
font-size: 1.2em;
line-height: 1.5em;
font-weight: 700;
font-weight: 600;
margin-bottom: 2px;
border-bottom: 1px solid var(--accent-colour);
padding-bottom: 2px;
}
.profile__guidelines__line:not(.profile__guidelines__line--header) {
margin-left: 1.3em;
margin-left: 1.25em;
list-style: square;
}
.profile__guidelines__emphasis {
font-weight: 700;
font-weight: 600;
}
.profile__guidelines__link {

View file

@ -101,12 +101,11 @@
min-width: 130px;
}
.profile__header__stat__name {
font-size: .9em;
font-variant: small-caps;
font-size: .875em;
cursor: inherit;
}
.profile__header__stat__value {
font-size: 1.3em;
font-size: 1.125em;
text-align: right;
cursor: inherit;
display: block;
@ -114,6 +113,9 @@
.profile__header__stat--date .profile__header__stat__value {
text-align: left;
}
.profile__header__stat:not(.profile__header__stat--date) .profile__header__stat__value {
font-feature-settings: 'ss01' 1, 'tnum' 1;
}
.profile__header__stat--link {
cursor: pointer;
}
@ -122,17 +124,18 @@
.profile__header__stat--link:focus,
.profile__header__stat--link:active,
.profile__header__stat--active {
padding-bottom: 8px;
border-bottom: 2px solid var(--accent-colour);
}
.profile__header__username {
font-size: 2em;
line-height: 1.5em;
font-size: 1.75em;
line-height: 1.125em;
}
.profile__header__title {
font-size: .9em;
line-height: 1.2em;
font-size: .875em;
line-height: 1.25em;
}
.profile__header__country {
@ -140,9 +143,9 @@
align-items: center;
}
.profile__header__country__name {
font-size: .9em;
font-size: .875em;
margin-left: 4px;
line-height: 1.2em;
line-height: 1.25em;
}
@media (max-width: 800px) {

View file

@ -12,15 +12,15 @@
}
.profile__warnings__datetime {
font-size: .9em;
font-size: .875em;
line-height: 1.5em;
font-style: italic;
padding-top: 2px;
}
.profile__warnings__body {
padding: 0 5px;
padding: 0 6px;
}
.profile__warnings__body p {
line-height: 1.4em;
line-height: 1.375em;
}

View file

@ -11,7 +11,6 @@
overflow: hidden;
border: 1px solid transparent;
border-radius: 5px;
font-size: 1.1em;
margin: 1px 1px 1px 0;
}
@ -33,5 +32,5 @@
}
.search__category__content {
padding: 2px 5px;
padding: 2px 6px;
}

View file

@ -4,7 +4,8 @@
overflow: hidden;
border: 1px solid transparent;
border-radius: 5px;
font-size: 1.5em;
font-family: var(--font-regular);
font-size: 1.125em;
}
.search__input__background {
background-color: var(--background-colour-translucent-9);
@ -24,7 +25,7 @@
border: 0;
background-color: transparent;
color: #fff;
padding: 5px 10px;
padding: 4px 12px;
font-size: inherit;
}

View file

@ -6,7 +6,7 @@
height: 60px;
line-height: 60px;
text-align: center;
font-size: 3em;
font-size: 2.5em;
flex: 0 0 auto;
}
.search__none__content {
@ -16,6 +16,6 @@
flex: 1 1 auto;
}
.search__none__title {
font-size: 1.5em;
line-height: 1.5em;
font-size: 1.25em;
line-height: 1.25em;
}

View file

@ -22,7 +22,7 @@
.settings__account-log__important {
display: flex;
align-items: center;
font-size: 1.4em;
font-size: 1.25em;
z-index: 2;
}

View file

@ -18,8 +18,8 @@
}
.settings__account__title {
font-size: .9em;
line-height: 1.8em;
font-size: .875em;
line-height: 1.75em;
}
.settings__account__input {
@ -43,4 +43,4 @@
.settings__account__section {
grid-column: 1 / 1;
}
}
}

View file

@ -2,7 +2,7 @@
margin: 4px;
}
.settings__login-attempts__none {
padding: 2px 5px;
padding: 2px 6px;
text-align: center;
}
@ -31,7 +31,7 @@
.settings__login-attempt__important {
display: flex;
align-items: center;
font-size: 1.4em;
font-size: 1.25em;
z-index: 2;
}

View file

@ -22,8 +22,7 @@
}
.settings__role__name {
font-size: 1.2em;
line-height: 1.7em;
line-height: 1.5em;
padding: 0 5px;
margin: 2px 0;
min-width: 160px;
@ -40,8 +39,8 @@
}
.settings__role__description {
font-size: .9em;
line-height: 1.8em;
font-size: .875em;
line-height: 1.75em;
padding: 0 2px;
margin: 0 2px;
flex: 1 1 auto;
@ -50,7 +49,7 @@
.settings__role__options {
flex: 0 0 auto;
display: flex;
font-size: 1.5em;
font-size: 1.25em;
justify-content: space-evenly;
}

View file

@ -29,7 +29,7 @@
.settings__session__important {
display: flex;
align-items: center;
font-size: 1.4em;
font-size: 1.25em;
z-index: 2;
}

View file

@ -6,10 +6,13 @@
}
.settings__description {
font-size: .9em;
padding: 2px 5px;
font-size: .875rem;
line-height: 1.25rem;
border-bottom: 1px solid var(--accent-colour);
margin: 1px 1px 2px;
padding: 2px 6px;
}
.settings__description p {
margin: .25rem 0;
}
.settings__pagination {
@ -39,7 +42,7 @@
display: block;
padding: 4px;
margin: 2px;
font-size: 1.5em;
font-size: 1.25em;
line-height: 1.5em;
border-radius: 2px;
transition: background-color .2s;
@ -62,4 +65,4 @@
display: inline-block;
padding: 4px 10px;
}
}
}

View file

@ -17,9 +17,9 @@
.settings__two-factor__code__text {
color: #000;
flex: 0 0 auto;
font-size: 1.2em;
line-height: 1.5em;
padding: 2px 6px;
font-family: var(--font-monospace);
font-feature-settings: 'ss07' 1;
}
.settings__two-factor__settings {
@ -30,7 +30,7 @@
flex: 1 1 auto;
}
.settings__two-factor__settings__status {
font-size: 1.5em;
font-size: 1.25em;
line-height: 2em;
}

View file

@ -51,16 +51,16 @@
}
.usercard__details__username {
font-size: 1.4em;
line-height: 1.4em;
font-size: 1.125em;
line-height: 1.5em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.usercard__details__title {
font-size: .9em;
line-height: 1.2em;
font-size: .75em;
line-height: 1em;
}
.usercard__details__country {
@ -68,9 +68,9 @@
align-items: center;
}
.usercard__details__country__name {
font-size: .9em;
font-size: .75em;
margin-left: 4px;
line-height: 1.2em;
line-height: 1em;
}
.usercard__stats {
@ -111,17 +111,19 @@
}
.usercard__stat__name {
display: block;
font-size: .9em;
font-size: .875em;
line-height: 1.5em;
font-variant: small-caps;
cursor: inherit;
}
.usercard__stat__value {
display: block;
font-size: 1.2em;
line-height: 1.5em;
font-size: 1.125em;
line-height: 1.25em;
cursor: inherit;
}
.usercard__stat:not(.usercard__stat--wide) .usercard__stat__value {
font-feature-settings: 'ss01' 1, 'tnum' 1;
}
@media (max-width: 800px) {
.usercard {
@ -153,8 +155,8 @@
.usercard__stat__name,
.usercard__stat__value {
display: inline-block;
font-size: .9em;
line-height: 1.2em;
font-size: .875em;
line-height: 1.25em;
font-variant: initial;
}
}

View file

@ -8,8 +8,8 @@
}
.userlist__empty {
text-align: center;
font-size: 2em;
line-height: 1.5em;
font-size: 1.5em;
line-height: 1.25em;
margin: 1em;
}
.userlist__container {

View file

@ -14,7 +14,7 @@
--end-colour: #f00;
}
.warning--bigger {
font-size: 1.4em;
font-size: 1.375em;
line-height: 1.5em;
}
.warning__content {

View file

@ -0,0 +1,152 @@
const MszCommentsApi = (() => {
const argsToFormData = args => {
const formData = new FormData;
for(const name in args)
formData.append(name, args[name]);
return formData;
};
return {
getCategory: async name => {
if(typeof name !== 'string' || name.trim() === '')
throw new Error('name is not a valid category name');
const { status, body } = await $xhr.get(
`/comments/categories/${name}`,
{ type: 'json' }
);
if(status !== 200)
throw new Error(body.error?.text ?? 'something went wrong', { cause: body.error?.name ?? 'something' });
return body;
},
updateCategory: async (name, args) => {
if(typeof name !== 'string' || name.trim() === '')
throw new Error('name is not a valid category name');
if(typeof args !== 'object' || args === null)
throw new Error('args must be a non-null object');
const { status, body } = await $xhr.post(
`/comments/categories/${name}`,
{ csrf: true, type: 'json' },
args
);
if(status !== 200)
throw new Error(body.error?.text ?? 'something went wrong', { cause: body.error?.name ?? 'something' });
return body;
},
getPost: async post => {
if(typeof post !== 'string' || post.trim() === '')
throw new Error('post is not a valid post id');
const { status, body } = await $xhr.get(
`/comments/posts/${post}`,
{ type: 'json' }
);
if(status !== 200)
throw new Error(body.error?.text ?? 'something went wrong', { cause: body.error?.name ?? 'something' });
return body;
},
getPostReplies: async post => {
if(typeof post !== 'string' || post.trim() === '')
throw new Error('post is not a valid post id');
const { status, body } = await $xhr.get(
`/comments/posts/${post}/replies`,
{ type: 'json' }
);
if(status !== 200)
throw new Error(body.error?.text ?? 'something went wrong', { cause: body.error?.name ?? 'something' });
return body;
},
createPost: async args => {
if(typeof args !== 'object' || args === null)
throw new Error('args must be a non-null object');
const { status, body } = await $xhr.post(
'/comments/posts',
{ csrf: true, type: 'json' },
argsToFormData(args)
);
if(status !== 201)
throw new Error(body.error?.text ?? 'something went wrong', { cause: body.error?.name ?? 'something' });
return body;
},
updatePost: async (post, args) => {
if(typeof post !== 'string' || post.trim() === '')
throw new Error('post is not a valid post id');
if(typeof args !== 'object' || args === null)
throw new Error('args must be a non-null object');
const { status, body } = await $xhr.patch(
`/comments/posts/${post}`,
{ csrf: true, type: 'json' },
argsToFormData(args)
);
if(status !== 200)
throw new Error(body.error?.text ?? 'something went wrong', { cause: body.error?.name ?? 'something' });
return body;
},
deletePost: async post => {
if(typeof post !== 'string' || post.trim() === '')
throw new Error('post is not a valid post id');
const { status, body } = await $xhr.delete(`/comments/posts/${post}`, { csrf: true, type: 'json' });
if(status !== 204)
throw new Error(body.error?.text ?? 'something went wrong', { cause: body.error?.name ?? 'something' });
},
restorePost: async post => {
if(typeof post !== 'string' || post.trim() === '')
throw new Error('post is not a valid post id');
const { status, body } = await $xhr.post(`/comments/posts/${post}/restore`, { csrf: true, type: 'json' });
if(status !== 200)
throw new Error(body.error?.text ?? 'something went wrong', { cause: body.error?.name ?? 'something' });
},
nukePost: async post => {
if(typeof post !== 'string' || post.trim() === '')
throw new Error('post is not a valid post id');
const { status } = await $xhr.post(`/comments/posts/${post}/nuke`, { csrf: true, type: 'json' });
if(status !== 200)
throw new Error(body.error?.text ?? 'something went wrong', { cause: body.error?.name ?? 'something' });
},
createVote: async (post, vote) => {
if(typeof post !== 'string' || post.trim() === '')
throw new Error('post is not a valid post id');
if(typeof vote === 'string')
vote = parseInt(vote);
if(typeof vote !== 'number' || isNaN(vote))
throw new Error('vote must be a number');
const { status, body } = await $xhr.post(
`/comments/posts/${post}/vote`,
{ csrf: true, type: 'json' },
{ vote }
);
if(status !== 201)
throw new Error(body.error?.text ?? 'something went wrong', { cause: body.error?.name ?? 'something' });
return body;
},
deleteVote: async post => {
if(typeof post !== 'string' || post.trim() === '')
throw new Error('post is not a valid post id');
const { status, body } = await $xhr.delete(
`/comments/posts/${post}/vote`,
{ csrf: true, type: 'json' }
);
if(status !== 200)
throw new Error(body.error?.text ?? 'something went wrong', { cause: body.error?.name ?? 'something' });
return body;
},
};
})();

View file

@ -0,0 +1,119 @@
#include comments/api.js
const MszCommentsFormNotice = function(args) {
const { body } = args ?? {};
const element = <div class="comments-notice">
<div class="comments-notice-inner">{body}</div>
</div>;
return {
get element() { return element; },
};
};
const MszCommentsForm = function(args) {
const {
userInfo, catInfo, postInfo,
listing, repliesToggle, replyToggle,
} = args ?? {};
const defaultStatus = () => <>Press <kbd>enter</kbd> to submit, use <kbd>shift</kbd>+<kbd>enter</kbd> to start a new line.</>;
const status = <div class="comments-form-status-text">{defaultStatus}</div>;
const element = <form class={{ 'comments-form': true, 'comments-form-root': !postInfo }} style={`--user-colour: ${userInfo.colour}`}>
<input type="hidden" name="category" value={catInfo.name} />
{postInfo ? <input type="hidden" name="reply_to" value={postInfo.id} /> : null}
<div class="comments-form-avatar">
<img src={userInfo.avatar} alt="" width="40" height="40" class="avatar" />
</div>
<div class="comments-form-wrap">
<div class="comments-form-input">
<textarea class="input__textarea" name="body" placeholder="Share your extensive insights..." onkeydown={ev => {
if(status.classList.contains('comments-form-status-error')) {
status.classList.remove('comments-form-status-error');
$removeChildren(status);
$appendChild(status, defaultStatus);
}
if(ev.key === 'Enter' && !ev.shiftKey) {
ev.preventDefault();
element.requestSubmit();
}
if(ev.key === 'p' && ev.altKey) {
ev.preventDefault();
if(element.elements.pin)
element.elements.pin.checked = !element.elements.pin.checked;
}
}} />
</div>
<div class="comments-form-actions">
<div class="comments-form-status">{status}</div>
{userInfo.can_pin && !postInfo ? <div class="comments-form-pin"><label><input type="checkbox" name="pin" /> Pin</label></div> : null}
<div class="comments-form-post"><button class="input__button">Post</button></div>
</div>
</div>
</form>;
const forAllFields = closure => {
for(let i = 0; i < element.elements.length; ++i)
closure(element.elements[i]);
};
const setDisabled = state => {
element.classList.toggle('comments-form-disabled', state);
forAllFields(field => field.disabled = state);
};
element.onsubmit = async ev => {
ev.preventDefault();
if(element.classList.contains('comments-form-disabled'))
return;
setDisabled(true);
try {
const fields = {};
forAllFields(field => {
if(!field.name)
return;
if(field.type === 'checkbox') {
if(field.checked)
fields[field.name] = field.value;
} else
fields[field.name] = field.value;
});
listing.addPost(catInfo, userInfo, await MszCommentsApi.createPost(fields));
listing.reorder();
listing.visible = true;
if(repliesToggle) {
repliesToggle.open = true;
++repliesToggle.count;
}
if(replyToggle?.active)
replyToggle.click();
element.elements.body.value = '';
if(element.elements.pin)
element.elements.pin.checked = false;
} catch(ex) {
status.classList.add('comments-form-status-error');
status.textContent = ex;
} finally {
setDisabled(false);
}
};
return {
get element() { return element; },
focus() {
element.elements.body.focus();
},
};
};

View file

@ -0,0 +1,9 @@
#include comments/section.jsx
const MszCommentsInit = () => {
const targets = Array.from($queryAll('.js-comments'));
for(const target of targets) {
const section = new MszCommentsSection({ category: target.dataset.category });
target.replaceWith(section.element);
}
};

View file

@ -0,0 +1,652 @@
#include msgbox.jsx
#include comments/api.js
#include comments/form.jsx
const MszCommentsEntryVoteButton = function(args) {
const { name, title, icon, vote } = args ?? {};
let element, counter;
const isCast = () => element?.classList.contains('comments-entry-action-vote-cast') === true;
element = <button class={`comments-entry-action comments-entry-action-vote-${name}`} onclick={() => { vote(isCast()); }}>
{icon}
</button>;
return {
get element() { return element; },
get disabled() { return element.disabled; },
set disabled(state) {
element.disabled = state;
},
get cast() { return isCast(); },
set cast(state) {
element.classList.toggle('comments-entry-action-vote-cast', state);
},
get count() { return counter ? parseInt(counter.textContent) : 0; },
set count(count) {
if(count > 0) {
if(!counter)
element.appendChild(counter = <span />);
const formatted = count.toLocaleString();
counter.textContent = formatted;
element.title = title.replace('$0', formatted).replace('$s', count === 1 ? '' : 's');
} else {
if(counter) {
element.removeChild(counter);
counter = undefined;
}
element.title = title.replace('$0', 'No').replace('$s', 's');
}
},
};
};
const MszCommentsEntryVoteActions = function(args) {
const { vote } = args ?? {};
const like = new MszCommentsEntryVoteButton({
name: 'like',
title: '$0 like$s',
icon: <i class="fas fa-chevron-up" />,
vote: cast => { vote(cast ? 0 : 1); }
});
const dislike = new MszCommentsEntryVoteButton({
name: 'dislike',
title: '$0 dislike$s',
icon: <i class="fas fa-chevron-down" />,
vote: cast => { vote(cast ? 0 : -1); }
});
const element = <div class="comments-entry-actions-group comments-entry-actions-group-votes">
{like}
{dislike}
</div>;
return {
get element() { return element; },
get disabled() { return element.classList.contains('comments-entry-actions-group-disabled'); },
set disabled(state) {
element.classList.toggle('comments-entry-actions-group-disabled', state);
like.disabled = dislike.disabled = state;
},
updateVotes(votes) {
like.count = votes?.positive ?? 0;
like.cast = votes?.vote > 0;
dislike.count = Math.abs(votes?.negative ?? 0);
dislike.cast = votes?.vote < 0;
},
};
};
const MszCommentsEntryReplyToggleButton = function(args) {
const { replies, toggle } = args ?? {};
let icon, counter;
const element = <button class="comments-entry-action" title="No replies" onclick={() => { toggle(); }}>
{icon = <i class="fas fa-plus" />}
{counter = <span />}
</button>;
const setVisible = state => {
element.classList.toggle('hidden', !state);
};
const setOpen = state => {
icon.classList.toggle('fa-plus', !state);
icon.classList.toggle('fa-minus', state);
};
const setCount = count => {
const formatted = count.toLocaleString();
counter.textContent = formatted;
element.title = `${count} ${count === 1 ? 'reply' : 'replies'}`;
setVisible(count > 0);
};
setCount(Array.isArray(replies) ? replies.length : (replies ?? 0));
return {
get element() { return element; },
get visible() { return !element.classList.contains('hidden'); },
set visible(state) { setVisible(state); },
get count() { return parseInt(counter.textContent); },
set count(count) { setCount(count); },
get open() { return element.classList.contains('fa-plus'); },
set open(state) { setOpen(state); },
};
};
const MszCommentsEntryReplyCreateButton = function(args) {
const { toggle } = args ?? {};
const element = <button class="comments-entry-action" title="Reply" onclick={() => { toggle(); }}>
<i class="fas fa-reply" />
<span>Reply</span>
</button>;
return {
get element() { return element; },
get visible() { return !element.classList.contains('hidden'); },
set visible(state) { element.classList.toggle('hidden', !state); },
get active() { return element.classList.contains('comments-entry-action-reply-active'); },
set active(state) { element.classList.toggle('comments-entry-action-reply-active', state); },
click() {
element.click();
},
};
};
const MszCommentsEntryReplyActions = function(args) {
const { replies, toggleReplies, toggleForm } = args ?? {};
const toggle = new MszCommentsEntryReplyToggleButton({ replies, toggle: toggleReplies });
const button = toggleForm ? new MszCommentsEntryReplyCreateButton({ toggle: toggleForm }) : undefined;
const element = <div class="comments-entry-actions-group comments-entry-actions-group-replies">
{toggle}
{button}
</div>;
const setVisible = state => {
element.classList.toggle('hidden', !state);
};
return {
get element() { return element; },
get visible() { return !element.classList.contains('hidden'); },
set visible(state) { setVisible(state); },
get toggle() { return toggle; },
get button() { return button; },
updateVisible() {
setVisible(toggle.visible || button?.visible === true);
},
};
};
const MszCommentsEntryGeneralButton = function(args) {
const { icon, title, action } = args ?? {};
const element = <button class="comments-entry-action" title={title} onclick={() => { action(); }}>{icon}</button>;
return {
get element() { return element; },
get visible() { return !element.classList.contains('hidden'); },
set visible(state) { element.classList.toggle('hidden', !state); },
get disabled() { return element.disabled; },
set disabled(state) { element.disabled = state; },
};
};
const MszCommentsEntryGeneralActions = function(args) {
let deleteButton, restoreButton, nukeButton, pinButton, unpinButton;
const element = <div class="comments-entry-actions-group">
{deleteButton = args.delete ? new MszCommentsEntryGeneralButton({ icon: <i class="fas fa-trash" />, title: 'Delete', action: args.delete }) : null}
{restoreButton = args.restore ? new MszCommentsEntryGeneralButton({ icon: <i class="fas fa-trash-restore" />, title: 'Restore', action: args.restore }) : null}
{nukeButton = args.nuke ? new MszCommentsEntryGeneralButton({ icon: <i class="fas fa-radiation-alt" />, title: 'Permanently delete', action: args.nuke }) : null}
{pinButton = args.pin ? new MszCommentsEntryGeneralButton({ icon: <i class="fas fa-thumbtack" />, title: 'Pin', action: args.pin }) : null}
{unpinButton = args.unpin ? new MszCommentsEntryGeneralButton({ icon: <i class="fas fa-screwdriver" />, title: 'Unpin', action: args.unpin }) : null}
</div>;
return {
get element() { return element; },
get visible() { return !element.classList.contains('hidden'); },
set visible(state) { element.classList.toggle('hidden', !state); },
get disabled() { return element.classList.contains('comments-entry-actions-group-disabled'); },
set disabled(state) {
element.classList.toggle('comments-entry-actions-group-disabled', state);
if(deleteButton)
deleteButton.disabled = state;
if(restoreButton)
restoreButton.disabled = state;
if(nukeButton)
nukeButton.disabled = state;
if(pinButton)
pinButton.disabled = state;
if(unpinButton)
unpinButton.disabled = state;
},
get deleteButton() { return deleteButton; },
get restoreButton() { return restoreButton; },
get nukeButton() { return nukeButton; },
get deleteVisible() { return deleteButton?.visible === true; },
set deleteVisible(state) {
if(deleteButton)
deleteButton.visible = state;
if(restoreButton)
restoreButton.visible = !state;
if(nukeButton)
nukeButton.visible = !state;
},
get pinButton() { return pinButton; },
get unpinButton() { return unpinButton; },
get pinVisible() { return pinButton?.visible === true; },
set pinVisible(state) {
if(pinButton)
pinButton.visible = state;
if(unpinButton)
unpinButton.visible = !state;
},
};
};
const MszCommentsEntryActions = function() {
const element = <div class="comments-entry-actions hidden" />;
const hideIfNoChildren = () => {
element.classList.toggle('hidden', element.childElementCount < 1);
};
hideIfNoChildren();
return {
get element() { return element; },
appendGroup(group) {
element.appendChild(group.element);
hideIfNoChildren();
},
};
};
const MszCommentsEntry = function(catInfo, userInfo, postInfo, listing, root) {
const actions = new MszCommentsEntryActions;
const voteActions = new MszCommentsEntryVoteActions({
vote: async vote => {
if(voteActions.disabled)
return;
voteActions.disabled = true;
try {
voteActions.updateVotes(vote === 0
? await MszCommentsApi.deleteVote(postInfo.id)
: await MszCommentsApi.createVote(postInfo.id, vote));
} catch(ex) {
console.error(ex);
} finally {
enableVoteActionsMaybe();
}
}
});
actions.appendGroup(voteActions);
const enableVoteActionsMaybe = () => {
voteActions.disabled = !userInfo.can_vote || !!postInfo.deleted || !!catInfo.locked;
};
enableVoteActionsMaybe();
voteActions.updateVotes(postInfo);
const repliesIsArray = Array.isArray(postInfo.replies);
const replies = new MszCommentsListing({ hidden: !repliesIsArray });
if(repliesIsArray)
replies.addPosts(catInfo, userInfo, postInfo.replies);
const repliesElem = <div class="comments-entry-replies">
{replies}
</div>;
let replyForm;
let repliesLoaded = replies.loaded;
const replyActions = new MszCommentsEntryReplyActions({
replies: postInfo.replies,
toggleReplies: async () => {
replyActions.toggle.open = replies.visible = !replies.visible;
if(!repliesLoaded) {
repliesLoaded = true;
try {
replies.addPosts(catInfo, userInfo, await MszCommentsApi.getPostReplies(postInfo.id));
} catch(ex) {
console.error(ex);
replyActions.toggle.open = false;
repliesLoaded = false;
}
}
},
toggleForm: userInfo.can_create ? () => {
if(replyForm) {
replyActions.button.active = false;
repliesElem.removeChild(replyForm.element);
replyForm = null;
} else {
replyActions.button.active = true;
replyForm = new MszCommentsForm({
userInfo, catInfo, postInfo,
listing: replies,
repliesToggle: replyActions.toggle,
replyToggle: replyActions.button,
});
$insertBefore(replies.element, replyForm.element);
replyForm.focus();
}
} : null,
});
actions.appendGroup(replyActions);
const enableReplyButtonMaybe = () => {
if(replyActions.button)
replyActions.button.visible = !catInfo.locked && !postInfo.deleted;
replyActions.updateVisible();
};
replyActions.toggle.open = replies.visible;
enableReplyButtonMaybe();
const generalActions = new MszCommentsEntryGeneralActions({
delete: postInfo.can_delete ? async () => {
generalActions.disabled = true;
try {
if(!await MszShowConfirmBox(`Are you sure you want to delete comment #${postInfo.id}?`, 'Deleting a comment'))
return;
postInfo.deleted = new Date;
await MszCommentsApi.deletePost(postInfo.id);
if(generalActions.restoreButton) {
setOptionalTime(deletedElem, new Date, 'commentDeleted');
generalActions.deleteVisible = false;
enableVoteActionsMaybe();
enableReplyButtonMaybe();
listing.reorder();
} else
nukeThePost();
} catch(ex) {
delete postInfo.deleted;
console.error(ex);
} finally {
generalActions.disabled = false;
}
} : null,
restore: postInfo.can_delete_any ? async () => {
generalActions.disabled = true;
const deleted = postInfo.deleted;
try {
delete postInfo.deleted;
await MszCommentsApi.restorePost(postInfo.id);
setOptionalTime(deletedElem, null, 'commentDeleted');
generalActions.deleteVisible = true;
enableVoteActionsMaybe();
enableReplyButtonMaybe();
listing.reorder();
} catch(ex) {
postInfo.deleted = deleted;
console.error(ex);
} finally {
generalActions.disabled = false;
}
} : null,
nuke: postInfo.can_delete_any ? async () => {
generalActions.disabled = true;
try {
await MszCommentsApi.nukePost(postInfo.id);
nukeThePost();
} catch(ex) {
console.error(ex);
} finally {
generalActions.disabled = false;
}
} : null,
pin: root && userInfo.can_pin ? async () => {
generalActions.disabled = true;
try {
if(!await MszShowConfirmBox(`Are you sure you want to pin comment #${postInfo.id}?`, 'Pinning a comment'))
return;
const result = await MszCommentsApi.updatePost(postInfo.id, { pin: 'on' });
generalActions.pinVisible = !result.pinned;
setOptionalTime(pinnedElem, result.pinned, 'commentPinned');
listing.reorder();
} catch(ex) {
console.error(ex);
} finally {
generalActions.disabled = false;
}
} : null,
unpin: root && userInfo.can_pin ? async () => {
generalActions.disabled = true;
try {
const result = await MszCommentsApi.updatePost(postInfo.id, { pin: '' });
generalActions.pinVisible = !result.pinned;
setOptionalTime(pinnedElem, result.pinned, 'commentPinned');
listing.reorder();
} catch(ex) {
console.error(ex);
} finally {
generalActions.disabled = false;
}
} : null,
});
actions.appendGroup(generalActions);
generalActions.deleteVisible = !postInfo.deleted;
generalActions.pinVisible = !postInfo.pinned;
const userAvatarElem = <img alt="" width="40" height="40" class="avatar" />;
const userNameElem = <div class="comments-entry-user" />;
const createdTimeElem = <a href={`#comment-${postInfo.id}`} class="comments-entry-time-link" />;
const editedElem = <div class="comments-entry-time comments-entry-time-edited">
<div class="comments-entry-time-icon"><i class="fas fa-pencil-alt" /></div>
</div>;
const pinnedElem = <div class="comments-entry-time comments-entry-time-pinned">
<div class="comments-entry-time-icon"><i class="fas fa-thumbtack" /></div>
</div>;
const deletedElem = <div class="comments-entry-time comments-entry-time-deleted">
<div class="comments-entry-time-icon"><i class="fas fa-trash" /></div>
</div>;
const bodyElem = <div class="comments-entry-body" />;
const setBody = body => { bodyElem.textContent = body ?? '[deleted]'; };
setBody(postInfo?.body);
const element = <div id={`comment-${postInfo.id}`} data-comment={postInfo.id} class={{ 'comments-entry': true, 'comments-entry-root': root }}>
<div class="comments-entry-main">
<div class="comments-entry-avatar">
{userAvatarElem}
</div>
<div class="comments-entry-wrap">
<div class="comments-entry-meta">
<div class="comments-entry-user">
{userNameElem}
</div>
<div class="comments-entry-time">
<div class="comments-entry-time-icon">&mdash;</div>
{createdTimeElem}
</div>
{editedElem}
{pinnedElem}
{deletedElem}
</div>
{bodyElem}
{actions}
</div>
</div>
{repliesElem}
</div>;
const setUserInfo = userInfo => {
$removeChildren(userNameElem);
if(userInfo) {
if(typeof userInfo.colour === 'string')
element.style.setProperty('--user-colour', userInfo.colour);
userAvatarElem.src = userInfo.avatar;
userNameElem.appendChild(<a class="comments-entry-user-link" href={userInfo.profile} style="color: var(--user-colour);">{userInfo.name}</a>);
} else {
element.style.removeProperty('--user-colour');
userAvatarElem.src = '/images/no-avatar.png';
userNameElem.appendChild(<span class="comments-entry-user-dead">Deleted user</span>);
}
};
setUserInfo(postInfo.user);
const setCreatedTime = date => {
if(typeof date === 'string')
date = new Date(date);
$removeChildren(createdTimeElem);
element.dataset.commentCreated = date.getTime();
const time = <time class="comments-entry-time-text" datetime={date.toISOString()} title={date.toString()}>{MszSakuya.formatTimeAgo(date)}</time>;
createdTimeElem.appendChild(time);
MszSakuya.trackElement(time);
};
setCreatedTime(postInfo.created);
const updateOrderValue = () => {
let order = parseInt(element.dataset.commentCreated ?? 0);
if(element.dataset.commentDeleted !== undefined)
order -= parseInt(element.dataset.commentDeleted);
else if(element.dataset.commentPinned !== undefined)
order += parseInt(element.dataset.commentPinned);
element.dataset.commentOrder = order;
};
const setOptionalTime = (elem, date, name, reorder=true, textIfTrue=null) => {
if(typeof date === 'string')
date = new Date(date);
while(!(elem.lastChild instanceof HTMLDivElement))
elem.removeChild(elem.lastChild);
if(date) {
if(date instanceof Date) {
if(name)
element.dataset[name] = date.getTime();
const timeElem = <time class="comments-entry-time-text" datetime={date.toISOString()} title={date.toString()}>{MszSakuya.formatTimeAgo(date)}</time>
elem.appendChild(timeElem);
MszSakuya.trackElement(timeElem);
} else {
// this is kiiiind of a hack but commentCreated isn't updated through this function so who cares lol !
if(name)
element.dataset[name] = element.dataset.commentCreated;
if(typeof textIfTrue === 'string')
elem.appendChild(<span>{textIfTrue}</span>);
}
elem.classList.remove('hidden');
} else {
if(name)
delete element.dataset[name];
elem.classList.add('hidden');
}
if(reorder)
updateOrderValue();
};
setOptionalTime(editedElem, postInfo.edited, 'commentEdited', false);
setOptionalTime(pinnedElem, postInfo.pinned, 'commentPinned', false);
setOptionalTime(deletedElem, postInfo.deleted, 'commentDeleted', false, 'deleted');
updateOrderValue();
const nukeThePost = () => {
if(replies.count < 1 && replyActions.toggle.count < 1)
listing.element.removeChild(element);
else {
generalActions.visible = false;
generalActions.disabled = true;
enableVoteActionsMaybe();
enableReplyButtonMaybe();
setUserInfo(null);
setBody(null);
setOptionalTime(deletedElem, true, 'commentDeleted', true, 'deleted');
listing.reorder();
}
};
return {
get element() { return element; },
updateLocked() {
enableVoteActionsMaybe();
enableReplyButtonMaybe();
replies.updateLocked();
},
};
};
const MszCommentsListing = function(options) {
let { hidden, root } = options ?? {};
let loading; // intentionally left as undefined here so the === null is still false
const entries = new Map;
const element = <div class={{ 'comments-listing': true, 'comments-listing-root': root, 'hidden': hidden }} />;
const pub = {
get element() { return element; },
get count() { return loading === null ? element.childElementCount : 0; },
get visible() { return !element.classList.contains('hidden'); },
set visible(state) { element.classList.toggle('hidden', !state); },
get loaded() { return loading === null; },
reset() {
entries.clear();
$removeChildren(element);
loading = new MszLoading;
element.appendChild(loading.element);
},
removeLoading() {
loading?.element.remove();
loading = null;
},
reorder() {
// this feels yucky but it works
const items = Array.from(element.children).sort((a, b) => parseInt(b.dataset.commentOrder) - parseInt(a.dataset.commentOrder));
for(const item of items)
element.appendChild(item);
},
updateLocked() {
for(const [, value] of entries)
value.updateLocked();
},
addPost(catInfo, userInfo, postInfo) {
const existing = element.querySelector(`[data-comment="${postInfo.id}"]`);
if(existing)
element.removeChild(existing);
const entry = new MszCommentsEntry(catInfo ?? {}, userInfo ?? {}, postInfo, pub, root);
entries.set(postInfo.id, entry);
element.appendChild(entry.element);
},
addPosts(catInfo, userInfo, posts) {
try {
if(!Array.isArray(posts))
throw 'posts must be an array';
catInfo ??= {};
userInfo ??= {};
for(const postInfo of posts)
pub.addPost(catInfo, userInfo, postInfo);
} finally {
pub.removeLoading();
}
},
};
return pub;
};

View file

@ -0,0 +1,94 @@
#include msgbox.jsx
#include comments/api.js
const MszCommentsOptionsLockAction = function(catInfo, reinit) {
const element = <button class="comments-options-action"/>;
const setLocked = state => {
$removeChildren(element);
if(state) {
element.appendChild(<i class="fas fa-unlock" />);
element.appendChild(<span>Unlock</span>);
} else {
element.appendChild(<i class="fas fa-lock" />);
element.appendChild(<span>Lock</span>);
}
};
setLocked(catInfo.locked);
element.onclick = async () => {
element.disabled = true;
try {
if(!catInfo.locked && !await MszShowConfirmBox(`Are you sure you want to lock comments category ${catInfo.name}?`, 'Locked a comment section'))
return;
const result = await MszCommentsApi.updateCategory(catInfo.name, { lock: catInfo.locked ? '0' : '1' });
if('locked' in result) {
if(result.locked)
catInfo.locked = result.locked;
else
delete catInfo.locked;
}
setLocked(catInfo.locked);
reinit();
} catch(ex) {
console.error(ex);
} finally {
element.disabled = false;
}
};
return {
get element() { return element; },
};
};
const MszCommentsOptionsRetryAction = function(section) {
const element = <button class="comments-options-action">
<i class="fas fa-sync-alt" />
Retry
</button>;
element.onclick = async () => {
element.disabled = true;
try {
await section.reload();
} catch(ex) {
console.error(ex);
} finally {
element.disabled = false;
}
};
return {
get element() { return element; },
};
};
const MszCommentsOptions = function() {
const actions = <div class="comments-options-actions" />;
const element = <div class="comments-options hidden">
{actions}
</div>;
const hideIfNoChildren = () => {
element.classList.toggle('hidden', actions.childElementCount < 1);
};
hideIfNoChildren();
return {
get element() { return element; },
reset() {
$removeChildren(actions);
hideIfNoChildren();
},
appendAction(action) {
actions.appendChild(action.element);
hideIfNoChildren();
},
};
};

View file

@ -0,0 +1,83 @@
#include msgbox.jsx
#include comments/api.js
#include comments/form.jsx
#include comments/listing.jsx
#include comments/options.jsx
const MszCommentsSection = function(args) {
let { category: catName } = args ?? {};
const options = new MszCommentsOptions;
const listing = new MszCommentsListing({ root: true });
const element = <div class="comments">
{options}
{listing}
</div>;
let form;
let retryAct;
const clearForm = () => {
if(form) {
element.removeChild(form.element);
form = undefined;
}
};
const setForm = elem => {
clearForm();
form = elem;
$insertBefore(element.firstChild, form.element);
};
const initForm = (userInfo, catInfo) => {
if(!userInfo)
setForm(new MszCommentsFormNotice({ body: 'You must be logged in to post comments.' }));
else if(!userInfo.can_create)
setForm(new MszCommentsFormNotice({ body: 'You are not allowed to comment.' }));
else if(catInfo.locked)
setForm(new MszCommentsFormNotice({ body: 'This comment section is closed.' }));
else
setForm(new MszCommentsForm({ userInfo, catInfo, listing }));
};
const pub = {
get element() { return element; },
async reload() {
clearForm();
listing.reset();
try {
const { user, category, posts } = await MszCommentsApi.getCategory(catName);
retryAct = undefined;
options.reset();
initForm(user, category);
if(user?.can_lock)
options.appendAction(new MszCommentsOptionsLockAction(
category,
() => {
initForm(user, category);
listing.updateLocked();
}
));
listing.addPosts(category, user, posts);
} catch(ex) {
console.error(ex);
listing.removeLoading();
form = new MszCommentsFormNotice({ body: 'Failed to load comments.' });
$insertBefore(element.firstChild, form.element);
if(!retryAct)
options.appendAction(retryAct = new MszCommentsOptionsRetryAction(pub));
}
},
};
pub.reload();
return pub;
};

View file

@ -9,41 +9,32 @@ const MszAudioEmbedPlayerEvents = function() {
};
const MszAudioEmbed = function(player) {
const elem = $create({
attrs: {
classList: ['aembed', 'aembed-' + player.getType()],
},
child: player,
});
const element = $element('div', { classList: ['aembed', 'aembed-' + player.getType()] }, player);
return {
getElement: function() {
return elem;
},
get element() { return element; },
get player() { return player; },
appendTo: function(target) {
target.appendChild(elem);
target.appendChild(element);
},
insertBefore: function(ref) {
$insertBefore(ref, elem);
$insertBefore(ref, element);
},
nuke: function() {
elem.remove();
element.remove();
},
replaceElement(target) {
$insertBefore(target, elem);
$insertBefore(target, element);
target.remove();
},
getPlayer: function() {
return player;
},
};
};
const MszAudioEmbedPlayer = function(metadata, options) {
options = options || {};
const shouldAutoplay = options.autoplay === undefined || options.autoplay,
haveNativeControls = options.nativeControls !== undefined && options.nativeControls;
const shouldAutoplay = options.autoplay === undefined || options.autoplay;
const haveNativeControls = options.nativeControls !== undefined && options.nativeControls;
const playerAttrs = {
src: metadata.url,
@ -58,26 +49,21 @@ const MszAudioEmbedPlayer = function(metadata, options) {
const watchers = new MszWatchers;
watchers.define(MszAudioEmbedPlayerEvents());
const player = $create({
tag: 'audio',
attrs: playerAttrs,
});
const element = $element('audio', playerAttrs);
const pub = {
getElement: function() {
return player;
},
get element() { return element; },
appendTo: function(target) {
target.appendChild(player);
target.appendChild(element);
},
insertBefore: function(ref) {
$insertBefore(ref, player);
$insertBefore(ref, element);
},
nuke: function() {
player.remove();
element.remove();
},
replaceElement(target) {
$insertBefore(target, player);
$insertBefore(target, element);
target.remove();
},
getType: function() { return 'external'; },
@ -86,76 +72,76 @@ const MszAudioEmbedPlayer = function(metadata, options) {
pub.watch = (name, handler) => watchers.watch(name, handler);
pub.unwatch = (name, handler) => watchers.unwatch(name, handler);
player.addEventListener('play', function() { watchers.call('play', pub); });
element.addEventListener('play', function() { watchers.call('play', pub); });
const pPlay = function() { player.play(); };
const pPlay = function() { element.play(); };
pub.play = pPlay;
const pPause = function() { player.pause(); };
const pPause = function() { element.pause(); };
pub.pause = pPause;
let stopCalled = false;
player.addEventListener('pause', function() {
element.addEventListener('pause', function() {
watchers.call(stopCalled ? 'stop' : 'pause', pub);
stopCalled = false;
});
const pStop = function() {
stopCalled = true;
player.pause();
player.currentTime = 0;
element.pause();
element.currentTime = 0;
};
pub.stop = pStop;
const pIsPlaying = function() { return !player.paused; };
const pIsPlaying = function() { return !element.paused; };
pub.isPlaying = pIsPlaying;
const pIsMuted = function() { return player.muted; };
const pIsMuted = function() { return element.muted; };
pub.isMuted = pIsMuted;
let lastMuteState = player.muted;
player.addEventListener('volumechange', function() {
if(lastMuteState !== player.muted) {
lastMuteState = player.muted;
let lastMuteState = element.muted;
element.addEventListener('volumechange', function() {
if(lastMuteState !== element.muted) {
lastMuteState = element.muted;
watchers.call('mute', pub, [lastMuteState]);
} else
watchers.call('volume', pub, [player.volume]);
watchers.call('volume', pub, [element.volume]);
});
const pSetMuted = function(state) { player.muted = state; };
const pSetMuted = function(state) { element.muted = state; };
pub.setMuted = pSetMuted;
const pGetVolume = function() { return player.volume; };
const pGetVolume = function() { return element.volume; };
pub.getVolume = pGetVolume;
const pSetVolume = function(volume) { player.volume = volume; };
const pSetVolume = function(volume) { element.volume = volume; };
pub.setVolume = pSetVolume;
const pGetPlaybackRate = function() { return player.playbackRate; };
const pGetPlaybackRate = function() { return element.playbackRate; };
pub.getPlaybackRate = pGetPlaybackRate;
player.addEventListener('ratechange', function() {
watchers.call('rate', pub, [player.playbackRate]);
element.addEventListener('ratechange', function() {
watchers.call('rate', pub, [element.playbackRate]);
});
const pSetPlaybackRate = function(rate) { player.playbackRate = rate; };
const pSetPlaybackRate = function(rate) { element.playbackRate = rate; };
pub.setPlaybackRate = pSetPlaybackRate;
window.addEventListener('durationchange', function() {
watchers.call('duration', pub, [player.duration]);
watchers.call('duration', pub, [element.duration]);
});
const pGetDuration = function() { return player.duration; };
const pGetDuration = function() { return element.duration; };
pub.getDuration = pGetDuration;
window.addEventListener('timeupdate', function() {
watchers.call('time', pub, [player.currentTime]);
watchers.call('time', pub, [element.currentTime]);
});
const pGetTime = function() { return player.currentTime; };
const pGetTime = function() { return element.currentTime; };
pub.getTime = pGetTime;
const pSeek = function(time) { player.currentTime = time; };
const pSeek = function(time) { element.currentTime = time; };
pub.seek = pSeek;
return pub;
@ -167,38 +153,32 @@ const MszAudioEmbedPlaceholder = function(metadata, options) {
if(typeof options.player !== 'function' && typeof options.onclick !== 'function')
throw 'Neither a player nor an onclick handler were provided.';
let title = [],
album = undefined;
let title = [];
let album;
if(metadata.media !== undefined && metadata.media.tags !== undefined) {
const tags = metadata.media.tags;
if(tags.title !== undefined) {
if(tags.artist !== undefined) {
title.push({
tag: 'span',
attrs: {
className: 'aembedph-info-title-artist',
},
child: tags.artist,
});
title.push($element(
'span',
{ className: 'aembedph-info-title-artist' },
tags.artist,
));
title.push(' - ');
}
title.push({
tag: 'span',
attrs: {
className: 'aembedph-info-title-title',
},
child: tags.title,
});
title.push($element(
'span',
{ className: 'aembedph-info-title-title' },
tags.title,
));
} else {
title.push({
tag: 'span',
attrs: {
className: 'aembedph-info-title-title',
},
child: metadata.title,
});
title.push($element(
'span',
{ className: 'aembedph-info-title-title' },
metadata.title,
));
}
if(tags.album !== undefined && tags.album !== tags.title)
@ -207,159 +187,131 @@ const MszAudioEmbedPlaceholder = function(metadata, options) {
const infoChildren = [];
infoChildren.push({
tag: 'h1',
attrs: {
className: 'aembedph-info-title',
},
child: title,
});
infoChildren.push($element(
'h1',
{ className: 'aembedph-info-title' },
...title,
));
infoChildren.push({
tags: 'p',
attrs: {
className: 'aembedph-info-album',
},
child: album,
});
infoChildren.push($element(
'p',
{ className: 'aembedph-info-album' },
album,
));
infoChildren.push({
tag: 'div',
attrs: {
className: 'aembedph-info-site',
},
child: metadata.site_name,
});
infoChildren.push($element(
'div',
{ className: 'aembedph-info-site' },
metadata.site_name,
));
const style = [];
if(typeof metadata.color !== 'undefined')
style.push('--aembedph-colour: ' + metadata.color);
const coverBackground = $create({
attrs: {
className: 'aembedph-bg',
},
child: {
tag: 'img',
attrs: {
alt: '',
src: metadata.image,
onerror: function(ev) {
coverBackground.classList.add('aembedph-bg-none');
},
const coverBackground = $element(
'div',
{ className: 'aembedph-bg' },
$element('img', {
alt: '',
src: metadata.image,
onerror: function(ev) {
coverBackground.classList.add('aembedph-bg-none');
},
},
});
}),
);
const coverPreview = $create({
attrs: {
className: 'aembedph-info-cover',
},
child: {
tag: 'img',
attrs: {
alt: '',
src: metadata.image,
onerror: function(ev) {
coverPreview.classList.add('aembedph-info-cover-none');
},
const coverPreview = $element(
'div',
{ className: 'aembedph-info-cover' },
$element('img', {
alt: '',
src: metadata.image,
onerror: function(ev) {
coverPreview.classList.add('aembedph-info-cover-none');
},
},
});
}),
);
const pub = {};
let element;
const pub = {
get element() { return element; },
};
const elem = $create({
attrs: {
element = $element(
'div',
{
className: ('aembedph aembedph-' + (options.type || 'external')),
style: style.join(';'),
title: metadata.title,
},
child: [
coverBackground,
{
attrs: {
className: 'aembedph-fg',
},
child: [
{
attrs: {
className: 'aembedph-info',
},
child: [
coverPreview,
{
attrs: {
className: 'aembedph-info-body',
},
child: infoChildren,
}
],
coverBackground,
$element(
'div',
{ className: 'aembedph-fg' },
$element(
'div',
{ className: 'aembedph-info' },
coverPreview,
$element(
'div',
{ className: 'aembedph-info-body' },
...infoChildren
),
),
$element(
'div',
{
className: 'aembedph-play',
onclick: function(ev) {
if(ev.target.tagName.toLowerCase() === 'a')
return;
if(typeof options.onclick === 'function') {
options.onclick(ev);
return;
}
const player = new options.player(metadata, options);
const embed = new MszAudioEmbed(player);
if(options.autoembed === undefined || options.autoembed)
embed.replaceElement(element);
if(typeof options.onembed === 'function')
options.onembed(embed);
},
{
attrs: {
className: 'aembedph-play',
onclick: function(ev) {
if(ev.target.tagName.toLowerCase() === 'a')
return;
if(typeof options.onclick === 'function') {
options.onclick(ev);
return;
}
const player = new options.player(metadata, options);
const embed = new MszAudioEmbed(player);
if(options.autoembed === undefined || options.autoembed)
embed.replaceElement(elem);
if(typeof options.onembed === 'function')
options.onembed(embed);
},
},
$element(
'div',
{ className: 'aembedph-play-internal', },
$element('i', { className: 'fas fa-play fa-3x fa-fw' }),
),
$element(
'div',
{ className: 'aembedph-play-external' },
$element(
'a',
{
className: 'aembedph-play-external-link',
href: metadata.url,
target: '_blank',
rel: 'noopener',
},
child: [
{
attrs: {
className: 'aembedph-play-internal',
},
child: {
tag: 'i',
attrs: {
className: 'fas fa-play fa-3x fa-fw',
},
},
},
{
attrs: {
className: 'aembedph-play-external',
},
child: {
tag: 'a',
attrs: {
className: 'aembedph-play-external-link',
href: metadata.url,
target: '_blank',
rel: 'noopener',
},
child: ('or listen on ' + metadata.site_name + '?')
},
}
],
}
],
},
],
});
`or listen on ${metadata.site_name}?`
),
),
),
),
);
pub.getElement = function() { return elem; };
pub.appendTo = function(target) { target.appendChild(elem); };
pub.insertBefore = function(ref) { $insertBefore(ref, elem); };
pub.appendTo = function(target) { target.appendChild(element); };
pub.insertBefore = function(ref) { $insertBefore(ref, element); };
pub.nuke = function() {
elem.remove();
element.remove();
};
pub.replaceElement = function(target) {
$insertBefore(target, elem);
$insertBefore(target, element);
target.remove();
};

View file

@ -30,18 +30,7 @@ const MszEmbed = (function() {
}
$removeChildren(target);
target.appendChild($create({
tag: 'i',
attrs: {
className: 'fas fa-2x fa-spinner fa-pulse',
style: {
width: '32px',
height: '32px',
lineHeight: '32px',
textAlign: 'center',
},
},
}));
target.appendChild((new MszLoading({ inline: true })).element);
if(filtered.has(cleanUrl))
filtered.get(cleanUrl).push(target);
@ -51,16 +40,16 @@ const MszEmbed = (function() {
const replaceWithUrl = function(targets, url) {
for(const target of targets) {
let body = $create({
tag: 'a',
attrs: {
let body = $element(
'a',
{
className: 'link',
href: url,
target: '_blank',
rel: 'noopener noreferrer',
},
child: url
});
url
);
$insertBefore(target, body);
target.remove();
}

Some files were not shown because too many files have changed in this diff Show more