Rewrote sidebar Javascript.

This commit is contained in:
Pachira 2024-06-24 21:36:07 +00:00
parent 73cd2c7623
commit 7750e2b1cb
40 changed files with 2136 additions and 2285 deletions

338
build.js
View file

@ -1,302 +1,50 @@
// IMPORTS
const assproc = require('@railcomm/assproc');
const { join: pathJoin } = require('path');
const fs = require('fs');
const swc = require('@swc/core');
const path = require('path');
const util = require('util');
const postcss = require('postcss');
const htmlminify = require('html-minifier-terser').minify;
const childProcess = require('child_process');
const utils = require('./src/utils.js');
const assproc = require('./src/assproc.js');
const exec = require('util').promisify(require('child_process').exec);
// CONFIG
const rootDir = __dirname;
const configFile = path.join(rootDir, 'config/config.json');
const srcDir = path.join(rootDir, 'src');
const pubDir = path.join(rootDir, 'public');
const pubAssetsDir = path.join(pubDir, 'assets');
const isDebugBuild = fs.existsSync(path.join(rootDir, '.debug'));
const buildTasks = {
js: [
{ source: 'proto.js', target: '/assets', name: 'proto.{hash}.js', buildVar: 'MAMI_PROTO_JS', buildVarsTarget: 'self' },
{ source: 'mami.js', target: '/assets', name: 'mami.{hash}.js', buildVar: 'MAMI_MAIN_JS' },
{ source: 'init.js', target: '/assets', name: 'init.{hash}.js', es: 'es5' },
],
css: [
{ source: 'mami.css', target: '/assets', name: 'mami.{hash}.css' },
],
html: [
{ source: 'mami.html', target: '/', name: 'index.html' },
],
webmanifest: [
{ source: 'mami.webmanifest', target: '/', name: 'mami.webmanifest', icons: '/icons' }
],
};
// PREP
const config = JSON.parse(fs.readFileSync(configFile));
const postcssPlugins = [ require('autoprefixer')({ remove: false }) ];
if(!isDebugBuild)
postcssPlugins.push(require('cssnano')({
preset: [
'cssnano-preset-default',
{
minifyGradients: false,
reduceIdents: false,
zindex: true,
}
],
}));
const swcJscOptions = {
target: 'es2020',
loose: false,
externalHelpers: false,
keepClassNames: true,
preserveAllComments: false,
transform: {},
parser: {
syntax: 'ecmascript',
jsx: true,
dynamicImport: false,
privateMethod: false,
functionBind: false,
exportDefaultFrom: false,
exportNamespaceFrom: false,
decorators: false,
decoratorsBeforeExport: false,
topLevelAwait: true,
importMeta: false,
},
transform: {
react: {
runtime: 'classic',
pragma: '$er',
},
},
};
const htmlMinifyOptions = {
collapseBooleanAttributes: true,
collapseWhitespace: true,
conservativeCollapse: false,
decodeEntities: false,
quoteCharacter: '"',
removeAttributeQuotes: true,
removeComments: true,
removeEmptyAttributes: true,
removeOptionalTags: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
sortAttributes: true,
sortClassName: true,
};
// BUILD
(async () => {
const htmlVars = { 'title': config.title };
const buildVars = {
FUTAMI_DEBUG: isDebugBuild,
FUTAMI_URL: config.common_url,
MAMI_URL: config.modern_url,
AMI_URL: config.compat_url,
GIT_HASH: await (() => {
return new Promise((resolve, reject) => {
childProcess.exec('git log --pretty="%H" -n1 HEAD', (err, stdout) => {
if(err) reject(err);
else resolve(stdout.trim());
});
});
})(),
const config = JSON.parse(fs.readFileSync(pathJoin(__dirname, 'config/config.json')));
const isDebug = fs.existsSync(pathJoin(__dirname, '.debug'));
const env = {
root: __dirname,
source: pathJoin(__dirname, 'src'),
public: pathJoin(__dirname, 'public'),
debug: isDebug,
swc: {
es: 'es2020',
},
vars: {
html: {
title: config.title,
},
build: {
FUTAMI_DEBUG: isDebug,
FUTAMI_URL: config.common_url,
MAMI_URL: config.modern_url,
AMI_URL: config.compat_url,
GIT_HASH: (await exec('git log --pretty="%H" -n1 HEAD')).stdout,
},
},
};
console.log('Ensuring assets directory exists...');
fs.mkdirSync(pubAssetsDir, { recursive: true });
const tasks = {
js: [
{ source: 'proto.js', target: '/assets', name: 'proto.{hash}.js', vars: { build: 'MAMI_PROTO_JS', html: ':source' } },
{ source: 'mami.js', target: '/assets', name: 'mami.{hash}.js', vars: { build: 'MAMI_MAIN_JS', html: ':source' } },
{ source: 'init.js', target: '/assets', name: 'init.{hash}.js', es: 'es5', vars: { html: ':source' } },
],
css: [
{ source: 'mami.css', target: '/assets', name: 'mami.{hash}.css', vars: { html: ':source' } },
],
webmanifest: [
{ source: 'mami.webmanifest', target: '/', name: 'mami.webmanifest', icons: '/icons', vars: { html: ':source' }, body: { name: config.title, short_name: config.title } }
],
html: [
{ source: 'mami.html', target: '/', name: 'index.html', template: 'html' },
],
};
console.log();
console.log('JS assets');
for(const info of buildTasks.js) {
console.log(`=> Building ${info.source}...`);
let origTarget = undefined;
if('es' in info) {
origTarget = swcJscOptions.target;
swcJscOptions.target = info.es;
}
const assprocOpts = {
prefix: '#',
entry: info.entry || 'main.js',
buildVars: buildVars,
};
const swcOpts = {
filename: info.source,
sourceMaps: false,
isModule: false,
minify: !isDebugBuild,
jsc: swcJscOptions,
};
const pubName = await assproc.process(path.join(srcDir, info.source), assprocOpts)
.then(output => swc.transform(output, swcOpts))
.then(output => {
const name = utils.strtr(info.name, { hash: utils.shortHash(output.code) });
const pubName = path.join(info.target || '', name);
console.log(` Saving to ${pubName}...`);
fs.writeFileSync(path.join(pubDir, pubName), output.code);
return pubName;
});
if(origTarget !== undefined)
swcJscOptions.target = origTarget;
htmlVars[info.source] = pubName;
if(typeof info.buildVar === 'string')
buildVars[info.buildVar] = pubName;
}
console.log();
console.log('CSS assets');
for(const info of buildTasks.css) {
console.log(`=> Building ${info.source}...`);
const sourcePath = path.join(srcDir, info.source);
const assprocOpts = {
prefix: '@',
entry: info.entry || 'main.css',
};
const postcssOpts = { from: sourcePath };
htmlVars[info.source] = await assproc.process(sourcePath, assprocOpts)
.then(output => postcss(postcssPlugins).process(output, postcssOpts)
.then(output => {
const name = utils.strtr(info.name, { hash: utils.shortHash(output.css) });
const pubName = path.join(info.target || '', name);
console.log(` Saving to ${pubName}...`);
fs.writeFileSync(path.join(pubDir, pubName), output.css);
return pubName;
}));
}
console.log();
console.log('Webmanifest assets...');
for(const info of buildTasks.webmanifest) {
console.log(`=> Building ${info.source}...`);
try {
const body = JSON.parse(fs.readFileSync(path.join(srcDir, info.source)));
body.name = config.title;
body.short_name = config.title;
if(typeof info.icons === 'string') {
const iconsDir = path.join(pubDir, info.icons);
if(fs.existsSync(iconsDir)) {
const files = (await fs.promises.readdir(iconsDir)).sort((a, b) => a.localeCompare(b, undefined, {
numeric: true,
sensitivity: 'base',
}));
body.icons = [];
for(const file of files) {
if(!file.endsWith('.png'))
continue;
const icon = {
src: path.join(info.icons, file),
type: 'image/png',
};
if(file[0] !== 'c') {
if(file[0] === 'm')
icon.purpose = 'maskable';
else if(file[0] === 'w')
icon.purpose = 'monochrome';
else continue;
}
let res = '';
for(let i = 1; i < file.length; ++i) {
if(file[i] === 'x')
break;
res += file[i];
}
if(res.length > 0)
icon.sizes = `${res}x${res}`;
body.icons.push(icon);
}
}
}
const data = JSON.stringify(body);
const name = utils.strtr(info.name, { hash: utils.shortHash(data) });
const pubName = path.join(info.target || '', name);
console.log(` Saving to ${pubName}...`);
const fullPath = path.join(pubDir, pubName);
const dirPath = path.dirname(fullPath);
if(!fs.existsSync(dirPath))
fs.mkdirSync(dirPath, { recursive: true });
fs.writeFileSync(fullPath, data);
htmlVars[info.source] = pubName;
} catch(err) {
console.error(err);
}
}
console.log();
console.log('HTML assets');
for(const info of buildTasks.html) {
console.log(`=> Building ${info.source}...`);
try {
let data = fs.readFileSync(path.join(srcDir, info.source));
data = utils.strtr(data, htmlVars);
if(!isDebugBuild)
data = await htmlminify(data, htmlMinifyOptions);
const name = utils.strtr(info.name, { hash: utils.shortHash(data) });
const pubName = path.join(info.target || '', name);
console.log(` Saving to ${pubName}...`);
const fullPath = path.join(pubDir, pubName);
const dirPath = path.dirname(fullPath);
if(!fs.existsSync(dirPath))
fs.mkdirSync(dirPath, { recursive: true });
fs.writeFileSync(fullPath, data);
htmlVars[info.source] = pubName;
} catch(err) {
console.error(err);
}
}
console.log();
console.log('Cleaning up old builds...');
assproc.housekeep(pubAssetsDir);
await assproc.process(env, tasks);
})();

507
package-lock.json generated
View file

@ -5,11 +5,7 @@
"packages": {
"": {
"dependencies": {
"@swc/core": "^1.5.24",
"autoprefixer": "^10.4.19",
"cssnano": "^6.1.2",
"html-minifier-terser": "^7.2.0",
"postcss": "^8.4.38"
"@railcomm/assproc": "^1.0.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
@ -70,15 +66,28 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@railcomm/assproc": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@railcomm/assproc/-/assproc-1.0.0.tgz",
"integrity": "sha512-u8BQht9di9yps7eVYYXbUaOeHCcbR8dKNLuc/KZ+E4uhPnFJ414WaIMH6h4QsMbDY7tAFehqFims7zM949nHGg==",
"license": "BSD-3-Clause",
"dependencies": {
"@swc/core": "^1.5.25",
"autoprefixer": "^10.4.19",
"cssnano": "^7.0.2",
"html-minifier-terser": "^7.2.0",
"postcss": "^8.4.38"
}
},
"node_modules/@swc/core": {
"version": "1.5.24",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.5.24.tgz",
"integrity": "sha512-Eph9zvO4xvqWZGVzTdtdEJ0Vqf0VIML/o/e4Qd2RLOqtfgnlRi7avmMu5C0oqciJ0tk+hqdUKVUZ4JPoPaiGvQ==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.6.3.tgz",
"integrity": "sha512-mZpei+LqE+AL+nwgERMQey9EJA9/yhHTN6nwbobH5GnSij/lhfTdGfAb1iumOrroqEcXbHUaK//7wOw7DjBGdA==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.7"
"@swc/types": "^0.1.8"
},
"engines": {
"node": ">=10"
@ -88,16 +97,16 @@
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.5.24",
"@swc/core-darwin-x64": "1.5.24",
"@swc/core-linux-arm-gnueabihf": "1.5.24",
"@swc/core-linux-arm64-gnu": "1.5.24",
"@swc/core-linux-arm64-musl": "1.5.24",
"@swc/core-linux-x64-gnu": "1.5.24",
"@swc/core-linux-x64-musl": "1.5.24",
"@swc/core-win32-arm64-msvc": "1.5.24",
"@swc/core-win32-ia32-msvc": "1.5.24",
"@swc/core-win32-x64-msvc": "1.5.24"
"@swc/core-darwin-arm64": "1.6.3",
"@swc/core-darwin-x64": "1.6.3",
"@swc/core-linux-arm-gnueabihf": "1.6.3",
"@swc/core-linux-arm64-gnu": "1.6.3",
"@swc/core-linux-arm64-musl": "1.6.3",
"@swc/core-linux-x64-gnu": "1.6.3",
"@swc/core-linux-x64-musl": "1.6.3",
"@swc/core-win32-arm64-msvc": "1.6.3",
"@swc/core-win32-ia32-msvc": "1.6.3",
"@swc/core-win32-x64-msvc": "1.6.3"
},
"peerDependencies": {
"@swc/helpers": "*"
@ -109,9 +118,9 @@
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.5.24",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.5.24.tgz",
"integrity": "sha512-M7oLOcC0sw+UTyAuL/9uyB9GeO4ZpaBbH76JSH6g1m0/yg7LYJZGRmplhDmwVSDAR5Fq4Sjoi1CksmmGkgihGA==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.3.tgz",
"integrity": "sha512-3r7cJf1BcE30iyF1rnOSKrEzIR+cqnyYSZvivrm62TZdXVsIjfXe1xulsKGxZgNeLY5erIu7ukvMvBvPhnQvqA==",
"cpu": [
"arm64"
],
@ -125,9 +134,9 @@
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.5.24",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.5.24.tgz",
"integrity": "sha512-MfcFjGGYognpSBSos2pYUNYJSmqEhuw5ceGr6qAdME7ddbjGXliza4W6FggsM+JnWwpqa31+e7/R+GetW4WkaQ==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.6.3.tgz",
"integrity": "sha512-8GLZ23IgVpF5xh2SbS5ZW/12/EEBuRU1hFOLB5rKERJU0y1RJ6YhDMf/FuOWhfHQcFM7TeedBwHIzaF+tdKKlw==",
"cpu": [
"x64"
],
@ -141,9 +150,9 @@
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.5.24",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.5.24.tgz",
"integrity": "sha512-amI2pwtcWV3E/m/nf+AQtn1LWDzKLZyjCmWd3ms7QjEueWYrY8cU1Y4Wp7wNNsxIoPOi8zek1Uj2wwFD/pttNQ==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.3.tgz",
"integrity": "sha512-VQ/bduX7WhLOlGbJLMG7UH0LBehjjx43R4yuk55rjjJLqpvX5fQzMsWhQdIZ5vsc+4ORzdgtEAlpumTv6bsD1A==",
"cpu": [
"arm"
],
@ -157,9 +166,9 @@
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.5.24",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.5.24.tgz",
"integrity": "sha512-sTSvmqMmgT1ynH/nP75Pc51s+iT4crZagHBiDOf5cq+kudUYjda9lWMs7xkXB/TUKFHPCRK0HGunl8bkwiIbuw==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.3.tgz",
"integrity": "sha512-jHIQ/PCwtdDBIF/BiC5DochswuCAIW/T5skJ+eDMbta7+QtEnZCXTZWpT5ORoEY/gtsE2fjpOA4TS6fBBvXqUw==",
"cpu": [
"arm64"
],
@ -173,9 +182,9 @@
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.5.24",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.5.24.tgz",
"integrity": "sha512-vd2/hfOBGbrX21FxsFdXCUaffjkHvlZkeE2UMRajdXifwv79jqOHIJg3jXG1F3ZrhCghCzirFts4tAZgcG8XWg==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.3.tgz",
"integrity": "sha512-gA6velEUD27Dwu0BlR9hCcFzkWq2YL2pDAU5qbgeuGhaMiUCBssfqTQB+2ctEnV+AZx+hSMJOHvtA+uFZjfRrw==",
"cpu": [
"arm64"
],
@ -189,9 +198,9 @@
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.5.24",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.5.24.tgz",
"integrity": "sha512-Zrdzi7NqzQxm2BvAG5KyOSBEggQ7ayrxh599AqqevJmsUXJ8o2nMiWQOBvgCGp7ye+Biz3pvZn1EnRzAp+TpUg==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.3.tgz",
"integrity": "sha512-fy4qoBDr5I8r+ZNCZxs/oZcmu4j/8mtSud6Ka102DaSxEjNg0vfIdo9ITsVIPsofhUTmDKjQsPB2O7YUlJAioQ==",
"cpu": [
"x64"
],
@ -205,9 +214,9 @@
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.5.24",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.5.24.tgz",
"integrity": "sha512-1F8z9NRi52jdZQCGc5sflwYSctL6omxiVmIFVp8TC9nngjQKc00TtX/JC2Eo2HwvgupkFVl5YQJidAck9YtmJw==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.3.tgz",
"integrity": "sha512-c/twcMbq/Gpq47G+b3kWgoaCujpXO11aRgJx6am+CprvP4uNeBHEpQkxD+DQmdWFHisZd0i9GB8NG3e7L9Rz9Q==",
"cpu": [
"x64"
],
@ -221,9 +230,9 @@
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.5.24",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.5.24.tgz",
"integrity": "sha512-cKpP7KvS6Xr0jFSTBXY53HZX/YfomK5EMQYpCVDOvfsZeYHN20sQSKXfpVLvA/q2igVt1zzy1XJcOhpJcgiKLg==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.3.tgz",
"integrity": "sha512-y6RxMtX45acReQmzkxcEfJscfBXce6QjuNgWQHHs9exA592BZzmolDUwgmAyjyvopz1lWX+KdymdZFKvuDSx4w==",
"cpu": [
"arm64"
],
@ -237,9 +246,9 @@
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.5.24",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.5.24.tgz",
"integrity": "sha512-IoPWfi0iwqjZuf7gE223+B97/ZwkKbu7qL5KzGP7g3hJrGSKAvv7eC5Y9r2iKKtLKyv5R/T6Ho0kFR/usi7rHw==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.3.tgz",
"integrity": "sha512-41h7z3xgukl1HDDwhquaeOPSP1OWeHl+mWKnJVmmwd3ui/oowUDCO856qa6JagBgPSnAGfyXwv6vthuXwyCcWA==",
"cpu": [
"ia32"
],
@ -253,9 +262,9 @@
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.5.24",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.5.24.tgz",
"integrity": "sha512-zHgF2k1uVJL8KIW+PnVz1To4a3Cz9THbh2z2lbehaF/gKHugH4c3djBozU4das1v35KOqf5jWIEviBLql2wDLQ==",
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.3.tgz",
"integrity": "sha512-//bnwo9b8Vp1ED06eXCHyGZ5xIpdkQgg2fuFDdtd1FITl7r5bdQh2ryRzPiKiGwgXZwZQitUshI4JeEX9IuW+Q==",
"cpu": [
"x64"
],
@ -275,9 +284,9 @@
"license": "Apache-2.0"
},
"node_modules/@swc/types": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.7.tgz",
"integrity": "sha512-scHWahbHF0eyj3JsxG9CFJgFdFNaVQCNAimBlT6PzS3n/HptxqREjsm4OH6AN3lYcffZYSPxXW8ua2BEHp0lJQ==",
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.8.tgz",
"integrity": "sha512-RNFA3+7OJFNYY78x0FYwi1Ow+iF1eF5WvmfY1nXPOEH4R2p/D4Cr1vzje7dNAI2aLFqpv8Wyz4oKSWqIZArpQA==",
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3"
@ -293,9 +302,9 @@
}
},
"node_modules/acorn": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz",
"integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@ -348,9 +357,9 @@
"license": "ISC"
},
"node_modules/browserslist": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
"integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
"version": "4.23.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz",
"integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==",
"funding": [
{
"type": "opencollective",
@ -367,10 +376,10 @@
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001587",
"electron-to-chromium": "^1.4.668",
"caniuse-lite": "^1.0.30001629",
"electron-to-chromium": "^1.4.796",
"node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
"update-browserslist-db": "^1.0.16"
},
"bin": {
"browserslist": "cli.js"
@ -408,9 +417,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001627",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001627.tgz",
"integrity": "sha512-4zgNiB8nTyV/tHhwZrFs88ryjls/lHiqFhrxCW4qSTeuRByBVnPYpDInchOIySWknznucaf31Z4KYqjfbrecVw==",
"version": "1.0.30001636",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz",
"integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==",
"funding": [
{
"type": "opencollective",
@ -520,16 +529,16 @@
}
},
"node_modules/cssnano": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz",
"integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==",
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.0.3.tgz",
"integrity": "sha512-lsekJctOTqdCn4cNrtrSwsuMR/fHC+oiVMHkp/OugBWtwjH8XJag1/OtGaYJGtz0un1fQcRy4ryfYTQsfh+KSQ==",
"license": "MIT",
"dependencies": {
"cssnano-preset-default": "^6.1.2",
"lilconfig": "^3.1.1"
"cssnano-preset-default": "^7.0.3",
"lilconfig": "^3.1.2"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"funding": {
"type": "opencollective",
@ -540,56 +549,56 @@
}
},
"node_modules/cssnano-preset-default": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz",
"integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==",
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.3.tgz",
"integrity": "sha512-dQ3Ba1p/oewICp/szF1XjFFgql8OlOBrI2YNBUUwhHQnJNoMOcQTa+Bi7jSJN8r/eM1egW0Ud1se/S7qlduWKA==",
"license": "MIT",
"dependencies": {
"browserslist": "^4.23.0",
"browserslist": "^4.23.1",
"css-declaration-sorter": "^7.2.0",
"cssnano-utils": "^4.0.2",
"postcss-calc": "^9.0.1",
"postcss-colormin": "^6.1.0",
"postcss-convert-values": "^6.1.0",
"postcss-discard-comments": "^6.0.2",
"postcss-discard-duplicates": "^6.0.3",
"postcss-discard-empty": "^6.0.3",
"postcss-discard-overridden": "^6.0.2",
"postcss-merge-longhand": "^6.0.5",
"postcss-merge-rules": "^6.1.1",
"postcss-minify-font-values": "^6.1.0",
"postcss-minify-gradients": "^6.0.3",
"postcss-minify-params": "^6.1.0",
"postcss-minify-selectors": "^6.0.4",
"postcss-normalize-charset": "^6.0.2",
"postcss-normalize-display-values": "^6.0.2",
"postcss-normalize-positions": "^6.0.2",
"postcss-normalize-repeat-style": "^6.0.2",
"postcss-normalize-string": "^6.0.2",
"postcss-normalize-timing-functions": "^6.0.2",
"postcss-normalize-unicode": "^6.1.0",
"postcss-normalize-url": "^6.0.2",
"postcss-normalize-whitespace": "^6.0.2",
"postcss-ordered-values": "^6.0.2",
"postcss-reduce-initial": "^6.1.0",
"postcss-reduce-transforms": "^6.0.2",
"postcss-svgo": "^6.0.3",
"postcss-unique-selectors": "^6.0.4"
"cssnano-utils": "^5.0.0",
"postcss-calc": "^10.0.0",
"postcss-colormin": "^7.0.1",
"postcss-convert-values": "^7.0.1",
"postcss-discard-comments": "^7.0.1",
"postcss-discard-duplicates": "^7.0.0",
"postcss-discard-empty": "^7.0.0",
"postcss-discard-overridden": "^7.0.0",
"postcss-merge-longhand": "^7.0.2",
"postcss-merge-rules": "^7.0.2",
"postcss-minify-font-values": "^7.0.0",
"postcss-minify-gradients": "^7.0.0",
"postcss-minify-params": "^7.0.1",
"postcss-minify-selectors": "^7.0.2",
"postcss-normalize-charset": "^7.0.0",
"postcss-normalize-display-values": "^7.0.0",
"postcss-normalize-positions": "^7.0.0",
"postcss-normalize-repeat-style": "^7.0.0",
"postcss-normalize-string": "^7.0.0",
"postcss-normalize-timing-functions": "^7.0.0",
"postcss-normalize-unicode": "^7.0.1",
"postcss-normalize-url": "^7.0.0",
"postcss-normalize-whitespace": "^7.0.0",
"postcss-ordered-values": "^7.0.1",
"postcss-reduce-initial": "^7.0.1",
"postcss-reduce-transforms": "^7.0.0",
"postcss-svgo": "^7.0.1",
"postcss-unique-selectors": "^7.0.1"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/cssnano-utils": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz",
"integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.0.tgz",
"integrity": "sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==",
"license": "MIT",
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
@ -694,9 +703,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.789",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.789.tgz",
"integrity": "sha512-0VbyiaXoT++Fi2vHGo2ThOeS6X3vgRCWrjPeO2FeIAWL6ItiSJ9BqlH8LfCXe3X1IdcG+S0iLoNaxQWhfZoGzQ==",
"version": "1.4.806",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.806.tgz",
"integrity": "sha512-nkoEX2QIB8kwCOtvtgwhXWy2IHVcOLQZu9Qo36uaGB835mdX/h8uLRlosL6QIhLVUnAiicXRW00PwaPZC74Nrg==",
"license": "ISC"
},
"node_modules/entities": {
@ -755,9 +764,9 @@
}
},
"node_modules/lilconfig": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz",
"integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz",
"integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==",
"license": "MIT",
"engines": {
"node": ">=14"
@ -903,376 +912,380 @@
}
},
"node_modules/postcss-calc": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz",
"integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==",
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.0.0.tgz",
"integrity": "sha512-OmjhudoNTP0QleZCwl1i6NeBwN+5MZbY5ersLZz69mjJiDVv/p57RjRuKDkHeDWr4T+S97wQfsqRTNoDHB2e3g==",
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "^6.0.11",
"postcss-selector-parser": "^6.0.16",
"postcss-value-parser": "^4.2.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12 || ^20.9 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.2.2"
"postcss": "^8.4.38"
}
},
"node_modules/postcss-colormin": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz",
"integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.1.tgz",
"integrity": "sha512-uszdT0dULt3FQs47G5UHCduYK+FnkLYlpu1HpWu061eGsKZ7setoG7kA+WC9NQLsOJf69D5TxGHgnAdRgylnFQ==",
"license": "MIT",
"dependencies": {
"browserslist": "^4.23.0",
"browserslist": "^4.23.1",
"caniuse-api": "^3.0.0",
"colord": "^2.9.3",
"postcss-value-parser": "^4.2.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-convert-values": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz",
"integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.1.tgz",
"integrity": "sha512-9x2ofb+hYPwHWMlWAzyWys2yMDZYGfkX9LodbaVTmLdlupmtH2AGvj8Up95wzzNPRDEzPIxQIkUaPJew3bT6xA==",
"license": "MIT",
"dependencies": {
"browserslist": "^4.23.0",
"browserslist": "^4.23.1",
"postcss-value-parser": "^4.2.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-discard-comments": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz",
"integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.1.tgz",
"integrity": "sha512-GVrQxUOhmle1W6jX2SvNLt4kmN+JYhV7mzI6BMnkAWR9DtVvg8e67rrV0NfdWhn7x1zxvzdWkMBPdBDCls+uwQ==",
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "^6.1.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-discard-duplicates": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz",
"integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.0.tgz",
"integrity": "sha512-bAnSuBop5LpAIUmmOSsuvtKAAKREB6BBIYStWUTGq8oG5q9fClDMMuY8i4UPI/cEcDx2TN+7PMnXYIId20UVDw==",
"license": "MIT",
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-discard-empty": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz",
"integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.0.tgz",
"integrity": "sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==",
"license": "MIT",
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-discard-overridden": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz",
"integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.0.tgz",
"integrity": "sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==",
"license": "MIT",
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-merge-longhand": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz",
"integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.2.tgz",
"integrity": "sha512-06vrW6ZWi9qeP7KMS9fsa9QW56+tIMW55KYqF7X3Ccn+NI2pIgPV6gFfvXTMQ05H90Y5DvnCDPZ2IuHa30PMUg==",
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.2.0",
"stylehacks": "^6.1.1"
"stylehacks": "^7.0.2"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-merge-rules": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz",
"integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.2.tgz",
"integrity": "sha512-VAR47UNvRsdrTHLe7TV1CeEtF9SJYR5ukIB9U4GZyZOptgtsS20xSxy+k5wMrI3udST6O1XuIn7cjQkg7sDAAw==",
"license": "MIT",
"dependencies": {
"browserslist": "^4.23.0",
"browserslist": "^4.23.1",
"caniuse-api": "^3.0.0",
"cssnano-utils": "^4.0.2",
"postcss-selector-parser": "^6.0.16"
"cssnano-utils": "^5.0.0",
"postcss-selector-parser": "^6.1.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-minify-font-values": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz",
"integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.0.tgz",
"integrity": "sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==",
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.2.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-minify-gradients": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz",
"integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.0.tgz",
"integrity": "sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==",
"license": "MIT",
"dependencies": {
"colord": "^2.9.3",
"cssnano-utils": "^4.0.2",
"cssnano-utils": "^5.0.0",
"postcss-value-parser": "^4.2.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-minify-params": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz",
"integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.1.tgz",
"integrity": "sha512-e+Xt8xErSRPgSRFxHeBCSxMiO8B8xng7lh8E0A5ep1VfwYhY8FXhu4Q3APMjgx9YDDbSp53IBGENrzygbUvgUQ==",
"license": "MIT",
"dependencies": {
"browserslist": "^4.23.0",
"cssnano-utils": "^4.0.2",
"browserslist": "^4.23.1",
"cssnano-utils": "^5.0.0",
"postcss-value-parser": "^4.2.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-minify-selectors": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz",
"integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.2.tgz",
"integrity": "sha512-dCzm04wqW1uqLmDZ41XYNBJfjgps3ZugDpogAmJXoCb5oCiTzIX4oPXXKxDpTvWOnKxQKR4EbV4ZawJBLcdXXA==",
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "^6.0.16"
"cssesc": "^3.0.0",
"postcss-selector-parser": "^6.1.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-normalize-charset": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz",
"integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.0.tgz",
"integrity": "sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==",
"license": "MIT",
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-normalize-display-values": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz",
"integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.0.tgz",
"integrity": "sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==",
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.2.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-normalize-positions": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz",
"integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.0.tgz",
"integrity": "sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==",
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.2.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-normalize-repeat-style": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz",
"integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.0.tgz",
"integrity": "sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==",
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.2.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-normalize-string": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz",
"integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.0.tgz",
"integrity": "sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==",
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.2.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-normalize-timing-functions": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz",
"integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.0.tgz",
"integrity": "sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==",
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.2.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-normalize-unicode": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz",
"integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.1.tgz",
"integrity": "sha512-PTPGdY9xAkTw+8ZZ71DUePb7M/Vtgkbbq+EoI33EuyQEzbKemEQMhe5QSr0VP5UfZlreANDPxSfcdSprENcbsg==",
"license": "MIT",
"dependencies": {
"browserslist": "^4.23.0",
"browserslist": "^4.23.1",
"postcss-value-parser": "^4.2.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-normalize-url": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz",
"integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.0.tgz",
"integrity": "sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==",
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.2.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-normalize-whitespace": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz",
"integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.0.tgz",
"integrity": "sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==",
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.2.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-ordered-values": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz",
"integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.1.tgz",
"integrity": "sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==",
"license": "MIT",
"dependencies": {
"cssnano-utils": "^4.0.2",
"cssnano-utils": "^5.0.0",
"postcss-value-parser": "^4.2.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-reduce-initial": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz",
"integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.1.tgz",
"integrity": "sha512-0JDUSV4bGB5FGM5g8MkS+rvqKukJZ7OTHw/lcKn7xPNqeaqJyQbUO8/dJpvyTpaVwPsd3Uc33+CfNzdVowp2WA==",
"license": "MIT",
"dependencies": {
"browserslist": "^4.23.0",
"browserslist": "^4.23.1",
"caniuse-api": "^3.0.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-reduce-transforms": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz",
"integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.0.tgz",
"integrity": "sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==",
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.2.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
@ -1292,31 +1305,31 @@
}
},
"node_modules/postcss-svgo": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz",
"integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.0.1.tgz",
"integrity": "sha512-0WBUlSL4lhD9rA5k1e5D8EN5wCEyZD6HJk0jIvRxl+FDVOMlJ7DePHYWGGVc5QRqrJ3/06FTXM0bxjmJpmTPSA==",
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.2.0",
"svgo": "^3.2.0"
"svgo": "^3.3.2"
},
"engines": {
"node": "^14 || ^16 || >= 18"
"node": "^18.12.0 || ^20.9.0 || >= 18"
},
"peerDependencies": {
"postcss": "^8.4.31"
}
},
"node_modules/postcss-unique-selectors": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz",
"integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.1.tgz",
"integrity": "sha512-MH7QE/eKUftTB5ta40xcHLl7hkZjgDFydpfTK+QWXeHxghVt3VoPqYL5/G+zYZPPIs+8GuqFXSTgxBSoB1RZtQ==",
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "^6.0.16"
"postcss-selector-parser": "^6.1.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
@ -1366,16 +1379,16 @@
}
},
"node_modules/stylehacks": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz",
"integrity": "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.2.tgz",
"integrity": "sha512-HdkWZS9b4gbgYTdMg4gJLmm7biAUug1qTqXjS+u8X+/pUd+9Px1E+520GnOW3rST9MNsVOVpsJG+mPHNosxjOQ==",
"license": "MIT",
"dependencies": {
"browserslist": "^4.23.0",
"postcss-selector-parser": "^6.0.16"
"browserslist": "^4.23.1",
"postcss-selector-parser": "^6.1.0"
},
"engines": {
"node": "^14 || ^16 || >=18.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0"
},
"peerDependencies": {
"postcss": "^8.4.31"
@ -1416,9 +1429,9 @@
}
},
"node_modules/terser": {
"version": "5.31.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz",
"integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==",
"version": "5.31.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.1.tgz",
"integrity": "sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==",
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",

View file

@ -1,9 +1,5 @@
{
"dependencies": {
"@swc/core": "^1.5.24",
"autoprefixer": "^10.4.19",
"cssnano": "^6.1.2",
"html-minifier-terser": "^7.2.0",
"postcss": "^8.4.38"
"@railcomm/assproc": "^1.0.0"
}
}

View file

@ -1,4 +1,4 @@
#buildvars
#vars self build
(function() {
var isCompatible = (function() {

View file

@ -6,7 +6,7 @@
flex-direction: column;
}
@media (max-width:768px) {
@media (max-width: 800px) {
.main {
margin-right: 44px;
}

View file

@ -3,6 +3,10 @@ select {
accent-color: currentcolor;
}
.setting__category {
overflow: hidden;
}
.setting__category-title {
font-size: 2em;
line-height: 1.1em;
@ -11,6 +15,7 @@ select {
position: sticky;
top: 0;
z-index: 1;
cursor: pointer;
}
.setting__container {
@ -60,3 +65,13 @@ select {
.setting__container--button .setting__input[disabled] {
opacity: .6;
}
.settings-warning {
font-size: .9em;
line-height: 1.4em;
margin: 5px;
padding: 5px;
background-color: darkred;
border: 2px solid red;
border-radius: 5px;
}

View file

@ -6,7 +6,7 @@
box-shadow: var(--theme-size-sidebar-box-shadow-x, 0) var(--theme-size-sidebar-box-shadow-y, 0) var(--theme-size-sidebar-box-shadow-blur, 0) var(--theme-size-sidebar-box-shadow-spread, 0) var(--theme-colour-sidebar-box-shadow, #000);
}
@media (max-width:768px) {
@media(max-width: 800px) {
.sidebar {
position: fixed;
top: max(4px, env(safe-area-inset-top));
@ -15,19 +15,8 @@
}
}
.sidebar--closed {
min-width: 40px
}
.sidebar--closed .sidebar__menus {
display: none
}
.sidebar__selector {
order: 1;
font: 2em/40px "Font Awesome 5 Free";
font-weight: 900;
text-align: center;
display: flex;
flex-direction: column;
overflow: auto;
@ -43,9 +32,14 @@
}
.sidebar__selector-mode {
color: inherit;
height: 40px;
width: 40px;
cursor: pointer;
background: transparent;
border: 0;
border-radius: 0;
display: block;
transition: background .1s, text-shadow .1s;
}
@ -61,74 +55,10 @@
background-color: var(--theme-colour-sidebar-selector-background-active, #ddd) !important;
}
.sidebar__selector-mode--hidden {
display: none;
}
.sidebar__selector-mode--attention {
text-shadow: var(--theme-size-sidebar-selector-attention-shadow-x, 0) var(--theme-size-sidebar-selector-attention-shadow-y, 0) var(--theme-size-sidebar-selector-attention-shadow-blur, 0) var(--theme-colour-sidebar-selector-attention-shadow, #888);
}
.sidebar__selector-mode--users:before {
content: "\f0c0"
}
.sidebar__selector-mode--channels:before {
content: "\f292"
}
.sidebar__selector-mode--settings:before {
content: "\f013"
}
.sidebar__selector-mode--uploads:before {
content: "\f093"
}
.sidebar__selector-mode--audio:before {
content: "\f028"
}
.sidebar__selector-mode--audio-off:before {
content: "\f026"
}
.sidebar__selector-mode--scroll:before {
content: "\f04e"
}
.sidebar__selector-mode--scroll-off:before {
content: "\f04c"
}
.sidebar__selector-mode--unembed:before {
font-weight: 400;
content: "\f146"
}
.sidebar__selector-mode--clear {
background-image: url('//static.flash.moe/images/bomb.png');
}
/*.sidebar__selector-mode--clear:before {
content: "\f2ed"
}*/
.sidebar__selector-mode--ping {
padding: 4px;
}
.sidebar__selector-mode--ping .ping {
width: 32px;
height: 32px;
}
.sidebar__selector-mode--menu-toggle-opened:before {
content: "\f152"
}
.sidebar__selector-mode--menu-toggle-closed:before {
content: "\f191"
}
.sidebar__menus {
flex-grow: 1;
flex-shrink: 1;
@ -137,14 +67,6 @@
scrollbar-width: thin;
}
.sidebar__menu {
display: none;
}
.sidebar__menu--active {
display: inline;
}
.sidebar__user,
.sidebar__channel {
display: flex;
@ -177,10 +99,6 @@
overflow: hidden;
}
.sidebar__user-options--hidden {
display: none
}
.sidebar__user-option {
padding: 2px 5px;
cursor: pointer;
@ -206,3 +124,12 @@
float: right;
line-height: 1.4em;
}
.sidebar-gutter-font-icon {
font-size: 1.5rem;
line-height: 40px;
}
.sidebar-gutter-element {
margin: 4px;
}

View file

@ -1,104 +0,0 @@
const MamiChannelInfo = function(name, hasPassword = false, isTemporary = true, isUserChannel = false) {
if(typeof name !== 'string')
throw 'name must be a string';
if(typeof hasPassword !== 'boolean')
throw 'hasPassword must be a boolean';
if(typeof isTemporary !== 'boolean')
throw 'isTemporary must be a boolean';
if(typeof isUserChannel !== 'boolean')
throw 'isUserChannel must be a boolean';
return {
get name() { return name; },
set name(value) {
if(typeof value !== 'string')
throw 'value must be a string';
name = value;
},
get hasPassword() { return hasPassword; },
set hasPassword(value) {
if(typeof value !== 'boolean')
throw 'value must be a boolean';
hasPassword = value;
},
get isTemporary() { return isTemporary; },
set isTemporary(value) {
if(typeof value !== 'boolean')
throw 'value must be a boolean';
isTemporary = value;
},
get isUserChannel() { return isUserChannel; },
};
};
Umi.Channels = (function() {
const chans = new Map;
let currentName = null;
const onAdd = [];
const onRemove = [];
const onClear = [];
const onUpdate = [];
const onSwitch = [];
return {
OnAdd: onAdd,
OnRemove: onRemove,
OnClear: onClear,
OnUpdate: onUpdate,
OnSwitch: onSwitch,
Add: function(channel) {
const channelName = channel.name;
if(!chans.has(channelName)) {
chans.set(channelName, channel);
for(const i in onAdd)
onAdd[i](channel);
}
},
Remove: function(channel) {
const channelName = channel.name;
if(chans.has(channelName)) {
chans.delete(channelName);
for(const i in onRemove)
onRemove[i](channel);
}
},
Clear: function() {
chans.clear();
for(const i in onClear)
onClear[i]();
},
All: function() {
return Array.from(chans.values());
},
Get: function(channelName) {
channelName = channelName.toString();
if(chans.has(channelName))
return chans.get(channelName);
return null;
},
Update: function(channelName, channel) {
channelName = channelName.toString();
chans.set(channelName, channel);
for(const i in onUpdate)
onUpdate[i](name, channel);
},
Current: function() {
return currentName;
},
Switch: function(channelName) {
const old = currentName;
currentName = channelName;
for(const i in onSwitch)
onSwitch[i](old, channelName);
},
};
})();

View file

@ -106,9 +106,9 @@ const MamiMessageBoxDialog = function(info) {
if(info.body !== undefined) {
if(Array.isArray(info.body))
for(const line of info.body)
body.appendChild(<p class="msgbox-dialog-line">{line}</p>);
body.appendChild(<p class="msgbox-dialog-line">{line.toString()}</p>);
else
body.appendChild(<p class="msgbox-dialog-line">{info.body}</p>);
body.appendChild(<p class="msgbox-dialog-line">{info.body.toString()}</p>);
}
const buttons = <div class="msgbox-dialog-buttons"/>;

View file

@ -1,6 +1,7 @@
window.Umi = { UI: {} };
#include animate.js
#include awaitable.js
#include common.js
#include compat.js
#include conman.js
@ -10,6 +11,7 @@ window.Umi = { UI: {} };
#include mobile.js
#include mszauth.js
#include txtrigs.js
#include users.js
#include utility.js
#include weeb.js
#include worker.js
@ -19,22 +21,32 @@ window.Umi = { UI: {} };
#include eeprom/eeprom.js
#include settings/backup.js
#include settings/settings.js
#include sidebar/act-clear-backlog.jsx
#include sidebar/act-collapse-all.jsx
#include sidebar/act-ping.jsx
#include sidebar/act-scroll.jsx
#include sidebar/act-sound.jsx
#include sidebar/act-toggle.jsx
#include sidebar/pan-channels.jsx
#include sidebar/pan-settings.jsx
#include sidebar/pan-uploads.jsx
#include sidebar/pan-users.jsx
#include sidebar/sidebar.jsx
#include sockchat/client.js
#include sockchat/handlers.js
#include sound/context.js
#include sound/osukeys.js
#include sound/sndtest.jsx
#include ui/baka.jsx
#include ui/chat-layout.js
#include ui/emotes.js
#include ui/hooks.js
#include ui/input-menus.js
#include ui/loading-overlay.jsx
#include ui/markup.js
#include ui/menus.js
#include ui/messages.jsx
#include ui/ping.jsx
#include ui/settings.jsx
#include ui/toggles.js
#include ui/uploads.js
#include ui/view.js
#include ui/youare.jsx
(async () => {
const eventTarget = new MamiEventTargetWindow;
@ -88,7 +100,6 @@ window.Umi = { UI: {} };
settings.define('compactView').default(false).create();
settings.define('autoScroll').default(true).create();
settings.define('closeTabConfirm').default(false).create();
settings.define('showChannelList').default(false).create();
settings.define('autoCloseUserContext').default(true).create();
settings.define('preventOverflow').default(false).create();
settings.define('expandTextBox').default(false).create();
@ -230,8 +241,41 @@ window.Umi = { UI: {} };
ctx.textTriggers = new MamiTextTriggers;
// should be dynamic when possible
const layout = new Umi.UI.ChatLayout;
const sidebar = new MamiSidebar;
MamiCompat('Umi.UI.Menus.Add', {
value: (baseId, title) => {
sidebar.createPanel({
name: `compat:${baseId}`,
text: title,
createdButton: button => {
button.element.id = `umi-menu-icons-${baseId}`;
button.element.append($e({
attrs: { className: `sidebar__selector-mode--${baseId}` },
}));
},
element: $e({
attrs: { 'class': `sidebar__menu--${baseId}`, id: `umi-menus-${baseId}` }
}),
});
}
});
MamiCompat('Umi.UI.Menus.Get', {
value: (baseId, icon) => {
const info = sidebar[icon ? 'getButton' : 'getPanel'](`compat:${baseId}`);
if(info === undefined)
return null;
return icon ? info.element : info.element.firstElementChild;
},
});
MamiCompat('Umi.UI.Menus.Attention', {
value: baseId => {
sidebar.getButton(`compat:${baseId}`)?.attention();
},
});
const layout = new Umi.UI.ChatLayout(sidebar);
await ctx.views.unshift(layout);
Umi.UI.View.AccentReload();
@ -299,115 +343,294 @@ window.Umi = { UI: {} };
MamiCompat('Umi.Parser.SockChatBBcode.EmbedStub', { value: () => {} }); // intentionally a no-op
MamiCompat('Umi.UI.View.SetText', { value: text => console.log(`Umi.UI.View.SetText(text: ${text})`) });
MamiCompat('Umi.UI.Menus.Add', { value: (baseId, title, initiallyHidden) => console.log(`Umi.UI.Menus.Add(baseId: ${baseId}, title: ${title}, initiallyHidden: ${initiallyHidden})`) });
MamiCompat('Umi.UI.Menus.Get', { value: (baseId, icon) => console.log(`Umi.UI.Menus.Get(baseId: ${baseId}, icon: ${icon})`) });
Umi.UI.Menus.Add('users', 'Users');
Umi.UI.Menus.Add('channels', 'Channels', !settings.get('showChannelList'));
Umi.UI.Menus.Add('settings', 'Settings');
const sbUsers = new MamiSidebarPanelUsers;
sidebar.createPanel(sbUsers);
let sidebarAnimation = null;
Umi.UI.Settings.Init();
Umi.UI.Toggles.Add('menu-toggle', {
'click': function() {
const sidebar = $c('sidebar')[0];
const toggle = Umi.UI.Toggles.Get('menu-toggle');
const isClosed = toggle.classList.contains('sidebar__selector-mode--menu-toggle-closed');
if(sidebarAnimation !== null) {
sidebarAnimation.cancel();
sidebarAnimation = null;
}
toggle.classList.toggle('sidebar__selector-mode--menu-toggle-opened', isClosed);
toggle.classList.toggle('sidebar__selector-mode--menu-toggle-closed', !isClosed);
let update;
if(isClosed)
update = function(t) {
sidebar.style.width = (40 + (220 * t)).toString() + 'px';
};
else
update = function(t) {
sidebar.style.width = (260 - (220 * t)).toString() + 'px';
};
sidebarAnimation = MamiAnimate({
duration: 500,
easing: 'outExpo',
update: update,
});
}
}, 'Toggle Sidebar');
Umi.UI.Toggles.Get('menu-toggle').classList.add('sidebar__selector-mode--menu-toggle-opened');
Umi.UI.Toggles.Add('scroll', {
'click': function() {
settings.toggle('autoScroll');
}
}, 'Autoscroll');
settings.watch('autoScroll', ev => {
Umi.UI.Toggles.Get('scroll').classList.toggle('sidebar__selector-mode--scroll-off', !ev.detail.value);
sbUsers.addOption({
name: 'profile',
text: 'View profile',
onclick: entry => window.open(futami.get('profile').replace('{user:id}', entry.id), '_blank'),
});
if(window.innerWidth < 768)
Umi.UI.Toggles.Get('menu-toggle').click();
Umi.UI.Toggles.Add('audio', {
'click': function() {
settings.toggle('soundEnable');
}
}, 'Sounds');
settings.watch('soundEnable', ev => {
Umi.UI.Toggles.Get('audio').classList.toggle('sidebar__selector-mode--audio-off', !ev.detail.value);
sbUsers.addOption({
name: 'action',
text: 'Describe action',
condition: entry => Umi.User.getCurrentUser()?.id === entry.id,
onclick: entry => { Umi.UI.View.SetText('/me '); },
});
Umi.UI.Toggles.Add('unembed', {
'click': function() {
const buttons = $qa('[data-embed="1"]');
for(const button of buttons)
button.click();
}
}, 'Collapse any expanded elements');
Umi.UI.Toggles.Add('clear', {
'click': function() {
ctx.msgbox.show({ body: 'ARE YOU SURE ABOUT THAT???', yes: true, no: true }).then(() => {
const explode = $e({
tag: 'img',
attrs: {
src: '//static.flash.moe/images/explode.gif',
alt: '',
style: {
position: 'absolute',
zIndex: 9001,
bottom: 0,
right: 0,
pointerEvents: 'none',
},
onLoad: function() {
setTimeout(function() {
$r(explode);
}, 1700);
soundCtx.library.play('misc:explode');
},
},
sbUsers.addOption({
name: 'nick',
text: 'Set nickname',
condition: entry => Umi.User.getCurrentUser()?.id === entry.id && Umi.User.getCurrentUser().perms.canSetNick,
onclick: entry => { Umi.UI.View.SetText('/nick '); },
});
sbUsers.addOption({
name: 'bans',
text: 'View bans',
condition: entry => Umi.User.getCurrentUser()?.id === entry.id && Umi.User.getCurrentUser().perms.canKick,
onclick: entry => { Umi.Server.sendMessage('/bans'); },
});
sbUsers.addOption({
name: 'kfe',
text: 'Kick Fucking Everyone',
condition: entry => Umi.User.getCurrentUser()?.id === entry.id && Umi.User.getCurrentUser().perms.canKick,
onclick: async entry => {
try {
await ctx.msgbox.show({
body: ['You are about to detonate the fucking bomb.', 'Are you sure?'],
yes: { text: 'FUCK IT, WE BALL' },
no: { text: 'nah' },
});
document.body.appendChild(explode);
Umi.UI.Messages.Clear(settings.get('explosionRadius'));
}).catch(() => {});
}
}, 'Clear Logs');
const names = sbUsers.getAllUserNames();
for(const name of names)
if(Umi.User.getCurrentUser()?.name !== name)
// this shouldn't call it like this but will have to leave it for now
Umi.Server.sendMessage(`/kick ${name}`);
} catch(ex) {}
},
});
sbUsers.addOption({
name: 'dm',
text: 'Send direct message',
condition: entry => Umi.User.getCurrentUser()?.id !== entry.id,
onclick: entry => { Umi.UI.View.SetText(`/msg ${entry.name} `); },
});
sbUsers.addOption({
name: 'kick',
text: 'Kick from chat',
condition: entry => Umi.User.hasCurrentUser() && Umi.User.getCurrentUser().id !== entry.id && Umi.User.getCurrentUser().perms.canKick,
onclick: entry => { Umi.UI.View.SetText(`/kick ${entry.name} `); },
});
sbUsers.addOption({
name: 'ipaddr',
text: 'View IP address',
condition: entry => Umi.User.hasCurrentUser() && Umi.User.getCurrentUser().id !== entry.id && Umi.User.getCurrentUser().perms.canKick,
onclick: entry => { Umi.Server.sendMessage(`/ip ${entry.name}`); },
});
const sbChannels = new MamiSidebarPanelChannels;
sidebar.createPanel(sbChannels);
const sbSettings = new MamiSidebarPanelSettings(settings);
sidebar.createPanel(sbSettings);
sbSettings.category(category => {
category.header('Interface');
category.setting('style').title('Style').type('select').options(() => Umi.UI.View.AccentColours).done();
category.setting('compactView').title('Use compact view').done();
category.setting('autoScroll').title('Enable auto scroll').done();
category.setting('closeTabConfirm').title('Confirm tab close').done();
category.setting('autoCloseUserContext').title('Auto-close user menus').done();
});
sbSettings.category(category => {
category.header('Text');
category.setting('preventOverflow').title('Prevent overflow').done();
category.setting('expandTextBox').title('Grow input box while typing').done();
category.setting('eepromAutoInsert').title('Auto-insert uploads').done();
category.setting('autoEmbedV1').title('Auto-embed media').done();
category.setting('autoEmbedPlay').title('Auto-play embedded media').done();
});
sbSettings.category(category => {
category.header('Notifications');
category.setting('flashTitle').title('Strobe title on new message').done();
category.setting('enableNotifications').title('Show notifications').done();
category.setting('notificationShowMessage').title('Show contents of message').done();
category.setting('notificationTriggers').title('Triggers').done();
});
sbSettings.category(category => {
category.header('Sound');
category.setting('soundEnable').title('Enable sound').done();
category.setting('soundPack').title('Sound pack').type('select').options(() => {
const options = { '': 'Default' };
for(const name of soundCtx.packs.names())
options[name] = soundCtx.packs.info(name).getTitle();
return options;
}).done();
category.setting('soundVolume').title('Sound volume').type('range').done();
category.setting('soundEnableJoin').title('Play join sound').done();
category.setting('soundEnableLeave').title('Play leave sound').done();
category.setting('soundEnableError').title('Play error sound').done();
category.setting('soundEnableServer').title('Play server message sound').done();
category.setting('soundEnableIncoming').title('Play receive message sound').done();
category.setting('onlySoundOnMention').title('Only plays sounds when you are mentioned').done();
category.setting('soundEnableOutgoing').title('Play send message sound').done();
category.setting('soundEnablePrivate').title('Play private message sound').done();
category.setting('soundEnableForceLeave').title('Play kick sound').done();
category.setting('minecraft').title('Minecraft').type('select').options(() => {
return {
'no': 'No Minecraft',
'yes': 'Yes Minecraft',
'old': 'Old Minecraft',
};
}).done();
category.setting('windowsLiveMessenger').title('Windows Live Messenger').done();
category.setting('seinfeld').title('Seinfeld').done();
});
sbSettings.category(category => {
category.header('Misc');
category.setting('onlyConnectWhenVisible2').title('Only connect when the tab is in the foreground').confirm([
'Please only disable this setting if you are using a desktop or laptop computer, this should always remain on on a phone, tablet or other device of that sort.',
'Are you sure you want to change this setting? Ignoring this warning may carry consequences.',
]).done();
category.setting('playJokeSounds').title('Run joke triggers').done();
category.setting('weeaboo').title('Weeaboo').done();
category.setting('motivationalImages').title('Make images motivational').done();
category.setting('motivationalVideos').title('Make videos motivational').done();
category.setting('osuKeysV2').title('osu! keyboard sounds').type('select').options(() => {
return {
'no': 'Off',
'yes': 'On',
'rng': 'On, random pitch',
};
}).done();
category.setting('explosionRadius').title('Messages to keep on clear').done();
});
sbSettings.category(category => {
category.header('Actions');
category.button('Open compatibility client', () => { window.open(window.AMI_URL, '_blank', 'noopener'); });
category.button('Manual reconnect', async button => {
const textOrig = button.textContent;
let lock = 10;
button.disabled = true;
button.textContent = 'Reconnecting...';
try {
await ctx.conMan.start();
while(--lock > 0) {
button.textContent = textOrig + ` (${lock}s)`;
await MamiSleep(1000);
}
} finally {
button.textContent = textOrig;
button.disabled = false;
}
});
category.button('Reload emoticons', async button => {
const textOrig = button.textContent;
button.disabled = true;
button.textContent = 'Reloading emoticons...';
try {
const emotes = await futami.getJson('emotes', true);
MamiEmotes.clear();
MamiEmotes.loadLegacy(emotes);
} finally {
Umi.UI.Emoticons.Init();
button.textContent = textOrig;
button.disabled = false;
}
});
category.button('Reload sound library', async button => {
const textOrig = button.textContent;
button.disabled = true;
button.textContent = 'Reloading sound library...';
try {
const sounds = await futami.getJson('sounds2');
if(Array.isArray(sounds.library))
soundCtx.library.register(sounds.library, true);
if(Array.isArray(sounds.packs)) {
soundCtx.packs.register(sounds.packs, true);
settings.touch('soundPack', true);
}
} finally {
button.textContent = textOrig;
button.disabled = false;
}
});
category.button('Reload joke triggers', async button => {
const textOrig = button.textContent;
button.disabled = true;
button.textContent = 'Reloading joke triggers...';
try {
const triggers = await futami.getJson('texttriggers', true);
ctx.textTriggers.clearTriggers();
ctx.textTriggers.addTriggers(triggers)
} finally {
button.textContent = textOrig;
button.disabled = false;
}
});
});
sbSettings.category(category => {
category.header('Settings');
category.button('Import settings', () => {
(new MamiSettingsBackup(settings)).importUpload(document.body);
}, ['Your current settings will be replaced with the ones in the export.', 'Are you sure you want to continue?']);
category.button('Export settings', () => {
const user = Umi.User.getCurrentUser();
let fileName;
if(user !== null)
fileName = `${user.name}'s settings.mami`;
(new MamiSettingsBackup(settings)).exportDownload(document.body, fileName);
});
category.button('Reset settings', () => {
settings.clear();
}, ['This will reset all your settings to their defaults values.', 'Are you sure you want to do this?']);
});
sbSettings.category(category => {
category.header('Debug');
category.collapse();
category.warning("Only touch these settings if you're ABSOLUTELY sure you know what you're doing, you're on your own if you break something.");
category.setting('dumpPackets').title('Dump packets to console').done();
category.setting('dumpEvents').title('Dump events to console').done();
category.setting('marqueeAllNames').title('Apply marquee on everyone').done();
category.setting('tmpDisableOldThemeSys').title('Disable old theme system').done();
category.setting('dbgAnimDurationMulti').title('Animation multiplier').type('range').done();
category.button('Test kick/ban notice', async button => {
button.disabled = true;
await ctx.views.push(new MamiForceDisconnectNotice({ perma: true, type: 'ban' }));
await MamiSleep(5000);
await ctx.views.pop();
button.disabled = false;
});
category.button('You are an idiot!', async button => {
button.disabled = true;
await ctx.views.push(new MamiYouAreAnIdiot(soundCtx.library, ctx.views));
button.disabled = false;
});
category.button('Sound test', async (button, ev) => {
button.disabled = true;
await ctx.views.push(new MamiSoundTest(
settings,
soundCtx.audio,
soundCtx.manager,
soundCtx.library,
[ev.clientX, ev.clientY],
));
button.disabled = false;
});
category.button('Reset audio context', () => {
soundCtx.reset();
});
});
const sbUploads = new MamiSidebarPanelUploads;
sidebar.createPanel(sbUploads);
const sbActToggle = new MamiSidebarActionToggle(sidebar);
sidebar.createAction(sbActToggle);
if(window.innerWidth < 800)
sbActToggle.click();
sidebar.createAction(new MamiSidebarActionScroll(settings));
sidebar.createAction(new MamiSidebarActionSound(settings));
sidebar.createAction(new MamiSidebarActionCollapseAll);
sidebar.createAction(new MamiSidebarActionClearBacklog(settings, soundCtx.library, ctx.msgbox));
const pingIndicator = new MamiPingIndicator;
const pingToggle = Umi.UI.Toggles.Add('ping', {
click: () => { ctx.msgbox.show({ body: `Your current ping is ${pingToggle.title}` }); },
}, 'Ready~');
pingToggle.appendChild(pingIndicator.getElement());
const sbActPing = new MamiSidebarActionPing(pingIndicator, ctx.msgbox);
sidebar.createAction(sbActPing);
Umi.UI.InputMenus.Add('markup', 'BB Code');
Umi.UI.InputMenus.Add('emotes', 'Emoticons');
@ -415,60 +638,80 @@ window.Umi = { UI: {} };
let doUpload;
ctx.eeprom = new MamiEEPROM(futami.get('eeprom2'), MamiMisuzuAuth.getLine);
ctx.eeprom.init()
.catch(ex => {
console.log('Failed to initialise EEPROM.', ex);
})
.catch(ex => { console.error('Failed to initialise EEPROM.', ex); })
.then(() => {
Umi.UI.Menus.Add('uploads', 'Upload History', !FUTAMI_DEBUG);
sbUploads.addOption({
name: 'view',
text: 'View upload',
condition: entry => entry.uploadInfo !== undefined,
onclick: entry => window.open(entry.uploadInfo.url),
});
sbUploads.addOption({
name: 'insert',
text: 'Insert into message',
condition: entry => entry.uploadInfo !== undefined,
onclick: entry => {
const upload = entry.uploadInfo;
let text;
if(upload.isImage()) {
text = `[img]${upload.url}[/img]`;
} else if(upload.isAudio()) {
text = `[audio]${upload.url}[/audio]`;
} else if(upload.isVideo()) {
text = `[video]${upload.url}[/video]`;
} else
text = location.protocol + upload.url;
Umi.UI.Markup.InsertRaw(text, '')
},
});
sbUploads.addOption({
name: 'delete',
text: 'Delete upload',
condition: entry => entry.uploadInfo !== undefined,
onclick: async entry => {
try {
await ctx.eeprom.delete(entry.uploadInfo);
sbUploads.deleteEntry(entry);
} catch(ex) {
console.error(ex);
await ctx.msgbox.show({ body: ['An error occurred while trying to delete an uploaded file:', ex] });
}
},
});
doUpload = async file => {
const uploadEntry = Umi.UI.Uploads.create(file.name);
const uploadTask = ctx.eeprom.create(file);
const entry = sbUploads.createEntry(file);
uploadTask.onProgress(prog => uploadEntry.setProgress(prog.progress));
uploadEntry.addOption('Cancel', () => uploadTask.abort());
const task = ctx.eeprom.create(file);
task.onProgress(prog => {
entry.progress = prog.progress;
});
entry.addOption({
name: 'cancel',
text: 'Cancel upload',
onclick: () => { task.abort(); },
});
try {
const fileInfo = await uploadTask.start();
const fileInfo = await task.start();
uploadEntry.hideOptions();
uploadEntry.clearOptions();
uploadEntry.removeProgress();
uploadEntry.addOption('Open', fileInfo.url);
uploadEntry.addOption('Insert', () => Umi.UI.Markup.InsertRaw(insertText, ''));
uploadEntry.addOption('Delete', () => {
ctx.eeprom.delete(fileInfo)
.then(() => uploadEntry.remove())
.catch(ex => {
console.error(ex);
ctx.msgbox.show({ body: ['An error occurred while trying to delete an uploaded file:', ex] });
});
});
let insertText;
if(fileInfo.isImage()) {
insertText = `[img]${fileInfo.url}[/img]`;
uploadEntry.setThumbnail(fileInfo.thumb);
} else if(fileInfo.isAudio()) {
insertText = `[audio]${fileInfo.url}[/audio]`;
uploadEntry.setThumbnail(fileInfo.thumb);
} else if(fileInfo.isVideo()) {
insertText = `[video]${fileInfo.url}[/video]`;
uploadEntry.setThumbnail(fileInfo.thumb);
} else
insertText = location.protocol + fileInfo.url;
entry.optionsVisible = false;
entry.uploadInfo = fileInfo;
entry.removeOption('cancel');
entry.nukeProgress();
sbUploads.reloadOptionsFor(entry);
if(settings.get('eepromAutoInsert'))
Umi.UI.Markup.InsertRaw(insertText, '');
entry.clickOption('insert');
} catch(ex) {
if(!ex.aborted) {
console.error(ex);
ctx.msgbox.show({ body: ['An error occurred while trying to upload a file:', ex] });
}
uploadEntry.remove();
sbUploads.deleteEntry(entry);
}
};
@ -562,13 +805,23 @@ window.Umi = { UI: {} };
const conMan = new MamiConnectionManager(sockChat, settings, futami.get('servers'), ctx.events.scopeTo('conn'));
ctx.conMan = conMan;
sbChannels.onClickEntry = async info => {
await sockChat.client.switchChannel(info);
// hack for DM channels
if(info.isUserChannel) {
sbChannels.setActiveEntry(info.name);
Umi.UI.Messages.SwitchChannel(info);
}
};
let sockChatRestarting;
const sockChatReconnect = () => {
if(conMan.isActive)
return;
pingToggle.title = 'Reconnecting...';
sbActPing.pingMs = -1;
pingIndicator.setStrength(-1);
const reconManAttempt = ev => {
@ -593,7 +846,10 @@ window.Umi = { UI: {} };
conMan.start();
};
const sockChatHandlers = new MamiSockChatHandlers(ctx, sockChat, setLoadingOverlay, sockChatReconnect, pingIndicator, pingToggle);
const sockChatHandlers = new MamiSockChatHandlers(
ctx, sockChat, setLoadingOverlay, sockChatReconnect, pingIndicator,
sbActPing, sbChannels, sbUsers
);
settings.watch('dumpEvents', ev => sockChatHandlers.setDumpEvents(ev.detail.value));
settings.watch('dumpPackets', ev => sockChat.setDumpPackets(ev.detail.value));
sockChatHandlers.register();

View file

@ -0,0 +1,30 @@
#include awaitable.js
#include ui/messages.jsx
const MamiSidebarActionClearBacklog = function(settings, sndLib, msgBox) {
return {
get name() { return 'act:clear-backlog'; },
get text() { return 'Clear backlog'; },
createdButton: button => {
button.element.append(<img class="sidebar-gutter-image" src="//static.flash.moe/images/bomb.png" alt="💣"/>);
},
onclick: async () => {
try {
await msgBox.show({ body: 'ARE YOU SURE ABOUT THAT???', yes: true, no: true });
const explode = <img src="//static.flash.moe/images/explode.gif" alt="BOOM!!!!!!!!!!!!!!!!!!"
style="position: absolute; z-index: 9001; bottom: 0; right: 0; pointer-events: none" />;
document.body.appendChild(explode);
sndLib.play('misc:explode');
Umi.UI.Messages.Clear(settings.get('explosionRadius'));
await MamiSleep(1700);
explode.remove();
} catch(ex) {}
},
};
};

View file

@ -0,0 +1,20 @@
#include utility.js
const MamiSidebarActionCollapseAll = function() {
// this should take a reference to the message container rather than just using $qa
return {
get name() { return 'act:collapse-all'; },
get text() { return 'Collapse any expanded elements'; },
createdButton: button => {
button.element.append(<i class="far fa-minus-square sidebar-gutter-font-icon"/>);
},
onclick: () => {
const buttons = $qa('[data-embed="1"]');
for(const button of buttons)
button.click();
},
};
};

View file

@ -0,0 +1,43 @@
#include awaitable.js
#include ui/messages.jsx
const MamiSidebarActionPing = function(pingIndicator, msgBox) {
let text, buttonElem, pingMs;
const updateText = () => {
if(pingMs === undefined)
text = 'Ready~';
else if(pingMs < 0)
text = 'Reconnecting...';
else
text = `${pingMs}ms`;
if(buttonElem !== undefined)
buttonElem.title = text;
};
updateText();
return {
get name() { return 'act:ping'; },
get text() { return text; },
get pingMs() { return pingMs; },
set pingMs(value) {
pingMs = value;
updateText();
},
createdButton: button => {
buttonElem = button.element;
updateText();
buttonElem.append(<div class="sidebar-gutter-element">{pingIndicator.element}</div>);
},
onclick: async () => {
await msgBox.show({
body: pingMs < 0 ? 'Reconnecting...' : `Your current ping is ${pingMs}ms.`
});
},
};
};

View file

@ -0,0 +1,22 @@
const MamiSidebarActionScroll = function(settings) {
const scrollOn = <i class="fas fa-forward sidebar-gutter-font-icon"/>;
const scrollOff = <i class="hidden fas fa-pause sidebar-gutter-font-icon"/>;
settings.watch('autoScroll', ev => {
scrollOn.classList.toggle('hidden', !ev.detail.value);
scrollOff.classList.toggle('hidden', ev.detail.value);
});
return {
get name() { return 'act:scroll'; },
get text() { return 'Auto scroll'; },
createdButton: button => {
button.element.append(scrollOn, scrollOff);
},
onclick: () => {
settings.toggle('autoScroll');
},
};
};

View file

@ -0,0 +1,22 @@
const MamiSidebarActionSound = function(settings) {
const soundOn = <i class="fas fa-volume-up sidebar-gutter-font-icon"/>;
const soundOff = <i class="hidden fas fa-volume-off sidebar-gutter-font-icon"/>;
settings.watch('soundEnable', ev => {
soundOn.classList.toggle('hidden', !ev.detail.value);
soundOff.classList.toggle('hidden', ev.detail.value);
});
return {
get name() { return 'act:sound'; },
get text() { return 'Sounds'; },
createdButton: button => {
button.element.append(soundOn, soundOff);
},
onclick: () => {
settings.toggle('soundEnable');
},
};
};

View file

@ -0,0 +1,33 @@
#include animate.js
const MamiSidebarActionToggle = function(sidebar) {
const toggleClose = <i class="fas fa-caret-square-right sidebar-gutter-font-icon"/>;
const toggleOpen = <i class="hidden fas fa-caret-square-left sidebar-gutter-font-icon"/>;
let buttonElem;
sidebar.frame.watch('mami:sidebar:toggle', ev => {
toggleClose.classList.toggle('hidden', !ev.detail.open);
toggleOpen.classList.toggle('hidden', ev.detail.open);
});
return {
get name() { return 'act:toggle'; },
get text() { return 'Toggle sidebar'; },
click: () => {
buttonElem?.click();
},
createdButton: button => {
buttonElem = button.element;
buttonElem.append(toggleClose, toggleOpen);
},
onclick: async () => {
try {
await sidebar.toggle();
} catch(ex) {}
},
};
};

View file

@ -0,0 +1,126 @@
const MamiSidebarPanelChannelsEntry = function(info) {
const nameElem = <div class="sidebar__channel-name">{info.name}</div>;
const html = <div class="sidebar__channel" data-channel-name={info.name}>
<div class="sidebar__channel-details">
{nameElem}
</div>
</div>;
let clickHandler;
html.addEventListener('click', ev => {
if(typeof clickHandler === 'function')
clickHandler(info, html, ev);
});
return {
get element() { return html; },
get name() { return html.dataset.channelName; },
set name(value) {
if(typeof value !== 'string')
throw 'value must be a string';
html.dataset.channelName = value;
nameElem.textContent = value;
},
get active() { return html.classList.contains('sidebar__channel--current'); },
set active(value) {
html.classList.toggle('sidebar__channel--current', value);
if(value)
html.classList.remove('sidebar__channel--unread');
},
get unread() { return html.classList.contains('sidebar__channel--unread'); },
set unread(value) {
if(!html.classList.contains('sidebar__channel--current'))
html.classList.toggle('sidebar__channel--unread', value);
},
set onclick(handler) {
clickHandler = handler;
},
click: () => { html.click(); },
};
};
const MamiSidebarPanelChannels = function() {
const html = <div class="sidebar__menu--channels"/>;
const entries = new Map;
let sbButton;
let clickHandler;
const invokeClick = (...args) => {
if(typeof clickHandler === 'function')
clickHandler(...args);
};
return {
get name() { return 'pan:channels'; },
get text() { return 'Channels'; },
get element() { return html; },
set onClickEntry(handler) {
clickHandler = handler;
},
createdButton: button => {
sbButton = button;
sbButton.element.append(<i class="fas fa-hashtag sidebar-gutter-font-icon"/>);
},
createEntry: info => {
if(entries.has(info.name))
throw 'a channel with this name has already been inserted';
const entry = new MamiSidebarPanelChannelsEntry(info);
entry.onclick = invokeClick;
if(info.isCurrent)
entry.active = true;
html.appendChild(entry.element);
entries.set(info.name, entry);
},
deleteEntry: name => {
const entry = entries.get(name);
if(entry === undefined)
return;
entries.delete(entry.name);
html.removeChild(entry.element);
},
updateEntry: (name, info) => {
const entry = entries.get(name);
if(entry === undefined)
return;
if(info.name !== undefined && entry.name !== info.name) {
entries.delete(entry.name);
entry.name = info.name;
entries.set(entry.name, entry);
}
},
hasEntry: name => entries.has(name),
clearEntries: () => {
for(const [name, entry] of entries) {
entries.delete(name);
html.removeChild(entry.element);
}
},
setActiveEntry: name => {
for(const entry of entries.values())
entry.active = entry.name === name;
},
setUnreadEntry: name => {
const entry = entries.get(name);
if(entry !== undefined) {
entry.unread = true;
if(sbButton !== undefined && entry.unread)
sbButton.attention();
}
},
};
};

View file

@ -0,0 +1,248 @@
const MamiSidebarPanelSettings = function(settings, msgBox) {
const copyright = <div class="mami-copyright">
<a href="//patchii.net/flashii/mami" target="_blank">Mami</a> # <a href={`//patchii.net/flashii/mami/commit/${GIT_HASH}`} target="_blank">{GIT_HASH.substring(0, 7)}</a> © <a href="//flash.moe" target="_blank">flash.moe</a><br/>
<a href="//railgun.sh/sockchat" target="_blank">Sock Chat documentation</a><br/>
</div>;
const html = <div class="sidebar__menu--settings">
{copyright}
</div>;
return {
get name() { return 'pan:settings'; },
get text() { return 'Settings'; },
get element() { return html; },
createdButton: button => {
button.element.append(<i class="fas fa-cog sidebar-gutter-font-icon"/>);
},
category: categoryHandler => {
const header = <div class="setting__category-title"/>;
const body = <div class="setting__category"/>;
const category = <div>
{header}
{body}
</div>;
let animation;
header.onclick = () => {
if(animation !== undefined) {
animation.cancel();
animation = undefined;
}
const closed = category.classList.contains('js-settings-closed');
let start, update, end, height;
if(closed) {
start = () => {
const curHeight = body.style.height;
body.style.height = null;
height = body.clientHeight;
body.style.height = curHeight;
};
update = t => body.style.height = `${height * t}px`;
end = () => body.style.height = null;
} else {
start = () => height = body.clientHeight;
update = t => body.style.height = `${height - (height * t)}px`;
end = () => body.style.height = '0';
}
category.classList.toggle('js-settings-closed', !closed);
animation = MamiAnimate({
duration: 500,
easing: 'outExpo',
start: start,
update: update,
end: end,
});
};
html.insertBefore(category, copyright);
categoryHandler({
header: text => {
if(typeof text !== 'string')
throw 'text must be a string';
header.textContent = text;
},
collapse: () => {
header.click();
},
warning: text => {
if(typeof text !== 'string')
throw 'text must be a string';
body.appendChild(<div class="settings-warning">{text}</div>);
},
button: (text, action, confirm) => {
const button = <button type="button" class="setting__input">{text}</button>;
button.addEventListener('click', ev => {
if(confirm !== undefined)
mami.msgbox.show({
body: confirm,
yes: { primary: false },
no: { primary: true },
}).then(() => { action(button, ev); }).catch(() => {});
else
action(button, ev);
});
body.appendChild(<div class={`setting__container setting__container--button`}>
<label class="setting__label">
{button}
</label>
</div>);
},
setting: name => {
const info = settings.info(name);
const setting = {
name: name,
title: name,
type: undefined,
options: undefined,
confirm: undefined,
disabled: info.immutable,
};
const detectType = () => {
if(info.type === 'boolean')
setting.type = 'checkbox';
else if(info.type === 'number')
setting.type = 'number';
else
setting.type = 'text';
};
const pub = {
title: value => {
if(typeof value !== 'string' && value !== undefined)
throw 'value must be a string';
setting.title = value ?? name;
return pub;
},
type: value => {
if(value === undefined) {
detectType();
} else {
if(typeof value !== 'string')
throw 'value must be a string';
setting.type = value;
}
return pub;
},
options: value => {
if(typeof value !== 'function')
throw 'value must be a function';
setting.options = value;
return pub;
},
confirm: value => {
if(!Array.isArray(value) && typeof value !== 'string' && value !== undefined)
throw 'value must be an array or a string';
setting.confirm = value;
return pub;
},
disable: (value = true) => {
if(!info.immutable)
setting.disabled = !!value;
return pub;
},
done: () => {
if(setting.type === undefined)
detectType();
const title = <div>{setting.title}</div>;
const input = setting.type === 'select'
? <select class="setting__input"/>
: <input type={setting.type} class="setting__input"/>;
const updateSelectOptions = () => {
const options = setting.options();
$rc(input);
for(const name in options)
input.appendChild(<option class={`setting__style setting__style--${setting.name}-${name}`} value={name}>
{options[name]}
</option>);
};
if(setting.disabled)
input.disabled = true;
if(info.min !== undefined)
input.min = info.min;
if(info.max !== undefined)
input.max = info.max;
const label = <label class="setting__label"/>;
const element = <div class={`setting__container setting__container--${setting.type}`}>
{label}
</div>;
if(setting.type === 'checkbox')
label.append(input, title);
else
label.append(title, input);
if(setting.type === 'checkbox') {
settings.watch(info.name, ev => input.checked = ev.detail.value);
input.addEventListener('change', () => {
if(setting.confirm !== undefined && input.checked !== info.fallback) {
msgBox.show({
body: setting.confirm,
yes: { primary: false },
no: { primary: true },
}).then(() => {
settings.toggle(info.name);
}).catch(() => {
input.checked = info.fallback;
});
} else {
settings.toggle(info.name);
}
});
} else {
// copied from previous version, i have no idea what i meant by this:
// hack: do this more gracefully when an actual API for the settings sidebar is exposed
// specifically for the sound pack
if(setting.type === 'select') {
updateSelectOptions();
input.addEventListener('click', () => {
if(input.childElementCount > 1)
return;
const value = settings.get(info.name);
updateSelectOptions();
input.value = value;
});
}
settings.watch(info.name, ev => input.value = ev.detail.value);
input.addEventListener('change', () => settings.set(info.name, input.value));
}
body.appendChild(element);
},
};
return pub;
},
});
},
};
};

View file

@ -0,0 +1,192 @@
#include utility.js
const MamiSidebarPanelUploadsEntry = function(fileInfo) {
const options = new Map;
let uploadInfo;
let detailsElem, thumbElem, nameElem, progElem, optsElem;
const html = <div class="sidebar__user" style="background: linear-gradient(270deg, transparent 0, #111 40%) #222; margin-bottom: 1px;">
{detailsElem = <div class="sidebar__user-details" title={fileInfo.name} style="transition: height .2s" onclick={() => { optsElem.classList.toggle('hidden'); }}>
{thumbElem = <div class="sidebar__user-avatar hidden" style="transition: width .2s, height .2s" onmouseover={() => {
thumbElem.style.width = '100px';
detailsElem.style.height = thumbElem.style.height = '100px';
}} onmouseleave={() => {
thumbElem.style.width = null;
detailsElem.style.height = thumbElem.style.height = null;
}}/>}
{nameElem = <div class="sidebar__user-name" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis">{fileInfo.name}</div>}
</div>}
{progElem = <progress class="eeprom-item-progress" max="100" value="0"/>}
{optsElem = <div class="sidebar__user-options"/>}
</div>;
const public = {
get element() { return html; },
get fileInfo() { return fileInfo; },
get uploadInfo() { return uploadInfo; },
set uploadInfo(value) {
if(uploadInfo !== undefined)
throw 'cannot set upload info after it has already been set';
uploadInfo = value;
nameElem.textContent = value.name;
if(value.isMedia()) {
thumbElem.style.backgroundImage = `url('${value.thumb}')`;
thumbElem.classList.remove('hidden');
}
},
get progress() {
if(progElem === undefined)
return 0;
return progElem.value / progElem.max;
},
set progress(value) {
if(progElem === undefined)
throw 'cannot set progress after it has been nuked';
if(!Number.isFinite(value))
throw 'value must be a finite number';
value = Math.max(0, Math.min(1, value));
progElem.value = Math.ceil(value * progElem.max);
},
get optionsVisible() { return !optsElem.classList.contains('hidden'); },
set optionsVisible(value) {
optsElem.classList.toggle('hidden', !value);
},
nukeProgress: () => {
if(progElem === undefined)
throw 'progress already nuked';
html.removeChild(progElem);
progElem = undefined;
},
hasOption: name => options.has(name),
getOptionNames: () => Array.from(options.keys()),
addOption: option => {
if(options.has(option.name))
throw 'option has already been defined';
let text;
if(typeof option.formatText === 'function')
text = option.formatText(public);
else
text = option.text ?? option.name;
const elem = <div class="sidebar__user-option" onclick={ev => {
if(typeof option.onclick === 'function')
option.onclick(public, elem, ev);
}}>{text}</div>;
options.set(option.name, {
option: option,
element: elem,
});
optsElem.appendChild(elem);
},
removeOption: name => {
const info = options.get(name);
if(info === undefined)
return;
optsElem.removeChild(info.element);
options.delete(name);
},
clickOption: name => {
const info = options.get(name);
if(info === undefined)
return;
info.element.click();
},
};
return public;
};
const MamiSidebarPanelUploads = function() {
const html = <div class="sidebar__menu--uploads"/>;
const options = new Map;
const entries = [];
const reloadOptionsFor = entry => {
const names = entry.getOptionNames();
for(const name of names)
if(name.startsWith(':') && !options.has(name))
entry.removeOption(name);
for(const [name, option] of options) {
if(typeof option.condition !== 'function' || option.condition(entry)) {
if(!names.includes(name))
entry.addOption(option);
} else {
if(names.includes(name))
entry.removeOption(name);
}
}
};
const reloadOptions = () => {
for(const entry of entries)
reloadOptionsFor(entry);
};
return {
get name() { return 'pan:uploads'; },
get text() { return 'Uploads'; },
get element() { return html; },
createdButton: button => {
button.element.append(<i class="fas fa-upload sidebar-gutter-font-icon"/>);
},
createEntry: info => {
const entry = new MamiSidebarPanelUploadsEntry(info);
entries.push(entry);
reloadOptionsFor(entry);
html.insertBefore(entry.element, html.firstElementChild);
return entry;
},
deleteEntry: entry => {
if(!entries.includes(entry))
return;
html.removeChild(entry.element);
$ari(entries, entry);
},
addOption: option => {
if(!option.name.startsWith(':'))
option.name = `:${option.name}`;
if(options.has(option.name))
throw 'option has already been defined';
options.set(option.name, option);
reloadOptions();
},
removeOption: name => {
if(!name.startsWith(':'))
name = `:${name}`;
if(!options.has(name))
return;
options.delete(name);
reloadOptions();
},
reloadOptions: reloadOptions,
reloadOptionsFor: reloadOptionsFor,
};
};

View file

@ -0,0 +1,326 @@
#include animate.js
#include users.js
#include utility.js
const MamiSidebarPanelUsersEntry = function(info) {
const id = info.id;
const self = info.self;
let name = info.name;
let colour = info.colour;
let isAway = info.status?.isAway === true;
let statusMessage = info.status?.message;
const options = new Map;
let avatarElem, nameElem, nameWrapperElem, statusElem, optsElem;
const html = <div class="sidebar__user">
<div class="sidebar__user-details" onclick={() => { setOptionsVisible(); }}>
{avatarElem = <div class="sidebar__user-avatar" />}
{nameElem = <div class="sidebar__user-name"/>}
</div>
{optsElem = <div class="sidebar__user-options hidden" />}
</div>;
const setName = value => {
name = value;
const shouldMarquee = name.length > 16 || mami.settings.get('marqueeAllNames');
let newWrapperElem;
if(shouldMarquee) {
if(!(nameWrapperElem instanceof HTMLMarqueeElement))
newWrapperElem = <marquee style="width: 178px; overflow: hidden"/>;
} else {
if(!(nameWrapperElem instanceof HTMLSpanElement))
newWrapperElem = <span/>;
}
if(newWrapperElem !== undefined) {
if(nameWrapperElem !== undefined)
nameElem.removeChild(nameWrapperElem);
nameWrapperElem = nameElem.insertBefore(newWrapperElem, nameElem.firstElementChild);
}
nameWrapperElem.textContent = name;
};
setName(name);
const setStatusMessage = value => {
isAway = typeof value === 'string' && value.trim() !== '';
statusMessage = isAway ? value : undefined;
if(isAway) {
if(!(statusElem instanceof HTMLElement))
statusElem = nameElem.appendChild(<div class="user-sidebar-afk"/>);
statusElem.textContent = statusMessage;
} else if(statusElem instanceof HTMLElement) {
nameElem.removeChild(statusElem);
statusElem = undefined;
}
};
setStatusMessage(statusMessage);
let optionsVisible = false, optionsAnim, optionsTimeout;
const setOptionsVisible = state => {
if(state === undefined)
state = !optionsVisible;
else if(state === optionsVisible)
return;
if(optionsTimeout !== undefined) {
clearTimeout(optionsTimeout);
optionsTimeout = undefined;
}
if(optionsAnim !== undefined) {
optionsAnim.cancel();
optionsAnim = undefined;
}
let start, update, end, height;
if(state) {
if(mami.settings.get('autoCloseUserContext'))
optionsTimeout = setTimeout(() => {
if(mami.settings.get('autoCloseUserContext'))
setOptionsVisible(false);
}, 300000);
start = () => {
optsElem.classList.remove('hidden');
const curHeight = optsElem.style.height;
optsElem.style.height = null;
height = optsElem.clientHeight;
optsElem.style.height = curHeight;
};
update = function(t) {
optsElem.style.height = `${height * t}px`;
};
end = () => {
optsElem.style.height = null;
};
} else {
start = () => {
height = optsElem.clientHeight;
};
update = t => {
optsElem.style.height = `${height - (height * t)}px`;
};
end = () => {
optsElem.style.height = '0';
optsElem.classList.add('hidden');
};
}
optionsAnim = MamiAnimate({
async: true,
delayed: true,
duration: 500,
easing: 'outExpo',
start: start,
update: update,
end: end,
});
optionsVisible = state;
return optionsAnim.start();
};
let avatar;
const updateAvatar = url => {
avatar = new MamiUserAvatarInfo(id);
avatarElem.style.backgroundImage = `url('${avatar.x60}')`;
};
updateAvatar();
const setColour = value => {
colour = value;
html.style.backgroundColor = colour;
nameElem.style.color = colour;
};
setColour(colour);
const public = {
get element() { return html; },
get id() { return id; },
get self() { return self; },
get name() { return name; },
set name(value) { setName(value); },
get colour() { return colour; },
set colour(value) { setColour(value); },
get isAway() { return isAway; },
get statusMessage() { return statusMessage; },
set statusMessage(value) { setStatusMessage(value); },
get optionsVisible() { return optionsVisible; },
setOptionsVisible: setOptionsVisible,
updateAvatar: updateAvatar,
hasOption: name => options.has(name),
getOptionNames: () => Array.from(options.keys()),
addOption: option => {
if(options.has(option.name))
throw 'option has already been defined';
let text;
if(typeof option.formatText === 'function')
text = option.formatText(public);
else
text = option.text ?? option.name;
const elem = <div class="sidebar__user-option" onclick={ev => {
if(typeof option.onclick === 'function')
option.onclick(public, elem, ev);
}}>{text}</div>;
options.set(option.name, {
option: option,
element: elem,
});
optsElem.appendChild(elem);
},
removeOption: name => {
const info = options.get(name);
if(info === undefined)
return;
optsElem.removeChild(info.element);
options.delete(name);
},
clickOption: name => {
const info = options.get(name);
if(info === undefined)
return;
info.element.click();
},
};
return public;
};
const MamiSidebarPanelUsers = function() {
const html = <div class="sidebar__menu--users"/>;
const options = new Map;
const entries = new Map;
const reloadOptionsFor = entry => {
const names = entry.getOptionNames();
for(const name of names)
if(name.startsWith(':') && !options.has(name))
entry.removeOption(name);
for(const [name, option] of options) {
if(typeof option.condition !== 'function' || option.condition(entry)) {
if(!names.includes(name))
entry.addOption(option);
} else {
if(names.includes(name))
entry.removeOption(name);
}
}
};
const reloadOptions = () => {
for(const entry of entries)
reloadOptionsFor(entry);
};
return {
get name() { return 'pan:users'; },
get text() { return 'Users'; },
get element() { return html; },
createdButton: button => {
button.element.append(<i class="fas fa-users sidebar-gutter-font-icon"/>);
},
// this method is cope (used for KFE exclusive please don't use it elsewhere cuz it will get nuked)
getAllUserNames: () => {
const names = [];
for(const [id, entry] of entries)
names.push(entry.name);
return names;
},
createEntry: info => {
if(entries.has(info.id))
throw 'this user has already been inserted';
const entry = new MamiSidebarPanelUsersEntry(info);
entries.set(entry.id, entry);
let ref;
if(entry.self)
ref = html.firstElementChild;
reloadOptionsFor(entry);
html.insertBefore(entry.element, ref);
return entry;
},
deleteEntry: userId => {
const entry = entries.get(userId);
if(entry === undefined)
return;
entries.delete(entry.id);
html.removeChild(entry.element);
},
updateEntry: (userId, info) => {
const entry = entries.get(userId);
if(entry === undefined)
return;
entry.updateAvatar();
if(info.name !== undefined)
entry.name = info.name;
if(info.colour !== undefined)
entry.colour = info.colour;
if(info.status !== undefined)
entry.statusMessage = info.status.message;
},
hasEntry: userId => entries.has(userId),
clearEntries: selfAlso => {
for(const [userId, entry] of entries) {
if(!selfAlso && entry.self)
continue;
entries.delete(userId);
html.removeChild(entry.element);
}
},
addOption: option => {
if(!option.name.startsWith(':'))
option.name = `:${option.name}`;
if(options.has(option.name))
throw 'option has already been defined';
options.set(option.name, option);
reloadOptions();
},
removeOption: name => {
if(!name.startsWith(':'))
name = `:${name}`;
if(!options.has(name))
return;
options.delete(name);
reloadOptions();
},
reloadOptions: reloadOptions,
reloadOptionsFor: reloadOptionsFor,
};
};

View file

@ -1,22 +1,296 @@
const MamiSidebarGutter = function() {
const html = <div class="sidebar__selector">
<div class="sidebar__selector__separator"/>
</div>;
const MamiSidebarGutterButton = function(name, info) {
const html = <button type="button" class="sidebar__selector-mode" title={info.text ?? name}/>;
const isBottom = info.pos === 'bottom';
html.addEventListener('click', ev => {
const onclick = info.onclick;
if(typeof onclick === 'function')
onclick(name, html, ev);
});
const isActive = () => html.classList.contains('sidebar__selector-mode--active');
return {
get element() { return html; },
get name() { return name; },
get isBottom() { return isBottom; },
get text() { return html.title; },
set text(value) {
html.title = value.toString();
},
get active() { return isActive(); },
set active(state) {
html.classList.toggle('sidebar__selector-mode--active', state);
html.classList.remove('sidebar__selector-mode--attention');
},
attention: () => {
if(!isActive())
html.classList.add('sidebar__selector-mode--attention');
},
click: () => {
html.click();
},
};
};
const MamiSidebarGutter = function() {
const buttons = new Map;
let activeButton;
const separator = <div class="sidebar__selector__separator js-gutter-separator"/>;
const html = <div class="sidebar__selector">{separator}</div>;
return {
get element() { return html; },
getButton: name => buttons.get(name),
setActiveButton: name => {
if(activeButton !== undefined) {
activeButton.active = false;
activeButton = undefined;
}
if(name === undefined)
return;
const button = buttons.get(name);
if(button === undefined)
return;
activeButton = button;
activeButton.active = true;
return button;
},
createButton: info => {
if(typeof info !== 'object' || info === null)
throw 'info must be a non-null object';
const name = info.name;
if(typeof name !== 'string')
throw 'button info must have a name';
if(buttons.has(name))
throw 'button already exists';
const button = new MamiSidebarGutterButton(name, info);
let refElem = separator;
if(info.pos === 'bottom')
refElem = refElem.nextElementSibling;
else if(typeof info.pos !== 'string' && info.pos !== undefined && info.pos !== 'top')
throw 'info.pos is not a valid value';
html.insertBefore(button.element, refElem);
buttons.set(button.name, button);
const create = info.createdButton;
if(typeof create === 'function')
create(button);
return button;
},
deleteButton: name => {
const button = buttons.get(name);
if(button === undefined)
return;
buttons.delete(button.name);
html.removeChild(button.element);
},
};
};
const MamiSidebarPanel = function(name, info) {
const html = <div class="sidebar__menu hidden">{info}</div>;
return {
get element() { return html; },
get name() { return name; },
get active() { return !html.classList.contains('hidden'); },
set active(state) {
html.classList.toggle('hidden', !state);
},
};
};
const MamiSidebarPanelFrame = function() {
const html = <div class="sidebar__menus"/>;
const panels = new Map;
let activePanel;
let animation;
const width = 220; // should probably not be hardcoded
const isOpen = () => !html.classList.contains('js-menu-closed');
return {
get element() { return html; },
get isOpen() { return isOpen(); },
get activePanelName() { return activePanel?.name; },
watch: (...args) => { html.addEventListener(...args); },
unwatch: (...args) => { html.removeEventListener(...args); },
toggle: open => {
const isOpened = isOpen();
if(open === undefined)
open = !isOpened;
else if(isOpened === open)
return;
if(animation !== undefined) {
animation.cancel();
animation = undefined;
}
html.classList.toggle('js-menu-closed', !open);
html.dispatchEvent(new CustomEvent('mami:sidebar:toggle', {
detail: { open: open, },
}));
const update = open
? t => { html.style.width = `${width * t}px`; }
: t => { html.style.width = `${width - (width * t)}px`; };
animation = MamiAnimate({
async: true,
delayed: true,
duration: 500,
easing: 'outExpo',
start: () => {
html.style.width = null;
html.style.overflowX = 'hidden';
if(activePanel !== undefined)
activePanel.element.style.minWidth = `${width}px`;
},
update: update,
end: () => {
html.style.overflowX = null;
if(open)
html.style.width = null;
if(activePanel !== undefined)
activePanel.element.style.minWidth = null;
},
});
return animation.start();
},
getPanel: name => panels.get(name),
switchPanel: name => {
if(activePanel !== undefined) {
activePanel.active = false;
activePanel = undefined;
}
const panel = panels.get(name);
if(panel === undefined)
return;
activePanel = panel;
activePanel.active = true;
return panel;
},
createPanel: info => {
if(typeof info !== 'object' || info === null)
throw 'info must be a non-null object';
const name = info.name;
if(typeof name !== 'string')
throw 'panel info must have a name';
if(panels.has(name))
throw 'panel already exists';
const panel = new MamiSidebarPanel(name, info);
html.appendChild(panel.element);
panels.set(panel.name, panel);
const create = info.createdPanel;
if(typeof create === 'function')
create(panel);
return panel;
},
};
};
const MamiSidebar = function() {
const html = <div class="sidebar"/>;
const gutter = new MamiSidebarGutter;
html.appendChild(gutter.getElement());
const frame = new MamiSidebarPanelFrame;
const html = <div class="sidebar">
{frame}
{gutter}
</div>;
const switchPanel = name => {
gutter.setActiveButton(name);
frame.switchPanel(name);
};
return {
get element() { return html; },
get gutter() { return gutter; },
get frame() { return frame; },
getButton: gutter.getButton,
getPanel: frame.getPanel,
toggle: open => {
if(open === undefined)
open = !frame.isOpen;
if(!open)
gutter.setActiveButton();
frame.toggle(open);
if(open)
gutter.setActiveButton(frame.activePanelName);
},
switchPanel: switchPanel,
createPanel: info => {
if(info.onclick === undefined)
info.onclick = async name => {
try {
switchPanel(name);
frame.toggle(true);
} catch(ex) {}
};
gutter.createButton(info);
frame.createPanel(info);
if(frame.isOpen && frame.activePanelName === undefined)
switchPanel(info.name);
},
createAction: info => {
if(info.pos === undefined)
info.pos = 'bottom';
gutter.createButton(info);
},
};
};

View file

@ -23,7 +23,7 @@ const MamiSockChat = function(protoWorker) {
client = await protoWorker.root.create('sockchat', { ping: futami.get('ping') });
await client.setDumpPackets(dumpPackets);
Umi.UI.Hooks.SetCallbacks(client.sendMessage, client.switchChannel);
Umi.UI.Hooks.SetCallbacks(client.sendMessage);
MamiCompat('Umi.Server', { get: () => client, configurable: true });
MamiCompat('Umi.Server.SendMessage', { value: text => client.sendMessage(text), configurable: true });

View file

@ -1,5 +1,4 @@
#include animate.js
#include channels.js
#include messages.js
#include parsing.js
#include users.js
@ -7,10 +6,12 @@
#include ui/baka.jsx
#include ui/emotes.js
#include ui/markup.js
#include ui/menus.js
#include ui/messages.jsx
const MamiSockChatHandlers = function(ctx, client, setLoadingOverlay, sockChatReconnect, pingIndicator, pingToggle) {
const MamiSockChatHandlers = function(
ctx, client, setLoadingOverlay, sockChatReconnect, pingIndicator,
sbActPing, sbChannels, sbUsers
) {
if(typeof ctx !== 'object' || ctx === null)
throw 'ctx must be an non-null object';
if(typeof client !== 'object' || client === null)
@ -20,9 +21,13 @@ const MamiSockChatHandlers = function(ctx, client, setLoadingOverlay, sockChatRe
if(typeof sockChatReconnect !== 'function')
throw 'sockChatReconnect must be a function';
if(typeof pingIndicator !== 'object' || pingIndicator === null)
throw 'pingIndicator must be an non-null object';
if(!(pingToggle instanceof Element))
throw 'pingToggle must be an instance of Element';
throw 'pingIndicator must be a non-null object';
if(typeof sbActPing !== 'object' || sbActPing === null)
throw 'sbActPing must be a non-null object';
if(typeof sbChannels !== 'object' || sbChannels === null)
throw 'sbChannels must be a non-null object';
if(typeof sbUsers !== 'object' || sbUsers === null)
throw 'sbUsers must be a non-null object';
const modals = new MamiSockChatModals(ctx.settings, ctx.msgbox, ctx.sound);
const handlers = {};
@ -73,7 +78,7 @@ const MamiSockChatHandlers = function(ctx, client, setLoadingOverlay, sockChatRe
if(ev.detail.diff >= 400) --strength;
if(ev.detail.diff >= 200) --strength;
pingToggle.title = `${ev.detail.diff.toLocaleString()}ms`;
sbActPing.pingMs = ev.detail.diff;
pingIndicator.setStrength(strength);
};
@ -96,6 +101,8 @@ const MamiSockChatHandlers = function(ctx, client, setLoadingOverlay, sockChatRe
Umi.User.setCurrentUser(userInfo);
Umi.Users.Add(userInfo);
sbUsers.createEntry(ev.detail.user);
Umi.UI.Markup.Reset();
Umi.UI.Emoticons.Init();
Umi.Parsing.Init();
@ -143,6 +150,8 @@ const MamiSockChatHandlers = function(ctx, client, setLoadingOverlay, sockChatRe
);
Umi.Users.Add(userInfo);
sbUsers.createEntry(ev.detail.user);
if(ev.detail.msg !== undefined)
Umi.UI.Messages.Add(new MamiMessageInfo(
'user:join',
@ -156,6 +165,8 @@ const MamiSockChatHandlers = function(ctx, client, setLoadingOverlay, sockChatRe
handlers['user:remove'] = ev => {
if(dumpEvents) console.log('user:remove', ev.detail);
sbUsers.deleteEntry(ev.detail.user.id);
const userInfo = Umi.Users.Get(ev.detail.user.id);
if(userInfo === null)
return;
@ -175,6 +186,8 @@ const MamiSockChatHandlers = function(ctx, client, setLoadingOverlay, sockChatRe
handlers['user:update'] = ev => {
if(dumpEvents) console.log('user:update', ev.detail);
sbUsers.updateEntry(ev.detail.user.id, ev.detail.user);
const userInfo = Umi.Users.Get(ev.detail.user.id);
userInfo.name = ev.detail.user.name;
userInfo.colour = ev.detail.user.colour;
@ -189,6 +202,8 @@ const MamiSockChatHandlers = function(ctx, client, setLoadingOverlay, sockChatRe
handlers['user:clear'] = () => {
if(dumpEvents) console.log('user:clear');
sbUsers.clearEntries();
const self = Umi.User.getCurrentUser();
Umi.Users.Clear();
if(self !== undefined)
@ -199,35 +214,28 @@ const MamiSockChatHandlers = function(ctx, client, setLoadingOverlay, sockChatRe
handlers['chan:add'] = ev => {
if(dumpEvents) console.log('chan:add', ev.detail);
Umi.Channels.Add(new MamiChannelInfo(
ev.detail.channel.name,
ev.detail.channel.hasPassword,
ev.detail.channel.isTemporary,
));
sbChannels.createEntry(ev.detail.channel);
};
handlers['chan:remove'] = ev => {
if(dumpEvents) console.log('chan:remove', ev.detail);
Umi.Channels.Remove(Umi.Channels.Get(ev.detail.channel.name));
sbChannels.deleteEntry(ev.detail.channel.name);
};
handlers['chan:update'] = ev => {
if(dumpEvents) console.log('chan:update', ev.detail);
const chanInfo = Umi.Channels.Get(ev.detail.channel.previousName);
chanInfo.name = ev.detail.channel.name;
chanInfo.hasPassword = ev.detail.channel.hasPassword;
chanInfo.isTemporary = ev.detail.channel.isTemporary;
Umi.Channels.Update(ev.detail.channel.previousName, chanInfo);
sbChannels.updateEntry(ev.detail.channel.previousName, ev.detail.channel);
};
handlers['chan:clear'] = () => {
if(dumpEvents) console.log('chan:clear');
Umi.Channels.Clear();
sbChannels.clearEntries();
};
handlers['chan:focus'] = ev => {
if(dumpEvents) console.log('chan:focus', ev.detail);
Umi.Channels.Switch(Umi.Channels.Get(ev.detail.channel.name));
sbChannels.setActiveEntry(ev.detail.channel.name);
Umi.UI.Messages.SwitchChannel(ev.detail.channel);
};
handlers['chan:join'] = ev => {
if(dumpEvents) console.log('chan:join', ev.detail);
@ -244,6 +252,8 @@ const MamiSockChatHandlers = function(ctx, client, setLoadingOverlay, sockChatRe
);
Umi.Users.Add(userInfo);
sbUsers.createEntry(ev.detail.user);
if(ev.detail.msg !== undefined)
Umi.UI.Messages.Add(new MamiMessageInfo(
'channel:join',
@ -303,13 +313,13 @@ const MamiSockChatHandlers = function(ctx, client, setLoadingOverlay, sockChatRe
}
// also hack
if(ev.detail.msg.flags.isPM) {
if(Umi.Channels.Get(channelName) === null)
Umi.Channels.Add(new MamiChannelInfo(channelName, false, true, true));
// this should be raised for other channels too, but that is not possible yet
Umi.UI.Menus.Attention('channels');
}
if(ev.detail.msg.flags.isPM && !sbChannels.hasEntry(channelName))
sbChannels.createEntry({
name: channelName,
hasPassword: false,
isTemporary: true,
isUserChannel: true,
});
let type, detail, author;
if(ev.detail.msg.isBot) {
@ -373,6 +383,8 @@ const MamiSockChatHandlers = function(ctx, client, setLoadingOverlay, sockChatRe
detail = { body: ev.detail.msg.text };
}
sbChannels.setUnreadEntry(channelName);
Umi.UI.Messages.Add(new MamiMessageInfo(
type,
ev.detail.msg.time,

View file

@ -1,69 +0,0 @@
#include channels.js
#include utility.js
#include ui/menus.js
Umi.UI.Channels = (function() {
const sidebarChannel = 'sidebar__channel';
const sidebarChannelCurrent = 'sidebar__channel--current';
const sidebarChannelUnread = 'sidebar__channel--unread';
const markUnread = function(id, mode) {
if(!id)
return;
const channel = $i('channel-' + id.toLowerCase().replace(' ', '-'));
if(!channel)
return;
if(!mode && !channel.classList.contains(sidebarChannelCurrent) && channel.classList.contains(sidebarChannelUnread))
channel.classList.add(sidebarChannelUnread);
else if (mode && channel.classList.contains(sidebarChannelUnread))
channel.classList.remove(sidebarChannelUnread);
};
return {
Add: function(channel) {
const id = 'channel-' + channel.name.toLowerCase().replace(' ', '-'),
cBase = $e({ attrs: { 'class': sidebarChannel, id: id } }),
cDetails = $e({ attrs: { 'class': sidebarChannel + '-details' } }),
cName = $e({ attrs: { 'class': sidebarChannel + '-name' } });
cBase.setAttribute('data-umi-channel', channel.name);
cBase.setAttribute('onclick', 'Umi.UI.Channels.Switch(this.getAttribute(\'data-umi-channel\'))');
cName.appendChild($t(channel.name));
cDetails.appendChild(cName);
cBase.appendChild(cDetails);
Umi.UI.Menus.Get('channels').appendChild(cBase);
},
Update: function(id, channel) {
const cBase = $i('channel-' + id.toLowerCase().replace(' ', '-'));
cBase.id = channel.name.toLowerCase().replace(' ', '-');
cBase.innerText = channel.name;
},
Remove: function(channel) {
$ri('channel-' + channel.name.toLowerCase().replace(' ', '-'));
},
RemoveAll: function() {
Umi.UI.Menus.Get('channels').innerHTML = '';
},
Reload: function(initial) {
const current = Umi.Channels.Current();
const channel = $i('channel-' + current.name.toLowerCase().replace(' ', '-'));
const prev = $c(sidebarChannelCurrent)[0];
if(prev instanceof Element)
prev.classList.remove(sidebarChannelCurrent);
channel.classList.add(sidebarChannelCurrent);
Umi.UI.Messages.SwitchChannel(current);
},
Switch: function(channelName) {
markUnread(channelName, true);
Umi.Channels.Switch(Umi.Channels.Get(channelName));
},
Unread: markUnread,
};
})();

View file

@ -1,10 +1,9 @@
#include utility.js
#include ui/chat-interface.js
#include ui/chat-sidebar.js
#include sidebar/sidebar.jsx
// this needs revising at some point but will suffice for now
Umi.UI.ChatLayout = function() {
const sideBar = new Umi.UI.ChatSideBar;
Umi.UI.ChatLayout = function(sideBar) {
const main = new Umi.UI.ChatInterface;
const html = $e({

View file

@ -1,29 +0,0 @@
#include utility.js
Umi.UI.ChatSideBarButtons = function() {
const html = $e({
attrs: {
id: 'umi-menu-gutter',
className: 'sidebar__selector',
},
child: [
{
attrs: {
className: 'sidebar__selector__separator js-gutter-separator',
},
},
],
});
return {
getElement: function() {
return html;
},
addMenuButton: function() {
//
},
addActionButton: function() {
//
},
};
};

View file

@ -1,19 +0,0 @@
#include utility.js
Umi.UI.ChatSideBarContainer = function() {
const html = $e({
attrs: {
id: 'umi-menus',
className: 'sidebar__menus',
},
});
return {
getElement: function() {
return html;
},
createMenu: function() {
//
},
};
};

View file

@ -1,24 +0,0 @@
#include utility.js
#include ui/chat-sidebar-container.js
#include ui/chat-sidebar-buttons.js
Umi.UI.ChatSideBar = function() {
const container = new Umi.UI.ChatSideBarContainer;
const buttons = new Umi.UI.ChatSideBarButtons;
const html = $e({
attrs: {
className: 'sidebar',
},
child: [
buttons,
container,
],
});
return {
getElement: function() {
return html;
},
};
};

View file

@ -1,62 +1,16 @@
#include channels.js
#include users.js
#include utility.js
#include ui/channels.js
#include ui/messages.jsx
#include ui/users.js
#include ui/view.js
Umi.UI.Hooks = (function() {
let sendMessage;
let switchChannel;
return {
SetCallbacks: (sendMessageFunc, switchChannelFunc) => {
SetCallbacks: (sendMessageFunc) => {
sendMessage = sendMessageFunc;
switchChannel = switchChannelFunc;
},
AddHooks: function() {
Umi.Users.OnAdd.push(function(user) {
Umi.UI.Users.Add(user);
});
Umi.Users.OnRemove.push(function(user) {
Umi.UI.Users.Remove(user);
});
Umi.Users.OnClear.push(function() {
Umi.UI.Users.RemoveAll();
});
Umi.Users.OnUpdate.push(function(id, user) {
Umi.UI.Users.Update(user);
});
Umi.Channels.OnAdd.push(function(channel) {
Umi.UI.Channels.Add(channel);
});
Umi.Channels.OnRemove.push(function(channel) {
Umi.UI.Channels.Remove(channel);
});
Umi.Channels.OnClear.push(function() {
Umi.UI.Channels.RemoveAll();
});
Umi.Channels.OnUpdate.push(function(name, channel) {
Umi.UI.Channels.Update(name, channel);
});
Umi.Channels.OnSwitch.push(function(name, channel) {
Umi.UI.Channels.Reload(name === null);
if(typeof switchChannel === 'function')
switchChannel(channel);
});
window.addEventListener('keydown', function(ev) {
if((ev.ctrlKey && ev.key !== 'v') || ev.altKey)
return;

View file

@ -78,11 +78,5 @@ Umi.UI.InputMenus = (function() {
return $i(id);
return null;
},
Remove: function(baseId) {
$ri(createButtonId(baseId));
$ri('umi-msg-menu-sub-' + baseId);
},
Toggle: toggle,
CreateButton: createButton,
};
})();

View file

@ -1,97 +0,0 @@
#include utility.js
Umi.UI.Menus = (function() {
const ids = [];
const sidebarMenu = 'sidebar__menu';
const sidebarMenuActive = 'sidebar__menu--active';
const sidebarMenuHidden = 'sidebar__menu--hidden';
const sidebarSelectorMode = 'sidebar__selector-mode';
const sidebarSelectorModeActive = 'sidebar__selector-mode--active';
const sidebarSelectorModeAttention = 'sidebar__selector-mode--attention';
const sidebarSelectorModeHidden = 'sidebar__selector-mode--hidden';
const attention = function(baseId, mode) {
if(mode === undefined)
mode = true;
const element = $i('umi-menu-icons-' + baseId);
element.classList.remove(sidebarSelectorModeHidden);
if(mode && !element.classList.contains(sidebarSelectorModeAttention) && !element.classList.contains('active'))
element.classList.add(sidebarSelectorModeAttention);
else if(!mode && element.classList.contains(sidebarSelectorModeAttention))
element.classList.remove(sidebarSelectorModeAttention);
};
const activate = function(baseId) {
const icon = 'umi-menu-icons-' + baseId,
menu = 'umi-menus-' + baseId;
const menuToggle = Umi.UI.Toggles.Get('menu-toggle');
if(menuToggle.className.indexOf('closed') !== -1)
menuToggle.click();
attention(baseId, false);
$c(sidebarMenuActive)[0].classList.remove(sidebarMenuActive);
$i(menu).classList.add(sidebarMenuActive);
$c(sidebarSelectorModeActive)[0].classList.remove(sidebarSelectorModeActive);
$i(icon).classList.add(sidebarSelectorModeActive);
};
return {
Add: function(baseId, title, initiallyHidden) {
if(ids.includes(baseId))
return;
ids.push(baseId);
const menuClass = [sidebarMenu, `${sidebarMenu}--${baseId}`];
const iconClass = [sidebarSelectorMode, `${sidebarSelectorMode}--${baseId}`];
const menus = $i('umi-menus');
const icons = $i('umi-menu-gutter');
if(menus.children.length < 1) {
menuClass.push(sidebarMenuActive);
iconClass.push(sidebarSelectorModeActive);
}
if(initiallyHidden) {
menuClass.push(sidebarMenuHidden);
iconClass.push(sidebarSelectorModeHidden);
}
const elem = $e({
attrs: {
id: `umi-menu-icons-${baseId}`,
classList: iconClass,
title: title,
onclick: function() {
activate(baseId);
},
},
});
icons.insertBefore(elem, icons.querySelector('.js-gutter-separator'));
menus.appendChild($e({ attrs: { 'class': menuClass, id: `umi-menus-${baseId}` } }));
},
Get: function(baseId, icon) {
const id = (icon ? 'umi-menu-icons' : 'umi-menus') + '-' + baseId;
if(ids.indexOf(baseId) >= 0)
return $i(id);
return null;
},
Remove: function(baseId) {
$ri('umi-menu-icons-' + baseId);
$ri('umi-menus-' + baseId);
},
Activate: activate,
Attention: attention,
Active: function() {
return $c(sidebarMenuActive)[0].id.substring(10); // LOL
},
};
})();

View file

@ -1,4 +1,3 @@
#include channels.js
#include common.js
#include parsing.js
#include title.js
@ -341,8 +340,6 @@ Umi.UI.Messages = (function() {
}
if(!msg.silent) {
Umi.UI.Channels.Unread(msg.channel);
if(mami.settings.get('enableNotifications') && isMentioned) {
const options = {};

View file

@ -57,6 +57,7 @@ const MamiPingIndicator = function(initialStrength) {
setStrength(initialStrength);
return {
get element() { return html; },
getElement: () => html,
setStrength: setStrength,
};

View file

@ -1,637 +0,0 @@
#include animate.js
#include awaitable.js
#include common.js
#include emotes.js
#include utility.js
#include settings/backup.js
#include sound/sndtest.jsx
#include ui/baka.jsx
#include ui/emotes.js
#include ui/menus.js
#include ui/view.js
#include ui/youare.jsx
Umi.UI.Settings = (function() {
const items = [
{
name: 'interface',
title: 'Interface',
items: [
{
name: 'style',
title: 'Style',
type: 'select',
options: () => Umi.UI.View.AccentColours,
},
{
name: 'compactView',
title: 'Compact view',
type: 'checkbox',
},
{
name: 'autoScroll',
title: 'Scroll to latest message',
type: 'checkbox',
},
{
name: 'closeTabConfirm',
title: 'Confirm tab close',
type: 'checkbox',
},
{
name: 'showChannelList',
title: 'Show channel list',
type: 'checkbox',
},
{
name: 'autoCloseUserContext',
title: 'Auto-close user menus',
type: 'checkbox',
},
],
},
{
name: 'text',
title: 'Text',
items: [
{
name: 'preventOverflow',
title: 'Prevent overflow',
type: 'checkbox',
},
{
name: 'expandTextBox',
title: 'Grow input box',
type: 'checkbox',
},
{
name: 'eepromAutoInsert',
title: 'Auto-insert uploads',
type: 'checkbox',
},
{
name: 'autoEmbedV1',
title: 'Auto-embed media',
type: 'checkbox',
},
{
name: 'autoEmbedPlay',
title: 'Auto-play media',
type: 'checkbox',
},
],
},
{
name: 'notification',
title: 'Notification',
items: [
{
name: 'flashTitle',
title: 'Strobe title on new message',
type: 'checkbox',
},
{
name: 'enableNotifications',
title: 'Show notifications',
type: 'checkbox',
},
{
name: 'notificationShowMessage',
title: 'Show contents of message',
type: 'checkbox',
},
{
name: 'notificationTriggers',
title: 'Triggers',
type: 'text',
},
],
},
{
name: 'sounds',
title: 'Sound',
items: [
{
name: 'soundEnable',
title: 'Enable sound',
type: 'checkbox',
},
{
name: 'soundPack',
title: 'Sound pack',
type: 'select',
options: () => {
const packs = mami.sound.packs;
const options = { '': 'Default' };
for(const name of packs.names())
options[name] = packs.info(name).getTitle();
return options;
},
},
{
name: 'soundVolume',
title: 'Sound volume',
type: 'range',
},
{
name: 'soundEnableJoin',
title: 'Play join sound',
type: 'checkbox',
},
{
name: 'soundEnableLeave',
title: 'Play leave sound',
type: 'checkbox',
},
{
name: 'soundEnableError',
title: 'Play error sound',
type: 'checkbox',
},
{
name: 'soundEnableServer',
title: 'Play server message sound',
type: 'checkbox',
},
{
name: 'soundEnableIncoming',
title: 'Play receive message sound',
type: 'checkbox',
},
{
name: 'onlySoundOnMention',
title: 'Do Not Play Any Sound Effects, Unless the Message Contains the Username of the User Controlling the Current Flashii Chat Session, Including but Not Limited To Joke Triggers, Receive Sounds and Join Sounds, but Excluding Typing Sounds',
type: 'checkbox',
},
{
name: 'soundEnableOutgoing',
title: 'Play send message sound',
type: 'checkbox',
},
{
name: 'soundEnablePrivate',
title: 'Play private message sound',
type: 'checkbox',
},
{
name: 'soundEnableForceLeave',
title: 'Play kick sound',
type: 'checkbox',
},
{
name: 'minecraft',
title: 'Minecraft',
type: 'select',
options: () => {
return {
'no': 'No Minecraft',
'yes': 'Yes Minecraft',
'old': 'Old Minecraft',
};
},
},
{
name: 'windowsLiveMessenger',
title: 'Windows Live Messenger',
type: 'checkbox',
},
{
name: 'seinfeld',
title: 'Seinfeld',
type: 'checkbox',
},
],
},
{
name: 'misc',
title: 'Misc',
items: [
{
name: 'onlyConnectWhenVisible2',
title: 'Only connect when the tab is in the foreground',
type: 'checkbox',
confirm: ['Please only disable this setting if you are using a desktop or laptop computer, this should always remain on on a phone, tablet or other device of that sort.', 'Are you sure you want to change this setting? Ignoring this warning may carry consequences.'],
},
{
name: 'playJokeSounds',
title: 'Run joke triggers',
type: 'checkbox',
},
{
name: 'weeaboo',
title: 'Weeaboo',
type: 'checkbox',
},
{
name: 'motivationalImages',
title: 'Make images motivational',
type: 'checkbox',
},
{
name: 'motivationalVideos',
title: 'Make videos motivational',
type: 'checkbox',
},
{
name: 'osuKeysV2',
title: 'osu! keyboard sounds',
type: 'select',
options: () => {
return {
'no': 'Off',
'yes': 'On',
'rng': 'On, random pitch',
};
},
},
{
name: 'explosionRadius',
title: 'Messages to keep on clear',
type: 'number',
},
],
},
{
name: 'actions',
title: 'Actions',
items: [
{
title: 'Open compatibility client',
type: 'button',
invoke: () => {
window.open(window.AMI_URL, '_blank', 'noopener');
},
},
{
title: 'Manual reconnect',
type: 'button',
invoke: async button => {
const textOrig = button.value;
let lock = 10;
button.disabled = true;
button.value = 'Reconnecting...';
try {
await mami.conMan.start();
while(--lock > 0) {
button.value = textOrig + ` (${lock}s)`;
await MamiSleep(1000);
}
} finally {
button.value = textOrig;
button.disabled = false;
}
},
},
{
title: 'Reload emoticons',
type: 'button',
invoke: async button => {
const textOrig = button.value;
button.disabled = true;
button.value = 'Reloading emoticons...';
try {
const emotes = await futami.getJson('emotes', true);
MamiEmotes.clear();
MamiEmotes.loadLegacy(emotes);
} finally {
Umi.UI.Emoticons.Init();
button.value = textOrig;
button.disabled = false;
}
},
},
{
title: 'Reload sound library',
type: 'button',
invoke: async button => {
const textOrig = button.value;
button.disabled = true;
button.value = 'Reloading sound library...';
try {
const sounds = await futami.getJson('sounds2');
if(Array.isArray(sounds.library))
mami.sound.library.register(sounds.library, true);
if(Array.isArray(sounds.packs)) {
mami.sound.packs.register(sounds.packs, true);
mami.settings.touch('soundPack', true);
}
} finally {
button.value = textOrig;
button.disabled = false;
}
},
},
{
title: 'Reload joke triggers',
type: 'button',
invoke: async button => {
const textOrig = button.value;
button.disabled = true;
button.value = 'Reloading joke triggers...';
try {
const triggers = await futami.getJson('texttriggers', true);
mami.textTriggers.clearTriggers();
mami.textTriggers.addTriggers(triggers)
} finally {
button.value = textOrig;
button.disabled = false;
}
},
},
],
},
{
name: 'settings',
title: 'Settings',
items: [
{
title: 'Import settings',
type: 'button',
confirm: ['Your current settings will be replaced with the ones in the export.', 'Are you sure you want to continue?'],
invoke: () => {
(new MamiSettingsBackup(mami.settings)).importUpload(document.body);
},
},
{
title: 'Export settings',
type: 'button',
invoke: () => {
const user = Umi.User.getCurrentUser();
let fileName;
if(user !== null)
fileName = `${user.name}'s settings.mami`;
(new MamiSettingsBackup(mami.settings)).exportDownload(document.body, fileName);
},
},
{
title: 'Reset settings',
type: 'button',
confirm: ['This will reset all your settings to their defaults values.', 'Are you sure you want to do this?'],
invoke: () => {
mami.settings.clear();
},
},
],
},
{
name: 'debug',
title: 'Debug',
collapse: true,
warning: "Only touch these settings if you're ABSOLUTELY sure you know what you're doing, you're on your own if you break something.",
items: [
{
name: 'dumpPackets',
title: 'Dump packets to console',
type: 'checkbox',
},
{
name: 'dumpEvents',
title: 'Dump events to console',
type: 'checkbox',
},
{
name: 'marqueeAllNames',
title: 'Apply marquee on everyone',
type: 'checkbox',
},
{
name: 'tmpDisableOldThemeSys',
title: 'Disable Old Theme System',
type: 'checkbox',
},
{
name: 'dbgAnimDurationMulti',
title: 'Animation multiplier',
type: 'range',
},
{
title: 'Test kick/ban notice',
type: 'button',
invoke: async button => {
button.disabled = true;
await mami.views.push(new MamiForceDisconnectNotice({ perma: true, type: 'ban' }));
await MamiSleep(5000);
await mami.views.pop();
button.disabled = false;
},
},
{
title: 'You are an idiot!',
type: 'button',
invoke: async button => {
button.disabled = true;
await mami.views.push(new MamiYouAreAnIdiot(mami.sound.library, mami.views));
button.disabled = false;
},
},
{
title: 'Sound Test',
type: 'button',
invoke: async (button, ev) => {
button.disabled = true;
await mami.views.push(new MamiSoundTest(
mami.settings,
mami.sound.audio,
mami.sound.manager,
mami.sound.library,
[ev.clientX, ev.clientY],
));
button.disabled = false;
},
},
{
title: 'Reset audio context',
type: 'button',
invoke: () => {
mami.sound.reset();
},
},
],
}
];
const createCopyright = function() {
return <div class="mami-copyright">
<a href="//patchii.net/flashii/mami" target="_blank">Mami</a> # <a href={`//patchii.net/flashii/mami/commit/${GIT_HASH}`} target="_blank">{GIT_HASH.substring(0, 7)}</a> © <a href="//flash.moe" target="_blank">flash.moe</a><br/>
<a href="//railgun.sh/sockchat" target="_blank">Sock Chat documentation</a><br/>
</div>;
};
const createSetting = function(display) {
let setting;
if('name' in display)
setting = mami.settings.info(display.name);
let input = display.type === 'select'
? <select class="setting__input"/>
: <input type={display.type} class="setting__input"/>;
if(display.disabled === true)
input.disabled = true;
const updateSelectOptions = () => {
const options = display.options();
$rc(input);
for(const name in options)
input.appendChild(<option class={['setting__style', `setting__style--${display.name}-${name}`]} value={name}>
{options[name]}
</option>);
};
if(display.type === 'select') {
updateSelectOptions();
} else if(display.type === 'button') {
input.value = display.title;
input.addEventListener('click', ev => {
if(display.confirm !== undefined)
mami.msgbox.show({
body: display.confirm,
yes: { primary: false },
no: { primary: true },
}).then(() => { display.invoke(input, ev); }).catch(() => {});
else
display.invoke(input, ev);
});
}
if(setting !== undefined) {
if(!input.disabled && setting.immutable)
input.disabled = true;
if(setting.min !== undefined)
input.min = setting.min;
if(setting.max !== undefined)
input.max = setting.max;
if(display.type === 'checkbox') {
mami.settings.watch(setting.name, ev => input.checked = ev.detail.value);
input.addEventListener('change', () => {
if(display.confirm !== undefined && input.checked !== setting.fallback) {
mami.msgbox.show({
body: display.confirm,
yes: { primary: false },
no: { primary: true },
}).then(() => {
mami.settings.toggle(setting.name);
}).catch(() => {
input.checked = setting.fallback;
});
} else {
mami.settings.toggle(setting.name);
}
});
} else {
// hack: do this more gracefully when an actual API for the settings sidebar is exposed
// specifically for the sound pack
if(display.type === 'select')
input.addEventListener('click', () => {
if(input.childElementCount > 1)
return;
const value = mami.settings.get(setting.name);
updateSelectOptions();
input.value = value;
});
mami.settings.watch(setting.name, ev => input.value = ev.detail.value);
input.addEventListener('change', () => mami.settings.set(setting.name, input.value));
}
}
let label = input;
if(display.type === 'checkbox') {
label = <label class="setting__label">
{input}
<div>{display.title}</div>
</label>;
} else if(display.type === 'button') {
label = <label class="setting__label">{input}</label>;
} else {
label = <label class="setting__label">
<div>{display.title}</div>
{input}
</label>;
}
return <div class={['setting__container', `setting__container--${display.type}`]}>{label}</div>;
};
const createCategory = function(category) {
const catHeader = <div class={['setting__category-title', `setting__category-title--${category.name}`]} style={{ cursor: 'pointer' }}>{category.title}</div>;
const catBody = <div class={['setting__category', `setting__category--${category.name}`]} style={{ overflow: 'hidden' }}/>;
const catContainer = <div>{catHeader}{catBody}</div>;
let anime, closed = false;
catHeader.onclick = () => {
if(anime !== undefined) {
anime.cancel();
anime = undefined;
}
let start, update, end, height;
if(closed) {
start = () => {
const curHeight = catBody.style.height;
catBody.style.height = null;
height = catBody.clientHeight;
catBody.style.height = curHeight;
};
update = t => catBody.style.height = `${height * t}px`;
end = () => catBody.style.height = null;
} else {
start = () => height = catBody.clientHeight;
update = t => catBody.style.height = `${height - (height * t)}px`;
end = () => catBody.style.height = '0';
}
closed = !closed;
anime = MamiAnimate({
duration: 500,
easing: 'outExpo',
start: start,
update: update,
end: end,
});
};
if(category.warning)
catBody.appendChild(<div style={{ fontSize: '.9em', lineHeight: '1.4em', margin: '5px', padding: '5px', backgroundColor: 'darkred', border: '2px solid red', borderRadius: '5px' }}>{category.warning}</div>);
if(category.items)
for(const item of category.items)
catBody.appendChild(createSetting(item));
return catContainer;
};
return {
Init: function() {
const html = Umi.UI.Menus.Get('settings');
$rc(html);
for(const category of items) {
const elem = createCategory(category);
html.appendChild(elem);
// only a little bit of hacking, stan
if(category.collapse)
elem.firstChild.click();
}
html.appendChild(createCopyright());
},
};
})();

View file

@ -1,42 +0,0 @@
#include utility.js
Umi.UI.Toggles = (function() {
const ids = [];
return {
Add: function(baseId, eventHandlers, title) {
if(ids.includes(baseId))
return;
ids.push(baseId);
const toggle = $e({
attrs: {
id: 'umi-toggles-' + baseId,
classList: ['sidebar__selector-mode', 'sidebar__selector-mode--' + baseId],
title: title,
},
});
if(typeof eventHandlers === 'object' && eventHandlers !== null)
for(const i in eventHandlers)
toggle.addEventListener(i, eventHandlers[i]);
const toggles = $i('umi-menu-gutter');
toggles.insertBefore(toggle, toggles.querySelector('.js-gutter-separator').nextElementSibling);
// hack cuz i'm lazy
if(baseId === 'ping')
return toggle;
},
Get: function(baseId, icon) {
const id = 'umi-toggles-' + baseId;
if(ids.indexOf(baseId) >= 0)
return $i(id);
return null;
},
Remove: function(baseId) {
$ri('umi-toggles-' + baseId);
},
};
})();

View file

@ -1,133 +0,0 @@
#include utility.js
#include ui/menus.js
Umi.UI.Uploads = (function() {
return {
create: function(fileName) {
const uploadHistory = Umi.UI.Menus.Get('uploads');
if(!uploadHistory) {
console.error('Upload history missing???');
return;
}
Umi.UI.Menus.Attention('uploads');
let nameWrap, options, thumb, name, prog;
const uploadEntry = $e({
attrs: {
className: 'sidebar__user',
style: {
background: 'linear-gradient(270deg, transparent 0, #111 40%) #222',
marginBottom: '1px',
},
},
child: [
{
attrs: {
className: 'sidebar__user-details',
title: fileName,
style: {
transition: 'height .2s',
},
onclick: function() {
uploadEntry.querySelector('.sidebar__user-options').classList.toggle('hidden');
},
},
child: [
{
attrs: {
className: 'sidebar__user-avatar hidden',
style: {
transition: 'width .2s, height .2s',
},
onmouseover: function() {
thumb.style.width = '100px';
nameWrap.style.height = thumb.style.height = '100px';
},
onmouseleave: function() {
thumb.style.width = null;
nameWrap.style.height = thumb.style.height = null;
},
},
created: function(elem) { thumb = elem; },
},
{
attrs: {
className: 'sidebar__user-name',
style: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
},
child: fileName,
created: function(elem) { name = elem; },
},
],
created: function(elem) { nameWrap = elem; },
},
{
tag: 'progress',
attrs: {
className: 'eeprom-item-progress',
max: 100,
value: 0,
},
created: function(elem) { prog = elem; },
},
{
attrs: {
className: 'sidebar__user-options',
},
created: function(elem) { options = elem; },
},
],
});
uploadHistory.insertBefore(uploadEntry, uploadHistory.firstChild);
return {
getElement: function() {
return uploadEntry;
},
remove: function() {
$r(uploadEntry);
},
clearOptions: function() {
options.innerHTML = '';
},
hideOptions: function() {
options.classList.add('hidden');
},
addOption: function(text, arg) {
const info = {
attrs: {
className: 'sidebar__user-option',
},
child: (text || '').toString(),
};
if(typeof arg === 'string') {
info.tag = 'a';
info.attrs.target = '_blank';
info.attrs.href = arg;
} else
info.attrs.onclick = arg;
options.appendChild($e(info));
},
setThumbnail: function(url) {
thumb.style.backgroundImage = "url('" + url + "')";
thumb.classList.remove('hidden');
},
removeProgress: function() {
$r(prog);
},
setProgress: function(progress) {
prog.value = Math.ceil(progress * 100);
},
};
},
};
})();

View file

@ -1,217 +0,0 @@
#include animate.js
#include common.js
#include users.js
#include utility.js
#include ui/menus.js
#include ui/view.js
Umi.UI.Users = (function() {
const toggleTimeouts = {};
const toggleUser = function(id) {
const prefix = 'user-' + id.toString();
const userOptions = $i(prefix + '-options-wrapper');
const closedClass = 'sidebar__user-options--hidden';
const isClosed = userOptions.classList.contains(closedClass);
let start, update, end, height;
if(prefix in toggleTimeouts) {
clearTimeout(toggleTimeouts[prefix]);
delete toggleTimeouts[prefix];
}
if(isClosed) {
if(mami.settings.get('autoCloseUserContext'))
toggleTimeouts[prefix] = setTimeout(function() {
if(mami.settings.get('autoCloseUserContext'))
toggleUser(id);
}, 300000);
start = () => {
userOptions.classList.remove(closedClass);
const curHeight = userOptions.style.height;
userOptions.style.height = null;
height = userOptions.clientHeight;
userOptions.style.height = curHeight;
};
update = function(t) {
userOptions.style.height = `${height * t}px`;
};
end = () => {
userOptions.style.height = null;
};
} else {
start = () => {
height = userOptions.clientHeight;
};
update = t => {
userOptions.style.height = `${height - (height * t)}px`;
};
end = () => {
userOptions.style.height = '0';
userOptions.classList.add(closedClass);
};
}
// todo: actually make cancellable
MamiAnimate({
duration: 500,
easing: 'outExpo',
start: start,
update: update,
end: end,
});
};
const createAction = function(text, events) {
const elem = $e({ tag: 'li', attrs: { 'class': 'sidebar__user-option' }, child: text });
for(const i in events)
elem.addEventListener(i, events[i]);
return elem;
};
return {
Add: function(user) {
const id = 'user-' + user.id;
const uBase = $e({ attrs: { 'class': 'sidebar__user', id: id } });
const uDetails = $e({ attrs: { 'class': 'sidebar__user-details', id: id + '-details' } });
const uAvatar = $e({ attrs: { 'class': 'sidebar__user-avatar', id: id + '-avatar' } });
const uName = $e({ attrs: { 'class': 'sidebar__user-name', id: id + '-name' } });
const uOptions = $e({ tag: 'ul', attrs: { 'class': 'sidebar__user-options', id: id + '-options' } });
uDetails.addEventListener('click', function() {
toggleUser(user.id);
}.bind(this));
const profileUrl = futami.get('profile');
if (profileUrl !== null || profileUrl.length > 1) {
uOptions.appendChild(createAction('View profile', {
'click': function() {
window.open(profileUrl.replace('{user:id}', user.id), '_blank');
}
}));
}
if (Umi.User.isCurrentUser(user)) {
uOptions.appendChild(createAction('Describe action', {
'click': function() {
Umi.UI.View.SetText('/me ');
}
}));
if (user.perms.canSetNick) {
uOptions.appendChild(createAction('Set nickname', {
'click': function() {
Umi.UI.View.SetText('/nick ');
}
}));
}
if (user.perms.canKick) {
uOptions.appendChild(createAction('View bans', {
'click': function() {
Umi.UI.View.SetText('/bans');
}
}));
uOptions.appendChild(createAction('Kick Fucking Everyone', {
'click': function() {
mami.msgbox.show({
body: ['You are about to detonate the fucking bomb.', 'Are you sure?'],
yes: { text: 'FUCK IT, WE BALL' },
no: { text: 'nah' },
}).then(() => {
const targets = Umi.Users.All().reverse();
for(const target of targets) // this shouldn't call it like this but will have to leave it for now
Umi.Server.sendMessage('/kick ' + target.name);
}).catch(() => {});
}
}));
}
}
else {
/*uOptions.appendChild(createAction('Send PM', {
'click': function() {
Umi.UI.View.SetText('/msg ' + user.name + ' ');
}
}));*/
if (Umi.User.getCurrentUser().perms.canKick) {
uOptions.appendChild(createAction('Kick', {
'click': function() {
Umi.UI.View.SetText('/kick ' + user.name);
}
}));
uOptions.appendChild(createAction('View IP', {
'click': function() {
Umi.UI.View.SetText('/ip ' + user.name);
}
}));
}
}
uName.style.color = user.colour;
uBase.style.backgroundColor = user.colour === 'inherit' ? '#fff' : user.colour;
if(user.status.isAway)
uName.appendChild($e({ attrs: { 'class': 'user-sidebar-afk' }, child: user.status.message }));
if(user.name.length > 16 || mami.settings.get('marqueeAllNames')) {
uName.appendChild($e({
tag: 'marquee',
attrs: {
style: {
width: '180px',
overflow: 'hidden',
},
},
child: user.name,
}));
} else {
uName.appendChild($t(user.name));
}
const avatarUrl = user.avatar.x60;
if(avatarUrl !== '') {
uAvatar.style.backgroundImage = `url('${avatarUrl}')`;
uDetails.appendChild(uAvatar);
}
uDetails.appendChild(uName);
uBase.appendChild(uDetails);
uBase.appendChild($e({child: uOptions, attrs: { id: id + '-options-wrapper', className: 'sidebar__user-options-wrapper sidebar__user-options--hidden', }}));
Umi.UI.Menus.Get('users').appendChild(uBase);
},
Update: function(user) {
const uBase = $i('user-' + user.id);
const uAvatar = $i('user-' + user.id + '-avatar');
const uName = $i('user-' + user.id + '-name');
uName.style.color = user.colour;
uBase.style.backgroundColor = user.colour === 'inherit' ? '#fff' : user.colour;
uName.textContent = '';
const avatarUrl = user.avatar.x60;
if(uAvatar instanceof Element && avatarUrl !== '')
uAvatar.style.backgroundImage = `url('${avatarUrl}')`;
if(user.status.isAway)
uName.appendChild($e({ attrs: { 'class': 'user-sidebar-afk' }, child: user.status.message }));
if(user.name.length > 16 || mami.settings.get('marqueeAllNames')) {
uName.appendChild($e({
tag: 'marquee',
attrs: {
style: {
width: '180px',
overflow: 'hidden',
},
},
child: user.name,
}));
} else
uName.appendChild($t(user.name));
},
Remove: function(user) {
$ri('user-' + user.id);
},
RemoveAll: function() {
Umi.UI.Menus.Get('users').innerHTML = '';
},
CreateAction: createAction,
ToggleUser: toggleUser,
};
})();

View file

@ -53,12 +53,6 @@ Umi.UI.View = (function() {
},
SetPosition: setPosition,
GetPosition: getPosition,
GoToStart: function() {
setPosition(0);
},
GoToEnd: function() {
setPosition($i('umi-msg-text').value.length);
},
GetSelectionLength: function() {
let length = getPosition(true) - getPosition();
if(length < 0)
@ -77,9 +71,6 @@ Umi.UI.View = (function() {
setText(out);
setPosition(current);
},
GetSelectedText: function() {
return getText().slice(getPosition(), getPosition(true));
},
GetText: getText,
SetText: setText,
};

View file

@ -127,39 +127,21 @@ Umi.User = (() => {
Umi.Users = (function() {
const users = new Map;
const onAdd = [];
const onRemove = [];
const onClear = [];
const onUpdate = [];
return {
OnAdd: onAdd,
OnRemove: onRemove,
OnClear: onClear,
OnUpdate: onUpdate,
Add: function(user) {
const userId = user.id;
if(!users.has(userId)) {
users.set(userId, user);
for(const i in onAdd)
onAdd[i](user);
}
},
Remove: function(user) {
const userId = user.id;
if(users.has(userId)) {
users.delete(userId);
for(const i in onRemove)
onRemove[i](user);
}
},
Clear: function() {
users.clear();
for(const i in onClear)
onClear[i]();
},
All: function() {
return Array.from(users.values());
@ -196,9 +178,6 @@ Umi.Users = (function() {
Update: function(userId, user) {
userId = userId.toString();
users.set(userId, user);
for(const i in onUpdate)
onUpdate[i](userId, user);
},
};
})();

View file

@ -237,8 +237,12 @@ const SockChatProtocol = function(dispatch, options) {
ctx.pseudoChannelName = name.substring(1);
} else {
ctx.pseudoChannelName = undefined;
if(ctx.channelName === name)
if(ctx.channelName === name) {
ctx.dispatch('chan:focus', {
channel: { name: ctx.channelName },
});
return;
}
ctx.channelName = name;
await sendMessage(`/join ${name}`);