commit e94c9118bd8c3100013f4165e114c4518b70a3e3 Author: flashwave Date: Thu Jan 18 19:50:37 2024 +0000 Imported Mami into own repository. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..78503a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +[Tt]humbs.db +[Dd]esktop.ini +.DS_Store +/config/config.ini +/config/config.json +/public/robots.txt +/.debug +/node_modules +/public/assets +/public/index.html +/public/legacy/index.html diff --git a/README.md b/README.md new file mode 100644 index 0000000..461e815 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Mami + +The Flashii Chat client. diff --git a/build.js b/build.js new file mode 100644 index 0000000..a2ef4a5 --- /dev/null +++ b/build.js @@ -0,0 +1,210 @@ +// IMPORTS +const fs = require('fs'); +const swc = require('@swc/core'); +const path = require('path'); +const util = require('util'); +const postcss = require('postcss'); +const jsminify = require('terser').minify; +const htmlminify = require('html-minifier-terser').minify; +const utils = require('./src/utils.js'); +const assproc = require('./src/assproc.js'); + + +// 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: 'websock.js', target: '/assets', name: 'mami-ws.{hash}.js', buildVar: 'MAMI_WS', buildVarsTarget: 'self', }, + { source: 'mami.js', target: '/assets', name: 'mami.{hash}.js', buildVar: 'MAMI_JS', }, + { source: 'mami-init.js', target: '/assets', name: 'mami-init.{hash}.js', es: 'es5', }, + ], + css: [ + { source: 'mami.css', target: '/assets', name: 'mami.{hash}.css', }, + ], + html: [ + { source: 'mami.html', target: '/', name: 'index.html', }, + ], +}; + + +// PREP +const config = JSON.parse(fs.readFileSync(configFile)); + +const postcssPlugins = []; +if(!isDebugBuild) postcssPlugins.push(require('cssnano')); +postcssPlugins.push(require('autoprefixer')({ + remove: false, +})); + +const swcJscOptions = { + target: 'es2021', + 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, + }; + + console.log('Ensuring assets directory exists...'); + fs.mkdirSync(pubAssetsDir, { recursive: true }); + + + 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('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); +})(); diff --git a/config/config.example.json b/config/config.example.json new file mode 100644 index 0000000..dabc3fe --- /dev/null +++ b/config/config.example.json @@ -0,0 +1,6 @@ +{ + "title": "Flashii Chat", + "common_url": "//futami.flashii.net/common.json", + "modern_url": "//chat.flashii.net", + "compat_url": "//sockchat.flashii.net" +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6cdd908 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1372 @@ +{ + "name": "chat.edgii.net", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@swc/core": "^1.3.104", + "autoprefixer": "^10.4.17", + "cssnano": "^6.0.3", + "html-minifier-terser": "^7.2.0", + "postcss": "^8.4.33", + "terser": "^5.27.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.21.tgz", + "integrity": "sha512-SRfKmRe1KvYnxjEMtxEr+J4HIeMX5YBg/qhRHpxEIGjhX1rshcHlnFUE9K0GazhVKWM7B+nARSkV8LuvJdJ5/g==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@swc/core": { + "version": "1.3.104", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.104.tgz", + "integrity": "sha512-9LWH/qzR/Pmyco+XwPiPfz59T1sryI7o5dmqb593MfCkaX5Fzl9KhwQTI47i21/bXYuCdfa9ySZuVkzXMirYxA==", + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.1", + "@swc/types": "^0.1.5" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.3.104", + "@swc/core-darwin-x64": "1.3.104", + "@swc/core-linux-arm-gnueabihf": "1.3.104", + "@swc/core-linux-arm64-gnu": "1.3.104", + "@swc/core-linux-arm64-musl": "1.3.104", + "@swc/core-linux-x64-gnu": "1.3.104", + "@swc/core-linux-x64-musl": "1.3.104", + "@swc/core-win32-arm64-msvc": "1.3.104", + "@swc/core-win32-ia32-msvc": "1.3.104", + "@swc/core-win32-x64-msvc": "1.3.104" + }, + "peerDependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.3.104", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.104.tgz", + "integrity": "sha512-rCnVj8x3kn6s914Adddu+zROHUn6mUEMkNKUckofs3W9OthNlZXJA3C5bS2MMTRFXCWamJ0Zmh6INFpz+f4Tfg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.3.104", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.104.tgz", + "integrity": "sha512-LBCWGTYkn1UjyxrmcLS3vZgtCDVhwxsQMV7jz5duc7Gas8SRWh6ZYqvUkjlXMDX1yx0uvzHrkaRw445+zDRj7Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.3.104", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.104.tgz", + "integrity": "sha512-iFbsWcx0TKHWnFBNCuUstYqRtfkyBx7FKv5To1Hx14EMuvvoCD/qUoJEiNfDQN5n/xU9g5xq4RdbjEWCFLhAbA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.3.104", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.104.tgz", + "integrity": "sha512-1BIIp+nUPrRHHaJ35YJqrwXPwYSITp5robqqjyTwoKGw2kq0x+A964kpWul6v0d7A9Ial8fyH4m13eSWBodD2A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.3.104", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.104.tgz", + "integrity": "sha512-IyDNkzpKwvLqmRwTW+s8f8OsOSSj1N6juZKbvNHpZRfWZkz3T70q3vJlDBWQwy8z8cm7ckd7YUT3eKcSBPPowg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.3.104", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.104.tgz", + "integrity": "sha512-MfX/wiRdTjE5uXHTDnaX69xI4UBfxIhcxbVlMj//N+7AX/G2pl2UFityfVMU2HpM12BRckrCxVI8F/Zy3DZkYQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.3.104", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.104.tgz", + "integrity": "sha512-5yeILaxA31gGEmquErO8yxlq1xu0XVt+fz5mbbKXKZMRRILxYxNzAGb5mzV41r0oHz6Vhv4AXX/WMCmeWl+HkQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.3.104", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.104.tgz", + "integrity": "sha512-rwcImsYnWDWGmeESG0XdGGOql5s3cG5wA8C4hHHKdH76zamPfDKKQFBsjmoNi0f1IsxaI9AJPeOmD4bAhT1ZoQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.3.104", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.104.tgz", + "integrity": "sha512-ICDA+CJLYC7NkePnrbh/MvXwDQfy3rZSFgrVdrqRosv9DKHdFjYDnA9++7ozjrIdFdBrFW2NR7pyUcidlwhNzA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.3.104", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.104.tgz", + "integrity": "sha512-fZJ1Ju62U4lMZVU+nHxLkFNcu0hG5Y0Yj/5zjrlbuX5N8J5eDndWAFsVnQhxRTZqKhZB53pvWRQs5FItSDqgXg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz", + "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==" + }, + "node_modules/@swc/types": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", + "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==" + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.17", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", + "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.22.2", + "caniuse-lite": "^1.0.30001578", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/browserslist": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001579", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz", + "integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } + }, + "node_modules/css-declaration-sorter": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.1.1.tgz", + "integrity": "sha512-dZ3bVTEEc1vxr3Bek9vGwfB5Z6ESPULhcRvO472mfjVnj8jRcTnKO8/JTczlvxM10Myb+wBM++1MtdO76eWcaQ==", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.0.3.tgz", + "integrity": "sha512-MRq4CIj8pnyZpcI2qs6wswoYoDD1t0aL28n+41c1Ukcpm56m1h6mCexIHBGjfZfnTqtGSSCP4/fB1ovxgjBOiw==", + "dependencies": { + "cssnano-preset-default": "^6.0.3", + "lilconfig": "^3.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-default": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.0.3.tgz", + "integrity": "sha512-4y3H370aZCkT9Ev8P4SO4bZbt+AExeKhh8wTbms/X7OLDo5E7AYUUy6YPxa/uF5Grf+AJwNcCnxKhZynJ6luBA==", + "dependencies": { + "css-declaration-sorter": "^7.1.1", + "cssnano-utils": "^4.0.1", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.0.2", + "postcss-convert-values": "^6.0.2", + "postcss-discard-comments": "^6.0.1", + "postcss-discard-duplicates": "^6.0.1", + "postcss-discard-empty": "^6.0.1", + "postcss-discard-overridden": "^6.0.1", + "postcss-merge-longhand": "^6.0.2", + "postcss-merge-rules": "^6.0.3", + "postcss-minify-font-values": "^6.0.1", + "postcss-minify-gradients": "^6.0.1", + "postcss-minify-params": "^6.0.2", + "postcss-minify-selectors": "^6.0.2", + "postcss-normalize-charset": "^6.0.1", + "postcss-normalize-display-values": "^6.0.1", + "postcss-normalize-positions": "^6.0.1", + "postcss-normalize-repeat-style": "^6.0.1", + "postcss-normalize-string": "^6.0.1", + "postcss-normalize-timing-functions": "^6.0.1", + "postcss-normalize-unicode": "^6.0.2", + "postcss-normalize-url": "^6.0.1", + "postcss-normalize-whitespace": "^6.0.1", + "postcss-ordered-values": "^6.0.1", + "postcss-reduce-initial": "^6.0.2", + "postcss-reduce-transforms": "^6.0.1", + "postcss-svgo": "^6.0.2", + "postcss-unique-selectors": "^6.0.2" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-utils": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.1.tgz", + "integrity": "sha512-6qQuYDqsGoiXssZ3zct6dcMxiqfT6epy7x4R0TQJadd4LWO3sPR6JH6ZByOvVLoZ6EdwPGgd7+DR1EmX3tiXQQ==", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.638", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.638.tgz", + "integrity": "sha512-gpmbAG2LbfPKcDaL5m9IKutKjUx4ZRkvGNkgL/8nKqxkXsBVYykVULboWlqCrHsh3razucgDJDuKoWJmGPdItA==" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/html-minifier-terser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", + "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "~5.3.2", + "commander": "^10.0.0", + "entities": "^4.4.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.15.1" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, + "node_modules/lilconfig": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "engines": { + "node": ">=14" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/postcss": { + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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==", + "dependencies": { + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-colormin": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.0.2.tgz", + "integrity": "sha512-TXKOxs9LWcdYo5cgmcSHPkyrLAh86hX1ijmyy6J8SbOhyv6ua053M3ZAM/0j44UsnQNIWdl8gb5L7xX2htKeLw==", + "dependencies": { + "browserslist": "^4.22.2", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-convert-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.0.2.tgz", + "integrity": "sha512-aeBmaTnGQ+NUSVQT8aY0sKyAD/BaLJenEKZ03YK0JnDE1w1Rr8XShoxdal2V2H26xTJKr3v5haByOhJuyT4UYw==", + "dependencies": { + "browserslist": "^4.22.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-comments": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.1.tgz", + "integrity": "sha512-f1KYNPtqYLUeZGCHQPKzzFtsHaRuECe6jLakf/RjSRqvF5XHLZnM2+fXLhb8Qh/HBFHs3M4cSLb1k3B899RYIg==", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.1.tgz", + "integrity": "sha512-1hvUs76HLYR8zkScbwyJ8oJEugfPV+WchpnA+26fpJ7Smzs51CzGBHC32RS03psuX/2l0l0UKh2StzNxOrKCYg==", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-empty": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.1.tgz", + "integrity": "sha512-yitcmKwmVWtNsrrRqGJ7/C0YRy53i0mjexBDQ9zYxDwTWVBgbU4+C9jIZLmQlTDT9zhml+u0OMFJh8+31krmOg==", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.1.tgz", + "integrity": "sha512-qs0ehZMMZpSESbRkw1+inkf51kak6OOzNRaoLd/U7Fatp0aN2HQ1rxGOrJvYcRAN9VpX8kUF13R2ofn8OlvFVA==", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.2.tgz", + "integrity": "sha512-+yfVB7gEM8SrCo9w2lCApKIEzrTKl5yS1F4yGhV3kSim6JzbfLGJyhR1B6X+6vOT0U33Mgx7iv4X9MVWuaSAfw==", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^6.0.2" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-rules": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.0.3.tgz", + "integrity": "sha512-yfkDqSHGohy8sGYIJwBmIGDv4K4/WrJPX355XrxQb/CSsT4Kc/RxDi6akqn5s9bap85AWgv21ArcUWwWdGNSHA==", + "dependencies": { + "browserslist": "^4.22.2", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^4.0.1", + "postcss-selector-parser": "^6.0.15" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.0.1.tgz", + "integrity": "sha512-tIwmF1zUPoN6xOtA/2FgVk1ZKrLcCvE0dpZLtzyyte0j9zUeB8RTbCqrHZGjJlxOvNWKMYtunLrrl7HPOiR46w==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.1.tgz", + "integrity": "sha512-M1RJWVjd6IOLPl1hYiOd5HQHgpp6cvJVLrieQYS9y07Yo8itAr6jaekzJphaJFR0tcg4kRewCk3kna9uHBxn/w==", + "dependencies": { + "colord": "^2.9.1", + "cssnano-utils": "^4.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-params": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.0.2.tgz", + "integrity": "sha512-zwQtbrPEBDj+ApELZ6QylLf2/c5zmASoOuA4DzolyVGdV38iR2I5QRMsZcHkcdkZzxpN8RS4cN7LPskOkTwTZw==", + "dependencies": { + "browserslist": "^4.22.2", + "cssnano-utils": "^4.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.2.tgz", + "integrity": "sha512-0b+m+w7OAvZejPQdN2GjsXLv5o0jqYHX3aoV0e7RBKPCsB7TYG5KKWBFhGnB/iP3213Ts8c5H4wLPLMm7z28Sg==", + "dependencies": { + "postcss-selector-parser": "^6.0.15" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.1.tgz", + "integrity": "sha512-aW5LbMNRZ+oDV57PF9K+WI1Z8MPnF+A8qbajg/T8PP126YrGX1f9IQx21GI2OlGz7XFJi/fNi0GTbY948XJtXg==", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.1.tgz", + "integrity": "sha512-mc3vxp2bEuCb4LgCcmG1y6lKJu1Co8T+rKHrcbShJwUmKJiEl761qb/QQCfFwlrvSeET3jksolCR/RZuMURudw==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.1.tgz", + "integrity": "sha512-HRsq8u/0unKNvm0cvwxcOUEcakFXqZ41fv3FOdPn916XFUrympjr+03oaLkuZENz3HE9RrQE9yU0Xv43ThWjQg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.1.tgz", + "integrity": "sha512-Gbb2nmCy6tTiA7Sh2MBs3fj9W8swonk6lw+dFFeQT68B0Pzwp1kvisJQkdV6rbbMSd9brMlS8I8ts52tAGWmGQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-string": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.1.tgz", + "integrity": "sha512-5Fhx/+xzALJD9EI26Aq23hXwmv97Zfy2VFrt5PLT8lAhnBIZvmaT5pQk+NuJ/GWj/QWaKSKbnoKDGLbV6qnhXg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.1.tgz", + "integrity": "sha512-4zcczzHqmCU7L5dqTB9rzeqPWRMc0K2HoR+Bfl+FSMbqGBUcP5LRfgcH4BdRtLuzVQK1/FHdFoGT3F7rkEnY+g==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.0.2.tgz", + "integrity": "sha512-Ff2VdAYCTGyMUwpevTZPZ4w0+mPjbZzLLyoLh/RMpqUqeQKZ+xMm31hkxBavDcGKcxm6ACzGk0nBfZ8LZkStKA==", + "dependencies": { + "browserslist": "^4.22.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-url": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.1.tgz", + "integrity": "sha512-jEXL15tXSvbjm0yzUV7FBiEXwhIa9H88JOXDGQzmcWoB4mSjZIsmtto066s2iW9FYuIrIF4k04HA2BKAOpbsaQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.1.tgz", + "integrity": "sha512-76i3NpWf6bB8UHlVuLRxG4zW2YykF9CTEcq/9LGAiz2qBuX5cBStadkk0jSkg9a9TCIXbMQz7yzrygKoCW9JuA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-ordered-values": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.1.tgz", + "integrity": "sha512-XXbb1O/MW9HdEhnBxitZpPFbIvDgbo9NK4c/5bOfiKpnIGZDoL2xd7/e6jW5DYLsWxBbs+1nZEnVgnjnlFViaA==", + "dependencies": { + "cssnano-utils": "^4.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.2.tgz", + "integrity": "sha512-YGKalhNlCLcjcLvjU5nF8FyeCTkCO5UtvJEt0hrPZVCTtRLSOH4z00T1UntQPj4dUmIYZgMj8qK77JbSX95hSw==", + "dependencies": { + "browserslist": "^4.22.2", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.1.tgz", + "integrity": "sha512-fUbV81OkUe75JM+VYO1gr/IoA2b/dRiH6HvMwhrIBSUrxq3jNZQZitSnugcTLDi1KkQh1eR/zi+iyxviUNBkcQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.15", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", + "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.2.tgz", + "integrity": "sha512-IH5R9SjkTkh0kfFOQDImyy1+mTCb+E830+9SV1O+AaDcoHTvfsvt6WwJeo7KwcHbFnevZVCsXhDmjFiGVuwqFQ==", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^3.2.0" + }, + "engines": { + "node": "^14 || ^16 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.2.tgz", + "integrity": "sha512-8IZGQ94nechdG7Y9Sh9FlIY2b4uS8/k8kdKRX040XHsS3B6d1HrJAkXrBSsSu4SuARruSsUjW3nlSw8BHkaAYQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.15" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/stylehacks": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.0.2.tgz", + "integrity": "sha512-00zvJGnCu64EpMjX8b5iCZ3us2Ptyw8+toEkb92VdmkEaRaSGBNKAoK6aWZckhXxmQP8zWiTaFaiMGIU8Ve8sg==", + "dependencies": { + "browserslist": "^4.22.2", + "postcss-selector-parser": "^6.0.15" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/svgo": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz", + "integrity": "sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/terser": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", + "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bfdb1d5 --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "@swc/core": "^1.3.104", + "autoprefixer": "^10.4.17", + "cssnano": "^6.0.3", + "html-minifier-terser": "^7.2.0", + "postcss": "^8.4.33", + "terser": "^5.27.0" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..62a03e7 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/icons/128x.png b/public/icons/128x.png new file mode 100644 index 0000000..89c8d82 Binary files /dev/null and b/public/icons/128x.png differ diff --git a/public/icons/16x.png b/public/icons/16x.png new file mode 100644 index 0000000..02755b3 Binary files /dev/null and b/public/icons/16x.png differ diff --git a/public/icons/256x.png b/public/icons/256x.png new file mode 100644 index 0000000..0a07b3a Binary files /dev/null and b/public/icons/256x.png differ diff --git a/public/icons/32x.png b/public/icons/32x.png new file mode 100644 index 0000000..18f71bd Binary files /dev/null and b/public/icons/32x.png differ diff --git a/public/icons/48x.png b/public/icons/48x.png new file mode 100644 index 0000000..acfdb63 Binary files /dev/null and b/public/icons/48x.png differ diff --git a/public/icons/512x.png b/public/icons/512x.png new file mode 100644 index 0000000..7f1c5b9 Binary files /dev/null and b/public/icons/512x.png differ diff --git a/public/icons/64x.png b/public/icons/64x.png new file mode 100644 index 0000000..6c69ebe Binary files /dev/null and b/public/icons/64x.png differ diff --git a/public/icons/90x.png b/public/icons/90x.png new file mode 100644 index 0000000..498f535 Binary files /dev/null and b/public/icons/90x.png differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..ac0178d --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,45 @@ +{ + "name": "Flashii Chat", + "short_name": "Flashii Chat", + "description": "Talk about trash and trash accessories.", + "start_url": "/", + "display": "standalone", + "theme_color": "#8559a5", + "background_color": "#222", + "scope": "/", + "offline_enabled": true, + "icons": [ + { + "src": "/icons/512x.png", + "sizes": "512x512" + }, + { + "src": "/icons/256x.png", + "sizes": "256x256" + }, + { + "src": "/icons/128x.png", + "sizes": "128x128" + }, + { + "src": "/icons/90x.png", + "sizes": "90x90" + }, + { + "src": "/icons/64x.png", + "sizes": "64x64" + }, + { + "src": "/icons/48x.png", + "sizes": "48x48" + }, + { + "src": "/icons/32x.png", + "sizes": "32x32" + }, + { + "src": "/icons/16x.png", + "sizes": "16x16" + } + ] +} diff --git a/public/picker.css b/public/picker.css new file mode 100644 index 0000000..6b14151 --- /dev/null +++ b/public/picker.css @@ -0,0 +1,261 @@ +.fw-colour-picker { + border-radius: 5px; + border: 2px solid #123456; + padding: 3px; + display: flex; + position: absolute; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + background-color: #444; + color: #fff; + box-shadow: 0 3px 10px #000; + font-family: Verdana, Geneva, Arial, Helvetica, sans-serif; + font-size: 12px; + line-height: 20px; + width: 290px; + transition: width .2s, border-color .2s; +} + +.fw-colour-picker-form { + width: 100%; +} + +.fw-colour-picker-tabbed { + width: 100%; + margin-bottom: 3px; +} +.fw-colour-picker-tabbed-container { + border: 2px solid #123456; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + border-radius: 5px 5px 0 0; + height: 234px; + overflow: auto; + transition: border-color .2s; +} +.fw-colour-picker-tabbed-list { + background-color: #222; + border-radius: 0 0 5px 5px; + overflow: auto; +} + +.fw-colour-picker-tab-button { + background: #333; + color: #fff; + border-radius: 0; + border-width: 0; + display: inline-block; + padding: 3px 5px; + height: 24px; + margin-right: 1px; + transition: background .2s, border-color .2s; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + cursor: pointer; +} +.fw-colour-picker-tab-button:hover { + background: #444; + border-color: #444; +} +.fw-colour-picker-tab-button:focus { + border-color: #fff; +} +.fw-colour-picker-tab-button-active { + background: #123456; + border-color: #123456; + color: #123456; +} + +.fw-colour-picker-middle-row { + width: 100%; + margin-bottom: 3px; + height: 60px; +} + +.fw-colour-picker-colour-preview-container { + display: inline-block; + width: 60px; + height: 60px; + vertical-align: middle; +} +.fw-colour-picker-colour-preview { + display: block; + width: 60px; + height: 60px; + border-radius: 5px; + background: #123456; +} + +.fw-colour-picker-values-container { + display: inline-block; + vertical-align: middle; +} + +.fw-colour-picker-values-child { + display: block; + padding: 2px 3px; + margin: 1px; + margin-left: 31px; + cursor: text; +} +.fw-colour-picker-values-child-label { + font-weight: 700; + width: 40px; + display: inline-block; + margin-right: 6px; + text-align: right; +} +.fw-colour-picker-values-child-input { + display: inline-block; + border: 1px solid #222; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + background: #333; + border-radius: 0; + color: #fff; + padding: 1px; + width: 100px; + transition: border-color .2s; + outline-style: none; +} +.fw-colour-picker-values-child-input:focus { + border-color: #777; +} + +.fw-colour-picker-buttons-container { + text-align: right; + margin: 1px; +} +.fw-colour-picker-buttons-container-inner { + text-align: left; + display: inline-block; +} + +.fw-colour-picker-buttons-button { + border-radius: 5px; + background: #222; + border-width: 0; + color: #fff; + font-size: 1.1em; + line-height: 1.2em; + padding: 3px 10px; + margin-left: 5px; + cursor: pointer; +} +.fw-colour-picker-buttons-button:focus { + box-shadow: 0 0 0 1px #000, inset 0 0 0 1px #fff; +} +.fw-colour-picker-buttons-button-submit { + font-weight: 700; + color: #123456; + background: #123456; + transition: background .2s; +} + +.fw-colour-picker-tab-container { + display: none; +} +.fw-colour-picker-tab-container-active { + display: block; +} + +.fw-colour-picker-tab-presets-container { + text-align: center; + padding-top: 3px; + padding-left: 3px; +} +.fw-colour-picker-presets-option { + display: inline-block; + vertical-align: middle; + width: 40px; + height: 40px; + margin-right: 3px; + margin-bottom: 3px; + border-radius: 5px; + text-decoration: none; + color: #fff; +} +.fw-colour-picker-presets-option:focus { + box-shadow: 0 0 0 1px #fff, 0 0 0 2px #000, inset 0 0 0 1px #000; +} +.fw-colour-picker-presets-option-active { + box-shadow: 0 0 0 1px #000, inset 0 0 0 1px #fff; +} + +.fw-colour-picker-tab-grid-container { + text-align: center; +} +.fw-colour-picker-grid-option { + display: inline-block; + vertical-align: middle; + width: 23px; + height: 23px; + z-index: 1; + position: relative; + cursor: cell; +} +.fw-colour-picker-grid-option:focus { + box-shadow: 0 0 0 1px #fff, 0 0 0 2px #000, inset 0 0 0 1px #000; + z-index: 3; +} +.fw-colour-picker-grid-option-active { + box-shadow: 0 0 0 1px #000, inset 0 0 0 1px #fff; + z-index: 2; +} + +.fw-colour-picker-slider { + margin: 0 2px; +} +.fw-colour-picker-slider-gradient { + width: 100%; + height: 6px; +} +.fw-colour-picker-slider-name { + font-weight: 700; + margin: 0 2px; +} +.fw-colour-picker-slider-value-slider-container { + display: inline-block; + width: 200px; + margin: 4px; +} +.fw-colour-picker-slider-value-slider { + width: 100%; + margin: 0; +} +.fw-colour-picker-slider-value-input-container { + display: inline-block; + vertical-align: top; + text-align: center; + width: 64px; +} +.fw-colour-picker-slider-value-input { + display: inline-block; + border: 1px solid #222; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + background: #333; + border-radius: 0; + color: #fff; + padding: 1px; + width: 50px; + transition: border-color .2s; + outline-style: none; +} +.fw-colour-picker-slider-value-input:focus { + border-color: #777; +} +.fw-colour-picker-slider-red .fw-colour-picker-slider-value-input:focus { + border-color: #f00; +} +.fw-colour-picker-slider-green .fw-colour-picker-slider-value-input:focus { + border-color: #0f0; +} +.fw-colour-picker-slider-blue .fw-colour-picker-slider-value-input:focus { + border-color: #00f; +} diff --git a/public/picker.js b/public/picker.js new file mode 100644 index 0000000..1f81e85 --- /dev/null +++ b/public/picker.js @@ -0,0 +1,505 @@ +var FwColourPicker = function(callback, options, colour, onClose) { + if(typeof callback !== 'function') + return; + if(typeof colour !== 'number') + colour = parseInt(colour || 0); + if(typeof options !== 'object') + options = {}; + + var readThres = 168, + lumiRed = .299, + lumiGreen = .587, + lumiBlue = .114; + + var extractRGB = function(raw) { + return [ + (raw >> 16) & 0xFF, + (raw >> 8) & 0xFF, + raw & 0xFF, + ]; + }; + + var calcLumi = function(raw) { + var rgb = extractRGB(raw); + return rgb[0] * lumiRed + + rgb[1] * lumiGreen + + rgb[2] * lumiBlue; + }; + + var textColour = function(raw) { + return calcLumi(raw) > readThres ? 0 : 0xFFFFFF; + }; + + var weightNum = function(n1, n2, w) { + w = Math.min(1, Math.max(0, w)); + return Math.round((n1 * w) + (n2 * (1 - w))); + }; + + var weightColour = function(c1, c2, w) { + c1 = extractRGB(c1); + c2 = extractRGB(c2); + return (weightNum(c1[0], c2[0], w) << 16) + | (weightNum(c1[1], c2[1], w) << 8) + | weightNum(c1[2], c2[2], w); + }; + + var shadeColour = function(raw, offset) { + if(offset == 0) + return raw; + + var dir = 0xFFFFFF; + if(offset < 0) { + dir = 0; + offset *= -1; + } + + return weightColour(dir, raw, offset); + }; + + var hexFormat = FwColourPicker.hexFormat; + + var verifyOption = function(name, type, def) { + if(typeof options[name] !== type) + options[name] = def; + }; + + verifyOption('posX', 'number', -1); + verifyOption('posY', 'number', -1); + verifyOption('presets', 'object', []); + verifyOption('showPresetsTab', 'boolean', options.presets.length > 0); + verifyOption('showGridTab', 'boolean', true); + verifyOption('showSlidersTab', 'boolean', true); + verifyOption('showHexValue', 'boolean', true); + verifyOption('showRawValue', 'boolean', true); + verifyOption('autoClose', 'boolean', true); + + var onColourChange = []; + var runOnColourChange = function() { + var text = textColour(colour); + for(var i = 0; i < onColourChange.length; ++i) + onColourChange[i](colour, text); + }; + + var setColour = function(raw) { + colour = parseInt(raw || 0) & 0xFFFFFF; + runOnColourChange(); + }; + + var apply = function() { + callback(pub, colour); + if(options.autoClose) + close(); + }; + + var cancel = function() { + callback(pub, null); + if(options.autoClose) + close(); + }; + + var close = function() { + if(onClose && onClose()) + return; + container.parentNode.removeChild(container); + }; + + var height = 96; + + var container = document.createElement('div'); + container.className = 'fw-colour-picker'; + onColourChange.push(function(colour) { + container.style.borderColor = hexFormat(colour); + }); + + var form = document.createElement('form'); + form.className = 'fw-colour-picker-form'; + container.appendChild(form); + + form.onsubmit = function(ev) { + ev.preventDefault(); + apply(); + return false; + }; + + var tabs = {}; + var activeTab = undefined; + onColourChange.push(function(colour, text) { + if(activeTab) { + activeTab.b.style.background = hexFormat(colour); + activeTab.b.style.borderColor = hexFormat(colour); + activeTab.b.style.color = hexFormat(text); + } + }); + + var tabbed = document.createElement('div'); + tabbed.className = 'fw-colour-picker-tabbed'; + + var tabbedContainer = document.createElement('div'); + tabbedContainer.className = 'fw-colour-picker-tabbed-container'; + tabbed.appendChild(tabbedContainer); + onColourChange.push(function(colour) { + tabbedContainer.style.borderColor = hexFormat(colour); + }); + + var tabbedList = document.createElement('div'); + tabbedList.className = 'fw-colour-picker-tabbed-list'; + tabbed.appendChild(tabbedList); + + var switchTab = function(id) { + if(activeTab == tabs[id]) + return; + + if(activeTab) { + activeTab.c.classList.remove('fw-colour-picker-tab-container-active'); + activeTab.b.classList.remove('fw-colour-picker-tab-button-active'); + activeTab.b.style.background = ''; + activeTab.b.style.borderColor = ''; + activeTab.b.style.color = ''; + } + + activeTab = tabs[id] || undefined; + + if(activeTab) { + activeTab.c.classList.add('fw-colour-picker-tab-container-active'); + activeTab.b.classList.add('fw-colour-picker-tab-button-active'); + activeTab.b.style.background = hexFormat(colour); + activeTab.b.style.borderColor = hexFormat(colour); + activeTab.b.style.color = hexFormat(textColour(colour)); + } + }; + + var createTab = function(id, name, construct) { + var tabContainer = construct(); + tabContainer.className = 'fw-colour-picker-tab-container fw-colour-picker-tab-' + id + '-container'; + tabbedContainer.appendChild(tabContainer); + + var tabButton = document.createElement('input'); + tabButton.type = 'button'; + tabButton.value = name; + tabButton.className = 'fw-colour-picker-tab-button fw-colour-picker-tab-' + id + '-button'; + tabButton.onclick = function() { switchTab(id); }; + tabbedList.appendChild(tabButton); + + tabs[id] = { c: tabContainer, b: tabButton }; + + if(activeTab === undefined) { + activeTab = tabs[id]; + tabContainer.className += ' fw-colour-picker-tab-container-active'; + tabButton.className += ' fw-colour-picker-tab-button-active'; + } + }; + + if(options.showPresetsTab) + createTab('presets', 'Presets', function() { + var presets = options.presets; + var cont = document.createElement('div'); + + for(var i = 0; i < presets.length; ++i) + (function(preset) { + var option = document.createElement('a'); + option.href = 'javascript:void(0);'; + option.className = 'fw-colour-picker-presets-option'; + option.style.background = hexFormat(preset.c); + option.title = preset.n; + option.onclick = function() { + setColour(preset.c); + }; + onColourChange.push(function(value) { + option.classList[(value === preset.c ? 'add' : 'remove')]('fw-colour-picker-presets-option-active'); + }); + cont.appendChild(option); + })(presets[i]); + + return cont; + }); + + if(options.showGridTab) + createTab('grid', 'Grid', function() { + var greys = [0xFFFFFF, 0xEBEBEB, 0xD6D6D6, 0xC2C2C2, 0xADADAD, 0x999999, 0x858585, 0x707070, 0x5C5C5C, 0x474747, 0x333333, 0]; + var colours = [0x00A1D8, 0x0061FE, 0x4D22B2, 0x982ABC, 0xB92D5D, 0xFF4015, 0xFF6A00, 0xFFAB01, 0xFDC700, 0xFEFB41, 0xD9EC37, 0x76BB40]; + var shades = [-.675, -.499, -.345, -.134, 0, .134, .345, .499, .675]; + + var cont = document.createElement('div'); + + for(var i = 0; i < greys.length; ++i) + (function(grey) { + var option = document.createElement('a'); + option.href = 'javascript:void(0);'; + option.className = 'fw-colour-picker-grid-option'; + option.style.background = hexFormat(grey); + option.onclick = function() { + setColour(grey); + }; + onColourChange.push(function(value) { + option.classList[(value === grey ? 'add' : 'remove')]('fw-colour-picker-grid-option-active'); + }); + cont.appendChild(option); + })(greys[i]); + + for(var i = 0; i < shades.length; ++i) + for(var j = 0; j < colours.length; ++j) + (function(colour) { + var option = document.createElement('a'); + option.href = 'javascript:void(0);'; + option.className = 'fw-colour-picker-grid-option'; + option.style.background = hexFormat(colour); + option.onclick = function() { + setColour(colour); + }; + onColourChange.push(function(value) { + option.classList[(value === colour ? 'add' : 'remove')]('fw-colour-picker-grid-option-active'); + }); + cont.appendChild(option); + })(shadeColour(colours[j], shades[i])); + + return cont; + }); + + if(options.showSlidersTab) + createTab('sliders', 'Sliders', function() { + var cont = document.createElement('div'); + + var addSlider = function(id, name, update, apply) { + var sCont = document.createElement('div'); + sCont.className = 'fw-colour-picker-slider fw-colour-picker-slider-' + id; + cont.appendChild(sCont); + + var sName = document.createElement('div'); + sName.className = 'fw-colour-picker-slider-name'; + sName.textContent = name; + sCont.appendChild(sName); + + var sValue = document.createElement('div'); + sValue.className = 'fw-colour-picker-slider-value'; + sCont.appendChild(sValue); + + var sValueSliderCont = document.createElement('div'); + sValueSliderCont.className = 'fw-colour-picker-slider-value-slider-container'; + sValue.appendChild(sValueSliderCont); + + var sGradient = document.createElement('div'); + sGradient.className = 'fw-colour-picker-slider-gradient'; + sValueSliderCont.appendChild(sGradient); + + var sValueSlider = document.createElement('input'); + sValueSlider.type = 'range'; + if(sValueSlider.type === 'range') { + sValueSlider.className = 'fw-colour-picker-slider-value-slider'; + sValueSlider.min = '0'; + sValueSlider.max = '255'; + sValueSlider.onchange = function() { + setColour(apply(colour, sValueSlider.value)); + }; + sValueSliderCont.appendChild(sValueSlider); + } else + sValueSlider = undefined; + + var sValueInputContainer = document.createElement('div'); + sValueInputContainer.className = 'fw-colour-picker-slider-value-input-container'; + sValue.appendChild(sValueInputContainer); + + var sValueInput = document.createElement('input'); + sValueInput.className = 'fw-colour-picker-slider-value-input'; + sValueInput.type = 'number'; + sValueInput.min = '0'; + sValueInput.max = '255'; + sValueInput.onchange = function() { + setColour(apply(colour, sValueInput.value)); + }; + sValueInputContainer.appendChild(sValueInput); + + sGradient.onmousedown = function(ev) { + if(ev.button === 0) + setColour(apply(colour, Math.floor(255 * (Math.max(0, Math.min(200, (ev.layerX - 5))) / 200)))); + }; + + onColourChange.push(function(colour) { + if(sValueSlider) + sValueSlider.value = update(colour); + sValueInput.value = update(colour); + + var gradient = 'linear-gradient(to right, ' + hexFormat(apply(colour, 0)) + ', ' + hexFormat(apply(colour, 0xFF)) + ')'; + sGradient.style.background = ''; + sGradient.style.background = gradient; + if(!sGradient.style.background) + sGradient.style.background = '-moz-' + gradient; + if(!sGradient.style.background) + sGradient.style.background = '-webkit-' + gradient; + }); + }; + + addSlider('red', 'Red', + function(value) { return (value >> 16) & 0xFF; }, + function(colour, value) { return (colour & 0xFFFF) | (Math.min(255, Math.max(0, value)) << 16); } + ); + addSlider('green', 'Green', + function(value) { return (value >> 8) & 0xFF; }, + function(colour, value) { return (colour & 0xFF00FF) | (Math.min(255, Math.max(0, value)) << 8); } + ); + addSlider('blue', 'Blue', + function(value) { return value & 0xFF; }, + function(colour, value) { return (colour & 0xFFFF00) | Math.min(255, Math.max(0, value)); } + ); + + return cont; + }); + + if(activeTab) { + height += 261; + form.appendChild(tabbed); + } + + var middleRow = document.createElement('div'); + middleRow.className = 'fw-colour-picker-middle-row'; + form.appendChild(middleRow); + + var colourPreviewContainer = document.createElement('div'); + colourPreviewContainer.className = 'fw-colour-picker-colour-preview-container'; + middleRow.appendChild(colourPreviewContainer); + + var colourPreview = document.createElement('div'); + colourPreview.className = 'fw-colour-picker-colour-preview'; + colourPreviewContainer.appendChild(colourPreview); + onColourChange.push(function(colour) { + colourPreview.style.background = hexFormat(colour); + }); + + var values = {}; + var valuesContainer = document.createElement('div'); + valuesContainer.className = 'fw-colour-picker-values-container'; + middleRow.appendChild(valuesContainer); + + var addValue = function(id, name, type, format, change) { + var valueContainer = document.createElement('label'); + valueContainer.className = 'fw-colour-picker-values-child fw-colour-picker-' + id + '-value'; + valuesContainer.appendChild(valueContainer); + + var valueLabel = document.createElement('div'); + valueLabel.textContent = name; + valueLabel.className = 'fw-colour-picker-values-child-label fw-colour-picker-' + id + '-value-label'; + valueContainer.appendChild(valueLabel); + + var valueInput = document.createElement('input'); + valueInput.type = type; + valueInput.value = '0'; + valueInput.className = 'fw-colour-picker-values-child-input fw-colour-picker-' + id + '-value-input'; + valueInput.onchange = function() { + change(valueInput.value); + }; + valueContainer.appendChild(valueInput); + + onColourChange.push(function(colour) { + valueInput.value = format(colour); + }); + + values[id] = { c: valueContainer, l: valueLabel, i: valueInput }; + }; + + if(options.showHexValue) + addValue('hex', 'Hex', 'text', function(value) { + return hexFormat(value); + }, function(value) { + while(value.substring(0, 1) === '#') + value = value.substring(1); + value = value.substring(0, 6); + + if(value.length === 3) + value = value.substring(0, 1) + value.substring(0, 1) + + value.substring(1, 2) + value.substring(1, 2) + + value.substring(2, 3) + value.substring(2, 3); + + if(value.length === 6) + setColour(parseInt(value, 16)); + }); + + if(options.showRawValue) + addValue('raw', 'Raw', 'number', function(value) { + return value; + }, function(value) { + setColour(Math.min(0xFFFFFF, Math.max(0, parseInt(value)))); + }); + + var buttons = {}; + var buttonsContainer = document.createElement('div'); + buttonsContainer.className = 'fw-colour-picker-buttons-container'; + form.appendChild(buttonsContainer); + + var buttonsContainerInner = document.createElement('div'); + buttonsContainerInner.className = 'fw-colour-picker-buttons-container-inner'; + buttonsContainer.appendChild(buttonsContainerInner); + + var addButton = function(id, name, action) { + var button = document.createElement('input'); + button.className = 'fw-colour-picker-buttons-button fw-colour-picker-buttons-' + id + '-button'; + if(action === null) { + button.className += ' fw-colour-picker-buttons-button-submit'; + onColourChange.push(function(colour, text) { + button.style.background = hexFormat(colour); + button.style.color = hexFormat(text); + }); + button.type = 'submit'; + } else { + button.onclick = function() { action(); }; + button.type = 'button'; + } + button.value = name; + buttonsContainerInner.appendChild(button); + buttons[id] = { b: button }; + }; + + addButton('cancel', 'Cancel', cancel); + addButton('apply', 'Apply', null); + + var setPosition = function(x, y) { + container.style.top = y >= 0 ? (y.toString() + 'px') : ''; + container.style.left = x >= 0 ? (x.toString() + 'px') : ''; + }; + + setPosition(options.posX, options.posY); + runOnColourChange(); + + var appendTo = function(parent) { + parent.appendChild(container); + }; + + var pub = { + getWidth: function() { return 290; }, + getHeight: function() { return height; }, + getColour: function() { return colour; }, + getContainer: function() { return container; }, + setColour: setColour, + appendTo: appendTo, + setPosition: setPosition, + switchTab: switchTab, + close: close, + suggestPosition: function(mouseEvent) { + var x = 10, y = 10; + + if(document.body.clientWidth > 340) { + x = mouseEvent.clientX; + y = mouseEvent.clientY; + + var height = pub.getHeight(), + width = pub.getWidth(); + + if(y > height + 20) + y -= height; + if(x > document.body.clientWidth - width - 20) + x -= width; + } + + return { + x: x, + y: y, + }; + }, + }; + + return pub; +}; +FwColourPicker.hexFormat = function(raw) { + var str = raw.toString(16).substring(0, 6); + if(str.length < 6) + str = '000000'.substring(str.length) + str; + return '#' + str; +}; diff --git a/public/vendor/fontawesome/LICENSE.txt b/public/vendor/fontawesome/LICENSE.txt new file mode 100644 index 0000000..f31bef9 --- /dev/null +++ b/public/vendor/fontawesome/LICENSE.txt @@ -0,0 +1,34 @@ +Font Awesome Free License +------------------------- + +Font Awesome Free is free, open source, and GPL friendly. You can use it for +commercial projects, open source projects, or really almost whatever you want. +Full Font Awesome Free license: https://fontawesome.com/license/free. + +# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) +In the Font Awesome Free download, the CC BY 4.0 license applies to all icons +packaged as SVG and JS file types. + +# Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) +In the Font Awesome Free download, the SIL OFL license applies to all icons +packaged as web and desktop font files. + +# Code: MIT License (https://opensource.org/licenses/MIT) +In the Font Awesome Free download, the MIT license applies to all non-font and +non-icon files. + +# Attribution +Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font +Awesome Free files already contain embedded comments with sufficient +attribution, so you shouldn't need to do anything additional when using these +files normally. + +We've kept attribution comments terse, so we ask that you do not actively work +to remove them from files, especially code. They're a great way for folks to +learn about Font Awesome. + +# Brand Icons +All brand icons are trademarks of their respective owners. The use of these +trademarks does not indicate endorsement of the trademark holder by Font +Awesome, nor vice versa. **Please do not use brand logos for any purpose except +to represent the company, product, or service to which they refer.** diff --git a/public/vendor/fontawesome/css/all.min.css b/public/vendor/fontawesome/css/all.min.css new file mode 100644 index 0000000..3d28ab2 --- /dev/null +++ b/public/vendor/fontawesome/css/all.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-bahai:before{content:"\f666"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-box-tissue:before{content:"\f95b"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buy-n-large:before{content:"\f8a6"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caravan:before{content:"\f8ff"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-alt:before{content:"\f422"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dailymotion:before{content:"\f952"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-disease:before{content:"\f7fa"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-alt:before{content:"\f424"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-faucet:before{content:"\f905"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-firefox-browser:before{content:"\f907"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-medical:before{content:"\f95c"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-holding-water:before{content:"\f4c1"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-sparkles:before{content:"\f95d"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-hands-wash:before{content:"\f95e"}.fa-handshake:before{content:"\f2b5"}.fa-handshake-alt-slash:before{content:"\f95f"}.fa-handshake-slash:before{content:"\f960"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-hat-wizard:before{content:"\f6e8"}.fa-hdd:before{content:"\f0a0"}.fa-head-side-cough:before{content:"\f961"}.fa-head-side-cough-slash:before{content:"\f962"}.fa-head-side-mask:before{content:"\f963"}.fa-head-side-virus:before{content:"\f964"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hospital-user:before{content:"\f80d"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-house-user:before{content:"\f965"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-ideal:before{content:"\f913"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-instagram-square:before{content:"\f955"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-house:before{content:"\f966"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lungs:before{content:"\f604"}.fa-lungs-virus:before{content:"\f967"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-mdb:before{content:"\f8ca"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microblog:before{content:"\f91a"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mixer:before{content:"\f956"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse:before{content:"\f8cc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-orcid:before{content:"\f8d2"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-arrows:before{content:"\f968"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-square:before{content:"\f91e"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-plane-slash:before{content:"\f969"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pump-medical:before{content:"\f96a"}.fa-pump-soap:before{content:"\f96b"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-record-vinyl:before{content:"\f8d9"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-shield-virus:before{content:"\f96c"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopify:before{content:"\f957"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-soap:before{content:"\f96e"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-stopwatch-20:before{content:"\f96f"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-store-alt-slash:before{content:"\f970"}.fa-store-slash:before{content:"\f971"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swift:before{content:"\f8e1"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toilet-paper-slash:before{content:"\f972"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-trailer:before{content:"\f941"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbraco:before{content:"\f8e8"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-unity:before{content:"\f949"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-virus:before{content:"\f974"}.fa-virus-slash:before{content:"\f975"}.fa-viruses:before{content:"\f976"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.fab,.far{font-weight:400}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.far,.fas{font-family:"Font Awesome 5 Free"}.fa,.fas{font-weight:900} \ No newline at end of file diff --git a/public/vendor/fontawesome/webfonts/fa-brands-400.eot b/public/vendor/fontawesome/webfonts/fa-brands-400.eot new file mode 100644 index 0000000..a1bc094 Binary files /dev/null and b/public/vendor/fontawesome/webfonts/fa-brands-400.eot differ diff --git a/public/vendor/fontawesome/webfonts/fa-brands-400.svg b/public/vendor/fontawesome/webfonts/fa-brands-400.svg new file mode 100644 index 0000000..46ad237 --- /dev/null +++ b/public/vendor/fontawesome/webfonts/fa-brands-400.svg @@ -0,0 +1,3570 @@ + + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/vendor/fontawesome/webfonts/fa-brands-400.ttf b/public/vendor/fontawesome/webfonts/fa-brands-400.ttf new file mode 100644 index 0000000..948a2a6 Binary files /dev/null and b/public/vendor/fontawesome/webfonts/fa-brands-400.ttf differ diff --git a/public/vendor/fontawesome/webfonts/fa-brands-400.woff b/public/vendor/fontawesome/webfonts/fa-brands-400.woff new file mode 100644 index 0000000..2a89d52 Binary files /dev/null and b/public/vendor/fontawesome/webfonts/fa-brands-400.woff differ diff --git a/public/vendor/fontawesome/webfonts/fa-brands-400.woff2 b/public/vendor/fontawesome/webfonts/fa-brands-400.woff2 new file mode 100644 index 0000000..141a90a Binary files /dev/null and b/public/vendor/fontawesome/webfonts/fa-brands-400.woff2 differ diff --git a/public/vendor/fontawesome/webfonts/fa-regular-400.eot b/public/vendor/fontawesome/webfonts/fa-regular-400.eot new file mode 100644 index 0000000..38cf251 Binary files /dev/null and b/public/vendor/fontawesome/webfonts/fa-regular-400.eot differ diff --git a/public/vendor/fontawesome/webfonts/fa-regular-400.svg b/public/vendor/fontawesome/webfonts/fa-regular-400.svg new file mode 100644 index 0000000..48634a9 --- /dev/null +++ b/public/vendor/fontawesome/webfonts/fa-regular-400.svg @@ -0,0 +1,803 @@ + + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/vendor/fontawesome/webfonts/fa-regular-400.ttf b/public/vendor/fontawesome/webfonts/fa-regular-400.ttf new file mode 100644 index 0000000..abe99e2 Binary files /dev/null and b/public/vendor/fontawesome/webfonts/fa-regular-400.ttf differ diff --git a/public/vendor/fontawesome/webfonts/fa-regular-400.woff b/public/vendor/fontawesome/webfonts/fa-regular-400.woff new file mode 100644 index 0000000..24de566 Binary files /dev/null and b/public/vendor/fontawesome/webfonts/fa-regular-400.woff differ diff --git a/public/vendor/fontawesome/webfonts/fa-regular-400.woff2 b/public/vendor/fontawesome/webfonts/fa-regular-400.woff2 new file mode 100644 index 0000000..7e0118e Binary files /dev/null and b/public/vendor/fontawesome/webfonts/fa-regular-400.woff2 differ diff --git a/public/vendor/fontawesome/webfonts/fa-solid-900.eot b/public/vendor/fontawesome/webfonts/fa-solid-900.eot new file mode 100644 index 0000000..d3b77c2 Binary files /dev/null and b/public/vendor/fontawesome/webfonts/fa-solid-900.eot differ diff --git a/public/vendor/fontawesome/webfonts/fa-solid-900.svg b/public/vendor/fontawesome/webfonts/fa-solid-900.svg new file mode 100644 index 0000000..7742838 --- /dev/null +++ b/public/vendor/fontawesome/webfonts/fa-solid-900.svg @@ -0,0 +1,4938 @@ + + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/vendor/fontawesome/webfonts/fa-solid-900.ttf b/public/vendor/fontawesome/webfonts/fa-solid-900.ttf new file mode 100644 index 0000000..5b97903 Binary files /dev/null and b/public/vendor/fontawesome/webfonts/fa-solid-900.ttf differ diff --git a/public/vendor/fontawesome/webfonts/fa-solid-900.woff b/public/vendor/fontawesome/webfonts/fa-solid-900.woff new file mode 100644 index 0000000..beec791 Binary files /dev/null and b/public/vendor/fontawesome/webfonts/fa-solid-900.woff differ diff --git a/public/vendor/fontawesome/webfonts/fa-solid-900.woff2 b/public/vendor/fontawesome/webfonts/fa-solid-900.woff2 new file mode 100644 index 0000000..978a681 Binary files /dev/null and b/public/vendor/fontawesome/webfonts/fa-solid-900.woff2 differ diff --git a/src/assproc.js b/src/assproc.js new file mode 100644 index 0000000..8fb7dec --- /dev/null +++ b/src/assproc.js @@ -0,0 +1,108 @@ +const fs = require('fs'); +const path = require('path'); +const readline = require('readline'); +const utils = require('./utils.js'); + +exports.process = async function(root, options) { + const macroPrefix = options.prefix || '#'; + const entryPoint = options.entry || ''; + + root = fs.realpathSync(root); + + const included = []; + + const processFile = async function(fileName) { + const fullPath = path.join(root, fileName); + if(included.includes(fullPath)) + return ''; + included.push(fullPath); + + if(!fullPath.startsWith(root)) + return '/* *** INVALID PATH: ' + fullPath + ' */'; + if(!fs.existsSync(fullPath)) + return '/* *** FILE NOT FOUND: ' + fullPath + ' */'; + + const lines = readline.createInterface({ + input: fs.createReadStream(fullPath), + crlfDelay: Infinity, + }); + + let output = ''; + let lastWasEmpty = false; + + if(options.showPath) + output += "/* *** PATH: " + fullPath + " */\n"; + + for await(const line of lines) { + const lineTrimmed = utils.trim(line); + if(lineTrimmed === '') + continue; + + if(line.startsWith(macroPrefix)) { + const args = lineTrimmed.split(' '); + const macro = utils.trim(utils.trimStart(args.shift(), macroPrefix)); + + switch(macro) { + case 'comment': + break; + + case 'include': { + const includePath = utils.trimEnd(args.join(' '), ';'); + output += utils.trim(await processFile(includePath)); + output += "\n"; + break; + } + + case 'buildvars': + if(typeof options.buildVars === 'object') { + const bvTarget = options.buildVarsTarget || 'window'; + const bvProps = []; + + for(const bvName in options.buildVars) + bvProps.push(`${bvName}: { value: ${JSON.stringify(options.buildVars[bvName])}, writable: false }`); + + if(Object.keys(bvProps).length > 0) + output += `Object.defineProperties(${bvTarget}, { ${bvProps.join(', ')} });\n`; + } + break; + + default: + output += line; + output += "\n"; + break; + } + } else { + output += line; + output += "\n"; + } + } + + return output; + }; + + return await processFile(entryPoint); +}; + +exports.housekeep = function(assetsPath) { + const files = fs.readdirSync(assetsPath).map(fileName => { + const stats = fs.statSync(path.join(assetsPath, fileName)); + return { + name: fileName, + lastMod: stats.mtimeMs, + }; + }).sort((a, b) => b.lastMod - a.lastMod).map(info => info.name); + + const regex = /^(.+)[\-\.]([a-f0-9]+)\.(.+)$/i; + const counts = {}; + + for(const fileName of files) { + const match = fileName.match(regex); + if(match) { + const name = match[1] + '-' + match[3]; + counts[name] = (counts[name] || 0) + 1; + + if(counts[name] > 5) + fs.unlinkSync(path.join(assetsPath, fileName)); + } else console.log(`Encountered file name in assets folder with unexpected format: ${fileName}`); + } +}; diff --git a/src/mami-init.js/main.js b/src/mami-init.js/main.js new file mode 100644 index 0000000..8091c05 --- /dev/null +++ b/src/mami-init.js/main.js @@ -0,0 +1,56 @@ +#buildvars + +(function() { + var isCompatible = (function() { + if(navigator.userAgent.indexOf('MSIE') >= 0) + return false; + if(navigator.userAgent.indexOf('Trident/') >= 0) + return false; + + if(!('Blob' in window) || !('prototype' in window.Blob) + || window.Blob.prototype.constructor.name !== 'Blob') + return false; + + if(!('AudioContext' in window) && !('webkitAudioContext' in window)) + return false; + + if(!('URL' in window) || !('createObjectURL' in window.URL) + || window.URL.prototype.constructor.name !== 'URL') + return false; + + if(!('localStorage' in window)) + return false; + try { + var testVar = 'test'; + localStorage.setItem(testVar, testVar); + if(localStorage.getItem(testVar) !== testVar) + throw ''; + localStorage.removeItem(testVar); + } catch(e) { + return false; + } + + try { + eval('const c = 1; let l = 2;'); + } catch(e) { + return false; + } + + try { + eval('for(const i of ["a", "b"]);'); + } catch(e) { + return false; + } + + return true; + })(); + + if(isCompatible) { + (function(script) { + script.src = MAMI_JS; + script.type = 'text/javascript'; + script.charset = 'utf-8'; + document.body.appendChild(script); + })(document.createElement('script')); + } else location.assign(window.AMI_URL); +})(); diff --git a/src/mami.css/__animations.css b/src/mami.css/__animations.css new file mode 100644 index 0000000..a10f8d6 --- /dev/null +++ b/src/mami.css/__animations.css @@ -0,0 +1,23 @@ +@keyframes rotate { + 0% { transform: rotate(0); } + 100% { transform: rotate(360deg); } +} + +@keyframes shake { + 0% { transform: translate(-1vw, -1vh); } + 10% { transform: translate(-.5vw, 2vh); } + 20% { transform: translate(0, -.5vh); } + 30% { transform: translate(-1vw, 1vh); } + 40% { transform: translate(2vw, 0vh); } + 50% { transform: translate(.5vw, -1vh); } + 60% { transform: translate(-1vw, -.5vh); } + 70% { transform: translate(-.5vw, -1.5vh); } + 80% { transform: translate(1vw, -1vh); } + 90% { transform: translate(-.5vw, .5vh); } + 100% { transform: translate(-1vw, -1vh); } +} + +@keyframes flash { + 0% { filter: invert(0); } + 100% { filter: invert(1); } +} diff --git a/src/mami.css/_main.css b/src/mami.css/_main.css new file mode 100644 index 0000000..41a93d6 --- /dev/null +++ b/src/mami.css/_main.css @@ -0,0 +1,92 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + outline-style: none; + -webkit-text-size-adjust: none; + text-size-adjust: none; +} + +html, +body { + width: 100%; + height: 100%; + background: #000; + color: #fff; + overflow: hidden; +} + +a { + color: inherit; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.hidden { + display: none !important; +} + +body > div { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; + overflow: auto; +} + +.noscript { + width: 100%; + height: 100%; + background: #000; + color: #fff; + font-family: Verdana, Geneva, Arial, Helvetica, sans-serif; + display: flex; + text-align: center; + justify-content: center; + align-items: center; + background: repeating-linear-gradient(-45deg, #220, #220 20px, #000 20px, #000 40px); + box-shadow: inset 0 0 1em #000; +} +.noscript-icon { + margin: 6px; +} +.noscript-content { + width: 100%; + background-color: rgba(0, 0, 0, .8); + padding: 20px; + box-shadow: 0 0 1em #000; +} +.noscript-header { + font-size: 1.4em; + line-height: 1.6em; +} +.noscript-body { + font-size: .9em; + margin-top: 4px; +} +.noscript-body p { + line-height: 1.5em; +} +.noscript-body code { + font-size: 1.3em; +} + +.sjis { + font-family: IPAMonaPGothic, 'IPA モナー Pゴシック', Monapo, Mona, 'MS PGothic', 'MS Pゴシック', monospace; + font-size: 16px; + line-height: 18px; +} + +.mami-copyright { + text-align: center; + padding: 6px 0; + font-size: .9em; + opacity: .8; + line-height: 1.5em; +} diff --git a/src/mami.css/chat.css b/src/mami.css/chat.css new file mode 100644 index 0000000..d28492c --- /dev/null +++ b/src/mami.css/chat.css @@ -0,0 +1,12 @@ +.chat { + flex: 1 1 auto; + margin-bottom: 5px; + overflow: auto; + font-size: 12px; + box-shadow: var(--theme-size-chat-box-shadow-x, 0) var(--theme-size-chat-box-shadow-y, 0) var(--theme-size-chat-box-shadow-blur, 0) var(--theme-size-chat-box-shadow-spread, 0) var(--theme-colour-chat-box-shadow, #000); + background-color: var(--theme-colour-chat-background, #fff); +} + +.chat--compact { + font-size: 13px +} diff --git a/src/mami.css/domaintrans.css b/src/mami.css/domaintrans.css new file mode 100644 index 0000000..8d600b5 --- /dev/null +++ b/src/mami.css/domaintrans.css @@ -0,0 +1,141 @@ +.domaintrans { + font-family: Verdana, Tahoma, Geneva, Arial, Helvetica, sans-serif; + font-size: 16px; + line-height: 20px; + + background-color: #222; + color: #ddd; + + text-shadow: 0 0 5px #000; + box-shadow: inset 0 0 1em #000; +} + +.domaintrans-body { + max-width: 500px; + margin: 20px auto; +} + +.domaintrans-eggs { + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; + gap: 10px; + margin: 20px; +} + +.domaintrans-domain { + margin: 10px; +} +.domaintrans-domain-main { + font-size: 1.2em; + line-height: 1.5em; +} +.domaintrans-domain-compat { + font-size: .8em; + line-height: 1.5em; + opacity: .8; +} + +.domaintrans-domain-header { + font-size: 1.4em; + line-height: 1.5em; + text-align: center; +} + +.domaintrans-domain-display { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + font-size: 1.2em; + line-height: 1.4em; + gap: 5px; +} + +.domaintrans-domain-text { + border: 1px solid #444; + background: #333; + border-radius: 5px; + padding: 2px 5px; +} + +.domaintrans-domain-arrow-down { + display: none; +} + +@media (max-width: 500px) { + .domaintrans-domain-display { + flex-direction: column; + } + + .domaintrans-domain-arrow-right { + display: none; + } + + .domaintrans-domain-arrow-down { + display: block; + } +} + +.domaintrans-text { + font-size: .8em; + line-height: 1.3em; + margin: 10px auto; +} +.domaintrans-text p { + margin: 1em 10px; +} + +.domaintrans-options { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; +} + +.domaintrans-option { + display: flex; + align-items: center; + color: #fff; + background: #333; + border-width: 0; + border-radius: 5px; + font-size: 16px; + max-width: 300px; + width: 100%; + text-shadow: initial; + text-align: left; + font-family: Verdana, Tahoma, Geneva, Arial, Helvetica, sans-serif; + padding: 10px 15px; + border: 1px solid #444; + transition: background .2s; +} +.domaintrans-option-icon { + transform: scale(1.4); + transition: transform .2s; +} +.domaintrans-option-text { + padding: 2px 10px; +} + +.domaintrans-option:focus { + outline: 2px solid #9475b2; +} + +.domaintrans-option:hover, +.domaintrans-option:focus { + background: #3d3d3d; + text-decoration: none; +} +.domaintrans-option:hover .domaintrans-option-icon, +.domaintrans-option:focus .domaintrans-option-icon { + transform: scale(1.6); +} + +.domaintrans-option:active { + background: #383838; +} +.domaintrans-option:active .domaintrans-option-icon { + transform: scale(1.3); +} diff --git a/src/mami.css/eeprom.css b/src/mami.css/eeprom.css new file mode 100644 index 0000000..11cdac0 --- /dev/null +++ b/src/mami.css/eeprom.css @@ -0,0 +1,14 @@ +.eeprom-item-progress { + margin: 5px; + margin-top: 0; + height: 14px; +} + +.eeprom-drop-overlay { + position: fixed; + top: 0; + bottom: 0; + right: 0; + left: 0; + background: rgba(0, 0, 0, .6); +} diff --git a/src/mami.css/emote.css b/src/mami.css/emote.css new file mode 100644 index 0000000..6286d04 --- /dev/null +++ b/src/mami.css/emote.css @@ -0,0 +1,24 @@ +.emoticon { + vertical-align: middle; + background: no-repeat center center transparent; + margin: 1px; + image-rendering: pixelated; + image-rendering: -moz-crisp-edges; + image-rendering: crisp-edges; +} + +.emoticon--button { + border: 0; + cursor: pointer; + vertical-align: middle; + height: 30px; + width: 30px; + display: inline-flex; + justify-content: center; + align-items: center +} + +.emoticon--button .emoticon { + max-height: 30px; + max-width: 30px +} diff --git a/src/mami.css/input.css b/src/mami.css/input.css new file mode 100644 index 0000000..b00c85f --- /dev/null +++ b/src/mami.css/input.css @@ -0,0 +1,69 @@ +.input { + flex-grow: 0; + flex-shrink: 0; + position: relative; +} + +.input__main { + display: flex; + height: 40px; + max-height: 140px; +} + +.input__text { + flex-grow: 1; + resize: none; +} + +.input__text, +.input__button { + border: 1px solid; + border-radius: 0; +} + +.input__text, +.input__button:not(.input__button--send) { + border-right: 0 +} + +.input__button { + border-left: 0; + font-size: 2em; + width: 40px; + cursor: pointer +} + +.input__button:before { + font-family: "Font Awesome 5 Free"; + font-weight: 900 +} + +.input__button--markup:before { + content: "\f121" +} + +.input__button--emotes:before { + content: "\f118" +} + +.input__button--upload:before { + content: "\f574" +} + +.input__button--send:before { + content: "\f1d8" +} + +.input__menus { + max-height: 100px; + overflow: auto +} + +.input__menu { + display: none; + padding: 1px +} + +.input__menu--active { + display: block +} diff --git a/src/mami.css/main.css b/src/mami.css/main.css new file mode 100644 index 0000000..8c1762f --- /dev/null +++ b/src/mami.css/main.css @@ -0,0 +1,19 @@ +@include __animations.css; +@include _main.css; +@include chat.css; +@include domaintrans.css; +@include eeprom.css; +@include emote.css; +@include input.css; +@include main_cont.css; +@include markup.css; +@include message.css; +@include overlay.css; +@include setting.css; +@include sidebar.css; +@include umi.css; +@include zz_ls_archaic.css; +@include zz_ls_blue.css; +@include zz_ls_dark.css; +@include zz_ls_light.css; +@include zz_ls_purple.css; diff --git a/src/mami.css/main_cont.css b/src/mami.css/main_cont.css new file mode 100644 index 0000000..af0e827 --- /dev/null +++ b/src/mami.css/main_cont.css @@ -0,0 +1,13 @@ +.main { + flex-grow: 2; + flex-shrink: 1; + margin: 5px; + display: flex; + flex-direction: column +} + +@media (max-width:768px) { + .main { + margin-right: 50px + } +} diff --git a/src/mami.css/markup.css b/src/mami.css/markup.css new file mode 100644 index 0000000..bf6bc7a --- /dev/null +++ b/src/mami.css/markup.css @@ -0,0 +1,17 @@ +.markup__button { + border: 0; + background: transparent; + font-family: Verdana, Tahoma, Geneva, Arial, Helvetica, sans-serif; + cursor: pointer; + padding: 4px 8px; + margin: 2px; + transition: background .1s +} + +.markup__link { + text-decoration: none +} + +.markup__link:hover { + text-decoration: underline +} diff --git a/src/mami.css/message.css b/src/mami.css/message.css new file mode 100644 index 0000000..103e6f3 --- /dev/null +++ b/src/mami.css/message.css @@ -0,0 +1,232 @@ +.message { + display: flex; + margin: 0 4px +} + +.message:hover .message__time { + opacity: 1 +} + +.message__meta { + display: inline-flex +} + +.message__container { + display: inline-flex; + margin-left: 4px +} + +.message__avatar { + background: no-repeat center center / cover transparent; + background-color: var(--theme-colour-message-avatar-background, #fff); + height: 40px; + width: 40px; + border-radius: 5%; + flex-shrink: 0; + flex-grow: 0; + margin: 0 4px; + display: none; + justify-content: center; + align-items: center; + font-size: 2em; + font-family: "Font Awesome 5 Free"; + font-weight: 900; +} + +.message__avatar--disabled { + background: transparent !important +} + +.message__avatar--disabled:before { + content: "\f007" +} + +.message__user { + font-weight: 700; + display: none +} + +.message__time { + font-size: .8em; + width: 40px; + margin-right: 8px; + text-align: center; + opacity: 0; + hyphens: auto; + transition: opacity .2s; + color: var(--theme-colour-message-time-colour, #000); +} + +.message__text { + white-space: pre-wrap; + word-wrap: normal; + word-break: break-word; + font-size: 1.2em +} + +.message:last-child { + margin-bottom: 8px +} + +.message--first { + margin-top: 8px +} + +.message--first:not(:first-child) { + border-top: 1px solid var(--theme-colour-message-separator, #000); + padding-top: 8px; +} + +.message--first .message__time { + width: auto; + margin: 0; + opacity: 1; +} + +.message--first .message__time:before { + content: "\A0@\A0" +} + +.message--first .message__container { + display: block +} + +.message--first .message__user, +.message--first .message__avatar { + display: inline-flex +} + +.message--first .message__user { + white-space: pre-wrap; + word-wrap: normal; + word-break: break-word; +} + +.message--user--1 .message__avatar { + background: transparent !important +} + +.message--user--1 .message__avatar:before { + content: "\f069" +} + +.message--user--1 .message__user { + display: none +} + +.message--user--1 .message__time:before { + content: "" !important +} + +.chat--compact .message { + padding: 0 4px !important; + margin: 0; + border: 0 solid transparent !important +} + +.chat--compact .message:nth-child(even) { + background-color: var(--theme-colour-message-compact-alternate-background, #ddd); +} + +.chat--compact .message__container { + display: inline-flex !important; + margin: 0 +} + +.chat--compact .message__meta { + display: inline-flex; + flex-shrink: 0 +} + +.chat--compact .message__avatar { + display: none !important +} + +.chat--compact .message__user { + order: 2; + display: block +} + +.chat--compact .message__user:after { + content: ":\A0"; + font-weight: 500; + color: var(--theme-colour-main-colour, #000); +} + +.chat--compact .message--highlight { + background-color: var(--theme-colour-message-compact-highlight, #aaa) !important; +} + +.chat--compact .message__time { + order: 1; + width: auto; + margin: 0; + opacity: 1 +} + +.chat--compact .message__time:before { + content: "" !important +} + +.chat--compact .message__time:after { + content: "\A0" +} + +.chat--compact .message__text { + font-size: 1em +} + +.chat--compact .message:last-child { + margin-bottom: 0 +} + +.chat--compact .message--user--1 .message__user { + display: none +} + +.chat--compact .message--user--1 .message__text:before { + content: "\A0*\A0"; + font-style: normal +} + +.prevent-overflow .message { + overflow: hidden; +} + +.message-tiny .message__avatar { + height: 20px; + width: 20px; +} +.message-tiny.message--first:not(:first-child) { + padding-top: 4px; +} +.message-tiny.message--first { + margin-top: 4px; +} + +.chat--compact .message-tiny .message-tiny-text { + order: 3; +} +.chat--compact .message-tiny .message__user:after { + content: ""; +} +.chat--compact .message-tiny .message__user:after { + content: ""; +} +.chat--compact .message-tiny.message--first { + margin-top: 0; +} + +.chat:not(.chat--compact) .message-tiny-fix { + margin-top: 8px !important; +} +.chat:not(.chat--compact) .message-big-fix { + margin-top: 4px !important; +} + +.avatar-filter-greyscale { + filter: grayscale(100%); +} +.avatar-filter-invert { + filter: invert(100%); +} diff --git a/src/mami.css/overlay.css b/src/mami.css/overlay.css new file mode 100644 index 0000000..c7cc36a --- /dev/null +++ b/src/mami.css/overlay.css @@ -0,0 +1,22 @@ +.overlay { + font-family: Verdana, Tahoma, Geneva, Arial, Helvetica, sans-serif; + font-size: 15px; + line-height: 20px; + display: flex; + justify-content: center; + align-items: center; + background-color: #222; + color: #ddd; + text-shadow: 0 0 5px #000; + text-align: center; + box-shadow: inset 0 0 1em #000; +} + +.overlay__message { + margin-top: 10px +} + +.overlay__status { + font-size: .8em; + color: #888 +} diff --git a/src/mami.css/setting.css b/src/mami.css/setting.css new file mode 100644 index 0000000..7f2416d --- /dev/null +++ b/src/mami.css/setting.css @@ -0,0 +1,65 @@ +select { + /* safari applies accent-color as the text colour :D */ + accent-color: currentcolor; +} + +.setting__category { + transition: max-height .2s; +} + +.setting__category-title { + font-size: 2em; + line-height: 1.1em; + padding: 4px; + text-align: right; + position: sticky; + top: 0; +} + +.setting__container { + padding: 2px 5px; + font-size: 1.2em; + line-height: 1.5em +} +.setting__container:last-child { + margin-bottom: 6px; +} + +.setting__container--select .setting__input, +.setting__container--number .setting__input, +.setting__container--text .setting__input { + width: 100%; + font-size: 1.1em; + line-height: 1.4em; + font-family: Verdana, Tahoma, Geneva, Arial, Helvetica, sans-serif +} + +.setting__container--range .setting__input { + border: 0; + width: 100%; + margin: 5px 0 +} + +.setting__container--checkbox .setting__label { + display: inline-flex; + align-items: center; + font-size: .8em; +} + +.setting__container--checkbox .setting__input { + margin-right: 5px; +} + +.setting__container--button .setting__input { + display: block; + width: 100%; + padding: 5px; + border-radius: 5px; + cursor: pointer; + margin: 5px 0; + transition: opacity .2s; +} + +.setting__container--button .setting__input[disabled] { + opacity: .6; +} diff --git a/src/mami.css/sidebar.css b/src/mami.css/sidebar.css new file mode 100644 index 0000000..e804ec7 --- /dev/null +++ b/src/mami.css/sidebar.css @@ -0,0 +1,198 @@ +.sidebar { + margin: 5px 5px 5px 0; + flex-shrink: 1; + flex-grow: 0; + font-family: Verdana, Tahoma, Geneva, Arial, Helvetica, sans-serif; + display: inline-flex; + 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) { + .sidebar { + position: fixed; + top: 0; + right: 0; + bottom: 0 + } +} + +.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; + flex-shrink: 0; + background: linear-gradient(90deg, var(--theme-colour-sidebar-selector-background-begin, #fff), var(--theme-colour-sidebar-selector-background-end, #000)) var(--theme-colour-sidebar-selector-background-begin, #fff); +} + +.sidebar__selector-top { + flex-grow: 1; + overflow: hidden; +} + +.sidebar__selector-mode { + height: 40px; + width: 40px; + cursor: pointer; + transition: background .1s, text-shadow .1s; +} + +.sidebar__selector-mode:hover { + background-color: var(--theme-colour-sidebar-selector-background-hover, #bbb); +} + +.sidebar__selector-mode:active { + background-color: var(--theme-colour-sidebar-selector-background-active, #ddd); +} + +.sidebar:not(.sidebar--closed) .sidebar__selector-mode--active { + 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 { + content: "\f127" +} + +.sidebar__selector-mode--clear { + background-image: url('//static.flash.moe/images/bomb.png'); +} +/*.sidebar__selector-mode--clear:before { + content: "\f2ed" +}*/ + +.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; + overflow: auto; + width: 220px; + scrollbar-width: thin; +} + +.sidebar__menu { + display: none; +} + +.sidebar__menu--active { + display: inline; +} + +.sidebar__user, +.sidebar__channel { + display: flex; + flex-direction: column; + padding: 1px; + font-size: 1.2em +} + +.sidebar__user-details, +.sidebar__channel-details { + display: flex; + align-items: center; + cursor: pointer; + height: 30px +} + +.sidebar__user-name, +.sidebar__channel-name { + flex-grow: 1; + padding: 0 5px; + min-height: 20px; +} + +.sidebar__user-options { + list-style: none; + margin: 1px 5px; + overflow: hidden; +} +.sidebar__user-options-wrapper { + overflow: hidden; +} + +.sidebar__user-options--hidden { + display: none +} + +.sidebar__user-option { + padding: 2px 5px; + cursor: pointer; + display: block; + text-decoration: none !important; +} + +.sidebar__user-avatar { + background: no-repeat center center / cover #000; + height: 30px; + width: 30px; + border-radius: 5%; + flex-shrink: 0; + flex-grow: 0; + order: 1 +} + +.user-sidebar-afk { + border-radius: 50px; + padding: 2px 6px; + font-size: .8em; + background-color: rgba(0, 0, 0, .5); + float: right; + line-height: 1.4em; +} diff --git a/src/mami.css/umi.css b/src/mami.css/umi.css new file mode 100644 index 0000000..58a041b --- /dev/null +++ b/src/mami.css/umi.css @@ -0,0 +1,28 @@ +.umi { + font-family: Tahoma, Geneva, Arial, Helvetica, sans-serif; + font-size: 12px; + line-height: 20px; + height: 100%; + width: 100%; + display: flex; + background-color: var(--theme-colour-main-background, #fff); + color: var(--theme-colour-main-colour, #000); + scrollbar-color: var(--theme-colour-scrollbar-foreground, #000) var(--theme-colour-scrollbar-background, #fff); + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); + accent-color: var(--theme-colour-main-accent, auto); + color-scheme: var(--theme-scheme, normal); +} + +.umi__main { + display: flex; + flex-grow: 1 +} + +.umi__input { + display: flex; + flex-grow: 1; + flex-shrink: 0 +} diff --git a/src/mami.css/zz_ls_archaic.css b/src/mami.css/zz_ls_archaic.css new file mode 100644 index 0000000..d043c3e --- /dev/null +++ b/src/mami.css/zz_ls_archaic.css @@ -0,0 +1,175 @@ +.setting__style--style-archaic { + background-color: #000; + color: #fff +} + +.umi--archaic { + background-color: #000; + color: #fff; + scrollbar-color: #212121 #000; +} + +.umi--archaic .chat { + box-shadow: 0 0 0 1px #888; + background-color: #000 +} + +.umi--archaic .chat .message__avatar { + background-color: #111 +} + +.umi--archaic .chat .message__time { + color: #888 +} + +.umi--archaic .chat .message--first:not(:first-child) { + border-color: #222 +} + +.umi--archaic .chat--compact .message:nth-child(even) { + background-color: #212121 +} + +.umi--archaic .chat--compact .message__user:after { + color: #fff +} + +.umi--archaic .chat--compact .message--highlight { + background-color: #3a3a3a +} + +.umi--archaic .sidebar { + box-shadow: 0 0 0 1px #888 +} + +.umi--archaic .sidebar__selector { + background: linear-gradient(90deg, #212121, #212121) #212121 +} + +.umi--archaic .sidebar__selector-mode--attention { + text-shadow: 0 0 10px #888 +} + +.umi--archaic .sidebar__selector-mode:hover { + background-color: #1a1a1a +} + +.umi--archaic .sidebar__selector-mode:active { + background-color: #111 +} + +.umi--archaic .sidebar:not(.sidebar--closed) .sidebar__selector-mode--active { + background-color: #111 !important +} + +.umi--archaic .sidebar__menus { + background-color: #000 +} + +.umi--archaic .sidebar__channel { + background: linear-gradient(270deg, transparent 0, #000 40%) transparent +} + +.umi--archaic .sidebar__channel--current { + background: linear-gradient(270deg, transparent 0, #212121 40%) transparent +} + +.umi--archaic .sidebar__channel--unread { + background-color: #000 +} + +.umi--archaic .sidebar__user { + background: linear-gradient(270deg, transparent 0, #000 40%) transparent +} + +.umi--archaic .sidebar__user:first-child { + background: linear-gradient(270deg, transparent 0, #212121 40%) transparent +} + +.umi--archaic .sidebar__user-option { + transition: background .2s +} + +.umi--archaic .sidebar__user-option:hover { + background: rgba(0, 0, 0, 0.5) +} + +.umi--archaic .input__text, +.umi--archaic .input__button { + color: #fff; + transition: background .1s; + border-color: #000 +} + +.umi--archaic .input__text, +.umi--archaic .input__button { + background-color: #000 +} + +.umi--archaic .input__button--send { + background-color: #111 +} + +.umi--archaic .input__button:hover { + background-color: #333 +} + +.umi--archaic .input__button--active, +.umi--archaic .input__button:active { + background-color: #222 +} + +.umi--archaic .input__main { + box-shadow: 0 0 0 1px #888 +} + +.umi--archaic .input__menus { + background-color: #000; + box-shadow: 0 0 0 1px #888 +} + +.umi--archaic .setting__category-title { + background: linear-gradient(270deg, #212121, #212121) #212121 +} + +.umi--archaic .setting__container--select .setting__input, +.umi--archaic .setting__container--text .setting__input, +.umi--archaic .setting__container--number .setting__input, +.umi--archaic .setting__container--button .setting__input { + border: 1px solid #888; + background-color: #000; + color: #fff; + transition: background .1s +} + +.umi--archaic .setting__container--select .setting__input:hover, +.umi--archaic .setting__container--text .setting__input:hover, +.umi--archaic .setting__container--number .setting__input:hover, +.umi--archaic .setting__container--button .setting__input:hover, +.umi--archaic .setting__container--select .setting__input:active, +.umi--archaic .setting__container--text .setting__input:active, +.umi--archaic .setting__container--number .setting__input:active, +.umi--archaic .setting__container--button .setting__input:active, +.umi--archaic .setting__container--select .setting__input:focus, +.umi--archaic .setting__container--text .setting__input:focus, +.umi--archaic .setting__container--number .setting__input:focus, +.umi--archaic .setting__container--button .setting__input:focus { + background-color: #000 +} + +.umi--archaic .markup__button { + color: #fff; + background-color: #111 +} + +.umi--archaic .markup__button:hover { + background-color: #333 +} + +.umi--archaic .markup__button:active { + background-color: #222 +} + +.umi--archaic .markup__link { + color: #1e90ff +} diff --git a/src/mami.css/zz_ls_blue.css b/src/mami.css/zz_ls_blue.css new file mode 100644 index 0000000..af107f6 --- /dev/null +++ b/src/mami.css/zz_ls_blue.css @@ -0,0 +1,175 @@ +.setting__style--style-blue { + background-color: #002545; + color: #fff +} + +.umi--blue { + background-color: #001434; + color: #fff; + scrollbar-color: #0b325a #002545; +} + +.umi--blue .chat { + box-shadow: 0 1px 4px #002545; + background-color: #002545 +} + +.umi--blue .chat .message__avatar { + background-color: #001434 +} + +.umi--blue .chat .message__time { + color: #888 +} + +.umi--blue .chat .message--first:not(:first-child) { + border-color: #0d355d +} + +.umi--blue .chat--compact .message:nth-child(even) { + background-color: #0d355d +} + +.umi--blue .chat--compact .message__user:after { + color: #fff +} + +.umi--blue .chat--compact .message--highlight { + background-color: #003d8e +} + +.umi--blue .sidebar { + box-shadow: 0 1px 4px #002545 +} + +.umi--blue .sidebar__selector { + background: linear-gradient(90deg, #0d355d, #002545) #0d355d +} + +.umi--blue .sidebar__selector-mode--attention { + text-shadow: 0 0 10px #96b6d3 +} + +.umi--blue .sidebar__selector-mode:hover { + background-color: #0d355d +} + +.umi--blue .sidebar__selector-mode:active { + background-color: #002545 +} + +.umi--blue .sidebar:not(.sidebar--closed) .sidebar__selector-mode--active { + background-color: #002545 !important +} + +.umi--blue .sidebar__menus { + background-color: #002545 +} + +.umi--blue .sidebar__channel { + background: linear-gradient(270deg, transparent 0, #002545 40%) transparent +} + +.umi--blue .sidebar__channel--current { + background: linear-gradient(270deg, transparent 0, #002545 40%) transparent +} + +.umi--blue .sidebar__channel--unread { + background-color: #0d355d +} + +.umi--blue .sidebar__user { + background: linear-gradient(270deg, transparent 0, #002545 40%) transparent +} + +.umi--blue .sidebar__user:first-child { + background: linear-gradient(270deg, transparent 0, #002545 40%) transparent +} + +.umi--blue .sidebar__user-option { + transition: background .2s +} + +.umi--blue .sidebar__user-option:hover { + background: rgba(0, 0, 0, 0.5) +} + +.umi--blue .input__text, +.umi--blue .input__button { + color: #fff; + transition: background .1s; + border-color: #0d355d +} + +.umi--blue .input__text, +.umi--blue .input__button { + background-color: #002545 +} + +.umi--blue .input__button--send { + background-color: #0d355d +} + +.umi--blue .input__button:hover { + background-color: #0d355d +} + +.umi--blue .input__button--active, +.umi--blue .input__button:active { + background-color: #002545 +} + +.umi--blue .input__main { + box-shadow: 0 1px 4px #002545 +} + +.umi--blue .input__menus { + background-color: #0d355d; + box-shadow: 0 1px 4px #0d355d +} + +.umi--blue .setting__category-title { + background: linear-gradient(270deg, #0d355d, #002545) #0d355d +} + +.umi--blue .setting__container--select .setting__input, +.umi--blue .setting__container--text .setting__input, +.umi--blue .setting__container--number .setting__input, +.umi--blue .setting__container--button .setting__input { + border: 1px solid #003d8e; + background-color: #002545; + color: #fff; + transition: background .1s +} + +.umi--blue .setting__container--select .setting__input:hover, +.umi--blue .setting__container--text .setting__input:hover, +.umi--blue .setting__container--number .setting__input:hover, +.umi--blue .setting__container--button .setting__input:hover, +.umi--blue .setting__container--select .setting__input:active, +.umi--blue .setting__container--text .setting__input:active, +.umi--blue .setting__container--number .setting__input:active, +.umi--blue .setting__container--button .setting__input:active, +.umi--blue .setting__container--select .setting__input:focus, +.umi--blue .setting__container--text .setting__input:focus, +.umi--blue .setting__container--number .setting__input:focus, +.umi--blue .setting__container--button .setting__input:focus { + background-color: #0d355d +} + +.umi--blue .markup__button { + color: #fff; + background-color: #0e466e +} + +.umi--blue .markup__button:hover { + background-color: #0f577f +} + +.umi--blue .markup__button:active { + background-color: #0e507a +} + +.umi--blue .markup__link { + color: #1e90ff +} diff --git a/src/mami.css/zz_ls_dark.css b/src/mami.css/zz_ls_dark.css new file mode 100644 index 0000000..f579bdb --- /dev/null +++ b/src/mami.css/zz_ls_dark.css @@ -0,0 +1,175 @@ +.setting__style--style-dark { + background-color: #111; + color: #fff +} + +.umi--dark { + background-color: #050505; + color: #fff; + scrollbar-color: #212121 #111; +} + +.umi--dark .chat { + box-shadow: 0 1px 4px #111; + background-color: #111 +} + +.umi--dark .chat .message__avatar { + background-color: #000 +} + +.umi--dark .chat .message__time { + color: #888 +} + +.umi--dark .chat .message--first:not(:first-child) { + border-color: #222 +} + +.umi--dark .chat--compact .message:nth-child(even) { + background-color: #212121 +} + +.umi--dark .chat--compact .message__user:after { + color: #fff +} + +.umi--dark .chat--compact .message--highlight { + background-color: #3a3a3a +} + +.umi--dark .sidebar { + box-shadow: 0 1px 4px #111 +} + +.umi--dark .sidebar__selector { + background: linear-gradient(90deg, #212121, #1a1a1a) #212121 +} + +.umi--dark .sidebar__selector-mode--attention { + text-shadow: 0 0 10px #96b6d3 +} + +.umi--dark .sidebar__selector-mode:hover { + background-color: #222 +} + +.umi--dark .sidebar__selector-mode:active { + background-color: #111 +} + +.umi--dark .sidebar:not(.sidebar--closed) .sidebar__selector-mode--active { + background-color: #111 !important +} + +.umi--dark .sidebar__menus { + background-color: #111 +} + +.umi--dark .sidebar__channel { + background: linear-gradient(270deg, transparent 0, #111 40%) transparent +} + +.umi--dark .sidebar__channel--current { + background: linear-gradient(270deg, transparent 0, #222 40%) transparent +} + +.umi--dark .sidebar__channel--unread { + background-color: #444 +} + +.umi--dark .sidebar__user { + background: linear-gradient(270deg, transparent 0, #111 40%) transparent +} + +.umi--dark .sidebar__user:first-child { + background: linear-gradient(270deg, transparent 0, #222 40%) transparent +} + +.umi--dark .sidebar__user-option { + transition: background .2s +} + +.umi--dark .sidebar__user-option:hover { + background: rgba(0, 0, 0, 0.5) +} + +.umi--dark .input__text, +.umi--dark .input__button { + color: #fff; + transition: background .1s; + border-color: #222 +} + +.umi--dark .input__text, +.umi--dark .input__button { + background-color: #111 +} + +.umi--dark .input__button--send { + background-color: #222 +} + +.umi--dark .input__button:hover { + background-color: #2a2a2a +} + +.umi--dark .input__button--active, +.umi--dark .input__button:active { + background-color: #1a1a1a +} + +.umi--dark .input__main { + box-shadow: 0 1px 4px #111 +} + +.umi--dark .input__menus { + background-color: #222; + box-shadow: 0 1px 4px #222 +} + +.umi--dark .setting__category-title { + background: linear-gradient(270deg, #222, #111) #222 +} + +.umi--dark .setting__container--select .setting__input, +.umi--dark .setting__container--number .setting__input, +.umi--dark .setting__container--text .setting__input, +.umi--dark .setting__container--button .setting__input { + border: 1px solid #333; + background-color: #111; + color: #fff; + transition: background .1s +} + +.umi--dark .setting__container--select .setting__input:hover, +.umi--dark .setting__container--text .setting__input:hover, +.umi--dark .setting__container--button .setting__input:hover, +.umi--dark .setting__container--number .setting__input:hover, +.umi--dark .setting__container--select .setting__input:active, +.umi--dark .setting__container--text .setting__input:active, +.umi--dark .setting__container--button .setting__input:active, +.umi--dark .setting__container--number .setting__input:active, +.umi--dark .setting__container--select .setting__input:focus, +.umi--dark .setting__container--text .setting__input:focus, +.umi--dark .setting__container--button .setting__input:focus, +.umi--dark .setting__container--number .setting__input:focus { + background-color: #222 +} + +.umi--dark .markup__button { + color: #fff; + background-color: #333 +} + +.umi--dark .markup__button:hover { + background-color: #444 +} + +.umi--dark .markup__button:active { + background-color: #3a3a3a +} + +.umi--dark .markup__link { + color: #1e90ff +} \ No newline at end of file diff --git a/src/mami.css/zz_ls_light.css b/src/mami.css/zz_ls_light.css new file mode 100644 index 0000000..f57dbc0 --- /dev/null +++ b/src/mami.css/zz_ls_light.css @@ -0,0 +1,175 @@ +.setting__style--style-light { + background-color: #eee; + color: #000 +} + +.umi--light { + background-color: #fff; + color: #000; + scrollbar-color: #ddd #eee; +} + +.umi--light .chat { + box-shadow: 0 1px 4px #eee; + background-color: #eee +} + +.umi--light .chat .message__avatar { + background-color: #fff +} + +.umi--light .chat .message__time { + color: #777 +} + +.umi--light .chat .message--first:not(:first-child) { + border-color: #ddd +} + +.umi--light .chat--compact .message:nth-child(even) { + background-color: #dedede +} + +.umi--light .chat--compact .message__user:after { + color: #000 +} + +.umi--light .chat--compact .message--highlight { + background-color: #d9d9d9 +} + +.umi--light .sidebar { + box-shadow: 0 1px 4px #eee +} + +.umi--light .sidebar__selector { + background: linear-gradient(90deg, #dedede, #e9e9e9) #dedede +} + +.umi--light .sidebar__selector-mode--attention { + text-shadow: 0 0 10px #96b6d3 +} + +.umi--light .sidebar__selector-mode:hover { + background-color: #ddd +} + +.umi--light .sidebar__selector-mode:active { + background-color: #eee +} + +.umi--light .sidebar:not(.sidebar--closed) .sidebar__selector-mode--active { + background-color: #eee !important +} + +.umi--light .sidebar__menus { + background-color: #eee +} + +.umi--light .sidebar__channel { + background: linear-gradient(270deg, transparent 0, #eee 40%) transparent +} + +.umi--light .sidebar__channel--current { + background: linear-gradient(270deg, transparent 0, #ddd 40%) transparent +} + +.umi--light .sidebar__channel--unread { + background-color: #bbb +} + +.umi--light .sidebar__user { + background: linear-gradient(270deg, transparent 0, #eee 40%) transparent +} + +.umi--light .sidebar__user:first-child { + background: linear-gradient(270deg, transparent 0, #ddd 40%) transparent +} + +.umi--light .sidebar__user-option { + transition: background .2s +} + +.umi--light .sidebar__user-option:hover { + background: rgba(255, 255, 255, 0.5) +} + +.umi--light .input__text, +.umi--light .input__button { + color: #000; + transition: background .1s; + border-color: #ddd +} + +.umi--light .input__text, +.umi--light .input__button { + background-color: #eee +} + +.umi--light .input__button--send { + background-color: #ddd +} + +.umi--light .input__button:hover { + background-color: #d9d9d9 +} + +.umi--light .input__button--active, +.umi--light .input__button:active { + background-color: #e9e9e9 +} + +.umi--light .input__main { + box-shadow: 0 1px 4px #eee +} + +.umi--light .input__menus { + background-color: #ddd; + box-shadow: 0 1px 4px #ddd +} + +.umi--light .setting__category-title { + background: linear-gradient(270deg, #ddd, #eee) #ddd +} + +.umi--light .setting__container--select .setting__input, +.umi--light .setting__container--text .setting__input, +.umi--light .setting__container--number .setting__input, +.umi--light .setting__container--button .setting__input { + border: 1px solid #ccc; + background-color: #eee; + color: #000; + transition: background .1s +} + +.umi--light .setting__container--select .setting__input:hover, +.umi--light .setting__container--text .setting__input:hover, +.umi--light .setting__container--number .setting__input:hover, +.umi--light .setting__container--button .setting__input:hover, +.umi--light .setting__container--select .setting__input:active, +.umi--light .setting__container--text .setting__input:active, +.umi--light .setting__container--number .setting__input:active, +.umi--light .setting__container--button .setting__input:active, +.umi--light .setting__container--select .setting__input:focus, +.umi--light .setting__container--text .setting__input:focus, +.umi--light .setting__container--number .setting__input:focus, +.umi--light .setting__container--button .setting__input:focus { + background-color: #ddd +} + +.umi--light .markup__button { + color: #000; + background-color: #ccc +} + +.umi--light .markup__button:hover { + background-color: #bbb +} + +.umi--light .markup__button:active { + background-color: #c9c9c9 +} + +.umi--light .markup__link { + color: #1e90ff +} diff --git a/src/mami.css/zz_ls_purple.css b/src/mami.css/zz_ls_purple.css new file mode 100644 index 0000000..7bb3a91 --- /dev/null +++ b/src/mami.css/zz_ls_purple.css @@ -0,0 +1,175 @@ +.setting__style--style-purple { + background-color: #2c2335; + color: #fff +} + +.umi--purple { + background-color: #251d2c; + color: #fff; + scrollbar-color: #4a3a59 #2c2335; +} + +.umi--purple .chat { + box-shadow: 0 1px 4px #2c2335; + background-color: #2c2335 +} + +.umi--purple .chat .message__avatar { + background-color: #251d2c +} + +.umi--purple .chat .message__time { + color: #888 +} + +.umi--purple .chat .message--first:not(:first-child) { + border-color: #4a3a59 +} + +.umi--purple .chat--compact .message:nth-child(even) { + background-color: #4a3a59 +} + +.umi--purple .chat--compact .message__user:after { + color: #fff +} + +.umi--purple .chat--compact .message--highlight { + background-color: #604c74 +} + +.umi--purple .sidebar { + box-shadow: 0 1px 4px #2c2335 +} + +.umi--purple .sidebar__selector { + background: linear-gradient(90deg, #4a3a59, #3b2f47) #4a3a59 +} + +.umi--purple .sidebar__selector-mode--attention { + text-shadow: 0 0 10px #a591ad +} + +.umi--purple .sidebar__selector-mode:hover { + background-color: #4a3a59 +} + +.umi--purple .sidebar__selector-mode:active { + background-color: #3b2f47 +} + +.umi--purple .sidebar:not(.sidebar--closed) .sidebar__selector-mode--active { + background-color: #3b2f47 !important +} + +.umi--purple .sidebar__menus { + background-color: #2c2335 +} + +.umi--purple .sidebar__channel { + background: linear-gradient(270deg, transparent 0, #2c2335 40%) transparent +} + +.umi--purple .sidebar__channel--current { + background: linear-gradient(270deg, transparent 0, #4a3a59 40%) transparent +} + +.umi--purple .sidebar__channel--unread { + background-color: #604c74 +} + +.umi--purple .sidebar__user { + background: linear-gradient(270deg, transparent 0, #2c2335 40%) transparent +} + +.umi--purple .sidebar__user:first-child { + background: linear-gradient(270deg, transparent 0, #4a3a59 40%) transparent +} + +.umi--purple .sidebar__user-option { + transition: background .2s +} + +.umi--purple .sidebar__user-option:hover { + background: rgba(0, 0, 0, 0.5) +} + +.umi--purple .input__text, +.umi--purple .input__button { + color: #fff; + transition: background .1s; + border-color: #4a3a59 +} + +.umi--purple .input__text, +.umi--purple .input__button { + background-color: #2c2335 +} + +.umi--purple .input__button--send { + background-color: #4a3a59 +} + +.umi--purple .input__button:hover { + background-color: #7e6397 +} + +.umi--purple .input__button--active, +.umi--purple .input__button:active { + background-color: #59466b +} + +.umi--purple .input__main { + box-shadow: 0 1px 4px #2c2335 +} + +.umi--purple .input__menus { + background-color: #4a3a59; + box-shadow: 0 1px 4px #4a3a59 +} + +.umi--purple .setting__category-title { + background: linear-gradient(270deg, #4a3a59, #3b2f47) #4a3a59 +} + +.umi--purple .setting__container--select .setting__input, +.umi--purple .setting__container--text .setting__input, +.umi--purple .setting__container--number .setting__input, +.umi--purple .setting__container--button .setting__input { + border: 1px solid #604c74; + background-color: #3b2f47; + color: #fff; + transition: background .1s +} + +.umi--purple .setting__container--select .setting__input:hover, +.umi--purple .setting__container--text .setting__input:hover, +.umi--purple .setting__container--number .setting__input:hover, +.umi--purple .setting__container--button .setting__input:hover, +.umi--purple .setting__container--select .setting__input:active, +.umi--purple .setting__container--text .setting__input:active, +.umi--purple .setting__container--number .setting__input:active, +.umi--purple .setting__container--button .setting__input:active, +.umi--purple .setting__container--select .setting__input:focus, +.umi--purple .setting__container--text .setting__input:focus, +.umi--purple .setting__container--number .setting__input:focus, +.umi--purple .setting__container--button .setting__input:focus { + background-color: #4a3a59 +} + +.umi--purple .markup__button { + color: #fff; + background-color: #59466b +} + +.umi--purple .markup__button:hover { + background-color: #7e6397 +} + +.umi--purple .markup__button:active { + background-color: #604c74 +} + +.umi--purple .markup__link { + color: #1e90ff +} diff --git a/src/mami.html b/src/mami.html new file mode 100644 index 0000000..32b908e --- /dev/null +++ b/src/mami.html @@ -0,0 +1,29 @@ + + + + + {title} + + + + + + + + + + + + + diff --git a/src/mami.js/animate.js b/src/mami.js/animate.js new file mode 100644 index 0000000..612ed2d --- /dev/null +++ b/src/mami.js/animate.js @@ -0,0 +1,258 @@ +const MamiAnimate = function(info) { + if(typeof info !== 'object') + throw 'info must be an object'; + + let onUpdate; + if('update' in info && typeof info.update === 'function') + onUpdate = info.update; + if(onUpdate === undefined) + throw 'update is a required parameter for info'; + + let duration; + if('duration' in info) + duration = parseFloat(info.duration); + if(duration <= 0) + throw 'duration is a required parameter for info and must be greater than 0'; + + let onStart; + if('start' in info && typeof info.start === 'function') + onStart = info.start; + + let onEnd; + if('end' in info && typeof info.end === 'function') + onEnd = info.end; + + let onCancel; + if('cancel' in info && typeof info.cancel === 'function') + onCancel = info.cancel; + + let easing; + if('easing' in info) + easing = info.easing; + + let delayed; + if('delayed' in info) + delayed = !!info.delayed; + + let async; + if('async' in info) + async = !!info.async; + + const easingType = typeof easing; + if(easingType !== 'function') { + if(easingType === 'string' + && easing.substring(0, 4) === 'ease' + && easing in MamiAnimate) + easing = MamiAnimate[easing]; + else + easing = MamiAnimate.easeLinear; + } + + let tStart, tLast, + cancel = false, + tRawCompletion = 0, + tCompletion = 0, + started = !delayed; + + const update = function(tCurrent) { + if(tStart === undefined) { + tStart = tCurrent; + if(onStart !== undefined) + onStart(); + } + + const tElapsed = tCurrent - tStart; + + tRawCompletion = Math.min(1, Math.max(0, tElapsed / duration)); + tCompletion = easing(tRawCompletion); + + onUpdate(tCompletion, tRawCompletion); + + if(tElapsed < duration) { + if(cancel) { + if(onCancel !== undefined) + onCancel(tCompletion, tRawCompletion); + return; + } + + tLast = tCurrent; + requestAnimationFrame(update); + } else if(onEnd !== undefined) + onEnd(); + }; + + let promise; + if(async) + promise = new Promise((resolve, reject) => { + if(onCancel === undefined) { + onCancel = reject; + } else { + const realOnCancel = onCancel; + onCancel = (...args) => { + realOnCancel(...args); + reject(...args); + }; + } + + if(onEnd === undefined) { + onEnd = resolve; + } else { + const realOnEnd = onEnd; + onEnd = (...args) => { + realOnEnd(...args); + resolve(...args); + }; + } + + if(!delayed) + requestAnimationFrame(update); + }); + + if(!delayed) { + if(promise !== undefined) + return promise; + + requestAnimationFrame(update); + } + + return { + getCompletion: function() { + return tCompletion; + }, + getRawCompletion: function() { + return tRawCompletion; + }, + start: () => { + if(!started) { + started = true; + requestAnimationFrame(update); + } + + if(promise !== undefined) + return promise; + }, + cancel: function() { + cancel = true; + }, + }; +}; + +// Yoinked from https://easings.net/ +MamiAnimate.C1 = 1.70158; +MamiAnimate.C2 = MamiAnimate.C1 + 1.525; +MamiAnimate.C3 = MamiAnimate.C1 + 1; +MamiAnimate.C4 = (2 * Math.PI) / 3; +MamiAnimate.C5 = (2 * Math.PI) / 4.5; +MamiAnimate.D1 = 2.75; +MamiAnimate.N1 = 7.5625; +MamiAnimate.easeLinear = function(x) { return x; }; +MamiAnimate.easeInSine = function(x) { return 1 - Math.cos((x * Math.PI) / 2); }; +MamiAnimate.easeOutSine = function(x) { return Math.sin((x * Math.PI) / 2); }; +MamiAnimate.easeInOutSine = function(x) { return -(Math.cos(Math.PI * x) - 1) / 2; }; +MamiAnimate.easeInCubic = function(x) { return x * x * x; }; +MamiAnimate.easeOutCubic = function(x) { return 1 - Math.pow(1 - x, 3); }; +MamiAnimate.easeInOutCubic = function(x) { + return x < .5 + ? (4 * x * x * x) + : (1 - Math.pow(-2 * x + 2, 3) / 2); +}; +MamiAnimate.easeInQuint = function(x) { return x * x * x * x * x; }; +MamiAnimate.easeOutQuint = function(x) { return 1 - Math.pow(1 - x, 5); }; +MamiAnimate.easeInOutQuint = function(x) { + return x < .5 + ? (16 * x * x * x * x * x) + : (1 - Math.pow(-2 * x + 2, 5) / 2); +}; +MamiAnimate.easeInCirc = function(x) { return 1 - Math.sqrt(1 - Math.pow(x, 2)); }; +MamiAnimate.easeOutCirc = function(x) { return Math.sqrt(1 - Math.pow(x - 1, 2)); }; +MamiAnimate.easeInOutCirc = function(x) { + return x < .5 + ? ((1 - Math.sqrt(1 - Math.pow(2 * x, 2))) / 2) + : ((Math.sqrt(1 - Math.pow(-2 * x + 2, 2)) + 1) / 2); +}; +MamiAnimate.easeInElastic = function(x) { + if(x === 0.0) + return 0; + if(x === 1.0) + return 1; + return -Math.pow(2, 10 * x - 10) * Math.sin((x * 10 - 10.75) * MamiAnimate.C4); +}; +MamiAnimate.easeOutElastic = function(x) { + if(x === 0.0) + return 0; + if(x === 1.0) + return 1; + return Math.pow(2, -10 * x) * Math.sin((x * 10 - .75) * MamiAnimate.C4) + 1; +}; +MamiAnimate.easeInOutElastic = function(x) { + if(x === 0.0) + return 0; + if(x === 1.0) + return 1; + return x < .5 + ? (-(Math.pow(2, 20 * x - 10) * Math.sin((20 * x - 11.125) * MamiAnimate.C5)) / 2) + : ((Math.pow(2, -20 * x + 10) * Math.sin((20 * x - 11.125) * MamiAnimate.C5)) / 2 + 1); +}; +MamiAnimate.easeInQuad = function(x) { return x * x; }; +MamiAnimate.easeOutQuad = function(x) { return 1 - (1 - x) * (1 - x); }; +MamiAnimate.easeInOutQuad = function(x) { + return x < .5 + ? (2 * x * x) + : (1 - Math.pow(-2 * x + 2, 2) / 2); +}; +MamiAnimate.easeInQuart = function(x) { return x * x * x * x; }; +MamiAnimate.easeOutQuart = function(x) { return 1 - Math.pow(1 - x, 4); }; +MamiAnimate.easeInOutQuart = function(x) { + return x < .5 + ? (8 * x * x * x * x) + : (1 - Math.pow(-2 * x + 2, 4) / 2); +}; +MamiAnimate.easeInExpo = function(x) { + if(x === 0.0) + return 0; + return Math.pow(2, 10 * x - 10); +}; +MamiAnimate.easeOutExpo = function(x) { + if(x === 1.0) + return 1; + return 1 - Math.pow(2, -10 * x); +}; +MamiAnimate.easeInOutExpo = function(x) { + if(x === 0.0) + return 0; + if(x === 1.0) + return 1; + return x < .5 + ? (Math.pow(2, 20 * x - 10) / 2) + : ((2 - Math.pow(2, -20 * x + 10)) / 2); +}; +MamiAnimate.easeInBack = function(x) { return MamiAnimate.C3 * x * x * x - MamiAnimate.C1 * x * x; }; +MamiAnimate.easeOutBack = function(x) { return 1 + MamiAnimate.C3 * Math.pow(x - 1, 3) + MamiAnimate.C1 * Math.pow(x - 1, 2); }; +MamiAnimate.easeInOutBack = function(x) { + return x < .5 + ? ((Math.pow(2 * x, 2) * ((MamiAnimate.C2 + 1) * 2 * x - MamiAnimate.C2)) / 2) + : ((Math.pow(2 * x - 2, 2) * ((MamiAnimate.C2 + 1) * (x * 2 - 2) + MamiAnimate.C2) + 2) / 2); +}; +MamiAnimate.easeInBounce = function(x) { return 1 - MamiAnimate.easeOutBounce(1 - x); }; +MamiAnimate.easeOutBounce = function(x) { + if(x < 1 / MamiAnimate.D1) + return MamiAnimate.N1 * x * x; + + if(x < 2 / MamiAnimate.D1) { + x -= 1.5; + return MamiAnimate.N1 * (x / MamiAnimate.D1) * x + .75; + } + + if(x < 2.5 / MamiAnimate.D1) { + x -= 2.25; + return MamiAnimate.N1 * (x / MamiAnimate.D1) * x + .9375; + } + + x -= 2.625; + return MamiAnimate.N1 * (x / MamiAnimate.D1) * x + .984375; +}; +MamiAnimate.easeInOutBounce = function(x) { + return x < .5 + ? ((1 - MamiAnimate.easeOutBounce(1 - 2 * x)) / 2) + : ((1 + MamiAnimate.easeOutBounce(2 * x - 1)) / 2); +}; diff --git a/src/mami.js/audio/buffer.js b/src/mami.js/audio/buffer.js new file mode 100644 index 0000000..4c4ad6f --- /dev/null +++ b/src/mami.js/audio/buffer.js @@ -0,0 +1,10 @@ +const MamiAudioBuffer = function(ctx, buffer) { + return { + getBuffer: function() { + return buffer; + }, + createSource: function() { + return ctx.createSource(buffer); + }, + }; +}; diff --git a/src/mami.js/audio/context.js b/src/mami.js/audio/context.js new file mode 100644 index 0000000..95d7037 --- /dev/null +++ b/src/mami.js/audio/context.js @@ -0,0 +1,151 @@ +#include utility.js +#include audio/buffer.js +#include audio/source.js + +const MamiSRLEEncode = function(input, cutoff) { + let output = '', last = '', repeat = 0; + + input = (input || '').toString(); + cutoff = cutoff || 1 + + for(let i = 0; i <= input.length; ++i) { + const chr = input[i]; + if(last === chr) + ++repeat; + else { + if(repeat > cutoff) + for(const repChr in repeat.toString()) output += ')!@#$%^&*('[parseInt(repChr)]; + else + output += last.repeat(repeat); + repeat = 0; + if(chr !== undefined) { + output += chr; + last = chr; + } + } + } + + return output; +}; + +const MamiSRLEDecode = function(input) { + let output = '', repeat = '', chr; + + input = (input || '').toString().split('').reverse(); + + for(;;) { + const chr = input.pop(), num = ')!@#$%^&*('.indexOf(chr); + if(num >= 0) repeat += num; + else { + if(repeat) { + output += output.slice(-1).repeat(parseInt(repeat)); + repeat = ''; + } + if(chr === undefined) break; + output += chr; + } + } + + return output; +}; +const MamiDetectAutoPlaySource = '/+NIxA!!FhpbmcA#PA$wA@fgAV$#q$#/$#A#OUxBTUUzLjEwMAIeA!)UCCQC8CIA@gA@H49wpKWgA!%&/+MYxA$NIA$ExBTUUzLjEwMFV^%/+MYxDsA@NIA$FV&&/+MYxHYA@NIA$FV&&'; + +const MamiDetectAutoPlay = async () => { + try { + const audio = $e('audio', { src: 'data:audio/mpeg;base64,' + MamiSRLEDecode(MamiDetectAutoPlaySource) }); + + try { + await audio.play(); + } catch(ex) { + if('name' in ex && ex.name !== 'NotAllowedError' && ex.name !== 'AbortError') { + console.error(ex); + throw ex; + } + + return false; + } + } catch(ex) {} + + return true; +}; + +const MamiAudioContext = function() { + const pub = {}; + + let volume = null, + isMuted = false; + + const ctxArgs = { latencyHint: 'playback' }; + let ctx = null, + mainGain = null; + const init = function() { + const audObj = window.AudioContext || window.webkitAudioContext; + try { + ctx = new audObj(ctxArgs); + } catch(ex) { + ctx = new audObj; + } + mainGain = ctx.createGain(); + + if(volume === null) { + volume = mainGain.gain.defaultValue; + isMuted = false; + } else + mainGain.gain.value = isMuted ? 0 : volume; + + mainGain.connect(ctx.destination); + }; + + init(); + pub.getContext = function() { return ctx; }; + pub.resetContext = init; + + pub.useGain = function(callback) { + callback.call(pub, mainGain.gain); + }; + + pub.getVolume = function() { + return volume; + }; + pub.setVolume = function(vol) { + volume = vol; + if(!isMuted) + mainGain.gain.value = volume; + }; + pub.isMuted = function() { + return isMuted; + }; + pub.setMuted = function(mute) { + mainGain.gain.value = (isMuted = mute) ? 0 : volume; + }; + + pub.createBuffer = function(url, callback) { + $x.get(url, { type: 'arraybuffer' }) + .then(resp => { + try { + ctx.decodeAudioData( + resp.body(), + buffer => callback.call(pub, true, new MamiAudioBuffer(pub, buffer)), + error => callback.call(pub, false, error) + ); + } catch(ex) { + callback.call(pub, false, ex); + } + }) + .catch(err => callback.call(pub, false, err)); + }; + + const createSource = function(buffer) { + const gain = ctx.createGain(); + gain.connect(mainGain); + + const source = ctx.createBufferSource(); + source.buffer = buffer; + source.connect(gain); + + return new MamiAudioSource(source, gain); + }; + pub.createSource = createSource; + + return pub; +}; diff --git a/src/mami.js/audio/source.js b/src/mami.js/audio/source.js new file mode 100644 index 0000000..c50b396 --- /dev/null +++ b/src/mami.js/audio/source.js @@ -0,0 +1,106 @@ +const MamiAudioSource = function(source, gain, buffer) { + const pub = {}; + + let volume = 1, + isMuted = false; + + let hasDisconnected = false; + + pub.getSource = function() { return source; }; + + const play = function() { + source.start(); + }; + pub.play = play; + + const disconnect = function() { + if(hasDisconnected) + return; + hasDisconnected = true; + + gain.disconnect(); + source.disconnect(); + }; + + const stop = function() { + source.stop(); + disconnect(); + }; + pub.stop = stop; + + pub.useGain = function(callback) { + callback.call(pub, gain.gain); + }; + pub.usePlaybackRate = function(callback) { + callback.call(pub, source.playbackRate); + }; + pub.useDetune = function(callback) { + callback.call(pub, source.detune); + }; + + pub.getRate = function(rate) { + return source.playbackRate.value; + }; + pub.setRate = function(rate) { + source.playbackRate.value = Math.min( + source.playbackRate.maxValue, + Math.max( + source.playbackRate.minValue, + rate + ) + ); + }; + pub.getDetune = function(rate) { + return source.detune.value; + }; + pub.setDetune = function(rate) { + source.detune.value = Math.min( + source.detune.maxValue, + Math.max( + source.detune.minValue, + rate + ) + ); + }; + + pub.getVolume = function() { + return volume; + }; + pub.setVolume = function(vol) { + volume = vol; + if(!isMuted) + gain.gain.value = volume; + }; + pub.isMuted = function() { + return isMuted; + }; + pub.setMuted = function(mute) { + gain.gain.value = (isMuted = mute) ? 0 : volume; + }; + + pub.setLoop = function(loop) { + if(typeof loop !== 'boolean') + loop = true; + source.loop = loop; + }; + pub.setLoopStart = function(start) { + if(typeof start !== 'number') + start = 0; + source.loopStart = start; + }; + pub.setLoopEnd = function(end) { + if(typeof end !== 'number') + end = 0; + source.loopEnd = end; + }; + + pub.whenEnded = function(handler) { + source.addEventListener('ended', handler.bind(pub)); + }; + + source.addEventListener('ended', function() { + disconnect(); + }); + + return pub; +}; diff --git a/src/mami.js/channel.js b/src/mami.js/channel.js new file mode 100644 index 0000000..7670789 --- /dev/null +++ b/src/mami.js/channel.js @@ -0,0 +1,19 @@ +Umi.Channel = function(name, pass, temp, pm) { + name = (name || {}).toString(); + pm = !!pm; + pass = !pm && !!pass; + temp = pm || !!temp; + + return { + getName: function() { return name; }, + setName: function(value) { name = (value || {}).toString(); }, + + hasPassword: function() { return pass; }, + setHasPassword: function(value) { pass = !!value; }, + + isTemporary: function() { return temp; }, + setTemporary: function(value) { temp = !!value; }, + + isUserChannel: function() { return pm; }, + }; +}; diff --git a/src/mami.js/channels.js b/src/mami.js/channels.js new file mode 100644 index 0000000..6a856ad --- /dev/null +++ b/src/mami.js/channels.js @@ -0,0 +1,68 @@ +Umi.Channels = (function() { + const chans = new Map; + let currentName = null; + + const onAdd = [], + onRemove = [], + onClear = [], + onUpdate = [], + onSwitch = []; + + return { + OnAdd: onAdd, + OnRemove: onRemove, + OnClear: onClear, + OnUpdate: onUpdate, + OnSwitch: onSwitch, + Add: function(channel) { + const channelName = channel.getName(); + if(!chans.has(channelName)) { + chans.set(channelName, channel); + + for(const i in onAdd) + onAdd[i](channel); + } + }, + Remove: function(channel) { + const channelName = channel.getName(); + 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); + }, + }; +})(); diff --git a/src/mami.js/common.js b/src/mami.js/common.js new file mode 100644 index 0000000..ac32d2a --- /dev/null +++ b/src/mami.js/common.js @@ -0,0 +1,35 @@ +#include utility.js + +const FutamiCommon = function(vars) { + vars = vars || {}; + + const get = function(name, fallback) { + return vars[name] || fallback || null; + }; + + return { + get: get, + getJson: async (name, noCache) => { + const options = { type: 'json' }; + if(noCache) + options.headers = { 'Cache-Control': 'no-cache' }; + + const resp = await $x.get(get(name), options); + return resp.body(); + }, + }; +}; + +FutamiCommon.load = async url => { + if(typeof url !== 'string' && 'FUTAMI_URL' in window) + url = window.FUTAMI_URL + '?t=' + Date.now().toString(); + + const resp = await $x.get(url, { + type: 'json', + headers: { + 'Cache-Control': 'no-cache' + } + }); + + return new FutamiCommon(resp.body()); +}; diff --git a/src/mami.js/compat.js b/src/mami.js/compat.js new file mode 100644 index 0000000..349e08b --- /dev/null +++ b/src/mami.js/compat.js @@ -0,0 +1,26 @@ +#include server.js + +// backwards compat for scripts +if(!Umi) window.Umi = {}; + +if(!Umi.Server) Umi.Server = {}; +if(!Umi.Server.sendMessage) Umi.Server.sendMessage = function() { console.log('Umi.Server.sendMessage called'); }; +if(!Umi.Server.SendMessage) Umi.Server.SendMessage = Umi.Server.sendMessage.bind(Umi.Server); + +if(!Umi.Protocol) Umi.Protocol = {}; +if(!Umi.Protocol.SockLegacy) Umi.Protocol.SockLegacy = {}; +if(!Umi.Protocol.SockLegacy.Protocol) Umi.Protocol.SockLegacy.Protocol = {}; +if(!Umi.Protocol.SockLegacy.Protocol.Instance) Umi.Protocol.SockLegacy.Protocol.Instance = {}; +if(!Umi.Protocol.SockLegacy.Protocol.Instance.SendMessage) Umi.Protocol.SockLegacy.Protocol.Instance.SendMessage = Umi.Server.sendMessage.bind(Umi.Server); + +if(!Umi.Parser) Umi.Parser = {}; +if(!Umi.Parser.SockChatBBcode) Umi.Parser.SockChatBBcode = {}; +if(!Umi.Parser.SockChatBBcode.EmbedStub) Umi.Parser.SockChatBBcode.EmbedStub = function() {}; + +if(!Umi.UI) Umi.UI = {}; +if(!Umi.UI.View) Umi.UI.View = {}; +if(!Umi.UI.View.SetText) Umi.UI.View.SetText = function() { console.log('Umi.UI.View.SetText called'); }; + +if(!Umi.UI.Menus) Umi.UI.Menus = {}; +if(!Umi.UI.Menus.Add) Umi.UI.Menus.Add = function() { console.log('Umi.UI.Menus.Add called'); }; +if(!Umi.UI.Menus.Get) Umi.UI.Menus.Get = function() { console.log('Umi.UI.Menus.Get called'); }; diff --git a/src/mami.js/context.js b/src/mami.js/context.js new file mode 100644 index 0000000..afb455c --- /dev/null +++ b/src/mami.js/context.js @@ -0,0 +1,154 @@ +#include txtrigs.js +#include audio/context.js +#include sound/sndmgr.js +#include sound/sndlibrary.js +#include sound/sndpacks.js +#include ui/views.js + +const MamiContext = function(targetBody) { + const pub = {}; + + const viewsCtx = new MamiUIViews(targetBody); + pub.getViews = () => viewsCtx; + + let audioCtx = null; + pub.hasAudio = function() { return audioCtx !== null; }; + pub.getAudio = function() { return audioCtx; }; + const initAudio = function() { + if(audioCtx !== null) + return; + + audioCtx = new MamiAudioContext; + }; + pub.initAudio = initAudio; + + let soundMgr = null, sndPckPlay = null; + const soundLib = new MamiSoundLibrary(), + soundPck = new MamiSoundPacks(); + + pub.initSound = function() { + if(soundMgr !== null) + return; + + initAudio(); + soundMgr = new MamiSoundManager(audioCtx); + sndPckPlay = new MamiSoundPackPlayer(soundMgr, soundLib); + }; + + pub.getSound = function() { return soundMgr; }; + pub.hasSound = function() { return soundMgr !== null; }; + pub.getSoundLibrary = function() { return soundLib; }; + pub.getSoundPacks = function() { return soundPck; }; + pub.getSoundPackPlayer = function() { return sndPckPlay; }; + + pub.playUrlSound = function(soundSources, complete, volume, rate) { + if(soundMgr === null) + return; + + const hasCallback = typeof complete === 'function'; + + try { + const soundUrl = soundMgr.findSupportedUrl(soundSources); + if(soundUrl === null) + return; + + const soundName = 'MamiCtx:' + soundUrl; + + if(soundMgr.isLoaded(soundName)) { + const source = soundMgr.get(soundName); + if(hasCallback) + source.whenEnded(function() { + complete(); + }); + if(typeof volume === 'number') + source.setVolume(volume); + if(typeof rate === 'number') + source.setRate(rate); + source.play(); + } else { + soundMgr.load(soundName, soundUrl, function(success, buffer) { + if(success) { + const source = buffer.createSource(); + if(hasCallback) + source.whenEnded(function() { + complete(); + }); + if(typeof volume === 'number') + source.setVolume(volume); + if(typeof rate === 'number') + source.setRate(rate); + source.play(); + } else { + console.error(buffer); + if(hasCallback) + complete(); + } + }); + } + } catch(ex) { + console.error(ex); + if(hasCallback) + complete(); + } + }; + + pub.playLibrarySound = function(soundName, complete, volume, rate) { + if(soundMgr === null) + return; + + const hasCallback = typeof complete === 'function'; + + try { + const soundInfo = soundLib.getSound(soundName); + + if(soundMgr.isLoaded(soundName)) { + const source = soundMgr.get(soundName); + if(hasCallback) + source.whenEnded(function() { + complete(); + }); + if(typeof volume === 'number') + source.setVolume(volume); + if(typeof rate === 'number') + source.setRate(rate); + source.play(); + } else { + soundMgr.load(soundName, soundInfo.getSources(), function(success, buffer) { + if(success) { + const source = buffer.createSource(); + if(hasCallback) + source.whenEnded(function() { + complete(); + }); + if(typeof volume === 'number') + source.setVolume(volume); + if(typeof rate === 'number') + source.setRate(rate); + source.play(); + } else { + console.error(buffer); + if(hasCallback) + complete(); + } + }); + } + } catch(ex) { + console.error(ex); + if(hasCallback) + complete(); + } + }; + + const txtTriggers = new MamiTextTriggers; + pub.getTextTriggers = function() { return txtTriggers; }; + + let eeprom = null; + pub.hasEEPROM = function() { return eeprom !== null }; + pub.getEEPROM = function() { return eeprom; }; + pub.createEEPROM = function(url, getToken) { + // new EEPROM api should take a callback to get auth info instead of a string + eeprom = new EEPROM(1, url, getToken()); + }; + + return pub; +}; diff --git a/src/mami.js/eeprom/eeprom.js b/src/mami.js/eeprom/eeprom.js new file mode 100644 index 0000000..8b0b2d8 --- /dev/null +++ b/src/mami.js/eeprom/eeprom.js @@ -0,0 +1,38 @@ +#include utility.js + +const MamiEEPROM = function() { + // +}; +MamiEEPROM.init = (function() { + let initialised = false; + + return () => { + return new Promise((resolve, reject) => { + if(initialised) + return new Promise(resolve => resolve()); + + // cuts off "/uploads", this is little disgusting + const src = futami.get('eeprom').slice(0, -8) + '/eeprom.js'; + + const script = $e({ + tag: 'script', + attrs: { + charset: 'utf-8', + type: 'text/javascript', + src: src, + onload: () => { + initialised = true; + resolve(); + }, + onerror: () => { + $r(script); + console.error('Failed to load EEPROM script!'); + reject(); + }, + }, + }); + + document.body.appendChild(script); + }); + }; +})(); diff --git a/src/mami.js/emotes.js b/src/mami.js/emotes.js new file mode 100644 index 0000000..6664fed --- /dev/null +++ b/src/mami.js/emotes.js @@ -0,0 +1,53 @@ +const MamiEmotes = (function() { + let emotes = []; + + const clear = function() { + emotes = []; + }; + + const add = function(emote) { + emotes.push(emote); + }; + + const addLegacy = function(emoteOld) { + const emote = { + url: emoteOld.Image, + minRank: emoteOld.Hierarchy, + strings: [], + }; + for(let i = 0; i < emoteOld.Text.length; ++i) + emote.strings.push(emoteOld.Text[i].slice(1, -1)); + add(emote); + }; + + return { + clear: clear, + add: add, + addLegacy: addLegacy, + load: function(batch) { + for(const emote of batch) + add(emote); + }, + loadLegacy: function(batch) { + for(const emote of batch) + addLegacy(emote); + }, + forEach: function(minRank, callback) { + for(const emote of emotes) + if(emote.minRank <= minRank) + callback(emote); + }, + findByName: function(minRank, name, returnString) { + const found = []; + for(const emote of emotes) + if(emote.minRank <= minRank) { + for(const string of emote.strings) + if(string.indexOf(name) === 0) { + found.push(returnString ? string : emote); + break; + } + } + return found; + }, + }; +})(); diff --git a/src/mami.js/main.js b/src/mami.js/main.js new file mode 100644 index 0000000..efcc0c0 --- /dev/null +++ b/src/mami.js/main.js @@ -0,0 +1,488 @@ +const Umi = { UI: {} }; + +#include animate.js +#include common.js +#include context.js +#include emotes.js +#include messages.js +#include mszauth.js +#include server.js +#include settings.js +#include utility.js +#include ui/chat-layout.js +#include ui/domaintrans.jsx +#include ui/elems.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/view.js +#include ui/settings.js +#include ui/title.js +#include ui/toggles.js +#include ui/uploads.js +#include audio/context.js +#include eeprom/eeprom.js + +(async () => { + const ctx = new MamiContext(document.body), + views = ctx.getViews(); + + Object.defineProperty(window, 'mami', { + value: ctx, + writable: false, + }); + + const sndLib = ctx.getSoundLibrary(), + sndPacks = ctx.getSoundPacks(); + + const lo = new Umi.UI.LoadingOverlay('spinner', 'Loading...'); + await views.push(lo); + + lo.setMessage('Loading environment...'); + try { + window.futami = await FutamiCommon.load(); + localStorage.setItem('mami:common', JSON.stringify(window.futami)); + } catch(ex) { + try { + const cached = JSON.parse(localStorage.getItem('mami:common')); + if(cached === null) + throw 'Cached data is null.'; + window.futami = new FutamiCommon(cached); + } catch(ex) { + console.error(ex); + lo.setIcon('cross'); + lo.setHeader('Failed!'); + lo.setMessage('Failed to load common settings!'); + return; + } + } + + + lo.setMessage('Fetching credentials...'); + try { + const auth = await MamiMisuzuAuth.update(); + if(!auth.ok) + throw 'Authentication failed.'; + } catch(ex) { + console.error(ex); + location.assign(futami.get('login')); + return; + } + + setInterval(() => { + MamiMisuzuAuth.update() + .then(auth => { + if(!auth.ok) + location.assign(futami.get('login')); + }) + }, 600000); + + + lo.setMessage('Getting element references...'); + + // should be dynamic when possible + const layout = new Umi.UI.ChatLayout; + await views.unshift(layout); + + Umi.UI.Elements.Chat = layout.getElement(); + + Umi.UI.Elements.Messages = $i('umi-messages'); + Umi.UI.Elements.Menus = $i('umi-menus'); + Umi.UI.Elements.Icons = $i('umi-menu-icons'); + Umi.UI.Elements.Toggles = $i('umi-toggles'); + Umi.UI.Elements.MessageContainer = $i('umi-msg-container'); + Umi.UI.Elements.MessageInput = $i('umi-msg-text'); + Umi.UI.Elements.MessageSend = $i('umi-msg-send'); + Umi.UI.Elements.MessageMenus = $i('umi-msg-menu'); + + + lo.setMessage('Loading sounds...'); + try { + const sounds = await futami.getJson('sounds2'); + if(Array.isArray(sounds.library)) + sndLib.loadSounds(sounds.library, true); + if(Array.isArray(sounds.packs)) + sndPacks.loadPacks(sounds.packs, true); + } catch(ex) { + console.error(ex); + } + + + lo.setMessage('Loading emoticons...'); + try { + const emotes = await futami.getJson('emotes'); + MamiEmotes.loadLegacy(emotes); + } catch(ex) { + console.error(ex); + } + + + lo.setMessage('Loading settings...'); + + Umi.Settings = new Umi.Settings(UmiSettings.settings); + + if(!await MamiDetectAutoPlay()) { + Umi.Settings.set('soundEnable', false); + Umi.Settings.virtualise('soundEnable'); + } + + const meta = UmiSettings.settings; + for(const setting of UmiSettings.settings) + if(setting.watcher) + Umi.Settings.watch(setting.id, setting.watcher); + + + if(!Umi.Settings.get('tmpSkipDomainPopUpThing')) + await (() => { + return new Promise((resolve) => { + views.push(new MamiDomainTransition(() => { + for(const setting of UmiSettings.settings) + if(setting.id === 'settingsImport') { + setting.click(); + break; + } + }, () => { + Umi.Settings.set('tmpSkipDomainPopUpThing', true); + views.pop(); + resolve(); + })); + }); + })(); + + + const onHashChange = () => { + if(location.hash === '#reset') { + for(const setting of UmiSettings.settings) + if(setting.emergencyReset) + Umi.Settings.remove(setting.id); + + location.assign('/'); + } + }; + + window.addEventListener('hashchange', onHashChange); + onHashChange(); + + window.addEventListener('keydown', ev => { + if(ev.altKey && ev.shiftKey && (ev.key === 'R' || ev.key === 'r')) { + Umi.Server.close(); + location.hash = 'reset'; + } + }); + + + lo.setMessage('Preparing UI...'); + + Umi.UI.Title.Set(futami.get('title')); + Umi.UI.View.AccentReload(); + Umi.UI.Hooks.AddMessageHooks(); + Umi.UI.Hooks.AddUserHooks(); + Umi.UI.Hooks.AddChannelHooks(); + Umi.UI.Hooks.AddTextHooks(); + + const mcPortalSnd = 'minecraft:nether:enter'; + if(Umi.Settings.get('minecraft') !== 'no' && ctx.hasSound() && sndLib.hasSound(mcPortalSnd)) + ctx.getSound().load(mcPortalSnd, sndLib.getSound(mcPortalSnd).getSources(), function(success, buffer) { + if(success) + buffer.createSource().play(); + }); + + + lo.setMessage('Loading EEPROM...'); + try { + await MamiEEPROM.init(); + ctx.createEEPROM( + futami.get('eeprom'), + function() { return 'Misuzu ' + MamiMisuzuAuth.getAuthToken(); } + ); + } catch(ex) { + console.error(ex); + } + + + lo.setMessage('Building menus...'); + + Umi.UI.Menus.Add('users', 'Users'); + Umi.UI.Menus.Add('channels', 'Channels', !Umi.Settings.get('showChannelList')); + Umi.UI.Menus.Add('settings', 'Settings'); + + let sidebarAnimation = null; + + Umi.UI.Settings.Init(); + Umi.UI.Toggles.Add('menu-toggle', { + 'click': function() { + const sidebar = $c('sidebar')[0], + toggle = Umi.UI.Toggles.Get('menu-toggle'), + toggleOpened = 'sidebar__selector-mode--menu-toggle-opened', + toggleClosed = 'sidebar__selector-mode--menu-toggle-closed', + isClosed = toggle.classList.contains(toggleClosed); + + if(sidebarAnimation !== null) { + sidebarAnimation.cancel(); + sidebarAnimation = null; + } + + if(isClosed) { + toggle.classList.add(toggleOpened); + toggle.classList.remove(toggleClosed); + } else { + toggle.classList.add(toggleClosed); + toggle.classList.remove(toggleOpened); + } + + 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: 'easeOutExpo', + 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() { + Umi.Settings.toggle('autoScroll'); + } + }, 'Autoscroll'); + Umi.Settings.watch('autoScroll', function(value) { + Umi.UI.Toggles.Get('scroll').classList[value ? 'remove' : 'add']('sidebar__selector-mode--scroll-off'); + }); + + if(window.innerWidth < 768) + Umi.UI.Toggles.Get('menu-toggle').click(); + + Umi.UI.Toggles.Add('audio', { + 'click': function() { + Umi.Settings.toggle('soundEnable'); + } + }, 'Sounds'); + Umi.Settings.watch('soundEnable', function(value) { + Umi.UI.Toggles.Get('audio').classList[value ? 'remove' : 'add']('sidebar__selector-mode--audio-off'); + }); + + Umi.UI.Toggles.Add('unembed', { + 'click': function() { + const buttons = $qa('[data-embed="1"]'); + for(const button of buttons) + button.click(); + } + }, 'Unembed any embedded media'); + + Umi.UI.Toggles.Add('clear', { + 'click': function() { + if(confirm('ARE YOU SURE ABOUT THAT???')) { + const limit = Umi.Settings.get('explosionRadius'); + 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); + + ctx.playLibrarySound('misc:explode'); + }, + }, + }); + + document.body.appendChild(explode); + + let backLog = Umi.Messages.All(); + backLog = backLog.slice(Math.max(backLog.length - limit, 0)); + + Umi.Messages.Clear(); + + for(const blMsg of backLog) + Umi.Messages.Add(blMsg); + } + } + }, 'Clear Logs'); + + + if(ctx.hasEEPROM()) { + Umi.UI.Menus.Add('uploads', 'Upload History', !FUTAMI_DEBUG); + + const doUpload = function(file) { + const uploadEntry = Umi.UI.Uploads.create(file.name), + uploadTask = ctx.getEEPROM().createUpload(file); + + uploadTask.onProgress = function(progressInfo) { + uploadEntry.setProgress(progressInfo.total, progressInfo.loaded); + }; + + uploadTask.onFailure = function(errorInfo) { + if(!errorInfo.userAborted) { + let errorText = 'Was unable to upload file.'; + + switch(errorInfo.error) { + case EEPROM.ERR_INVALID: + errorText = 'Upload request was invalid.'; + break; + case EEPROM.ERR_AUTH: + errorText = 'Upload authentication failed, refresh and try again.'; + break; + case EEPROM.ERR_ACCESS: + errorText = 'You\'re not allowed to upload files.'; + break; + case EEPROM.ERR_GONE: + errorText = 'Upload client has a configuration error or the server is gone.'; + break; + case EEPROM.ERR_DMCA: + errorText = 'This file has been uploaded before and was removed for copyright reasons, you cannot upload this file.'; + break; + case EEPROM.ERR_SERVER: + errorText = 'Upload server returned a critical error, try again later.'; + break; + case EEPROM.ERR_SIZE: + if(errorInfo.maxSize < 1) + errorText = 'Selected file is too large.'; + else { + const _t = ['bytes', 'KB', 'MB', 'GB', 'TB'], + _i = parseInt(Math.floor(Math.log(errorInfo.maxSize) / Math.log(1024))), + _s = Math.round(errorInfo.maxSize / Math.pow(1024, _i), 2); + + errorText = 'Upload may not be larger than %1 %2.'.replace('%1', _s).replace('%2', _t[_i]); + } + break; + } + + alert(errorText); + } + + uploadEntry.remove(); + }; + + uploadTask.onComplete = function(fileInfo) { + uploadEntry.hideOptions(); + uploadEntry.clearOptions(); + uploadEntry.removeProgress(); + + uploadEntry.addOption('Open', fileInfo.url); + uploadEntry.addOption('Insert', function() { + Umi.UI.Markup.InsertRaw(insertText, ''); + }); + uploadEntry.addOption('Delete', function() { + ctx.getEEPROM().deleteUpload(fileInfo).start(); + uploadEntry.remove(); + }); + + let insertText = location.protocol + fileInfo.url; + + if(fileInfo.isImage()) { + insertText = '[img]' + fileInfo.url + '[/img]'; + uploadEntry.setThumbnail(fileInfo.thumb); + } else if(fileInfo.isAudio() || fileInfo.type === 'application/x-font-gdos') { + insertText = '[audio]' + fileInfo.url + '[/audio]'; + uploadEntry.setThumbnail(fileInfo.thumb); + } else if(fileInfo.isVideo()) { + insertText = '[video]' + fileInfo.url + '[/video]'; + uploadEntry.setThumbnail(fileInfo.thumb); + } + + + if(Umi.Settings.get('eepromAutoInsert')) + Umi.UI.Markup.InsertRaw(insertText, ''); + }; + + uploadEntry.addOption('Cancel', function() { + uploadTask.abort(); + }); + + uploadTask.start(); + }; + + const uploadForm = $e({ + tag: 'input', + attrs: { + type: 'file', + multiple: true, + style: { + display: 'none', + }, + onchange: function(ev) { + for(let i = 0; i < ev.target.files.length; ++i) + doUpload(ev.target.files[i]); + }, + }, + }); + document.body.appendChild(uploadForm); + + Umi.UI.InputMenus.AddButton('upload', 'Upload', function() { + uploadForm.click(); + }); + + Umi.UI.Elements.MessageInput.onpaste = function(ev) { + if(ev.clipboardData && ev.clipboardData.files.length > 0) + for(const file of ev.clipboardData.files) + doUpload(file); + }; + + document.body.ondragenter = function(ev) { + ev.preventDefault(); + ev.stopPropagation(); + }; + document.body.ondragover = function(ev) { + ev.preventDefault(); + ev.stopPropagation(); + }; + document.body.ondragleave = function(ev) { + ev.preventDefault(); + ev.stopPropagation(); + }; + document.body.ondrop = function(ev) { + ev.preventDefault(); + ev.stopPropagation(); + if(ev.dataTransfer && ev.dataTransfer.files.length > 0) + for(const file of ev.dataTransfer.files) { + if(file.name.slice(-5) === '.mami' + && confirm('This file appears to be a settings export. Do you want to import it?')) { + Umi.Settings.importFile(file); + return; + } + + doUpload(file); + } + }; + } + + Umi.UI.InputMenus.Add('markup', 'BB Code'); + Umi.UI.InputMenus.Add('emotes', 'Emoticons'); + + window.addEventListener('beforeunload', function(ev) { + if(Umi.Settings.get('closeTabConfirm')) { + ev.preventDefault(); + return ev.returnValue = 'Are you sure you want to close the tab?'; + } + }); + + + lo.setMessage('Connecting...'); + Umi.Server.open(views); + + if(window.dispatchEvent) + window.dispatchEvent(new Event('umi:connect')); +})(); + +#include compat.js diff --git a/src/mami.js/message.js b/src/mami.js/message.js new file mode 100644 index 0000000..51ce1a4 --- /dev/null +++ b/src/mami.js/message.js @@ -0,0 +1,23 @@ +Umi.Message = function(msgId, time, user, text, channel, highlight, botInfo, isAction) { + msgId = (msgId || '').toString(); + time = time === null ? new Date() : new Date(parseInt(time || 0) * 1000); + user = user || {}; + text = (text || '').toString(); + channel = (channel || '').toString(); + highlight = !!highlight; + isAction = !!isAction; + + const msgIdInt = parseInt(msgId); + + return { + getId: function() { return msgId; }, + getIdInt: function() { return msgIdInt; }, + getTime: function() { return time; }, + getUser: function() { return user; }, + getText: function() { return text; }, + getChannel: function() { return channel; }, + shouldHighlight: function() { return highlight; }, + getBotInfo: function() { return botInfo; }, + isAction: function() { return isAction; }, + }; +}; diff --git a/src/mami.js/messages.js b/src/mami.js/messages.js new file mode 100644 index 0000000..fb57688 --- /dev/null +++ b/src/mami.js/messages.js @@ -0,0 +1,73 @@ +#include channels.js +#include server.js + +Umi.Messages = (function() { + const msgs = new Map; + + const onSend = [], + onAdd = [], + onRemove = [], + onClear = []; + + return { + OnSend: onSend, + OnAdd: onAdd, + OnRemove: onRemove, + OnClear: onClear, + Send: function(text) { + Umi.Server.sendMessage(text); + for(const i in onSend) + onSend[i](text); + }, + Add: function(msg) { + const msgId = msg.getId(); + if(!msgs.has(msgId)) { + msgs.set(msgId, msg); + + for(const i in onAdd) + onAdd[i](msg); + + if(window.CustomEvent) + window.dispatchEvent(new CustomEvent('umi:message_add', { + detail: msg, + })); + } + }, + Remove: function(msg) { + const msgId = msg.getId(); + if(msgs.has(msgId)) { + msgs.delete(msgId); + + for(const i in onRemove) + onRemove[i](msg); + } + }, + Clear: function() { + msgs.clear(); + + for(const i in onClear) + onClear[i](); + }, + All: function(channel, excludeNull) { + if(!channel) + return Array.from(msgs.values()); + + if(!Umi.Channels.Get(channel)) + return null; + + const filtered = []; + msgs.forEach(function(msg) { + if(msg.getChannel() === channel || (!excludeNull && msg.getChannel() === null)) + filtered.push(msg); + }); + + return filtered.slice(Math.max(filtered.length - 30, 0)); + }, + Get: function(msgId) { + msgId = msgId.toString(); + if(msgs.has(msgId)) + return msgs.get(msgId); + return null; + }, + }; +})(); diff --git a/src/mami.js/mszauth.js b/src/mami.js/mszauth.js new file mode 100644 index 0000000..9861231 --- /dev/null +++ b/src/mami.js/mszauth.js @@ -0,0 +1,29 @@ +#include common.js +#include utility.js + +const MamiMisuzuAuth = (function() { + let userId = null, + authToken = null; + + return { + getUserId: function() { return userId; }, + getAuthToken: function() { return authToken; }, + getInfo: function() { + return { + method: 'Misuzu', + token: authToken, + }; + }, + update: async () => { + const resp = await $x.get(futami.get('token'), { authed: true, type: 'json' }); + + const body = resp.body(); + if(body.ok) { + userId = body.usr.toString(); + authToken = body.tkn; + } + + return body; + }, + }; +})(); diff --git a/src/mami.js/parsing.js b/src/mami.js/parsing.js new file mode 100644 index 0000000..e472edc --- /dev/null +++ b/src/mami.js/parsing.js @@ -0,0 +1,388 @@ +#include messages.js +#include settings.js +#include utility.js +#include ui/markup.js + +if(!Umi.Parser) Umi.Parser = {}; +if(!Umi.Parser.SockChatBBcode) Umi.Parser.SockChatBBcode = {}; + +Umi.Parsing = (function() { + const bbCodes = [ + { + tag: 'b', + replace: '{0}', + button: true, + }, + { + tag: 'i', + replace: '{0}', + button: true, + }, + { + tag: 'u', + replace: '{0}', + button: true, + }, + { + tag: 's', + replace: '{0}', + button: true, + }, + { + tag: 'quote', + replace: '{0}', + button: true, + }, + { + tag: 'code', + replace: '{0}', + button: true, + }, + { + tag: 'sjis', + replace: '{0}', + }, + { + tag: 'color', + hasArg: true, + stripArg: ';:{}<>&|\\/~\'"', + replace: '{1}', + isToggle: true, + button: true, + }, + { + tag: 'img', + stripText: '"\'', + replace: '{0} [Embed]', + button: true, + }, + { + tag: 'url', + stripText: '"\'', + replace: '{0}', + }, + { + tag: 'url', + hasArg: true, + stripArg: '"\'', + replace: '{1}', + button: true, + }, + { + tag: 'video', + stripText: '"\'', + replace: '{0} [Embed]', + button: true, + }, + { + tag: 'audio', + stripText: '"\'', + replace: '{0} [Embed]', + button: true, + }, + { + tag: 'spoiler', + stripText: '"\'', + replace: '*** HIDDEN *** [Reveal]', + button: true, + } + ]; + + const replaceAll = function(haystack, needle, replace, ignore) { + return haystack.replace( + new RegExp( + needle.replace(/([\/\,\!\\\^\$\{\}\[\]\(\)\.\*\+\?\|\<\>\-\&])/g, '\\$&'), + (ignore ? 'gi' : 'g') + ), + (typeof (replace) == 'string') + ? replace.replace(/\$/g, '$$$$') + : replace + ); + } + const stripChars = function(str, chars) { + if(!chars) + return str; + + for(let i = 0; i < chars.length; i++) + str = replaceAll(str, chars[i], ''); + + return str; + } + + const extractMotiv = function(elem) { + const msgId = parseInt(elem.parentNode.parentNode.parentNode.parentNode.id.substring(8)); + let topText = 'Top Text', + bottomText = 'Bottom Text'; + + const msg = Umi.Messages.Get(msgId); + if(msg) { + const msgText = msg.getText().replace(/\[(.*?)\](.*?)\[\/(.*?)\]/g, '').trim(); + + if(msgText.length > 0) { + const msgTextParts = msgText.split(' '), + topTextLength = Math.ceil(msgTextParts.length / 10), + topTextParts = msgTextParts.slice(0, topTextLength); + let bottomTextParts = null; + + if(msgTextParts.length === 1 || Math.random() > .7) { + bottomTextParts = msgTextParts; + } else { + bottomTextParts = msgTextParts.slice(topTextLength); + } + + topText = topTextParts.join(' '); + bottomText = bottomTextParts.join(' '); + } + } + + return { + top: topText, + bottom: bottomText, + }; + }; + const motivFrame = function(texts, body) { + return $e({ + attrs: { + style: { + display: 'inline-block', + textAlign: 'center', + fontFamily: '\'Times New Roman\', serif', + backgroundColor: 'black', + fontVariant: 'small-caps', + fontSize: '1.3em', + lineHeight: '1.4em', + }, + }, + child: [ + { + tag: 'div', + attrs: { + style: { + border: '3px double #fff', + maxWidth: '50vw', + maxHeight: '50vh', + marginTop: '30px', + marginLeft: '50px', + marginRight: '50px', + boxSizing: 'content-box', + display: 'inline-block', + }, + }, + child: body, + }, + { + tag: 'h1', + child: texts.top, + attrs: { + style: { + color: '#fff', + textDecoration: 'none !important', + margin: '10px', + textTransform: 'uppercase', + }, + }, + }, + { + tag: 'p', + child: texts.bottom, + attrs: { + style: { + color: '#fff', + textDecoration: 'none !important', + margin: '10px', + }, + }, + }, + ], + }); + }; + + const toggleImage = function(element) { + const url = element.parentElement.title, + container = element.parentElement.getElementsByTagName('span')[0], + anchor = container.getElementsByTagName('a')[0], + isEmbedded = container.title !== 'link'; + + if(isEmbedded) { + container.title = 'link'; + element.textContent = 'Embed'; + element.dataset.embed = '0'; + anchor.textContent = url; + } else { + container.title = 'image'; + element.textContent = 'Remove'; + element.dataset.embed = '1'; + + let html = $e({ + tag: 'img', + attrs: { + src: url, + alt: url, + style: { + maxWidth: '50vw', + maxHeight: '50vh', + verticalAlign: 'middle', + }, + }, + }); + + if(Umi.Settings.get('motivationalImages')) + html = motivFrame( + extractMotiv(element), + html + ); + + anchor.textContent = ''; + anchor.appendChild(html); + } + }; + const toggleAudio = function(element) { + const url = element.parentElement.title, + container = element.parentElement.getElementsByTagName('span')[0], + isEmbedded = container.title !== 'link'; + + if(isEmbedded) { + container.title = 'link'; + element.dataset.embed = '0'; + element.textContent = 'Embed'; + container.textContent = ''; + container.appendChild($e({ + tag: 'a', + attrs: { + href: url, + target: '_blank', + rel: 'nofollow noreferrer noopener', + className: 'markup__link', + }, + child: url, + })); + } else { + container.title = 'audio'; + element.dataset.embed = '1'; + element.textContent = 'Remove'; + container.textContent = ''; + container.appendChild($e({ + tag: 'audio', + attrs: { + src: url, + controls: true, + }, + })); + } + }; + const toggleVideo = function(element) { + const url = element.parentElement.title, + container = element.parentElement.getElementsByTagName('span')[0], + isEmbedded = container.title !== 'link'; + + if(isEmbedded) { + container.title = 'link'; + element.dataset.embed = '0'; + element.textContent = 'Embed'; + container.textContent = ''; + container.appendChild($e({ + tag: 'a', + attrs: { + href: url, + target: '_blank', + rel: 'nofollow noreferrer noopener', + className: 'markup__link', + }, + child: url, + })); + } else { + container.title = 'video'; + element.dataset.embed = '1'; + element.textContent = 'Remove'; + + let html = $e({ + tag: 'video', + attrs: { + src: url, + controls: true, + style: { + maxWidth: '800px', + maxHeight: '600px', + }, + }, + }); + + if(Umi.Settings.get('motivationalVideos')) + html = motivFrame( + extractMotiv(element), + html + ); + + container.textContent = ''; + container.appendChild(html); + } + }; + const toggleSpoiler = function(element) { + const container = element.parentElement, + target = container.querySelector('span'); + + if(container.dataset.revealed === 'yes') { + container.dataset.revealed = 'no'; + target.textContent = '*** HIDDEN ***'; + element.textContent = 'Reveal'; + } else { + container.dataset.revealed = 'yes'; + target.textContent = container.dataset.shit; + element.textContent = 'Hide'; + } + }; + + Umi.Parser.SockChatBBcode.EmbedImage = toggleImage; + Umi.Parser.SockChatBBcode.EmbedAudio = toggleAudio; + Umi.Parser.SockChatBBcode.EmbedVideo = toggleVideo; + Umi.Parser.SockChatBBcode.ToggleSpoiler = toggleSpoiler; + + return { + Init: function() { + for (let i = 0; i < bbCodes.length; i++) { + const bbCode = bbCodes[i]; + if(!bbCode.button) + continue; + + const start = '[{0}]'.replace('{0}', bbCode.tag + (bbCode.arg ? '=' : '')), end = '[/{0}]'.replace('{0}', bbCode.tag); + const text = (bbCode.tag.length > 1 ? bbCode.tag.substring(0, 1).toUpperCase() + bbCode.tag.substring(1) : bbCode.tag).replace('Color', 'Colour'); + + Umi.UI.Markup.Add(bbCode.tag, text, start, end); + } + }, + Parse: function(element, message) { + for(let i = 0; i < bbCodes.length; i++) { + const bbCode = bbCodes[i]; + + if(!bbCode.hasArg) { + let at = 0; + while((at = element.innerHTML.indexOf('[' + bbCode.tag + ']', at)) != -1) { + let end; + if((end = element.innerHTML.indexOf('[/' + bbCode.tag + ']', at)) != -1) { + const inner = stripChars(element.innerHTML.substring(at + ('[' + bbCode.tag + ']').length, end), bbCode.stripText == undefined ? '' : bbCode.stripText); + const replace = replaceAll(bbCode.replace, '{0}', inner); + element.innerHTML = element.innerHTML.substring(0, at) + replace + element.innerHTML.substring(end + ('[/' + bbCode.tag + ']').length); + at += replace.length; + } else break; + } + } else { + let at = 0; + while((at = element.innerHTML.indexOf('[' + bbCode.tag + '=', at)) != -1) { + let start, end; + if((start = element.innerHTML.indexOf(']', at)) != -1) { + if((end = element.innerHTML.indexOf('[/' + bbCode.tag + ']', start)) != -1) { + const arg = stripChars(element.innerHTML.substring(at + ('[' + bbCode.tag + '=').length, start), '[]' + (bbCode.stripArg == undefined ? '' : bbCode.stripArg)); + const inner = stripChars(element.innerHTML.substring(start + 1, end), bbCode.stripText == undefined ? '' : bbCode.stripText); + const replace = replaceAll(replaceAll(bbCode.replace, '{1}', inner), '{0}', arg); + element.innerHTML = element.innerHTML.substring(0, at) + replace + element.innerHTML.substring(end + ('[/' + bbCode.tag + ']').length); + at += replace.length; + } else break; + } else break; + } + } + } + + return element; + }, + }; +})(); diff --git a/src/mami.js/rng.js b/src/mami.js/rng.js new file mode 100644 index 0000000..c67c700 --- /dev/null +++ b/src/mami.js/rng.js @@ -0,0 +1,119 @@ +// Reimplementation of https://source.dot.net/#System.Private.CoreLib/Random.cs,bb77e610694e64ca +const MamiRNG = function(seed) { + const MBIG = 0x7FFFFFFF; + const MSEED = 161803398; + + if((typeof seed).toLowerCase() !== 'number') + seed = Math.round(Date.now() / 1000); + + const seedArray = new Int32Array(56); + + const vars = new Int32Array(2); + const mjVal = 0, mkVal = 1; + let ii = 0; + + vars[mjVal] = seed; + vars[mjVal] = (vars[mjVal] === -0x80000000) ? 0x7FFFFFFF : Math.abs(vars[mjVal]); + vars[mjVal] = MSEED - vars[mjVal]; + seedArray[55] = vars[mjVal]; + vars[mkVal] = 1; + + for(let i = 1; i < 55; i++) { + if((ii += 21) >= 55) + ii -= 55; + + seedArray[ii] = vars[mkVal]; + vars[mkVal] = vars[mjVal] - vars[mkVal]; + + if(vars[mkVal] < 0) + vars[mkVal] += MBIG; + + vars[mjVal] = seedArray[ii]; + } + + for(let k = 1; k < 5; k++) { + for(let i = 0; i < 56; i++) { + let n = i + 30; + + if(n >= 55) + n -= 55; + + seedArray[i] -= seedArray[1 + n]; + + if(seedArray[i] < 0) + seedArray[i] += MBIG; + } + } + + let inext = 0, + inextp = 21; + + const internalSample = function() { + const retVal = new Int32Array(1); + let locINext = inext, + locINextp = inextp; + + if(++locINext >= 56) + locINext = 1; + if(++locINextp >= 56) + locINextp = 1; + + retVal[0] = seedArray[locINext]; + retVal[0] -= seedArray[locINextp]; + + if(retVal[0] == MBIG) + retVal[0]--; + if(retVal[0] < 0) + retVal[0] += MBIG; + + seedArray[locINext] = retVal[0]; + inext = locINext; + inextp = locINextp; + + return retVal[0]; + }; + + const sample = function() { + return internalSample() * (1.0 / MBIG); + }; + + return { + sample: sample, + next: function(minValue, maxValue) { + let hasMinVal = (typeof minValue).toLowerCase() === 'number', + hasMaxVal = (typeof maxValue).toLowerCase() === 'number'; + const vars = new Int32Array(3), + minVal = 0, maxVal = 1, retVal = 2; + + if(hasMinVal) { + if(!hasMaxVal) { + hasMinVal = false; + hasMaxVal = true; + vars[maxVal] = minValue; + } else { + vars[minVal] = minValue; + vars[maxVal] = maxValue; + } + } + + if(hasMaxVal) { + if(hasMinVal) { + if(vars[minVal] > vars[maxVal]) + throw 'Argument out of range.'; + + const range = vars[maxVal] - vars[minVal]; + + vars[retVal] = sample() * range; + vars[retVal] += vars[minVal]; + + return vars[retVal]; + } + + vars[retVal] = sample() * vars[maxVal]; + return vars[retVal]; + } + + return internalSample(); + }, + }; +}; diff --git a/src/mami.js/server.js b/src/mami.js/server.js new file mode 100644 index 0000000..787cabb --- /dev/null +++ b/src/mami.js/server.js @@ -0,0 +1,26 @@ +#include sockchat_old.js + +Umi.Server = (function() { + let proto = null; + + return { + open: function(...args) { + proto = new Umi.Protocol.SockChat.Protocol(...args); + proto.open(); + }, + close: function() { + if(!proto) + return; + proto.close(); + proto = null; + }, + sendMessage: function(text) { + if(!proto) + return; + text = (text || '').toString(); + if(!text) + return; + proto.sendMessage(text); + }, + }; +})(); diff --git a/src/mami.js/servers.js b/src/mami.js/servers.js new file mode 100644 index 0000000..315359e --- /dev/null +++ b/src/mami.js/servers.js @@ -0,0 +1,27 @@ +#include common.js +#include utility.js + +const UmiServers = (function() { + let servers = undefined, + index = Number.MAX_SAFE_INTEGER - 1; + + return { + getServer: function(callback) { + // FutamiCommon is delayed load + if(servers === undefined) { + const futamiServers = futami.get('servers'); + $as(futamiServers); + servers = futamiServers; + } + + if(++index >= servers.length) + index = 0; + + let server = servers[index]; + if(server.includes('//')) + server = location.protocol.replace('http', 'ws') + server; + + callback(server); + }, + }; +})(); diff --git a/src/mami.js/settings.js b/src/mami.js/settings.js new file mode 100644 index 0000000..0138601 --- /dev/null +++ b/src/mami.js/settings.js @@ -0,0 +1,871 @@ +#include common.js +#include emotes.js +#include txtrigs.js +#include utility.js +#include weeb.js +#include ui/emotes.js +#include ui/view.js +#include sound/sndpacks.js +#include sound/umisound.js +#include sound/osukeys.js + +// Add anything you use Umi.Settings with here, lookups should probably be restricted or something to make sure +const UmiSettings = { + categories: [ + { + id: 'interface', + name: 'Interface', + }, + { + id: 'text', + name: 'Text', + }, + { + id: 'notification', + name: 'Notification', + }, + { + id: 'sounds', + name: 'Sound', + }, + { + id: 'misc', + name: 'Misc', + }, + { + id: 'settings', + name: 'Settings', + }, + { + id: 'debug', + name: '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.", + } + ], + settings: [ + { + id: 'style', + name: 'Style', + category: 'interface', + type: 'select', + data: function() { return Umi.UI.View.AccentColours; }, + dataType: 'call', + mutable: true, + default: 'dark', + watcher: function(v, n, i) { + if(!i) Umi.UI.View.AccentReload(); + }, + }, + { + id: 'compactView', + name: 'Compact view', + category: 'interface', + type: 'checkbox', + mutable: true, + default: false, + watcher: function(v, n, i) { + if(!i) Umi.UI.View.AccentReload(); + }, + }, + { + id: 'autoScroll', + name: 'Scroll to latest message', + category: 'interface', + type: 'checkbox', + mutable: true, + default: true, + }, + { + id: 'closeTabConfirm', + name: 'Confirm tab close', + category: 'interface', + type: 'checkbox', + mutable: true, + default: false, + }, + { + id: 'showChannelList', + name: 'Show channel list', + category: 'interface', + type: 'checkbox', + mutable: true, + default: false, + }, + { + id: 'fancyInfo', + name: 'Fancy server messages', + category: 'interface', + type: 'checkbox', + mutable: true, + default: true, + }, + { + id: 'autoCloseUserContext', + name: 'Auto-close user menus', + category: 'interface', + type: 'checkbox', + mutable: true, + default: true, + }, + { + id: 'enableParser', + name: 'Parse markup', + category: 'text', + type: 'checkbox', + mutable: true, + default: true, + }, + { + id: 'enableEmoticons', + name: 'Parse emoticons', + category: 'text', + type: 'checkbox', + mutable: true, + default: true, + }, + { + id: 'autoParseUrls', + name: 'Auto detect links', + category: 'text', + type: 'checkbox', + mutable: true, + default: true, + }, + { + id: 'preventOverflow', + name: 'Prevent overflow', + category: 'text', + type: 'checkbox', + mutable: true, + default: false, + watcher: function(v) { + document.body.classList[v ? 'add' : 'remove']('prevent-overflow'); + }, + }, + { + id: 'expandTextBox', + name: 'Grow input box', + category: 'text', + type: 'checkbox', + mutable: true, + default: false, + }, + { + id: 'eepromAutoInsert', + name: 'Auto-insert uploads', + category: 'text', + type: 'checkbox', + mutable: true, + default: true, + }, + { + id: 'autoEmbedV1', + name: 'Auto-embed media', + category: 'text', + type: 'checkbox', + mutable: true, + default: false, + }, + { + id: 'soundEnable', + name: 'Enable sound', + category: 'sounds', + type: 'checkbox', + mutable: true, + default: true, + //virtual: true, only when no autoplay + watcher: function(v, n, i) { + if(v && !mami.hasSound()) { + mami.initSound(); + Umi.Settings.touch('soundVolume'); + Umi.Settings.touch('soundPack', true); + } + + if(mami.hasAudio()) + mami.getAudio().setMuted(!v); + }, + }, + { + id: 'soundPack', + name: 'Sound pack', + category: 'sounds', + type: 'select', + data: function() { + const packs = {}; + mami.getSoundPacks().forEachPack(function(pack) { + packs[pack.getName()] = pack.getTitle(); + }); + return packs; + }, + dataType: 'call', + mutable: true, + default: 'ajax-chat', + watcher: function(v, n, i) { + const packs = mami.getSoundPacks(); + if(!packs.hasPack(v)) { + Umi.Settings.remove(n); + return; + } + + const player = mami.getSoundPackPlayer(); + if(player !== null) { + player.loadPack(packs.getPack(v)); + if(!i) player.playEvent('server'); + } + }, + }, + { + id: 'soundVolume', + name: 'Sound volume', + category: 'sounds', + type: 'range', + mutable: true, + default: 80, + watcher: function(v, n, i) { + if(mami.hasAudio()) + mami.getAudio().setVolume(v / 100); + }, + }, + { + id: 'soundEnableJoin', + name: 'Play join sound', + category: 'sounds', + type: 'checkbox', + mutable: true, + default: true, + }, + { + id: 'soundEnableLeave', + name: 'Play leave sound', + category: 'sounds', + type: 'checkbox', + mutable: true, + default: true, + }, + { + id: 'soundEnableError', + name: 'Play error sound', + category: 'sounds', + type: 'checkbox', + mutable: true, + default: true, + }, + { + id: 'soundEnableServer', + name: 'Play server message sound', + category: 'sounds', + type: 'checkbox', + mutable: true, + default: true, + }, + { + id: 'soundEnableIncoming', + name: 'Play receive message sound', + category: 'sounds', + type: 'checkbox', + mutable: true, + default: true, + }, + { + id: 'onlySoundOnMention', + name: 'Only play receive sound on mention', + category: 'sounds', + type: 'checkbox', + mutable: true, + default: false, + }, + { + id: 'soundEnableOutgoing', + name: 'Play send message sound', + category: 'sounds', + type: 'checkbox', + mutable: true, + default: true, + }, + { + id: 'soundEnablePrivate', + name: 'Play private message sound', + category: 'sounds', + type: 'checkbox', + mutable: true, + default: true, + }, + { + id: 'soundEnableForceLeave', + name: 'Play kick sound', + category: 'sounds', + type: 'checkbox', + mutable: true, + default: true, + }, + { + id: 'minecraft', + name: 'Minecraft', + category: 'sounds', + type: 'select', + mutable: true, + dataType: 'object', + data: { + 'no': 'No Minecraft', + 'yes': 'Yes Minecraft', + 'old': 'Old Minecraft', + }, + default: 'no', + watcher: function(v, n, i) { + if(!i) Umi.Sound.Play('join'); + }, + }, + { + id: 'playSoundOnConnect', + name: 'Play join sound on connect', + category: 'sounds', + type: 'checkbox', + mutable: true, + default: false, + }, + { + id: 'windowsLiveMessenger', + name: 'Windows Live Messenger', + category: 'sounds', + type: 'checkbox', + mutable: true, + default: false, + }, + { + id: 'seinfeld', + name: 'Seinfeld', + category: 'sounds', + type: 'checkbox', + mutable: true, + default: false, + }, + { + id: 'flashTitle', + name: 'Strobe title on new message', + category: 'notification', + type: 'checkbox', + mutable: true, + default: true, + }, + { + id: 'showServerMsgInTitle', + name: 'Show server message in title', + category: 'notification', + type: 'checkbox', + mutable: true, + default: true, + }, + { + id: 'enableNotifications', + name: 'Show notifications', + category: 'notification', + type: 'checkbox', + mutable: true, + default: false, + watcher: function(v, n, i) { + if(!v) return; + + if(Notification.permission === 'granted' && Notification.permission !== 'denied') + return; + + Notification.requestPermission() + .then(perm => { + if(perm !== 'granted') + Umi.Settings.set('enableNotifications', false); + }); + }, + }, + { + id: 'notificationShowMessage', + name: 'Show contents of message', + category: 'notification', + type: 'checkbox', + mutable: true, + default: false, + }, + { + id: 'notificationTriggers', + name: 'Triggers', + category: 'notification', + type: 'text', + mutable: true, + default: '', + }, + { + id: 'playJokeSounds', + name: 'Run joke triggers', + category: 'misc', + type: 'checkbox', + mutable: true, + default: true, + watcher: function(v, n, i) { + if(v) { + const triggers = mami.getTextTriggers(); + if(!triggers.hasTriggers()) + futami.getJson('texttriggers').then(trigInfos => triggers.addTriggers(trigInfos)); + } + }, + }, + { + id: 'weeaboo', + name: 'Weeaboo', + category: 'misc', + type: 'checkbox', + mutable: true, + default: false, + watcher: function(v, n, i) { + if(v) Weeaboo.init(); + }, + }, + { + id: 'motivationalImages', + name: 'Make images motivational', + category: 'misc', + type: 'checkbox', + mutable: true, + default: false, + }, + { + id: 'motivationalVideos', + name: 'Make videos motivational', + category: 'misc', + type: 'checkbox', + mutable: true, + default: false, + }, + { + id: 'osuKeysV2', + name: 'osu! keyboard sounds', + category: 'misc', + type: 'select', + mutable: true, + dataType: 'object', + data: { + 'no': 'Off', + 'yes': 'On', + 'rng': 'On, random pitch', + }, + default: 'no', + watcher: function(v, n, i) { + // migrate old value + if(i && Umi.Settings.has('osuKeys')) { + Umi.Settings.set('osuKeysV2', Umi.Settings.get('osuKeys') ? 'yes' : 'no'); + Umi.Settings.remove('osuKeys'); + return; + } + + OsuKeys.setEnable(v !== 'no'); + OsuKeys.setRandomRate(v === 'rng'); + }, + }, + { + id: 'explosionRadius', + name: 'Messages to keep on clear', + category: 'misc', + type: 'number', + mutable: true, + default: 20, + }, + { + id: 'reloadEmoticons', + name: 'Reload emoticons', + category: 'misc', + type: 'button', + mutable: true, + dataType: 'void', + click: function() { + const emotes = futami.get('emotes'); + setTimeout(function() { + this.disabled = true; + + futami.getJson('emotes', true) + .then(emotes => { + MamiEmotes.clear(); + MamiEmotes.loadLegacy(emotes); + }) + .finally(() => { + Umi.UI.Emoticons.Init(); + this.disabled = false; + }); + }, 200); + }, + }, + { + id: 'reloadJokeTriggers', + name: 'Reload joke triggers', + category: 'misc', + type: 'button', + mutable: true, + dataType: 'void', + click: function() { + this.disabled = true; + + const triggers = mami.getTextTriggers(); + triggers.clearTriggers(); + + if(Umi.Settings.get('playJokeSounds')) + futami.getJson('texttriggers', true) + .then(trigInfos => triggers.addTriggers(trigInfos)) + .finally(() => this.disabled = false); + }, + }, + { + id: 'dumpPackets', + name: 'Dump packets to console', + category: 'debug', + type: 'checkbox', + mutable: true, + default: false, + }, + { + id: 'openLegacyChat', + name: 'Open compatibility client', + category: 'misc', + type: 'button', + mutable: true, + dataType: 'void', + click: function() { + const meow = $e('a', { href: window.AMI_URL, target: '_blank', style: { display: 'none' } }); + document.body.appendChild(meow); + meow.click(); + $r(meow); + }, + }, + { + id: 'neverUseWorker', + name: 'Never use Worker for connection', + category: 'debug', + type: 'checkbox', + mutable: true, + default: false, + emergencyReset: true, + confirm: "If you're here it likely means that you mistakenly believe that your browser doesn't suck. You may go ahead but if disabling this causes any annoyances for other users you will be expunged.", + }, + { + id: 'forceUseWorker', + name: 'Always use Worker for connection', + category: 'debug', + type: 'checkbox', + mutable: true, + default: false, + emergencyReset: true, + }, + { + id: 'marqueeAllNames', + name: 'Apply marquee on everyone', + category: 'debug', + type: 'checkbox', + mutable: true, + default: false, + }, + { + id: 'resolveUrls', + name: 'Resolve URL meta data', + category: 'debug', + type: 'checkbox', + mutable: true, + default: false, + }, + { + id: 'tmpDisableOldThemeSys', + name: 'Disable Old Theme System', + category: 'debug', + type: 'checkbox', + mutable: true, + default: false, + emergencyReset: true, + watcher: function(v, n, i) { + if(!i) Umi.UI.View.AccentReload(); + }, + }, + { + id: 'tmpSkipDomainPopUpThing', + name: 'Skip domain pop up thing', + category: 'debug', + type: 'checkbox', + mutable: true, + default: false, + emergencyReset: true, + }, + { + id: 'settingsImport', + name: 'Import settings', + category: 'settings', + type: 'button', + mutable: true, + dataType: 'void', + click: function() { + $ri('-mami-settings-import-field'); + + imp = $e('input', { + id: '-mami-settings-import-field', + type: 'file', + accept: '.mami', + style: { display: 'none' }, + }); + imp.addEventListener('change', function() { + if(imp.files.length > 0) + Umi.Settings.importFile(imp.files[0]); + + $r(imp); + }); + document.body.appendChild(imp); + imp.click(); + }, + }, + { + id: 'settingsExport', + name: 'Export settings', + category: 'settings', + type: 'button', + mutable: true, + dataType: 'void', + click: function() { + const data = { + a: 'Mami Settings Export', + v: 1, + d: [], + }; + + for(const setting of UmiSettings.settings) + if(setting.mutable && setting.type !== 'button') + data.d.push({ + i: setting.id, + v: Umi.Settings.get(setting.id) + }); + + const user = Umi.User.getCurrentUser(); + let fileName = 'settings.mami'; + + if(user !== null) + fileName = user.getName() + '\'s settings.mami'; + + const exp = $e('a', { + href: URL.createObjectURL(new Blob( + [btoa(JSON.stringify(data))], + { type: 'application/octet-stream' } + )), + download: fileName, + target: '_blank', + style: { display: 'none' } + }); + document.body.appendChild(exp); + exp.click(); + $r(exp); + }, + }, + { + id: 'settingsReset', + name: 'Reset settings', + category: 'settings', + type: 'button', + mutable: true, + dataType: 'void', + click: function() { + if(!confirm('This will reset all your settings to their defaults values. Are you sure you want to do this?')) + return; + + for(const setting of UmiSettings.settings) + if(setting.mutable) + Umi.Settings.remove(setting.id); + }, + }, + { + id: 'resetAudioContext', + name: 'Reset audio context', + category: 'debug', + type: 'button', + mutable: true, + dataType: 'void', + click: function() { + if(mami.hasAudio()) + mami.getAudio().resetContext(); + if(mami.hasSound()) + mami.getSound().clear(); + }, + }, + ], +}; + +Umi.Settings = function(metaData) { + let getRaw = null, setRaw = null, removeRaw = null; + const valid = [], mutable = [], locked = [], virtual = []; + const watchers = new Map, defaults = new Map, virtuals = new Map; + const prefix = 'umi-'; + + for(const setting of metaData) { + valid.push(setting.id); + if(setting.mutable && setting.dataType !== 'void') + mutable.push(setting.id); + if(setting.virtual) + virtual.push(setting.id); + if(setting.default) + defaults.set(setting.id, setting.default); + } + + getRaw = function(name) { + let value = null; + if(virtual.includes(name)) + value = virtuals.get(name); + else + value = localStorage.getItem(prefix + name); + return value === undefined ? null : JSON.parse(value); + }; + setRaw = function(name, value) { + value = JSON.stringify(value); + + if(virtual.includes(name)) + virtuals.set(name, value); + else + localStorage.setItem(prefix + name, value); + }; + removeRaw = function(name) { + virtuals.delete(name); + localStorage.removeItem(prefix + name); + }; + + removeRaw('cookiesMigrated'); + + const hasValue = function(name) { + if(!mutable.includes(name)) + return false; + + const value = getRaw(name); + return value !== null && value !== undefined; + }; + + const getValue = function(name) { + if(!valid.includes(name)) + return null; + + const value = mutable.includes(name) ? getRaw(name) : null; + if(value === null || value === undefined) + return defaults.get(name) || null; + + return value; + }; + + const setValue = function(name, value) { + if(!mutable.includes(name)) + return; + + if(locked.includes(name)) + return; + locked.push(name); + + if(getValue(name) !== value) { + if(value === defaults.get(name)) + removeRaw(name); + else + setRaw(name, value); + callWatcher(name); + } + + $ari(locked, name); + }; + + const callWatcher = function(name, initial) { + if(watchers.has(name)) { + const w = watchers.get(name), + v = getValue(name); + initial = !!initial; + for(const f of w) + f(v, name, initial); + } + }; + + return { + has: hasValue, + get: getValue, + set: setValue, + remove: function(name) { + if(!mutable.includes(name)) + return; + + if(locked.includes(name)) + return; + locked.push(name); + + removeRaw(name); + callWatcher(name); + + $ari(locked, name); + }, + toggle: function(name) { + setValue(name, !getValue(name)); + }, + touch: callWatcher, + watch: function(name, callback) { + if(!mutable.includes(name)) + return; + if(!watchers.has(name)) + watchers.set(name, []); + const callbacks = watchers.get(name); + if(!callbacks.includes(callback)) + callbacks.push(callback); + callback(getValue(name), name, true); + }, + unwatch: function(name, callback) { + if(!watchers.get(name)) + return; + $ari(watchers.get(name), callback); + }, + virtualise: function(name) { + if(mutable.includes(name) && !virtual.includes(name)) { + const value = getRaw(name); + virtual.push(name); + + if(value !== null && value !== undefined) + setRaw(name, value); + } + }, + importFile: function(file) { + const reader = new FileReader; + reader.addEventListener('load', function() { + let data = atob(reader.result); + if(!data) { + alert('This is not a settings export. (1)'); + return; + } + + data = JSON.parse(data); + if(!data) { + alert('This is not a settings export. (2)'); + return; + } + + if(!data.a || !data.v || data.a !== 'Mami Settings Export') { + alert('This is not a settings export. (3)'); + return; + } + + if(data.v < 1) { + alert('Version of this settings export cannot be interpreted.'); + return; + } + if(data.v > 1) { + alert('This settings export is not compatible with this version of the chat client.'); + return; + } + + if(!Array.isArray(data.d)) { + alert('Settings export contains invalid data.'); + return; + } + + if(confirm('Your current settings will be replaced with the ones in the export. Are you sure you want to continue?')) { + const settings = {}; + for(const setting of data.d) + if(setting.i) + settings[setting.i] = setting.v; + + for(const setting of UmiSettings.settings) + if(setting.mutable && setting.type !== 'button' && setting.id in settings) + setValue(setting.id, settings[setting.id]); + } + }); + reader.readAsText(file); + }, + }; +}; diff --git a/src/mami.js/sockchat_old.js b/src/mami.js/sockchat_old.js new file mode 100644 index 0000000..de9e364 --- /dev/null +++ b/src/mami.js/sockchat_old.js @@ -0,0 +1,968 @@ +#include channel.js +#include channels.js +#include common.js +#include message.js +#include messages.js +#include mszauth.js +#include user.js +#include users.js +#include parsing.js +#include servers.js +#include settings.js +#include txtrigs.js +#include websock.js +#include ui/emotes.js +#include ui/markup.js +#include ui/menus.js +#include ui/messages.jsx +#include ui/view.js +#include ui/loading-overlay.jsx +#include sound/umisound.js + +if(!Umi) window.Umi = {}; +if(!Umi.Protocol) Umi.Protocol = {}; +if(!Umi.Protocol.SockChat) Umi.Protocol.SockChat = {}; +if(!Umi.Protocol.SockChat.Protocol) Umi.Protocol.SockChat.Protocol = {}; + +Umi.Protocol.SockChat.Protocol = function(views) { + const pub = {}; + Umi.Protocol.SockChat.Protocol.Instance = pub; + + const chatBot = new Umi.User('-1', 'Server'); + + let noReconnect = false, + connectAttempts = 0, + wasKicked = false, + isRestarting = false; + + let userId = null, + channelName = null, + pmUserName = null; + + let sock = null; + + const send = function(opcode, data) { + if(!sock) return; + let msg = opcode; + if(data) msg += "\t" + data.join("\t"); + console.log(msg); + sock.send(msg); + }; + const sendPing = function() { + if(userId === null) + return; + lastPing = Date.now(); + send('0', [userId]); + }; + const sendAuth = function(args) { + if(userId !== null) + return; + send('1', args); + }; + const sendMessage = function(text) { + if(userId === null) + return; + + if(text.substring(0, 1) !== '/' && pmUserName !== null) + text = '/msg ' + pmUserName + ' ' + text; + + send('2', [userId, text]); + }; + + const switchChannel = function(name) { + if(channelName === name) + return; + + channelName = name; + sendMessage('/join ' + name); + }; + + const startKeepAlive = function() { + if(!sock) return; + sock.sendInterval("0\t" + userId, futami.get('ping') * 1000); + }; + const stopKeepAlive = function() { + if(!sock) return; + sock.clearIntervals(); + }; + + const getLoadingOverlay = async (icon, header, message) => { + const currentView = views.current(); + + if('setIcon' in currentView) { + currentView.setIcon(icon); + currentView.setHeader(header); + currentView.setMessage(message); + return currentView; + } + + const loading = new Umi.UI.LoadingOverlay(icon, header, message); + await views.push(loading); + + return loading; + }; + + const playBannedSfx = function() { + if(!mami.hasSound()) + return; + + const urls = { + mp3: '//static.flash.moe/sounds/touhou-death.mp3', + ogg: '//static.flash.moe/sounds/touhou-death.ogg' + }; + + mami.getSound().load('banSFX', urls, function(success, buffer) { + if(!success) { + console.log('Failed to load kick/ban SFX: ' + buffer); + return; + } + + buffer.createSource().play(); + }); + }; + const playBannedBgm = function(preload) { + if(!mami.hasSound()) + return; + + const urls = { + opus: '//static.flash.moe/sounds/players-score.opus', + caf: '//static.flash.moe/sounds/players-score.caf' + }; + + mami.getSound().load('banBGM', urls, function(success, buffer) { + if(!success) { + console.log('Failed to load kick/ban SFX: ' + buffer); + return; + } + + if(!preload) { + const source = buffer.createSource(); + source.setLoopStart(10.512); + source.setLoopEnd(38.074); + source.setLoop(); + source.play(); + } + }); + }; + + const onOpen = function(ev) { + if(Umi.Settings.get('dumpPackets')) + console.log(ev); + + wasKicked = false; + isRestarting = false; + + getLoadingOverlay('spinner', 'Loading...', 'Authenticating...'); + + const authInfo = MamiMisuzuAuth.getInfo(); + sendAuth([authInfo.method, authInfo.token]); + }; + + const closeReasons = { + '_1000': 'The connection has been ended.', + '_1001': 'Something went wrong on the server side.', + '_1002': 'Your client sent broken data to the server.', + '_1003': 'Your client sent data to the server that it doesn\'t understand.', + '_1005': 'No additional information was provided.', + '_1006': 'You lost connection unexpectedly!', + '_1007': 'Your client sent broken data to the server.', + '_1008': 'Your client did something the server did not agree with.', + '_1009': 'Your client sent too much data to the server at once.', + '_1011': 'Something went wrong on the server side.', + '_1012': 'The server is restarting, reconnecting soon...', + '_1013': 'You cannot connect to the server right now, try again later.', + '_1015': 'Your client and the server could not establish a secure connection.', + }; + + const onClose = function(ev) { + if(Umi.Settings.get('dumpPackets')) + console.log(ev); + + userId = null; + channelName = null; + pmUserName = null; + stopKeepAlive(); + + if(wasKicked) + return; + + let code = ev.code; + if(isRestarting && code === 1006) { + code = 1012; + } else if(code === 1012) + isRestarting = true; + + const msg = closeReasons['_' + code.toString()] + || ('Something caused an unexpected connection loss, the error code was: ' + ev.code.toString() + '.'); + + getLoadingOverlay('unlink', 'Disconnected!', msg); + + //Umi.Messages.Clear(); + Umi.Users.Clear(); + + connectAttempts = 0; + + setTimeout(function() { + beginConnecting(); + }, 5000); + }; + + const parseBotMessage = function(parts) { + let text = ''; + + switch(parts[1]) { + case 'silence': + text = 'You have been silenced!'; + break; + + case 'unsil': + text = 'You are no longer silenced!'; + break; + + case 'silok': + text = '{0} is now silenced.'.replace('{0}', parts[2]); + break; + + case 'usilok': + text = '{0} is no longer silenced.'.replace('{0}', parts[2]); + break; + + case 'flood': + text = '{0} got kicked for flood protection.'.replace('{0}', parts[2]); + break; + + case 'flwarn': + text = 'You are about to hit the flood limit! If you continue you will be kicked.'; + break; + + case 'unban': + text = '{0} is no longer banned.'.replace('{0}', parts[2]); + break; + + case 'banlist': + const blentries = parts[2].split(', '); + for(const i in blentries) + blentries[i] = blentries[i].slice(92, -4); + + text = 'Banned: {0}'.replace('{0}', blentries.join(', ')); + break; + + case 'who': + const wentries = parts[2].split(', '); + for(const i in wentries) { + const isSelf = wentries[i].includes(' style="font-weight: bold;"'); + wentries[i] = wentries[i].slice(isSelf ? 102 : 75, -4); + if(isSelf) wentries[i] += ' (You)'; + } + + text = 'Online: {0}'.replace('{0}', wentries.join(', ')); + break; + + case 'whochan': + const wcentries = (parts[3] || '').split(', '); + for(const i in wcentries) { + const isSelf = wcentries[i].includes(' style="font-weight: bold;"'); + wcentries[i] = wcentries[i].slice(isSelf ? 102 : 75, -4); + if(isSelf) wcentries[i] += ' (You)'; + } + + text = 'Online in {0}: {1}'.replace('{0}', parts[2]).replace('{1}', wcentries.join(', ')); + break; + + case 'silerr': + text = 'This user has already been silenced!'; + break; + + case 'usilerr': + text = "This user isn't silenced!"; + break; + + case 'silperr': + text = "You aren't allowed to silence this user!"; + break; + + case 'usilperr': + text = "You aren't allowed to remove the silence on this user!"; + break; + + case 'silself': + text = 'Why would you even try to silence yourself?'; + break; + + case 'delerr': + text = "You aren't allowed to delete this message!"; + break; + + case 'notban': + text = "{0} isn't banned!".replace('{0}', parts[2]); + break; + + case 'whoerr': + text = '{0} does not exist!'.replace('{0}', parts[2]); + break; + + case 'join': + text = '{0} joined.'.replace('{0}', parts[2]); + break; + + case 'jchan': + text = '{0} joined the channel.'.replace('{0}', parts[2]); + break; + + case 'leave': + text = '{0} left.'.replace('{0}', parts[2]); + break; + + case 'timeout': + text = '{0} exploded.'.replace('{0}', parts[2]); + break; + + case 'lchan': + text = '{0} left the channel.'.replace('{0}', parts[2]); + break; + + case 'kick': + text = '{0} got kicked.'.replace('{0}', parts[2]); + break; + + case 'nick': + text = '{0} changed their name to {1}.'.replace('{0}', parts[2]).replace('{1}', parts[3]); + break; + + case 'crchan': + text = '{0} has been created.'.replace('{0}', parts[2]); + break; + + case 'delchan': + text = '{0} has been deleted.'.replace('{0}', parts[2]); + break; + + case 'cpwdchan': + text = 'Changed the channel password!'; + break; + + case 'cprivchan': + text = 'Change access level of the channel!'; + break; + + case 'ipaddr': + text = 'IP of {0}: {1}'.replace('{0}', parts[2]).replace('{1}', parts[3]); + break; + + case 'cmdna': + text = "You aren't allowed to use '{0}'.".replace('{0}', parts[2]); + break; + + case 'nocmd': + text = "The command '{0}' does not exist.".replace('{0}', parts[2]); + break; + + case 'cmderr': + text = "You didn't format the command correctly!"; + break; + + case 'usernf': + text = "{0} isn't logged in to the chat right now!".replace('{0}', parts[2]); + break; + + case 'kickna': + text = "You aren't allowed to kick {0}!".replace('{0}', parts[2]); + break; + + case 'samechan': + text = 'You are already in {0}!'.replace('{0}', parts[2]); + break; + + case 'ipchan': + case 'nochan': + text = "{0} doesn't exist!".replace('{0}', parts[2]); + break; + + case 'nopwchan': + text = "{0} has a password! Use '/join {0} [password]'.".replace('{0}', parts[2]); + break; + + case 'ipwchan': + text = "Wrong password! Couldn't join {0}.".replace('{0}', parts[2]); + break; + + case 'inchan': + text = "Channel names can't start with @ or *!"; + break; + + case 'nischan': + text = '{0} already exists!'.replace('{0}', parts[2]); + break; + + case 'ndchan': + text = "You aren't allowed to delete {0}!".replace('{0}', parts[2]); + break; + + case 'namchan': + text = "You aren't allowed to edit {0}!".replace('{0}', parts[2]); + break; + + case 'nameinuse': + text = '{0} is currently taken!'.replace('{0}', parts[2]); + break; + + case 'rankerr': + text = "You can't set the access level of a channel higher than your own!"; + break; + + case 'reconnect': + text = 'Connection lost! Attempting to reconnect...'; + break; + + case 'generr': + text = 'Something happened.'; + break; + + case 'say': + default: + text = parts[2]; + } + + return text; + }; + + const unfuckText = function(text) { + const elem = document.createElement('div'); + elem.innerHTML = text.replace(/ /g, "\n"); + text = elem.innerText; + return text; + }; + + const onMessage = function(ev) { + const data = ev.data.split("\t"); + + if(Umi.Settings.get('dumpPackets')) + console.log(data); + + switch(data[0]) { + case '0': // ping + // nothing to do + break; + + case '1': // join + if(userId === null) { + if(data[1] == 'y') { + userId = data[2]; + channelName = data[6]; + + views.pop(ctx => MamiAnimate({ + async: true, + duration: 120, + easing: 'easeInOutSine', + start: () => { + ctx.toElem.style.zIndex = '100'; + ctx.fromElem.style.pointerEvents = 'none'; + ctx.fromElem.style.zIndex = '200'; + }, + update: t => { + ctx.fromElem.style.transform = `scale(${1 + (.25 * t)})`; + ctx.fromElem.style.opacity = 1 - (1 * t).toString(); + }, + end: () => { + ctx.toElem.style.zIndex = null; + }, + })); + + startKeepAlive(); + + if(Umi.Settings.get('playSoundOnConnect')) + Umi.Sound.Play('join'); + } else { + switch (data[2]) { + case 'joinfail': + wasKicked = true; + + const jfuntil = new Date(parseInt(data[3]) * 1000); + let banmsg = 'You were banned until {0}!'.replace('{0}', jfuntil.toLocaleString()); + + if(data[3] === '-1') + banmsg = 'You have been banned till the end of time, please try again in a different dimension.'; + + getLoadingOverlay('hammer', 'Banned!', banmsg); + playBannedBgm(); + break; + + case 'authfail': + let message = 'Authentication failed!'; + const afredir = futami.get('login'); + + if(afredir) { + message = 'Authentication failed, redirecting to login page...'; + setTimeout(function() { + location.assign(afredir); + }, 2000); + } + + getLoadingOverlay('cross', 'Failed!', message); + break; + + case 'sockfail': + getLoadingOverlay('cross', 'Failed!', 'Too many active connections.'); + break; + + default: + getLoadingOverlay('cross', 'Failed!', 'Connection failed!'); + break; + } + break; + } + } + + const juser = new Umi.User(data[2], data[3], data[4], data[5]); + + if(userId === juser.getId()) + Umi.User.setCurrentUser(juser); + + Umi.Users.Add(juser); + + if(Umi.User.isCurrentUser(juser)) { + Umi.UI.Markup.Reset(); + Umi.UI.Emoticons.Init(); + Umi.Parsing.Init(); + break; + } + + Umi.Messages.Add(new Umi.Message( + data[6], + data[1], + chatBot, + '{0} joined.'.replace('{0}', juser.getName()), + channelName, + false, + { + type: 'join', + isError: false, + args: [juser.getName()], + target: juser, + } + )); + Umi.Sound.Play('join'); + break; + + case '2': // message + let text = data[3], + sound = 'incoming'; + const muser = Umi.Users.Get(data[2]) || chatBot, + textParts = text.split("\f"); + isPM = data[5][4] === '1', + isAction = data[5][1] === '1' && data[5][3] === '0', + isBot = data[2] === '-1', + pmChannel = '@' + muser.getName(), + botInfo = {}; + + text = unfuckText(text); + + if(isBot) { + sound = 'server'; + botInfo.isError = textParts[0] !== '0'; + botInfo.type = textParts[1]; + botInfo.args = textParts.slice(2); + text = parseBotMessage(textParts); + + if(botInfo.isError) + sound = 'error'; + else if(textParts[1] === 'unban') + sound = 'unban'; + } + + if(isPM) { + if(muser.getId() === userId) { + const tmpMsg = text.split(' '); + pmChannel = '@' + tmpMsg.shift(); + text = tmpMsg.join(' '); + } + + if(muser.getName() !== pmUserName) + sound = 'private'; + + if(Umi.Channels.Get(pmChannel) === null) + Umi.Channels.Add(new Umi.Channel(pmChannel, false, true, true)); + + Umi.UI.Menus.Attention('channels'); + } + + if(muser.getId() === userId) + sound = 'outgoing'; + + if(Umi.Settings.get('playJokeSounds')) + try { + const trigger = mami.getTextTriggers().getTrigger(text); + if(trigger.isSoundType()) { + sound = ''; + mami.playLibrarySound( + trigger.getRandomSoundName(), + null, + trigger.getVolume(), + trigger.getRate() + ); + } + } catch(ex) {} + + Umi.Messages.Add(new Umi.Message( + data[4], + data[1], + muser, + text, + isPM ? pmChannel : (data[6] || channelName), + false, + botInfo, + isAction + )); + + if(!Umi.Settings.get('onlySoundOnMention') && sound !== '') + Umi.Sound.Play(sound); + break; + + case '3': // leave + const luser = Umi.Users.Get(data[1]); + let ltext = '', + lsound = null; + + switch(data[3]) { + case 'flood': + ltext = '{0} got kicked for flood protection.'; + lsound = 'flood'; + break; + + case 'timeout': + ltext = '{0} exploded.'; + lsound = 'timeout'; + break; + + case 'kick': + ltext = '{0} got bludgeoned to death.'; + lsound = 'kick'; + break; + + case 'leave': + default: + ltext = '{0} left.'; + lsound = 'leave'; + break; + } + + Umi.Messages.Add(new Umi.Message( + data[5], + data[4], + chatBot, + ltext.replace('{0}', luser.getName()), + channelName, + false, + { + type: data[3], + isError: false, + args: [luser.getName()], + target: luser, + } + )); + Umi.Users.Remove(luser); + + if(lsound !== null) + Umi.Sound.Play(lsound); + break; + + case '4': // channel + switch(data[1]) { + case '0': + Umi.Channels.Add(new Umi.Channel(data[2], data[3] === '1', data[4] === '1')); + break; + + case '1': + const uchannel = Umi.Channels.Get(data[2]); + uchannel.setName(data[3]); + uchannel.setHasPassword(data[4] === '1'); + uchannel.setTemporary(data[5] === '1'); + Umi.Channels.Update(data[2], uchannel); + break; + + case '2': + Umi.Channels.Remove(Umi.Channels.Get(data[2])); + break; + } + break; + + case '5': // user move + switch(data[1]) { + case '0': + const umuser = new Umi.User(data[2], data[3], data[4], data[5]), + text = '{0} joined the channel.'; + + Umi.Users.Add(umuser); + Umi.Messages.Add(new Umi.Message( + data[6], + null, + chatBot, + text.replace('{0}', umuser.getName()), + channelName, + false, + { + type: 'jchan', + isError: false, + args: [ + umuser.getName() + ], + target: umuser, + } + )); + Umi.Sound.Play('join'); + break; + + case '1': + if(data[2] === userId) + return; + + const mouser = Umi.Users.Get(+data[2]), + motext = '{0} has left the channel.'; + + Umi.Messages.Add(new Umi.Message( + data[3], + null, + chatBot, + motext.replace('{0}', mouser.getName()), + channelName, + false, + { + type: 'lchan', + isError: false, + args: [ + mouser.getName() + ], + target: mouser, + } + )); + Umi.Sound.Play('leave'); + Umi.Users.Remove(mouser); + break; + + case '2': + Umi.Channels.Switch(Umi.Channels.Get(data[2])); + break; + } + break; + + case '6': // message delete + Umi.Messages.Remove(Umi.Messages.Get(data[1])); + break; + + case '7': // context populate + switch(data[1]) { + case '0': // users + const cpuamount = parseInt(data[2]); + let cpustart = 3; + + for(let i = 0; i < cpuamount; i++) { + const user = new Umi.User(data[cpustart], data[cpustart + 1], data[cpustart + 2], data[cpustart + 3]); + Umi.Users.Add(user); + cpustart += 5; + } + break; + + case '1': // message + let cmid = +data[8], + cmtext = data[7], + cmflags = data[10]; + const cmuser = new Umi.User(data[3], data[4], data[5], data[6]), + cmtextParts = cmtext.split("\f"), + cmbotInfo = {}; + + cmtext = unfuckText(cmtext); + + if(isNaN(cmid)) + cmid = -Math.ceil(Math.random() * 10000000000); + + if(cmtextParts[1]) { + cmbotInfo.isError = cmtextParts[0] !== '0'; + cmbotInfo.type = cmtextParts[1]; + cmbotInfo.args = cmtextParts.slice(2); + cmtext = parseBotMessage(cmtextParts); + } + + Umi.Messages.Add(new Umi.Message( + cmid, + data[2], + cmuser, + cmtext, + channelName, + false, + cmbotInfo, + cmflags[1] === '1' && cmflags[3] === '0', + )); + break; + + case '2': // channels + const ecpamount = +data[2]; + let ecpstart = 3; + + for(let i = 0; i < ecpamount; i++) { + const channel = new Umi.Channel( + data[ecpstart], + data[ecpstart + 1] === '1', + data[ecpstart + 2] === '1' + ); + Umi.Channels.Add(channel); + + if(channel.getName() === channelName) + Umi.Channels.Switch(channel); + + ecpstart = ecpstart + 3; + } + break; + } + break; + + case '8': // context clear + const cckeep = Umi.Users.Get(userId); + + switch(data[1]) { + case '0': + Umi.UI.Messages.RemoveAll(); + break; + + case '1': + Umi.Users.Clear(); + Umi.Users.Add(cckeep); + break; + + case '2': + Umi.Channels.Clear(); + break; + + case '3': + Umi.Messages.Clear(); + Umi.Users.Clear(); + Umi.Users.Add(cckeep); + break; + + case '4': + Umi.UI.Messages.RemoveAll(); + Umi.Users.Clear(); + Umi.Users.Add(cckeep); + Umi.Channels.Clear(); + break; + } + break; + + case '9': // baka + noReconnect = true; + wasKicked = true; + + const isBan = data[1] === '1'; + + let message = 'You were kicked, refresh to log back in!'; + if(isBan) { + if(data[2] === '-1') { + message = 'You have been banned till the end of time, please try again in a different dimension.'; + } else { + const until = new Date(parseInt(data[2]) * 1000); + message = 'You were banned until {0}!'.replace('{0}', until.toLocaleString()); + } + } + + + let icon, header; + if(isBan) { + icon = 'hammer'; + header = 'Banned!'; + } else { + icon = 'bomb'; + header = 'Kicked!'; + } + + const currentView = views.currentElement(); + + MamiAnimate({ + duration: 550, + easing: 'easeOutExpo', + start: function() { + playBannedBgm(true); + playBannedSfx(); + }, + update: function(t) { + const scale = 'scale(' + (1 - .5 * t).toString() + ', ' + (1 - 1 * t).toString() + ')'; + currentView.style.transform = scale; + }, + end: function() { + getLoadingOverlay(icon, header, message).then(() => { + playBannedBgm(); + + // there's currently no way to reconnect after a kick/ban so just dispose of the ui entirely + if(views.count() > 1) + views.shift(); + }); + }, + }); + break; + + case '10': // user update + const spuser = Umi.Users.Get(+data[1]); + spuser.setName(data[2]); + spuser.setColour(data[3]); + spuser.setPermissions(data[4]); + Umi.Users.Update(spuser.getId(), spuser); + break; + } + }; + + const beginConnecting = function() { + if(noReconnect) + return; + + ++connectAttempts; + + let str = 'Connecting to server...'; + if(connectAttempts > 1) + str += ' (Attempt ' + connectAttempts.toString() + ')'; + + getLoadingOverlay('spinner', 'Loading...', str); + + UmiServers.getServer(function(server) { + if(Umi.Settings.get('dumpPackets')) + console.log('Connecting to ' + server); + + sock = new UmiWebSocket(server, function(ev) { + switch(ev.act) { + case 'ws:open': + onOpen(ev); + break; + case 'ws:close': + onClose(ev); + break; + case 'ws:message': + onMessage(ev); + break; + default: + console.log(ev.data); + break; + } + }); + }); + }; + + Umi.Channels.OnSwitch.push(function(old, channel) { + if(channel.isUserChannel()) { + pmUserName = channel.getName().substring(1); + } else { + pmUserName = null; + switchChannel(channel.getName()); + } + }); + + pub.sendMessage = sendMessage; + + pub.open = function() { + noReconnect = false; + beginConnecting(); + }; + + pub.close = function() { + noReconnect = true; + sock.close(); + }; + + return pub; +}; diff --git a/src/mami.js/sound/osukeys.js b/src/mami.js/sound/osukeys.js new file mode 100644 index 0000000..ae013f2 --- /dev/null +++ b/src/mami.js/sound/osukeys.js @@ -0,0 +1,78 @@ +#include rng.js + +const OsuKeys = (function() { + const urlBase = '//static.flash.moe/sounds/'; + const rng = new MamiRNG; + let sndRng = false; + + const playSound = function(name) { + if(!mami.hasSound()) + return; + + const urls = { + ogg: urlBase + name + '.ogg', + mp3: urlBase + name + '.mp3', + }; + + mami.getSound().load('OsuKeys:' + name, urls, function(success, buffer) { + if(success) { + const source = buffer.createSource(); + if(sndRng) + source.setRate(1.8 - (rng.sample() * 1.5)); + source.play(); + } + }); + }; + + const playPress = function() { playSound('key-press-' + rng.next(1, 5).toString()); }; + const playCaps = function() { playSound('key-caps'); }; + const playConfirm = function() { playSound('key-confirm'); }; + const playDelete = function() { playSound('key-delete'); }; + const playMove = function() { playSound('key-movement'); }; + + const keyHandler = function(ev) { + switch(ev.key) { + case 'Enter': + case 'NumpadEnter': + playConfirm(); + break; + case 'Backspace': + case 'Delete': + playDelete(); + break; + case 'NumLock': + case 'CapsLock': + case 'ScrollLock': + playCaps(); + break; + case 'ArrowUp': + case 'ArrowDown': + case 'ArrowLeft': + case 'ArrowRight': + playMove(); + break; + case 'Control': + case 'Alt': + case 'Shift': + break; + default: + playPress(); + break; + } + }; + + return { + playConfirm: playConfirm, + playDelete: playDelete, + playCaps: playCaps, + playMove: playMove, + playPress: playPress, + setEnable: function(value) { + if(value) + window.addEventListener('keydown', keyHandler); + else + window.removeEventListener('keydown', keyHandler); + }, + setRandomRate: function(value) { sndRng = !!value; }, + }; +})(); diff --git a/src/mami.js/sound/seinfeld.js b/src/mami.js/sound/seinfeld.js new file mode 100644 index 0000000..ffbd56b --- /dev/null +++ b/src/mami.js/sound/seinfeld.js @@ -0,0 +1,17 @@ +#include rng.js + +const Seinfeld = (function() { + const urlBase = '//static.flash.moe/sounds/seinfeld/', sounds = 22, rng = new MamiRNG; + + return { + getRandom: function() { + const name = (rng.next(sounds) + 1).toString(), + url = urlBase + name; + + return { + opus: url + '.opus', + caf: url + '.caf', + }; + }, + }; +})(); diff --git a/src/mami.js/sound/sndlibrary.js b/src/mami.js/sound/sndlibrary.js new file mode 100644 index 0000000..a1f6003 --- /dev/null +++ b/src/mami.js/sound/sndlibrary.js @@ -0,0 +1,83 @@ +const MamiSoundInfo = function(name, isReadOnly, title, sources) { + name = (name || '').toString(); + isReadOnly = !!isReadOnly; + title = (title || ('Nameless Sound (' + name + ')')).toString(); + sources = sources || {}; + + return { + getName: function() { + return name; + }, + isReadOnly: function() { + return isReadOnly; + }, + getTitle: function() { + return title; + }, + getSources: function() { + return sources; + }, + }; +}; + +const MamiSoundLibrary = function() { + const sounds = new Map; + + const addSound = function(soundInfo) { + if(sounds.has(soundInfo.getName())) + throw 'a sound with that name has already been registered'; + sounds.set(soundInfo.getName(), soundInfo); + }; + + const loadSoundFormats = ['opus', 'caf', 'ogg', 'mp3', 'wav']; + const loadSound = function(soundInfo, readOnly) { + if(typeof soundInfo !== 'object') + throw 'soundInfo must be an object'; + if(typeof soundInfo.name !== 'string') + throw 'soundInfo must contain a name field'; + if(typeof soundInfo.sources !== 'object') + throw 'soundInfo must contain a sources field'; + + let sources = {}, + sCount = 0; + for(const fmt of loadSoundFormats) { + if(typeof soundInfo.sources[fmt] !== 'string') + continue; + sources[fmt] = soundInfo.sources[fmt]; + ++sCount; + } + + if(sCount < 1) + throw 'soundInfo does not contain any valid sources'; + + addSound(new MamiSoundInfo(soundInfo.name, readOnly, soundInfo.title, sources)); + }; + + return { + addSound: addSound, + loadSound: loadSound, + loadSounds: function(soundInfos, readOnly) { + for(const soundInfo of soundInfos) + loadSound(soundInfo, readOnly); + }, + removeSound: function(name) { + sounds.delete(name); + }, + clearSounds: function() { + sounds.clear(); + }, + forEachSound: function(body) { + if(typeof body !== 'function') + return; + sounds.forEach(body); + }, + hasSound: function(name) { + return sounds.has(name); + }, + getSound: function(name) { + if(!sounds.has(name)) + throw 'No sound with this name has been registered.'; + return sounds.get(name); + } + }; +}; diff --git a/src/mami.js/sound/sndmgr.js b/src/mami.js/sound/sndmgr.js new file mode 100644 index 0000000..074ad0f --- /dev/null +++ b/src/mami.js/sound/sndmgr.js @@ -0,0 +1,162 @@ +#include utility.js +#include sound/sndlibrary.js +#include sound/sndpacks.js + +const MamiSoundManager = function(context) { + const supported = [], fallback = [], + formats = { + opus: 'audio/ogg;codecs=opus', + ogg: 'audio/ogg;codecs=vorbis', + mp3: 'audio/mpeg;codecs=mp3', + caf: 'audio/x-caf;codecs=opus', + wav: 'audio/wav', + }; + + const loaded = new Map; + + (function() { + const elem = $e('audio'); + for(const name in formats) { + const format = formats[name], support = elem.canPlayType(format); + if(support === 'probably') + supported.push(name); + else if(support === 'maybe') + fallback.push(name); + } + })(); + + const pub = {}; + + const findSupportedUrl = function(urls) { + if(typeof urls === 'string') + return urls; + + // lol we're going backwards again + if(Array.isArray(urls)) { + const tmp = urls; + urls = {}; + + for(const item of tmp) { + let type = null; + + if(item.type) + type = item.type; + else { + switch(item.format) { + case 'audio/mpeg': + case 'audio/mp3': + type = 'mp3'; + break; + case 'audio/ogg': // could also be opus, oops! + type = 'ogg'; + break; + case 'audio/opus': // isn't real lol + type = 'opus'; + break; + case 'audio/x-caf': + type = 'caf'; + break; + } + } + + if(type === null || type === undefined) + continue; + + urls[type] = item.url; + } + } + + // check "probably" formats + let url = null; + for(const type of supported) { + if(type in urls) { + url = urls[type]; + break; + } + } + + // check "maybe" formats + if(url === null) + for(const type of fallback) { + if(type in urls) { + url = urls[type]; + break; + } + } + + return url; + }; + + pub.findSupportedUrl = findSupportedUrl; + + pub.loadAnonymous = function(urls, callback) { + const url = findSupportedUrl(urls); + if(url === null) { + callback.call(pub, false, 'No supported audio format could be determined.'); + return; + } + + context.createBuffer(url, function(success, buffer) { + callback.call(pub, success, buffer); + }); + }; + + pub.load = function(name, urls, callback) { + const hasCallback = typeof callback === 'function'; + + if(loaded.has(name)) { + if(hasCallback) + callback.call(pub, true, loaded.get(name)); + return; + } + + const url = findSupportedUrl(urls); + if(url === null) { + const msg = 'No supported audio format could be determined.'; + if(hasCallback) + callback.call(pub, false, msg); + else + throw msg; + return; + } + + context.createBuffer(url, function(success, buffer) { + if(!success) { + if(hasCallback) + callback.call(pub, success, buffer); + else // not actually sure if this will do anything but maybe it'll show in the console + throw buffer; + return; + } + + loaded.set(name, buffer); + + if(hasCallback) + callback.call(pub, success, buffer); + }); + }; + + pub.unload = function(name) { + loaded.delete(name); + }; + pub.reset = function() { + loaded.clear(); + }; + + pub.play = function(name) { + if(loaded.has(name)) + loaded.get(name).createSource().play(); + }; + + pub.isLoaded = function(name) { + return loaded.has(name); + }; + + pub.get = function(name) { + if(!loaded.has(name)) + return null; + return loaded.get(name).createSource(); + }; + + return pub; +}; diff --git a/src/mami.js/sound/sndpacks.js b/src/mami.js/sound/sndpacks.js new file mode 100644 index 0000000..40a837d --- /dev/null +++ b/src/mami.js/sound/sndpacks.js @@ -0,0 +1,197 @@ +#include settings.js +#include sound/sndlibrary.js + +const MamiSoundPack = function(name, isReadOnly, title, events) { + name = (name || '').toString(); + isReadOnly = !!isReadOnly; + title = (title || '').toString(); + events = events || new Map; + + return { + getName: function() { + return name; + }, + isReadOnly: function() { + return isReadOnly; + }, + getTitle: function() { + return title; + }, + setTitle: function(newTitle) { + if(isReadOnly) + throw 'Cannot edit read only sound pack.'; + title = (newTitle || '').toString(); + }, + getEventNames: function() { + return Array.from(events.keys()); + }, + hasEventSound: function(eventName) { + return events.has(eventName); + }, + getEventSound: function(eventName) { + if(!events.has(eventName)) + throw 'event not registered'; + return events.get(eventName); + }, + setEventSound: function(eventName, soundName) { + events.set( + (eventName || '').toString(), + (soundName || '').toString() + ); + }, + removeEventSound: function(eventName) { + events.delete(eventName); + }, + clearEventSounds: function() { + events.clear(); + }, + clone: function(newName, readOnly) { + return new MamiSoundPack(newName, readOnly, 'Clone of ' + title, new Map(events)); + }, + }; +}; + +const MamiSoundPackPlayer = function(soundMgr, sndLibrary) { + const pub = {}; + let pack = null; + + pub.loadPack = function(packInfo) { + if(typeof pack !== 'object' && typeof pack.getEventSound !== 'function') + throw 'pack is not a valid soundpack'; + pack = packInfo; + }; + + pub.unloadPack = function() { + pack = null; + buffers.clear(); + }; + + pub.hasPack = function() { + return pack !== null; + }; + + const playSource = function(source, hasCallback, callback) { + if(hasCallback) + source.whenEnded(function() { + callback(true); + }); + source.play(); + }; + + const handlePlayError = function(ex, hasCallback, callback) { + console.error(ex); + if(hasCallback) + callback(false, ex); + }; + + pub.hasEvent = function(eventName) { + return pack !== null && pack.hasEventSound(eventName); + }; + + pub.playEvent = function(eventName, callback) { + if(pack === null) + return; + + if(Array.isArray(eventName)) { + const names = eventName; + eventName = null; + + for(const name of names) { + if(pack.hasEventSound(name)) { + eventName = name; + break; + } + } + + if(eventName === null) + return; + } else if(!pack.hasEventSound(eventName)) + return; + + const hasCallback = typeof callback === 'function'; + + try { + const soundInfo = sndLibrary.getSound(pack.getEventSound(eventName)), + soundName = soundInfo.getName(); + + if(soundMgr.isLoaded(soundName)) { + playSource(soundMgr.get(soundName), hasCallback, callback); + } else { + soundMgr.load(soundName, soundInfo.getSources(), function(success, buffer) { + if(success) + playSource(buffer.createSource(), hasCallback, callback); + else + handlePlayError(buffer, hasCallback, callback); + }); + } + } catch(ex) { + handlePlayError(ex, hasCallback, callback); + } + }; + + return pub; +}; + +const MamiSoundPacks = function() { + const packs = new Map; + + const addPack = function(packInfo) { + if(packs.has(packInfo.getName())) + throw 'a pack with that name has already been registered'; + packs.set(packInfo.getName(), packInfo); + }; + + const loadPack = function(packInfo, readOnly) { + if(typeof packInfo !== 'object') + throw 'packInfo must be an object'; + if(typeof packInfo.name !== 'string') + throw 'packInfo must contain a name field'; + if(typeof packInfo.events !== 'object') + throw 'packInfo must contain a events field'; + + const events = new Map; + for(const eventName in packInfo.events) { + if(typeof packInfo.events[eventName] !== 'string') + continue; + events.set(eventName, packInfo.events[eventName]); + } + + if(events.size < 1) + throw 'packInfo does not contain any valid events'; + + addPack(new MamiSoundPack( + packInfo.name, + readOnly, + packInfo.title, + events + )); + }; + + return { + addPack: addPack, + loadPack: loadPack, + loadPacks: function(packInfos, readOnly) { + for(const packInfo of packInfos) + loadPack(packInfo, readOnly); + }, + removePack: function(name) { + packs.delete(name); + }, + clearPacks: function() { + packs.clear(); + }, + forEachPack: function(body) { + if(typeof body !== 'function') + return; + packs.forEach(body); + }, + hasPack: function(name) { + return packs.has(name); + }, + getPack: function(name) { + if(!packs.has(name)) + throw 'No pack with this name has been registered.'; + return packs.get(name); + }, + }; +}; diff --git a/src/mami.js/sound/umisound.js b/src/mami.js/sound/umisound.js new file mode 100644 index 0000000..fb25142 --- /dev/null +++ b/src/mami.js/sound/umisound.js @@ -0,0 +1,129 @@ +#include settings.js +#include audio/context.js +#include sound/sndpacks.js +#include sound/seinfeld.js + +Umi.Sound = (function() { + return { + Play: function(sound) { + if(!sound || sound === 'none' || !mami.hasSound()) + return; + + const sndPackPlay = mami.getSoundPackPlayer(); + + switch(sound) { + case 'join': + if(!Umi.Settings.get('soundEnableJoin')) + return; + + if(Umi.Settings.get('seinfeld')) { + mami.playUrlSound(Seinfeld.getRandom()); + break; + } + + switch(Umi.Settings.get('minecraft')) { + case 'yes': + mami.playLibrarySound('minecraft:door:open'); + break; + + case 'old': + mami.playLibrarySound('minecraft:door:open-old'); + break; + + default: + sndPackPlay.playEvent('join'); + break; + } + break; + + case 'leave': + if(!Umi.Settings.get('soundEnableLeave')) + return; + + switch(Umi.Settings.get('minecraft')) { + case 'yes': + mami.playLibrarySound('minecraft:door:close'); + break; + + case 'old': + mami.playLibrarySound('minecraft:door:close-old'); + break; + + default: + sndPackPlay.playEvent('leave'); + break; + } + break; + + case 'error': + if(!Umi.Settings.get('soundEnableError')) + return; + + sndPackPlay.playEvent('error'); + break; + + case 'server': + if(!Umi.Settings.get('soundEnableServer')) + return; + + sndPackPlay.playEvent('server'); + break; + + case 'unban': + if(!Umi.Settings.get('soundEnableServer')) + return; + + sndPackPlay.playEvent(['unban', 'server']); + break; + + case 'incoming': + if(!Umi.Settings.get('soundEnableIncoming')) + return; + + if(Umi.Settings.get('windowsLiveMessenger')) { + mami.playLibrarySound('msn:incoming'); + } else { + sndPackPlay.playEvent('incoming'); + } + break; + + case 'outgoing': + if(!Umi.Settings.get('soundEnableOutgoing')) + return; + + sndPackPlay.playEvent('outgoing'); + break; + + case 'private': + case 'incoming-priv': + if(!Umi.Settings.get('soundEnablePrivate')) + return; + + sndPackPlay.playEvent(['incoming-priv', 'incoming']); + break; + + case 'flood': + if(!Umi.Settings.get('soundEnableForceLeave')) + return; + + sndPackPlay.playEvent(['flood', 'kick', 'leave']); + break; + + case 'timeout': + if(!Umi.Settings.get('soundEnableForceLeave')) + return; + + sndPackPlay.playEvent(['timeout', 'leave']); + break; + + case 'kick': + case 'forceLeave': + if(!Umi.Settings.get('soundEnableForceLeave')) + return; + + sndPackPlay.playEvent(['kick', 'leave']); + break; + } + }, + }; +})(); diff --git a/src/mami.js/themes.js b/src/mami.js/themes.js new file mode 100644 index 0000000..7af7698 --- /dev/null +++ b/src/mami.js/themes.js @@ -0,0 +1,338 @@ +// try to reduce these and create variations +const UmiThemes = [ + { + id: 'dark', + name: 'Dark', + scheme: 'dark', + colours: { + 'main-accent': 0x8559A5, + 'main-background': 0x050505, + 'main-colour': 0xFFFFFF, + 'scrollbar-foreground': 0x212121, + 'scrollbar-background': 0x111111, + 'chat-background': 0x111111, + 'chat-box-shadow': 0x111111, + 'message-avatar-background': 0, + 'message-separator': 0x222222, + 'message-time-colour': 0x888888, + 'message-compact-alternate-background': 0x212121, + 'message-compact-highlight': 0x3A3A3A, + 'sidebar-background': 0x111111, + 'sidebar-background-current': 0x222222, + 'sidebar-background-highlight': 0x444444, + 'sidebar-background-option': 0, + 'sidebar-box-shadow': 0x111111, + 'sidebar-selector-background-begin': 0x212121, + 'sidebar-selector-background-end': 0x1A1A1A, + 'sidebar-selector-background-hover': 0x222222, + 'sidebar-selector-background-active': 0x111111, + 'sidebar-selector-attention-shadow': 0x96B6D3, + 'input-background': 0x111111, + 'input-box-shadow': 0x111111, + 'input-colour': 0xFFFFFF, + 'input-border': 0x222222, + 'input-send': 0x222222, + 'input-button-hover': 0x2A2A2A, + 'input-button-active': 0x1A1A1A, + 'input-menu-background': 0x222222, + 'input-menu-box-shadow': 0x222222, + 'input-menu-button': 0x333333, + 'input-menu-button-hover': 0x444444, + 'input-menu-button-active': 0x3A3A3A, + 'settings-category-title-begin': 0x222222, + 'settings-category-title-end': 0x111111, + 'settings-input-border': 0x333333, + 'settings-input-background': 0x111111, + 'settings-input-colour': 0xFFFFFF, + 'settings-input-focus': 0x222222, + 'settings-markup-link-colour': 0x1E90FF, + }, + sizes: { + 'chat-box-shadow-y': 1, + 'chat-box-shadow-blur': 4, + 'sidebar-box-shadow-y': 1, + 'sidebar-box-shadow-blur': 4, + 'sidebar-selector-attention-shadow-blur': 10, + 'input-box-shadow-y': 1, + 'input-box-shadow-blur': 4, + 'input-menu-box-shadow-y': 1, + 'input-menu-box-shadow-blur': 4, + }, + }, + { + id: 'light', + name: 'Light', + scheme: 'light', + colours: { + 'main-accent': 0x8559A5, + 'main-background': 0xFFFFFF, + 'main-colour': 0, + 'scrollbar-foreground': 0xDDDDDD, + 'scrollbar-background': 0xEEEEEE, + 'chat-background': 0xEEEEEE, + 'chat-box-shadow': 0xEEEEEE, + 'message-avatar-background': 0xFFFFFF, + 'message-separator': 0xDDDDDD, + 'message-time-colour': 0x777777, + 'message-compact-alternate-background': 0xDEDEDE, + 'message-compact-highlight': 0xD9D9D9, + 'sidebar-background': 0xEEEEEE, + 'sidebar-background-current': 0xDDDDDD, + 'sidebar-background-highlight': 0xBBBBBB, + 'sidebar-background-option': 0xFFFFFF, + 'sidebar-box-shadow': 0xEEEEEE, + 'sidebar-selector-background-begin': 0xDEDEDE, + 'sidebar-selector-background-end': 0xE9E9E9, + 'sidebar-selector-background-hover': 0xDDDDDD, + 'sidebar-selector-background-active': 0xEEEEEE, + 'sidebar-selector-attention-shadow': 0x96B6D3, + 'input-background': 0xEEEEEE, + 'input-box-shadow': 0xEEEEEE, + 'input-colour': 0, + 'input-border': 0xDDDDDD, + 'input-send': 0xDDDDDD, + 'input-button-hover': 0xD9D9D9, + 'input-button-active': 0xE9E9E9, + 'input-menu-background': 0xDDDDDD, + 'input-menu-box-shadow': 0xDDDDDD, + 'input-menu-button': 0xCCCCCC, + 'input-menu-button-hover': 0xBBBBBB, + 'input-menu-button-active': 0xC9C9C9, + 'settings-category-title-begin': 0xDDDDDD, + 'settings-category-title-end': 0xEEEEEE, + 'settings-input-border': 0xCCCCCC, + 'settings-input-background': 0xEEEEEE, + 'settings-input-colour': 0, + 'settings-input-focus': 0xDDDDDD, + 'settings-markup-link-colour': 0x1E90FF, + }, + sizes: { + 'chat-box-shadow-y': 1, + 'chat-box-shadow-blur': 4, + 'sidebar-box-shadow-y': 1, + 'sidebar-box-shadow-blur': 4, + 'sidebar-selector-attention-shadow-blur': 10, + 'input-box-shadow-y': 1, + 'input-box-shadow-blur': 4, + 'input-menu-box-shadow-y': 1, + 'input-menu-box-shadow-blur': 4, + }, + }, + { + id: 'blue', + name: 'Blue', + scheme: 'dark', + colours: { + 'main-accent': 0x0D355D, + 'main-background': 0x001434, + 'main-colour': 0xFFFFFF, + 'scrollbar-foreground': 0x0B325A, + 'scrollbar-background': 0x002545, + 'chat-background': 0x002545, + 'chat-box-shadow': 0x002545, + 'message-avatar-background': 0x001434, + 'message-separator': 0x0D355D, + 'message-time-colour': 0x888888, + 'message-compact-alternate-background': 0x0D355D, + 'message-compact-highlight': 0x003D8E, + 'sidebar-background': 0x002545, + 'sidebar-background-current': 0x002545, + 'sidebar-background-highlight': 0x0D355D, + 'sidebar-background-option': 0, + 'sidebar-box-shadow': 0x002545, + 'sidebar-selector-background-begin': 0x0D355D, + 'sidebar-selector-background-end': 0x002545, + 'sidebar-selector-background-hover': 0x0D355D, + 'sidebar-selector-background-active': 0x002545, + 'sidebar-selector-attention-shadow': 0x96B6D3, + 'input-background': 0x002545, + 'input-box-shadow': 0x002545, + 'input-colour': 0xFFFFFF, + 'input-border': 0x0D355D, + 'input-send': 0x0D355D, + 'input-button-hover': 0x0D355D, + 'input-button-active': 0x002545, + 'input-menu-background': 0x0D355D, + 'input-menu-box-shadow': 0x0D355D, + 'input-menu-button': 0x0E466E, + 'input-menu-button-hover': 0x0F577F, + 'input-menu-button-active': 0x0E507A, + 'settings-category-title-begin': 0x0D355D, + 'settings-category-title-end': 0x002545, + 'settings-input-border': 0x003D8E, + 'settings-input-background': 0x002545, + 'settings-input-colour': 0xFFFFFF, + 'settings-input-focus': 0x0D355D, + 'settings-markup-link-colour': 0x1E90FF, + }, + sizes: { + 'chat-box-shadow-y': 1, + 'chat-box-shadow-blur': 4, + 'sidebar-box-shadow-y': 1, + 'sidebar-box-shadow-blur': 4, + 'sidebar-selector-attention-shadow-blur': 10, + 'input-box-shadow-y': 1, + 'input-box-shadow-blur': 4, + 'input-menu-box-shadow-y': 1, + 'input-menu-box-shadow-blur': 4, + }, + }, + { + id: 'purple', + name: 'Purple', + scheme: 'dark', + colours: { + 'main-accent': 0x59466B, + 'main-background': 0x251D2C, + 'main-colour': 0xFFFFFF, + 'scrollbar-foreground': 0x4A3A59, + 'scrollbar-background': 0x2C2335, + 'chat-background': 0x2C2335, + 'chat-box-shadow': 0x2C2335, + 'message-avatar-background': 0x251D2C, + 'message-separator': 0x4A3A59, + 'message-time-colour': 0x888888, + 'message-compact-alternate-background': 0x4A3A59, + 'message-compact-highlight': 0x604C74, + 'sidebar-background': 0x2C2335, + 'sidebar-background-current': 0x4A3A59, + 'sidebar-background-highlight': 0x604C74, + 'sidebar-background-option': 0, + 'sidebar-box-shadow': 0x2C2335, + 'sidebar-selector-background-begin': 0x4A3A59, + 'sidebar-selector-background-end': 0x3B2F47, + 'sidebar-selector-background-hover': 0x4A3A59, + 'sidebar-selector-background-active': 0x3B2F47, + 'sidebar-selector-attention-shadow': 0xA591AD, + 'input-background': 0x2C2335, + 'input-box-shadow': 0x2C2335, + 'input-colour': 0xFFFFFF, + 'input-border': 0x4A3A59, + 'input-send': 0x4A3A59, + 'input-button-hover': 0x7E6397, + 'input-button-active': 0x59466B, + 'input-menu-background': 0x4A3A59, + 'input-menu-box-shadow': 0x4A3A59, + 'input-menu-button': 0x59466B, + 'input-menu-button-hover': 0x7E6397, + 'input-menu-button-active': 0x604C74, + 'settings-category-title-begin': 0x4A3A59, + 'settings-category-title-end': 0x3B2F47, + 'settings-input-border': 0x604C74, + 'settings-input-background': 0x3B2F47, + 'settings-input-colour': 0xFFFFFF, + 'settings-input-focus': 0x4A3A59, + 'settings-markup-link-colour': 0x1E90FF, + }, + sizes: { + 'chat-box-shadow-y': 1, + 'chat-box-shadow-blur': 4, + 'sidebar-box-shadow-y': 1, + 'sidebar-box-shadow-blur': 4, + 'sidebar-selector-attention-shadow-blur': 10, + 'input-box-shadow-y': 1, + 'input-box-shadow-blur': 4, + 'input-menu-box-shadow-y': 1, + 'input-menu-box-shadow-blur': 4, + }, + }, + { + id: 'archaic', + name: 'Archaic', + scheme: 'dark', + colours: { + 'main-accent': 0x333333, + 'main-background': 0, + 'main-colour': 0xFFFFFF, + 'scrollbar-foreground': 0x212121, + 'scrollbar-background': 0, + 'chat-background': 0, + 'chat-box-shadow': 0x888888, + 'message-avatar-background': 0x111111, + 'message-separator': 0x222222, + 'message-time-colour': 0x888888, + 'message-compact-alternate-background': 0x212121, + 'message-compact-highlight': 0x3A3A3A, + 'sidebar-background': 0, + 'sidebar-background-current': 0x212121, + 'sidebar-background-highlight': 0, + 'sidebar-background-option': 0, + 'sidebar-box-shadow': 0x888888, + 'sidebar-selector-background-begin': 0x212121, + 'sidebar-selector-background-end': 0x212121, + 'sidebar-selector-background-hover': 0x1A1A1A, + 'sidebar-selector-background-active': 0x111111, + 'sidebar-selector-attention-shadow': 0x888888, + 'input-background': 0, + 'input-box-shadow': 0x888888, + 'input-colour': 0xFFFFFF, + 'input-border': 0, + 'input-send': 0x111111, + 'input-button-hover': 0x333333, + 'input-button-active': 0x222222, + 'input-menu-background': 0, + 'input-menu-box-shadow': 0x888888, + 'input-menu-button': 0x111111, + 'input-menu-button-hover': 0x333333, + 'input-menu-button-active': 0x222222, + 'settings-category-title-begin': 0x212121, + 'settings-category-title-end': 0x212121, + 'settings-input-border': 0x888888, + 'settings-input-background': 0, + 'settings-input-colour': 0xFFFFFF, + 'settings-input-focus': 0, + 'settings-markup-link-colour': 0x1E90FF, + }, + sizes: { + 'chat-box-shadow-spread': 1, + 'sidebar-box-shadow-spread': 1, + 'sidebar-selector-attention-shadow-blur': 10, + 'input-box-shadow-spread': 1, + 'input-menu-box-shadow-spread': 1, + }, + }, +]; + +const UmiThemeApply = function(theme) { + const varPfx = '--theme-', + themeType = typeof theme; + if(themeType === 'number') + theme = UmiThemes[theme]; + else if(themeType === 'string') + for(const t of UmiThemes) + if(t.id === theme) { + theme = t; + break; + } + + if(typeof theme !== 'object') + throw 'Invalid theme.'; + + for(let propName in document.body.style) // this doesn't really work + if(propName && propName.indexOf(varPfx) === 0) + document.body.style.removeProperty(propName); + + if(typeof theme.scheme === 'string') + document.body.style.setProperty(varPfx + 'scheme', theme.scheme); + + if(theme.colours) { + const coloursPfx = varPfx + 'colour-'; + + for(const propName in theme.colours) { + document.body.style.setProperty( + coloursPfx + propName, + '#' + (theme.colours[propName].toString(16).padStart(6, '0')) + ); + } + } + + if(theme.sizes) { + const sizesPfx = varPfx + 'size-'; + + for(const propName in theme.sizes) + document.body.style.setProperty( + sizesPfx + propName, + theme.sizes[propName].toString() + 'px' + ); + } +}; diff --git a/src/mami.js/txtrigs.js b/src/mami.js/txtrigs.js new file mode 100644 index 0000000..d614286 --- /dev/null +++ b/src/mami.js/txtrigs.js @@ -0,0 +1,130 @@ +const MamiTextTrigger = function(info) { + const type = info.type, + match = info.match; + + for(const i in match) { + match[i] = match[i].split(';'); + for(const j in match[i]) + match[i][j] = match[i][j].trim().split(':'); + } + + const pub = { + getType: function() { return type; }, + isSoundType: function() { return type === 'sound'; }, + isAliasType: function() { return type === 'alias'; }, + isMatch: function(matchText) { + for(const i in match) { + const out = (function(filters, text) { + let result = false; + + for(const j in filters) { + const filter = filters[j]; + switch(filter[0]) { + case 'lc': + text = text.toLowerCase(); + break; + + case 'is': + if(text === filter.slice(1).join(':')) + result = true; + break; + + case 'starts': + if(text.indexOf(filter.slice(1).join(':')) === 0) + result = true; + break; + + case 'has': + if(text.includes(filter.slice(1).join(':'))) + result = true; + break; + + case 'hasnot': + if(text.includes(filter.slice(1).join(':'))) + result = false; + break; + + default: + console.error('Unknown filter encountered: ' + filter.join(':')); + break; + } + } + + return result; + })(match[i], matchText); + + if(out) return true; + } + + return false; + }, + }; + + if(type === 'sound') { + const volume = info.volume || 1.0, + rate = info.rate || 1.0, + names = info.sounds || []; + + pub.getVolume = function() { return volume; }; + pub.getRate = function() { + if(rate === 'rng') + return 1.8 - (Math.random() * 1.5); + return rate; + }; + pub.getSoundNames = function() { return names; }; + pub.getRandomSoundName = function() { return names[Math.floor(Math.random() * names.length)]; }; + } else if(type === 'alias') { + const aliasFor = info.for || []; + + pub.getFor = function() { return aliasFor; }; + } else + throw 'Unsupported trigger type.'; + + return pub; +}; + +const MamiTextTriggers = function() { + let triggers = []; + + const addTrigger = function(triggerInfo) { + if(triggerInfo === null || typeof triggerInfo.type !== 'string') + throw 'triggerInfo is not a valid trigger'; + + triggers.push(new MamiTextTrigger(triggerInfo)); + }; + + const getTrigger = function(text, returnAlias) { + for(const i in triggers) { + let trigger = triggers[i]; + if(trigger.isMatch(text)) { + if(trigger.isAliasType() && !returnAlias) { + const aliasFor = trigger.getFor(); + trigger = getTrigger(aliasFor[Math.floor(Math.random() * aliasFor.length)]); + } + + return trigger; + } + } + + throw 'no trigger that matches this text'; + }; + + return { + addTrigger: addTrigger, + addTriggers: function(triggerInfos) { + for(const i in triggerInfos) + try { + addTrigger(triggerInfos[i]); + } catch(ex) { + console.error(ex); + } + }, + clearTriggers: function() { + triggers = []; + }, + hasTriggers: function() { + return triggers.length > 0; + }, + getTrigger: getTrigger, + }; +}; diff --git a/src/mami.js/ui/channels.js b/src/mami.js/ui/channels.js new file mode 100644 index 0000000..21a93f0 --- /dev/null +++ b/src/mami.js/ui/channels.js @@ -0,0 +1,76 @@ +#include channels.js +#include messages.js +#include utility.js +#include ui/menus.js + +Umi.UI.Channels = (function() { + const sidebarChannel = 'sidebar__channel', + sidebarChannelCurrent = 'sidebar__channel--current', + 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.getName().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.getName()); + cBase.setAttribute('onclick', 'Umi.UI.Channels.Switch(this.getAttribute(\'data-umi-channel\'))'); + + cName.appendChild($t(channel.getName())); + 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.getName().toLowerCase().replace(' ', '-'); + cBase.innerText = channel.getName(); + }, + Remove: function(channel) { + $ri('channel-' + channel.getName().toLowerCase().replace(' ', '-')); + }, + RemoveAll: function() { + Umi.UI.Menus.Get('channels').innerHTML = ''; + }, + Reload: function(initial) { + const current = Umi.Channels.Current(), + channel = $i('channel-' + current.getName().toLowerCase().replace(' ', '-')), + prev = $c(sidebarChannelCurrent)[0]; + + if((typeof prev).toLowerCase() !== 'undefined') + prev.classList.remove(sidebarChannelCurrent); + + channel.classList.add(sidebarChannelCurrent); + + if(initial) + return; + + Umi.UI.Messages.RemoveAll(); + const channelMsgs = Umi.Messages.All(current.getName()); + for(const channelMsg of channelMsgs) + Umi.UI.Messages.Add(channelMsg); + }, + Switch: function(channelName) { + markUnread(channelName, true); + Umi.Channels.Switch(Umi.Channels.Get(channelName)); + }, + Unread: markUnread, + }; +})(); diff --git a/src/mami.js/ui/chat-input-main.js b/src/mami.js/ui/chat-input-main.js new file mode 100644 index 0000000..9f46e12 --- /dev/null +++ b/src/mami.js/ui/chat-input-main.js @@ -0,0 +1,33 @@ +#include utility.js + +Umi.UI.ChatInputMain = function() { + const html = $e({ + attrs: { + id: 'umi-msg-container', + className: 'input__main', + }, + child: [ + { + tag: 'textarea', + attrs: { + id: 'umi-msg-text', + className: 'input__text', + autofocus: true, + }, + }, + { + tag: 'button', + attrs: { + id: 'umi-msg-send', + className: 'input__button input__button--send', + }, + }, + ], + }); + + return { + getElement: function() { + return html; + }, + }; +}; diff --git a/src/mami.js/ui/chat-input-menus.js b/src/mami.js/ui/chat-input-menus.js new file mode 100644 index 0000000..27f8190 --- /dev/null +++ b/src/mami.js/ui/chat-input-menus.js @@ -0,0 +1,16 @@ +#include utility.js + +Umi.UI.ChatInputMenus = function() { + const html = $e({ + attrs: { + id: 'umi-msg-menu', + className: 'input__menus', + }, + }); + + return { + getElement: function() { + return html; + }, + }; +}; diff --git a/src/mami.js/ui/chat-input.js b/src/mami.js/ui/chat-input.js new file mode 100644 index 0000000..f05b9e6 --- /dev/null +++ b/src/mami.js/ui/chat-input.js @@ -0,0 +1,30 @@ +#include utility.js +#include ui/chat-input-main.js +#include ui/chat-input-menus.js + +Umi.UI.ChatInput = function() { + const menus = new Umi.UI.ChatInputMenus, + main = new Umi.UI.ChatInputMain; + + const html = $e({ + attrs: { + className: 'input', + }, + child: [ + menus, + main, + ], + }); + + return { + getMenus: function() { + return menus; + }, + getMain: function() { + return main; + }, + getElement: function() { + return html; + }, + }; +}; diff --git a/src/mami.js/ui/chat-interface.js b/src/mami.js/ui/chat-interface.js new file mode 100644 index 0000000..5e6c9d7 --- /dev/null +++ b/src/mami.js/ui/chat-interface.js @@ -0,0 +1,30 @@ +#include utility.js +#include ui/chat-message-list.js +#include ui/chat-input.js + +Umi.UI.ChatInterface = function() { + const messages = new Umi.UI.ChatMessageList, + input = new Umi.UI.ChatInput; + + const html = $e({ + attrs: { + className: 'main', + }, + child: [ + messages, + input, + ], + }); + + return { + getMessageList: function() { + return messages; + }, + getInput: function() { + return input; + }, + getElement: function() { + return html; + }, + }; +}; diff --git a/src/mami.js/ui/chat-layout.js b/src/mami.js/ui/chat-layout.js new file mode 100644 index 0000000..b3ba9a5 --- /dev/null +++ b/src/mami.js/ui/chat-layout.js @@ -0,0 +1,32 @@ +#include utility.js +#include ui/chat-interface.js +#include ui/chat-sidebar.js + +// this needs revising at some point but will suffice for now +Umi.UI.ChatLayout = function() { + const sideBar = new Umi.UI.ChatSideBar, + main = new Umi.UI.ChatInterface; + + const html = $e({ + attrs: { + id: 'umi-chat', + className: 'umi', + }, + child: [ + main, + sideBar, + ], + }); + + return { + getSideBar: function() { + return sideBar; + }, + getInterface: function() { + return main; + }, + getElement: function() { + return html; + }, + }; +}; diff --git a/src/mami.js/ui/chat-message-list.js b/src/mami.js/ui/chat-message-list.js new file mode 100644 index 0000000..0b6df12 --- /dev/null +++ b/src/mami.js/ui/chat-message-list.js @@ -0,0 +1,16 @@ +#include utility.js + +Umi.UI.ChatMessageList = function() { + const html = $e({ + attrs: { + id: 'umi-messages', + className: 'chat', + }, + }); + + return { + getElement: function() { + return html; + }, + }; +}; diff --git a/src/mami.js/ui/chat-sidebar-buttons.js b/src/mami.js/ui/chat-sidebar-buttons.js new file mode 100644 index 0000000..dbffaa4 --- /dev/null +++ b/src/mami.js/ui/chat-sidebar-buttons.js @@ -0,0 +1,35 @@ +#include utility.js + +Umi.UI.ChatSideBarButtons = function() { + const html = $e({ + attrs: { + className: 'sidebar__selector', + }, + child: [ + { + attrs: { + id: 'umi-menu-icons', + className: 'sidebar__selector-top', + }, + }, + { + attrs: { + id: 'umi-toggles', + className: 'sidebar__selector-bottom', + }, + }, + ], + }); + + return { + getElement: function() { + return html; + }, + addMenuButton: function() { + // + }, + addActionButton: function() { + // + }, + }; +}; diff --git a/src/mami.js/ui/chat-sidebar-container.js b/src/mami.js/ui/chat-sidebar-container.js new file mode 100644 index 0000000..1c116b8 --- /dev/null +++ b/src/mami.js/ui/chat-sidebar-container.js @@ -0,0 +1,19 @@ +#include utility.js + +Umi.UI.ChatSideBarContainer = function() { + const html = $e({ + attrs: { + id: 'umi-menus', + className: 'sidebar__menus', + }, + }); + + return { + getElement: function() { + return html; + }, + createMenu: function() { + // + }, + }; +}; diff --git a/src/mami.js/ui/chat-sidebar.js b/src/mami.js/ui/chat-sidebar.js new file mode 100644 index 0000000..e836108 --- /dev/null +++ b/src/mami.js/ui/chat-sidebar.js @@ -0,0 +1,24 @@ +#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, + buttons = new Umi.UI.ChatSideBarButtons; + + const html = $e({ + attrs: { + className: 'sidebar', + }, + child: [ + buttons, + container, + ], + }); + + return { + getElement: function() { + return html; + }, + }; +}; diff --git a/src/mami.js/ui/domaintrans.jsx b/src/mami.js/ui/domaintrans.jsx new file mode 100644 index 0000000..7290a5f --- /dev/null +++ b/src/mami.js/ui/domaintrans.jsx @@ -0,0 +1,70 @@ +#include common.js +#include rng.js + +const MamiDomainTransition = function(onImport, onDismiss) { + if(typeof onImport !== 'function') + throw 'onImport must be a function'; + if(typeof onDismiss !== 'function') + throw 'onDismiss must be a function'; + + let eggsTarget; + const html =
+
+
+
+ Flashii Chat +
+
+
sockchat.flashii.net
+
+
+
chat.flashii.net
+
+
+
+
+ Compatibility Chat +
+
+
sockchat.flashii.net/legacy
+
+
+
sockchat.flashii.net
+
+
+ {eggsTarget =
} +
+

At long last, chat is being moved back to its original subdomain. What was meant to be a temporary drop-in lasted a little bit longer than expected!

+

You will need to transfer your settings using the compatibility client. Going there will present you with a similar pop-up that will let you save your settings to a file, press "Import Settings" below and select the file you saved to copy them over. If you're new or wish to start anew, just press "Continue to chat".

+

This screen won't show up again after you press "Continue to chat".

+
+
+ +
+
Open compatibility client
+
+ + +
+
+
; + + const soundRNG = new MamiRNG(); + const soundNames = []; + mami.getSoundLibrary().forEachSound((info, name) => soundNames.push(name)); + + const playRandomSound = () => mami.playLibrarySound(soundNames[soundRNG.next(soundNames.length)]); + + for(let i = 0; i < 10; ++i) + eggsTarget.appendChild( playRandomSound()} />); + + return { + getElement: () => html, + }; +}; diff --git a/src/mami.js/ui/elems.js b/src/mami.js/ui/elems.js new file mode 100644 index 0000000..e702d35 --- /dev/null +++ b/src/mami.js/ui/elems.js @@ -0,0 +1,11 @@ +Umi.UI.Elements = { + Messages: null, + Menus: null, + Icons: null, + Toggles: null, + MessageContainer: null, + MessageInput: null, + MessageSend: null, + MessageMenus: null, + Chat: null, +}; diff --git a/src/mami.js/ui/emotes.js b/src/mami.js/ui/emotes.js new file mode 100644 index 0000000..2770d16 --- /dev/null +++ b/src/mami.js/ui/emotes.js @@ -0,0 +1,70 @@ +#include emotes.js +#include user.js +#include utility.js +#include ui/input-menus.js +#include ui/view.js + +Umi.UI.Emoticons = (function() { + return { + Init: function() { + const menu = Umi.UI.InputMenus.Get('emotes'); + menu.innerHTML = ''; + + MamiEmotes.forEach(Umi.User.getCurrentUser().getRank(), function(emote) { + menu.appendChild($e({ + tag: 'button', + attrs: { + className: 'emoticon emoticon--button', + title: emote.strings[0], + dataset: { + umiEmoticon: ':' + emote.strings[0] + ':', + }, + onclick: 'Umi.UI.Emoticons.Insert(this)', + }, + child: { + tag: 'img', + attrs: { + className: 'emoticon', + src: emote.url, + alt: emote.strings[0], + }, + } + })); + }); + }, + Parse: function(element, message) { + if(!Umi.Settings.get('enableEmoticons')) + return element; + + let inner = element.innerHTML; + + MamiEmotes.forEach(message.getUser().getRank(), function(emote) { + const image = $e({ + tag: 'img', + attrs: { + className: 'emoticon', + src: emote.url, + }, + }); + + for (const i in emote.strings) { + const trigger = ':' + emote.strings[i] + ':', + match = new RegExp(trigger, 'g'); + image.alt = trigger; + inner = inner.replace(match, image.outerHTML); + } + }); + + element.innerHTML = inner; + + return element; + }, + Insert: function(sender) { + const emoticon = sender.getAttribute('data-umi-emoticon'); + Umi.UI.View.EnterAtCursor(sender.getAttribute('data-umi-emoticon')); + Umi.UI.View.SetPosition(Umi.UI.View.GetPosition() + emoticon.length); + Umi.UI.View.SetPosition(Umi.UI.View.GetPosition(), true); + Umi.UI.View.Focus(); + }, + }; +})(); diff --git a/src/mami.js/ui/hooks.js b/src/mami.js/ui/hooks.js new file mode 100644 index 0000000..346ffb1 --- /dev/null +++ b/src/mami.js/ui/hooks.js @@ -0,0 +1,204 @@ +#include channels.js +#include common.js +#include settings.js +#include user.js +#include sound/umisound.js +#include ui/channels.js +#include ui/elems.js +#include ui/messages.jsx +#include ui/title.js +#include ui/users.js +#include ui/view.js + +Umi.UI.Hooks = (function() { + return { + AddMessageHooks: function() { + Umi.Messages.OnSend.push(function(msg) { + Umi.UI.View.SetText(''); + }); + + Umi.Messages.OnRemove.push(function(msg) { + Umi.UI.Messages.Remove(msg); + }); + + Umi.Messages.OnClear.push(function() { + Umi.UI.Messages.RemoveAll(); + }); + + window.addEventListener('focus', function() { + Umi.UI.Title.Clear(); + }); + + Umi.Messages.OnAdd.push(function(msg) { + Umi.UI.Channels.Unread(msg.getChannel()); + Umi.UI.Messages.Add(msg); + + if(!document.hidden && Umi.Settings.get('flashTitle')) + return; + + let title = ' ' + msg.getUser().getName(), + channel = Umi.Channels.Current() || null; + if(msg.getUser().isBot() && Umi.Settings.get('showServerMsgInTitle')) + title = ' ' + msg.getText(); + + if(channel !== null && channel.getName() !== msg.getChannel()) + title += ' @ ' + channel.getName(); + + Umi.UI.Title.Flash(['[ @]' + title, '[@ ]' + title]); + + if(Umi.Settings.get('enableNotifications') && Umi.User.getCurrentUser() !== null) { + const triggers = (Umi.Settings.get('notificationTriggers') || '').toLowerCase().split(' '), + options = {}; + + triggers.push((Umi.User.getCurrentUser() || { getName: function() { return ''; } }).getName().toLowerCase()); + options.body = 'Click here to see what they said.'; + + if(Umi.Settings.get('notificationShowMessage')) + options.body += "\n" + msg.getText(); + + const avatarUrl = futami.get('avatar'); + if(avatarUrl.length > 0) + options.icon = avatarUrl.replace('{user:id}', msg.getUser().getId()).replace('{resolution}', '80').replace('{user:avatar_change}', msg.getUser().getAvatarTime().toString()); + + for(const trigger of triggers) { + const message = ' ' + msg.getText() + ' '; + + if(trigger.trim() === '') + continue; + + if(message.toLowerCase().indexOf(' ' + trigger + ' ') >= 0) { + new Notification('{0} mentioned you!'.replace('{0}', msg.getUser().getName()), options); + if(Umi.Settings.get('onlySoundOnMention')) + Umi.Sound.Play('incoming'); + break; + } + } + } + }); + }, + AddUserHooks: 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); + }); + }, + AddChannelHooks: function() { + 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); + }); + }, + AddTextHooks: function() { + window.addEventListener('keydown', function(ev) { + if(ev.ctrlKey || ev.altKey || ev.metaKey) + return; + + const tagName = ev.target.tagName.toLowerCase(); + if(tagName !== 'textarea' && tagName !== 'input') + Umi.UI.Elements.MessageInput.focus(); + }); + + Umi.UI.Elements.MessageInput.addEventListener('input', function(ev) { + const elemInput = Umi.UI.Elements.MessageInput, elemParent = elemInput.parentNode; + let height = 40; + + if(Umi.Settings.get('expandTextBox') && elemInput.scrollHeight > elemInput.clientHeight) { + /*const cols = Math.floor(elemInput.clientWidth / 8), + rows = Math.floor(elemInput.textLength / cols); + + if(rows > 1) + height = 15.5 * (rows + 1);*/ + height = elemInput.scrollHeight; + } + + if(height > 40) + elemParent.style.height = height.toString() + 'px'; + else + elemParent.style.height = null; + }); + + Umi.UI.Elements.MessageInput.addEventListener('keydown', function(ev) { + switch(ev.key) { + case 'Tab': + if(!ev.shiftKey || !ev.ctrlKey) { + ev.preventDefault(); + + const text = Umi.UI.View.GetText(); + if(text.length > 0) { + const start = Umi.UI.View.GetPosition(); + let position = start, + snippet = ''; + while(position >= 0 && text.charAt(position - 1) !== ' ' && text.charAt(position - 1) !== "\n") { + --position; + snippet = text.charAt(position) + snippet; + } + + let insertText = undefined; + + if(snippet.indexOf(':') === 0) { + let emoteRank = 0; + if(Umi.User.hasCurrentUser()) + emoteRank = Umi.User.getCurrentUser().getRank(); + const emotes = MamiEmotes.findByName(emoteRank, snippet.substring(1), true); + if(emotes.length > 0) + insertText = ':' + emotes[0] + ':'; + } else { + const users = Umi.Users.Find(snippet); + if(users.length === 1) + insertText = users[0].getName(); + } + + if(insertText !== undefined) { + Umi.UI.View.SetText(text.slice(0, start - snippet.length) + text.slice(start)); + Umi.UI.View.SetPosition(start - snippet.length); + Umi.UI.View.EnterAtCursor(insertText); + Umi.UI.View.SetPosition(Umi.UI.View.GetPosition() + insertText.length); + Umi.UI.View.SetPosition(Umi.UI.View.GetPosition(), true); + } + } + } + break; + + case 'Enter': + case 'NumpadEnter': + if(!ev.shiftKey) { + ev.preventDefault(); + Umi.Messages.Send(Umi.UI.Elements.MessageInput.value); + return; + } + break; + } + }); + + Umi.UI.Elements.MessageSend.addEventListener('click', function(ev) { + Umi.Messages.Send(Umi.UI.Elements.MessageInput.value); + }); + }, + }; +})(); diff --git a/src/mami.js/ui/input-menus.js b/src/mami.js/ui/input-menus.js new file mode 100644 index 0000000..fa037c8 --- /dev/null +++ b/src/mami.js/ui/input-menus.js @@ -0,0 +1,75 @@ +#include utility.js +#include ui/elems.js +#include ui/input-menus.js + +Umi.UI.InputMenus = (function() { + const ids = []; + let current = ''; + + const inputMenuActive = 'input__menu--active', + inputButtonActive = 'input__button--active'; + + const toggle = function(baseId) { + const button = Umi.UI.Elements.MessageMenus.id + '-btn-' + baseId, + menu = Umi.UI.Elements.MessageMenus.id + '-sub-' + baseId; + + if($c(inputMenuActive).length) + $c(inputMenuActive)[0].classList.remove(inputMenuActive); + + if($c(inputButtonActive).length) + $c(inputButtonActive)[0].classList.remove(inputButtonActive); + + if(current !== baseId) { + $i(menu).classList.add(inputMenuActive); + $i(button).classList.add(inputButtonActive); + current = baseId; + } else current = ''; + }; + + const createButton = function(id, title, onClick) { + return $e({ + tag: 'button', + attrs: { + id: Umi.UI.Elements.MessageMenus.id + '-btn-' + id, + classList: ['input__button', 'input__button--' + id], + title: title, + onclick: onClick || (function() { + toggle(id); + }), + }, + }); + }; + + return { + Add: function(baseId, title) { + if(ids.indexOf(baseId) < 0) { + ids.push(baseId); + Umi.UI.Elements.MessageContainer.insertBefore(createButton(baseId, title), Umi.UI.Elements.MessageSend); + Umi.UI.Elements.MessageMenus.appendChild( + $e({ attrs: { 'class': ['input__menu', 'input__menu--' + baseId], id: Umi.UI.Elements.MessageMenus.id + '-sub-' + baseId } }) + ); + } + }, + AddButton: function(baseId, title, onClick) { + if(ids.indexOf(baseId) < 0) { + ids.push(baseId); + Umi.UI.Elements.MessageContainer.insertBefore( + createButton(baseId, title, onClick), + Umi.UI.Elements.MessageSend + ); + } + }, + Get: function(baseId, button) { + const id = Umi.UI.Elements.MessageMenus.id + '-' + (button ? 'btn' : 'sub') + '-' + baseId; + if(ids.indexOf(baseId) >= 0) + return $i(id); + return null; + }, + Remove: function(baseId) { + $ri(Umi.UI.Elements.MessageMenus.id + '-btn-' + baseId); + $ri(Umi.UI.Elements.MessageMenus.id + '-sub-' + baseId); + }, + Toggle: toggle, + CreateButton: createButton, + }; +})(); diff --git a/src/mami.js/ui/loading-overlay.jsx b/src/mami.js/ui/loading-overlay.jsx new file mode 100644 index 0000000..2b8eb9d --- /dev/null +++ b/src/mami.js/ui/loading-overlay.jsx @@ -0,0 +1,51 @@ +#include utility.js + +Umi.UI.LoadingOverlay = function(icon, header, message) { + const icons = { + 'spinner': 'fas fa-3x fa-fw fa-spinner fa-pulse', + 'checkmark': 'fas fa-3x fa-fw fa-check-circle', + 'cross': 'fas fa-3x fa-fw fa-times-circle', + 'hammer': 'fas fa-3x fa-fw fa-gavel', + 'bomb': 'fas fa-3x fa-fw fa-bomb', + 'unlink': 'fas fa-3x fa-fw fa-unlink', + 'reload': 'fas fa-3x fa-fw fa-sync fa-spin', + 'warning': 'fas fa-exclamation-triangle fa-3x', + 'question': 'fas fa-3x fa-question-circle', + 'poop': 'fas fa-3x fa-poop', + }; + + let iconElem, headerElem, messageElem; + const html =
+
+ {iconElem =
} + {headerElem =
} + {messageElem =
} +
+
; + + const setIcon = name => { + name = (name || '').toString(); + if(!(name in icons)) + name = 'question'; + iconElem.className = icons[name]; + }; + + const setHeader = text => headerElem.textContent = (text || '').toString(); + const setMessage = text => messageElem.textContent = (text || '').toString(); + + setIcon(icon); + setHeader(header); + setMessage(message); + + return { + setIcon: setIcon, + setHeader: setHeader, + setMessage: setMessage, + getElement: function() { + return html; + }, + implode: function() { + html.parentNode.removeChild(html); + }, + }; +}; diff --git a/src/mami.js/ui/markup.js b/src/mami.js/ui/markup.js new file mode 100644 index 0000000..3505f3f --- /dev/null +++ b/src/mami.js/ui/markup.js @@ -0,0 +1,68 @@ +#include common.js +#include utility.js +#include ui/elems.js +#include ui/input-menus.js +#include ui/markup.js +#include ui/view.js + +Umi.UI.Markup = (function() { + const insertRaw = function(start, end) { + const selectionLength = Umi.UI.View.GetSelectionLength(); + Umi.UI.View.EnterAtCursor(start); + Umi.UI.View.SetPosition(Umi.UI.View.GetPosition() + selectionLength + start.length); + Umi.UI.View.EnterAtCursor(end); + Umi.UI.View.SetPosition(Umi.UI.View.GetPosition() - selectionLength); + Umi.UI.View.SetPosition(Umi.UI.View.GetPosition() + selectionLength, true); + Umi.UI.View.Focus(); + }; + + const insert = function(ev) { + if(this.dataset.umiTagName === 'color' && this.dataset.umiPickerVisible !== 'yes') { + const elem = this; + elem.dataset.umiPickerVisible = 'yes'; + + const picker = new FwColourPicker( + function(picker, result) { + if(result !== null) + insertRaw( + '[color=' + FwColourPicker.hexFormat(result) + ']', + '[/color]' + ); + }, { presets: futami.get('colours') }, null, function() { + elem.dataset.umiPickerVisible = 'no'; + } + ); + picker.appendTo(document.body); + const pos = picker.suggestPosition(ev); + picker.setPosition(pos.x, pos.y); + } else + insertRaw( + this.dataset.umiBeforeCursor, + this.dataset.umiAfterCursor + ); + }; + + return { + Add: function(name, text, beforeCursor, afterCursor) { + Umi.UI.InputMenus.Get('markup').appendChild($e({ + tag: 'button', + attrs: { + id: Umi.UI.Elements.MessageMenus.id + '-markup-btn-' + name, + classList: ['markup__button', 'markup__button--' + name], + dataset: { + umiTagName: name, + umiBeforeCursor: beforeCursor, + umiAfterCursor: afterCursor, + }, + onclick: insert, + }, + child: text, + })); + }, + Reset: function() { + Umi.UI.InputMenus.Get('markup').innerHTML = ''; + }, + Insert: insert, + InsertRaw: insertRaw, + }; +})(); diff --git a/src/mami.js/ui/menus.js b/src/mami.js/ui/menus.js new file mode 100644 index 0000000..f852744 --- /dev/null +++ b/src/mami.js/ui/menus.js @@ -0,0 +1,94 @@ +#include utility.js +#include ui/elems.js + +Umi.UI.Menus = (function() { + const ids = []; + + const sidebarMenu = 'sidebar__menu', + sidebarMenuActive = 'sidebar__menu--active', + sidebarMenuHidden = 'sidebar__menu--hidden', + sidebarSelectorMode = 'sidebar__selector-mode', + sidebarSelectorModeActive = 'sidebar__selector-mode--active', + sidebarSelectorModeAttention = 'sidebar__selector-mode--attention', + sidebarSelectorModeHidden = 'sidebar__selector-mode--hidden'; + + const attention = function(baseId, mode) { + if(mode === undefined) + mode = true; + + const element = $i(Umi.UI.Elements.Icons.id + '-' + 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.UI.Elements.Icons.id + '-' + baseId, + menu = Umi.UI.Elements.Menus.id + '-' + 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.indexOf(baseId) < 0) { + ids.push(baseId); + + const menuClass = [sidebarMenu, sidebarMenu + '--' + baseId], + iconClass = [sidebarSelectorMode, sidebarSelectorMode + '--' + baseId]; + + if(Umi.UI.Elements.Menus.children.length < 1) { + menuClass.push(sidebarMenuActive); + iconClass.push(sidebarSelectorModeActive); + } + + if(initiallyHidden) { + menuClass.push(sidebarMenuHidden); + iconClass.push(sidebarSelectorModeHidden); + } + + Umi.UI.Elements.Icons.appendChild($e({ + attrs: { + id: Umi.UI.Elements.Icons.id + '-' + baseId, + classList: iconClass, + title: title, + onclick: function() { + activate(baseId); + }, + }, + })); + + Umi.UI.Elements.Menus.appendChild($e({ attrs: { 'class': menuClass, id: Umi.UI.Elements.Menus.id + '-' + baseId } })); + } + }, + Get: function(baseId, icon) { + const id = (icon ? Umi.UI.Elements.Icons : Umi.UI.Elements.Menus).id + '-' + baseId; + if(ids.indexOf(baseId) >= 0) + return $i(id); + + return null; + }, + Remove: function(baseId) { + $ri(Umi.UI.Elements.Icons.id + '-' + baseId); + $ri(Umi.UI.Elements.Menus.id + '-' + baseId); + }, + Activate: activate, + Attention: attention, + Active: function() { + return $c(sidebarMenuActive)[0].id.substring(10); // LOL + }, + }; +})(); diff --git a/src/mami.js/ui/messages.jsx b/src/mami.js/ui/messages.jsx new file mode 100644 index 0000000..581fdd0 --- /dev/null +++ b/src/mami.js/ui/messages.jsx @@ -0,0 +1,244 @@ +#include channels.js +#include common.js +#include parsing.js +#include settings.js +#include url.js +#include users.js +#include utility.js +#include weeb.js +#include ui/emotes.js + +Umi.UI.Messages = (function() { + let lastMsgUser = null, + lastMsgChannel = null, + lastWasTiny = null; + + return { + Add: function(msg) { + if(msg.getChannel() !== null + && Umi.Channels.Current() !== null + && msg.getChannel() !== Umi.Channels.Current().getName()) + return; + + let isTiny = false, + skipTextParsing = false, + msgText = msg.getText(); + + let eBase = null, + eAvatar = null, + eText = null, + eMeta = null, + eUser = null; + + const sender = msg.getUser(); + let avatarUser = sender, + avatarSize = '80'; + + const userClass = 'message--user-' + sender.getId(); + + const classes = ['message', userClass]; + const styles = {}; + + const avatarClasses = ['message__avatar']; + + const msgIsFirst = lastMsgUser !== sender.getId() || lastMsgChannel !== msg.getChannel(); + if(msgIsFirst) + classes.push('message--first'); + + if(msg.shouldHighlight()) + classes.push('message--highlight'); + + if(msg.isAction()) { + isTiny = true; + classes.push('message-action'); + } + + if(sender.getId() === "136") + styles.transform = 'scaleY(' + (0.76 + (0.01 * Math.max(0, Math.ceil(Date.now() / (7 * 24 * 60 * 60000)) - 2813))).toString() + ')'; + + const msgDateTimeObj = msg.getTime(); + const msgDateTime = msgDateTimeObj.getHours().toString().padStart(2, '0') + + ':' + msgDateTimeObj.getMinutes().toString().padStart(2, '0') + + ':' + msgDateTimeObj.getSeconds().toString().padStart(2, '0'); + + if(sender.isBot() && Umi.Settings.get('fancyInfo')) { + const botInfo = msg.getBotInfo(); + + if(botInfo) { + if(botInfo.type === 'join' || botInfo.type === 'jchan' + || botInfo.type === 'leave' || botInfo.type === 'lchan' + || botInfo.type === 'kick' || botInfo.type === 'flood' + || botInfo.type === 'timeout') { + const target = botInfo.target || Umi.Users.FindExact(botInfo.args[0]); + + if(target) { + isTiny = true; + skipTextParsing = true; + avatarUser = target; + msgText = 'did something'; + + $ari(classes, userClass); + + switch(botInfo.type) { + case 'join': + msgText = 'has joined'; + break; + case 'leave': + msgText = 'has disconnected'; + avatarClasses.push('avatar-filter-greyscale'); + break; + case 'jchan': + msgText = 'has joined the channel'; + break; + case 'lchan': + msgText = 'has left the channel'; + avatarClasses.push('avatar-filter-greyscale'); + break; + case 'kick': + msgText = 'got bludgeoned to death'; + avatarClasses.push('avatar-filter-invert'); + break; + case 'flood': + msgText = 'got kicked for flood protection'; + avatarClasses.push('avatar-filter-invert'); + break; + case 'timeout': + msgText = 'exploded'; + avatarClasses.push('avatar-filter-greyscale'); + break; + } + } + } + } + } + + if(isTiny) { + if(!msgIsFirst) // small messages must always be "first" + classes.push('message--first'); + classes.push('message-tiny'); + + avatarSize = '40'; + msgText = "\xA0" + msgText; + + eBase =
+ {eAvatar =
} +
+ {eMeta =
+ {eUser =
{avatarUser.getName()}
} + {eText =
} +
{msgDateTime}
+
} +
+
; + } else { + eBase =
+ {eAvatar =
} +
+ {eMeta =
+ {eUser =
{avatarUser.getName()}
} +
{msgDateTime}
+
} + {eText =
} +
+
; + } + + eText.innerText = msgText; + + if(!skipTextParsing) { + eText = Umi.UI.Emoticons.Parse(eText, msg); + eText = Umi.Parsing.Parse(eText, msg); + + const urls = []; + + if(Umi.Settings.get('autoParseUrls')) { + const textSplit = eText.innerText.split(' '); + for(const textPart of textSplit) { + const uri = Umi.URI.Parse(textPart); + + if(uri !== null && uri.Slashes !== null) { + urls.push(textPart); + + const linkElement = $e({ + tag: 'a', + attrs: { + className: 'markup__link', + href: textPart, + target: '_blank', + rel: 'nofollow noreferrer noopener', + }, + child: textPart, + }); + + eText.innerHTML = eText.innerHTML.replace(textPart.replace(/&/g, '&'), linkElement.outerHTML); + } + } + } + + if(Umi.Settings.get('resolveUrls')) { + // todo: resolve urls + } + + if(Umi.Settings.get('weeaboo')) { + eText.appendChild($t(Weeaboo.getTextSuffix(sender))); + + const kaomoji = Weeaboo.getRandomKaomoji(true, msg); + if(kaomoji) { + eText.appendChild($t(' ')); + eText.appendChild($t(kaomoji)); + } + } + + if(Umi.Settings.get('weeaboo')) + eUser.appendChild($t(Weeaboo.getNameSuffix(sender))); + } + + if(isTiny !== lastWasTiny) { + if(!msgIsFirst) + eBase.classList.add('message--first'); + eBase.classList.add(isTiny ? 'message-tiny-fix' : 'message-big-fix'); + } + lastWasTiny = isTiny; + + const avatarUrl = futami.get('avatar'); + if (avatarUrl !== null && avatarUrl.length > 1) { + eAvatar.style.backgroundImage = 'url({0})'.replace('{0}', avatarUrl.replace('{user:id}', avatarUser.getId()) + .replace('{resolution}', avatarSize).replace('{user:avatar_change}', avatarUser.getAvatarTime().toString())); + } else eAvatar.classList.add('message__avatar--disabled'); + + Umi.UI.Elements.Messages.appendChild(eBase); + lastMsgUser = sender.getId(); + lastMsgChannel = msg.getChannel(); + + if(Umi.Settings.get('autoEmbedV1')) { + const callEmbedOn = eBase.querySelectorAll('a[onclick^="Umi.Parser.SockChatBBcode.Embed"]'); + for(const embedElem of callEmbedOn) + if(embedElem.dataset.embed !== '1') + embedElem.click(); + } + + if(Umi.Settings.get('autoScroll')) + Umi.UI.Elements.Messages.scrollTop = Umi.UI.Elements.Messages.scrollHeight; + + if(window.CustomEvent) + window.dispatchEvent(new CustomEvent('umi:ui:message_add', { + detail: { + element: eBase, + message: msg, + }, + })); + }, + Remove: function(msg) { + lastMsgUser = null; + lastMsgChannel = null; + lastWasTiny = null; + $ri('message-' + msg.getId()); + }, + RemoveAll: function() { + lastMsgUser = null; + lastMsgChannel = null; + lastWasTiny = null; + Umi.UI.Elements.Messages.innerHTML = ''; + }, + }; +})(); diff --git a/src/mami.js/ui/settings.js b/src/mami.js/ui/settings.js new file mode 100644 index 0000000..d4acc93 --- /dev/null +++ b/src/mami.js/ui/settings.js @@ -0,0 +1,280 @@ +#include settings.js +#include utility.js +#include ui/menus.js + +Umi.UI.Settings = (function() { + let copyright = null; + const createCopyright = function() { + if(copyright !== null) + return; + + copyright = $e({ + attrs: { + className: 'mami-copyright', + }, + child: [ + 'Mami', + ' © ', + { + tag: 'a', + child: 'flash.moe', + attrs: { + href: '//flash.moe', + target: '_blank', + }, + }, + { tag: 'br', }, + { + tag: 'a', + child: 'Sock Chat Documentation', + attrs: { + href: '//railgun.sh/sockchat', + target: '_blank', + }, + }, + ], + }); + + Umi.UI.Menus.Get('settings').appendChild(copyright); + }; + + const addCategory = function(category) { + const catBody = $e({ + attrs: { + id: Umi.UI.Menus.Get('settings').id + '-category-' + category.id, + classList: ['setting__category', 'setting__category--' + category.id], + style: { overflow: 'hidden' }, + }, + }), + catHeader = $e({ + attrs: { + classList: ['setting__category-title', 'setting__category-title--' + category.id], + style: { + cursor: 'pointer', + }, + onclick: function() { + if(catBody.dataset.mamiClosed) { + delete catBody.dataset.mamiClosed; + catBody.style.maxHeight = null; + const meow = catBody.clientHeight; + catBody.style.maxHeight = '0'; + setTimeout(function() { + catBody.style.maxHeight = meow.toString() + 'px'; + }, 50); + } else { + catBody.dataset.mamiClosed = 1; + if(!catBody.style.maxHeight) { + catBody.style.maxHeight = catBody.clientHeight.toString() + 'px'; + setTimeout(function() { + catBody.style.maxHeight = '0'; + }, 50); + } else catBody.style.maxHeight = '0'; + } + }, + }, + child: category.name, + }); + + $ib(copyright, catHeader); + $ib(copyright, catBody); + + if(category.collapse) { + catBody.dataset.mamiClosed = 1; + catBody.style.maxHeight = '0'; + } + + if(category.warning) + catBody.appendChild($e({ + attrs: { + style: { + fontSize: '.9em', + lineHeight: '1.4em', + margin: '5px', + padding: '5px', + backgroundColor: 'darkred', + border: '2px solid red', + borderRadius: '5px', + }, + }, + child: category.warning, + })); + }; + + const addSetting = function(setting) { + if(!setting.category) + return; + + let entry = null; + const settingsHtml = Umi.UI.Menus.Get('settings'), + typeAlias = setting.type === 'url' ? 'text' : setting.type, + container = $e({ + attrs: { + 'class': 'setting__container setting__container--' + typeAlias, + id: settingsHtml.id + '-' + setting.id + } + }); + + switch(setting.type) { + case 'select': + entry = $e({ tag: 'select', attrs: { 'class': 'setting__input' } }); + entry.disabled = !setting.mutable; + + let data = {}; + const dataType = setting.dataType || 'setting'; + + switch(dataType) { + case 'object': + data = setting.data; + break; + case 'call': + data = setting.data(); + break; + } + + for(const _i in data) { + const _j = data[_i], + _x = $e({ tag: 'option', attrs: { 'class': 'setting__style setting__style--' + setting.id + '-' + _i } }); + _x.value = _i; + _x.appendChild($t(_j)); + entry.appendChild(_x); + } + + Umi.Settings.watch(setting.id, function(v) { + const keys = Object.keys(data); + for(let i = 0; i < keys.length; ++i) { + if(keys[i] === v) { + entry.selectedIndex = i; + break; + } + } + }); + + entry.addEventListener('change', function(ev) { + Umi.Settings.set(setting.id, entry.value); + }); + + container.appendChild($e({ + tag: 'label', + attrs: { + className: 'setting__label', + }, + child: [ + { child: setting.name }, + entry, + ], + })); + break; + + case 'checkbox': + entry = $e({ tag: 'input', attrs: { type: 'checkbox', 'class': 'setting__input' } }); + entry.disabled = !setting.mutable; + + entry.addEventListener('click', function(ev) { + if(setting.confirm && !confirm(setting.confirm)) { + entry.checked = !entry.checked; + return; + } + + Umi.Settings.toggle(setting.id); + }); + + Umi.Settings.watch(setting.id, function(v) { + entry.checked = v; + }); + + container.appendChild($e({ + tag: 'label', + attrs: { + className: 'setting__label', + }, + child: [ + entry, + { child: setting.name }, + ], + })); + break; + + case 'button': // this really shouldn't probably be here i think + entry = $e({ tag: 'input', attrs: { 'class': 'setting__input' } }); + entry.disabled = !setting.mutable; + entry.value = setting.name; + entry.type = setting.type; + entry.addEventListener('click', setting.click); + + container.appendChild($e({ + tag: 'label', + attrs: { + className: 'setting__label', + }, + child: entry, + })); + break; + + case 'url': + case 'text': + case 'number': + entry = $e({ tag: 'input', attrs: { 'class': 'setting__input' } }); + entry.disabled = !setting.mutable; + entry.type = setting.type; + + Umi.Settings.watch(setting.id, function(v) { + if(entry.value !== v) + entry.value = v; + }); + entry.addEventListener('keyup', function(ev) { + Umi.Settings.set(setting.id, entry.value); + }); + + container.appendChild($e({ + tag: 'label', + attrs: { + className: 'setting__label', + }, + child: [ + { child: setting.name }, + entry, + ], + })); + break; + + case 'range': + entry = $e({ tag: 'input', attrs: { 'class': 'setting__input' } }); + entry.disabled = !setting.mutable; + entry.type = setting.type; + + Umi.Settings.watch(setting.id, function(v) { + entry.value = v; + }); + entry.addEventListener('change', function(ev) { + Umi.Settings.set(setting.id, entry.value); + }); + + container.appendChild($e({ + tag: 'label', + attrs: { + className: 'setting__label', + }, + child: [ + { child: setting.name }, + entry, + ], + })); + break; + } + + $i(settingsHtml.id + '-category-' + setting.category).appendChild(container); + }; + + return { + Init: function() { + createCopyright(); + + for(const category of UmiSettings.categories) + addCategory(category); + for(const setting of UmiSettings.settings) + addSetting(setting); + }, + Add: addSetting, + AddCat: addCategory, + }; +})(); diff --git a/src/mami.js/ui/title.js b/src/mami.js/ui/title.js new file mode 100644 index 0000000..38f2376 --- /dev/null +++ b/src/mami.js/ui/title.js @@ -0,0 +1,45 @@ +#include common.js + +Umi.UI.Title = (function() { + let activeFlash = null; + + const setTitle = function(text) { + document.title = text; + }; + + const clearTitle = function() { + if(activeFlash !== null) { + clearInterval(activeFlash); + activeFlash = null; + setTitle(futami.get('title')); + } + }; + + const flashTitle = function(titles, interval, repeat) { + if(interval === undefined) interval = 500; + if(repeat === undefined) repeat = 5; + + let round = 0; + const target = titles.length * repeat; + + clearTitle(); + setTitle(titles[0]); + + activeFlash = setInterval(function() { + if(round >= target) { + clearTitle(); + setTitle(futami.get('title')); + return; + } + + ++round; + setTitle(titles[round % titles.length]); + }, interval); + }; + + return { + Set: setTitle, + Clear: clearTitle, + Flash: flashTitle, + }; +})(); diff --git a/src/mami.js/ui/toggles.js b/src/mami.js/ui/toggles.js new file mode 100644 index 0000000..c2e9a2d --- /dev/null +++ b/src/mami.js/ui/toggles.js @@ -0,0 +1,36 @@ +#include utility.js +#include ui/elems.js + +Umi.UI.Toggles = (function() { + const ids = []; + + return { + Add: function(baseId, eventHandlers, title) { + if(ids.indexOf(baseId) < 0) { + ids.push(baseId); + + const toggle = $e({ + attrs: { + id: Umi.UI.Elements.Toggles.id + '-' + baseId, + classList: ['sidebar__selector-mode', 'sidebar__selector-mode--' + baseId], + title: title, + }, + }); + + for(const i in eventHandlers) + toggle.addEventListener(i, eventHandlers[i]); + + Umi.UI.Elements.Toggles.insertBefore(toggle, Umi.UI.Elements.Toggles.firstChild); + } + }, + Get: function(baseId, icon) { + const id = Umi.UI.Elements.Toggles.id + '-' + baseId; + if(ids.indexOf(baseId) >= 0) + return $i(id); + return null; + }, + Remove: function(baseId) { + $ri(Umi.UI.Elements.Toggles.id + '-' + baseId); + }, + }; +})(); diff --git a/src/mami.js/ui/uploads.js b/src/mami.js/ui/uploads.js new file mode 100644 index 0000000..2c79f7e --- /dev/null +++ b/src/mami.js/ui/uploads.js @@ -0,0 +1,135 @@ +#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.log('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(total, loaded) { + prog.value = Math.ceil((loaded / total) * 100); + }, + }; + }, + }; +})(); diff --git a/src/mami.js/ui/users.js b/src/mami.js/ui/users.js new file mode 100644 index 0000000..fe590f7 --- /dev/null +++ b/src/mami.js/ui/users.js @@ -0,0 +1,222 @@ +#include animate.js +#include common.js +#include settings.js +#include user.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(), + userOptions = $i(prefix + '-options-wrapper'), + closedClass = 'sidebar__user-options--hidden', + isClosed = userOptions.classList.contains(closedClass); + let update = null, + start = null, + end = null; + + if(prefix in toggleTimeouts) { + clearTimeout(toggleTimeouts[prefix]); + delete toggleTimeouts[prefix]; + } + + if(isClosed) { + if(Umi.Settings.get('autoCloseUserContext')) + toggleTimeouts[prefix] = setTimeout(function() { + if(Umi.Settings.get('autoCloseUserContext')) + toggleUser(id); + }, 300000); + + start = function() { + userOptions.classList.remove(closedClass); + }; + update = function(t) { + userOptions.style.maxHeight = (230 * t).toString() + 'px'; + }; + } else { + end = function() { + userOptions.classList.add(closedClass); + }; + update = function(t) { + userOptions.style.maxHeight = (230 - (230 * t)).toString() + 'px'; + }; + } + + MamiAnimate({ + duration: 500, + easing: 'easeOutExpo', + 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.getId(), + uBase = $e({ attrs: { 'class': 'sidebar__user', id: id } }), + uDetails = $e({ attrs: { 'class': 'sidebar__user-details', id: id + '-details' } }), + uAvatar = $e({ attrs: { 'class': 'sidebar__user-avatar', id: id + '-avatar' } }), + uName = $e({ attrs: { 'class': 'sidebar__user-name', id: id + '-name' } }), + uOptions = $e({ tag: 'ul', attrs: { 'class': 'sidebar__user-options', id: id + '-options' } }); + + uDetails.addEventListener('click', function() { + toggleUser(user.getId()); + }.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.getId()), '_blank'); + } + })); + } + if (Umi.User.isCurrentUser(user)) { + uOptions.appendChild(createAction('Describe action', { + 'click': function() { + Umi.UI.View.SetText('/me '); + } + })); + if (user.canSetNickName()) { + uOptions.appendChild(createAction('Set nickname', { + 'click': function() { + Umi.UI.View.SetText('/nick '); + } + })); + } + if (user.canBan()) { + uOptions.appendChild(createAction('View bans', { + 'click': function() { + Umi.UI.View.SetText('/bans'); + } + })); + uOptions.appendChild(createAction('Kick Fucking Everyone', { + 'click': function() { + if(confirm('You are about to detonate the fucking bomb. Are you sure?')) { + const targets = Umi.Users.All(); + for(const target of targets) + Umi.Server.sendMessage('/kick ' + target.getName()); + } + } + })); + } + } + else { + /*uOptions.appendChild(createAction('Send PM', { + 'click': function() { + Umi.UI.View.SetText('/msg ' + user.getName() + ' '); + } + }));*/ + if (Umi.User.getCurrentUser().canBan()) { + uOptions.appendChild(createAction('Kick', { + 'click': function() { + Umi.UI.View.SetText('/kick ' + user.getName()); + } + })); + uOptions.appendChild(createAction('View IP', { + 'click': function() { + Umi.UI.View.SetText('/ip ' + user.getName()); + } + })); + } + } + uName.style.color = user.getColour(); + uBase.style.backgroundColor = user.getColour() === 'inherit' ? '#fff' : user.getColour(); + + let afkText = '', + sbUserName = user.getName(); + + if(sbUserName.indexOf('<') === 0) { + afkText = sbUserName.substring(1, sbUserName.indexOf('>')); + sbUserName = sbUserName.substring(afkText.length + 3); + } + + const isAFK = afkText.length > 0; + if(isAFK) + uName.appendChild($e({ attrs: { 'class': 'user-sidebar-afk' }, child: afkText })); + + if(sbUserName.length > 16 || Umi.Settings.get('marqueeAllNames')) { + uName.appendChild($e({ + tag: 'marquee', + attrs: { + style: { + width: '180px', + overflow: 'hidden', + }, + }, + child: sbUserName, + })); + } else { + uName.appendChild($t(sbUserName)); + } + + const avatarUrl = futami.get('avatar'); + if(avatarUrl !== null && avatarUrl.length > 1) { + uAvatar.style.backgroundImage = 'url({0})'.replace('{0}', avatarUrl.replace('{user:id}', user.getId()).replace('{resolution}', '80').replace('{user:avatar_change}', user.getAvatarTime().toString())); + 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.getId()), + uAvatar = $i('user-' + user.getId() + '-avatar'), + uName = $i('user-' + user.getId() + '-name'); + uName.style.color = user.getColour(); + uBase.style.backgroundColor = user.getColour() === 'inherit' ? '#fff' : user.getColour(); + uName.textContent = ''; + + let afkText = '', + sbUserName = user.getName(); + + const avatarUrl = futami.get('avatar'); + if(avatarUrl !== null && avatarUrl.length > 1) + uAvatar.style.backgroundImage = 'url({0})'.replace('{0}', avatarUrl.replace('{user:id}', user.getId()).replace('{resolution}', '80').replace('{user:avatar_change}', user.getAvatarTime().toString())); + + if(sbUserName.indexOf('<') === 0) { + afkText = sbUserName.substring(1, sbUserName.indexOf('>')); + sbUserName = sbUserName.substring(afkText.length + 3); + } + + const isAFK = afkText.length > 0; + if(isAFK) + uName.appendChild($e({ attrs: { 'class': 'user-sidebar-afk' }, child: afkText })); + + if(sbUserName.length > 16 || Umi.Settings.get('marqueeAllNames')) { + uName.appendChild($e({ + tag: 'marquee', + attrs: { + style: { + width: '180px', + overflow: 'hidden', + }, + }, + child: sbUserName, + })); + } else + uName.appendChild($t(sbUserName)); + }, + Remove: function(user) { + $ri('user-' + user.getId()); + }, + RemoveAll: function() { + Umi.UI.Menus.Get('users').innerHTML = ''; + }, + CreateAction: createAction, + ToggleUser: toggleUser, + }; +})(); diff --git a/src/mami.js/ui/view.js b/src/mami.js/ui/view.js new file mode 100644 index 0000000..ad06129 --- /dev/null +++ b/src/mami.js/ui/view.js @@ -0,0 +1,95 @@ +#include settings.js +#include themes.js +#include ui/elems.js + +Umi.UI.View = (function() { + const accentColours = { + 'dark': 'Dark', + 'light': 'Light', + 'blue': 'Blue', + 'purple': 'Purple', + 'archaic': 'Archaic' + }; + + const getPosition = function(end) { + return Umi.UI.Elements.MessageInput[end ? 'selectionEnd' : 'selectionStart']; + }; + const setPosition = function(pos, end) { + Umi.UI.Elements.MessageInput[end ? 'selectionEnd' : 'selectionStart'] = pos; + }; + + const getText = function() { + return Umi.UI.Elements.MessageInput.value; + }; + const setText = function(text) { + Umi.UI.Elements.MessageInput.value = text; + }; + + return { + AccentColours: accentColours, + AccentReload: function() { + const available = Object.keys(accentColours), + name = Umi.Settings.get('style'), + compact = 'chat--compact', + classes = ['umi']; + + if(available.indexOf(name) < 0) + return; + + // should probably be moved elsewhere eventually + // the entire AccentReload function should probably be axed + UmiThemeApply(name); + + if(!Umi.Settings.get('tmpDisableOldThemeSys')) + classes.push('umi--' + name); + + if(Umi.UI.Elements.Chat.className.indexOf('hidden') >= 0) + classes.push('hidden'); + + Umi.UI.Elements.Chat.className = ''; + Umi.UI.Elements.Chat.classList.add.apply(Umi.UI.Elements.Chat.classList, classes); + if(Umi.Settings.get('compactView')) { + if(Umi.UI.Elements.Chat.className.indexOf(compact) < 0) + Umi.UI.Elements.Messages.classList.add(compact); + } else + Umi.UI.Elements.Messages.classList.remove(compact); + + if(Umi.Settings.get('autoScroll')) + Umi.UI.Elements.Messages.scrollTop = Umi.UI.Elements.Messages.scrollHeight; + }, + Focus: function() { + Umi.UI.Elements.MessageInput.focus(); + }, + SetPosition: setPosition, + GetPosition: getPosition, + GoToStart: function() { + setPosition(0); + }, + GoToEnd: function() { + setPosition(Umi.UI.Elements.MessageInput.value.length); + }, + GetSelectionLength: function() { + let length = getPosition(true) - getPosition(); + if(length < 0) + length = getPosition() - getPosition(true); + return length; + }, + EnterAtCursor: function(text, overwrite) { + const value = getText(), + current = getPosition(); + let out = ''; + + out += value.slice(0, current); + out += text; + out += value.slice(current + (overwrite ? text.length : 0)); + + setText(out); + setPosition(current); + }, + GetSelectedText: function() { + return getText().slice(getPosition(), getPosition(true)); + }, + GetText: getText, + SetText: setText, + }; +})(); diff --git a/src/mami.js/ui/views.js b/src/mami.js/ui/views.js new file mode 100644 index 0000000..1faa831 --- /dev/null +++ b/src/mami.js/ui/views.js @@ -0,0 +1,150 @@ +#include utility.js + +const MamiUIViews = function(targetBody) { + if(!(targetBody instanceof Element)) + throw 'targetBody must be an instance of window.Element'; + + const views = []; + + const extractElement = elementInfo => { + let element; + if(elementInfo instanceof Element) { + element = elementInfo; + } else if('getElement' in elementInfo) { + element = elementInfo.getElement(); + } else throw 'elementInfo is not a valid type'; + + if(!(element instanceof Element)) + throw 'element is not an instance of Element'; + if(element === targetBody) + throw 'element may not be the same as targetBody'; + + return element; + }; + + const doTransition = async (transition, ctx) => { + if(transition === undefined) + return; + + if(typeof transition !== 'function') + return; + + await transition(ctx); + }; + + const push = async (elementInfo, transition) => { + if(typeof elementInfo !== 'object') + throw 'elementInfo must be an object'; + + const element = extractElement(elementInfo); + + if(!views.includes(elementInfo)) + views.push(elementInfo); + + if(!targetBody.contains(element)) + targetBody.appendChild(element); + + if(views.length > 1) { + const prevElemInfo = views[views.length - 2]; + const prevElem = extractElement(prevElemInfo); + + await doTransition(transition, { + toInfo: elementInfo, + toElem: element, + fromInfo: prevElemInfo, + fromElem: prevElem, + }); + + if(!prevElem.classList.contains('hidden')) + prevElem.classList.add('hidden'); + } + + if(element.classList.contains('hidden')) + element.classList.remove('hidden'); + }; + + const pop = async transition => { + const elementInfo = views.pop(); + if(elementInfo === undefined) + throw 'views is empty'; + + const element = extractElement(elementInfo); + + if(views.length > 0) { + const nextElemInfo = views[views.length - 1]; + const nextElem = extractElement(nextElemInfo); + + if(nextElem.classList.contains('hidden')) + nextElem.classList.remove('hidden'); + + await doTransition(transition, { + toInfo: nextElemInfo, + toElem: nextElem, + fromInfo: elementInfo, + fromElem: element, + }); + } + + if(!element.classList.contains('hidden')) + element.classList.add('hidden'); + + if(targetBody.contains(element)) + targetBody.removeChild(element); + + return elementInfo; + }; + + const current = () => { + if(views.length < 1) + return undefined; + + return views[views.length - 1]; + }; + + return { + push: push, + pop: pop, + count: () => views.length, + current: current, + currentElement: () => { + const currentInfo = current(); + if(currentInfo === undefined) + return undefined; + + return extractElement(currentInfo); + }, + unshift: async elementInfo => { + if(views.length < 1) + return await push(elementInfo, null); + + if(typeof elementInfo !== 'object') + throw 'elementInfo must be an object'; + + const element = extractElement(elementInfo); + + if(!views.includes(elementInfo)) + views.unshift(elementInfo); + + if(!element.classList.contains('hidden')) + element.classList.add('hidden'); + + if(!targetBody.contains(element)) + targetBody.appendChild(element); + }, + shift: async () => { + if(views.length < 2) + return await pop(null); + + const elementInfo = views.shift(); + const element = extractElement(elementInfo); + + if(element.classList.contains('hidden')) + element.classList.remove('hidden'); + + if(targetBody.contains(element)) + targetBody.removeChild(element); + + return elementInfo; + }, + }; +}; diff --git a/src/mami.js/uniqstr.js b/src/mami.js/uniqstr.js new file mode 100644 index 0000000..3efc8a0 --- /dev/null +++ b/src/mami.js/uniqstr.js @@ -0,0 +1,38 @@ +const MamiRandomInt = function(min, max) { + let ret = 0; + const range = max - min; + + const bitsNeeded = Math.ceil(Math.log2(range)); + if(bitsNeeded > 53) + return -1; + + const bytesNeeded = Math.ceil(bitsNeeded / 8), + mask = Math.pow(2, bitsNeeded) - 1; + + const bytes = new Uint8Array(bytesNeeded); + crypto.getRandomValues(bytes); + + let p = (bytesNeeded - 1) * 8; + for(let i = 0; i < bytesNeeded; ++i) { + ret += bytes[i] * Math.pow(2, p); + p -= 8; + } + + ret &= mask; + + if(ret >= range) + return MamiRandomInt(min, max); + + return min + ret; +}; + +const MamiUniqueStr = (function() { + const chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789'; + + return function(length) { + let str = ''; + for(let i = 0; i < length; ++i) + str += chars[MamiRandomInt(0, chars.length)]; + return str; + }; +})(); diff --git a/src/mami.js/url.js b/src/mami.js/url.js new file mode 100644 index 0000000..a3fbd53 --- /dev/null +++ b/src/mami.js/url.js @@ -0,0 +1,22 @@ +Umi.URI = (function() { + const regex = new RegExp("([A-Za-z][A-Za-z0-9+\\-.]*):(?:(//)(?:((?:[A-Za-z0-9\\-._~!$&'()*+,;=:]|%[0-9A-Fa-f]{2})*)@)?((?:\\[(?:(?:(?:(?:[0-9A-Fa-f]{1,4}:){6}|::(?:[0-9A-Fa-f]{1,4}:){5}|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::)(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)|[Vv][0-9A-Fa-f]+\\.[A-Za-z0-9\\-._~!$&'()*+,;=:]+)\\]|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:[A-Za-z0-9\\-._~!$&'()*+,;=]|%[0-9A-Fa-f]{2})*))(?::([0-9]*))?((?:/(?:[A-Za-z0-9\\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*)|/((?:(?:[A-Za-z0-9\\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:/(?:[A-Za-z0-9\\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*)?)|((?:[A-Za-z0-9\\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:/(?:[A-Za-z0-9\\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*)|)(?:\\?((?:[A-Za-z0-9\\-._~!$&'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*))?(?:\\#((?:[A-Za-z0-9\\-._~!$&'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*))?"); + + return { + Parse: function(url) { + const match = url.match(regex); + if(match === null) + return null; + + return { + Protocol: match[1] || null, + Slashes: match[2] || null, + Authority: match[3] || null, + Host: match[4] || null, + Port: match[5] || null, + Path: match[6] || match[7] || match[8] || null, + Query: match[9] || null, + Hash: match[10] || null, + }; + }, + }; +})(); diff --git a/src/mami.js/user.js b/src/mami.js/user.js new file mode 100644 index 0000000..091e760 --- /dev/null +++ b/src/mami.js/user.js @@ -0,0 +1,66 @@ +Umi.User = function(userId, userName, userColour, userPerms) { + userId = (userId || '').toString(); + userColour = (userColour || 'inherit').toString(); + + const userIdInt = parseInt(userId); + + const setName = function(name) { + userName = (name || '').toString().replace('<', '<').replace('>', '>'); + }; + setName(userName); + + const setColour = function(colour) { + userColour = (colour || 'inherit').toString(); + }; + setColour(userColour); + + let userRank = 0; + let canBan = false; + let canSetNickName = false; + let canCreateChannel = false; + + const setPerms = function(perms) { + perms = (perms || '').toString(); + perms = perms.split(perms.includes("\f") ? "\f" : ' '); + + userRank = parseInt(perms[0] || 0); + canBan = (perms[1] || '0') === '1'; + canSetNickName = (perms[3] || '0') === '1'; + canCreateChannel = (perms[4] || '0') === '1' || (perms[4] || '0') === '2'; + }; + setPerms(userPerms); + + let avatarTime = Date.now(); + + return { + getId: function() { return userId; }, + getIdInt: function() { return userIdInt; }, + + getName: function() { return userName; }, + setName: setName, + + getColour: function() { return userColour; }, + setColour: setColour, + + setPermissions: setPerms, + + getRank: function() { return userRank; }, + isCurrentUser: function() { return Umi.User.currentUser && Umi.User.currentUser.userId === userId; }, + canBan: function() { return canBan; }, + canSilence: function() { return canBan; }, + canCreateChannel: function() { return canCreateChannel; }, + canSetNickName: function() { return canSetNickName; }, + getAvatarTime: function() { return avatarTime; }, + bumpAvatarTime: function() { avatarTime = Date.now(); }, + + isBot: function() { return userId === '-1'; }, + }; +}; +Umi.User.currentUser = undefined; +Umi.User.hasCurrentUser = function() { return Umi.User.currentUser !== undefined; }; +Umi.User.getCurrentUser = function() { return Umi.User.currentUser; }; +Umi.User.setCurrentUser = function(user) { Umi.User.currentUser = user; }; +Umi.User.isCurrentUser = function(user) { + return Umi.User.currentUser !== undefined + && (Umi.User.currentUser === user || Umi.User.currentUser.getId() === user.getId()); +}; diff --git a/src/mami.js/users.js b/src/mami.js/users.js new file mode 100644 index 0000000..e002ae9 --- /dev/null +++ b/src/mami.js/users.js @@ -0,0 +1,77 @@ +Umi.Users = (function() { + const users = new Map; + + const onAdd = [], + onRemove = [], + onClear = [], + onUpdate = []; + + return { + OnAdd: onAdd, + OnRemove: onRemove, + OnClear: onClear, + OnUpdate: onUpdate, + Add: function(user) { + const userId = user.getId(); + if(!users.has(userId)) { + users.set(userId, user); + + for(const i in onAdd) + onAdd[i](user); + } + }, + Remove: function(user) { + const userId = user.getId(); + 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()); + }, + Get: function(userId) { + userId = userId.toString(); + if(users.has(userId)) + return users.get(userId); + return null; + }, + Find: function(userName) { + const found = []; + userName = userName.toLowerCase(); + + users.forEach(function(user) { + if(user.getName().toLowerCase().includes(userName)) + found.push(user); + }); + + return found; + }, + FindExact: function(userName) { + userName = userName.toLowerCase(); + + for(const user of users.values()) + if(user.getName().toLowerCase() === userName) + return user; + + return null; + }, + Update: function(userId, user) { + userId = userId.toString(); + + users.set(userId, user); + user.bumpAvatarTime(); + + for(const i in onUpdate) + onUpdate[i](userId, user); + }, + }; +})(); diff --git a/src/mami.js/utility.js b/src/mami.js/utility.js new file mode 100644 index 0000000..021e293 --- /dev/null +++ b/src/mami.js/utility.js @@ -0,0 +1,267 @@ +const $i = document.getElementById.bind(document); +const $c = document.getElementsByClassName.bind(document); +const $q = document.querySelector.bind(document); +const $qa = document.querySelectorAll.bind(document); +const $t = document.createTextNode.bind(document); + +const $r = function(element) { + if(element && element.parentNode) + element.parentNode.removeChild(element); +}; + +const $ri = function(name) { + $r($i(name)); +}; + +const $ib = function(ref, elem) { + ref.parentNode.insertBefore(elem, ref); +}; + +const $rc = function(element) { + while(element.lastChild) + element.removeChild(element.lastChild); +}; + +const $e = function(info, attrs, child, created) { + info = info || {}; + + if(typeof info === 'string') { + info = {tag: info}; + if(attrs) + info.attrs = attrs; + if(child) + info.child = child; + if(created) + info.created = created; + } + + const elem = document.createElement(info.tag || 'div'); + + if(info.attrs) { + const attrs = info.attrs; + + for(let key in attrs) { + const attr = attrs[key]; + if(attr === undefined || attr === null) + continue; + + switch(typeof attr) { + case 'function': + if(key.substring(0, 2) === 'on') + key = key.substring(2).toLowerCase(); + elem.addEventListener(key, attr); + break; + + case 'object': + if(attr instanceof Array) { + if(key === 'class') + key = 'classList'; + + const prop = elem[key]; + let addFunc = null; + + if(prop instanceof Array) + addFunc = prop.push.bind(prop); + else if(prop instanceof DOMTokenList) + addFunc = prop.add.bind(prop); + + if(addFunc !== null) { + for(let j = 0; j < attr.length; ++j) + addFunc(attr[j]); + } else { + if(key === 'classList') + key = 'class'; + elem.setAttribute(key, attr.toString()); + } + } else { + for(const attrKey in attr) + elem[key][attrKey] = attr[attrKey]; + } + break; + + case 'boolean': + if(attr) + elem.setAttribute(key, ''); + break; + + default: + if(key === 'className') + key = 'class'; + elem.setAttribute(key, attr.toString()); + break; + } + } + } + + if(info.child) { + let children = info.child; + + if(!Array.isArray(children)) + children = [children]; + + for(const child of children) { + switch(typeof child) { + case 'string': + elem.appendChild($t(child)); + break; + + case 'object': + if(child instanceof Element) + elem.appendChild(child); + else if(child.getElement) { + const childElem = child.getElement(); + if(childElem instanceof Element) + elem.appendChild(childElem); + else + elem.appendChild($e(child)); + } else + elem.appendChild($e(child)); + break; + + default: + elem.appendChild($t(child.toString())); + break; + } + } + } + + if(info.created) + info.created(elem); + + return elem; +}; +const $er = (type, props, ...children) => $e({ tag: type, attrs: props, child: children }); + +const $ar = function(array, index) { + array.splice(index, 1); +}; +const $ari = function(array, item) { + let index; + while(array.length > 0 && (index = array.indexOf(item)) >= 0) + $ar(array, index); +}; +const $arf = function(array, predicate) { + let index; + while(array.length > 0 && (index = array.findIndex(predicate)) >= 0) + $ar(array, index); +}; + +const $as = function(array) { + if(array.length < 2) + return; + + for(let i = array.length - 1; i > 0; --i) { + let j = Math.floor(Math.random() * (i + 1)), + tmp = array[i]; + array[i] = array[j]; + array[j] = tmp; + } +}; + +const $x = (function() { + const send = function(method, url, options, body) { + if(options === undefined) + options = {}; + else if(typeof options !== 'object') + throw 'options must be undefined or an object'; + + const xhr = new XMLHttpRequest; + const requestHeaders = new Map; + + if('headers' in options && typeof options.headers === 'object') + for(const name in options.headers) + if(options.headers.hasOwnProperty(name)) + requestHeaders.set(name.toLowerCase(), options.headers[name]); + + if(typeof options.download === 'function') { + xhr.onloadstart = ev => options.download(ev); + xhr.onprogress = ev => options.download(ev); + xhr.onloadend = ev => options.download(ev); + } + + if(typeof options.upload === 'function') { + xhr.upload.onloadstart = ev => options.upload(ev); + xhr.upload.onprogress = ev => options.upload(ev); + xhr.upload.onloadend = ev => options.upload(ev); + } + + if(options.authed) + xhr.withCredentials = true; + + if(typeof options.timeout === 'number') + xhr.timeout = options.timeout; + + if(typeof options.type === 'string') + xhr.responseType = options.type; + + if(typeof options.abort === 'function') + options.abort(() => xhr.abort()); + + if(typeof options.xhr === 'function') + options.xhr(() => xhr); + + if(typeof body === 'object') { + if(body instanceof URLSearchParams) { + requestHeaders.set('content-type', 'application/x-www-form-urlencoded'); + } else if(body instanceof FormData) { + requestHeaders.set('content-type', 'multipart/form-data'); + } else if(body instanceof Blob || body instanceof ArrayBuffer || body instanceof DataView) { + if(!requestHeaders.has('content-type')) + requestHeaders.set('content-type', 'application/octet-stream'); + } else if(!requestHeaders.has('content-type')) { + const bodyParts = []; + for(const name in body) + if(body.hasOwnProperty(name)) + bodyParts.push(encodeURIComponent(name) + '=' + encodeURIComponent(body[name])); + body = bodyParts.join('&'); + requestHeaders.set('content-type', 'application/x-www-form-urlencoded'); + } + } + + return new Promise((resolve, reject) => { + let responseHeaders = undefined; + + xhr.onload = ev => resolve({ + status: xhr.status, + body: () => xhr.response, + text: () => xhr.responseText, + headers: () => { + if(responseHeaders !== undefined) + return responseHeaders; + + responseHeaders = new Map; + + const raw = xhr.getAllResponseHeaders().trim().split(/[\r\n]+/); + for(const name in raw) + if(raw.hasOwnProperty(name)) { + const parts = raw[name].split(': '); + responseHeaders.set(parts.shift(), parts.join(': ')); + } + + return responseHeaders; + }, + xhr: xhr, + ev: ev, + }); + + xhr.onerror = ev => reject({ + xhr: xhr, + ev: ev, + }); + + xhr.open(method, url); + for(const [name, value] of requestHeaders) + xhr.setRequestHeader(name, value); + xhr.send(body); + }); + }; + + return { + send: send, + get: (url, options, body) => send('GET', url, options, body), + post: (url, options, body) => send('POST', url, options, body), + delete: (url, options, body) => send('DELETE', url, options, body), + patch: (url, options, body) => send('PATCH', url, options, body), + put: (url, options, body) => send('PUT', url, options, body), + }; +})(); diff --git a/src/mami.js/websock.js b/src/mami.js/websock.js new file mode 100644 index 0000000..fe8fdac --- /dev/null +++ b/src/mami.js/websock.js @@ -0,0 +1,115 @@ +#include settings.js + +const UmiWebSocket = function(server, message, useWorker) { + if(typeof useWorker === 'undefined') + useWorker = (function() { + // Overrides + if(Umi.Settings.get('neverUseWorker')) + return false; + if(Umi.Settings.get('forceUseWorker')) + return true; + + // Detect chromosomes + if((!!window.chrome || (!!window.Intl && !!Intl.v8BreakIterator)) && 'CSS' in window) + return true; + + // Phones + if((/iphone|ipod|android|ie|blackberry|fennec/i).test(navigator.userAgent.toLowerCase())) + return true; + + return false; + })(); + + let send, close, sendInterval, clearIntervals; + + if(useWorker) { + const worker = new Worker(MAMI_WS); + worker.addEventListener('message', function(ev) { + message(ev.data); + }); + worker.postMessage({act: 'ws:open', server: server}); + send = function(text) { + worker.postMessage({act: 'ws:send', text: text}); + }; + close = function() { + worker.postMessage({act: 'ws:close'}); + }; + getIntervals = function() { + worker.postMessage({act: 'ws:intervals'}); + }; + sendInterval = function(text, interval) { + worker.postMessage({act: 'ws:send_interval', text: text, interval: interval}); + }; + clearIntervals = function() { + worker.postMessage({act: 'ws:clear_intervals'}); + }; + } else { + const websocket = new WebSocket(server), intervals = []; + websocket.addEventListener('open', function(ev) { + message({ + act: 'ws:open', + type: ev.type, + timeStamp: ev.timeStamp, + isTrusted: ev.isTrusted, + }); + }); + websocket.addEventListener('close', function(ev) { + message({ + act: 'ws:close', + type: ev.type, + timeStamp: ev.timeStamp, + isTrusted: ev.isTrusted, + code: ev.code, + reason: ev.reason, + wasClean: ev.wasClean, + }); + }); + websocket.addEventListener('error', function(ev) { + message({ + act: 'ws:error', + type: ev.type, + timeStamp: ev.timeStamp, + isTrusted: ev.isTrusted, + }); + }); + websocket.addEventListener('message', function(ev) { + message({ + act: 'ws:message', + type: ev.type, + timeStamp: ev.timeStamp, + isTrusted: ev.isTrusted, + data: ev.data, + origin: ev.origin, + lastEventId: ev.lastEventId, + }); + }); + send = function(text) { + websocket.send(text); + }; + close = function() { + websocket.close(); + }; + getIntervals = function() { + return intervals; + }; + sendInterval = function(text, interval) { + intervals.push(setInterval(function() { + if(websocket) + websocket.send(text); + }, interval)); + }; + clearIntervals = function() { + for(let i = 0; i < intervals.length; ++i) + clearInterval(intervals[i]); + }; + } + + return { + isUsingWorker: useWorker, + send: send, + close: close, + getIntervals: getIntervals, + sendInterval: sendInterval, + clearIntervals: clearIntervals, + }; +}; diff --git a/src/mami.js/weeb.js b/src/mami.js/weeb.js new file mode 100644 index 0000000..1368fea --- /dev/null +++ b/src/mami.js/weeb.js @@ -0,0 +1,86 @@ +#include common.js +#include rng.js +#include utility.js + +const Weeaboo = (function() { + let kaomoji = []; + const textSfx = [ + 'desu', 'desu wa', 'desu wa ne', 'desu yo', + 'nya', 'nyo', 'nyu', 'nyoron', 'da ze', 'nanodesu', + 'de gozaru', 'desu no', + ]; + const userSfx = new Map; + const pub = {}; + + pub.init = function() { + if(kaomoji.length > 0) + return; + + $x.get(futami.get('kaomoji')) + .then(resp => kaomoji = resp.text().split("\n")); + }; + + pub.getRandomKaomoji = function(allowEmpty, message) { + if(kaomoji.length < 1) + return ''; + + if((typeof allowEmpty).toLowerCase() !== 'boolean') + allowEmpty = true; + + const rng = new MamiRNG(message.getIdInt() || undefined); + + if(allowEmpty && rng.next(0, 10000) <= 9000) + return ''; + + return kaomoji[rng.next() % kaomoji.length]; + }; + + pub.getNameSuffix = function(user) { + if((typeof user).toLowerCase() !== 'object' || user === null) + return ''; + + if(user.getRank() >= 10) + return '-sama'; + if(user.getRank() >= 5) + return '-sensei'; + if(user.getColour().toLowerCase() === '#f02d7d') + return '-san'; + if(user.getColour().toLowerCase() === '#0099ff') + return '-wan'; + + switch(user.getIdInt() % 3) { + default: + return '-chan'; + case 1: + return '-tan'; + case 2: + return '-kun'; + } + }; + + pub.getTextSuffix = function(user) { + if((typeof user).toLowerCase() !== 'object' || user === null) + return ''; + + const userId = user.getId(); + if(userId === '3' || userId === '242') + return ' de geso'; + + if(!userSfx.has(userId)) { + const rng = new MamiRNG(0x51DEB00B | user.getIdInt()); + let str = ' '; + + str += textSfx[rng.next() % textSfx.length]; + if(rng.next(0, 100) >= 50) + str += '~'; + + userSfx.set(userId, str); + + return str; + } + + return userSfx.get(userId); + }; + + return pub; +})(); diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..d825d83 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,35 @@ +const crypto = require('crypto'); + +exports.strtr = (str, replacements) => str.toString().replace( + /{([^}]+)}/g, (match, key) => replacements[key] || match +); + +const trim = function(str, chars, flags) { + if(chars === undefined) + chars = " \n\r\t\v\0"; + + let start = 0, + end = str.length; + + if(flags & 0x01) + while(start < end && chars.indexOf(str[start]) >= 0) + ++start; + + if(flags & 0x02) + while(end > start && chars.indexOf(str[end - 1]) >= 0) + --end; + + return (start > 0 || end < str.length) + ? str.substring(start, end) + : str; +}; + +exports.trimStart = (str, chars) => trim(str, chars, 0x01); +exports.trimEnd = (str, chars) => trim(str, chars, 0x02); +exports.trim = (str, chars) => trim(str, chars, 0x03); + +exports.shortHash = function(text) { + const hash = crypto.createHash('sha256'); + hash.update(text); + return hash.digest('hex').substring(0, 8); +}; diff --git a/src/websock.js/main.js b/src/websock.js/main.js new file mode 100644 index 0000000..08669d7 --- /dev/null +++ b/src/websock.js/main.js @@ -0,0 +1,91 @@ +let websocket, intervals = []; + +addEventListener('message', function(ev) { + if(!ev || !ev.data || !ev.data.act) + return; + + switch(ev.data.act) { + case 'ws:open': + if(!ev.data.server) + break; + + websocket = new WebSocket(ev.data.server); + websocket.addEventListener('open', function(ev) { + postMessage({ + act: 'ws:open', + type: ev.type, + timeStamp: ev.timeStamp, + isTrusted: ev.isTrusted, + }); + }); + websocket.addEventListener('close', function(ev) { + postMessage({ + act: 'ws:close', + type: ev.type, + timeStamp: ev.timeStamp, + isTrusted: ev.isTrusted, + code: ev.code, + reason: ev.reason, + wasClean: ev.wasClean, + }); + }); + websocket.addEventListener('error', function(ev) { + postMessage({ + act: 'ws:error', + type: ev.type, + timeStamp: ev.timeStamp, + isTrusted: ev.isTrusted, + }); + }); + websocket.addEventListener('message', function(ev) { + postMessage({ + act: 'ws:message', + type: ev.type, + timeStamp: ev.timeStamp, + isTrusted: ev.isTrusted, + data: ev.data, + origin: ev.origin, + lastEventId: ev.lastEventId, + }); + }); + break; + + case 'ws:close': + for(const interval of intervals) + clearInterval(interval); + if(!websocket) + break; + websocket.close(); + websocket = null; + intervals = []; + break; + + case 'ws:send': + if(!websocket) + break; + websocket.send(ev.data.text); + break; + + case 'ws:intervals': + postMessage({ + act: 'ws:intervals', + intervals: intervals, + }); + break; + + case 'ws:send_interval': + (function(interval, text) { + intervals.push(setInterval(function() { + if(websocket) + websocket.send(text); + }, interval)); + })(ev.data.interval, ev.data.text); + break; + + case 'ws:clear_intervals': + for(const interval of intervals) + clearInterval(interval); + intervals = []; + break; + } +});