diff --git a/.dependency-cruiser.cjs b/.dependency-cruiser.cjs new file mode 100644 index 00000000..488ff28b --- /dev/null +++ b/.dependency-cruiser.cjs @@ -0,0 +1,414 @@ +/** @type {import('dependency-cruiser').IConfiguration} */ +module.exports = { + forbidden: [ + { + name: 'no-circular', + severity: 'warn', + comment: + 'This dependency is part of a circular relationship. You might want to revise ' + + 'your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ', + from: {}, + to: { + circular: true + } + }, + { + name: 'no-orphans', + comment: + "This is an orphan module - it's likely not used (anymore?). Either use it or " + + "remove it. If it's logical this module is an orphan (i.e. it's a config file), " + + "add an exception for it in your dependency-cruiser configuration. By default " + + "this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " + + "files (.d.ts), tsconfig.json and some of the babel and webpack configs.", + severity: 'warn', + from: { + orphan: true, + pathNot: [ + '(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$', // dot files + '[.]d[.]ts$', // TypeScript declaration files + '(^|/)tsconfig[.]json$', // TypeScript config + '(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$' // other configs + ] + }, + to: {}, + }, + { + name: 'no-deprecated-core', + comment: + 'A module depends on a node core module that has been deprecated. Find an alternative - these are ' + + "bound to exist - node doesn't deprecate lightly.", + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'core' + ], + path: [ + '^v8/tools/codemap$', + '^v8/tools/consarray$', + '^v8/tools/csvparser$', + '^v8/tools/logreader$', + '^v8/tools/profile_view$', + '^v8/tools/profile$', + '^v8/tools/SourceMap$', + '^v8/tools/splaytree$', + '^v8/tools/tickprocessor-driver$', + '^v8/tools/tickprocessor$', + '^node-inspect/lib/_inspect$', + '^node-inspect/lib/internal/inspect_client$', + '^node-inspect/lib/internal/inspect_repl$', + '^async_hooks$', + '^punycode$', + '^domain$', + '^constants$', + '^sys$', + '^_linklist$', + '^_stream_wrap$' + ], + } + }, + { + name: 'not-to-deprecated', + comment: + 'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' + + 'version of that module, or find an alternative. Deprecated modules are a security risk.', + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'deprecated' + ] + } + }, + { + name: 'no-non-package-json', + severity: 'error', + comment: + "This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " + + "That's problematic as the package either (1) won't be available on live (2 - worse) will be " + + "available on live with an non-guaranteed version. Fix it by adding the package to the dependencies " + + "in your package.json.", + from: {}, + to: { + dependencyTypes: [ + 'npm-no-pkg', + 'npm-unknown' + ] + } + }, + { + name: 'not-to-unresolvable', + comment: + "This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " + + 'module: add it to your package.json. In all other cases you likely already know what to do.', + severity: 'error', + from: {}, + to: { + couldNotResolve: true + } + }, + { + name: 'no-duplicate-dep-types', + comment: + "Likely this module depends on an external ('npm') package that occurs more than once " + + "in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " + + "maintenance problems later on.", + severity: 'warn', + from: {}, + to: { + moreThanOneDependencyType: true, + // as it's pretty common to have a type import be a type only import + // _and_ (e.g.) a devDependency - don't consider type-only dependency + // types for this rule + dependencyTypesNot: ["type-only"] + } + }, + + /* rules you might want to tweak for your specific situation: */ + + { + name: 'not-to-spec', + comment: + 'This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. ' + + "If there's something in a spec that's of use to other modules, it doesn't have that single " + + 'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.', + severity: 'error', + from: {}, + to: { + path: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' + } + }, + { + name: 'not-to-dev-dep', + severity: 'error', + comment: + "This module depends on an npm package from the 'devDependencies' section of your " + + 'package.json. It looks like something that ships to production, though. To prevent problems ' + + "with npm packages that aren't there on production declare it (only!) in the 'dependencies'" + + 'section of your package.json. If this module is development only - add it to the ' + + 'from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration', + from: { + path: '^(src/webpage)', + pathNot: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' + }, + to: { + dependencyTypes: [ + 'npm-dev', + ], + // type only dependencies are not a problem as they don't end up in the + // production code or are ignored by the runtime. + dependencyTypesNot: [ + 'type-only' + ], + pathNot: [ + 'node_modules/@types/' + ] + } + }, + { + name: 'optional-deps-used', + severity: 'info', + comment: + "This module depends on an npm package that is declared as an optional dependency " + + "in your package.json. As this makes sense in limited situations only, it's flagged here. " + + "If you're using an optional dependency here by design - add an exception to your" + + "dependency-cruiser configuration.", + from: {}, + to: { + dependencyTypes: [ + 'npm-optional' + ] + } + }, + { + name: 'peer-deps-used', + comment: + "This module depends on an npm package that is declared as a peer dependency " + + "in your package.json. This makes sense if your package is e.g. a plugin, but in " + + "other cases - maybe not so much. If the use of a peer dependency is intentional " + + "add an exception to your dependency-cruiser configuration.", + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'npm-peer' + ] + } + } + ], + options: { + + /* Which modules not to follow further when encountered */ + doNotFollow: { + /* path: an array of regular expressions in strings to match against */ + path: ['node_modules'] + }, + + /* Which modules to exclude */ + // exclude : { + // /* path: an array of regular expressions in strings to match against */ + // path: '', + // }, + + /* Which modules to exclusively include (array of regular expressions in strings) + dependency-cruiser will skip everything not matching this pattern + */ + // includeOnly : [''], + + /* List of module systems to cruise. + When left out dependency-cruiser will fall back to the list of _all_ + module systems it knows of. It's the default because it's the safe option + It might come at a performance penalty, though. + moduleSystems: ['amd', 'cjs', 'es6', 'tsd'] + + As in practice only commonjs ('cjs') and ecmascript modules ('es6') + are widely used, you can limit the moduleSystems to those. + */ + + // moduleSystems: ['cjs', 'es6'], + + /* + false: don't look at JSDoc imports (the default) + true: dependency-cruiser will detect dependencies in JSDoc-style + import statements. Implies "parser": "tsc", so the dependency-cruiser + will use the typescript parser for JavaScript files. + + For this to work the typescript compiler will need to be installed in the + same spot as you're running dependency-cruiser from. + */ + // detectJSDocImports: true, + + /* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/main/' + to open it on your online repo or `vscode://file/${process.cwd()}/` to + open it in visual studio code), + */ + // prefix: `vscode://file/${process.cwd()}/`, + + /* false (the default): ignore dependencies that only exist before typescript-to-javascript compilation + true: also detect dependencies that only exist before typescript-to-javascript compilation + "specify": for each dependency identify whether it only exists before compilation or also after + */ + // tsPreCompilationDeps: false, + + /* list of extensions to scan that aren't javascript or compile-to-javascript. + Empty by default. Only put extensions in here that you want to take into + account that are _not_ parsable. + */ + // extraExtensionsToScan: [".json", ".jpg", ".png", ".svg", ".webp"], + + /* if true combines the package.jsons found from the module up to the base + folder the cruise is initiated from. Useful for how (some) mono-repos + manage dependencies & dependency definitions. + */ + // combinedDependencies: false, + + /* if true leave symlinks untouched, otherwise use the realpath */ + // preserveSymlinks: false, + + /* TypeScript project file ('tsconfig.json') to use for + (1) compilation and + (2) resolution (e.g. with the paths property) + + The (optional) fileName attribute specifies which file to take (relative to + dependency-cruiser's current working directory). When not provided + defaults to './tsconfig.json'. + */ + tsConfig: { + fileName: 'tsconfig.json' + }, + + /* Webpack configuration to use to get resolve options from. + + The (optional) fileName attribute specifies which file to take (relative + to dependency-cruiser's current working directory. When not provided defaults + to './webpack.conf.js'. + + The (optional) `env` and `arguments` attributes contain the parameters + to be passed if your webpack config is a function and takes them (see + webpack documentation for details) + */ + // webpackConfig: { + // fileName: 'webpack.config.js', + // env: {}, + // arguments: {} + // }, + + /* Babel config ('.babelrc', '.babelrc.json', '.babelrc.json5', ...) to use + for compilation + */ + // babelConfig: { + // fileName: '.babelrc', + // }, + + /* List of strings you have in use in addition to cjs/ es6 requires + & imports to declare module dependencies. Use this e.g. if you've + re-declared require, use a require-wrapper or use window.require as + a hack. + */ + // exoticRequireStrings: [], + + /* options to pass on to enhanced-resolve, the package dependency-cruiser + uses to resolve module references to disk. The values below should be + suitable for most situations + + If you use webpack: you can also set these in webpack.conf.js. The set + there will override the ones specified here. + */ + enhancedResolveOptions: { + /* What to consider as an 'exports' field in package.jsons */ + exportsFields: ["exports"], + /* List of conditions to check for in the exports field. + Only works when the 'exportsFields' array is non-empty. + */ + conditionNames: ["import", "require", "node", "default", "types"], + /* The extensions, by default are the same as the ones dependency-cruiser + can access (run `npx depcruise --info` to see which ones that are in + _your_ environment). If that list is larger than you need you can pass + the extensions you actually use (e.g. [".js", ".jsx"]). This can speed + up module resolution, which is the most expensive step. + */ + // extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"], + /* What to consider a 'main' field in package.json */ + mainFields: ["module", "main", "types", "typings"], + /* A list of alias fields in package.jsons + + See [this specification](https://github.com/defunctzombie/package-browser-field-spec) and + the webpack [resolve.alias](https://webpack.js.org/configuration/resolve/#resolvealiasfields) + documentation. + + Defaults to an empty array (= don't use alias fields). + */ + // aliasFields: ["browser"], + }, + + /* skipAnalysisNotInRules will make dependency-cruiser execute + analysis strictly necessary for checking the rule set only. + + See https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#skipanalysisnotinrules + for details + */ + skipAnalysisNotInRules: true, + + /* List of built-in modules to use on top of the ones node declares. + + See https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#builtinmodules-influencing-what-to-consider-built-in--core-modules + for details + */ + builtInModules: { + add: [ + "bun", + "bun:ffi", + "bun:jsc", + "bun:sqlite", + "bun:test", + "bun:wrap", + "detect-libc", + "undici", + "ws" + ] + }, + + reporterOptions: { + dot: { + /* pattern of modules that can be consolidated in the detailed + graphical dependency graph. The default pattern in this configuration + collapses everything in node_modules to one folder deep so you see + the external modules, but their innards. + */ + collapsePattern: 'node_modules/(?:@[^/]+/[^/]+|[^/]+)', + + /* Options to tweak the appearance of your graph.See + https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions + for details and some examples. If you don't specify a theme + dependency-cruiser falls back to a built-in one. + */ + // theme: { + // graph: { + // /* splines: "ortho" gives straight lines, but is slow on big graphs + // splines: "true" gives bezier curves (fast, not as nice as ortho) + // */ + // splines: "true" + // }, + // } + }, + archi: { + /* pattern of modules that can be consolidated in the high level + graphical dependency graph. If you use the high level graphical + dependency graph reporter (`archi`) you probably want to tweak + this collapsePattern to your situation. + */ + collapsePattern: '^(?:packages|src|lib(s?)|app(s?)|bin|test(s?)|spec(s?))/[^/]+|node_modules/(?:@[^/]+/[^/]+|[^/]+)', + + /* Options to tweak the appearance of your graph. If you don't specify a + theme for 'archi' dependency-cruiser will use the one specified in the + dot section above and otherwise use the default one. + */ + // theme: { }, + }, + "text": { + "highlightFocused": true + }, + } + } +}; +// generated: dependency-cruiser@17.0.2 on 2025-10-13T01:46:58.028Z diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..0d638322 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve. +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser: [e.g. Chrome, Safari] + - Browser Version: [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1, Android 16] + - Browser: [e.g. stock browser, safari] + - Browser Version: [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..28a84803 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project. +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Example: I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..6ff7a909 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,5 @@ +# Description +A description of the pull request. + +# Related issues +Any related issues, delete if there are none. diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 00000000..3f53646d --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,18 @@ +name: Docker Image CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build the Docker image + run: docker build . --file Dockerfile --tag my-image-name:$(date +%s) diff --git a/.github/workflows/tsc-check.yml b/.github/workflows/tsc-check.yml new file mode 100644 index 00000000..bd008b01 --- /dev/null +++ b/.github/workflows/tsc-check.yml @@ -0,0 +1,20 @@ +name: TSC check CI + +on: + push: + pull_request: + +jobs: + tsc: + name: tsc + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: install node v22 + uses: actions/setup-node@v1 + with: + node-version: 22 + - name: npm i + run: npm i + - name: tsc + run: tsc --noEmit diff --git a/.gitignore b/.gitignore index 84887f9a..44b79cee 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,6 @@ uptime.json .dist/ bun.lockb src/webpage/translations/langs.js + +build.js +dependency-graph.svg diff --git a/.swcrc b/.swcrc new file mode 100644 index 00000000..3b7eb071 --- /dev/null +++ b/.swcrc @@ -0,0 +1,13 @@ +{ + "$schema": "https://swc.rs/schema.json", + "jsc": { + "parser": { + "syntax": "typescript", + "tsx": false + }, + + "target": "es2024" + }, + "sourceMaps": true, + "minify": true +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..9887eae0 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,5 @@ +# Code of conduct +It's nothing complicated, I want to foster a nice community, if there's any issues feel free to contact me in any way you see fit, though please don't create problems for me. I'm just one person and I want to work on this project, not community management. I will likely start with a warning or two if any issues arise, though this is up to my (mathium05) sole discretion. +Generally follow https://docs.spacebar.chat/contributing/conduct/ and you should be fine. Do not try to pull technicalities, this is a FOSS project, not a court of law. + +Happy coding! diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..147a54c9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1 @@ +This ain't exactly rocket science, please just describe what you've done, and follow all normal steps, it may take a few days for me to get back to you, life happens. Just try to keep the pull requests fairly small, so adding one thing or fixing one thing, but you may fix multiple bugs in one patch if they're either related or small enough. These are all soft rules and I am going to be more lenient. diff --git a/CompressingImages.md b/CompressingImages.md index 98f2533c..9e7b0db4 100644 --- a/CompressingImages.md +++ b/CompressingImages.md @@ -2,31 +2,39 @@ This is for in the future when I want to compress more images or anyone else for # Lossless ### https://squoosh.app/ -good at reducing the pallet, a first step for images that have a limited number of colors, bad at actually compressing things though, for all formats except webp. +Good at reducing the pallet, a first step for images that have a limited number of colors, bad at actually compressing things though, for all formats except WEBP and PNG. ## PNGs: -good ratios, though not as good as other options, though better compatibility +Good ratios, though not as good as other options, though better compatibility. +### oxipng +*(you can also use through squoosh with the same results)* +Seems to be the best of all of the options, not sure if it's all you would need, but it did shrink pngs further than the other two tools afterwards. +```bash +oxipng -o max --strip all --alpha +``` +`all` may be replaced with `safe` if you want to be a bit safer. + ### pngcrush -Good, but should be ran before optipng, but isn't as good as it, use in tandom +Good, but should be ran before optipng, but isn't as good as it, use in tandom. ### optipng -The best tool to really shrink pngs to be as small as they can be. +The second best tool to really shrink pngs to be as small as they can be. ## WEBP -it's better than png, though I have a feeling more could be done to compress these +It's better than png, though I have a feeling more could be done to compress these ### cwebp -so far this seems to be the best way to compress webp images with a command that kinda looks like this one +So far this seems to be the best way to compress WEBP images with a command that kinda looks like this one: ```bash cwebp -lossless -z 9 in.webp -o out.webp ``` -while for all other formats squoosh is not recommended, for webp it'll be identical due to cwebp using the same libary as squoosh. +While for all other formats squoosh is not recommended, for WEBP it'll be identical due to cWEBP using the same libary as squoosh. ## AVIF As far as I can tell, this format just sucks at its job, at least for lossless images ## JPEGXL -Really good at compression size, though it's not supported anywhere outside of safari as of now. +Really good at compression size, though it's not supported anywhere outside of Safari as of now. ### cjxl -this command should do the trick for compressing +This command should do the trick for compressing: ```bash cjxl input.png output.jxl -q 100 -e 10 ``` @@ -35,6 +43,6 @@ cjxl input.png output.jxl -q 100 -e 10 ## SVGs: ### https://svgomg.net/ -great tool, if anyone knows how to squish them further, let me know, some manual work may go a long way to help shrink svgs, though I'm not doing that right now lol. +Great tool! If anyone knows how to squish them further, let me know! Some manual work may go a long way to help shrink SVGs, though I'm not doing that right now lol. -I may look into other formats soon as well, though these are the main two I'm currently using +I may look into other formats soon as well, though these are the main two I'm currently using. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..f831b1c6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM node:18-bullseye AS builder + +WORKDIR /devel +RUN apt-get update ; apt-get upgrade -y ; apt-get install -y build-essential +COPY . . +RUN npm i ; npm run build + +FROM node:20-alpine + +EXPOSE 8080 +WORKDIR /exec +RUN apk add --update nodejs npm +COPY --from=builder /devel/ . +RUN adduser -D jankclient + +USER jankclient + +CMD ["npm", "start"] diff --git a/InstanceInfo.md b/InstanceInfo.md index 6314c9aa..6deb0c86 100644 --- a/InstanceInfo.md +++ b/InstanceInfo.md @@ -1,15 +1,15 @@ -# How to add your instance to Jank Client -inside of webpage you'll see a file called `instances.json` in that file you'll need to add your instance and its information in the following format if you want your instance to be a part of the drop down. +# How to add your instance to Fermi Client +Inside of `webpage` you'll see a file called `instances.json` in that file you'll need to add your instance and its information in the following format if you want your instance to be a part of the dropdown. ``` { "name":, - "description"?:, + "description"?:, "descriptionLong"?:, - "image"?:, + "image"?:, "url"?:, - "language":, - "country":, - "display":, + "language":, + "country":, + "display":, "urls"?:{ "wellknown":, "api":, @@ -19,26 +19,27 @@ inside of webpage you'll see a file called `instances.json` in that file you'll }, "contactInfo"?:{ "discord"?:, - "github"?:, - "email"?:, - "spacebar":?:, - "matrix"?:, - "mastodon"?: + "github"?:, + "email"?:, + "spacebar":?:, + "matrix"?:, + "mastodon"?: } } ``` -anything with a `?` in-front of its `:` are optional, though you must either include `"URL"` or `"URLs"`, but you may include both, though the client will most likely ignore `"URLs"` in favor of `"URL"`, though it may use `"URLs"` as a fallback if `"URL"` does not resolve, do not rely on this behavior. -wellknown should be a url that can resolve the wellknown, but it should only be the base URL and not the full wellknown url. +Anything with a `?` in-front of its `:` are optional, though you must either include `"URL"` or `"URLs"`, but you may include both, though the client will most likely ignore `"URLs"` in favor of `"URL"`, though it may use `"URLs"` as a fallback if `"URL"` does not resolve, do not rely on this behavior. +`wellknown` should be a url that can resolve, but it should only be the base URL and not the full wellknown URL. Some of these values may not be used right now, though they will likely be used in the future, so feel free to fill out what you like, though the more you fill out the more information we can give the users about your instance in the future. -language should be [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1_codes). -Country should be [ISO 8166-2 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2). -You can also add yourself to [this](https://github.com/spacebarchat/spacebarchat/tree/master/instances) list, and you should, though there are some disadvantages to only being in that list +`language` should be [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1_codes). +`country` should be [ISO 8166-2 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2). +You can also add yourself to [this](https://github.com/spacebarchat/spacebarchat/tree/master/instances) list, and you should, though there are **some disadvantages** to only being in that list such as not being on the Fermi Discovery list which shows Guilds more higher up on the Fermi client, etc. + # Questions -## Do I have to do this to let Jank Client connect to my server? -No, you may choose to not do this, this just makes it easier for people using Jank Client to find and use your instance as it's in the dropdown menu for instances, though the user may enter any instance they please. -## If my instance isn't spacebar is that allowed to be entered? -If it's spacebar compatable, yes it may be entered, though if there are too many incompatablities, it may not be included, or may need a warning of sorts. -## I'm hosting my own instance of spacebar and would like to change the defualt instance on my instance of Jank Client to my own instance. +## Do I have to do this to let Fermi Client connect to my server? +No, you may choose to not do this, this just makes it easier for people using Fermi Client to find and use your instance as it's in the dropdown menu for instances, though the user may enter any instance they please. +## If my instance isn't Spacebar is that allowed to be entered? +If it's Spacebar compatible then yes, it may be entered. Though if there are **too many incompatibilities** however, it may not be included, or may need a warning of sorts. +## I'm hosting my own instance of Spacebar and would like to change the default instance on my instance of Fermi Client to my own instance. Just change the first entry in the list to your own, and it should connect without issue. -## Why would I put my instance in this list over the official spacebar list? -While putting your instance in the other list will get it to show up on jank client, this list does have more settings, and will show up earlier in the results, though either list will work to get in the dropdown menu +## Why would I put my instance in this list over the official Spacebar list? +While putting your instance in the other list will get it to show up on Fermi client, this list does have more settings, and will show up earlier in the results, though either list will work to get in the dropdown menu. diff --git a/README.md b/README.md index af4289e5..6bf2e62c 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,37 @@ -# Jank Client -Jank Client is a [Spacebar](https://spacebar.chat) Client written in TS, HTML, and CSS. +# Fermi +Fermi is a [Spacebar](https://spacebar.chat) Client written in TS, HTML, and CSS. -To run it, clone the repo and do `npm install`, then -`node index.js` -or do the equivalent with bun +![](src/webpage/home/SS1.webp) + +To build it, clone the repo and run `npm install`, then `npm run build` +To run it, use `npm start` +or do the equivalent with bun. Both [Bun](https://bun.sh) and [Node.js](https://nodejs.org) are supported, and should function as expected. -To access Jank Client after starting, simply go to http://localhost:8080/login and either register a new account, or log in with your email and password. +To access Fermi after starting, simply go to http://localhost:8080/login and either register a new account, or log in with your email and password. If there are any issues please report them either here, or to me dirrectly on spacebar ## Adding instances to the dropdown -Please see [this](https://github.com/MathMan05/JankClient/blob/main/InstanceInfo.md) for how to add an instance to the dropdown picker +Please see [this](https://github.com/MathMan05/Fermi/blob/main/InstanceInfo.md) for how to add an instance to the dropdown picker. +## How to statically host Fermi +[Click here](./howToStaticallyHost.md) ## RoadMap You can view the current roadmap on https://github.com/users/MathMan05/projects/1. ## AI Code -AI code due to not being GPLv3 compatable is not allowed in this repo. I thought this didn't need to be said, but it doesn't. +AI code due to not being GPLv3 compatable is not allowed in this repo. I thought this didn't need to be said, but it does. +And to be clear, *any* use of AI is not allowed in Fermi. ## Link -The official SpaceBar server for Jank Client https://jankclient.greysilly7.xyz/invite/USgYJo?instance=https%3A%2F%2Fspacebar.chat +The official Spacebar server for Fermi: https://fermi.chat/invite/USgYJo?instance=https%3A%2F%2Fspacebar.chat + +The current hosted instance of Fermi: https://fermi.chat/ -old invite for the official client https://dev.app.spacebar.chat/invite/USgYJo +## Star History -The current hosted instance of JankClient https://jankclient.greysilly7.xyz/ + + + + + Star History Chart + + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..02c02659 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +## Supported Versions + +Currently, I only support the most up to date version of Fermi client, there are no stable releases, but this is planned for in the future + +| Version | Supported | +| ------- | ------------------ | +| main | :white_check_mark: | + + +## Reporting a Vulnerability + +If there's an issue please disclose it responsibly to me, or here on Github privately. diff --git a/build.ts b/build.ts new file mode 100644 index 00000000..3f8f7c24 --- /dev/null +++ b/build.ts @@ -0,0 +1,259 @@ +import {promises as fs} from "fs"; +import * as swc from "@swc/core"; +import {fileURLToPath} from "node:url"; +import path from "node:path"; +import child_process from "child_process"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +let urlMaybe = process.env.URL; +if (urlMaybe && URL.canParse(urlMaybe)) { + if (urlMaybe.endsWith("/")) { + const temp = urlMaybe.split("/"); + temp.pop(); + urlMaybe = temp.join("/"); + } +} else { + urlMaybe = undefined; +} +let entryPoints: {sPath: string; newPath: string}[] = []; +async function bundleFiles() { + const filter = new Set(); + entryPoints = entryPoints.filter((_) => { + if (filter.has(_.sPath)) return false; + filter.add(_.sPath); + return true; + }); + let mod = await swc.bundle( + entryPoints.map(({sPath}) => { + return { + entry: sPath, + output: { + path: path.parse(sPath).dir, + name: path.parse(sPath).base, + }, + env: { + targets: "Chrome > 120", + }, + module: { + minify: true, + sourceMaps: true, + isModule: true, + jsc: { + minify: { + mangle: false, + }, + }, + }, + externalModules: ["/translations/langs.js"], + options: { + minify: !process.argv.includes("watch"), + jsc: { + minify: { + mangle: false, + }, + }, + }, + }; + }), + ); + + await Promise.all( + entryPoints.map(async ({sPath, newPath}) => { + const code = mod[path.parse(sPath).base] || mod; + const newfileDir = path.join(newPath, path.parse(sPath).name); + + await Promise.all([ + fs.writeFile( + newfileDir + ".js", + code.code + "\n" + `//# sourceMappingURL=${path.parse(sPath).name}.js.map`, + ), + code.map ? fs.writeFile(newfileDir + ".js.map", code.map as string) : null, + ]); + }), + ); +} +async function moveFiles(curPath: string, newPath: string, first = true) { + async function processFile(file: string) { + const Prom: Promise[] = []; + if ((await fs.stat(path.join(curPath, file))).isDirectory()) { + await fs.mkdir(path.join(newPath, file)); + Prom.push(moveFiles(path.join(curPath, file), path.join(newPath, file), false)); + } else { + if (!file.endsWith(".ts")) { + if (file.endsWith(".html")) { + for (const [, match] of (await fs.readFile(path.join(curPath, file))) + .toString() + .matchAll(/ processFile(_))); +} +async function crawlDir(dir: string) { + const dirs = await fs.readdir(dir); + const m = await Promise.all( + dirs.map(async (file) => { + const idir = path.join(dir, file); + const stats = await fs.lstat(idir); + if (stats.isDirectory()) { + return [file, await crawlDir(idir)] as const; + } else { + if (file.startsWith(".")) { + //Don't show hidden files lol + return [file, undefined] as const; + } + return [file, file] as const; + } + }), + ); + const obj = {}; + m.forEach((_) => (obj[_[0]] = _[1])); + return obj; +} +async function build() { + entryPoints = []; + console.time("build"); + + console.time("Cleaning dir"); + try { + await fs.rm(path.join(__dirname, "dist"), {recursive: true}); + } catch {} + await fs.mkdir(path.join(__dirname, "dist")); + console.timeEnd("Cleaning dir"); + + console.time("Moving files"); + await moveFiles(path.join(__dirname, "src"), path.join(__dirname, "dist")); + console.timeEnd("Moving files"); + + console.time("Bundling TS"); + await bundleFiles(); + console.timeEnd("Bundling TS"); + + console.time("Moving translations"); + try { + await fs.mkdir(path.join(__dirname, "dist", "webpage", "translations")); + } catch {} + let langs = await fs.readdir(path.join(__dirname, "translations")); + langs = langs.filter((e) => e !== "qqq.json"); + const langobj = {}; + for (const lang of langs) { + const str = (await fs.readFile(path.join(__dirname, "translations", lang))).toString(); + const json = JSON.parse(str); + langobj[lang] = json.readableName; + fs.writeFile(path.join(__dirname, "dist", "webpage", "translations", lang), str); + } + await fs.writeFile( + path.join(__dirname, "dist", "webpage", "translations", "langs.js"), + `const langs=${JSON.stringify(langobj)};export{langs}`, + ); + console.timeEnd("Moving translations"); + + let revision = process.env.VER; + if (!revision) { + console.time("Getting git commit hash"); + revision = child_process.execSync("git rev-parse HEAD").toString().trim(); + await fs.writeFile(path.join(__dirname, "dist", "webpage", "getupdates"), revision); + console.timeEnd("Getting git commit hash"); + } + + console.time("Writing version"); + await fs.writeFile(path.join(__dirname, "dist", "webpage", "getupdates"), revision); + console.timeEnd("Writing version"); + + console.time("Building Service File"); + const dir = await crawlDir(path.join(__dirname, "dist", "webpage")); + dir["files.json"] = "files.json"; + await fs.writeFile(path.join(__dirname, "dist", "webpage", "files.json"), JSON.stringify(dir)); + + console.timeEnd("Building Service File"); + + console.timeEnd("build"); + console.log(""); +} + +await build(); +if (process.argv.includes("watch")) { + let last = Date.now(); + (async () => { + for await (const thing of fs.watch(path.join(__dirname, "src"), {recursive: true})) { + if (Date.now() - last < 100) { + continue; + } + last = Date.now(); + try { + await build(); + } catch {} + } + })(); + (async () => { + for await (const thing of fs.watch(path.join(__dirname, "translations"))) { + if (Date.now() - last < 100) { + continue; + } + last = Date.now(); + try { + await build(); + } catch {} + } + })(); +} diff --git a/buildnode.js b/buildnode.js new file mode 100644 index 00000000..db54aa83 --- /dev/null +++ b/buildnode.js @@ -0,0 +1,15 @@ +import * as swc from "@swc/core"; +import path from "node:path"; +import {fileURLToPath} from "node:url"; +import {promises as fs} from "fs"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const mod = await swc.transformFile(path.join(__dirname, "build.ts"), { + minify: true, + sourceMaps: true, + isModule: true, +}); + +await fs.writeFile(path.join(__dirname, "build.js"), mod.code); diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 00000000..ba6944f9 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,8 @@ +services: + jank: + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + ports: + - "8080:8080" diff --git a/emoji-packer.cjs b/emoji-packer.cjs deleted file mode 100644 index 46be3c7f..00000000 --- a/emoji-packer.cjs +++ /dev/null @@ -1,113 +0,0 @@ -const emojilist=require("./EmojiList/data-by-group.json"); -console.log(emojilist); - -const buffer=new ArrayBuffer(2**26); -const view = new DataView(buffer, 0); -let i=0; -function write16(numb){ - view.setUint16(i,numb); - i+=2; -} -function write8(numb){ - view.setUint8(i,numb); - i+=1; -} -function writeString8(str){ - const encode=new TextEncoder("utf-8").encode(str); - write8(encode.length); - for(const thing of encode){ - write8(thing); - } -} -function writeString16(str){ - const encode=new TextEncoder("utf-8").encode(str); - write16(encode.length); - for(const thing of encode){ - write8(thing); - } -} -function writeStringNo(str){ - const encode=new TextEncoder("utf-8").encode(str); - for(const thing of encode){ - write8(thing); - } -} - -write16(emojilist.length); -for(const thing of emojilist){ - writeString16(thing.name); - write16(thing.emojis.length); - for(const emoji of thing.emojis){ - writeString8(emoji.name); - write8(new TextEncoder("utf-8").encode(emoji.emoji).length+128*emoji.skin_tone_support); - writeStringNo(emoji.emoji); - } -} -const out=new ArrayBuffer(i); -const ar=new Uint8Array(out); -const br=new Uint8Array(buffer); -for(const thing in ar){ - ar[thing]=br[thing]; -} -console.log(i,ar); - -function decodeEmojiList(buffer){ - const view = new DataView(buffer, 0); - let i=0; - function read16(){ - const int=view.getUint16(i); - i+=2; - return int; - } - function read8(){ - const int=view.getUint8(i); - i+=1; - return int; - } - function readString8(){ - return readStringNo(read8()); - } - function readString16(){ - return readStringNo(read16()); - } - function readStringNo(length){ - const array=new Uint8Array(length); - - for(let i=0;i127; - const emoji=readStringNo(len-skin_tone_support*128); - emojis.push({ - name, - skin_tone_support, - emoji - }); - } - build.push({ - name, - emojis - }); - } - return build; -} -console.log(JSON.stringify(decodeEmojiList(out))); - -const fs = require("node:fs"); -fs.writeFile("./webpage/emoji.bin",new Uint8Array(out),_=>{ - -}); diff --git a/emoji-packer.cts b/emoji-packer.cts new file mode 100644 index 00000000..89688968 --- /dev/null +++ b/emoji-packer.cts @@ -0,0 +1,126 @@ +const res = await fetch( + "https://raw.githubusercontent.com/muan/unicode-emoji-json/refs/heads/main/data-by-group.json", +); +const emojilist = (await res.json()) as { + name: string; + slug: string; + emojis: { + emoji: string; + name: string; + slug: string; + skin_tone_support: boolean; + }[]; +}[]; +console.log(emojilist); + +const buffer = new ArrayBuffer(2 ** 26); +const view = new DataView(buffer, 0); +let i = 0; +function write16(numb: number) { + view.setUint16(i, numb); + i += 2; +} +function write8(numb: number) { + view.setUint8(i, numb); + i += 1; +} +function writeString8(str: string) { + const encode = new TextEncoder().encode(str); + write8(encode.length); + for (const thing of encode) { + write8(thing); + } +} +function writeString16(str: string) { + const encode = new TextEncoder().encode(str); + write16(encode.length); + for (const thing of encode) { + write8(thing); + } +} +function writeStringNo(str: string) { + const encode = new TextEncoder().encode(str); + for (const thing of encode) { + write8(thing); + } +} + +write16(emojilist.length); +for (const thing of emojilist) { + writeString16(thing.name); + write16(thing.emojis.length); + for (const emoji of thing.emojis) { + writeString8(emoji.name); + write8(new TextEncoder().encode(emoji.emoji).length + 128 * +emoji.skin_tone_support); + writeStringNo(emoji.emoji); + } +} +const out = new ArrayBuffer(i); +const ar = new Uint8Array(out); +const br = new Uint8Array(buffer); +for (const thing in ar) { + ar[thing] = br[thing]; +} +console.log(i, ar); + +function decodeEmojiList(buffer: ArrayBuffer) { + const view = new DataView(buffer, 0); + let i = 0; + function read16() { + const int = view.getUint16(i); + i += 2; + return int; + } + function read8() { + const int = view.getUint8(i); + i += 1; + return int; + } + function readString8() { + return readStringNo(read8()); + } + function readString16() { + return readStringNo(read16()); + } + function readStringNo(length) { + const array = new Uint8Array(length); + + for (let i = 0; i < length; i++) { + array[i] = read8(); + } + //console.log(array); + return new TextDecoder("utf-8").decode(array.buffer); + } + const build: { + name: string; + emojis: {name: string; skin_tone_support: boolean; emoji: string}[]; + }[] = []; + let cats = read16(); + + for (; cats !== 0; cats--) { + const name = readString16(); + const emojis: {name: string; skin_tone_support: boolean; emoji: string}[] = []; + let emojinumber = read16(); + for (; emojinumber !== 0; emojinumber--) { + //console.log(emojis); + const name = readString8(); + const len = read8(); + const skin_tone_support = len > 127; + const emoji = readStringNo(len - +skin_tone_support * 128); + emojis.push({ + name, + skin_tone_support, + emoji, + }); + } + build.push({ + name, + emojis, + }); + } + return build; +} +console.log(JSON.stringify(decodeEmojiList(out))); + +const fs = require("node:fs"); +fs.writeFile("./webpage/emoji.bin", new Uint8Array(out), (_) => {}); diff --git a/eslint.config.cjs b/eslint.config.cjs.hidden similarity index 100% rename from eslint.config.cjs rename to eslint.config.cjs.hidden diff --git a/gulpfile.cjs b/gulpfile.cjs deleted file mode 100644 index f317e6fb..00000000 --- a/gulpfile.cjs +++ /dev/null @@ -1,125 +0,0 @@ -const gulp = require("gulp"); -const ts = require("gulp-typescript"); -const swc = require("gulp-swc"); -const tsProject = ts.createProject("tsconfig.json"); -const argv = require("yargs").argv; -const rimraf = require("rimraf"); -const plumber = require("gulp-plumber"); -const sourcemaps = require('gulp-sourcemaps'); -const fs=require("fs"); -const swcOptions = { -jsc: { - parser: { - syntax: "typescript", - tsx: false, - decorators: true, - dynamicImport: true, - }, - transform: { - react: { - runtime: "automatic", - }, - }, - target: "es2022", - loose: false, - externalHelpers: false, - keepClassNames: true, - }, - module: { - type: "es6", - strict: true, - strictMode: true, - lazy: false, - noInterop: false, - }, - sourceMaps: true, - minify: false, -}; - - - -gulp.task('watch', function () { - gulp.watch('./src', gulp.series("default")); - gulp.watch('./translations', gulp.series("default")); -}, {debounceDelay: 10}); - -// Clean task to delete the dist directory -gulp.task("clean", (cb) => { - return rimraf.rimraf("dist").then(cb()); - }); - const exec = require('child_process').exec; - // Task to compile TypeScript files using SWC - gulp.task("scripts", async () => { - if (argv.swc) { - return gulp - .src("src/**/*.ts") - .pipe(sourcemaps.init()) - .pipe(plumber()) // Prevent pipe breaking caused by errors - .pipe(swc(swcOptions)) - .pipe(sourcemaps.write('.')) - .pipe(gulp.dest("dist")); - } else if(argv.bunswc){ - return await new Promise(ret=>{ - exec("bun swc ./src -s -d dist").on('exit', function (code) { - ret(); - }); - }) - }else { - console.warn("[WARN] Using TSC compiler, will be slower than SWC"); - return gulp - .src("src/**/*.ts") - .pipe(sourcemaps.init()) - .pipe(plumber()) // Prevent pipe breaking caused by errors - .pipe(tsProject()) - .pipe(sourcemaps.write('.')) - .pipe(gulp.dest("dist")); - } -}); - -// Task to copy HTML files -gulp.task("copy-html", () => { -return gulp - .src("src/**/*.html") - .pipe(plumber()) // Prevent pipe breaking caused by errors - .pipe(gulp.dest("dist")); -}); -gulp.task("copy-translations", () => { -let langs=fs.readdirSync("translations"); -langs=langs.filter((e)=>e!=="qqq.json"); -const langobj={}; -for(const lang of langs){ - const json=JSON.parse(fs.readFileSync("translations/"+lang).toString()); - langobj[lang]=json.readableName; -} -if(!fs.existsSync("dist/webpage/translations")) fs.mkdirSync("dist/webpage/translations") -fs.writeFileSync("dist/webpage/translations/langs.js",`const langs=${JSON.stringify(langobj)};export{langs}`); -return gulp - .src("translations/*.json") - .pipe(plumber()) // Prevent pipe breaking caused by errors - .pipe(gulp.dest("dist/webpage/translations")); -}); -// Task to copy other static assets (e.g., CSS, images) -gulp.task("copy-assets", () => { -return gulp - .src([ - "src/**/*.css", - "src/**/*.bin", - "src/**/*.ico", - "src/**/*.json", - "src/**/*.js", - "src/**/*.png", - "src/**/*.jpg", - "src/**/*.jpeg", - "src/**/*.webp", - "src/**/*.gif", - "src/**/*.svg", - ],{encoding:false}) - .pipe(plumber()) // Prevent pipe breaking caused by errors - .pipe(gulp.dest("dist")); -}); - -// Default task to run all tasks -gulp.task( -"default", -gulp.series("clean", gulp.parallel("scripts", "copy-html", "copy-assets"), "copy-translations") -); diff --git a/howToStaticallyHost.md b/howToStaticallyHost.md new file mode 100644 index 00000000..77f880ac --- /dev/null +++ b/howToStaticallyHost.md @@ -0,0 +1,19 @@ +### How to statically host Fermi +Fermi due to its service worker, will technically work without any of this, but here's what you need to keep in mind for statically hosting it. +### I will assume the following +* `404.html` will be used for 404 responses +* `index.html` will be used when in that directory +* stuff like `/app` will just use the html file at `/app.html` + +Here's the other thing you need to do: + +Firstly you'll need to build Fermi like normal and host out the `./dist/webpage` directory. + +You need to make some rewrites, not redirects from these addresses: +* `/channels/*` -> `/app.html` +* `/invite/*` -> `invite.html` +* `/template/*` -> `template.html` + +Other than these three rewrites, everything else should work as expected! + +*(the reason why the service worker can fix this is due to it doing the rewrites on the client side)* diff --git a/package-lock.json b/package-lock.json index 025864cd..93a380fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5377 +1,575 @@ { - "name": "jankclient", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "jankclient", - "version": "0.1.0", - "license": "GPL-3.0", - "dependencies": { - "@html-eslint/parser": "^0.27.0", - "compression": "^1.7.4", - "express": "^4.19.2", - "node-fetch": "^3.3.2", - "ts-to-jsdoc": "^2.2.0" - }, - "devDependencies": { - "@eslint/js": "^9.10.0", - "@html-eslint/eslint-plugin": "^0.25.0", - "@types/compression": "^1.7.5", - "@types/eslint__js": "^8.42.3", - "@types/express": "^4.17.21", - "@types/node-fetch": "^2.6.11", - "eslint": "^8.57.1", - "gulp": "^5.0.0", - "gulp-copy": "^5.0.0", - "gulp-typescript": "^6.0.0-alpha.1", - "typescript": "^5.6.2", - "typescript-eslint": "^7.18.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@eslint/js": { - "version": "9.10.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz", - "integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@gulpjs/messages": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@gulpjs/messages/-/messages-1.1.0.tgz", - "integrity": "sha512-Ys9sazDatyTgZVb4xPlDufLweJ/Os2uHWOv+Caxvy2O85JcnT4M3vc73bi8pdLWlv3fdWQz3pdI9tVwo8rQQSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@gulpjs/to-absolute-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", - "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-negated-glob": "^1.0.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@html-eslint/eslint-plugin": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@html-eslint/eslint-plugin/-/eslint-plugin-0.25.0.tgz", - "integrity": "sha512-5DlvqO8bbe90cKSfFDuEblyrEnhAdgNTjWxXeUxt/XXC2OuMC8CsxzLZjtK3+0X6yM8m4xcE3fymCPwg7zdcXQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@html-eslint/parser": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@html-eslint/parser/-/parser-0.27.0.tgz", - "integrity": "sha512-F/A1M0jnDAYoRvJiiSC7pIBD9DAsf4EhbndbvEi81aozD/wI8WWXON50xZPUaGHCI1C+2syTVifxDz8MvDKaQA==", - "license": "MIT", - "dependencies": { - "es-html-parser": "^0.0.9" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@ts-morph/common": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.22.0.tgz", - "integrity": "sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==", - "license": "MIT", - "dependencies": { - "fast-glob": "^3.3.2", - "minimatch": "^9.0.3", - "mkdirp": "^3.0.1", - "path-browserify": "^1.0.1" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/compression": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz", - "integrity": "sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/eslint": { - "version": "8.56.11", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.11.tgz", - "integrity": "sha512-sVBpJMf7UPo/wGecYOpk2aQya2VUGeHhe38WG7/mN5FufNSubf5VT9Uh9Uyp8/eLJpu1/tuhJ/qTo4mhSB4V4Q==", - "dev": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint__js": { - "version": "8.42.3", - "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz", - "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true - }, - "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.5", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", - "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.14.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz", - "integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" - } - }, - "node_modules/@types/qs": { - "version": "6.9.16", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", - "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", - "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/type-utils": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", - "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", - "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", - "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/types": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", - "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", - "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", - "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", - "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", - "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ansi-wrap": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/append-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", - "integrity": "sha512-WLbYiXzD3y/ATLZFufV/rZvWdZOs+Z/+5v1rBZ463Jn398pa6kcde27cvozYnBoxXblGZTFfoPpsaEw0orU5BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-equal": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/array-slice": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/async-done": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/async-done/-/async-done-2.0.0.tgz", - "integrity": "sha512-j0s3bzYq9yKIVLKGE/tWlCpa3PfFLcrDZLTSVdnnCTGagXuXBJO4SsY9Xdk/fQBirCkH4evW5xOeJXqlAQFdsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.4.4", - "once": "^1.4.0", - "stream-exhaust": "^1.0.2" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/async-settle": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-2.0.0.tgz", - "integrity": "sha512-Obu/KE8FurfQRN6ODdHN9LuXqwC+JFIM9NRyZqJJ4ZfLJmIYN9Rg0/kb+wF70VV5+fJusTMQlJ1t5rF7J/ETdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-done": "^2.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/b4a": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", - "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/bach": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/bach/-/bach-2.0.1.tgz", - "integrity": "sha512-A7bvGMGiTOxGMpNupYl9HQTf0FFDNF4VCmks4PJpFyN1AX2pdKuxuwdvUz2Hu388wcgp+OvGFNsumBfFNkR7eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-done": "^2.0.0", - "async-settle": "^2.0.0", - "now-and-later": "^3.0.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/bare-events": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", - "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", - "dev": true, - "license": "Apache-2.0", - "optional": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bl": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", - "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^6.0.3", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/buffer-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", - "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", - "integrity": "sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/cloneable-readable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", - "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "process-nextick-args": "^2.0.0", - "readable-stream": "^2.3.5" - } - }, - "node_modules/code-block-writer": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", - "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", - "license": "MIT" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "license": "MIT", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/copy-props": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-4.0.0.tgz", - "integrity": "sha512-bVWtw1wQLzzKiYROtvNlbJgxgBYt2bMJpkCbKmXM3xyijvcjjWXEk5nyrrT3bgJ7ODb19ZohE2T0Y3FgNPyoTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "each-props": "^3.0.0", - "is-plain-object": "^5.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "node_modules/each-props": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/each-props/-/each-props-3.0.0.tgz", - "integrity": "sha512-IYf1hpuWrdzse/s/YJOrFmU15lyhSzxelNVAHTEG3DtP4QsLTWZUzcUL3HMXmKQxXpa4EIrBPpwRgj0aehdvAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^5.0.0", - "object.defaults": "^1.1.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-html-parser": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/es-html-parser/-/es-html-parser-0.0.9.tgz", - "integrity": "sha512-oniQMi+466VFsDzcdron9Ry/sqUJpDJg1bbDn0jFJKDdxXhwIOYDr4DgBnO5/yPLGj2xv+n5yy4L1Q0vAC5TYQ==" - }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "homedir-polyfill": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true, - "license": "MIT" - }, - "node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/findup-sync": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", - "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.3", - "micromatch": "^4.0.4", - "resolve-dir": "^1.0.1" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/fined": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-2.0.0.tgz", - "integrity": "sha512-OFRzsL6ZMHz5s0JrsEr+TpdGNCtrVtnuG3x1yzGNiQHT0yaDnXAj8V/lWcpJVrnoDpcwXcASxAZYbuXda2Y82A==", - "dev": true, - "license": "MIT", - "dependencies": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^5.0.0", - "object.defaults": "^1.1.0", - "object.pick": "^1.3.0", - "parse-filepath": "^1.0.2" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/flagged-respawn": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-2.0.0.tgz", - "integrity": "sha512-Gq/a6YCi8zexmGHMuJwahTGzXlAZAOsbCVKduWXC6TlLCjjFRlExMJc4GC2NYPYZ0r/brw9P7CpRgQmlPVeOoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true - }, - "node_modules/flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - } - }, - "node_modules/for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", - "dev": true, - "license": "MIT", - "dependencies": { - "for-in": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-mkdirp-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", - "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.8", - "streamx": "^2.12.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-stream": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.2.tgz", - "integrity": "sha512-R8z6eTB55t3QeZMmU1C+Gv+t5UnNRkA55c5yo67fAVfxODxieTwsjNG7utxS/73NdP1NbDgCrhVEg2h00y4fFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@gulpjs/to-absolute-glob": "^4.0.0", - "anymatch": "^3.1.3", - "fastq": "^1.13.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "is-negated-glob": "^1.0.0", - "normalize-path": "^3.0.0", - "streamx": "^2.12.5" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-watcher": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-6.0.0.tgz", - "integrity": "sha512-wGM28Ehmcnk2NqRORXFOTOR064L4imSw3EeOqU5bIwUf62eXGwg89WivH6VMahL8zlQHeodzvHpXplrqzrz3Nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-done": "^2.0.0", - "chokidar": "^3.5.3" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "dev": true, - "license": "MIT", - "dependencies": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glogg": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/glogg/-/glogg-2.2.0.tgz", - "integrity": "sha512-eWv1ds/zAlz+M1ioHsyKJomfY7jbDDPpwSkv14KQj89bycx1nvK5/2Cj/T9g7kzJcX5Bc7Yv22FjfBZS/jl94A==", - "dev": true, - "license": "MIT", - "dependencies": { - "sparkles": "^2.1.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/gulp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/gulp/-/gulp-5.0.0.tgz", - "integrity": "sha512-S8Z8066SSileaYw1S2N1I64IUc/myI2bqe2ihOBzO6+nKpvNSg7ZcWJt/AwF8LC/NVN+/QZ560Cb/5OPsyhkhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob-watcher": "^6.0.0", - "gulp-cli": "^3.0.0", - "undertaker": "^2.0.0", - "vinyl-fs": "^4.0.0" - }, - "bin": { - "gulp": "bin/gulp.js" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/gulp-cli": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-3.0.0.tgz", - "integrity": "sha512-RtMIitkT8DEMZZygHK2vEuLPqLPAFB4sntSxg4NoDta7ciwGZ18l7JuhCTiS5deOJi2IoK0btE+hs6R4sfj7AA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@gulpjs/messages": "^1.1.0", - "chalk": "^4.1.2", - "copy-props": "^4.0.0", - "gulplog": "^2.2.0", - "interpret": "^3.1.1", - "liftoff": "^5.0.0", - "mute-stdout": "^2.0.0", - "replace-homedir": "^2.0.0", - "semver-greatest-satisfied-range": "^2.0.0", - "string-width": "^4.2.3", - "v8flags": "^4.0.0", - "yargs": "^16.2.0" - }, - "bin": { - "gulp": "bin/gulp.js" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/gulp-copy": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/gulp-copy/-/gulp-copy-5.0.0.tgz", - "integrity": "sha512-XgTPwevIxr5bPITtrq24euacqASBMOy2R30IEUXX1mLsbd/GoOo0AM3T9qjF9L2Yp+muRr/spdRskVUjUCDqkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "plugin-error": "^2.0.1", - "through2": "^2.0.3" - }, - "peerDependencies": { - "gulp": "^4.0.1 || ^5.0.0" - } - }, - "node_modules/gulp-typescript": { - "version": "6.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-6.0.0-alpha.1.tgz", - "integrity": "sha512-KoT0TTfjfT7w3JItHkgFH1T/zK4oXWC+a8xxKfniRfVcA0Fa1bKrIhztYelYmb+95RB80OLMBreknYkdwzdi2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.1", - "plugin-error": "^1.0.1", - "source-map": "^0.7.3", - "through2": "^3.0.1", - "vinyl": "^2.2.0", - "vinyl-fs": "^3.0.3" - }, - "engines": { - "node": ">= 8" - }, - "peerDependencies": { - "typescript": "~2.7.1 || >=2.8.0-dev || >=2.9.0-dev || ~3.0.0 || >=3.0.0-dev || >=3.1.0-dev || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.7.0-dev " - } - }, - "node_modules/gulp-typescript/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true, - "license": "MIT" - }, - "node_modules/gulp-typescript/node_modules/fs-mkdirp-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", - "integrity": "sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.11", - "through2": "^2.0.3" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-typescript/node_modules/fs-mkdirp-stream/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/gulp-typescript/node_modules/glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "node_modules/gulp-typescript/node_modules/glob-stream": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", - "integrity": "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "extend": "^3.0.0", - "glob": "^7.1.1", - "glob-parent": "^3.1.0", - "is-negated-glob": "^1.0.0", - "ordered-read-streams": "^1.0.0", - "pumpify": "^1.3.5", - "readable-stream": "^2.1.5", - "remove-trailing-separator": "^1.0.1", - "to-absolute-glob": "^2.0.0", - "unique-stream": "^2.0.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-typescript/node_modules/is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gulp-typescript/node_modules/lead": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", - "integrity": "sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow==", - "dev": true, - "license": "MIT", - "dependencies": { - "flush-write-stream": "^1.0.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-typescript/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gulp-typescript/node_modules/now-and-later": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", - "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.3.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-typescript/node_modules/plugin-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", - "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^1.0.1", - "arr-diff": "^4.0.0", - "arr-union": "^3.1.0", - "extend-shallow": "^3.0.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-typescript/node_modules/plugin-error/node_modules/ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-wrap": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gulp-typescript/node_modules/resolve-options": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", - "integrity": "sha512-NYDgziiroVeDC29xq7bp/CacZERYsA9bXYd1ZmcJlF3BcrZv5pTb4NG7SjdyKDnXZ84aC4vo2u6sNKIA1LCu/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "value-or-function": "^3.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-typescript/node_modules/through2": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", - "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "2 || 3" - } - }, - "node_modules/gulp-typescript/node_modules/to-through": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", - "integrity": "sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "through2": "^2.0.3" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-typescript/node_modules/to-through/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/gulp-typescript/node_modules/value-or-function": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", - "integrity": "sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-typescript/node_modules/vinyl-fs": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", - "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", - "dev": true, - "license": "MIT", - "dependencies": { - "fs-mkdirp-stream": "^1.0.0", - "glob-stream": "^6.1.0", - "graceful-fs": "^4.0.0", - "is-valid-glob": "^1.0.0", - "lazystream": "^1.0.0", - "lead": "^1.0.0", - "object.assign": "^4.0.4", - "pumpify": "^1.3.5", - "readable-stream": "^2.3.3", - "remove-bom-buffer": "^3.0.0", - "remove-bom-stream": "^1.2.0", - "resolve-options": "^1.1.0", - "through2": "^2.0.0", - "to-through": "^2.0.0", - "value-or-function": "^3.0.0", - "vinyl": "^2.0.0", - "vinyl-sourcemap": "^1.1.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-typescript/node_modules/vinyl-fs/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/gulp-typescript/node_modules/vinyl-sourcemap": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", - "integrity": "sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "append-buffer": "^1.0.2", - "convert-source-map": "^1.5.0", - "graceful-fs": "^4.1.6", - "normalize-path": "^2.1.1", - "now-and-later": "^2.0.0", - "remove-bom-buffer": "^3.0.0", - "vinyl": "^2.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulplog": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-2.2.0.tgz", - "integrity": "sha512-V2FaKiOhpR3DRXZuYdRLn/qiY0yI5XmqbTKrYbdemJ+xOh2d2MOweI/XFgMzd/9+1twdvMwllnZbWZNJ+BOm4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "glogg": "^2.2.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse-passwd": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC" - }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-core-module": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", - "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", - "dev": true, - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extendable/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-negated-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", - "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-unc-path": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "unc-path-regex": "^0.1.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-valid-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", - "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/last-run": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/last-run/-/last-run-2.0.0.tgz", - "integrity": "sha512-j+y6WhTLN4Itnf9j5ZQos1BGPCS8DAwmgMroR3OzfxAsBxam0hMw7J8M3KqZl0pLQJ1jNnwIexg5DYpC/ctwEQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lead": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", - "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/liftoff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-5.0.0.tgz", - "integrity": "sha512-a5BQjbCHnB+cy+gsro8lXJ4kZluzOijzJ1UVVfyJYZC+IP2pLv1h4+aysQeKuTmyO8NAqfyQAk4HWaP/HjcKTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "extend": "^3.0.2", - "findup-sync": "^5.0.0", - "fined": "^2.0.0", - "flagged-respawn": "^2.0.0", - "is-plain-object": "^5.0.0", - "rechoir": "^0.8.0", - "resolve": "^1.20.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/mute-stdout": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-2.0.0.tgz", - "integrity": "sha512-32GSKM3Wyc8dg/p39lWPKYu8zci9mJFzV1Np9Of0ZEpe6Fhssn/FbI7ywAMd40uX+p3ZKh3T5EeCFv81qS3HmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/now-and-later": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", - "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.defaults": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-each": "^1.0.1", - "array-slice": "^1.0.0", - "for-own": "^1.0.0", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ordered-read-streams": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", - "integrity": "sha512-Z87aSjx3r5c0ZB7bcJqIgIRX5bxR7A4aSzvIbaxd0oTkWBCOoKfuGHiKj60CHVUgg1Phm5yMZzBdt8XqRs73Mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "^2.0.1" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-filepath": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "license": "MIT" - }, - "node_modules/path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-root-regex": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/plugin-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-2.0.1.tgz", - "integrity": "sha512-zMakqvIDyY40xHOvzXka0kUvf40nYIuwRE8dWhti2WtjQZ31xAgBZBhxsK7vK3QbRXS1Xms/LO7B5cuAsfB2Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^1.0.1" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/plugin-error/node_modules/ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-wrap": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", - "dev": true, - "license": "MIT" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/remove-bom-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", - "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5", - "is-utf8": "^0.2.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remove-bom-stream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", - "integrity": "sha512-wigO8/O08XHb8YPzpDDT+QmRANfW6vLqxfaXm1YXhnFf3AkSLyjfG3GEFg4McZkmgL7KvCj5u2KczkvSP6NfHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "remove-bom-buffer": "^3.0.0", - "safe-buffer": "^5.1.0", - "through2": "^2.0.3" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", - "dev": true, - "license": "ISC" - }, - "node_modules/replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/replace-homedir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-2.0.0.tgz", - "integrity": "sha512-bgEuQQ/BHW0XkkJtawzrfzHFSN70f/3cNOiHa2QsYxqrjaC30X1k74FJ6xswVBP0sr0SpGIdVFuPwfrYziVeyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-options": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", - "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "value-or-function": "^4.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-greatest-satisfied-range": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-2.0.0.tgz", - "integrity": "sha512-lH3f6kMbwyANB7HuOWRMlLCa2itaCrZJ+SAqqkSZrZKO/cAsk2EOyaKHUtNkVLFyFW9pct22SFesFp3Z7zpA0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "sver": "^1.8.3" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 8" - } - }, - "node_modules/sparkles": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-2.1.0.tgz", - "integrity": "sha512-r7iW1bDw8R/cFifrD3JnQJX0K1jqT0kprL48BiBpLZLJPmAm34zsVBsK5lc7HirZYZqMW65dOXZgbAGt/I6frg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stream-composer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", - "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "streamx": "^2.13.2" - } - }, - "node_modules/stream-exhaust": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", - "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", - "dev": true, - "license": "MIT" - }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/streamx": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz", - "integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-fifo": "^1.3.2", - "queue-tick": "^1.0.1", - "text-decoder": "^1.1.0" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sver": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/sver/-/sver-1.8.4.tgz", - "integrity": "sha512-71o1zfzyawLfIWBOmw8brleKyvnbn73oVHNCsu51uPMz/HWiKkkXsI31JjHW5zqXEqnPYkIiHd8ZmL7FCimLEA==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "semver": "^6.3.0" - } - }, - "node_modules/sver/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.0.tgz", - "integrity": "sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/through2-filter": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", - "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", - "dev": true, - "license": "MIT", - "dependencies": { - "through2": "~2.0.0", - "xtend": "~4.0.0" - } - }, - "node_modules/to-absolute-glob": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", - "integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-absolute": "^1.0.0", - "is-negated-glob": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/to-through": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", - "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "streamx": "^2.12.5" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", - "dev": true, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/ts-morph": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-21.0.1.tgz", - "integrity": "sha512-dbDtVdEAncKctzrVZ+Nr7kHpHkv+0JDJb2MjjpBaj8bFeCkePU9rHfMklmhuLFnpeq/EJZk2IhStY6NzqgjOkg==", - "license": "MIT", - "dependencies": { - "@ts-morph/common": "~0.22.0", - "code-block-writer": "^12.0.0" - } - }, - "node_modules/ts-to-jsdoc": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ts-to-jsdoc/-/ts-to-jsdoc-2.2.0.tgz", - "integrity": "sha512-5miE85Iy8Hwo3KU4QpoXxSYbTyA7cUitgUAMZF6cQgvOzRmonNFWbxiYE5JcREqV5uvb0DGT/2BTwemlgyV3UQ==", - "license": "MIT", - "dependencies": { - "arg": "^5.0.1", - "ts-morph": "^21.0.1" - }, - "bin": { - "ts-to-jsdoc": "bin/ts-to-jsdoc" - } - }, - "node_modules/ts-to-jsdoc/node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "license": "MIT" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", - "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.18.0.tgz", - "integrity": "sha512-PonBkP603E3tt05lDkbOMyaxJjvKqQrXsnow72sVeOFINDE/qNmnnd+f9b4N+U7W6MXnnYyrhtmF2t08QWwUbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "7.18.0", - "@typescript-eslint/parser": "7.18.0", - "@typescript-eslint/utils": "7.18.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/undertaker": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-2.0.0.tgz", - "integrity": "sha512-tO/bf30wBbTsJ7go80j0RzA2rcwX6o7XPBpeFcb+jzoeb4pfMM2zUeSDIkY1AWqeZabWxaQZ/h8N9t35QKDLPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bach": "^2.0.1", - "fast-levenshtein": "^3.0.0", - "last-run": "^2.0.0", - "undertaker-registry": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/undertaker-registry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-2.0.0.tgz", - "integrity": "sha512-+hhVICbnp+rlzZMgxXenpvTxpuvA67Bfgtt+O9WOE5jo7w/dyiF1VmoZVIHvP2EkUjsyKyTwYKlLhA+j47m1Ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/undertaker/node_modules/fast-levenshtein": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", - "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fastest-levenshtein": "^1.0.7" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, - "license": "MIT" - }, - "node_modules/unique-stream": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", - "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-stable-stringify-without-jsonify": "^1.0.1", - "through2-filter": "^3.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.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==", - "dev": true, - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/v8flags": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", - "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/value-or-function": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", - "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vinyl-contents": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", - "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^5.0.0", - "vinyl": "^3.0.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/vinyl-contents/node_modules/replace-ext": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", - "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/vinyl-contents/node_modules/vinyl": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", - "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^2.1.2", - "clone-stats": "^1.0.0", - "remove-trailing-separator": "^1.1.0", - "replace-ext": "^2.0.0", - "teex": "^1.0.1" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/vinyl-fs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", - "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fs-mkdirp-stream": "^2.0.1", - "glob-stream": "^8.0.0", - "graceful-fs": "^4.2.11", - "iconv-lite": "^0.6.3", - "is-valid-glob": "^1.0.0", - "lead": "^4.0.0", - "normalize-path": "3.0.0", - "resolve-options": "^2.0.0", - "stream-composer": "^1.0.2", - "streamx": "^2.14.0", - "to-through": "^3.0.0", - "value-or-function": "^4.0.0", - "vinyl": "^3.0.0", - "vinyl-sourcemap": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/vinyl-fs/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vinyl-fs/node_modules/replace-ext": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", - "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/vinyl-fs/node_modules/vinyl": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", - "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^2.1.2", - "clone-stats": "^1.0.0", - "remove-trailing-separator": "^1.1.0", - "replace-ext": "^2.0.0", - "teex": "^1.0.1" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/vinyl-sourcemap": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", - "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "convert-source-map": "^2.0.0", - "graceful-fs": "^4.2.10", - "now-and-later": "^3.0.0", - "streamx": "^2.12.5", - "vinyl": "^3.0.0", - "vinyl-contents": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/vinyl-sourcemap/node_modules/replace-ext": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", - "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/vinyl-sourcemap/node_modules/vinyl": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", - "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^2.1.2", - "clone-stats": "^1.0.0", - "remove-trailing-separator": "^1.1.0", - "replace-ext": "^2.0.0", - "teex": "^1.0.1" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } + "name": "jankclient", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "jankclient", + "version": "0.2.0", + "license": "GPL-3.0", + "dependencies": { + "@swc/core": "1.11.24" + }, + "devDependencies": { + "@types/audioworklet": "^0.0.90", + "@types/node": "^25.5.0", + "@types/node-fetch": "^2.6.13", + "prettier": "^3.7.4", + "typescript": "^5.9.3" + } + }, + "node_modules/@swc/core": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.24.tgz", + "integrity": "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.21" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.11.24", + "@swc/core-darwin-x64": "1.11.24", + "@swc/core-linux-arm-gnueabihf": "1.11.24", + "@swc/core-linux-arm64-gnu": "1.11.24", + "@swc/core-linux-arm64-musl": "1.11.24", + "@swc/core-linux-x64-gnu": "1.11.24", + "@swc/core-linux-x64-musl": "1.11.24", + "@swc/core-win32-arm64-msvc": "1.11.24", + "@swc/core-win32-ia32-msvc": "1.11.24", + "@swc/core-win32-x64-msvc": "1.11.24" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.24.tgz", + "integrity": "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.24.tgz", + "integrity": "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.24.tgz", + "integrity": "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.24.tgz", + "integrity": "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.24.tgz", + "integrity": "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.24.tgz", + "integrity": "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.24.tgz", + "integrity": "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.24.tgz", + "integrity": "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.24.tgz", + "integrity": "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.24.tgz", + "integrity": "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz", + "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@types/audioworklet": { + "version": "0.0.90", + "resolved": "https://registry.npmjs.org/@types/audioworklet/-/audioworklet-0.0.90.tgz", + "integrity": "sha512-fG1NU0kxzr/vha7zavWL/qR/kzUTtvmOqQ6vGo0/iKf0ZvezDLPgT+bP6x6VwDqeDeBc8HQpzkKDyFGAc8EJeg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + } + } } diff --git a/package.json b/package.json index 1b889083..b69ec55e 100644 --- a/package.json +++ b/package.json @@ -1,48 +1,30 @@ { - "name": "jankclient", - "version": "0.2.0", - "description": "A SpaceBar Client written in TS HTML and CSS to run, clone the repo and do either `npm start` or `bun start` both bun and node are supported, and both should function as expected. To access Jank Client after init simply go to http://localhost:8080/login and login with your username and password.", - "main": ".dist/index.js", - "type": "module", - "scripts": { - "lint": "eslint .", - "build": "npx gulp", - "start": "node dist/index.js" - }, - "author": "MathMan05", - "license": "GPL-3.0", - "dependencies": { - "compression": "^1.7.4", - "express": "^4.19.2", - "gulp-sourcemaps": "^3.0.0", - "gulp-swc": "^2.2.0", - "rimraf": "^6.0.1" - }, - "devDependencies": { - "@html-eslint/parser": "^0.27.0", - "eslint-plugin-html": "^8.1.1", - "@stylistic/eslint-plugin-js": "^2.8.0", - "@typescript-eslint/eslint-plugin": "^8.14.0", - "@typescript-eslint/parser": "^8.14.0", - "@rsbuild/core": "^1.1.4", - "@rsbuild/plugin-node-polyfill": "^1.2.0", - "@swc/core": "^1.7.26", - "swc": "^1.0.11", - "@eslint/js": "^9.10.0", - "@html-eslint/eslint-plugin": "^0.25.0", - "@stylistic/eslint-plugin": "^2.3.0", - "@types/compression": "^1.7.5", - "@types/eslint__js": "^8.42.3", - "@types/express": "^4.17.21", - "@types/node-fetch": "^2.6.11", - "eslint": "^8.57.1", - "eslint-plugin-sonarjs": "^1.0.4", - "eslint-plugin-unicorn": "^55.0.0", - "gulp": "^5.0.0", - "gulp-copy": "^5.0.0", - "gulp-plumber": "^1.2.1", - "gulp-typescript": "^6.0.0-alpha.1", - "typescript": "^5.6.2", - "typescript-eslint": "^8.14.0" - } + "name": "jankclient", + "version": "0.2.0", + "description": "A Spacebar Client written in TS, HTML and CSS. to run, clone the repo and do either `npm start` or `bun start` both bun and node are supported, and both should function as expected. To access Fermi Client after init simply go to http://localhost:8080/login and login with your username and password.", + "main": ".dist/index.js", + "type": "module", + "scripts": { + "bunBuild": "bun build.ts", + "build": "node buildnode.js && node build.js", + "start": "node dist/index.js" + }, + "author": "MathMan05", + "license": "GPL-3.0", + "dependencies": { + "@swc/core": "1.11.24" + }, + "devDependencies": { + "@types/audioworklet": "^0.0.90", + "@types/node": "^25.5.0", + "@types/node-fetch": "^2.6.13", + "prettier": "^3.7.4", + "typescript": "^5.9.3" + }, + "prettier": { + "useTabs": true, + "printWidth": 100, + "semi": true, + "bracketSpacing": false + } } diff --git a/src/index.ts b/src/index.ts index 1b6040f8..ea7a2d6b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,176 +1,205 @@ -#!/usr/bin/env node - -import compression from"compression"; -import express, { Request, Response }from"express"; -import fs from"node:fs/promises"; -import path from"node:path"; -import{ observe, uptime }from"./stats.js"; -import{ getApiUrls, inviteResponse }from"./utils.js"; -import{ fileURLToPath }from"node:url"; +import http from "http"; +import fs from "node:fs/promises"; +import path from "node:path"; +import {observe} from "./stats.js"; +import {getApiUrls} from "./utils.js"; +import {fileURLToPath} from "node:url"; import {readFileSync} from "fs"; -import process from"node:process"; +import process from "node:process"; const devmode = (process.env.NODE_ENV || "development") === "development"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +type dirtype = Map; +async function getDirectories(path: string): Promise { + return new Map( + await Promise.all( + (await fs.readdir(path)).map(async function (file): Promise<[string, string | dirtype]> { + if ((await fs.stat(path + "/" + file)).isDirectory()) { + return [file, await getDirectories(path + "/" + file)]; + } else { + return [file, file]; + } + }), + ), + ); +} +let dirs: dirtype | undefined = undefined; +async function combinePath(path: string, tryAgain = true, reqpath: string): Promise { + if (!dirs) { + dirs = await getDirectories(__dirname); + } + const pathDir = path + .split("/") + .reverse() + .filter((_) => _ !== ""); + function find(arr: string[], search: dirtype | string | undefined): boolean { + if (search == undefined) return false; + if (arr.length === 0) { + return typeof search == "string"; + } + if (typeof search == "string") { + return false; + } + const thing = arr.pop() as string; + return find(arr, search.get(thing)); + } + if (find(pathDir, dirs)) { + return __dirname + path; + } else if (reqpath.startsWith("/channels")) { + return __dirname + "/webpage/app.html"; + } else { + if (!path.includes(".")) { + const str = await combinePath(path + ".html", false, reqpath); + if (str !== __dirname + "/webpage/404.html") { + return str; + } + } + if (devmode && tryAgain) { + dirs = await getDirectories(__dirname); + return combinePath(path, false, reqpath); + } + return __dirname + "/webpage/404.html"; + } +} interface Instance { - name: string; - [key: string]: any; + name: string; + [key: string]: any; +} +function guessMime(str: string) { + const ext = str.split(".").at(-1); + switch (ext) { + case "js": + case "cjs": + return "text/javascript"; + case "html": + return "text/html"; + case "css": + return "text/css"; + case "svg": + return "image/svg+xml"; + case "ico": + return "image/x-icon"; + case "png": + case "jpeg": + case "webp": + return "image/" + ext; + default: + return "text/plain"; + } } +const app = http.createServer(async (req, res) => { + const url = new URL(req.url as string, "http://localhost"); + const pathstr = url.pathname; + + async function sendFile(file: string) { + try { + const f = await fs.readFile(file); + res.writeHead(200, {"Content-Type": guessMime(file)}); + res.write(f); + res.end(); + } catch { + res.writeHead(404, {"Content-Type": "text/html"}); + res.write("Uh, this ain't supposed to happen"); + res.end(); + } + } + if (pathstr === "/") { + sendFile(path.join(__dirname, "webpage", "index.html")); + return; + } -const app = express(); + if (pathstr.startsWith("/instances.json")) { + res.writeHead(200, {"Content-Type": "text/plain"}); + res.write(JSON.stringify(instances)); + res.end(); + return; + } -type instace={ - name:string, - description?:string, - descriptionLong?:string, - image?:string, - url?:string, - language:string, - country:string, - display:boolean, - urls?:{ - wellknown:string, - api:string, - cdn:string, - gateway:string, - login?:string - }, - contactInfo?:{ - discord?:string, - github?:string, - email?:string, - spacebar?:string, - matrix?:string, - mastodon?:string - } -} -const instances=JSON.parse(readFileSync(process.env.JANK_INSTANCES_PATH||(__dirname+"/webpage/instances.json")).toString()) as instace[]; + if (pathstr.startsWith("/invite/")) { + sendFile(path.join(__dirname, "webpage", "invite.html")); + return; + } + if (pathstr.startsWith("/template/")) { + sendFile(path.join(__dirname, "webpage", "template.html")); + return; + } + const filePath = await combinePath("/webpage/" + pathstr, true, pathstr); + sendFile(filePath); +}); + +export type instance = { + name: string; + description?: string; + descriptionLong?: string; + image?: string; + url?: string; + language: string; + country: string; + display: boolean; + urls?: { + wellknown: string; + api: string; + cdn: string; + gateway: string; + login?: string; + }; + contactInfo?: { + discord?: string; + github?: string; + email?: string; + spacebar?: string; + matrix?: string; + mastodon?: string; + }; +}; +const instances = JSON.parse( + readFileSync(process.env.JANK_INSTANCES_PATH || __dirname + "/webpage/instances.json").toString(), +) as instance[]; const instanceNames = new Map(); -for(const instance of instances){ +for (const instance of instances) { instanceNames.set(instance.name, instance); } -app.use(compression()); - -async function updateInstances(): Promise{ - try{ - const response = await fetch("https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/instances/instances.json"); +async function updateInstances(): Promise { + try { + const response = await fetch( + "https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/instances/instances.json", + ); const json = (await response.json()) as Instance[]; - for(const instance of json){ - if(instanceNames.has(instance.name)){ + for (const instance of json) { + if (instanceNames.has(instance.name)) { const existingInstance = instanceNames.get(instance.name); - if(existingInstance){ - for(const key of Object.keys(instance)){ - if(!existingInstance[key]){ + if (existingInstance) { + for (const key of Object.keys(instance)) { + if (!(key in existingInstance)) { existingInstance[key] = instance[key]; } } } - }else{ + } else { instances.push(instance as any); } } observe(instances); - }catch(error){ + } catch (error) { console.error("Error updating instances:", error); } } updateInstances(); - -app.use("/getupdates", async (_req: Request, res: Response)=>{ - try{ - const stats = await fs.stat(path.join(__dirname, "webpage")); - res.send(stats.mtimeMs.toString()); - }catch(error){ - console.error("Error getting updates:", error); - res.status(500).send("Error getting updates"); - } -}); - -app.use("/services/oembed", (req: Request, res: Response)=>{ - inviteResponse(req, res); -}); - -app.use("/uptime", (req: Request, res: Response)=>{ - const instanceUptime = uptime.get(req.query.name as string); - res.send(instanceUptime); -}); - -app.use("/", async (req: Request, res: Response)=>{ - const scheme = req.secure ? "https" : "http"; - const host = `${scheme}://${req.get("Host")}`; - const ref = host + req.originalUrl; - - if(host && ref){ - const link = `${host}/services/oembed?url=${encodeURIComponent(ref)}`; - res.set( - "Link", - `<${link}>; rel="alternate"; type="application/json+oembed"; title="Jank Client oEmbed format"` - ); - } - - if(req.path === "/"){ - res.sendFile(path.join(__dirname, "webpage", "home.html")); - return; - } - - if(req.path.startsWith("/instances.json")){ - res.json(instances); - return; - } - - if(req.path.startsWith("/invite/")){ - res.sendFile(path.join(__dirname, "webpage", "invite.html")); - return; - } - const filePath = path.join(__dirname, "webpage", req.path); - try{ - await fs.access(filePath); - if(devmode){ - const filePath2 = path.join(__dirname, "../src/webpage", req.path); - try{ - await fs.access(filePath2); - res.sendFile(filePath2); - return; - }catch{} - } - res.sendFile(filePath); - }catch{ - try{ - await fs.access(`${filePath}.html`); - if(devmode){ - const filePath2 = path.join(__dirname, "../src/webpage", req.path); - try{ - await fs.access(filePath2 + ".html"); - res.sendFile(filePath2 + ".html"); - return; - }catch{} - } - res.sendFile(`${filePath}.html`); - }catch{ - if(req.path.startsWith("/src/webpage")){ - const filePath2 = path.join(__dirname, "..", req.path); - try{ - await fs.access(filePath2); - res.sendFile(filePath2); - return; - }catch{} - } - res.sendFile(path.join(__dirname, "webpage", "index.html")); - } - } +/* +app.set("trust proxy", (ip: unknown) => { + if (typeof ip !== "string") return false; + return ip.startsWith("127."); }); - -app.set('trust proxy', (ip:string) => ip.startsWith("127.")); - +*/ const PORT = process.env.PORT || Number(process.argv[2]) || 8080; -app.listen(PORT, ()=>{ +app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); }); -export{ getApiUrls }; +export {getApiUrls}; diff --git a/src/stats.ts b/src/stats.ts index 1356d30d..5634e2be 100644 --- a/src/stats.ts +++ b/src/stats.ts @@ -1,39 +1,38 @@ -import fs from"node:fs"; -import path from"node:path"; -import{ getApiUrls }from"./utils.js"; -import{ fileURLToPath }from"node:url"; -import{ setTimeout, clearTimeout }from"node:timers"; +import fs from "node:fs"; +import path from "node:path"; +import {getApiUrls} from "./utils.js"; +import {fileURLToPath} from "node:url"; +import {setTimeout, clearTimeout} from "node:timers"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); interface UptimeEntry { - time: number; - online: boolean; + time: number; + online: boolean; } interface Instance { - name: string; - urls?: { api: string }; - url?: string; - online?: boolean; - uptime?: { - daytime: number; - weektime: number; - alltime: number; - }; + name: string; + urls?: {api: string}; + url?: string; + online?: boolean; + uptime?: { + daytime: number; + weektime: number; + alltime: number; + }; } const uptimeObject: Map = loadUptimeObject(); -export{ uptimeObject as uptime }; -function loadUptimeObject(): Map{ - const filePath = process.env.JANK_UPTIME_JSON_PATH||path.join(__dirname, "..", "uptime.json"); - if(fs.existsSync(filePath)){ - try{ +function loadUptimeObject(): Map { + const filePath = process.env.JANK_UPTIME_JSON_PATH || path.join(__dirname, "..", "uptime.json"); + if (fs.existsSync(filePath)) { + try { const data = JSON.parse(fs.readFileSync(filePath, "utf8")); return new Map(Object.entries(data)); - }catch(error){ + } catch (error) { console.error("Error reading uptime.json:", error); return new Map(); } @@ -43,26 +42,26 @@ function loadUptimeObject(): Map{ let saveTimeout: ReturnType | null = null; -function saveUptimeObject(): void{ - if(saveTimeout){ +function saveUptimeObject(): void { + if (saveTimeout) { clearTimeout(saveTimeout); } - saveTimeout = setTimeout(()=>{ + saveTimeout = setTimeout(() => { const data = Object.fromEntries(uptimeObject); fs.writeFile( - process.env.JANK_UPTIME_JSON_PATH||path.join(__dirname, "..", "uptime.json"), + process.env.JANK_UPTIME_JSON_PATH || path.join(__dirname, "..", "uptime.json"), JSON.stringify(data), - error=>{ - if(error){ + (error) => { + if (error) { console.error("Error saving uptime.json:", error); } - } + }, ); }, 5000); // Batch updates every 5 seconds } -function removeUndefinedKey(): void{ - if(uptimeObject.has("undefined")){ +function removeUndefinedKey(): void { + if (uptimeObject.has("undefined")) { uptimeObject.delete("undefined"); saveUptimeObject(); } @@ -70,101 +69,89 @@ function removeUndefinedKey(): void{ removeUndefinedKey(); -export async function observe(instances: Instance[]): Promise{ +export async function observe(instances: Instance[]): Promise { const activeInstances = new Set(); - const instancePromises = instances.map(instance=>resolveInstance(instance, activeInstances) - ); + const instancePromises = instances.map((instance) => resolveInstance(instance, activeInstances)); await Promise.allSettled(instancePromises); updateInactiveInstances(activeInstances); } -async function resolveInstance( - instance: Instance, - activeInstances: Set -): Promise{ - try{ +async function resolveInstance(instance: Instance, activeInstances: Set): Promise { + try { calcStats(instance); const api = await getApiUrl(instance); - if(!api){ + if (!api) { handleUnresolvedApi(instance); return; } activeInstances.add(instance.name); await checkHealth(instance, api); scheduleHealthCheck(instance, api); - }catch(error){ + } catch (error) { console.error("Error resolving instance:", error); } } -async function getApiUrl(instance: Instance): Promise{ - if(instance.urls){ +async function getApiUrl(instance: Instance): Promise { + if (instance.urls) { return instance.urls.api; } - if(instance.url){ - const urls = await getApiUrls(instance.url); + if (instance.url) { + const urls = await getApiUrls(instance.url, [], false); return urls ? urls.api : null; } return null; } -function handleUnresolvedApi(instance: Instance): void{ +function handleUnresolvedApi(instance: Instance): void { setStatus(instance, false); console.warn(`${instance.name} does not resolve api URL`, instance); - setTimeout(()=>resolveInstance(instance, new Set()), 1000 * 60 * 30); + setTimeout(() => resolveInstance(instance, new Set()), 1000 * 60 * 30); } -function scheduleHealthCheck(instance: Instance, api: string): void{ +function scheduleHealthCheck(instance: Instance, api: string): void { const checkInterval = 1000 * 60 * 30; const initialDelay = Math.random() * 1000 * 60 * 10; - setTimeout(()=>{ + setTimeout(() => { checkHealth(instance, api); - setInterval(()=>checkHealth(instance, api), checkInterval); + setInterval(() => checkHealth(instance, api), checkInterval); }, initialDelay); } -async function checkHealth( - instance: Instance, - api: string, - tries = 0 -): Promise{ - try{ - const response = await fetch(`${api}/ping`, { method: "HEAD" }); +async function checkHealth(instance: Instance, api: string, tries = 0): Promise { + try { + const response = await fetch(`${api}/ping`, {method: "HEAD"}); console.log(`Checking health for ${instance.name}: ${response.status}`); - if(response.ok || tries > 3){ + if (response.ok || tries > 3) { setStatus(instance, response.ok); - }else{ + } else { retryHealthCheck(instance, api, tries); } - }catch(error){ + } catch (error) { console.error(`Error checking health for ${instance.name}:`, error); - if(tries > 3){ + if (tries > 3) { setStatus(instance, false); - }else{ + } else { retryHealthCheck(instance, api, tries); } } } -function retryHealthCheck( - instance: Instance, - api: string, - tries: number -): void{ - setTimeout(()=>checkHealth(instance, api, tries + 1), 30000); +function retryHealthCheck(instance: Instance, api: string, tries: number): void { + setTimeout(() => checkHealth(instance, api, tries + 1), 30000); } -function updateInactiveInstances(activeInstances: Set): void{ - for(const key of uptimeObject.keys()){ - if(!activeInstances.has(key)){ +function updateInactiveInstances(activeInstances: Set): void { + for (const key of uptimeObject.keys()) { + if (!activeInstances.has(key)) { setStatus(key, false); } } } -function calcStats(instance: Instance): void{ +function calcStats(instance: Instance): void { const obj = uptimeObject.get(instance.name); - if(!obj)return; + if (!obj) return; const now = Date.now(); const day = now - 1000 * 60 * 60 * 24; @@ -176,7 +163,7 @@ function calcStats(instance: Instance): void{ let weektime = 0; let online = false; - for(let i = 0; i < obj.length; i++){ + for (let i = 0; i < obj.length; i++) { const entry = obj[i]; online = entry.online; const stamp = entry.time; @@ -186,11 +173,11 @@ function calcStats(instance: Instance): void{ totalTimePassed += timePassed; alltime += Number(online) * timePassed; - if(stamp + timePassed > week){ + if (stamp + timePassed > week) { const weekTimePassed = Math.min(timePassed, nextStamp - week); weektime += Number(online) * weekTimePassed; - if(stamp + timePassed > day){ + if (stamp + timePassed > day) { const dayTimePassed = Math.min(weekTimePassed, nextStamp - day); daytime += Number(online) * dayTimePassed; } @@ -198,13 +185,7 @@ function calcStats(instance: Instance): void{ } instance.online = online; - instance.uptime = calculateUptimeStats( - totalTimePassed, - alltime, - daytime, - weektime, - online - ); + instance.uptime = calculateUptimeStats(totalTimePassed, alltime, daytime, weektime, online); } function calculateUptimeStats( @@ -212,46 +193,46 @@ function calculateUptimeStats( alltime: number, daytime: number, weektime: number, - online: boolean -): { daytime: number; weektime: number; alltime: number }{ + online: boolean, +): {daytime: number; weektime: number; alltime: number} { const dayInMs = 1000 * 60 * 60 * 24; const weekInMs = dayInMs * 7; alltime /= totalTimePassed; - if(totalTimePassed > dayInMs){ + if (totalTimePassed > dayInMs) { daytime = daytime || (online ? dayInMs : 0); daytime /= dayInMs; - if(totalTimePassed > weekInMs){ + if (totalTimePassed > weekInMs) { weektime = weektime || (online ? weekInMs : 0); weektime /= weekInMs; - }else{ + } else { weektime = alltime; } - }else{ + } else { weektime = alltime; daytime = alltime; } - return{ daytime, weektime, alltime }; + return {daytime, weektime, alltime}; } -function setStatus(instance: string | Instance, status: boolean): void{ +function setStatus(instance: string | Instance, status: boolean): void { const name = typeof instance === "string" ? instance : instance.name; let obj = uptimeObject.get(name); - if(!obj){ + if (!obj) { obj = []; uptimeObject.set(name, obj); } const lastEntry = obj.at(-1); - if(!lastEntry || lastEntry.online !== status){ - obj.push({ time: Date.now(), online: status }); + if (!lastEntry || lastEntry.online !== status) { + obj.push({time: Date.now(), online: status}); saveUptimeObject(); - if(typeof instance !== "string"){ + if (typeof instance !== "string") { calcStats(instance); } } diff --git a/src/utils.ts b/src/utils.ts index 029a13c7..1c7f543f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,98 +1,112 @@ -import{ Request, Response }from"express"; +import {instance} from "./index.js"; interface ApiUrls { - api: string; - gateway: string; - cdn: string; - wellknown: string; + api: string; + gateway: string; + cdn: string; + wellknown: string; } -interface Invite { - guild: { - name: string; - description?: string; - icon?: string; - id: string; - }; - inviter?: { - username: string; - }; -} - -export async function getApiUrls(url: string): Promise{ - if(!url.endsWith("/")){ +export async function getApiUrls( + url: string, + instances: instance[], + check = true, +): Promise { + if (!url.endsWith("/")) { url += "/"; } - try{ - const info: ApiUrls = await fetch(`${url}.well-known/spacebar`).then(res=>res.json()); - const api = info.api; - const apiUrl = new URL(api); - const policies: any = await fetch( - `${api}${apiUrl.pathname.includes("api") ? "" : "api"}/policies/instance/domains` - ).then(res=>res.json()); - return{ - api: policies.apiEndpoint, - gateway: policies.gateway, - cdn: policies.cdn, - wellknown: url, - }; - }catch(error){ - console.error("Error fetching API URLs:", error); - return null; - } -} -export async function inviteResponse(req: Request, res: Response): Promise{ - let url: URL; - try{ - url = new URL(req.query.url as string); - }catch{ - const scheme = req.secure ? "https" : "http"; - const host = `${scheme}://${req.get("Host")}`; - url = new URL(host); + if (check) { + let valid = false; + for (const instance of instances) { + const urlstr = instance.url || instance.urls?.api; + if (!urlstr) { + continue; + } + try { + if (new URL(urlstr).host === new URL(url).host) { + valid = true; + break; + } + } catch (e) { + //console.log(e); + } + } + if (!valid) { + throw new Error("Invalid instance"); + } } - try{ - if(url.pathname.startsWith("invite")){ - throw new Error("Invalid invite URL"); + const hostName = new URL(url).hostname; + try { + return await getApiUrlsV2(url); + } catch (e) { + console.warn( + `[WARN] Failed to get V2 API URLs for ${hostName}, trying V1...`, + (e as Error).message, + ); + try { + return await getApiUrlsV1(url); + } catch (e) { + console.error(`[ERROR] Failed to get V1 API URLs for ${hostName}:`, (e as Error).message); + throw e; } + } +} - const code = url.pathname.split("/")[2]; - const instance = url.searchParams.get("instance"); - if(!instance){ - throw new Error("Instance not specified"); - } +//region Well-Known V1 Interfaces - const urls = await getApiUrls(instance); - if(!urls){ - throw new Error("Failed to get API URLs"); - } +interface WellKnownV1 { + api: string; +} - const invite = await fetch(`${urls.api}/invites/${code}`).then(json=>json.json() as Promise); - const title = invite.guild.name; - const description = invite.inviter - ? `${invite.inviter.username} has invited you to ${invite.guild.name}${invite.guild.description ? `\n${invite.guild.description}` : ""}` - : `You've been invited to ${invite.guild.name}${invite.guild.description ? `\n${invite.guild.description}` : ""}`; - const thumbnail = invite.guild.icon - ? `${urls.cdn}/icons/${invite.guild.id}/${invite.guild.icon}.png` - : ""; +export async function getApiUrlsV1(url: string): Promise { + const info: WellKnownV1 = await fetch(`${url}.well-known/spacebar`).then((res) => res.json()); + const api = info.api; + const apiUrl = new URL(api); + const policies: any = await fetch( + `${api}${apiUrl.pathname.includes("api") ? "" : "api"}/policies/instance/domains`, + ).then((res) => res.json()); + return { + api: policies.apiEndpoint, + gateway: policies.gateway, + cdn: policies.cdn, + wellknown: url, + }; +} +//endregion - res.json({ - type: "link", - version: "1.0", - title, - thumbnail, - description, - }); - }catch(error){ - console.error("Error processing invite response:", error); - res.json({ - type: "link", - version: "1.0", - title: "Jank Client", - thumbnail: "/logo.webp", - description: "A spacebar client that has DMs, replying and more", - url: url.toString(), - }); - } -} \ No newline at end of file +//region Well-Known V2 Interfaces +interface WellKnownV2BasicEndpoint { + baseUrl: string; +} + +interface WellKnownV2ApiVersions { + default: string; + active: string[]; +} + +interface WellKnownV2GatewayOptions { + encoding: ("json" | "etf")[]; + compression: ("zlib-stream" | "zstd-stream" | null)[]; +} + +interface WellKnownV2 { + admin?: WellKnownV2BasicEndpoint; + api: WellKnownV2BasicEndpoint & {apiVersions: WellKnownV2ApiVersions}; + cdn: WellKnownV2BasicEndpoint; + gateway: WellKnownV2BasicEndpoint & WellKnownV2GatewayOptions; +} + +export async function getApiUrlsV2(url: string): Promise { + const info: WellKnownV2 = await fetch(`${url}.well-known/spacebar/client`).then((res) => + res.json(), + ); + return { + api: info.api.baseUrl + "/api/v" + info.api.apiVersions.default, + gateway: info.gateway.baseUrl, + cdn: info.cdn.baseUrl, + wellknown: url, + }; +} +//endregion \ No newline at end of file diff --git a/src/webpage/404.html b/src/webpage/404.html new file mode 100644 index 00000000..1c77c2e3 --- /dev/null +++ b/src/webpage/404.html @@ -0,0 +1,55 @@ + + + + + + + It seems you're lost + + + + + + + + + + +
+ +
+

It seems you're lost

+

404 Page Not Found

+ Maybe you meant to go to one of these places + +
+

If you found this page within the client please report it:

+ Github +
+
+
+ + + diff --git a/src/webpage/404.ts b/src/webpage/404.ts new file mode 100644 index 00000000..c1f941c1 --- /dev/null +++ b/src/webpage/404.ts @@ -0,0 +1,38 @@ +import {I18n} from "./i18n"; +import {setTheme, SW} from "./utils/utils"; +if (document.getElementById("404-page")) { + await setTheme(); + await I18n.done; + I18n.translatePage(); + + const easterEvents = [ + () => { + window.open("https://youtube.com/watch?v=dQw4w9WgXcQ"); + }, + () => { + window.open("https://youtube.com/watch?v=fC7oUOUEEi4"); + }, + () => { + alert(I18n[404].whatelse()); + }, + ]; + + const where = document.getElementById("whereever"); + if (where) { + where.onclick = () => { + const event = easterEvents[Math.floor(Math.random() * easterEvents.length)]; + event(); + }; + } + while (true) { + await new Promise((res) => setTimeout(res, 100)); + + if (SW.worker) { + const valid = await SW.isValid(window.location.href); + if (valid) { + window.location.reload(); + } + break; + } + } +} diff --git a/src/webpage/Commissioner-Regular.woff2 b/src/webpage/Commissioner-Regular.woff2 new file mode 100644 index 00000000..3b9aaba7 Binary files /dev/null and b/src/webpage/Commissioner-Regular.woff2 differ diff --git a/src/webpage/Dbadges.ts b/src/webpage/Dbadges.ts new file mode 100644 index 00000000..dee060ce --- /dev/null +++ b/src/webpage/Dbadges.ts @@ -0,0 +1,247 @@ +//For those wondering what in the world this is, this is the badge data for all of the badges public_flags represents in a seperate file so it's not as much of a mess +const badgeArr = [ + [ + "staff", + { + id: "staff", + description: "staff", + translate: true, + icon: "5e74e9b61934fc1f67c65515d1f7e60d", + }, + ], + [ + "partner", + { + id: "partner", + description: "partner", + translate: true, + icon: "3f9748e53446a137a052f3454e2de41e", + }, + ], + [ + "certified_moderator", + { + id: "certified_moderator", + description: "certified_moderator", + translate: true, + icon: "fee1624003e2fee35cb398e125dc479b", + }, + ], + [ + "hypesquad", + { + id: "hypesquad", + description: "hypesquad", + translate: true, + icon: "bf01d1073931f921909045f3a39fd264", + }, + ], + [ + "hypesquad_house_1", + { + id: "hypesquad_house_1", + description: "hypesquad_house_1", + translate: true, + icon: "8a88d63823d8a71cd5e390baa45efa02", + }, + ], + [ + "hypesquad_house_2", + { + id: "hypesquad_house_2", + description: "hypesquad_house_2", + translate: true, + icon: "011940fd013da3f7fb926e4a1cd2e618", + }, + ], + [ + "hypesquad_house_3", + { + id: "hypesquad_house_3", + description: "hypesquad_house_3", + translate: true, + icon: "3aa41de486fa12454c3761e8e223442e", + }, + ], + [ + "bug_hunter_level_1", + { + id: "bug_hunter_level_1", + description: "bug_hunter_level_1", + translate: true, + icon: "2717692c7dca7289b35297368a940dd0", + }, + ], + [ + "bug_hunter_level_2", + { + id: "bug_hunter_level_2", + description: "bug_hunter_level_2", + translate: true, + icon: "848f79194d4be5ff5f81505cbd0ce1e6", + }, + ], + [ + "active_developer", + { + id: "active_developer", + description: "active_developer", + translate: true, + icon: "6bdc42827a38498929a4920da12695d9", + }, + ], + [ + "verified_developer", + { + id: "verified_developer", + description: "verified_developer", + translate: true, + icon: "6df5892e0f35b051f8b61eace34f4967", + }, + ], + [ + "early_supporter", + { + id: "early_supporter", + description: "early_supporter", + translate: true, + icon: "7060786766c9c840eb3019e725d2b358", + }, + ], + [ + "premium", + { + id: "premium", + description: "premium", + translate: true, + icon: "2ba85e8026a8614b640c2837bcdfe21b", + }, + ], + [ + "guild_booster_lvl1", + { + id: "guild_booster_lvl1", + description: "guild_booster_lvl1", + translate: true, + icon: "51040c70d4f20a921ad6674ff86fc95c", + }, + ], + [ + "guild_booster_lvl2", + { + id: "guild_booster_lvl2", + description: "guild_booster_lvl2", + translate: true, + icon: "0e4080d1d333bc7ad29ef6528b6f2fb7", + }, + ], + [ + "guild_booster_lvl3", + { + id: "guild_booster_lvl3", + description: "guild_booster_lvl3", + translate: true, + icon: "72bed924410c304dbe3d00a6e593ff59", + }, + ], + [ + "guild_booster_lvl4", + { + id: "guild_booster_lvl4", + description: "guild_booster_lvl4", + translate: true, + icon: "df199d2050d3ed4ebf84d64ae83989f8", + }, + ], + [ + "guild_booster_lvl5", + { + id: "guild_booster_lvl5", + description: "guild_booster_lvl5", + translate: true, + icon: "996b3e870e8a22ce519b3a50e6bdd52f", + }, + ], + [ + "guild_booster_lvl6", + { + id: "guild_booster_lvl6", + description: "guild_booster_lvl6", + translate: true, + icon: "991c9f39ee33d7537d9f408c3e53141e", + }, + ], + [ + "guild_booster_lvl7", + { + id: "guild_booster_lvl7", + description: "guild_booster_lvl7", + translate: true, + icon: "cb3ae83c15e970e8f3d410bc62cb8b99", + }, + ], + [ + "guild_booster_lvl8", + { + id: "guild_booster_lvl8", + description: "guild_booster_lvl8", + translate: true, + icon: "7142225d31238f6387d9f09efaa02759", + }, + ], + [ + "guild_booster_lvl9", + { + id: "guild_booster_lvl9", + description: "guild_booster_lvl9", + translate: true, + icon: "ec92202290b48d0879b7413d2dde3bab", + }, + ], + [ + "bot_commands", + { + id: "bot_commands", + description: "bot_commands", + translate: true, + icon: "6f9e37f9029ff57aef81db857890005e", + }, + ], + [ + "automod", + { + id: "automod", + description: "automod", + translate: true, + icon: "f2459b691ac7453ed6039bbcfaccbfcd", + }, + ], + [ + "application_guild_subscription", + { + id: "application_guild_subscription", + description: "application_guild_subscription", + translate: true, + icon: "d2010c413a8da2208b7e4f35bd8cd4ac", + }, + ], + [ + "legacy_username", + { + id: "legacy_username", + description: "legacy_username", + translate: true, + icon: "6de6d34650760ba5551a79732e98ed60", + }, + ], + [ + "quest_completed", + { + id: "quest_completed", + description: "quest_completed", + translate: true, + icon: "7d9ae358c8c5e118768335dbe68b4fb8", + }, + ], +]; +export {badgeArr}; diff --git a/src/webpage/app.html b/src/webpage/app.html new file mode 100644 index 00000000..20e51607 --- /dev/null +++ b/src/webpage/app.html @@ -0,0 +1,152 @@ + + + + + + Fermi + + + + + + + + + + + + +
+
+ +

Fermi is loading

+

This shouldn't take long

+

Switch accounts

+
+
+
+
+
+
+

Server Name

+
+
+
+
+
+ + +
+
+
+
+
+ + +
+

USERNAME

+

STATUS

+
+
+ +
+
+
+
+
+
+
+
+
+ + + + Channel name + + +
+
+ +
+
+ + +
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ + +
+ + +
+
+ +
+
+
+
+
+
+
+
+
+ + + + + + + diff --git a/src/webpage/audio.ts b/src/webpage/audio.ts deleted file mode 100644 index 13b90cef..00000000 --- a/src/webpage/audio.ts +++ /dev/null @@ -1,196 +0,0 @@ -import{ getBulkInfo }from"./login.js"; - -class AVoice{ - audioCtx: AudioContext; - info: { wave: string | Function; freq: number }; - playing: boolean; - myArrayBuffer: AudioBuffer; - gainNode: GainNode; - buffer: Float32Array; - source: AudioBufferSourceNode; - constructor(wave: string | Function, freq: number, volume = 1){ - this.audioCtx = new window.AudioContext(); - this.info = { wave, freq }; - this.playing = false; - this.myArrayBuffer = this.audioCtx.createBuffer( - 1, - this.audioCtx.sampleRate, - this.audioCtx.sampleRate - ); - this.gainNode = this.audioCtx.createGain(); - this.gainNode.gain.value = volume; - this.gainNode.connect(this.audioCtx.destination); - this.buffer = this.myArrayBuffer.getChannelData(0); - this.source = this.audioCtx.createBufferSource(); - this.source.buffer = this.myArrayBuffer; - this.source.loop = true; - this.source.start(); - this.updateWave(); - } - get wave(): string | Function{ - return this.info.wave; - } - get freq(): number{ - return this.info.freq; - } - set wave(wave: string | Function){ - this.info.wave = wave; - this.updateWave(); - } - set freq(freq: number){ - this.info.freq = freq; - this.updateWave(); - } - updateWave(): void{ - const func = this.waveFunction(); - for(let i = 0; i < this.buffer.length; i++){ - this.buffer[i] = func(i / this.audioCtx.sampleRate, this.freq); - } - } - waveFunction(): Function{ - if(typeof this.wave === "function"){ - return this.wave; - } - switch(this.wave){ - case"sin": - return(t: number, freq: number)=>{ - return Math.sin(t * Math.PI * 2 * freq); - }; - case"triangle": - return(t: number, freq: number)=>{ - return Math.abs(((4 * t * freq) % 4) - 2) - 1; - }; - case"sawtooth": - return(t: number, freq: number)=>{ - return((t * freq) % 1) * 2 - 1; - }; - case"square": - return(t: number, freq: number)=>{ - return(t * freq) % 2 < 1 ? 1 : -1; - }; - case"white": - return(_t: number, _freq: number)=>{ - return Math.random() * 2 - 1; - }; - case"noise": - return(_t: number, _freq: number)=>{ - return 0; - }; - } - return new Function(); - } - play(): void{ - if(this.playing){ - return; - } - this.source.connect(this.gainNode); - this.playing = true; - } - stop(): void{ - if(this.playing){ - this.source.disconnect(); - this.playing = false; - } - } - static noises(noise: string): void{ - switch(noise){ - case"three": { - const voicy = new AVoice("sin", 800); - voicy.play(); - setTimeout(_=>{ - voicy.freq = 1000; - }, 50); - setTimeout(_=>{ - voicy.freq = 1300; - }, 100); - setTimeout(_=>{ - voicy.stop(); - }, 150); - break; - } - case"zip": { - const voicy = new AVoice((t: number, freq: number)=>{ - return Math.sin((t + 2) ** Math.cos(t * 4) * Math.PI * 2 * freq); - }, 700); - voicy.play(); - setTimeout(_=>{ - voicy.stop(); - }, 150); - break; - } - case"square": { - const voicy = new AVoice("square", 600, 0.4); - voicy.play(); - setTimeout(_=>{ - voicy.freq = 800; - }, 50); - setTimeout(_=>{ - voicy.freq = 1000; - }, 100); - setTimeout(_=>{ - voicy.stop(); - }, 150); - break; - } - case"beep": { - const voicy = new AVoice("sin", 800); - voicy.play(); - setTimeout(_=>{ - voicy.stop(); - }, 50); - setTimeout(_=>{ - voicy.play(); - }, 100); - setTimeout(_=>{ - voicy.stop(); - }, 150); - break; - } - case "join":{ - const voicy = new AVoice("triangle", 600,.1); - voicy.play(); - setTimeout(_=>{ - voicy.freq=800; - }, 75); - setTimeout(_=>{ - voicy.freq=1000; - }, 150); - setTimeout(_=>{ - voicy.stop(); - }, 200); - break; - } - case "leave":{ - const voicy = new AVoice("triangle", 850,.5); - voicy.play(); - setTimeout(_=>{ - voicy.freq=700; - }, 100); - setTimeout(_=>{ - voicy.stop(); - voicy.freq=400; - }, 180); - setTimeout(_=>{ - voicy.play(); - }, 200); - setTimeout(_=>{ - voicy.stop(); - }, 250); - break; - } - } - } - static get sounds(){ - return["three", "zip", "square", "beep"]; - } - static setNotificationSound(sound: string){ - const userinfos = getBulkInfo(); - userinfos.preferences.notisound = sound; - localStorage.setItem("userinfos", JSON.stringify(userinfos)); - } - static getNotificationSound(){ - const userinfos = getBulkInfo(); - return userinfos.preferences.notisound; - } -} -export{ AVoice as AVoice }; diff --git a/src/webpage/audio/audio.md b/src/webpage/audio/audio.md new file mode 100644 index 00000000..af7d567d --- /dev/null +++ b/src/webpage/audio/audio.md @@ -0,0 +1,38 @@ +# Jank Audio format (Fermi does use this format, but it's still called Jank) +This is a markdown file that will try to describe the Fermi client audio format in sufficient detail so people will know how this weird custom format works into the future. +This is a byte-aligned format, which uses the sequence jasf in ASCI as a magic number at the start. + +The next 8 bits will decide how many voices this file has/will provide, if the value is 255 you'll instead have a 16 bit number that follows for how many voices there are, this *should* be unused, but I wouldn't be totally surprised if it did get used. + +then it'll parse for that many voices, which will be formatted like the following: +name:String8; +length:f32; **if this is 0, this is not an custom sound and is instead refering to something else which will be explained later™** + +Given a non-zero length, this will parse the sounds as following: +|instruction | description | +| ---------- | ----------- | +| 000 | read float32 and use as value | +| 001 | read time(it'll be the time in seconds) | +| 002 | read frequency in hz | +| 003 | the constant PI | +| 004 | Math.sin() on the following sequence | +| 005 | multiplies the next two expressions | +| 006 | adds the next two expressions | +| 007 | divides the first expression by the second | +| 008 | subtracts the second expression by the first | +| 009 | first expression to the power of the second | +| 010 | first expression to the modulo of the second | +| 011 | absolute power of the next expression | +| 012 | round the next expression | +| 013 | Math.cos() on the next expression | + +> [!NOTE] +> This is likely to expand in the future as more things are needed, but this is just how it is currently. + +Once you've read all of the sounds in the file, you can move on to parsing the tracks. +This starts out by reading a u16 to find out how many tracks there are, then you'll go on to try and parse that many. + +Each track will then read a u16 to find out how long it is, then it'll read bytes as the following. +It'll first read the index (which is either a u8 or u16 depending on if the amount of voices was u8 or u16), which is the index of the voice 1-indexed, then if it's not 0 it'll parse two float32s in this order, the volume then the pitch of the sound, if it was 0 it'll instead read one 32f as a delay in the track. If it's a default sound it'll also read a third 32f for length + +Then finally you'll parse the audios which are the complete tracks. You'll first parse a u16 to get how many audios there are, then for each audio you'll first parse a string8 for the name, then a u16 for the length then according to the length you'll go on to parse a u16 to get the track (1-indexed again) where if it's 0 you'll instead add a delay according to the next f32, how many ever times according to the length. diff --git a/src/webpage/audio/index.html b/src/webpage/audio/index.html new file mode 100644 index 00000000..7dcf8bd4 --- /dev/null +++ b/src/webpage/audio/index.html @@ -0,0 +1,39 @@ + + + + + + Fermi Audio + + + + + + + + + + +

This will eventually be something

+

+ I want to let the sound system of Fermi not be so hard coded, but I still need to work on + everything a bit before that can happen. Thanks for your patience. +

+

why does this tool need to exist?

+

+ For size reasons Fermi does not use normal sound files, so I need to make this whole format to + be more adaptable +

+ + + + diff --git a/src/webpage/audio/page.ts b/src/webpage/audio/page.ts new file mode 100644 index 00000000..2f1945d1 --- /dev/null +++ b/src/webpage/audio/page.ts @@ -0,0 +1,192 @@ +import {BinWrite} from "../utils/binaryUtils.js"; +import {setTheme} from "../utils/utils.js"; +import {Play} from "./play.js"; +if (window.location.pathname.startsWith("/audio")) { + await setTheme(); + const w = new BinWrite(2 ** 12); + w.writeStringNo("jasf"); + w.write8(4); + + w.writeString8("sin"); + w.write32Float(0); + w.writeString8("triangle"); + w.write32Float(0); + w.writeString8("square"); + w.write32Float(0); + + w.writeString8("custom"); + w.write32Float(150); + //return Math.sin(((t + 2) ** Math.cos(t * 4)) * Math.PI * 2 * freq); + //Math.sin((((t+2)**Math.cos((t*4)))*((Math.PI*2)*f))) + w.write8(4); //sin + w.write8(5); //times + { + w.write8(9); //Power + + { + w.write8(6); //adding + w.write8(1); //t + w.write8(0); + w.write32Float(2); //2 + } + w.write8(13); //cos + w.write8(5); // times + w.write8(1); //t + w.write8(0); + w.write32Float(4); //4 + } + { + w.write8(5); //times + w.write8(5); //times + w.write8(3); //PI + w.write8(0); + w.write32Float(2); //2 + w.write8(2); //freq + } + + w.write16(5); //5 tracks + + w.write16(1); //zip + w.write8(4); + w.write32Float(1); + w.write32Float(640); + + w.write16(3); //beep + { + w.write8(1); + w.write32Float(1); + w.write32Float(700); + w.write32Float(50); + + w.write8(0); + w.write32Float(100); + + w.write8(1); + w.write32Float(1); + w.write32Float(700); + w.write32Float(50); + } + + w.write16(5); //three + { + w.write8(1); + w.write32Float(1); + w.write32Float(800); + w.write32Float(50); + + w.write8(0); + w.write32Float(50); + + w.write8(1); + w.write32Float(1); + w.write32Float(1000); + w.write32Float(50); + + w.write8(0); + w.write32Float(50); + + w.write8(1); + w.write32Float(1); + w.write32Float(1300); + w.write32Float(50); + } + + w.write16(5); //square + { + w.write8(3); + w.write32Float(1); + w.write32Float(600); + w.write32Float(50); + + w.write8(0); + w.write32Float(50); + + w.write8(3); + w.write32Float(1); + w.write32Float(800); + w.write32Float(50); + + w.write8(0); + w.write32Float(50); + + w.write8(3); + w.write32Float(1); + w.write32Float(1000); + w.write32Float(50); + } + w.write16(5); //three + { + w.write8(1); + w.write32Float(1); + w.write32Float(600); + w.write32Float(75); + + w.write8(0); + w.write32Float(75); + + w.write8(1); + w.write32Float(1); + w.write32Float(800); + w.write32Float(75); + + w.write8(0); + w.write32Float(75); + + w.write8(1); + w.write32Float(1); + w.write32Float(1000); + w.write32Float(50); + } + + w.write16(5); //5 audio + + w.writeString8("zip"); + w.write16(1); + w.write16(1); + + w.writeString8("beep"); + w.write16(1); + w.write16(2); + + w.writeString8("three"); + w.write16(1); + w.write16(3); + + w.writeString8("square"); + w.write16(1); + w.write16(4); + + w.writeString8("join"); + w.write16(1); + w.write16(5); + const buff = w.getBuffer(); + const play = new Play(buff); + /* +const zip=play.audios.get("square"); +if(zip){ + setInterval(()=>{ + zip.play() + },1000) + ; + console.log(play.voices[3][0].info.wave) +}; +*/ + console.log(play, buff); + document.onclick = () => { + play.play("join", 15); + }; + + const download = document.getElementById("download"); + if (download) { + download.onclick = () => { + const blob = new Blob([buff], {type: "binary"}); + const downloadUrl = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = downloadUrl; + a.download = "sounds.jasf"; + document.body.appendChild(a); + a.click(); + URL.revokeObjectURL(downloadUrl); + }; + } +} diff --git a/src/webpage/audio/play.ts b/src/webpage/audio/play.ts new file mode 100644 index 00000000..3abae643 --- /dev/null +++ b/src/webpage/audio/play.ts @@ -0,0 +1,55 @@ +export class Play { + buffer: ArrayBuffer; + worklet!: AudioWorkletNode; + audioContext: AudioContext; + tracks: string[] = []; + onload = () => {}; + constructor(buffer: ArrayBuffer) { + this.buffer = buffer; + this.audioContext = new AudioContext(); + this.audioContext.audioWorklet.addModule("/audio/worklet/worklet.js").then((_) => { + this.worklet = new AudioWorkletNode(this.audioContext, "audio"); + this.worklet.connect(this.audioContext.destination); + + const events = ["click", "keydown", "touchstart"] as const; + const func = () => { + this.start(); + events.forEach((event) => document.removeEventListener(event, func)); + }; + events.forEach((event) => document.addEventListener(event, func)); + console.log(this.audioContext); + + this.sendMessage({name: "bin", bin: buffer}); + + this.sendMessage({name: "getTracks"}); + this.worklet.port.onmessage = (message) => { + const data = message.data as recvMessage; + switch (data.name) { + case "tracks": + this.tracks = data.tracks; + this.onload(); + console.log(this.tracks); + } + }; + }); + } + private async start() { + if (this.audioContext.state === "suspended") { + this.sendMessage({name: "clear"}); + await this.audioContext.resume(); + } + } + private sendMessage(message: sendMessage) { + this.worklet.port.postMessage(message); + } + async play(soundName: string, volume: number) { + volume /= 200; + await this.start(); + this.sendMessage({name: "start", data: {name: soundName, volume}}); + } + static async playURL(url: string) { + const res = await fetch(url); + const arr = await res.arrayBuffer(); + return new Play(arr); + } +} diff --git a/src/webpage/audio/sounds.jasf b/src/webpage/audio/sounds.jasf new file mode 100644 index 00000000..8d77c5eb Binary files /dev/null and b/src/webpage/audio/sounds.jasf differ diff --git a/src/webpage/audio/worklet/audio.ts b/src/webpage/audio/worklet/audio.ts new file mode 100644 index 00000000..fb6e4165 --- /dev/null +++ b/src/webpage/audio/worklet/audio.ts @@ -0,0 +1,53 @@ +import {BinRead} from "../../utils/binaryUtils.js"; +import {Track} from "./track.js"; + +export class Audio { + name: string; + tracks: (Track | number)[]; + constructor(name: string, tracks: (Track | number)[]) { + this.tracks = tracks; + this.name = name; + } + static parse(read: BinRead, trackarr: Track[]): Audio { + const name = read.readAsciiString8(); + const length = read.read16(); + const tracks: (Track | number)[] = []; + for (let i = 0; i < length; i++) { + let index = read.read16(); + if (index === 0) { + tracks.push(read.readFloat32()); + } else { + tracks.push(trackarr[index - 1]); + } + } + return new Audio(name, tracks); + } + isdone(time: number) { + let cur = 0; + for (const thing of this.tracks) { + if (thing instanceof Track) { + if (!thing.isdone(time - cur)) { + return false; + } + } else { + cur += thing; + } + } + return true; + } + getNumber(time: number) { + let cur = 0; + let av = 0; + for (const thing of this.tracks) { + if (thing instanceof Track) { + const vol = thing.getNumber(time - cur); + if (vol !== 0) { + av += Math.log(10 ** av + 10 ** vol); + } + } else { + cur += thing; + } + } + return av; + } +} diff --git a/src/webpage/audio/worklet/mixAudio.ts b/src/webpage/audio/worklet/mixAudio.ts new file mode 100644 index 00000000..fafddc21 --- /dev/null +++ b/src/webpage/audio/worklet/mixAudio.ts @@ -0,0 +1,3 @@ +export function mixAudio(vol1: number, vol2: number) { + return vol1 + vol2; +} diff --git a/src/webpage/audio/worklet/play.ts b/src/webpage/audio/worklet/play.ts new file mode 100644 index 00000000..d18f3d9d --- /dev/null +++ b/src/webpage/audio/worklet/play.ts @@ -0,0 +1,48 @@ +import {BinRead} from "../../utils/binaryUtils.js"; +import {Track} from "./track.js"; +import {AVoice} from "./voice.js"; +import {Audio} from "./audio.js"; +export class Play { + voices: [AVoice, string][]; + tracks: Track[]; + audios: Map; + constructor(voices: [AVoice, string][], tracks: Track[], audios: Map) { + this.voices = voices; + this.tracks = tracks; + this.audios = audios; + } + static parseBin(buffer: ArrayBuffer) { + const read = new BinRead(buffer); + if (read.readAsciiStringNo(4) !== "jasf") throw new Error("this is not a jasf file"); + let voices = read.read8(); + let six = false; + if (voices === 255) { + voices = read.read16(); + six = true; + } + const voiceArr: [AVoice, string][] = []; + for (let i = 0; i < voices; i++) { + voiceArr.push(AVoice.getVoice(read)); + } + + const tracks = read.read16(); + const trackArr: Track[] = []; + for (let i = 0; i < tracks; i++) { + trackArr.push(Track.parse(read, voiceArr, six)); + } + + const audios = read.read16(); + const audioArr = new Map(); + for (let i = 0; i < audios; i++) { + const a = Audio.parse(read, trackArr); + audioArr.set(a.name, a); + } + + return new Play(voiceArr, trackArr, audioArr); + } + static async playURL(url: string) { + const res = await fetch(url); + const arr = await res.arrayBuffer(); + return this.parseBin(arr); + } +} diff --git a/src/webpage/audio/worklet/track.ts b/src/webpage/audio/worklet/track.ts new file mode 100644 index 00000000..63c1763a --- /dev/null +++ b/src/webpage/audio/worklet/track.ts @@ -0,0 +1,65 @@ +import {BinRead} from "../../utils/binaryUtils.js"; +import {mixAudio} from "./mixAudio.js"; +import {AVoice} from "./voice.js"; + +export class Track { + seq: (AVoice | number)[]; + constructor(playing: (AVoice | number)[]) { + this.seq = playing; + } + static parse(read: BinRead, play: [AVoice, string][], six: boolean): Track { + const length = read.read16(); + const play2: (AVoice | number)[] = []; + for (let i = 0; i < length; i++) { + let index: number; + if (six) { + index = read.read16(); + } else { + index = read.read8(); + } + if (index === 0) { + play2.push(read.readFloat32()); + continue; + } + index--; + if (!play[index]) throw new Error("voice not found"); + const [voice] = play[index]; + let temp: AVoice; + if (voice.info.wave instanceof Function) { + temp = voice.clone(read.readFloat32(), read.readFloat32()); + } else { + temp = voice.clone(read.readFloat32(), read.readFloat32(), read.readFloat32()); + } + play2.push(temp); + } + return new Track(play2); + } + isdone(time: number) { + let cur = 0; + for (const thing of this.seq) { + if (thing instanceof AVoice) { + if (!thing.isdone(time - cur)) { + return false; + } + } else { + cur += thing; + } + } + return true; + } + getNumber(time: number) { + let cur = 0; + let av = 0; + for (const thing of this.seq) { + if (thing instanceof AVoice) { + const vol = thing.getNumber(time - cur); + if (vol !== 0) { + av += mixAudio(av, vol); + } + } else { + cur += thing; + } + } + return av; + } +} diff --git a/src/webpage/audio/worklet/tsconfig.json b/src/webpage/audio/worklet/tsconfig.json new file mode 100644 index 00000000..37e74c8a --- /dev/null +++ b/src/webpage/audio/worklet/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "alwaysStrict": true, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "importHelpers": false, + "incremental": true, + "lib": ["esnext"], + "module": "ESNext", + "moduleResolution": "Bundler", + "newLine": "lf", + "noEmitHelpers": false, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "preserveConstEnums": true, + "pretty": true, + "removeComments": false, + "resolveJsonModule": true, + "sourceMap": true, + "strict": true, + "target": "es2022", + "useDefineForClassFields": true, + "resolvePackageJsonImports": true, + "skipLibCheck": true, + "outDir": "./dist" + } +} diff --git a/src/webpage/audio/worklet/types.ts b/src/webpage/audio/worklet/types.ts new file mode 100644 index 00000000..b5de8346 --- /dev/null +++ b/src/webpage/audio/worklet/types.ts @@ -0,0 +1,17 @@ +type sendMessage = + | { + name: "bin"; + bin: ArrayBuffer; + } + | { + name: "getTracks"; + } + | { + name: "start"; + data: {name: string; volume: number}; + } + | {name: "clear"}; +type recvMessage = { + name: "tracks"; + tracks: string[]; +}; diff --git a/src/webpage/audio/worklet/voice.ts b/src/webpage/audio/worklet/voice.ts new file mode 100644 index 00000000..0a34d22f --- /dev/null +++ b/src/webpage/audio/worklet/voice.ts @@ -0,0 +1,176 @@ +import {BinRead} from "../../utils/binaryUtils.js"; + +class AVoice { + info: {wave: string | ((t: number, freq: number) => number); freq: number}; + playing: boolean; + length = 1; + volume: number; + constructor( + wave: string | ((t: number, freq: number) => number), + freq: number, + volume = 1, + length = 1000, + ) { + this.length = length; + this.info = {wave, freq}; + this.playing = false; + this.volume = volume; + } + clone(volume: number, freq: number, length = this.length) { + return new AVoice(this.wave, freq, volume, length); + } + get wave(): string | ((t: number, freq: number) => number) { + return this.info.wave; + } + get freq(): number { + return this.info.freq; + } + set wave(wave: string | ((t: number, freq: number) => number)) { + this.info.wave = wave; + } + set freq(freq: number) { + this.info.freq = freq; + } + getNumber(time: number) { + if (time < 0 || time > this.length) { + return 0; + } + function smoothening(num: number) { + return 1 / (1 + Math.E ** (-10 * (num - 0.5))); + } + let fade = 1; + const range = 10; + if (time < range) { + fade = smoothening(time / range); + } + if (this.length - time < range) { + fade = smoothening((this.length - time) / range); + } + const func = this.waveFunction(); + return func(time / 1000, this.freq) * fade; + } + isdone(time: number) { + return time > this.length; + } + waveFunction(): (t: number, freq: number) => number { + if (typeof this.wave === "function") { + return this.wave; + } + switch (this.wave) { + case "sin": + return (t: number, freq: number) => { + return Math.sin(t * Math.PI * 2 * freq); + }; + case "triangle": + return (t: number, freq: number) => { + return Math.abs(((4 * t * freq) % 4) - 2) - 1; + }; + case "sawtooth": + return (t: number, freq: number) => { + return ((t * freq) % 1) * 2 - 1; + }; + case "square": + return (t: number, freq: number) => { + return (t * freq) % 2 < 1 ? 1 : -1; + }; + case "white": + return (_t: number, _freq: number) => { + return Math.random() * 2 - 1; + }; + } + return () => 0; + } + /* + static noises(noise: string): void { + switch (noise) { + case "join": { + const voicy = new AVoice("triangle", 600, 0.1); + voicy.play(); + setTimeout((_) => { + voicy.freq = 800; + }, 75); + setTimeout((_) => { + voicy.freq = 1000; + }, 150); + setTimeout((_) => { + voicy.stop(); + }, 200); + break; + } + case "leave": { + const voicy = new AVoice("triangle", 850, 0.5); + voicy.play(); + setTimeout((_) => { + voicy.freq = 700; + }, 100); + setTimeout((_) => { + voicy.stop(); + voicy.freq = 400; + }, 180); + setTimeout((_) => { + voicy.play(); + }, 200); + setTimeout((_) => { + voicy.stop(); + }, 250); + break; + } + } + } + */ + static getVoice(read: BinRead): [AVoice, string] { + const name = read.readAsciiString8(); + let length = read.readFloat32(); + let special: ((t: number, freq: number) => number) | string; + if (length !== 0) { + special = this.parseExpression(read); + } else { + special = name; + length = 1; + } + return [new AVoice(special, 0, 0, length), name]; + } + static parseExpression(read: BinRead): (t: number, freq: number) => number { + return new Function("t", "f", `return ${this.PEHelper(read)};`) as ( + t: number, + freq: number, + ) => number; + } + static PEHelper(read: BinRead): string { + let state = read.read8(); + switch (state) { + case 0: + return "" + read.readFloat32(); + case 1: + return "t"; + case 2: + return "f"; + case 3: + return `Math.PI`; + case 4: + return `Math.sin(${this.PEHelper(read)})`; + case 5: + return `(${this.PEHelper(read)}*${this.PEHelper(read)})`; + case 6: + return `(${this.PEHelper(read)}+${this.PEHelper(read)})`; + case 7: + return `(${this.PEHelper(read)}/${this.PEHelper(read)})`; + case 8: + return `(${this.PEHelper(read)}-${this.PEHelper(read)})`; + case 9: + return `(${this.PEHelper(read)}**${this.PEHelper(read)})`; + case 10: + return `(${this.PEHelper(read)}%${this.PEHelper(read)})`; + case 11: + return `Math.abs(${this.PEHelper(read)})`; + case 12: + return `Math.round(${this.PEHelper(read)})`; + case 13: + return `Math.cos(${this.PEHelper(read)})`; + default: + throw new Error("unexpected case found!"); + } + } +} + +export {AVoice as AVoice}; diff --git a/src/webpage/audio/worklet/worklet.ts b/src/webpage/audio/worklet/worklet.ts new file mode 100644 index 00000000..5afa311e --- /dev/null +++ b/src/webpage/audio/worklet/worklet.ts @@ -0,0 +1,66 @@ +import {Audio} from "./audio"; +import {mixAudio} from "./mixAudio"; +import {Play} from "./play"; +let plays: [[number], string, number][] = []; +class TestProcessor extends AudioWorkletProcessor implements AudioWorkletProcessorImpl { + play?: Play; + constructor() { + super(); + this.port.onmessage = (e) => { + const message = e.data as sendMessage; + switch (message.name) { + case "bin": + this.play = Play.parseBin(message.bin); + break; + case "getTracks": + this.postMessage({ + name: "tracks", + tracks: this.play ? [...this.play.audios.keys()] : [], + }); + break; + case "start": + plays.push([[0], message.data.name, message.data.volume]); + break; + case "clear": + plays = []; + break; + } + }; + } + postMessage(message: recvMessage) { + this.port.postMessage(message); + } + + process( + _inputs: Float32Array[][], + outputs: Float32Array[][], + _parameters: Record, + ) { + const output = outputs[0]; + const mplays = plays + .map((_) => [_[0], this.play?.audios.get(_[1]), _[2]] as const) + .filter((play) => play[1]) as [[number], Audio, number][]; + if (!mplays.length) return true; + const channel = output[0]; + + for (let i = 0; i < channel.length; i++) { + let av = 0; + for (const play of mplays) { + const vol = play[1].getNumber((play[0][0] / sampleRate) * 1000) * play[2]; + if (vol !== 0) { + av += mixAudio(av, vol); + } + + play[0][0]++; + } + channel[i] = av; + } + plays = mplays + .filter((play) => !play[1].isdone((play[0][0] / sampleRate) * 1000)) + .map((_) => [_[0], _[1].name, _[2]]); + + return true; + } +} + +registerProcessor("audio", TestProcessor); diff --git a/src/webpage/bot.ts b/src/webpage/bot.ts index 9d85e4b0..f457c4b0 100644 --- a/src/webpage/bot.ts +++ b/src/webpage/bot.ts @@ -1,36 +1,37 @@ -import { mainuserjson } from "./jsontypes.js"; -import { Localuser } from "./localuser.js"; -import { MarkDown } from "./markdown.js"; -import { Form, Settings } from "./settings.js"; -import { User } from "./user.js"; +import {mainuserjson} from "./jsontypes.js"; +import {Localuser} from "./localuser.js"; +import {MarkDown} from "./markdown.js"; +import {Form, Settings} from "./settings.js"; +import {User} from "./user.js"; import {guildjson} from "./jsontypes.js"; -import { PermissionToggle } from "./role.js"; -import { Permissions } from "./permissions.js"; -import { I18n } from "./i18n.js"; -class Bot{ - readonly owner:Localuser; - readonly token:string; - readonly json:mainuserjson; - headers: { "Content-type": string; Authorization: string }; - get localuser(){ +import {PermissionToggle} from "./role.js"; +import {Permissions} from "./permissions.js"; +import {I18n} from "./i18n.js"; +import {CDNParams} from "./utils/cdnParams.js"; +class Bot { + readonly owner: Localuser; + readonly token: string; + readonly json: mainuserjson; + headers: {"Content-type": string; Authorization: string}; + get localuser() { return this.owner; } - get info(){ + get info() { return this.localuser.info; } - constructor(json:mainuserjson,token:string,owner:Localuser){ - this.owner=owner; - this.token=token; - this.json=json; - this.headers={ + constructor(json: mainuserjson, token: string, owner: Localuser) { + this.owner = owner; + this.token = token; + this.json = json; + this.headers = { "Content-type": "application/json; charset=UTF-8", Authorization: token, }; } - settings(){ - const settings = new Settings(I18n.getTranslation("botSettings")); - const botOptions = settings.addButton("Profile",{ltr:true}); - const bot=new User(this.json,this.localuser); + settings() { + const settings = new Settings(I18n.botSettings()); + const botOptions = settings.addButton("Profile", {ltr: true}); + const bot = new User(this.json, this.localuser); { const hypotheticalProfile = document.createElement("div"); @@ -39,7 +40,7 @@ class Bot{ let newbio: string | undefined; const hypouser = bot.clone(); let color: string; - async function regen(){ + async function regen() { hypotheticalProfile.textContent = ""; const hypoprofile = await hypouser.buildprofile(-1, -1); @@ -51,23 +52,23 @@ class Bot{ settingsRight.addHTMLArea(hypotheticalProfile); const finput = settingsLeft.addFileInput( - I18n.getTranslation("uploadPfp"), - _=>{ - if(file){ + I18n.uploadPfp(), + (_) => { + if (file) { this.updatepfp(file); } }, - { clear: true } + {clear: true}, ); - finput.watchForChange(_=>{ - if(!_){ + finput.watchForChange((_) => { + if (!_) { file = null; hypouser.avatar = null; hypouser.hypotheticalpfp = true; regen(); return; } - if(_.length){ + if (_.length) { file = _[0]; const blob = URL.createObjectURL(file); hypouser.avatar = blob; @@ -77,23 +78,23 @@ class Bot{ }); let bfile: undefined | File | null; const binput = settingsLeft.addFileInput( - I18n.getTranslation("uploadBanner"), - _=>{ - if(bfile !== undefined){ + I18n.uploadBanner(), + (_) => { + if (bfile !== undefined) { this.updatebanner(bfile); } }, - { clear: true } + {clear: true}, ); - binput.watchForChange(_=>{ - if(!_){ + binput.watchForChange((_) => { + if (!_) { bfile = null; hypouser.banner = undefined; hypouser.hypotheticalbanner = true; regen(); return; } - if(_.length){ + if (_.length) { bfile = _[0]; const blob = URL.createObjectURL(bfile); hypouser.banner = blob; @@ -103,9 +104,9 @@ class Bot{ }); let changed = false; const pronounbox = settingsLeft.addTextInput( - I18n.getTranslation("pronouns"), - _=>{ - if(newpronouns || newbio || changed){ + I18n.pronouns(), + (_) => { + if (newpronouns || newbio || changed) { this.updateProfile({ pronouns: newpronouns, bio: newbio, @@ -113,33 +114,31 @@ class Bot{ }); } }, - { initText: bot.pronouns } + {initText: bot.pronouns}, ); - pronounbox.watchForChange(_=>{ + pronounbox.watchForChange((_) => { hypouser.pronouns = _; newpronouns = _; regen(); }); - const bioBox = settingsLeft.addMDInput(I18n.getTranslation("bio"), _=>{}, { + const bioBox = settingsLeft.addMDInput(I18n.bio(), (_) => {}, { initText: bot.bio.rawString, }); - bioBox.watchForChange(_=>{ + bioBox.watchForChange((_) => { newbio = _; hypouser.bio = new MarkDown(_, this.owner); regen(); }); - if(bot.accent_color){ + if (bot.accent_color) { color = "#" + bot.accent_color.toString(16); - }else{ + } else { color = "transparent"; } - const colorPicker = settingsLeft.addColorInput( - I18n.getTranslation("profileColor"), - _=>{}, - { initColor: color } - ); - colorPicker.watchForChange(_=>{ + const colorPicker = settingsLeft.addColorInput(I18n.profileColor(), (_) => {}, { + initColor: color, + }); + colorPicker.watchForChange((_) => { console.log(); color = _; hypouser.accent_color = Number.parseInt("0x" + _.substr(1), 16); @@ -148,65 +147,82 @@ class Bot{ }); } { - const guildsettings=settings.addButton("Guilds"); - guildsettings.addTitle(I18n.getTranslation("botGuilds")); - fetch(this.info.api+"/users/@me/guilds/",{ - headers:this.headers - }).then(_=>_.json()).then((json:(guildjson["properties"])[])=>{ - for(const guild of json){ - const content = document.createElement("div"); - content.classList.add("discovery-guild"); - - if(guild.banner){ - const banner = document.createElement("img"); - banner.classList.add("banner"); - banner.crossOrigin = "anonymous"; - banner.src = this.info.cdn + "/icons/" + guild.id + "/" + guild.banner + ".png?size=256"; - banner.alt = ""; - content.appendChild(banner); - } + const guildsettings = settings.addButton("Guilds"); + guildsettings.addTitle(I18n.botGuilds()); + fetch(this.info.api + "/users/@me/guilds/", { + headers: this.headers, + }) + .then((_) => _.json()) + .then((json: guildjson["properties"][]) => { + for (const guild of json) { + const content = document.createElement("div"); + content.classList.add("discovery-guild"); - const nameContainer = document.createElement("div"); - nameContainer.classList.add("flex"); - const img = document.createElement("img"); - img.classList.add("icon"); - img.crossOrigin = "anonymous"; - img.src = this.info.cdn + (guild.icon? "/icons/" + guild.id + "/" + guild.icon + ".png?size=48": "/embed/avatars/3.png"); - img.alt = ""; - nameContainer.appendChild(img); + if (guild.banner) { + const banner = document.createElement("img"); + banner.classList.add("banner"); + banner.crossOrigin = "anonymous"; + banner.src = + this.info.cdn + + "/banners/" + + guild.id + + "/" + + guild.banner + + ".png" + + new CDNParams({expectedSize: 256}); + banner.alt = ""; + content.appendChild(banner); + } - const name = document.createElement("h3"); - name.textContent = guild.name; - nameContainer.appendChild(name); - content.appendChild(nameContainer); - const desc = document.createElement("p"); - desc.textContent = guild.description; - content.appendChild(desc); + const nameContainer = document.createElement("div"); + nameContainer.classList.add("flex"); + const img = document.createElement("img"); + img.classList.add("icon"); + img.crossOrigin = "anonymous"; + img.src = + this.info.cdn + + (guild.icon + ? "/icons/" + + guild.id + + "/" + + guild.icon + + ".png" + + new CDNParams({expectedSize: 96}) + : "/embed/avatars/3.png"); + img.alt = ""; + nameContainer.appendChild(img); + const name = document.createElement("h3"); + name.textContent = guild.name; + nameContainer.appendChild(name); + content.appendChild(nameContainer); + const desc = document.createElement("p"); + desc.textContent = guild.description; + content.appendChild(desc); - guildsettings.addHTMLArea(content); - content.onclick=()=>{ - const guildsetting=guildsettings.addSubOptions(guild.name); - guildsetting.addHTMLArea(content); - guildsetting.addButtonInput("",I18n.getTranslation("leaveGuild"),()=>{ - if(confirm(I18n.getTranslation("confirmGuildLeave",guild.name))){ - fetch(this.info.api+"/users/@me/guilds/"+guild.id,{ - method:"DELETE", - headers:this.headers - }) - } - }) + guildsettings.addHTMLArea(content); + content.onclick = () => { + const guildsetting = guildsettings.addSubOptions(guild.name); + guildsetting.addHTMLArea(content); + guildsetting.addButtonInput("", I18n.leaveGuild(), () => { + if (confirm(I18n.confirmGuildLeave(guild.name))) { + fetch(this.info.api + "/users/@me/guilds/" + guild.id, { + method: "DELETE", + headers: this.headers, + }); + } + }); + }; } - } - }) + }); } settings.show(); } - updatepfp(file: Blob): void{ + updatepfp(file: Blob): void { const reader = new FileReader(); reader.readAsDataURL(file); - reader.onload = ()=>{ + reader.onload = () => { fetch(this.info.api + "/users/@me", { method: "PATCH", headers: this.headers, @@ -216,11 +232,11 @@ class Bot{ }); }; } - updatebanner(file: Blob | null): void{ - if(file){ + updatebanner(file: Blob | null): void { + if (file) { const reader = new FileReader(); reader.readAsDataURL(file); - reader.onload = ()=>{ + reader.onload = () => { fetch(this.info.api + "/users/@me", { method: "PATCH", headers: this.headers, @@ -229,7 +245,7 @@ class Bot{ }), }); }; - }else{ + } else { fetch(this.info.api + "/users/@me", { method: "PATCH", headers: this.headers, @@ -239,40 +255,39 @@ class Bot{ }); } } - updateProfile(json: { - bio?: string; - pronouns?: string; - accent_color?: number; - }){ + updateProfile(json: {bio?: string; pronouns?: string; accent_color?: number}) { fetch(this.info.api + "/users/@me/profile", { method: "PATCH", headers: this.headers, body: JSON.stringify(json), }); } - static InviteMaker(id:string,container:Form,info:Localuser["info"]){ - const gen=container.addSubOptions(I18n.getTranslation("UrlGen"),{ - noSubmit:true + static InviteMaker(id: string, container: Form, info: Localuser["info"]) { + const gen = container.addSubOptions(I18n.UrlGen(), { + noSubmit: true, }); const params = new URLSearchParams(""); params.set("instance", info.wellknown); params.set("client_id", id); params.set("scope", "bot"); - const url=gen.addText(""); - const perms=new Permissions("0"); - for(const perm of Permissions.info()){ - const permsisions=new PermissionToggle(perm,perms,gen); + const url = gen.addText(""); + const perms = new Permissions("0"); + for (const perm of Permissions.info()) { + const permsisions = new PermissionToggle(perm, perms, gen); gen.options.push(permsisions); gen.generate(permsisions); } - const cancel=setInterval(()=>{ - if(!gen.container.deref()){ + const cancel = setInterval(() => { + if (!gen.container.deref()) { clearInterval(cancel); } - params.set("permissions",perms.allow.toString()); + params.set("permissions", perms.allow.toString()); const encoded = params.toString(); - url.setText(`${location.origin}/oauth2/authorize?${encoded}`); - },100) + const urlStr = `${location.origin}/oauth2/authorize?${encoded}`; + if (urlStr == url.elm.deref()?.textContent) return; + console.log(urlStr, url.text); + url.setText(urlStr); + }, 100); } } export {Bot}; diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index 9493308c..06551791 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -1,27 +1,43 @@ -"use strict"; -import{ Message }from"./message.js"; -import{ AVoice }from"./audio.js"; -import{ Contextmenu }from"./contextmenu.js"; -import{ Guild }from"./guild.js"; -import{ Localuser }from"./localuser.js"; -import{ Permissions }from"./permissions.js"; -import{ Dialog, Settings }from"./settings.js"; -import{ Role, RoleList }from"./role.js"; -import{ InfiniteScroller }from"./infiniteScroller.js"; -import{ SnowFlake }from"./snowflake.js"; -import{channeljson,embedjson,messageCreateJson,messagejson,readyjson,startTypingjson}from"./jsontypes.js"; -import{ MarkDown }from"./markdown.js"; -import{ Member }from"./member.js"; -import { Voice } from "./voice.js"; -import { User } from "./user.js"; -import { I18n } from "./i18n.js"; - -declare global { -interface NotificationOptions { -image?: string | null | undefined; -} -} -class Channel extends SnowFlake{ +import {Message} from "./message.js"; +import {Contextmenu} from "./contextmenu.js"; +import {Guild, makeInviteMenu} from "./guild.js"; +import {Localuser} from "./localuser.js"; +import {Permissions} from "./permissions.js"; +import {Dialog, Float, Settings} from "./settings.js"; +import {Role, RoleList} from "./role.js"; +import {InfiniteScroller} from "./infiniteScroller.js"; +import {SnowFlake} from "./snowflake.js"; +import { + channeljson, + embedjson, + filejson, + memberjson, + messageCreateJson, + messagejson, + mute_config, + readyjson, + startTypingjson, + tagjson, + threadMember, + threadMetadata, +} from "./jsontypes.js"; +import {MarkDown, saveCaretPosition} from "./markdown.js"; +import {Member} from "./member.js"; +import {Voice} from "./voice.js"; +import {User, userVolMenu} from "./user.js"; +import {I18n} from "./i18n.js"; +import {mobile, createImg, safeImg} from "./utils/utils.js"; +import {webhookMenu} from "./webhooks.js"; +import {File} from "./file.js"; +import {Sticker} from "./sticker.js"; +import {CustomHTMLDivElement} from "./index.js"; +import {Direct} from "./direct.js"; +import {NotificationHandler} from "./notificationHandler.js"; +import {Command} from "./interactions/commands.js"; +import {Tag} from "./tag.js"; +import {CDNParams} from "./utils/cdnParams.js"; + +class Channel extends SnowFlake { editing!: Message | null; type!: number; owner!: Guild; @@ -32,78 +48,294 @@ class Channel extends SnowFlake{ children!: Channel[]; guild_id!: string; permission_overwrites!: Map; - permission_overwritesar!: [Role, Permissions][]; + permission_overwritesar: [Role | Promise, Permissions][] = []; topic!: string; nsfw!: boolean; position: number = 0; - lastreadmessageid: string | undefined; - lastmessageid: string | undefined; - mentions!: number; + private lastreadmessageidint?: string; + get lastreadmessageid() { + return this.lastreadmessageidint; + } + set lastreadmessageid(id: string | undefined) { + const cur = this.lastreadmessageidint; + this.lastreadmessageidint = id; + const m = this.messages.get(this.idToNext.get(cur as string) as string); + if (m) { + console.log(m); + m.generateMessage(); + } + const m2 = this.messages.get(this.idToNext.get(id as string) as string); + if (m2) { + m2.generateMessage(); + } + } + lastmessageid?: string; + trueLastMessageid?: string; + rate_limit_per_user: number = 0; + mentions = 0; lastpin!: string; move_id?: string; typing!: number; - message_notifications:number=3; + message_notifications: number = 3; allthewayup!: boolean; static contextmenu = new Contextmenu("channel menu"); replyingto!: Message | null; infinite!: InfiniteScroller; - idToPrev: Map = new Map(); - idToNext: Map = new Map(); - messages: Map = new Map(); - voice?:Voice; - bitrate:number=128000; - - muted:boolean=false; - mute_config= {selected_time_window: -1,end_time: 0} - handleUserOverrides(settings:{message_notifications: number,muted: boolean,mute_config: {selected_time_window: number,end_time: number},channel_id: string}){ - this.message_notifications=settings.message_notifications; - this.muted=settings.muted; - this.mute_config=settings.mute_config; - } - static setupcontextmenu(){ - this.contextmenu.addbutton(()=>I18n.getTranslation("channel.copyId"), function(this: Channel){ - navigator.clipboard.writeText(this.id); - }); + idToPrev: Map; + idToNext: Map; + messages: Map; + voice?: Voice; + bitrate: number = 128000; + threadData?: threadMetadata; + mute_config: mute_config | null = {selected_time_window: -1, end_time: 0}; + updateEvents = new Set<() => void>(); + fireEvents() { + for (const thing of this.updateEvents) { + thing(); + } + } + setLastMessageId(id: string) { + this.lastmessageid = id; + try { + if (BigInt(id) > BigInt(this.trueLastMessageid || "0")) { + this.trueLastMessageid = id; + } + } catch {} + } + handleUserOverrides(settings: { + message_notifications: number; + muted: boolean; + mute_config: mute_config | null; + channel_id: string; + }) { + this.message_notifications = settings.message_notifications; + this.mute_config = settings.mute_config; + } + static setupcontextmenu() { + this.contextmenu.addButton( + () => I18n.channel.markRead(), + function (this: Channel) { + this.readbottom(); + }, + ); - this.contextmenu.addbutton(()=>I18n.getTranslation("channel.markRead"), function(this: Channel){ - this.readbottom(); - }); + //TODO invite icon + this.contextmenu.addButton( + () => I18n.channel.makeInvite(), + function (this: Channel) { + this.createInvite(); + }, + { + visible: function () { + return this.hasPermission("CREATE_INSTANT_INVITE") && this.type !== 4 && !this.isThread(); + }, + color: "blue", + }, + ); + this.contextmenu.addSeperator(); + this.contextmenu.addButton( + () => I18n.threads.leave(), + function () { + fetch(this.info.api + "/channels/" + this.id + "/thread-members/@me", { + method: "DELETE", + headers: this.headers, + }); + }, + { + visible: function () { + return this.isThread() && !!this.member; + }, + }, + ); - this.contextmenu.addbutton(()=>I18n.getTranslation("channel.settings"), function(this: Channel){ - this.generateSettings(); - },null,function(){ - return this.hasPermission("MANAGE_CHANNELS"); - }); + this.contextmenu.addButton( + () => I18n.threads.join(), + function () { + fetch(this.info.api + "/channels/" + this.id + "/thread-members/@me", { + method: "POST", + headers: this.headers, + }); + }, + { + visible: function () { + return this.isThread() && !this.member; + }, + }, + ); - this.contextmenu.addbutton( - ()=>I18n.getTranslation("channel.delete"), - function(this: Channel){ - this.deleteChannel(); + this.contextmenu.addButton( + () => I18n.threads.editTags(), + function () { + this.editTags(); + }, + { + visible: function () { + return !!(this.isThread() && this.parent?.isForum() && this.parent.availableTags); + }, }, - null, - function(){ - return this.isAdmin(); - } ); - this.contextmenu.addbutton( - ()=>I18n.getTranslation("guild.notifications"), - function(){ + + this.contextmenu.addSeperator(); + //TODO notifcations icon + this.contextmenu.addButton( + () => I18n.guild.notifications(), + function () { this.setnotifcation(); - } - ) + }, + ); + this.contextmenu.addButton( + () => I18n.channel.mute(), + function () { + this.muteChannel(); + }, + { + visible: function () { + return !this.muted && this.type !== 4; + }, + }, + ); + this.contextmenu.addButton( + () => I18n.channel.unmute(), + function () { + this.unmuteChannel(); + }, + { + visible: function () { + return this.muted; + }, + }, + ); - this.contextmenu.addbutton( - ()=>I18n.getTranslation("channel.makeInvite"), - function(this: Channel){ - this.createInvite(); + this.contextmenu.addButton( + () => I18n.channel.settings(), + function (this: Channel) { + this.generateSettings(); }, - null, - function(){ - return this.hasPermission("CREATE_INSTANT_INVITE") && this.type !== 4; - } + { + visible: function () { + return this.hasPermission("MANAGE_CHANNELS") || this.owner_id === this.localuser.user.id; + }, + icon: { + css: "svg-settings", + }, + }, + ); + + this.contextmenu.addButton( + function () { + if (this.isThread()) { + return I18n.channel.deleteThread(); + } + if (this.type === 4) { + return I18n.channel.deleteCat(); + } + return I18n.channel.delete(); + }, + function (this: Channel) { + this.deleteChannel(); + }, + { + visible: function () { + return this.isThread() + ? this.hasPermission("MANAGE_THREADS") + : this.hasPermission("MANAGE_CHANNELS"); + }, + icon: { + css: "svg-delete", + }, + color: "red", + }, + ); + + this.contextmenu.addSeperator(); + //TODO copy ID icon + this.contextmenu.addButton( + function () { + if (this.type == 4) { + return I18n.channel.copyIdCat(); + } + return I18n.channel.copyId(); + }, + function (this: Channel) { + navigator.clipboard.writeText(this.id); + }, + ); + } + unmuteChannel() { + const mute_config = { + selected_time_window: -1, + end_time: 0, + }; + fetch(this.info.api + "/users/@me/guilds/" + this.guild.id + "/settings", { + method: "PATCH", + headers: this.headers, + body: JSON.stringify({ + channel_overrides: { + [this.id]: { + message_notifications: this.mentions, + muted: false, + mute_config, + channel_id: this.id, + }, + }, + }), + }); + this.mute_config = mute_config; + this.html?.deref()?.classList.remove("muted"); + this.unreads(); + this.guild.unreads(); + } + muteChannel() { + const dio = new Dialog(I18n.channel.mute()); + const opt = dio.options; + let time = 1800; + opt.addSelect( + I18n.muteDuration(), + () => {}, + (["30m", "1h", "6h", "12h", "1d", "7d", "30d", "never"] as const).map((e) => + I18n.inviteOptions[e](), + ), + ).onchange = (e) => { + time = [1800, 3600, 21600, 43200, 86400, 604800, 2592000, 1 << 30][e]; + }; + opt.addButtonInput("", I18n.submit(), () => { + const mute_config = { + selected_time_window: time, + end_time: Math.floor(new Date(Date.now() + time * 1000).getTime()), + }; + fetch(this.info.api + "/users/@me/guilds/" + this.guild.id + "/settings", { + method: "PATCH", + headers: this.headers, + body: JSON.stringify({ + channel_overrides: { + [this.id]: { + message_notifications: this.mentions, + muted: true, + mute_config, + channel_id: this.id, + }, + }, + }), + }); + this.mute_config = mute_config; + this.html?.deref()?.classList.add("muted"); + dio.hide(); + this.unreads(); + this.guild.unreads(); + }); + dio.show(); + } + get muted() { + return !!this.mute_config && new Date(this.mute_config.end_time).getTime() > Date.now(); + } + icon?: string; + iconUrl() { + return ( + `${this.info.cdn}/channel-icons/${this.id}/${this.icon}.png` + + new CDNParams({expectedSize: 32}) ); } - createInvite(){ + createInvite() { const div = document.createElement("div"); div.classList.add("invitediv"); const text = document.createElement("span"); @@ -116,13 +348,13 @@ class Channel extends SnowFlake{ const copy = document.createElement("span"); copy.classList.add("copybutton", "svgicon", "svg-copy"); copycontainer.append(copy); - copycontainer.onclick = _=>{ - if(text.textContent){ + copycontainer.onclick = (_) => { + if (text.textContent) { navigator.clipboard.writeText(text.textContent); } }; div.append(copycontainer); - const update = ()=>{ + const update = () => { fetch(`${this.info.api}/channels/${this.id}/invites`, { method: "POST", headers: this.headers, @@ -135,8 +367,8 @@ class Channel extends SnowFlake{ temporary: uses !== 0, }), }) - .then(_=>_.json()) - .then(json=>{ + .then((_) => _.json()) + .then((json) => { const params = new URLSearchParams(""); params.set("instance", this.info.wellknown); const encoded = params.toString(); @@ -144,154 +376,343 @@ class Channel extends SnowFlake{ }); }; update(); - const inviteOptions=new Dialog("",{noSubmit:true}); - inviteOptions.options.addTitle(I18n.getTranslation("inviteOptions.title")); - inviteOptions.options.addText(I18n.getTranslation("invite.subtext",this.name,this.guild.properties.name)); + const inviteOptions = new Dialog("", {noSubmit: true}); + inviteOptions.options.addTitle(I18n.inviteOptions.title()); + inviteOptions.options.addText(I18n.invite.subtext(this.name, this.guild.properties.name)); - inviteOptions.options.addSelect(I18n.getTranslation("invite.expireAfter"),()=>{}, - ["30m","1h","6h","12h","1d","7d","30d","never"].map((e)=>I18n.getTranslation("inviteOptions."+e)) - ).onchange=(e)=>{expires=[1800, 3600, 21600, 43200, 86400, 604800, 2592000, 0][e];update()}; + inviteOptions.options.addSelect( + I18n.invite.expireAfter(), + () => {}, + (["30m", "1h", "6h", "12h", "1d", "7d", "30d", "never"] as const).map((e) => + I18n.inviteOptions[e](), + ), + ).onchange = (e) => { + expires = [1800, 3600, 21600, 43200, 86400, 604800, 2592000, 0][e]; + update(); + }; - const timeOptions=["1","5","10","25","50","100"].map((e)=>I18n.getTranslation("inviteOptions.limit",e)) - timeOptions.unshift(I18n.getTranslation("inviteOptions.noLimit")) - inviteOptions.options.addSelect(I18n.getTranslation("invite.expireAfter"),()=>{},timeOptions) - .onchange=(e)=>{uses=[0, 1, 5, 10, 25, 50, 100][e];update()}; + const timeOptions = (["1", "5", "10", "25", "50", "100"] as const).map((e) => + I18n.inviteOptions.limit(e), + ); + timeOptions.unshift(I18n.inviteOptions.noLimit()); + inviteOptions.options.addSelect(I18n.invite.expireAfter(), () => {}, timeOptions).onchange = ( + e, + ) => { + uses = [0, 1, 5, 10, 25, 50, 100][e]; + update(); + }; inviteOptions.options.addHTMLArea(div); inviteOptions.show(); } - generateSettings(){ + generateSettings() { this.sortPerms(); - const settings = new Settings(I18n.getTranslation("channel.settingsFor",this.name)); + const settings = new Settings(I18n.channel.settingsFor(this.name)); { - const gensettings=settings.addButton("Settings"); - const form=gensettings.addForm("",()=>{},{ - fetchURL:this.info.api + "/channels/" + this.id, + const gensettings = settings.addButton(I18n.channel.settings()); + const form = gensettings.addForm("", () => {}, { + fetchURL: this.info.api + "/channels/" + this.id, method: "PATCH", headers: this.headers, + traditionalSubmit: true, }); - form.addTextInput(I18n.getTranslation("channel.name:"),"name",{initText:this.name}); - form.addMDInput(I18n.getTranslation("channel.topic:"),"topic",{initText:this.topic}); - form.addCheckboxInput(I18n.getTranslation("channel.nsfw:"),"nsfw",{initState:this.nsfw}); - if(this.type!==4){ - const options=["voice", "text", "announcement"]; - form.addSelect("Type:","type",options.map(e=>I18n.getTranslation("channel."+e)),{ - defaultIndex:options.indexOf({0:"text", 2:"voice", 5:"announcement", 4:"category" }[this.type] as string) - },options); - form.addPreprocessor((obj:any)=>{ - obj.type={text: 0, voice: 2, announcement: 5, category: 4 }[obj.type as string] - }) + form.addTextInput(I18n.channel["name:"](), "name", { + initText: this.name, + }); + form.addMDInput(I18n.channel["topic:"](), "topic", { + initText: this.topic, + }); + form.addImageInput(I18n.channel.icon(), "icon", { + initImg: this.icon ? this.iconUrl() : undefined, + clear: true, + }); + form.addCheckboxInput(I18n.channel["nsfw:"](), "nsfw", { + initState: this.nsfw, + }); + const times = [ + 0, + 5, + 10, + 15, + 30, + 60, + 120, + 300, + 600, + 900, + 1800, + 60 * 60, + 60 * 60 * 2, + 60 * 60 * 6, + ]; + form.addSelect( + I18n.channel.slowmode(), + "rate_limit_per_user", + ["0s", "5s", "10s", "15s", "30s", "1m", "2m", "5m", "10m", "15m", "30m", "1h", "2h", "6h"], + { + defaultIndex: (times.findIndex((_) => _ === this.rate_limit_per_user) + 1 || 1) - 1, + }, + times, + ); + const arcTimes = [60, 60 * 24, 60 * 24 * 3, 60 * 24 * 7]; + form.addSelect( + I18n.channel.hideThreads(), + "default_auto_archive_duration", + ["1h", "24h", "3d", "7d"], + { + defaultIndex: arcTimes.findIndex((_) => _ === (this.defaultAutoArchiveDuration ?? 1440)), + }, + arcTimes, + ); + if (this.isForum()) { + form.addText(I18n.forum.settings.editTags()); + const tags = document.createElement("div"); + tags.classList.add("forumTagSelect"); + const tagSet = new Set(); + const makeTagHTML = (tag: Tag) => { + tagSet.add(tag.id); + let html = tag.makeHTML(); + html.onclick = () => { + const d = new Dialog(I18n.forum.settings.editTag()); + const f = d.options.addForm( + "", + (_, obj) => { + tag.update(obj as tagjson); + const newhtml = tag.makeHTML(); + html.parentNode?.insertBefore(newhtml, html); + html.remove(); + newhtml.onclick = html.onclick; + html = newhtml; + d.hide(); + }, + { + fetchURL: this.info.api + "/channels/" + this.id + "/tags/" + tag.id, + method: "PUT", + headers: this.headers, + }, + ); + f.addTextInput(I18n.forum.settings.tagName(), "name", { + initText: tag.name, + }); + f.addCheckboxInput(I18n.forum.settings.moderated(), "moderated", { + initState: tag.moderated, + }); + f.addButtonInput("", I18n.forum.settings.delTag(), async () => { + const res = await fetch(this.info.api + "/channels/" + this.id + "/tags/" + tag.id, { + method: "DELETE", + headers: this.headers, + }); + if (res.ok) { + d.hide(); + html.remove(); + } + }); + d.show(); + }; + tags.append(html); + }; + for (const tag of this.availableTags) { + makeTagHTML(tag); + } + form.addHTMLArea(tags); + form.addButtonInput("", I18n.forum.settings.addTag(), () => { + const d = new Dialog(I18n.forum.settings.editTag()); + const f = d.options.addForm( + "", + (json) => { + const channel = json as channeljson; + if (!channel.available_tags) return; + const dontHave = channel.available_tags.filter((_) => !tagSet.has(_.id)); + for (const tagJson of dontHave) { + makeTagHTML(new Tag(tagJson, this)); + } + d.hide(); + }, + { + fetchURL: this.info.api + "/channels/" + this.id + "/tags", + method: "POST", + headers: this.headers, + }, + ); + f.addTextInput(I18n.forum.settings.tagName(), "name", { + initText: "", + }); + f.addCheckboxInput(I18n.forum.settings.moderated(), "moderated", { + initState: false, + }); + d.show(); + }); + } + if (this.type !== 4 && !this.isThread() && !this.isForum()) { + const options = ["voice", "text", "announcement"] as const; + form.addSelect( + "Type:", + "type", + options.map((e) => I18n.channel[e]()), + { + defaultIndex: options.indexOf( + {0: "text", 2: "voice", 5: "announcement", 4: "category"}[this.type] as + | "text" + | "voice" + | "announcement", + ), + }, + options, + ); + form.addPreprocessor((obj: any) => { + obj.type = {text: 0, voice: 2, announcement: 5, category: 4}[obj.type as string]; + }); } + } + if (!this.isThread()) { + const s1 = settings.addButton(I18n.channel.permissions(), {optName: ""}); + + (async () => { + const list = await Promise.all( + this.permission_overwritesar.map(async (_) => { + return [await _[0], _[1]] as [Role | User, Permissions]; + }), + ); + s1.options.push( + new RoleList(list, this.guild, this.updateRolePermissions.bind(this), this), + ); + })(); + const inviteMenu = settings.addButton(I18n.guild.invites()); + makeInviteMenu(inviteMenu, this.owner, this.info.api + `/channels/${this.id}/invites`); + + const webhooks = settings.addButton(I18n.webhooks.base()); + webhookMenu(this.guild, this.info.api + `/channels/${this.id}/webhooks`, webhooks, this.id); } - const s1 = settings.addButton("Permissions"); - s1.options.push( - new RoleList( - this.permission_overwritesar, - this.guild, - this.updateRolePermissions.bind(this), - this - ) - ); + settings.show(); } - sortPerms(){ - this.permission_overwritesar.sort((a, b)=>{ - return( - this.guild.roles.indexOf(a[0]) - - this.guild.roles.indexOf(b[0]) - ); + sortPerms() { + console.log(this.permission_overwritesar + ""); + this.permission_overwritesar.sort((a, b) => { + if (a[0] instanceof Promise) return -1; + if (b[0] instanceof Promise) return 1; + return this.guild.roles.indexOf(a[0]) - this.guild.roles.indexOf(b[0]); }); + console.log(this.permission_overwritesar + ""); } - setUpInfiniteScroller(){ + setUpInfiniteScroller() { this.infinite = new InfiniteScroller( - async (id: string, offset: number): Promise=>{ - if(offset === 1){ - if(this.idToPrev.has(id)){ + async (id: string, offset: number): Promise => { + if (offset === 1) { + if (this.idToPrev.has(id)) { return this.idToPrev.get(id); - }else{ + } else { await this.grabBefore(id); return this.idToPrev.get(id); } - }else{ - if(this.idToNext.has(id)){ + } else { + if (this.idToNext.has(id)) { return this.idToNext.get(id); - }else if(this.lastmessage?.id !== id){ + } else if (this.lastmessage?.id !== id) { await this.grabAfter(id); return this.idToNext.get(id); - }else{ - + } else { } } return undefined; }, - async (id: string): Promise=>{ + (id: string): HTMLElement => { //await new Promise(_=>{setTimeout(_,Math.random()*10)}) - const messgage = this.messages.get(id); - try{ - if(messgage){ - return messgage.buildhtml(); - }else{ + const message = this.messages.get(id); + try { + if (message) { + const html = this.fakeMessages.get(message); + if (html) { + return html; + } + return message.buildhtml(); + } else { console.error(id + " not found"); } - }catch(e){ + } catch (e) { console.error(e); } return document.createElement("div"); }, - async (id: string)=>{ + async (id: string) => { const message = this.messages.get(id); - try{ - if(message){ + try { + if (message) { + const html = this.fakeMessages.get(message); + if (html) { + html.remove(); + return true; + } message.deleteDiv(); return true; } - }catch(e){ + } catch (e) { console.error(e); - }finally{ + } finally { } return false; }, - this.readbottom.bind(this) + this.readbottom.bind(this), ); } - constructor( - json: channeljson | -1, - owner: Guild, - id: string = json === -1 ? "" : json.id - ){ + last_pin_timestamp?: string; + member?: threadMember; + + memberCount?: number; + messageCount?: number; + totalMessageSent?: number; + availableTags: Tag[] = []; + flags!: number; + defaultAutoArchiveDuration!: number; + + constructor(json: channeljson | -1, owner: Guild, id: string = json === -1 ? "" : json.id) { super(id); - if(json === -1){ + this.idToNext = owner.localuser.idToNext; + this.idToPrev = owner.localuser.idToPrev; + this.messages = owner.localuser.messages; + + this.owner = owner; + this.headers = this.owner.headers; + + if (json === -1) { return; } + this.localuser.channelids.set(this.id, this); + this.flags = json.flags || 0; + this.memberCount = json.member_count; + this.defaultAutoArchiveDuration = json.default_auto_archive_duration; + this.appliedTags = json.applied_tags || []; + this.availableTags = json.available_tags?.map((tag) => new Tag(tag, this)) || []; + this.messageCount = json.message_count; + this.totalMessageSent = json.total_message_sent; + this.owner_id = json.owner_id; + this.member = json.member; + this.rate_limit_per_user = json.rate_limit_per_user || 0; this.editing; this.type = json.type; - this.owner = owner; - this.headers = this.owner.headers; + this.name = json.name; - if(json.parent_id){ + if (json.parent_id) { this.parent_id = json.parent_id; } this.parent = undefined; this.children = []; + this.icon = json.icon; this.guild_id = json.guild_id; this.permission_overwrites = new Map(); this.permission_overwritesar = []; - for(const thing of json.permission_overwrites){ - if(thing.id === "1182819038095799904" ||thing.id === "1182820803700625444"){ - continue; - } - if(!this.permission_overwrites.has(thing.id)){ + for (const thing of json.permission_overwrites || []) { + if (!this.permission_overwrites.has(thing.id)) { //either a bug in the server requires this, or the API is cursed - this.permission_overwrites.set( - thing.id, - new Permissions(thing.allow, thing.deny) - ); + this.permission_overwrites.set(thing.id, new Permissions(thing.allow, thing.deny)); const permission = this.permission_overwrites.get(thing.id); - if(permission){ + if (permission) { const role = this.guild.roleids.get(thing.id); - if(role){ + if (role) { this.permission_overwritesar.push([role, permission]); + } else { + this.permission_overwritesar.push([this.localuser.getUser(thing.id), permission]); } } } @@ -301,162 +722,325 @@ class Channel extends SnowFlake{ this.nsfw = json.nsfw; this.position = json.position; this.lastreadmessageid = undefined; - if(json.last_message_id){ - this.lastmessageid = json.last_message_id; - }else{ + if (json.last_message_id) { + this.setLastMessageId(json.last_message_id); + } else { this.lastmessageid = undefined; } + if (this.type === 2 && this.localuser.voiceFactory) { + this.voice = this.localuser.voiceFactory.makeVoice(this.guild.id, this.id, { + bitrate: this.bitrate, + }); + this.setUpVoice(); + } this.setUpInfiniteScroller(); this.perminfo ??= {}; - if(this.type===2&&this.localuser.voiceFactory){ - this.voice=this.localuser.voiceFactory.makeVoice(this.guild.id,this.id,{bitrate:this.bitrate}); - this.setUpVoice(); + this.threadData = json.thread_metadata; + const read = this.localuser.unknownRead.get(this.id); + if (read) { + this.readStateInfo(read); + this.localuser.unknownRead.delete(this.id); } } - get perminfo(){ + get perminfo() { return this.guild.perminfo.channels[this.id]; } - set perminfo(e){ + set perminfo(e) { this.guild.perminfo.channels[this.id] = e; } - isAdmin(){ + isAdmin() { return this.guild.isAdmin(); } - get guild(){ + get guild() { return this.owner; } - get localuser(){ + get localuser() { return this.guild.localuser; } - get info(){ + get info() { return this.owner.info; } - readStateInfo(json: readyjson["d"]["read_state"]["entries"][0]){ - this.lastreadmessageid = json.last_message_id; + pinnedMessages?: Message[]; + async pinnedClick(rect: DOMRect) { + const div = document.createElement("div"); + div.classList.add("flexttb", "pinnedMessages"); + div.style.top = rect.bottom + 20 + "px"; + div.style.right = window.innerWidth - rect.right + "px"; + document.body.append(div); + Contextmenu.keepOnScreen(div); + Contextmenu.declareMenu(div); + this.last_pin_timestamp = this.lastpin; + const l = (e: MouseEvent) => { + if (e.target instanceof HTMLElement && div.contains(e.target)) { + return; + } + div.remove(); + document.removeEventListener("click", l); + }; + document.addEventListener("mouseup", l); + if (!this.pinnedMessages) { + const pinnedM = (await ( + await fetch(`${this.info.api}/channels/${this.id}/pins`, {headers: this.headers}) + ).json()) as messagejson[]; + this.pinnedMessages = pinnedM.map((_) => { + if (this.messages.has(_.id)) { + return this.messages.get(_.id) as Message; + } else { + return new Message(_, this); + } + }); + } + const pinnedM = document.getElementById("pinnedMDiv"); + if (pinnedM) { + pinnedM.classList.remove("unreadPin"); + } + if (this.pinnedMessages.length === 0) { + const b = document.createElement("b"); + b.classList.add("noPins"); + b.textContent = I18n.noPins(); + div.append(b); + return; + } + div.append( + ...this.pinnedMessages.map((_) => { + const html = _.buildhtml(undefined, true); + html.style.cursor = "pointer"; + html.onclick = async () => { + div.remove(); + await this.focus(_.id); + }; + Message.contextmenu.bindContextmenu(html, _); + return html; + }), + ); + } + readStateInfo(json: readyjson["d"]["read_state"]["entries"][0]) { + const next = this.messages.get(this.idToNext.get(this.lastreadmessageid as string) as string); + this.lastreadmessageid = isNaN(+json.last_message_id) ? "0" : json.last_message_id; this.mentions = json.mention_count; this.mentions ??= 0; this.lastpin = json.last_pin_timestamp; + if (next) { + next.generateMessage(); + } } - get hasunreads(): boolean{ - if(!this.hasPermission("VIEW_CHANNEL")){ + get hasunreads(): boolean { + return this.unreadState(); + } + private unreadState(): boolean { + if (this.muted) return false; + if (!this.hasPermission("VIEW_CHANNEL")) { return false; } - return( - Boolean(this.lastmessageid) && - (!this.lastreadmessageid || - SnowFlake.stringToUnixTime(this.lastmessageid as string) > - SnowFlake.stringToUnixTime(this.lastreadmessageid)) && - this.type !== 4 - ); + if (this.mentions) return true; + let lastreadmessage = SnowFlake.stringToUnixTime(this.lastreadmessageid || "0"); + if (this.guild.member) { + const joinedAt = new Date(this.guild.member.joined_at).getTime(); + if (!lastreadmessage || lastreadmessage < joinedAt) { + lastreadmessage = joinedAt; + } + } + const lastmessage = SnowFlake.stringToUnixTime(this.trueLastMessageid || "0"); + return !!lastmessage && (!lastreadmessage || lastmessage > lastreadmessage) && this.type !== 4; } - hasPermission(name: string, member = this.guild.member): boolean{ - if(member.isAdmin()){ + + hasPermission(name: string, member = this.guild.member): boolean { + if (member.isAdmin()) { return true; } - const roles=new Set(member.roles); - const everyone=this.guild.roles[this.guild.roles.length-1]; - if(!member.user.bot||true){ - roles.add(everyone) + if (this.isThread() && this.parent) { + return this.parent.hasPermission(name, member); + } + if (this.guild.member.commuicationDisabledLeft()) { + const allowSet = new Set(["READ_MESSAGE_HISTORY", "VIEW_CHANNEL"]); + if (!allowSet.has(name)) { + return false; + } + } + const roles = new Set(member.roles); + const everyone = this.guild.roleids.get(this.guild.id); + if (everyone) roles.add(everyone); + + const premission = this.permission_overwrites.get(member.id); + if (premission) { + const perm = premission.getPermission(name); + if (perm) { + return perm === 1; + } } - for(const thing of roles){ + + for (const thing of roles) { const premission = this.permission_overwrites.get(thing.id); - if(premission){ + if (premission) { const perm = premission.getPermission(name); - if(perm){ + if (perm) { return perm === 1; } } - if(thing.permissions.getPermission(name)){ + } + for (const thing of roles) { + if (thing.permissions.getPermission(name)) { return true; } } return false; } - get canMessage(): boolean{ - if(this.permission_overwritesar.length === 0 &&this.hasPermission("MANAGE_CHANNELS")){ - const role = this.guild.roles.find(_=>_.name === "@everyone"); - if(role){ + get canMessage(): boolean { + if (this.permission_overwritesar.length === 0 && this.hasPermission("MANAGE_CHANNELS")) { + const role = this.guild.roles.find((_) => _.name === "@everyone"); + if (role) { this.addRoleToPerms(role); } } - return this.hasPermission("SEND_MESSAGES"); + return this.isThread() + ? this.hasPermission("SEND_MESSAGES_IN_THREADS") + : this.hasPermission("SEND_MESSAGES"); } - sortchildren(){ - this.children.sort((a, b)=>{ + sortchildren() { + this.children.sort((a, b) => { return a.position - b.position; }); } - resolveparent(_guild: Guild){ + resolveparent(_guild: Guild = this.owner) { const parentid = this.parent_id; - if(!parentid)return false; + if (!parentid) return false; this.parent = this.localuser.channelids.get(parentid); this.parent ??= undefined; - if(this.parent !== undefined){ + if (this.parent !== undefined) { this.parent.children.push(this); } return this.parent !== undefined; } - calculateReorder(){ - let position = -1; + calculateReorder(position: number) { const build: { id: string; position: number | undefined; parent_id: string | undefined; }[] = []; - for(const thing of this.children){ + for (const thing of this.children) { const thisthing: { id: string; position: number | undefined; parent_id: string | undefined; - } = { id: thing.id, position: undefined, parent_id: undefined }; + } = {id: thing.id, position: undefined, parent_id: undefined}; - if(thing.position < position){ - thing.position = thisthing.position = position + 1; + if (thing.position != position) { + thisthing.position = thing.position = position; } - position = thing.position; - if(thing.move_id && thing.move_id !== thing.parent_id){ + if (thing.move_id && thing.move_id !== thing.parent_id) { thing.parent_id = thing.move_id; thisthing.parent_id = thing.parent?.id; thing.move_id = undefined; //console.log(this.guild.channelids[thisthing.parent_id.id]); } - if(thisthing.position || thisthing.parent_id){ + if (thisthing.position || thisthing.parent_id) { build.push(thisthing); } + position++; } - return build; + return [build, position] as const; } static dragged: [Channel, HTMLDivElement] | [] = []; - html: WeakRef | undefined; - get visable(){ + html: WeakRef | undefined; + get visible() { return this.hasPermission("VIEW_CHANNEL"); } - voiceUsers=new WeakRef(document.createElement("div")); - createguildHTML(admin = false): HTMLDivElement{ - const div = document.createElement("div"); + voiceUsers = new WeakRef(document.createElement("div")); + iconElm = new WeakRef(document.createElement("span") as HTMLSpanElement | safeImg); + renderIcon() { + let icon = this.iconElm.deref(); + if (this.icon && !this.localuser.perminfo.user.disableIcons) { + if (icon instanceof HTMLImageElement) { + icon.setSrcs(this.iconUrl()); + } else { + const old = icon; + const div = this.html?.deref(); + icon = createImg(this.iconUrl(), undefined, div, "icon"); + this.iconElm = new WeakRef(icon); + if (old) { + try { + old.before(icon); + old.remove(); + } catch {} + } + } + icon.classList.add("space"); + return icon; + } else if (!(icon instanceof HTMLSpanElement)) { + const old = icon; + icon = document.createElement("span"); + this.iconElm = new WeakRef(icon); + if (old) { + try { + old.before(icon); + old.remove(); + } catch {} + } + } + icon.classList = ""; + if (this.type === 0) { + if (this.guild.properties.rules_channel_id === this.id) { + icon.classList.add("space", "svgicon", "svg-rules"); + } else { + icon.classList.add("space", "svgicon", this.nsfw ? "svg-channelnsfw" : "svg-channel"); + } + } else if (this.type === 2) { + // + icon.classList.add("space", "svgicon", this.nsfw ? "svg-voicensfw" : "svg-voice"); + } else if (this.type === 5) { + // + icon.classList.add("space", "svgicon", this.nsfw ? "svg-announcensfw" : "svg-announce"); + } else if (this.type === 15) { + icon.classList.add("space", "svgicon", this.nsfw ? "svg-forumnsfw" : "svg-forum"); + } else { + console.log(this.type); + } + return icon; + } + isThread() { + return this.type === 10 || this.type === 11 || this.type === 12; + } + createguildHTML(admin = false): HTMLDivElement { + const div = this.html?.deref() || document.createElement("div"); + div.innerHTML = ""; + + if (this.muted) { + div.classList.add("muted"); + setTimeout( + () => { + div.classList.remove("muted"); + }, + Math.min((this.mute_config?.end_time as number) - Date.now(), 2147483647), + ); + } + this.html = new WeakRef(div); - if(!this.visable){ + if (!this.visible) { let quit = true; - for(const thing of this.children){ - if(thing.visable){ + for (const thing of this.children) { + if (thing.visible) { quit = false; } } - if(quit){ + if (quit) { return div; } } - // @ts-ignore I dont wanna deal with this - div.all = this; - div.draggable = admin; - div.addEventListener("dragstart", e=>{ + div.draggable = admin && !this.isThread(); + div.addEventListener("dragstart", (e) => { Channel.dragged = [this, div]; e.stopImmediatePropagation(); }); - div.addEventListener("dragend", ()=>{ + div.addEventListener("dragend", () => { Channel.dragged = []; }); - if(this.type === 4){ + const childrendiv = document.createElement("div"); + childrendiv.classList.add("channels"); + for (const channel of this.children.filter((_) => !_.isThread() || _.threadVis())) { + childrendiv.appendChild(channel.createguildHTML(admin)); + } + + if (this.type === 4) { this.sortchildren(); const caps = document.createElement("div"); @@ -468,244 +1052,351 @@ class Channel extends SnowFlake{ const myhtml = document.createElement("p2"); myhtml.classList.add("ellipsis"); myhtml.textContent = this.name; + this.nameSpan = new WeakRef(myhtml); decdiv.appendChild(myhtml); caps.appendChild(decdiv); - const childrendiv = document.createElement("div"); - if(admin){ + + if (this.guild.member.hasPermission("MANAGE_CHANNELS")) { const addchannel = document.createElement("span"); - addchannel.classList.add("addchannel","svgicon","svg-plus"); + addchannel.classList.add("addchannel", "svgicon", "svg-plus"); caps.appendChild(addchannel); - addchannel.onclick = _=>{ + addchannel.onclick = (_) => { this.guild.createchannels(this.createChannel.bind(this)); }; this.coatDropDiv(decdiv, childrendiv); } div.appendChild(caps); - caps.classList.add("flexltr","capsflex"); - decdiv.classList.add("flexltr","channeleffects"); + caps.classList.add("flexltr", "capsflex"); + decdiv.classList.add("flexltr", "channeleffects"); decdiv.classList.add("channel"); - Channel.contextmenu.bindContextmenu(decdiv, this,undefined); - // @ts-ignore I dont wanna deal with this - decdiv.all = this; + Channel.contextmenu.bindContextmenu(decdiv, this, undefined); - for(const channel of this.children){ - childrendiv.appendChild(channel.createguildHTML(admin)); - } - childrendiv.classList.add("channels"); - setTimeout((_: any)=>{ - if(!this.perminfo.collapsed){ + setTimeout((_: any) => { + if (!this.perminfo.collapsed) { childrendiv.style.height = childrendiv.scrollHeight + "px"; } }, 100); - div.appendChild(childrendiv); - if(this.perminfo.collapsed){ + + if (this.perminfo.collapsed) { decoration.classList.add("hiddencat"); childrendiv.style.height = "0px"; } - decdiv.onclick = ()=>{ - if(childrendiv.style.height !== "0px"){ + const handleColapse = async (animate: boolean = true) => { + if (this.perminfo.collapsed) { decoration.classList.add("hiddencat"); - this.perminfo.collapsed = true; - this.localuser.userinfo.updateLocal(); + childrendiv.style.height = childrendiv.scrollHeight + "px"; + await new Promise((res) => setTimeout(res, 0)); childrendiv.style.height = "0px"; - }else{ + } else { decoration.classList.remove("hiddencat"); - this.perminfo.collapsed = false; - this.localuser.userinfo.updateLocal(); - childrendiv.style.height = childrendiv.scrollHeight + "px"; + if (childrendiv.style.height === "0px" && animate) { + childrendiv.style.height = childrendiv.scrollHeight + "px"; + } else { + childrendiv.style.removeProperty("height"); + } } }; - }else{ + const observer = new MutationObserver(handleColapse.bind(this, false)); + observer.observe(childrendiv, {childList: true, subtree: true}); + + decdiv.onclick = () => { + this.perminfo.collapsed = !this.perminfo.collapsed; + handleColapse(); + }; + } else { + childrendiv.classList.add("threads"); div.classList.add("channel"); - if(this.hasunreads){ - div.classList.add("cunread"); - } - Channel.contextmenu.bindContextmenu(div, this,undefined); - if(admin){ + this.unreads(); + Channel.contextmenu.bindContextmenu(div, this, undefined); + if (admin && !this.isThread()) { this.coatDropDiv(div); } - // @ts-ignore I dont wanna deal with this - div.all = this; const button = document.createElement("button"); button.classList.add("channelbutton"); + div.append(button); const myhtml = document.createElement("span"); myhtml.classList.add("ellipsis"); myhtml.textContent = this.name; - if(this.type === 0){ - const decoration = document.createElement("span"); - button.appendChild(decoration); - decoration.classList.add("space", "svgicon", "svg-channel"); - }else if(this.type === 2){ - // - const decoration = document.createElement("span"); - button.appendChild(decoration); - decoration.classList.add("space", "svgicon", "svg-voice"); - }else if(this.type === 5){ - // - const decoration = document.createElement("span"); - button.appendChild(decoration); - decoration.classList.add("space", "svgicon", "svg-announce"); - }else{ - console.log(this.type); - } + this.nameSpan = new WeakRef(myhtml); + const decoration = this.renderIcon(); + button.appendChild(decoration); button.appendChild(myhtml); - button.onclick = _=>{ + button.onclick = (_) => { this.getHTML(); const toggle = document.getElementById("maintoggle") as HTMLInputElement; toggle.checked = true; }; - if(this.type===2){ - const voiceUsers=document.createElement("div"); + if (this.type === 2) { + const voiceUsers = document.createElement("div"); div.append(voiceUsers); - this.voiceUsers=new WeakRef(voiceUsers); + this.voiceUsers = new WeakRef(voiceUsers); this.updateVoiceUsers(); } } + div.appendChild(childrendiv); return div; } - async moveForDrag(x:number){ - const mainarea=document.getElementById("mainarea"); - if(!mainarea) return; - if(x===-1){ + owner_id?: string; + threadVis() { + return (this.member && !this.threadData?.archived) || this.localuser.channelfocus === this; + } + async moveForDrag(x: number) { + const mainarea = document.getElementById("mainarea"); + if (!mainarea) return; + if (x === -1) { mainarea.style.removeProperty("left"); mainarea.style.removeProperty("transition"); return; } - mainarea.style.left=x+"px"; - mainarea.style.transition="left 0s" + mainarea.style.left = x + "px"; + mainarea.style.transition = "left 0s"; } - async setUpVoice(){ - if(!this.voice) return; - this.voice.onMemberChange=async (memb,joined)=>{ - console.log(memb,joined); - if(typeof memb!=="string"){ - await Member.new(memb,this.guild); + async setUpVoice() { + if (!this.voice) return; + this.voice.onUserVol = (u) => { + u.volume = this.localuser.getUserAudio(u.id) / 100; + }; + this.voice.onMemberChange = async (memb, joined) => { + console.log(memb, joined); + if (typeof memb !== "string") { + await Member.new(memb, this.guild); + } + + const users = this.usersDiv.deref(); + if (users) { + const user = await this.localuser.getUser(typeof memb === "string" ? memb : memb.id); + if (joined) { + this.makeUserBox(user, users); + } else { + this.destUserBox(user); + } } + this.updateVoiceUsers(); - if(this.voice===this.localuser.currentVoice){ - AVoice.noises("join"); + if (this.voice === this.localuser.currentVoice && this.voice?.open) { + this.localuser.play?.play("join", this.localuser.getNotiVolume()); } - } + }; + this.voice.onUserChange = (user, change) => { + this.boxChange(user, change); + }; + this.voice.onSpeakingChange = (id, speaking) => { + const box = this.boxMap.get(id); + if (box) { + if (speaking) { + box.classList.add("speaking"); + } else { + box.classList.remove("speaking"); + } + } + const tray = this.voiceTray.get(id); + console.log("tray! :3"); + if (tray && tray.parentElement) { + const parent = tray.parentElement; + const pfp = parent.children[0].getElementsByClassName("pfp")[0]; + if (!pfp) return; + if (speaking) { + pfp.classList.add("speaking"); + } else { + pfp.classList.remove("speaking"); + } + } + }; } - async updateVoiceUsers(){ - const voiceUsers=this.voiceUsers.deref(); - if(!voiceUsers||!this.voice) return; - console.warn(this.voice.userids) + voiceTray = new Map(); + async updateVoiceUsers() { + const voiceUsers = this.voiceUsers.deref(); + if (!voiceUsers || !this.voice) return; + console.warn(this.voice.userids); - const html=(await Promise.all(this.voice.userids.entries().toArray().map(async _=>{ - const user=await User.resolve(_[0],this.localuser); - console.log(user); - const member=await Member.resolveMember(user,this.guild); - const array=[member,_[1]] as [Member, typeof _[1]]; - return array; - }))).flatMap(([member,_obj])=>{ - if(!member){ + const html = ( + await Promise.all( + this.voice.userids + .entries() + .toArray() + .map(async (_) => { + const user = await User.resolve(_[0], this.localuser); + console.log(user); + const member = await Member.resolveMember(user, this.guild); + const array = [member, _[1]] as [Member, (typeof _)[1]]; + return array; + }), + ) + ).flatMap(([member, obj]) => { + if (!member) { console.warn("This is weird, member doesn't exist :P"); return []; } - const div=document.createElement("div"); - div.classList.add("voiceuser"); - const span=document.createElement("span"); - span.textContent=member.name; - div.append(span); + const div = document.createElement("div"); + userVolMenu.bindContextmenu(div, this.localuser, member.id); + div.classList.add("voiceuser", "flexltr"); + const span = document.createElement("span"); + span.textContent = member.name; + member.subName(span); + + const tray = document.createElement("div"); + tray.classList.add("flexltr", "voiceTray"); + + div.append(member.user.buildpfp(member), span, tray); + member.user.bind(div, member.guild); + + this.voiceTray.set(member.id, tray); + this.boxChange(member.id, obj); return div; }); - voiceUsers.innerHTML=""; + voiceUsers.innerHTML = ""; voiceUsers.append(...html); } - get myhtml(){ - if(this.html){ + get myhtml() { + if (this.html) { return this.html.deref(); - }else{ + } else { return; } } - readbottom(){ - if(!this.hasunreads){ + readbottom() { + if (!this.unreadState()) { + this.guild.unreads(); return; } - fetch( - this.info.api +"/channels/" + this.id + "/messages/" + this.lastmessageid + "/ack", - { - method: "POST", - headers: this.headers, - body: JSON.stringify({}), - } - ); - this.lastreadmessageid = this.lastmessageid; + this.mentions = 0; + console.log(this.trueLastMessageid); + fetch(this.info.api + "/channels/" + this.id + "/messages/" + this.trueLastMessageid + "/ack", { + method: "POST", + headers: this.headers, + body: JSON.stringify({}), + }); + const next = this.messages.get(this.idToNext.get(this.lastreadmessageid as string) as string); + this.lastreadmessageid = this.trueLastMessageid; this.guild.unreads(); - if(this.myhtml){ - this.myhtml.classList.remove("cunread"); + this.unreads(); + if (next) { + next.generateMessage(); } + this.infinite.toBottom(); } - coatDropDiv(div: HTMLDivElement, container: HTMLElement | boolean = false){ - div.addEventListener("dragenter", event=>{ + static lastDragDiv = document.createElement("div"); + coatDropDiv(div: HTMLDivElement, container: HTMLElement | false = false) { + div.style.position = "relative"; + div.addEventListener("dragenter", (event) => { console.log("enter"); event.preventDefault(); }); + const dragDiv = () => { + if (Channel.lastDragDiv !== div) { + Channel.lastDragDiv.classList.remove("dragTopView"); + Channel.lastDragDiv.classList.remove("dragBottomView"); + } + Channel.lastDragDiv = div; + }; + + div.addEventListener("dragover", (event) => { + dragDiv(); + const height = div.getBoundingClientRect().height; - div.addEventListener("dragover", event=>{ + if (event.offsetY / height < 0.5) { + div.classList.add("dragTopView"); + div.classList.remove("dragBottomView"); + } else { + div.classList.remove("dragTopView"); + div.classList.add("dragBottomView"); + } event.preventDefault(); }); - - div.addEventListener("drop", event=>{ + div.addEventListener("dragleave", () => { + dragDiv(); + div.classList.remove("dragTopView"); + div.classList.remove("dragBottomView"); + }); + div.addEventListener("drop", (event) => { + dragDiv(); + div.classList.remove("dragTopView"); + div.classList.remove("dragBottomView"); const that = Channel.dragged[0]; - if(!that)return; + if (!that) return; event.preventDefault(); - if(container){ + const height = div.getBoundingClientRect().height; + const before = event.offsetY / height < 0.5; + if (container && that.type !== 4 && !before) { that.move_id = this.id; - if(that.parent){ + if (that.parent) { that.parent.children.splice(that.parent.children.indexOf(that), 1); } that.parent = this; - (container as HTMLElement).prepend( - Channel.dragged[1] as HTMLDivElement - ); + container.prepend(Channel.dragged[1] as HTMLDivElement); this.children.unshift(that); - }else{ + } else { console.log(this, Channel.dragged); - that.move_id = this.parent_id; - if(that.parent){ + let thisy = this as Channel; + if (that.type === 4) { + console.log("check", this); + if (this.parent) { + thisy = this.parent; + console.log("overwrite :3", thisy); + } + } + that.move_id = thisy.parent_id; + if (that.parent) { that.parent.children.splice(that.parent.children.indexOf(that), 1); - }else{ - this.guild.headchannels.splice( - this.guild.headchannels.indexOf(that), - 1 - ); + } else { + thisy.guild.headchannels.splice(thisy.guild.headchannels.indexOf(that), 1); } - that.parent = this.parent; - if(that.parent){ + that.parent = thisy.parent; + if (that.parent) { const build: Channel[] = []; - for(let i = 0; i < that.parent.children.length; i++){ + for (let i = 0; i < that.parent.children.length; i++) { build.push(that.parent.children[i]); - if(that.parent.children[i] === this){ + if (that.parent.children[i] === thisy) { + if (before) build.pop(); build.push(that); + if (before) build.push(thisy); } } that.parent.children = build; - }else{ + console.log(build); + } else { const build: Channel[] = []; - for(let i = 0; i < this.guild.headchannels.length; i++){ - build.push(this.guild.headchannels[i]); - if(this.guild.headchannels[i] === this){ + for (let i = 0; i < thisy.guild.headchannels.length; i++) { + build.push(thisy.guild.headchannels[i]); + if (thisy.guild.headchannels[i] === thisy) { + if (before) build.pop(); build.push(that); + if (before) build.push(thisy); } } - this.guild.headchannels = build; + thisy.guild.headchannels = build; } - if(Channel.dragged[1]){ - div.after(Channel.dragged[1]); + if (Channel.dragged[1]) { + if (this === thisy && this.type !== 4) { + if (before) { + div.before(Channel.dragged[1]); + } else { + div.after(Channel.dragged[1]); + } + } else { + let tdiv = div.parentElement as HTMLDivElement; + if (!tdiv) return; + tdiv = tdiv.parentElement as HTMLDivElement; + if (!tdiv) return; + + Channel.dragged[1].remove(); + if (before) { + tdiv.before(Channel.dragged[1]); + } else { + tdiv.after(Channel.dragged[1]); + } + } } } - this.guild.calculateReorder(); + this.guild.calculateReorder(that.id); }); return div; } - createChannel(name: string, type: number){ + createChannel(name: string, type: number) { fetch(this.info.api + "/guilds/" + this.guild.id + "/channels", { method: "POST", headers: this.headers, @@ -715,36 +1406,38 @@ class Channel extends SnowFlake{ parent_id: this.id, permission_overwrites: [], }), - }); + }) + .then((_) => _.json()) + .then((_) => this.guild.goToChannelDelay(_.id)); } - deleteChannel(){ + deleteChannel() { fetch(this.info.api + "/channels/" + this.id, { method: "DELETE", headers: this.headers, }); } - setReplying(message: Message){ - if(this.replyingto?.div){ + setReplying(message: Message) { + if (this.replyingto?.div) { this.replyingto.div.classList.remove("replying"); } this.replyingto = message; const typebox = document.getElementById("typebox") as HTMLElement; typebox.focus(); - if(!this.replyingto?.div)return; + if (!this.replyingto?.div) return; console.log(message); this.replyingto.div.classList.add("replying"); this.makereplybox(); } - makereplybox(){ + makereplybox() { const replybox = document.getElementById("replybox") as HTMLElement; const typebox = document.getElementById("typebox") as HTMLElement; - if(this.replyingto){ + if (this.replyingto) { replybox.innerHTML = ""; const span = document.createElement("span"); - span.textContent = I18n.getTranslation("replyingTo", this.replyingto.author.username); + span.textContent = I18n.replyingTo(this.replyingto.author.username); const X = document.createElement("button"); - X.onclick = _=>{ - if(this.replyingto?.div){ + X.onclick = (_) => { + if (this.replyingto?.div) { this.replyingto.div.classList.remove("replying"); } replybox.classList.add("hideReplyBox"); @@ -753,159 +1446,1411 @@ class Channel extends SnowFlake{ typebox.classList.remove("typeboxreplying"); }; replybox.classList.remove("hideReplyBox"); - X.classList.add("cancelReply","svgicon","svg-x"); + X.classList.add("cancelReply", "svgicon", "svg-x"); replybox.append(span); replybox.append(X); typebox.classList.add("typeboxreplying"); - }else{ + } else { replybox.classList.add("hideReplyBox"); + replybox.innerHTML = ""; typebox.classList.remove("typeboxreplying"); } } - async getmessage(id: string): Promise{ + async getmessage(id: string): Promise { const message = this.messages.get(id); - if(message){ + if (message) { return message; - }else{ + } else { const gety = await fetch( - this.info.api + "/channels/" +this.id +"/messages?limit=1&around=" +id, - { headers: this.headers } + this.info.api + "/channels/" + this.id + "/messages?limit=1&around=" + id, + {headers: this.headers}, ); const json = await gety.json(); + if (json.length === 0) { + return undefined; + } return new Message(json[0], this); } } - editLast(){ - let message:Message|undefined=this.lastmessage; - while(message&&message.author!==this.localuser.user){ - message=this.messages.get(this.idToPrev.get(message.id) as string); - } - if(message){ - message.setEdit(); + async getMessages(id: string) { + const m = await this.getmessage(id); + if (!m) return; + const waits: Promise[] = []; + let m1: string | undefined = m.id; + for (let i = 0; i <= 50; i++) { + if (!m1) { + waits.push(this.grabBefore(id)); + break; + } + if ((this.idToNext.has(m1) && !this.idToNext.get(m1)) || this.lastmessage?.id === m1) break; + m1 = this.idToNext.get(m1); + } + m1 = m.id; + for (let i = 0; i <= 50; i++) { + if (!m1) { + waits.push(this.grabAfter(id)); + break; + } + if (this.idToPrev.has(m1) && !this.idToPrev.get(m1)) break; + m1 = this.idToPrev.get(m1); } + await Promise.all(waits); + console.log(waits); } - static genid: number = 0; - async getHTML(addstate=true){ - const id = ++Channel.genid; - if(this.localuser.channelfocus){ - this.localuser.channelfocus.infinite.delete(); + async focus(id: string, flash = true) { + const prom = this.getMessages(id); + + if (await Promise.race([prom, new Promise((res) => setTimeout(() => res(true), 300))])) { + const loading = document.getElementById("loadingdiv") as HTMLDivElement; + Channel.regenLoadingMessages(); + loading.classList.add("loading"); + await prom; + loading.classList.remove("loading"); } - if(this.guild !== this.localuser.lookingguild){ - this.guild.loadGuild(); + + if (this.localuser.channelfocus !== this) { + await this.getHTML(true); + } + + try { + await this.infinite.focus(id, flash, true); + } catch {} + } + editLast() { + let message: Message | undefined = this.lastmessage; + while (message && message.author !== this.localuser.user) { + message = this.messages.get(this.idToPrev.get(message.id) as string); + } + if (message) { + message.setEdit(); + } + } + static genid: number = 0; + nsfwPannel() { + (document.getElementById("typebox") as HTMLDivElement).contentEditable = "" + false; + (document.getElementById("upload") as HTMLElement).style.visibility = "hidden"; + (document.getElementById("typediv") as HTMLElement).style.visibility = "hidden"; + const messages = document.getElementById("scrollWrap") as HTMLDivElement; + const messageContainers = Array.from(messages.getElementsByClassName("messagecontainer")); + for (const thing of messageContainers) { + thing.remove(); + } + const elements = Array.from(messages.getElementsByClassName("scroller")); + for (const elm of elements) { + elm.remove(); + console.warn("rouge element detected and removed"); + } + const div = document.getElementById("sideDiv") as HTMLDivElement; + div.innerHTML = ""; + const float = new Float(""); + const options = float.options; + //@ts-ignore weird hack, ik, but the user here does have that information + //TODO make an extention of the user class with these aditional properties + //TODO make a popup for `nsfw_allowed==null` to input age + if (this.localuser.user.nsfw_allowed) { + options.addTitle("This is a NSFW channel, do you wish to proceed?"); + const buttons = options.addOptions("", {ltr: true}); + buttons.addButtonInput("", "Yes", () => { + this.perminfo.nsfwOk = true; + this.getHTML(); + }); + buttons.addButtonInput("", "No", () => { + window.history.back(); + }); + } else { + options.addTitle("You are not allowed in this channel."); + } + const html = float.generateHTML(); + html.classList.add("messagecontainer"); + messages.append(html); + } + unreadPins() { + if (!this.last_pin_timestamp && !this.lastpin) return false; + return this.last_pin_timestamp !== this.lastpin; + } + boxMap = new Map(); + liveMap = new Map(); + destUserBox(user: User) { + const box = this.boxMap.get(user.id); + if (!box) return; + box.remove(); + this.boxMap.delete(user.id); + const live = this.liveMap.get(user.id); + if (live) { + live.remove(); + this.liveMap.delete(user.id); + } + } + boxVid(id: string, elm: HTMLVideoElement) { + //TODO make a loading screen thingy if the video isn't progressing in time yet + const box = this.boxMap.get(id); + if (!box) return; + console.log("vid", elm); + box.append(elm); + } + makeBig(box: HTMLElement) { + const par = box.parentElement; + if (!par) return; + if (par.children[0] !== box || !box.classList.contains("bigBox")) { + box.classList.add("bigBox"); + if (par.children[0] !== box) { + par.children[0].classList.remove("bigBox"); + } + } else { + par.children[0].classList.remove("bigBox"); + } + par.prepend(box); + } + decorateLive(id: string) { + if (!this.voice) return; + const box = this.liveMap.get(id); + if (!box) return; + box.innerHTML = ""; + const live = this.voice.getLive(id); + const self = id === this.localuser.user.id; + if (!this.voice.open) { + const span = document.createElement("span"); + span.textContent = I18n.vc.joinForStream(); + box.append(span); + } else if (live) { + const leave = document.createElement("button"); + leave.classList.add("leave"); + leave.textContent = self ? I18n.vc.stopstream() : I18n.vc.leavestream(); + leave.onclick = (e) => { + e.stopImmediatePropagation(); + if (self) { + this.voice?.stopStream(); + } else { + this.voice?.leaveLive(id); + } + }; + box.append(live, leave); + } else if (!self) { + const joinB = document.createElement("button"); + joinB.textContent = I18n.vc.joinstream(); + joinB.classList.add("joinb"); + box.append(joinB); + joinB.onclick = () => { + if (!this.voice) return; + box.innerHTML = ""; + const span = document.createElement("span"); + span.textContent = I18n.vc.joiningStream(); + box.append(span); + this.voice.joinLive(id); + }; + } + } + purgeVid(id: string) { + const box = this.boxMap.get(id); + if (!box) return; + const videos = Array.from(box.getElementsByTagName("video")); + videos.forEach((_) => _.remove()); + } + boxChange(id: string, change: {deaf: boolean; muted: boolean; video: boolean; live: boolean}) { + const box = this.boxMap.get(id); + + if (!this.voice) return; + if (box) { + console.warn("purge:" + id); + const vid = this.voice.videos.get(id); + if (vid && change.video) { + this.boxVid(id, vid); + } else if (!change.video) { + this.purgeVid(id); + } + Array.from(box.getElementsByClassName("statBub")).forEach((_) => _.remove()); + const statBub = document.createElement("div"); + statBub.classList.add("statBub"); + if (change.muted) { + const span = document.createElement("span"); + span.classList.add("svg-micmute"); + statBub.append(span); + box.append(statBub); + } else if (change.video) { + const span = document.createElement("span"); + span.classList.add("svg-video"); + statBub.append(span); + box.append(statBub); + } + } + + const live = this.liveMap.get(id); + if (live && !change.live) { + live.remove(); + this.liveMap.delete(id); + } else if (!live && change.live && box) { + const livediv = document.createElement("div"); + this.liveMap.set(id, livediv); + livediv.onclick = () => { + this.makeBig(livediv); + }; + box.parentElement?.prepend(livediv); + this.decorateLive(id); + } + + const tray = this.voiceTray.get(id); + if (tray) { + console.warn("tray build", tray, change); + tray.innerHTML = ""; + if (change.muted) { + const span = document.createElement("span"); + span.classList.add("svg-micmute"); + tray.append(span); + } + if (change.video) { + const span = document.createElement("span"); + span.classList.add("svg-video"); + tray.append(span); + } + } + } + async makeUserBox(user: User, users: HTMLElement) { + const memb = Member.resolveMember(user, this.guild); + const box = document.createElement("div"); + userVolMenu.bindContextmenu(box, this.localuser, user.id); + box.onclick = () => { + this.makeBig(box); + }; + this.boxMap.set(user.id, box); + if (user.accent_color != undefined) { + box.style.setProperty( + "--accent_color", + `#${user.accent_color.toString(16).padStart(6, "0")}`, + ); + } + memb.then((_) => { + if (!_) return; + if (_.accent_color !== undefined) { + box.style.setProperty("--accent_color", `#${_.accent_color.toString(16).padStart(6, "0")}`); + } + }); + + box.append(user.buildpfp(this.guild)); + + const span = document.createElement("span"); + span.textContent = user.name; + memb.then((_) => { + if (!_) { + user.subName(span); + return; + } + _.subName(span); + span.textContent = _.name; + }); + span.classList.add("voiceUsername"); + box.append(span); + users.append(box); + if (!this.voice) return; + const change = this.voice.userids.get(user.id); + if (!change) return; + this.boxChange(user.id, change); + } + usersDiv = new WeakRef(document.createElement("div")); + async setUpVoiceArea() { + if (!this.voice) throw new Error("voice not found?"); + const voiceArea = document.getElementById("voiceArea") as HTMLElement; + const buttonRow = document.createElement("div"); + buttonRow.classList.add("flexltr", "buttonRow"); + const updateMicIcon = () => { + mspan.classList.remove("svg-micmute", "svg-mic"); + mspan.classList.add(this.localuser.mute ? "svg-micmute" : "svg-mic"); + }; + + const mute = document.createElement("div"); + const mspan = document.createElement("span"); + mute.append(mspan); + updateMicIcon(); + this.localuser.updateOtherMic = updateMicIcon; + mute.onclick = () => { + this.localuser.mute = !this.localuser.mute; + this.localuser.updateMic(); + }; + mute.classList.add("muteVoiceIcon"); + + const muteOpt = document.createElement("div"); + muteOpt.classList.add("muteOptDiv"); + const mOptSpan = document.createElement("span"); + mOptSpan.classList.add("svg-category"); + muteOpt.append(mOptSpan); + mute.append(muteOpt); + muteOpt.onclick = async (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + const menu = new Contextmenu(""); + const mics = await this.localuser.getAudioDeviceList(); + for (const mic of mics) { + menu.addButton( + mic.label, + () => { + this.localuser.setNewDefualtDevice(mic.deviceId); + }, + { + icon: { + css: + mic.deviceId === this.localuser.getDefaultAudio() ? "svg-select" : "svg-noSelect", + }, + }, + ); + } + const box = muteOpt.getBoundingClientRect(); + menu.makemenu(box.left, box.top - 34 - window.innerHeight); + }; + + const updateCallIcon = () => { + cspan.classList.remove("svg-call", "svg-hangup"); + cspan.classList.add(this.voice?.open ? "svg-hangup" : "svg-call"); + }; + const call = document.createElement("div"); + const cspan = document.createElement("span"); + call.append(cspan); + updateCallIcon(); + call.onclick = async () => { + if (this.voice?.userids.has(this.localuser.user.id)) { + this.voice.leave(); + } else if (this.voice) { + await this.localuser.joinVoice(this); + } + updateCallIcon(); + }; + call.classList.add("callVoiceIcon"); + + const updateVideoIcon = () => { + vspan.classList.remove("svg-video", "svg-novideo"); + vspan.classList.add(this.localuser.voiceFactory?.video ? "svg-video" : "svg-novideo"); + }; + const video = document.createElement("div"); + const vspan = document.createElement("span"); + video.append(vspan); + updateVideoIcon(); + video.onclick = async () => { + if (!this.voice) return; + if (!this.voice.open) return; + if (this.localuser.voiceFactory?.video) { + this.voice.stopVideo(); + } else { + const cam = await navigator.mediaDevices.getUserMedia({ + video: { + advanced: [ + { + aspectRatio: 1.75, + }, + ], + }, + }); + if (!cam) return; + this.voice.startVideo(cam); + } + updateVideoIcon(); + }; + video.classList.add("callVoiceIcon"); + + const updateLiveIcon = () => { + lspan.classList.remove("svg-stream", "svg-stopstream"); + lspan.classList.add(this.voice?.isLive() ? "svg-stopstream" : "svg-stream"); + }; + const live = document.createElement("div"); + const lspan = document.createElement("span"); + live.append(lspan); + updateLiveIcon(); + live.onclick = async () => { + if (!this.voice?.open) return; + if (this.voice?.isLive()) { + this.voice?.stopStream(); + } else { + const stream = await navigator.mediaDevices.getDisplayMedia(); + const v = await this.voice?.createLive(stream); + console.log(v); + } + updateLiveIcon(); + }; + live.classList.add("callVoiceIcon"); + + const chat = document.createElement("div"); + const chatspan = document.createElement("span"); + chatspan.classList.add("svg-frmessage"); + chat.append(chatspan); + updateLiveIcon(); + chat.onclick = async () => { + this.voiceMode = this.voiceMode === "VoiceOnly" ? "ChatAndVoice" : "VoiceOnly"; + this.getHTML(true); + }; + chat.classList.add("callVoiceIcon"); + + buttonRow.append(mute, video, live, call, chat); + + const users = document.createElement("div"); + const mut = new MutationObserver(() => { + const arr = Array.from(users.children); + const box = arr.find((_) => _.classList.contains("bigBox")); + if (box && arr[0] !== box) { + users.prepend(box); + } + }); + mut.observe(users, { + childList: true, + }); + users.classList.add("voiceUsers"); + + this.voice.userids.forEach(async (_, id) => { + const user = await this.localuser.getUser(id); + this.makeUserBox(user, users); + }); + [...this.liveMap].forEach(([_, box]) => { + users.prepend(box); + }); + this.usersDiv = new WeakRef(users); + + voiceArea.append(users, buttonRow); + this.voice.onVideo = (vid, id) => { + this.localuser.regenVoiceIcons(); + console.warn("happened"); + this.boxVid(id, vid); + updateVideoIcon(); + }; + this.voice.onGotStream = (_vid, id) => { + this.localuser.regenVoiceIcons(); + updateLiveIcon(); + this.decorateLive(id); + }; + this.voice.onconnect = () => { + if (!this.voice) return; + for (const [_, user] of this.voice.users) { + this.decorateLive(user); + } + }; + this.voice.onLeaveStream = (id) => { + this.decorateLive(id); + updateLiveIcon(); + this.localuser.regenVoiceIcons(); + }; + + this.voice.onLeave = () => { + updateCallIcon(); + updateVideoIcon(); + for (const [id] of this.boxMap) { + this.purgeVid(id); + } + if (!this.voice) return; + for (const [_, user] of this.voice.users) { + this.decorateLive(user); + } + }; + } + files: Blob[] = []; + htmls = new WeakMap(); + textSave = ""; + collectBox() { + const typebox = document.getElementById("typebox") as CustomHTMLDivElement; + const [files, html] = this.localuser.fileExtange([], new WeakMap()); + this.files = files; + this.htmls = html; + this.textSave = MarkDown.gatherBoxText(typebox); + typebox.textContent = ""; + } + curCommand?: Command; + curWatch = () => {}; + async submitCommand() { + if (!this.curCommand) return; + const typebox = document.getElementById("typebox") as CustomHTMLDivElement; + if (await this.curCommand.submit(typebox, this)) { + this.curCommand = undefined; + const typebox = document.getElementById("typebox") as CustomHTMLDivElement; + typebox.markdown.boxEnabled = true; + typebox.innerHTML = ""; + typebox.markdown.boxupdate(); + typebox.removeEventListener("keyup", this.curWatch); + } + } + startCommand(command: Command) { + this.curCommand = command; + const typebox = document.getElementById("typebox") as CustomHTMLDivElement; + typebox.markdown.boxEnabled = false; + const func = () => { + const node = window.getSelection()?.focusNode; + if (this.localuser.channelfocus === this) { + const out = command.collect(typebox, this, node || undefined); + if (!out) { + typebox.markdown.boxEnabled = true; + typebox.markdown.boxupdate(); + typebox.removeEventListener("keyup", func); + } + } + }; + this.curWatch = func; + typebox.addEventListener("keyup", func); + command.render(typebox, this); + } + isForum() { + return this.type === 15 || this.type === 16; + } + async renderThread(update = true) { + let div = document.createElement("div"); + if (update) { + const up = async () => { + if (!document.contains(div)) { + this.updateEvents.delete(up); + return; + } + console.log(this.updateEvents); + const c = await this.renderThread(false); + if (!c) return; + div.after(c); + div.remove(); + div = c; + }; + this.updateEvents.add(up); + } + div.classList.add("flexttb", "forumPostBody"); + div.onclick = (e) => { + if (e.button === 0) this.getHTML(); + }; + Channel.contextmenu.bindContextmenu(div, this, undefined); + const tags = document.createElement("div"); + tags.classList.add("flexltr", "tagDiv"); + for (const tagid of this.appliedTags) { + const tag = this.parent?.availableTags.find((tag) => tag.id === tagid); + if (!tag) continue; + tags.append(tag.makeHTML()); + } + div.append(tags); + + const title = document.createElement("h3"); + title.textContent = new MarkDown(this.name).makeHTML().textContent; + div.append(title); + + const member = + (await this.localuser.getMember(this.owner_id as string, this.guild.id)) || + (await User.resolve(this.owner_id as string, this.localuser)); + const message = this.localuser.messages.get(this.id); + + const name = document.createElement("span"); + name.classList.add("forumUsername"); + name.textContent = (member?.name || I18n.user.deleted()) + ": "; + + const messageContent = document.createElement("span"); + messageContent.classList.add("messageForumContent"); + if (message) messageContent.append(message.content.makeHTML()); + else messageContent.textContent = I18n.message.deleted(); + + const mrow = document.createElement("div"); + mrow.classList.add("flexltr"); + mrow.append(name, messageContent); + div.append(mrow); + + const micon = document.createElement("span"); + micon.classList.add("svg-frmessage", "svgicon"); + + const mcount = document.createElement("span"); + mcount.textContent = this.messageCount + ""; + + const msep = document.createElement("span"); + msep.classList.add("msep"); + + const mtime = document.createElement("span"); + const updateTime = () => { + mtime.textContent = MarkDown.relTime( + new Date( + this.lastmessageid ? SnowFlake.stringToUnixTime(this.lastmessageid) : this.getUnixTime(), + ), + () => { + if (!document.contains(mtime)) return; + updateTime(); + }, + ); + }; + updateTime(); + + const lrow = document.createElement("div"); + lrow.classList.add("forumMessageRow", "flexltr"); + + lrow.append(micon, mcount, msep, mtime); + div.append(lrow); + + return div; + } + appliedTags = [] as string[]; + forumFilters = { + sortActive: true, + recentFirst: true, + tagMatchAll: false, + tags: [] as string[], + }; + hasFetchedForForum = false; + editTags() { + const d = new Dialog(I18n.threads.editTags()); + if (!this.appliedTags || !this.parent) return; + let tagList = [...this.appliedTags]; + const patchList = () => { + fetch(this.info.api + "/channels/" + this.id, { + method: "PATCH", + headers: this.headers, + body: JSON.stringify({ + applied_tags: tagList, + }), + }); + }; + + const tags = document.createElement("div"); + tags.classList.add("forumTagSelect"); + for (const tag of this.parent.availableTags.filter( + (tag) => !tag.moderated || this.hasPermission("MANAGE_THREADS"), + )) { + const html = tag.makeHTML(); + html.onclick = () => { + if (tagList.includes(tag.id)) { + tagList = tagList.filter((id) => id !== tag.id); + html.classList.remove("selected"); + patchList(); + } else { + tagList.push(tag.id); + tagList.sort(); + html.classList.add("selected"); + patchList(); + } + }; + if (tagList.includes(tag.id)) html.classList.add("selected"); + tags.append(html); + } + const opt = d.options; + opt.addHTMLArea(tags); + d.show(); + } + async fetchForum() { + const arr = (await ( + await fetch(this.info.api + "/channels/" + this.id + "/post-data", { + method: "POST", + headers: this.headers, + body: JSON.stringify({thread_ids: this.children.map(({id}) => id)}), + }) + ).json()) as { + threads: Record; + }; + for (const [id, {first_message, owner}] of Object.entries(arr.threads)) { + const child = this.children.find(({id: cid}) => cid === id); + if (!child) continue; + if (owner) Member.new(owner, this.guild); + if (first_message) { + const m = this.localuser.messages.get(first_message.id); + if (!m) this.localuser.messages.set(first_message.id, new Message(first_message, child)); + } + } + this.hasFetchedForForum = true; + return; + } + + umap = new Map(); + search: [string, number, string][] = []; + hasAllThreads = false; + + async fetchThreads(text: string, offset: number) { + const filters = this.forumFilters; + const uid = `${filters.recentFirst}-${filters.sortActive}-${filters.tagMatchAll}-${filters.tags.join("-")}-${text}`; + const search = new URLSearchParams([ + ["archived", "true"], + ["limit", "25"], + ["sort_by", filters.sortActive ? "last_message_time" : "creation_time"], + ["sort_order", filters.recentFirst ? "desc" : "asc"], + ["tag_setting", filters.tagMatchAll ? "match_all" : "match_some"], + ["offset", offset + ""], + ["tag", filters.tags.join(",")], + ]); + const res = (await ( + await fetch(this.info.api + "/channels/" + this.id + "/threads/search?" + search, { + headers: this.headers, + }) + ).json()) as { + threads: channeljson[]; + members: memberjson[]; + messages: messagejson[]; + total_results: number; + has_more: boolean; + }; + for (const threadjson of res.threads) { + if (this.localuser.channelids.has(threadjson.id)) continue; + const thread = new Channel(threadjson, this.guild); + this.localuser.channelids.set(threadjson.id, thread); + thread.resolveparent(); + } + for (const message of res.messages) { + const thread = this.localuser.channelids.get(message.channel_id); + if (!thread) continue; + const m = this.localuser.messages.get(message.id); + if (!m) this.localuser.messages.set(message.id, new Message(message, thread)); + } + for (const member of res.members) { + Member.new(member, this.guild); + } + if (res.has_more) { + const lastid = res.threads.at(-1)?.id as string; + if (text) { + const arr = this.search.find(([id]) => id === uid); + if (arr) { + arr[1] = offset + 25; + arr[2] = lastid; + } else { + this.search.push([uid, offset + 25, lastid]); + if (this.search.length > 200) { + this.search.shift(); + } + } + } else { + this.umap.set(uid, [offset + 25, lastid]); + } + } else { + if (text) { + const arr = this.search.find(([id]) => id === uid); + if (arr) { + arr[1] = offset + 25; + arr[2] = "-1"; + } else { + this.search.push([uid, offset + 25, "-1"]); + if (this.search.length > 200) { + this.search.shift(); + } + } + } else { + this.umap.clear(); + this.search = []; + this.hasAllThreads = true; + } + } + } + + genCurSort(text: string, offset: number) { + const filters = this.forumFilters; + text = text.toLowerCase(); + let match = this.children.filter((thread) => thread.name.toLowerCase().includes(text)); + const tagSet = new Set(filters.tags); + if (tagSet.size) + match = match.filter((thread) => { + const tags = new Set(thread.appliedTags); + if (filters.tagMatchAll) { + return tagSet.isSubsetOf(tags); + } else { + return tags.intersection(tagSet).size; + } + }); + match.sort((c1, c2) => { + if (filters.sortActive) { + return +(Number(c1.lastmessageid || c1.id) > Number(c2.lastmessageid || c2.id)) ^ + +filters.recentFirst + ? 1 + : -1; + } else { + return +(Number(c1.id) > Number(c2.id)) ^ +filters.recentFirst ? 1 : -1; + } + }); + + return [match.slice(offset, offset + 25), !!match.at(offset + 25)] as const; + } + + async findMatches(text: string, offset: number): Promise { + if (this.hasAllThreads) return this.genCurSort(text, offset); + + const filters = this.forumFilters; + const uid = `${filters.recentFirst}-${filters.sortActive}-${filters.tagMatchAll}-${filters.tags.join("-")}-${text}`; + let offset2 = 0; + let lastid = ""; + if (text) { + [, offset2, lastid] = this.search.find(([id]) => id === uid) || ["", 0, ""]; + } else { + [offset2, lastid] = this.umap.get(uid) || [0, ""]; } - if(this.localuser.channelfocus && this.localuser.channelfocus.myhtml){ + + if (lastid) { + const [list, more] = this.genCurSort(text, offset); + for (const elm of list) { + if (elm.id === lastid) { + await this.fetchThreads(text, offset2); + return this.genCurSort(text, offset); + } + } + return [list, more]; + } else { + await this.fetchThreads(text, offset2); + return this.genCurSort(text, offset); + } + } + + async forumSearch(text: string, div: HTMLDivElement) { + while (!this.hasFetchedForForum) await this.fetchForum(); + let offset = 0; + const flipPage = async (by: number) => { + offset += by; + div.scrollTop = 0; + const [match, more] = await this.findMatches(text, offset); + + await renderDiv(match, more); + }; + + const renderDiv = async (threads: Channel[], more: boolean) => { + div.innerHTML = ""; + + const sortRow = document.createElement("div"); + sortRow.classList.add("flexltr", "forumSortRow"); + div.append(sortRow); + + const cMenu = new Contextmenu("sortAndStuff"); + const opts = I18n.forum.sortOptions; + + cMenu.addText(opts.sortby.title()); + cMenu.addButton( + opts.sortby.recent(), + () => { + this.forumFilters.sortActive = true; + flipPage(0); + }, + { + icon: { + css: this.forumFilters.sortActive ? "svg-select" : "svg-noSelect", + }, + }, + ); + cMenu.addButton( + opts.sortby.posted(), + () => { + this.forumFilters.sortActive = false; + flipPage(0); + }, + { + icon: { + css: this.forumFilters.sortActive ? "svg-noSelect" : "svg-select", + }, + }, + ); + cMenu.addSeperator(); + + cMenu.addText(opts.sortOrder.title()); + cMenu.addButton( + opts.sortOrder.recent(), + () => { + this.forumFilters.recentFirst = true; + flipPage(0); + }, + { + icon: { + css: this.forumFilters.recentFirst ? "svg-select" : "svg-noSelect", + }, + }, + ); + + cMenu.addButton( + opts.sortOrder.old(), + () => { + this.forumFilters.recentFirst = false; + flipPage(0); + }, + { + icon: { + css: this.forumFilters.recentFirst ? "svg-noSelect" : "svg-select", + }, + }, + ); + cMenu.addSeperator(); + + cMenu.addText(opts.tagMatch.title()); + cMenu.addButton( + opts.tagMatch.some(), + () => { + this.forumFilters.tagMatchAll = false; + flipPage(0); + }, + { + icon: {css: this.forumFilters.tagMatchAll ? "svg-noSelect" : "svg-select"}, + }, + ); + cMenu.addButton( + opts.tagMatch.all(), + () => { + this.forumFilters.tagMatchAll = true; + flipPage(0); + }, + { + icon: { + css: this.forumFilters.tagMatchAll ? "svg-select" : "svg-noSelect", + }, + }, + ); + + const sortOptionButton = document.createElement("button"); + sortOptionButton.textContent = opts.name(); + + sortOptionButton.onclick = (e) => { + if (e.button !== 0) return; + e.preventDefault(); + e.stopImmediatePropagation(); + const box = sortOptionButton.getBoundingClientRect(); + cMenu.makemenu(box.left, box.bottom + 6); + }; + + const tags = document.createElement("div"); + tags.classList.add("forumTagSelect"); + + const allMenu = new Contextmenu("allTagMenu"); + + for (const tag of this.availableTags) { + const html = tag.makeHTML(); + if (this.forumFilters.tags.includes(tag.id)) html.classList.add("selected"); + html.onclick = () => { + if (this.forumFilters.tags.includes(tag.id)) { + this.forumFilters.tags = this.forumFilters.tags.filter((id) => id !== tag.id); + } else { + this.forumFilters.tags.push(tag.id); + this.forumFilters.tags.sort(); + } + flipPage(0); + }; + tags.append(html); + allMenu.addButton( + tag.name, + () => { + if (this.forumFilters.tags.includes(tag.id)) { + this.forumFilters.tags = this.forumFilters.tags.filter((id) => id !== tag.id); + } else { + this.forumFilters.tags.push(tag.id); + this.forumFilters.tags.sort(); + } + flipPage(0); + }, + { + icon: {css: this.forumFilters.tags.includes(tag.id) ? "svg-select" : "svg-noSelect"}, + }, + ); + } + + const allTags = document.createElement("button"); + allTags.classList.add("allTagButton"); + allTags.textContent = I18n.forum.allTags(); + + allTags.onclick = (e) => { + if (e.button !== 0) return; + e.preventDefault(); + e.stopImmediatePropagation(); + const box = allTags.getBoundingClientRect(); + allMenu.makemenu(box.right - window.innerWidth, box.bottom + 6); + }; + + sortRow.append(sortOptionButton, tags, allTags); + + div.append(...(await Promise.all(threads.map((_) => _.renderThread())))); + + const buttonRow = document.createElement("div"); + buttonRow.classList.add("flexltr", "forumButtonRow"); + + const next = document.createElement("button"); + next.textContent = I18n.forum.next(); + next.disabled = !more; + if (more) { + next.onclick = () => { + flipPage(25); + }; + } + + const back = document.createElement("button"); + back.textContent = I18n.forum.back(); + back.disabled = !offset; + if (offset) { + back.onclick = () => { + flipPage(-25); + }; + } + + buttonRow.append(back, next); + div.append(buttonRow); + }; + await flipPage(0); + } + renderForum() { + (document.getElementById("typediv") as HTMLElement).style.visibility = "hidden"; + const container = document.createElement("div"); + + container.classList.add("messagecontainer", "flexttb", "forumBody"); + (document.getElementById("scrollWrap") as HTMLElement).append(container); + const superContainer = document.createElement("div"); + superContainer.classList.add("forumHead", "flexttb"); + const headContainer = document.createElement("div"); + superContainer.append(headContainer); + headContainer.classList.add("flexltr"); + container.append(superContainer); + + const icon = document.createElement("span"); + icon.classList.add("svgicon", "svg-search", "forumIcon"); + headContainer.append(icon); + + const text = document.createElement("input"); + text.type = "text"; + text.classList.add("forumSearch"); + headContainer.append(text); + text.placeholder = I18n.forum.creorsear(); + + const post = document.createElement("button"); + post.textContent = I18n.forum.newPost(); + post.classList.add("newPostForumButton"); + headContainer.append(post); + + post.onclick = () => { + const postF = async () => { + if (this.flags & (1 << 4) && !tagList.length) { + showError(I18n.forum.errors.tagsReq()); + return; + } + const content = md.rawString; + if (!content) { + showError(I18n.forum.errors.requireText()); + return; + } + const res = (await ( + await fetch(this.info.api + "/channels/" + this.id + "/threads", { + method: "POST", + headers: this.headers, + body: JSON.stringify({ + name: text.value, + applied_tags: tagList, + message: { + content, + }, + }), + }) + ).json()) as channeljson; + this.localuser.goToChannel(res.id); + }; + post.onclick = postF; + post.textContent = I18n.forum.post(); + const box = document.createElement("div"); + box.classList.add("messageEditContainer"); + const area = document.createElement("div"); + const sb = document.createElement("div"); + sb.style.position = "absolute"; + sb.style.width = "100%"; + const search = document.createElement("div"); + search.classList.add("searchOptions", "flexttb"); + area.classList.add("editMessage"); + try { + area.contentEditable = "plaintext-only"; + } catch { + area.contentEditable = "true"; + } + const md = new MarkDown("", this, {keep: true}); + area.append(md.makeHTML()); + area.addEventListener("keyup", (event) => { + if (this.localuser.keyup(event)) return; + }); + area.addEventListener("keydown", (event) => { + this.localuser.keydown(event); + }); + md.giveBox(area, (str, pre) => { + this.localuser.search(search, md, str, pre); + }); + sb.append(search); + box.append(sb, area); + superContainer.append(box); + setTimeout(() => { + area.focus(); + const fun = saveCaretPosition(area, Infinity); + if (fun) fun(); + }); + box.oncontextmenu = (e) => { + e.stopImmediatePropagation(); + }; + + let tagList: string[] = []; + + const tags = document.createElement("div"); + tags.classList.add("forumTagSelect"); + for (const tag of this.availableTags.filter( + (tag) => !tag.moderated || this.hasPermission("MANAGE_THREADS"), + )) { + const html = tag.makeHTML(); + html.onclick = () => { + if (tagList.includes(tag.id)) { + tagList = tagList.filter((id) => id !== tag.id); + html.classList.remove("selected"); + } else { + tagList.push(tag.id); + tagList.sort(); + html.classList.add("selected"); + } + }; + tags.append(html); + } + superContainer.append(tags); + function showError(text: string) { + errorText.textContent = text; + setTimeout(() => { + errorText.textContent = ""; + }, 5000); + } + + const errorText = document.createElement("span"); + errorText.classList.add("forumPostError"); + superContainer.append(errorText); + }; + + const theadList = document.createElement("div"); + theadList.classList.add("flexttb", "forumList"); + container.append(theadList); + + let curidsearch = 0; + text.onkeyup = async () => { + const thisid = ++curidsearch; + await new Promise((res) => setTimeout(res, 120)); + if (thisid !== curidsearch) return; + this.forumSearch(text.value, theadList); + }; + this.forumSearch("", theadList); + } + async getHTML(addstate = true, getMessages: boolean | void = undefined, aroundMessage?: string) { + if (!this.visible) { + this.guild.loadChannel(); + return; + } + if (this.owner instanceof Direct) { + this.owner.freindDiv?.classList.remove("viewChannel"); + } + if (this.localuser.channelfocus) { + this.localuser.channelfocus.collectBox(); + } + const typebox = document.getElementById("typebox") as CustomHTMLDivElement; + typebox.markdown.boxEnabled = !this.curCommand; + if (this.curCommand) { + this.curCommand.render(typebox, this); + } + typebox.style.setProperty("--channel-text", JSON.stringify(I18n.channel.typebox(this.name))); + if (!this.curCommand && !this.isForum()) { + const md = typebox.markdown; + md.owner = this; + typebox.textContent = this.textSave; + md.boxupdate(Infinity); + } + if (this.isForum()) { + typebox.textContent = ""; + } + this.localuser.fileExtange(this.files, this.htmls); + + if (getMessages === undefined) { + getMessages = this.type !== 2 || !this.localuser.voiceAllowed; + } + + const messages = document.getElementById("scrollWrap") as HTMLDivElement; + const messageContainers = Array.from(messages.getElementsByClassName("messagecontainer")); + for (const thing of messageContainers) { + thing.remove(); + } + const chatArea = document.getElementById("chatArea") as HTMLElement; + + const voiceArea = document.getElementById("voiceArea") as HTMLElement; + voiceArea.innerHTML = ""; + if (getMessages && this.type !== 2) { + chatArea.style.removeProperty("display"); + } else { + if (this.voiceMode === "VoiceOnly") { + chatArea.style.setProperty("display", "none"); + } else { + chatArea.style.removeProperty("display"); + getMessages = true; + } + this.setUpVoiceArea(); + this.localuser.memberListUpdate(); + } + + const pinnedM = document.getElementById("pinnedMDiv"); + if (pinnedM) { + if (this.unreadPins()) { + pinnedM.classList.add("unreadPin"); + } else { + pinnedM.classList.remove("unreadPin"); + } + } + if (addstate) { + history.pushState( + [this.guild_id, this.id, aroundMessage], + "", + "/channels/" + this.guild_id + "/" + this.id + (aroundMessage ? `/${aroundMessage}` : ""), + ); + } + this.localuser.pageTitle("#" + this.name); + const channelTopic = document.getElementById("channelTopic") as HTMLSpanElement; + if (this.topic) { + channelTopic.innerHTML = ""; + channelTopic.append(new MarkDown(this.topic, this).makeHTML()); + channelTopic.removeAttribute("hidden"); + channelTopic.onclick = () => { + const d = new Dialog(this.name); + d.options.addHTMLArea(new MarkDown(this.topic, this).makeHTML()); + d.show(); + }; + } else { + channelTopic.setAttribute("hidden", ""); + channelTopic.onclick = () => {}; + } + if (this.guild !== this.localuser.lookingguild) { + this.guild.loadGuild(); + } + + if (this.localuser.channelfocus && this.localuser.channelfocus.myhtml) { this.localuser.channelfocus.myhtml.classList.remove("viewChannel"); } - if(this.myhtml){ + if (this.myhtml) { this.myhtml.classList.add("viewChannel"); } + const id = ++Channel.genid; + + if (this.localuser.channelfocus && this.localuser.channelfocus !== this) { + this.localuser.channelfocus.infinite.delete(); + + if (this.localuser.channelfocus.isThread() && !this.localuser.channelfocus.member) { + const prev = this.localuser.channelfocus; + this.localuser.channelfocus = this; + prev.parent?.createguildHTML(); + } + } else if (this.localuser.channelfocus === this && !aroundMessage && !this.isForum()) { + if (this.lastmessageid) + this.infinite.focus(aroundMessage || this.lastmessageid, !!aroundMessage, true); + return; + } this.guild.prevchannel = this; this.guild.perminfo.prevchannel = this.id; - this.localuser.userinfo.updateLocal(); this.localuser.channelfocus = this; - const prom = this.infinite.delete(); - if(addstate){ - history.pushState([this.guild_id,this.id], "", "/channels/" + this.guild_id + "/" + this.id); + + if (this.isThread() && !this.member) { + this.parent?.createguildHTML(); + if (this.myhtml) { + this.myhtml.classList.add("viewChannel"); + } } - this.localuser.pageTitle("#" + this.name); - const channelTopic = document.getElementById("channelTopic") as HTMLSpanElement; - if(this.topic){ - channelTopic.innerHTML =""; - channelTopic.append(new MarkDown( - this.topic, - this - ).makeHTML()); - channelTopic.removeAttribute("hidden"); - }else channelTopic.setAttribute("hidden", ""); - const loading = document.getElementById("loadingdiv") as HTMLDivElement; - Channel.regenLoadingMessages(); - loading.classList.add("loading"); - this.rendertyping(); + if ( + this.nsfw && //@ts-ignore another hack + (!this.perminfo.nsfwOk || !this.localuser.user.nsfw_allowed) + ) { + this.nsfwPannel(); + return; + } + this.slowmode(); this.localuser.getSidePannel(); - if(this.voice&&localStorage.getItem("Voice enabled")){ - this.localuser.joinVoice(this); - } - (document.getElementById("typebox") as HTMLDivElement).contentEditable =""+this.canMessage; - (document.getElementById("upload") as HTMLElement).style.visibility=this.canMessage?"visible":"hidden"; - (document.getElementById("typediv") as HTMLElement).style.visibility="visible"; - (document.getElementById("typebox") as HTMLDivElement).focus(); - await this.putmessages(); + if (this.isForum()) { + this.renderForum(); + return; + } + + const prom = this.infinite.delete(); + if (getMessages) { + const loading = document.getElementById("loadingdiv") as HTMLDivElement; + Channel.regenLoadingMessages(); + loading.classList.add("loading"); + } + this.rendertyping(); + + try { + (document.getElementById("typebox") as HTMLDivElement).contentEditable = this.canMessage + ? "plaintext-only" + : "false"; + } catch { + (document.getElementById("typebox") as HTMLDivElement).contentEditable = this.canMessage + ? "true" + : "false"; + } + (document.getElementById("upload") as HTMLElement).style.visibility = this.canMessage + ? "visible" + : "hidden"; + (document.getElementById("gifTB") as HTMLElement).style.display = this.canMessage + ? "block" + : "none"; + (document.getElementById("stickerTB") as HTMLElement).style.display = this.canMessage + ? "block" + : "none"; + (document.getElementById("emojiTB") as HTMLElement).style.display = this.canMessage + ? "block" + : "none"; + (document.getElementById("mobileSend") as HTMLElement).style.display = this.canMessage + ? "block" + : "none"; + (document.getElementById("typediv") as HTMLElement).style.visibility = "visible"; + if (!mobile) { + (document.getElementById("typebox") as HTMLDivElement).focus(); + } else { + (document.getElementById("typebox") as HTMLDivElement).blur(); + } + if (getMessages) await this.putmessages(); await prom; - if(id !== Channel.genid){ + if (id !== Channel.genid) { return; } this.makereplybox(); - await this.buildmessages(); + if (getMessages) await this.buildmessages(aroundMessage); //loading.classList.remove("loading"); - } typingmap: Map = new Map(); - async typingStart(typing: startTypingjson): Promise{ + async typingStart(typing: startTypingjson): Promise { const memb = await Member.new(typing.d.member!, this.guild); - if(!memb)return; - if(memb.id === this.localuser.user.id){ + if (!memb) return; + this.typingmap.set(memb, Date.now()); + memb.user.statusChange(); + setTimeout(() => { + this.rendertyping(); + memb.user.statusChange(); + }, 10000); + if (memb.id === this.localuser.user.id) { console.log("you is typing"); return; } console.log("user is typing and you should see it"); - this.typingmap.set(memb, Date.now()); - setTimeout(this.rendertyping.bind(this), 10000); this.rendertyping(); } - similar(str:string){ - if(this.type===4) return -1; - const strl=Math.max(str.length,1) - if(this.name.includes(str)){ - return strl/this.name.length; - }else if(this.name.toLowerCase().includes(str.toLowerCase())){ - return strl/this.name.length/1.2; + similar(str: string) { + if (this.type === 4) return -1; + const strl = Math.max(str.length, 1); + if (this.name.includes(str)) { + return strl / this.name.length; + } else if (this.name.toLowerCase().includes(str.toLowerCase())) { + return strl / this.name.length / 1.2; } return 0; } - rendertyping(): void{ + rendertyping(): void { const typingtext = document.getElementById("typing") as HTMLDivElement; let build = ""; let showing = false; let i = 0; const curtime = Date.now() - 5000; - for(const thing of this.typingmap.keys()){ - if((this.typingmap.get(thing) as number) > curtime){ - if(i !== 0){ - build += ", "; - } - i++; - if(thing.nick){ - build += thing.nick; - }else{ - build += thing.user.username; + for (const thing of this.typingmap.keys()) { + const self = thing.id === this.localuser.user.id; + if ((this.typingmap.get(thing) as number) > curtime) { + if (!self) { + if (i !== 0) { + build += ", "; + } + i++; + if (thing.nick) { + build += thing.nick; + } else { + build += thing.user.username; + } + showing = true; } - showing = true; - }else{ + } else { this.typingmap.delete(thing); } } - build=I18n.getTranslation("typing",i+"",build); - if(this.localuser.channelfocus === this){ - if(showing){ + build = I18n.typing(i + "", build); + if (this.localuser.channelfocus === this) { + if (showing) { typingtext.classList.remove("hidden"); - const typingtext2 = document.getElementById( - "typingtext" - ) as HTMLDivElement; + const typingtext2 = document.getElementById("typingtext") as HTMLDivElement; typingtext2.textContent = build; - }else{ + } else { typingtext.classList.add("hidden"); } } } - static regenLoadingMessages(){ + static regenLoadingMessages() { const loading = document.getElementById("loadingdiv") as HTMLDivElement; loading.innerHTML = ""; - for(let i = 0; i < 15; i++){ + for (let i = 0; i < 15; i++) { const div = document.createElement("div"); div.classList.add("loadingmessage"); - if(Math.random() < 0.5){ + if (Math.random() < 0.5) { const pfp = document.createElement("div"); pfp.classList.add("loadingpfp"); const username = document.createElement("div"); @@ -922,358 +2867,417 @@ class Channel extends SnowFlake{ } } lastmessage: Message | undefined; - setnotifcation(){ - const defualt=I18n.getTranslation("guild."+["all", "onlyMentions", "none","default"][this.guild.message_notifications]) - const options=["all", "onlyMentions", "none","default"].map(e=>I18n.getTranslation("guild."+e,defualt)); - const notiselect=new Dialog(""); - const form=notiselect.options.addForm("",(_,sent:any)=>{ - notiselect.hide(); - console.log(sent); - this.message_notifications = sent.channel_overrides[this.id].message_notifications; - },{ - fetchURL:`${this.info.api}/users/@me/guilds/${this.guild.id}/settings/`, - method:"PATCH", - headers:this.headers - }); - form.addSelect(I18n.getTranslation("guild.selectnoti"),"message_notifications",options,{ - radio:true, - defaultIndex:this.message_notifications - },[0,1,2,3]); + setnotifcation() { + const optionsArr = ["all", "onlyMentions", "none", "default"] as const; + const defualt = I18n.guild[optionsArr[this.guild.message_notifications]](); + const options = optionsArr.map((e) => I18n.guild[e](defualt)); + const notiselect = new Dialog(""); + const form = notiselect.options.addForm( + "", + (_, sent: any) => { + notiselect.hide(); + console.log(sent); + this.message_notifications = sent.channel_overrides[this.id].message_notifications; + }, + { + fetchURL: `${this.info.api}/users/@me/guilds/${this.guild.id}/settings/`, + method: "PATCH", + headers: this.headers, + }, + ); + form.addSelect( + I18n.guild.selectnoti(), + "message_notifications", + options, + { + radio: true, + defaultIndex: this.message_notifications, + }, + [0, 1, 2, 3], + ); - form.addPreprocessor((e:any)=>{ - const message_notifications=e.message_notifications; + form.addPreprocessor((e: any) => { + const message_notifications = e.message_notifications; delete e.message_notifications; - e.channel_overrides={ - [this.id]:{ + e.channel_overrides = { + [this.id]: { message_notifications, - muted:this.muted, - mute_config:this.mute_config, - channel_id:this.id - } - } - }) - /* - let noti = this.message_notifications; - const defualt=I18n.getTranslation("guild."+["all", "onlyMentions", "none","default"][this.guild.message_notifications]) - const options=["all", "onlyMentions", "none","default"].map(e=>I18n.getTranslation("guild."+e,defualt)) - const notiselect = new Dialog([ - "vdiv", - [ - "radio", - I18n.getTranslation("guild.selectnoti"), - options, - function(e: string){ - noti = options.indexOf(e); - }, - noti, - ], - [ - "button", - "", - "submit", - (_: any)=>{ - // - fetch(this.info.api + `/users/@me/guilds/${this.guild.id}/settings/`, { - method: "PATCH", - headers: this.headers, - body: JSON.stringify({ - channel_overrides:{ - [this.id]:{ - message_notifications: noti, - muted:false, - mute_config:{ - selected_time_window:0, - end_time:0 - }, - channel_id:this.id - } - } - }), - }).then(()=>notiselect.hide()); - this.message_notifications = noti; + muted: this.muted, + mute_config: this.mute_config, + channel_id: this.id, }, - ], - ]); - */ + }; + }); notiselect.show(); } - async putmessages(){ + async putmessages() { //TODO swap out with the WS op code - if(this.allthewayup){ + if (this.allthewayup) { return; } - if(this.lastreadmessageid && this.messages.has(this.lastreadmessageid)){ + if (this.lastreadmessageid && this.messages.has(this.lastreadmessageid)) { return; } - const j = await fetch( - this.info.api + "/channels/" + this.id + "/messages?limit=100", - { - headers: this.headers, - } - ); + const j = await fetch(this.info.api + "/channels/" + this.id + "/messages?limit=100", { + headers: this.headers, + }); - const response = await j.json(); - if(response.length !== 100){ + const response = (await j.json()) as messagejson[]; + if (response.length !== 100) { this.allthewayup = true; } let prev: Message | undefined; - for(const thing of response){ + for (const thing of response) { const message = new Message(thing, this); - if(prev){ + if (prev) { this.idToNext.set(message.id, prev.id); this.idToPrev.set(prev.id, message.id); - }else{ + } else { this.lastmessage = message; - this.lastmessageid = message.id; + this.setLastMessageId(message.id); } prev = message; } + if (!response.length) { + this.lastmessageid = undefined; + this.lastreadmessageid = undefined; + } + await this.slowmode(); } - delChannel(json: channeljson){ + delChannel(json: channeljson) { const build: Channel[] = []; - for(const thing of this.children){ - if(thing.id !== json.id){ + for (const thing of this.children) { + if (thing.id !== json.id) { build.push(thing); } } this.children = build; } - async grabAfter(id: string){ - if(id === this.lastmessage?.id){ + afterProm?: Promise; + afterProms = new Map void>(); + async grabAfter(id: string) { + if (this.idToNext.has(id)) { return; } - await fetch( - this.info.api + "/channels/" +this.id +"/messages?limit=100&after=" +id,{ - headers: this.headers, + if (id === this.lastmessage?.id) { + return; + } + if (this.afterProm) return new Promise((res) => this.afterProms.set(id, res)); + let tempy: string | undefined = id; + while (tempy && tempy.includes("fake")) { + tempy = this.idToPrev.get(tempy); + } + if (!tempy) return; + id = tempy; + this.afterProm = new Promise(async (res) => { + const messages = (await ( + await fetch(this.info.api + "/channels/" + this.id + "/messages?limit=100&after=" + id, { + headers: this.headers, + }) + ).json()) as messagejson[]; + let i = 0; + let previd: string = id; + for (const response of messages) { + let messager: Message; + let willbreak = false; + if (this.messages.has(response.id)) { + messager = this.messages.get(response.id) as Message; + willbreak = true; + } else { + messager = new Message(response, this); + } + this.idToPrev.set(messager.id, previd); + this.idToNext.set(previd, messager.id); + + const res = this.afterProms.get(previd); + if (res) { + res(); + this.afterProms.delete(previd); + } + + previd = messager.id; + if (willbreak) { + break; + } + i++; } - ) - .then(j=>{ - return j.json(); - }) - .then(response=>{ - let previd: string = id; - for(const i in response){ - let messager: Message; - let willbreak = false; - if(this.messages.has(response[i].id)){ - messager = this.messages.get(response[i].id) as Message; - willbreak = true; - }else{ - messager = new Message(response[i], this); - } - this.idToPrev.set(messager.id, previd); - this.idToNext.set(previd, messager.id); - previd = messager.id; - if(willbreak){ - break; - } + if (i === 0) { + this.idToNext.set(id, undefined); + } + { + const res = this.afterProms.get(previd); + if (res) { + res(); + this.beforeProms.delete(previd); } - //out.buildmessages(); - }); + } + res(); + this.afterProm = undefined; + if (this.afterProms.size !== 0) { + const [id] = this.afterProms.entries().next().value as [string, () => void]; + this.grabAfter(id); + } + }); + return new Promise((res) => this.afterProms.set(id, res)); + } + async getArround(id: string) { + if (!this.messages.has(id)) { + await this.getmessage(id); + } else { + console.log("have " + id); + } + await Promise.all([this.grabBefore(id), this.grabAfter(id)]); } topid!: string; - async grabBefore(id: string){ - if(this.topid && id === this.topid){ + beforeProm?: Promise; + beforeProms = new Map void>(); + async grabBefore(id: string) { + if (this.beforeProm) return this.beforeProm; + if (this.topid && id === this.topid) { return; } - - await fetch( - this.info.api + "/channels/" + this.id +"/messages?before=" + id + "&limit=100", - { - headers: this.headers, + this.beforeProm = new Promise(async (res) => { + let tempy: string | undefined = id; + while (tempy && tempy.includes("fake")) { + tempy = this.idToPrev.get(tempy); } - ) - .then(j=>{ - return j.json(); - }) - .then((response: messagejson[])=>{ - if(response.length < 100){ - this.allthewayup = true; - if(response.length === 0){ - this.topid = id; - } + if (!tempy) { + const res2 = this.beforeProms.get(id); + res2?.(); + res(); + return; + } + id = tempy; + const messages = (await ( + await fetch( + this.info.api + "/channels/" + this.id + "/messages?before=" + id + "&limit=100", + { + headers: this.headers, + }, + ) + ).json()) as messagejson[]; + let previd = id; + let i = 0; + for (const response of messages) { + let messager: Message; + if (this.messages.has(response.id)) { + messager = this.messages.get(response.id) as Message; + } else { + messager = new Message(response, this); } - let previd = id; - for(const i in response){ - let messager: Message; - let willbreak = false; - if(this.messages.has(response[i].id)){ - console.log("flaky"); - messager = this.messages.get(response[i].id) as Message; - willbreak = true; - }else{ - messager = new Message(response[i], this); - } - this.idToNext.set(messager.id, previd); - this.idToPrev.set(previd, messager.id); - previd = messager.id; + this.idToNext.set(messager.id, previd); + this.idToPrev.set(previd, messager.id); - if(Number(i) === response.length - 1 && response.length < 100){ - this.topid = previd; - } - if(willbreak){ - break; - } + const res = this.beforeProms.get(previd); + if (res) { + res(); + this.beforeProms.delete(previd); } - }); - } - /** - * Please dont use this, its not implemented. - * @deprecated - * @todo - **/ - async grabArround(/* id: string */){ - //currently unused and no plans to use it yet - throw new Error("please don't call this, no one has implemented it :P"); + + previd = messager.id; + + if (i < 99) { + this.topid = previd; + } + + i++; + } + if (i < 100) { + this.allthewayup = true; + if (i === 0) { + this.topid = id; + this.idToPrev.set(id, undefined); + } + } + { + const res = this.beforeProms.get(previd); + if (res) { + res(); + this.beforeProms.delete(previd); + } + } + this.beforeProm = undefined; + res(); + if (this.beforeProms.size !== 0) { + const [id] = this.beforeProms.entries().next().value as [string, () => void]; + this.grabBefore(id); + } + }); + return new Promise((res) => this.beforeProms.set(id, res)); } - async buildmessages(){ + async buildmessages(id: string | void) { this.infinitefocus = false; - this.tryfocusinfinate(); + await this.tryfocusinfinate(id, !!id); } infinitefocus = false; - async tryfocusinfinate(){ - if(this.infinitefocus)return; + async tryfocusinfinate(id: string | void, flash = false) { + if (typeof id === "string" && !this.messages.has(id)) await this.getmessage(id); + if (this.infinitefocus) return; this.infinitefocus = true; - const messages = document.getElementById("channelw") as HTMLDivElement; - const messageContainers = Array.from( - messages.getElementsByClassName("messagecontainer") - ); - for(const thing of messageContainers){ + const messages = document.getElementById("scrollWrap") as HTMLDivElement; + const messageContainers = Array.from(messages.getElementsByClassName("messagecontainer")); + for (const thing of messageContainers) { thing.remove(); } const loading = document.getElementById("loadingdiv") as HTMLDivElement; const removetitle = document.getElementById("removetitle"); //messages.innerHTML=""; - let id: string | undefined; - if(this.lastreadmessageid && this.messages.has(this.lastreadmessageid)){ - id = this.lastreadmessageid; - }else if(this.lastreadmessageid && (id = this.findClosest(this.lastreadmessageid))){ - - }else if(this.lastmessageid && this.messages.has(this.lastmessageid)){ - id = this.goBackIds(this.lastmessageid, 50); + if (!id) { + if (this.lastreadmessageid && this.messages.has(this.lastreadmessageid)) { + id = this.lastreadmessageid; + } else if (this.lastreadmessageid && (id = this.findClosest(this.lastreadmessageid))) { + } else if (this.lastmessageid && this.messages.has(this.lastmessageid)) { + id = this.goBackIds(this.lastmessageid, 50); + } } - if(!id){ - if(!removetitle){ + if (!id) { + if (!removetitle) { const title = document.createElement("h2"); title.id = "removetitle"; - title.textContent = I18n.getTranslation("noMessages"); - title.classList.add("titlespace","messagecontainer"); + title.textContent = I18n.noMessages(); + title.classList.add("titlespace", "messagecontainer"); messages.append(title); } this.infinitefocus = false; loading.classList.remove("loading"); return; - }else if(removetitle){ + } else if (removetitle) { removetitle.remove(); } - if(this.localuser.channelfocus !== this){ + if (this.localuser.channelfocus !== this) { return; } const elements = Array.from(messages.getElementsByClassName("scroller")); - for(const elm of elements){ + for (const elm of elements) { elm.remove(); console.warn("rouge element detected and removed"); } - messages.append(await this.infinite.getDiv(id)); - this.infinite.updatestuff(); - this.infinite.watchForChange().then(async _=>{ + messages.append(await this.infinite.getDiv(id, flash)); + /* + await this.infinite.watchForChange().then(async (_) => { //await new Promise(resolve => setTimeout(resolve, 0)); - this.infinite.focus(id, false); //if someone could figure out how to make this work correctly without this, that's be great :P - loading.classList.remove("loading"); + + await this.infinite.focus(id, falsh); //if someone could figure out how to make this work correctly without this, that's be great :P + + + this.infinite.focus(id, falsh, true); }); + */ + await this.focus(id, flash); + loading.classList.remove("loading"); //this.infinite.focus(id.id,false); } - private goBackIds( - id: string, - back: number, - returnifnotexistant = true - ): string | undefined{ - while(back !== 0){ + private goBackIds(id: string, back: number, returnifnotexistant = true): string | undefined { + while (back !== 0) { const nextid = this.idToPrev.get(id); - if(nextid){ + if (nextid) { id = nextid; back--; - }else{ - if(returnifnotexistant){ + } else { + if (returnifnotexistant) { break; - }else{ + } else { return undefined; } } } return id; } - private findClosest(id: string | undefined){ - if(!this.lastmessageid || !id)return; + private findClosest(id: string | undefined) { + const mTime = (id: string) => { + return this.messages.get(id)?.getTimeStamp() || -1; + }; + if (!this.lastmessageid || !id) return; let flake: string | undefined = this.lastmessageid; - const time = SnowFlake.stringToUnixTime(id); - let flaketime = SnowFlake.stringToUnixTime(flake); - while(flake && time < flaketime){ + const time = mTime(id); + let flaketime = mTime(flake); + while (flake && time < flaketime) { flake = this.idToPrev.get(flake); - if(!flake){ + if (!flake) { return; } - flaketime = SnowFlake.stringToUnixTime(flake); + flaketime = mTime(flake); } return flake; } - updateChannel(json: channeljson){ + nameSpan = new WeakRef(document.createElement("span") as HTMLElement); + updateChannel(json: channeljson) { this.type = json.type; + if (json.flags !== undefined) this.flags = json.flags; this.name = json.name; + this.owner_id = json.owner_id; + this.icon = json.icon; + this.defaultAutoArchiveDuration = json.default_auto_archive_duration; + this.renderIcon(); + this.threadData = json.thread_metadata; + this.rate_limit_per_user = json.rate_limit_per_user || 0; + this.appliedTags = json.applied_tags || this.appliedTags; + this.availableTags = + json.available_tags?.map((tag) => new Tag(tag, this)) || this.availableTags; + this.slowmode(); + + const span = this.nameSpan.deref(); + if (span) span.textContent = this.name; const parent = this.localuser.channelids.get(json.parent_id); - if(parent){ + if (parent) { this.parent = parent; this.parent_id = parent.id; - }else{ + } else { this.parent = undefined; this.parent_id = undefined; } - this.children = []; this.guild_id = json.guild_id; - const oldover=this.permission_overwrites; + const oldover = this.permission_overwrites; this.permission_overwrites = new Map(); - this.permission_overwritesar=[]; - for(const thing of json.permission_overwrites){ - if(thing.id === "1182819038095799904" || thing.id === "1182820803700625444"){ - continue; - } - this.permission_overwrites.set( - thing.id, - new Permissions(thing.allow, thing.deny) - ); + this.permission_overwritesar = []; + for (const thing of json.permission_overwrites || []) { + this.permission_overwrites.set(thing.id, new Permissions(thing.allow, thing.deny)); const permisions = this.permission_overwrites.get(thing.id); - if(permisions){ + if (permisions) { const role = this.guild.roleids.get(thing.id); - if(role){ + if (role) { this.permission_overwritesar.push([role, permisions]); + } else { + this.permission_overwritesar.push([this.localuser.getUser(thing.id), permisions]); } } } - const nchange=[...new Set().union(oldover).difference(this.permission_overwrites)]; - const pchange=[...new Set().union(this.permission_overwrites).difference(oldover)]; - for(const thing of nchange){ - const role=this.guild.roleids.get(thing); - if(role){ - this.croleUpdate(role,new Permissions("0"),false) + const nchange = [...new Set().union(oldover).difference(this.permission_overwrites)]; + const pchange = [...new Set().union(this.permission_overwrites).difference(oldover)]; + for (const thing of nchange) { + const role = this.guild.roleids.get(thing); + if (role) { + this.croleUpdate(role, new Permissions("0"), false); + } else { + const user = this.localuser.getUser(thing); + user.then((_) => { + if (_) this.croleUpdate(_, new Permissions("0"), false); + }); } } - for(const thing of pchange){ - const role=this.guild.roleids.get(thing); - const perms=this.permission_overwrites.get(thing); - if(role&&perms){ - this.croleUpdate(role,perms,true); + for (const thing of pchange) { + const role = this.guild.roleids.get(thing); + const perms = this.permission_overwrites.get(thing); + if (role && perms) { + this.croleUpdate(role, perms, true); + } else if (perms) { + const user = this.localuser.getUser(thing); + user.then((_) => { + if (_) this.croleUpdate(_, perms, true); + }); } } - console.log(pchange,nchange); + console.log(pchange, nchange); this.topic = json.topic; this.nsfw = json.nsfw; + this.fireEvents(); } - croleUpdate:(role:Role,perm:Permissions,added:boolean)=>unknown=()=>{}; - typingstart(){ - if(this.typing > Date.now()){ + croleUpdate: (role: Role | User, perm: Permissions, added: boolean) => unknown = () => {}; + typingstart() { + if (this.typing > Date.now()) { return; } this.typing = Date.now() + 6000; @@ -1282,23 +3286,323 @@ class Channel extends SnowFlake{ headers: this.headers, }); } - get notification(){ + get trueNotiValue() { + const val = this.notification; + if (val === "default") { + switch (Number(this.guild.message_notifications)) { + case 0: + return "all"; + case 1: + return "mentions"; + case 2: + return "none"; + default: + return "mentions"; + } + } + + return val; + } + get notification() { let notinumber: number | null = this.message_notifications; - if(Number(notinumber) === 3){ + if (Number(notinumber) === 3) { notinumber = null; } notinumber ??= this.guild.message_notifications; - console.warn("info:",notinumber); - switch(Number(notinumber)){ + console.warn("info:", notinumber); + switch (Number(notinumber)) { case 0: - return"all"; + return "all"; case 1: - return"mentions"; + return "mentions"; case 2: - return"none"; + return "none"; case 3: default: - return"default"; + return "default"; + } + } + fakeMessages = new WeakMap(); + nonceMap = new Map(); + destroyFakeMessage(id: string) { + const message = this.messages.get(id); + if (!message) return; + message.deleteEvent(); + + const div = this.fakeMessages.get(message); + div?.remove(); + this.fakeMessages.delete(message); + this.messages.delete(id); + + for (const {url} of message.attachments) { + try { + URL.revokeObjectURL(url); + } catch {} + } + } + + async makeFakeMessage( + content: string, + files: filejson[] = [], + reply = undefined, + sticker_ids: string[], + nonce: string, + embeds: embedjson[] = [], + ) { + if (this.nonces.has(nonce)) return; + const m = new Message( + { + author: this.localuser.user.tojson(), + channel_id: this.id, + guild_id: this.guild.id, + id: "fake" + Math.random(), + content: content.trim(), + timestamp: new Date().toISOString(), + edited_timestamp: null, + mentions: [], + mention_roles: [], + mention_everyone: false, + attachments: files, + tts: false, + embeds, + reactions: [], + nonce, + type: 0, + pinned: false, + message_reference: reply, + sticker_items: sticker_ids + .map((_) => { + return Sticker.getFromId(_, this.localuser); + }) + .filter((_) => _ !== undefined), + }, + this, + ); + if (!this.lastmessageid) { + this.topid = m.id; + } + this.nonceMap.set(nonce, m.id); + const prev = this.lastmessage; + const makeRecent = () => { + if (this.lastmessageid) { + this.idToNext.set(this.lastmessageid, m.id); + this.idToPrev.set(m.id, this.lastmessageid); + } + this.lastmessage = m; + this.setLastMessageId(m.id); + }; + makeRecent(); + + const html = m.buildhtml(prev, true); + html.classList.add("messagediv", "loadingMessage"); + this.fakeMessages.set(m, html); + let loadingP = document.createElement("span"); + + const buttons = document.createElement("div"); + buttons.classList.add("flexltr"); + + const retryB = document.createElement("button"); + retryB.textContent = I18n.message.retry(); + + const dont = document.createElement("button"); + dont.textContent = I18n.message.delete(); + dont.onclick = (_) => { + this.fakeMessages.delete(m); + html.remove(); + for (const {url} of m.attachments) { + URL.revokeObjectURL(url); + } + }; + dont.style.marginLeft = "4px"; + buttons.append(retryB, dont); + + if (this === this.localuser.channelfocus) { + if (!this.infinitefocus) { + await this.tryfocusinfinate(); + } + await this.infinite.addedBottom(); + this.focus(m.id, false); + } + + return { + progress: (total: number, sofar: number) => { + if (total < 20000 || sofar === total) { + loadingP.remove(); + return; + } + html.append(loadingP); + loadingP.textContent = File.filesizehuman(sofar) + " / " + File.filesizehuman(total); + }, + failed: (retry: () => void) => { + m.deleteEvent(); + makeRecent(); + loadingP.remove(); + html.append(buttons); + retryB.onclick = () => { + retry(); + html.classList.remove("erroredMessage"); + buttons.remove(); + }; + html.classList.add("erroredMessage"); + }, + void: () => { + m.deleteEvent(); + html.remove(); + }, + }; + } + async uploadFile(files: globalThis.File[]) { + const urls = (await ( + await fetch(this.info.api + "/channels/" + this.id + "/attachments", { + headers: this.headers, + body: JSON.stringify({ + files: files.map((file, index) => { + return { + file_size: file.size, + filename: file.name, + id: index + "", + }; + }), + }), + method: "POST", + }) + ).json()) as { + attachments: { + id: string; + upload_url: string; + upload_filename: string; + original_content_type?: string; + }[]; + }; + Promise.all( + urls.attachments.map(async ({upload_url, id}) => { + return await ( + await fetch(upload_url, { + body: files[+id], + method: "PUT", + }) + ).json(); + }), + ); + return urls.attachments; + } + nonces = new Set(); + lastSentMessage?: Message; + canMessageRightNow() { + if (this.guild.member) { + const member = this.guild.member; + if (member.commuicationDisabledLeft()) return false; + } + const t = this.lastSentMessage?.getTimeStamp(); + if (!t) return true; + if ( + this.hasPermission("BYPASS_SLOWMODE") || + this.hasPermission("MANAGE_MESSAGES") || + this.hasPermission("MANAGE_CHANNELS") + ) + return true; + + if (this.rate_limit_per_user === 0) return true; + + let canMessage = t + this.rate_limit_per_user * 1000; + return canMessage <= Date.now(); + } + async slowmode(bad = false) { + const realbox = document.getElementById("realbox") as HTMLDivElement; + Array.from(realbox.getElementsByClassName("slowmodeTimer")).forEach((_) => _.remove()); + if (this.guild.member) { + const member = this.guild.member; + const left = member.commuicationDisabledLeft(); + if (left) { + const span = document.createElement("span"); + span.classList.add("slowmodeTimer"); + realbox.append(span); + const canMessage = +(member.communication_disabled_until as Date); + const tick = () => { + const timeTill = canMessage - Date.now(); + if (timeTill <= 0 || !document.contains(span)) { + if (document.contains(span)) span.remove(); + clearInterval(int); + return; + } + span.textContent = I18n.channel.TimeOutCool(formatTime(timeTill)); + }; + tick(); + const int = setInterval(tick, 1000); + } + } + function formatTime(timeTill: number) { + let seconds = Math.round(timeTill / 1000); + let minutes = Math.floor(seconds / 60); + seconds -= minutes * 60; + let hours = Math.floor(minutes / 60); + minutes -= hours * 60; + let build = ""; + build = seconds + ""; + if (minutes || hours) { + build = minutes + ":" + build.padStart(2, "0"); + } + if (hours) { + build = hours + ":" + build.padStart(5, "0"); + } + return build; + } + if (!this.rate_limit_per_user) return; + if ( + this.hasPermission("BYPASS_SLOWMODE") || + this.hasPermission("MANAGE_MESSAGES") || + this.hasPermission("MANAGE_CHANNELS") + ) + return; + let m: Message | undefined = this.lastSentMessage || this.lastmessage; + if (!this.lastSentMessage) { + while (m) { + if (m.author.id === this.localuser.user.id) { + this.lastSentMessage = m; + break; + } + m = this.messages.get(this.idToNext.get(m.id) as string); + } + } + + if (!m && bad) { + const q = new URLSearchParams([ + ["author_id", this.localuser.user.id], + ["limit", "1"], + ]); + const { + messages: [message], + } = (await ( + await fetch(this.info.api + "/guilds/" + this.guild.id + "/messages/search/?" + q, { + headers: this.headers, + }) + ).json()) as {messages: messagejson[]}; + m = new Message(message, this); + this.lastSentMessage = m; + } + if (!m) return; + const t = m.getTimeStamp(); + let canMessage = t + this.rate_limit_per_user * 1000; + + if (canMessage <= Date.now()) { + realbox.classList.remove("cantSendMessage"); + return; + } else { + realbox.classList.add("cantSendMessage"); + const span = document.createElement("span"); + span.classList.add("slowmodeTimer"); + realbox.append(span); + const tick = () => { + const timeTill = canMessage - Date.now(); + if (timeTill <= 0 || !document.contains(span)) { + if (document.contains(span)) span.remove(); + clearInterval(int); + return; + } + span.textContent = I18n.channel.SlowmodeCool(formatTime(timeTill)); + }; + tick(); + const int = setInterval(tick, 1000); } } async sendMessage( @@ -1306,179 +3610,361 @@ class Channel extends SnowFlake{ { attachments = [], replyingto = null, - }: { attachments: Blob[]; embeds: embedjson; replyingto: Message | null } - ){ + embeds = [], + sticker_ids = [], + nonce = undefined, + }: { + attachments: Blob[]; + embeds: embedjson[]; + replyingto: Message | null; + sticker_ids: string[]; + nonce?: string; + }, + onRes = (_e: "Ok" | "NotOk") => {}, + ) { + let ressy = (_e: "Ok" | "NotOk") => {}; + let resOnce = false; + if ( + content.trim() === "" && + attachments.length === 0 && + embeds.length == 0 && + sticker_ids.length === 0 + ) { + return; + } let replyjson: any; - if(replyingto){ + if (replyingto) { replyjson = { guild_id: replyingto.guild.id, channel_id: replyingto.channel.id, message_id: replyingto.id, }; } - if(attachments.length === 0){ + + let prom: Promise; + let res: XMLHttpRequest; + let funcs: + | undefined + | { + progress: (total: number, sofar: number) => void; + failed: (restart: () => void) => void; + void: () => void; + }; + const progress = (e: ProgressEvent) => { + funcs?.progress(e.total, e.loaded); + }; + + const fail = () => { + console.warn("failed"); + funcs?.failed(() => { + res.open("POST", this.info.api + "/channels/" + this.id + "/messages"); + res.setRequestHeader("Authorization", this.headers.Authorization); + if (ctype) { + res.setRequestHeader("Content-type", ctype); + } + res.send(rbody); + }); + }; + + const promiseHandler = (resolve: () => void) => { + res.responseType = "json"; + res.onload = () => { + if (res.status !== 200) { + ressy("NotOk"); + onRes("NotOk"); + fail(); + const body = res.response as {code: number}; + if (body.code === 20016) { + this.slowmode(true); + } + return; + } else { + ressy("Ok"); + onRes("Ok"); + if (!resOnce && res?.status) { + resOnce = true; + } + } + resolve(); + }; + }; + + let rbody: string | FormData; + let ctype: string | undefined; + const maybeUpdate = () => { + if ("updatePosition" in this && this.updatePosition instanceof Function) { + console.log("here?"); + this.updatePosition(Date.now()); + } + }; + maybeUpdate(); + ressy = async (e) => { + if (e == "NotOk") { + funcs?.void(); + return; + } + }; + if (attachments.length === 0) { const body = { content, - nonce: Math.floor(Math.random() * 1000000000), + nonce: nonce || Math.floor(Math.random() * 1000000000) + "", message_reference: undefined, + sticker_ids, + embeds, }; - if(replyjson){ + if (replyjson) { body.message_reference = replyjson; } - return await fetch(this.info.api + "/channels/" + this.id + "/messages", { + res = new XMLHttpRequest(); + res.responseType = "json"; + res.upload.onprogress = progress; + res.onerror = fail; + prom = new Promise(promiseHandler); + res.open("POST", this.info.api + "/channels/" + this.id + "/messages"); + res.setRequestHeader("Content-type", (ctype = this.headers["Content-type"])); + res.setRequestHeader("Authorization", this.headers.Authorization); + funcs = await this.makeFakeMessage( + content, + [], + body.message_reference, + sticker_ids, + body.nonce, + embeds, + ); + + try { + res.send((rbody = JSON.stringify(body))); + } catch { + fail(); + } + /* + res = fetch(this.info.api + "/channels/" + this.id + "/messages", { method: "POST", headers: this.headers, body: JSON.stringify(body), }); - }else{ + */ + } else { const formData = new FormData(); const body = { content, - nonce: Math.floor(Math.random() * 1000000000), + nonce: nonce || Math.floor(Math.random() * 1000000000) + "", message_reference: undefined, + sticker_ids, + embeds, }; - if(replyjson){ + if (replyjson) { body.message_reference = replyjson; } formData.append("payload_json", JSON.stringify(body)); - for(const i in attachments){ + for (const i in attachments) { formData.append("files[" + i + "]", attachments[i]); } - return await fetch(this.info.api + "/channels/" + this.id + "/messages", { + + res = new XMLHttpRequest(); + res.responseType = "json"; + res.upload.onprogress = progress; + res.onerror = fail; + prom = new Promise(promiseHandler); + res.open("POST", this.info.api + "/channels/" + this.id + "/messages", true); + + res.setRequestHeader("Authorization", this.headers.Authorization); + + funcs = await this.makeFakeMessage( + content, + attachments.map((_) => ({ + id: "string", + filename: "", + content_type: _.type, + size: _.size, + url: URL.createObjectURL(_), + })), + body.message_reference, + sticker_ids, + body.nonce, + ); + try { + res.send((rbody = formData)); + } catch { + fail(); + } + /* + res = fetch(this.info.api + "/channels/" + this.id + "/messages", { method: "POST", body: formData, - headers: { Authorization: this.headers.Authorization }, + headers: {Authorization: this.headers.Authorization}, }); + */ } + + return prom; + } + unreads() { + if (!this.hasunreads) { + if (this.myhtml) { + this.myhtml.classList.remove("cunread", "mentioned"); + } + } else { + if (this.myhtml) { + this.myhtml.classList.add("cunread"); + } + if (this.mentions !== 0) { + this.myhtml?.classList.add("mentioned"); + } + } + } + async goToBottom() { + if (this.lastmessageid) await this.focus(this.lastmessageid, false); } - messageCreate(messagep: messageCreateJson): void{ - if(!this.hasPermission("VIEW_CHANNEL")){ + async messageCreate(messagep: messageCreateJson): Promise { + if (this.totalMessageSent !== undefined) this.totalMessageSent++; + if (this.messageCount !== undefined) this.messageCount++; + if (!this.hasPermission("VIEW_CHANNEL")) { return; } + if (this.messages.get(messagep.d.id)) { + console.error("Duped message?"); + return; + } + this.nonces.add(messagep.d.nonce); + setTimeout( + () => { + this.nonces.delete(messagep.d.nonce); + }, + 1000 * 60 * 5, + ); + + if (!this.lastmessageid) { + this.topid = messagep.d.id; + } const messagez = new Message(messagep.d, this); + Member.resolveMember(messagez.author, this.guild).then((_) => { + this.typingmap.delete(_ as Member); + this.rendertyping(); + }); this.lastmessage = messagez; - if(this.lastmessageid){ + if (this.lastmessageid && this.lastmessageid !== messagez.id) { this.idToNext.set(this.lastmessageid, messagez.id); this.idToPrev.set(messagez.id, this.lastmessageid); + } else { + console.error("something bad happened"); } + if ( + (messagez.mentionsuser(this.localuser.user) || this.guild.id === "@me") && + messagez.author !== this.localuser.user + ) { + this.mentions++; + } + this.setLastMessageId(messagez.id); - this.lastmessageid = messagez.id; - - if(messagez.author === this.localuser.user){ + if (this.infinite.atBottom()) { this.lastreadmessageid = messagez.id; - if(this.myhtml){ - this.myhtml.classList.remove("cunread"); - } - }else{ - if(this.myhtml){ - this.myhtml.classList.add("cunread"); - } } + + this.unreads(); this.guild.unreads(); - if(this === this.localuser.channelfocus){ - if(!this.infinitefocus){ - this.tryfocusinfinate(); + if (this === this.localuser.channelfocus) { + if (!this.infinitefocus) { + await this.tryfocusinfinate(); } - this.infinite.addedBottom(); + await this.infinite.addedBottom(); } - if(messagez.author === this.localuser.user){ - return; + + if (messagez.author === this.localuser.user) { + this.lastSentMessage = messagez; + this.slowmode(); + this.mentions = 0; + this.unreads(); + this.guild.unreads(); + if (this == this.localuser.channelfocus) { + setTimeout(() => this.goToBottom()); + } } - if( - this.localuser.lookingguild?.prevchannel === this && document.hasFocus() - ){ + this.fireEvents(); + + if (messagez.author === this.localuser.user) { return; } - if(this.notification === "all"){ - this.notify(messagez); - }else if( - this.notification === "mentions" && messagez.mentionsuser(this.localuser.user) - ){ - this.notify(messagez); + if (this.localuser.lookingguild?.prevchannel === this && document.hasFocus()) { + return; } + + this.notify(messagez); } - notititle(message: Message): string{ - return( - message.author.username + " > " + this.guild.properties.name + " > " + this.name - ); + notititle(message: Message): string { + return message.author.username + " > " + this.guild.properties.name + " > " + this.name; } - notify(message: Message, deep = 0){ - AVoice.noises(AVoice.getNotificationSound()); - if(!("Notification" in window)){ - }else if(Notification.permission === "granted"){ - let noticontent: string | undefined | null = message.content.textContent; - if(message.embeds[0]){ - noticontent ||= message.embeds[0]?.json.title; - noticontent ||= message.content.textContent; - } - noticontent ||= I18n.getTranslation("blankMessage"); - let imgurl: null | string = null; - const images = message.getimages(); - if(images.length){ - const image = images[0]; - if(image.proxy_url){ - imgurl ||= image.proxy_url; - } - imgurl ||= image.url; - } - const notification = new Notification(this.notititle(message), { - body: noticontent, - icon: message.author.getpfpsrc(), - image: imgurl, - }); - notification.addEventListener("click", _=>{ - window.focus(); - this.getHTML(); - }); - }else if(Notification.permission !== "denied"){ - Notification.requestPermission().then(()=>{ - if(deep === 3){ + notify(message: Message, deep = 0) { + if (this.muted) return; + + if (this.localuser.status === "dnd") return; + + if (this.guild.muted) { + return; + } + if (this.trueNotiValue === "none") { + return; + } else if (this.notification === "mentions" && !message.mentionsuser(this.localuser.user)) { + return; + } + if (message.author.relationshipType == 2) { + return; + } + + if (this.localuser.play) { + this.localuser.playSound(); + } else { + console.warn("no play 3:"); + } + if ("Notification" in window && Notification.permission === "granted") { + NotificationHandler.sendMessageNotification(message); + } else if (Notification.permission !== "denied") { + Notification.requestPermission().then(() => { + if (deep === 3) { return; } this.notify(message, deep + 1); }); } } - async addRoleToPerms(role: Role){ - await fetch( - this.info.api + "/channels/" + this.id + "/permissions/" + role.id, - { - method: "PUT", - headers: this.headers, - body: JSON.stringify({ - allow: "0", - deny: "0", - id: role.id, - type: 0, - }), - } - ); + voiceMode: "VoiceOnly" | "ChatAndVoice" = "VoiceOnly"; + async addRoleToPerms(role: Role | User) { + await fetch(this.info.api + "/channels/" + this.id + "/permissions/" + role.id, { + method: "PUT", + headers: this.headers, + body: JSON.stringify({ + allow: "0", + deny: "0", + id: role.id, + type: role instanceof User ? 1 : 0, + }), + }); const perm = new Permissions("0", "0"); this.permission_overwrites.set(role.id, perm); - this.permission_overwritesar.push([role, perm]); + this.permission_overwritesar.push([ + role instanceof User ? new Promise((res) => res(role)) : role, + perm, + ]); } - async updateRolePermissions(id: string, perms: Permissions){ + async updateRolePermissions(id: string, perms: Permissions) { const permission = this.permission_overwrites.get(id); - if(permission){ + if (permission) { permission.allow = perms.allow; permission.deny = perms.deny; - }else{ + } else { //this.permission_overwrites.set(id,perms); } - await fetch( - this.info.api + "/channels/" + this.id + "/permissions/" + id, - { - method: "PUT", - headers: this.headers, - body: JSON.stringify({ - allow: perms.allow.toString(), - deny: perms.deny.toString(), - id, - type: 0, - }), - } - ); + await fetch(this.info.api + "/channels/" + this.id + "/permissions/" + id, { + method: "PUT", + headers: this.headers, + body: JSON.stringify({ + allow: perms.allow.toString(), + deny: perms.deny.toString(), + id, + type: this.localuser.userMap.get(id) ? 1 : 0, + }), + }); } } Channel.setupcontextmenu(); -export{ Channel }; +export {Channel}; diff --git a/src/webpage/contextmenu.ts b/src/webpage/contextmenu.ts index 8d6eac38..c21d5106 100644 --- a/src/webpage/contextmenu.ts +++ b/src/webpage/contextmenu.ts @@ -1,150 +1,471 @@ -import{ iOS }from"./login.js"; -class Contextmenu{ - static currentmenu: HTMLElement | ""; +import {mobile, removeAni} from "./utils/utils.js"; +type iconJson = + | { + src: string; + } + | { + css: string; + } + | { + html: HTMLElement; + }; + +interface menuPart { + group?: string; + makeContextHTML( + obj1: x, + obj2: y, + menu: HTMLDivElement, + layered: contextCluster[], + processed: WeakSet>, + ): void; +} + +class ContextButton implements menuPart { + private text: string | ((this: x, arg: y) => string); + private onClick: (this: x, arg: y, e: MouseEvent) => void; + private icon?: iconJson; + private visible?: (this: x, arg: y) => boolean; + private enabled?: (this: x, arg: y) => boolean; + //TODO there *will* be more colors + private color?: "red" | "blue"; + group?: string; + constructor( + text: ContextButton["text"], + onClick: ContextButton["onClick"], + addProps: { + icon?: iconJson; + visible?: (this: x, arg: y) => boolean; + enabled?: (this: x, arg: y) => boolean; + color?: "red" | "blue"; + group?: string; + } = {}, + ) { + this.text = text; + this.onClick = onClick; + this.icon = addProps.icon; + this.visible = addProps.visible; + this.enabled = addProps.enabled; + this.color = addProps.color; + this.group = addProps.group; + } + isVisible(obj1: x, obj2: y): boolean { + if (!this.visible) return true; + return this.visible.call(obj1, obj2); + } + makeContextHTML(obj1: x, obj2: y, menu: HTMLDivElement) { + if (!this.isVisible(obj1, obj2)) { + return; + } + + const intext = document.createElement("button"); + intext.classList.add("contextbutton"); + intext.append(this.textContent(obj1, obj2)); + + intext.disabled = !!this.enabled && !this.enabled.call(obj1, obj2); + + if (this.icon) { + if ("src" in this.icon) { + const icon = document.createElement("img"); + icon.classList.add("svgicon"); + icon.src = this.icon.src; + intext.append(icon); + } else if ("css" in this.icon) { + const icon = document.createElement("span"); + icon.classList.add(this.icon.css, "svgicon"); + switch (this.color) { + case "red": + icon.style.background = "var(--red)"; + break; + case "blue": + icon.style.background = "var(--blue)"; + break; + } + intext.append(icon); + } else { + intext.append(this.icon.html); + } + } + + switch (this.color) { + case "red": + intext.style.color = "var(--red)"; + break; + case "blue": + intext.style.color = "var(--blue)"; + break; + } + + intext.onclick = (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + removeAni(menu); + this.onClick.call(obj1, obj2, e); + }; + + menu.append(intext); + } + textContent(x: x, y: y) { + if (this.text instanceof Function) { + return this.text.call(x, y); + } + return this.text; + } +} +class ContextGroup implements menuPart { + private visible?: (this: x, arg: y) => boolean; + groupSel: string; + group = undefined; + constructor( + group: string, + addProps: { + visible?: (this: x, arg: y) => boolean; + } = {}, + ) { + this.visible = addProps.visible; + + this.groupSel = group; + } + isVisible(obj1: x, obj2: y): boolean { + if (!this.visible) return true; + return this.visible.call(obj1, obj2); + } + makeContextHTML( + x: x, + y: y, + menuHtml: HTMLDivElement, + layered: contextCluster[], + processed: WeakSet>, + ) { + if (!this.isVisible(x, y)) { + return; + } + for (const [menu, x, y] of layered) { + for (const part of menu.buttons) { + if (part.group === this.groupSel && !processed.has(part)) { + processed.add(part); + part.makeContextHTML(x, y, menuHtml, [], processed); + } + } + } + } +} +class Seperator implements menuPart { + private visible?: (obj1: x, obj2: y) => boolean; + group?: string; + constructor(visible?: (obj1: x, obj2: y) => boolean, group?: string) { + this.visible = visible; + this.group = group; + } + makeContextHTML(obj1: x, obj2: y, menu: HTMLDivElement): void { + if (!this.visible || this.visible(obj1, obj2)) { + if (menu.children[menu.children.length - 1].tagName === "HR") { + return; + } + menu.append(document.createElement("hr")); + } + } +} + +class ContextMenuText implements menuPart { + private visible?: (obj1: x, obj2: y) => boolean; + group?: string; + text: string; + constructor(text: string, visible?: (obj1: x, obj2: y) => boolean, group?: string) { + this.visible = visible; + this.group = group; + this.text = text; + } + makeContextHTML(obj1: x, obj2: y, menu: HTMLDivElement): void { + if (!this.visible || this.visible(obj1, obj2)) { + const span = document.createElement("span"); + span.textContent = this.text; + menu.append(span); + } + } +} +class ContextMenuSlider implements menuPart { + private visible?: (obj1: x, obj2: y) => boolean; + group?: string; + text: ContextButton["text"]; + slider: (obj1: x, obj2: y, slide: number) => unknown; + startVal?: (obj1: x, obj2: y) => number; + constructor( + text: ContextButton["text"], + slider: (obj1: x, obj2: y, slide: number) => unknown, + visible?: (obj1: x, obj2: y) => boolean, + group?: string, + {startVal}: {startVal?: (obj1: x, obj2: y) => number} = {}, + ) { + this.visible = visible; + this.group = group; + this.text = text; + this.slider = slider; + this.startVal = startVal; + } + makeContextHTML(obj1: x, obj2: y, menu: HTMLDivElement): void { + if (!this.visible || this.visible(obj1, obj2)) { + const sliderDiv = document.createElement("div"); + sliderDiv.classList.add("flexttb"); + const span = document.createElement("span"); + span.textContent = typeof this.text == "string" ? this.text : this.text.call(obj1, obj2); + sliderDiv.append(span); + + const slider = document.createElement("input"); + slider.type = "range"; + sliderDiv.append(slider); + slider.value = this.startVal?.(obj1, obj2) + "" || "100"; + slider.oninput = () => { + this.slider(obj1, obj2, +slider.value); + }; + menu.append(sliderDiv); + } + } +} + +declare global { + interface HTMLElementEventMap { + layered: LayeredEvent; + } +} +type contextCluster = [Contextmenu, X, Y]; +class LayeredEvent extends CustomEvent { + menus: contextCluster[]; + primary?: contextCluster; + constructor(mouse: MouseEvent, menus: LayeredEvent["menus"]) { + super("layered", {bubbles: true}); + this.menus = menus; + queueMicrotask(() => { + console.log(this); + const pop = this.primary || menus.pop(); + if (!pop) return; + const [menu, addinfo, other] = pop; + menu.makemenu(mouse.clientX, mouse.clientY, addinfo, other, undefined, menus); + }); + } +} + +class Contextmenu { + static currentmenu: HTMLElement | "" = ""; + static prevmenus: HTMLElement[] = []; name: string; - buttons: [ - string|(()=>string), - (this: x, arg: y, e: MouseEvent) => void, - string | null, - (this: x, arg: y) => boolean, - (this: x, arg: y) => boolean, - string - ][]; + buttons: menuPart[]; div!: HTMLDivElement; - static setup(){ - Contextmenu.currentmenu = ""; - document.addEventListener("click", event=>{ - if(Contextmenu.currentmenu === ""){ - return; + static declareMenu(html: HTMLElement | false = false, keep: false | true | HTMLElement = false) { + if (Contextmenu.currentmenu !== "") { + if (keep === false) { + removeAni(Contextmenu.currentmenu); + } else if (keep === true) { + this.prevmenus.push(Contextmenu.currentmenu); + } else { + while (Contextmenu.currentmenu && Contextmenu.currentmenu !== keep) { + removeAni(Contextmenu.currentmenu); + Contextmenu.currentmenu = this.prevmenus.pop() || ""; + } + if (Contextmenu.currentmenu) { + this.prevmenus.push(Contextmenu.currentmenu); + } } - if(!Contextmenu.currentmenu.contains(event.target as Node)){ - Contextmenu.currentmenu.remove(); - Contextmenu.currentmenu = ""; + } + if (html) { + Contextmenu.currentmenu = html; + } else { + Contextmenu.currentmenu = this.prevmenus.pop() || ""; + } + } + static setup() { + Contextmenu.declareMenu(); + document.addEventListener("click", (event) => { + while (Contextmenu.currentmenu && !Contextmenu.currentmenu.contains(event.target as Node)) { + Contextmenu.declareMenu(); } }); } - constructor(name: string){ + private layered = false; + constructor(name: string, layered = false) { this.name = name; + this.layered = layered; this.buttons = []; } - addbutton( - text: string|(()=>string), - onclick: (this: x, arg: y, e: MouseEvent) => void, - img: null | string = null, - shown: (this: x, arg: y) => boolean = _=>true, - enabled: (this: x, arg: y) => boolean = _=>true - ){ - this.buttons.push([text, onclick, img, shown, enabled, "button"]); - return{}; - } - addsubmenu( - text: string|(()=>string), - onclick: (this: x, arg: y, e: MouseEvent) => void, - img = null, - shown: (this: x, arg: y) => boolean = _=>true, - enabled: (this: x, arg: y) => boolean = _=>true - ){ - this.buttons.push([text, onclick, img, shown, enabled, "submenu"]); - return{}; - } - private makemenu(x: number, y: number, addinfo: x, other: y){ + + addButton( + text: ContextButton["text"], + onClick: ContextButton["onClick"], + addProps: { + icon?: iconJson; + visible?: (this: x, arg: y) => boolean; + enabled?: (this: x, arg: y) => boolean; + color?: "red" | "blue"; + group?: string; + } = {}, + ) { + const button = new ContextButton(text, onClick, addProps); + this.buttons.push(button); + return button; + } + addSeperator(visible?: (obj1: x, obj2: y) => boolean, group?: string) { + this.buttons.push(new Seperator(visible, group)); + } + addText(text: string, visible?: (obj1: x, obj2: y) => boolean, group?: string) { + this.buttons.push(new ContextMenuText(text, visible, group)); + } + addSlider( + text: ContextButton["text"], + slider: (obj1: x, obj2: y, val: number) => unknown, + visible?: (obj1: x, obj2: y) => boolean, + group?: string, + opts: {startVal?: (obj1: x, obj2: y) => number} = {}, + ) { + this.buttons.push(new ContextMenuSlider(text, slider, visible, group, opts)); + } + addGroup( + group: string, + addprops?: { + visible?: (this: x, arg: y) => boolean; + }, + ) { + this.buttons.push(new ContextGroup(group, addprops)); + } + makemenu( + x: number, + y: number, + addinfo: x, + other: y, + keep: boolean | HTMLElement = false, + layered: LayeredEvent["menus"] = [], + ) { const div = document.createElement("div"); div.classList.add("contextmenu", "flexttb"); + const processed = new WeakSet>(); - let visibleButtons = 0; - for(const thing of this.buttons){ - if(!thing[3].call(addinfo, other))continue; - visibleButtons++; - - const intext = document.createElement("button"); - intext.disabled = !thing[4].call(addinfo, other); - intext.classList.add("contextbutton"); - if(thing[0] instanceof Function){ - intext.textContent = thing[0](); - }else{ - intext.textContent = thing[0]; - } - console.log(thing); - if(thing[5] === "button" || thing[5] === "submenu"){ - intext.onclick = (e)=>{ - div.remove(); - thing[1].call(addinfo, other,e) - }; - } + for (const button of this.buttons) { + button.makeContextHTML(addinfo, other, div, layered, processed); + } + if (div.children[div.children.length - 1]?.tagName !== "HR") { + div.append(document.createElement("hr")); + } + new ContextGroup("default").makeContextHTML(addinfo, other, div, layered, processed); - div.appendChild(intext); + while (div.children[div.children.length - 1]?.tagName === "HR") { + div.children[div.children.length - 1].remove(); } - if(visibleButtons == 0)return; + if (div.childNodes.length === 0) return; - if(Contextmenu.currentmenu != ""){ - Contextmenu.currentmenu.remove(); + Contextmenu.declareMenu(div, keep); + + if (y > 0) { + div.style.top = y + "px"; + } else { + div.style.bottom = y * -1 + "px"; + } + if (x > 0) { + div.style.left = x + "px"; + } else { + div.style.right = x * -1 + "px"; } - div.style.top = y + "px"; - div.style.left = x + "px"; + document.body.appendChild(div); Contextmenu.keepOnScreen(div); - console.log(div); - Contextmenu.currentmenu = div; + return this.div; } - bindContextmenu(obj: HTMLElement, addinfo: x, other: y,touchDrag:(x:number,y:number)=>unknown=()=>{},touchEnd:(x:number,y:number)=>unknown=()=>{}){ - const func = (event: MouseEvent)=>{ - event.preventDefault(); + bindContextmenu( + obj: HTMLElement, + addinfo: x, + other: y, + touchDrag: (x: number, y: number) => unknown = () => {}, + touchEnd: (x: number, y: number) => unknown = () => {}, + click: "right" | "left" = "right", + ) { + const func = (event: MouseEvent) => { + const selectedText = window.getSelection(); + if (selectedText) { + //Don't override context menus for highlighted text + for (let ranges = 0; ranges < selectedText.rangeCount; ranges++) { + const range = selectedText.getRangeAt(ranges); + const rect = range.getBoundingClientRect(); + if ( + rect.left < event.clientX && + rect.right > event.clientX && + rect.top < event.clientY && + rect.bottom > event.clientY + ) { + return; + } + } + } event.stopImmediatePropagation(); - this.makemenu(event.clientX, event.clientY, addinfo, other); + event.preventDefault(); + const layered = new LayeredEvent(event, []); + obj.dispatchEvent(layered); }; - obj.addEventListener("contextmenu", func); - if(iOS){ - let hold:NodeJS.Timeout|undefined; - let x!:number; - let y!:number; - obj.addEventListener("touchstart",(event: TouchEvent)=>{ - x=event.touches[0].pageX; - y=event.touches[0].pageY; - if(event.touches.length > 1){ - event.preventDefault(); - event.stopImmediatePropagation(); - this.makemenu(event.touches[0].clientX, event.touches[0].clientY, addinfo, other); - }else{ - // - event.stopImmediatePropagation(); - hold=setTimeout(()=>{ - if(lastx**2+lasty**2>10**2) return; + obj.addEventListener("layered", (layered) => { + if (this.layered) { + layered.menus.push([this as Contextmenu, addinfo, other]); + } else if (!layered.primary) { + layered.primary = [this as Contextmenu, addinfo, other]; + } + return; + }); + if (click === "right") { + obj.addEventListener("contextmenu", func); + } else { + obj.addEventListener("click", func); + } + //NOTE not sure if this code is correct, seems fine at least for now + if (mobile) { + let hold: NodeJS.Timeout | undefined; + let x!: number; + let y!: number; + obj.addEventListener( + "touchstart", + (event: TouchEvent) => { + x = event.touches[0].pageX; + y = event.touches[0].pageY; + if (event.touches.length > 1) { + event.preventDefault(); + event.stopImmediatePropagation(); this.makemenu(event.touches[0].clientX, event.touches[0].clientY, addinfo, other); - console.log(obj); - },500) - } - },{passive: false}); - let lastx=0; - let lasty=0; - obj.addEventListener("touchend",()=>{ - if(hold){ + } else { + // + event.stopImmediatePropagation(); + hold = setTimeout(() => { + if (lastx ** 2 + lasty ** 2 > 10 ** 2) return; + this.makemenu(event.touches[0].clientX, event.touches[0].clientY, addinfo, other); + console.log(obj); + }, 500); + } + }, + {passive: false}, + ); + let lastx = 0; + let lasty = 0; + obj.addEventListener("touchend", () => { + if (hold) { clearTimeout(hold); } - touchEnd(lastx,lasty); + touchEnd(lastx, lasty); }); - obj.addEventListener("touchmove",(event)=>{ - lastx=event.touches[0].pageX-x; - lasty=event.touches[0].pageY-y; - touchDrag(lastx,lasty); + obj.addEventListener("touchmove", (event) => { + lastx = event.touches[0].pageX - x; + lasty = event.touches[0].pageY - y; + touchDrag(lastx, lasty); }); } return func; } - static keepOnScreen(obj: HTMLElement){ + static keepOnScreen(obj: HTMLElement) { const html = document.documentElement.getBoundingClientRect(); - const docheight = html.height; + const docheight = window.innerHeight; const docwidth = html.width; const box = obj.getBoundingClientRect(); - console.log(box, docheight, docwidth); - if(box.right > docwidth){ - console.log("test"); - obj.style.left = docwidth - box.width + "px"; + if (box.right > docwidth) { + obj.style.left = Math.floor(docwidth - box.width) + "px"; } - if(box.bottom > docheight){ - obj.style.top = docheight - box.height + "px"; + if (box.bottom > docheight) { + obj.style.top = Math.floor(docheight - box.height) + "px"; } } } Contextmenu.setup(); -export{ Contextmenu }; +export {Contextmenu}; diff --git a/src/webpage/direct.ts b/src/webpage/direct.ts index d3c03244..8c3c3c46 100644 --- a/src/webpage/direct.ts +++ b/src/webpage/direct.ts @@ -1,301 +1,371 @@ -import{ Guild }from"./guild.js"; -import{ Channel }from"./channel.js"; -import{ Message }from"./message.js"; -import{ Localuser }from"./localuser.js"; -import{ User }from"./user.js"; -import{channeljson,dirrectjson,memberjson,messagejson}from"./jsontypes.js"; -import{ Permissions }from"./permissions.js"; -import{ SnowFlake }from"./snowflake.js"; -import{ Contextmenu }from"./contextmenu.js"; -import { I18n } from "./i18n.js"; -import { Float, FormError } from "./settings.js"; - -class Direct extends Guild{ - declare channelids: { [key: string]: Group }; +import {Guild} from "./guild.js"; +import {Channel} from "./channel.js"; +import {Message} from "./message.js"; +import {Localuser} from "./localuser.js"; +import {User} from "./user.js"; +import {channeljson, dirrectjson, memberjson, messageCreateJson, readyjson} from "./jsontypes.js"; +import {Permissions} from "./permissions.js"; +import {SnowFlake} from "./snowflake.js"; +import {Contextmenu} from "./contextmenu.js"; +import {I18n} from "./i18n.js"; +import {Dialog, Float, FormError} from "./settings.js"; +import {Discovery} from "./discovery.js"; +import {createImg} from "./utils/utils.js"; +import {CDNParams} from "./utils/cdnParams.js"; + +class Direct extends Guild { channels: Group[]; - getUnixTime(): number{ + getUnixTime(): number { throw new Error("Do not call this for Direct, it does not make sense"); } - constructor(json: dirrectjson[], owner: Localuser){ + discovery: Discovery; + constructor(json: dirrectjson[], owner: Localuser) { super(-1, owner, null); this.message_notifications = 0; - this.owner = owner; - this.headers = this.localuser.headers; this.channels = []; - this.channelids = {}; - // @ts-ignore + // @ts-ignore it's a hack, but it's a hack that works this.properties = {}; this.roles = []; this.roleids = new Map(); this.prevchannel = undefined; - this.properties.name = I18n.getTranslation("DMs.name"); - for(const thing of json){ + this.properties.name = I18n.DMs.name(); + for (const thing of json) { const temp = new Group(thing, this); this.channels.push(temp); - this.channelids[temp.id] = temp; this.localuser.channelids.set(temp.id, temp); } this.headchannels = this.channels; + this.discovery = new Discovery(this); } - createChannelpac(json: any){ + createChannelpac(json: any) { const thischannel = new Group(json, this); - this.channelids[thischannel.id] = thischannel; this.channels.push(thischannel); + this.localuser.channelids.set(thischannel.id, thischannel); this.sortchannels(); this.printServers(); return thischannel; } - delChannel(json: channeljson){ - const channel = this.channelids[json.id]; + delChannel(json: channeljson) { + const channel = this.localuser.channelids.get(json.id) as Group; super.delChannel(json); - if(channel){ + if (channel) { channel.del(); } } - getHTML(){ - const ddiv=document.createElement("div"); - const build=super.getHTML(); - const freindDiv=document.createElement("div"); - freindDiv.classList.add("liststyle","flexltr","friendsbutton"); + freindDiv?: HTMLDivElement; + getHTML() { + const voiceArea = document.getElementById("voiceArea") as HTMLElement; + voiceArea.innerHTML = ""; + const sideContainDiv = document.getElementById("sideContainDiv"); + if (sideContainDiv) { + sideContainDiv.classList.remove("searchDiv"); + sideContainDiv.classList.remove("hideSearchDiv"); + } + const searchBox = document.getElementById("searchBox"); + if (searchBox) searchBox.textContent = ""; - const icon=document.createElement("span"); - icon.classList.add("svgicon","svg-friends","space"); + const ddiv = document.createElement("div"); + const build = super.getHTML(); + const freindDiv = document.createElement("div"); + freindDiv.classList.add("liststyle", "flexltr", "friendsbutton"); + + const icon = document.createElement("span"); + icon.classList.add("svgicon", "svg-friends", "space"); freindDiv.append(icon); + this.freindDiv = freindDiv; - freindDiv.append(I18n.getTranslation("friends.friends")); - ddiv.append(freindDiv); - freindDiv.onclick=()=>{ + freindDiv.append(I18n.friends.friends()); + freindDiv.onclick = () => { this.loadChannel(null); - } + }; + + const newDm = document.createElement("div"); + newDm.classList.add("flexltr", "dmline"); + newDm.onclick = () => this.makeGroup(); + + const span = document.createElement("span"); + span.classList.add("svg-plus", "svgicon", "addchannel"); - ddiv.append(build); + newDm.append(I18n.dms(), span); + + ddiv.append(freindDiv, newDm, build); return ddiv; } - noChannel(addstate:boolean){ - if(addstate){ - history.pushState([this.id,undefined], "", "/channels/" + this.id); + async loadChannel(id?: string | null | undefined, addstate = true, message?: string) { + if (id === "discover") { + this.removePrevChannel(); + this.discovery.makeMenu(); + return; + } + await super.loadChannel(id, addstate, message); + } + async makeGroup() { + const dio = new Dialog(I18n.group.select()); + const opt = dio.options; + dio.show(); + const invited = await User.makeSelector( + opt, + I18n.group.createdm(), + [...this.localuser.inrelation].filter((_) => _.relationshipType === 1), + ); + dio.hide(); + if (invited && invited.size !== 0) { + const {id}: {id: string} = await ( + await fetch(this.localuser.info.api + "/users/@me/channels", { + method: "POST", + headers: this.headers, + body: JSON.stringify({ + recipients: [...invited].map((_) => _.id), + }), + }) + ).json(); + this.localuser.goToChannel(id); + dio.hide(); + } + } + noChannel(addstate: boolean) { + if (addstate) { + history.pushState([this.id, undefined], "", "/channels/" + this.id); + } + if (this.freindDiv) { + this.freindDiv.classList.add("viewChannel"); } - this.localuser.pageTitle(I18n.getTranslation("friends.friendlist")); + this.localuser.pageTitle(I18n.friends.friendlist()); const channelTopic = document.getElementById("channelTopic") as HTMLSpanElement; channelTopic.removeAttribute("hidden"); - channelTopic.textContent=""; + channelTopic.textContent = ""; + channelTopic.onclick = () => {}; const loading = document.getElementById("loadingdiv") as HTMLDivElement; loading.classList.remove("loading"); this.localuser.getSidePannel(); - const messages = document.getElementById("channelw") as HTMLDivElement; - for(const thing of Array.from(messages.getElementsByClassName("messagecontainer"))){ + const messages = document.getElementById("scrollWrap") as HTMLDivElement; + for (const thing of Array.from(messages.getElementsByClassName("messagecontainer"))) { thing.remove(); } - const container=document.createElement("div"); - container.classList.add("messagecontainer","flexttb","friendcontainer") + const container = document.createElement("div"); + container.classList.add("messagecontainer", "flexttb", "friendcontainer"); messages.append(container); - const checkVoid=()=>{ - if(this.localuser.channelfocus!==undefined||this.localuser.lookingguild!==this){ - this.localuser.relationshipsUpdate=()=>{}; + const checkVoid = () => { + if (this.localuser.channelfocus !== undefined || this.localuser.lookingguild !== this) { + this.localuser.relationshipsUpdate = () => {}; } - } - function genuserstrip(user:User,icons:HTMLElement):HTMLElement{ - const div=document.createElement("div"); - div.classList.add("flexltr","liststyle"); + }; + function genuserstrip(user: User, icons: HTMLElement): HTMLElement { + const div = document.createElement("div"); + div.classList.add("flexltr", "liststyle"); user.bind(div); - div.append(user.buildpfp()); + div.append(user.buildpfp(undefined, div)); - const userinfos=document.createElement("div"); + const userinfos = document.createElement("div"); userinfos.classList.add("flexttb"); - const username=document.createElement("span"); - username.textContent=user.name; - userinfos.append(username,user.getStatus()); + const username = document.createElement("span"); + username.textContent = user.name; + user.subName(username); + userinfos.append(username, user.getStatus()); div.append(userinfos); - User.contextmenu.bindContextmenu(div,user,undefined); - userinfos.style.flexGrow="1"; + User.contextmenu.bindContextmenu(div, user, undefined); + userinfos.style.flexGrow = "1"; div.append(icons); return div; } { - //TODO update on users coming online - const online=document.createElement("button"); - online.textContent=I18n.getTranslation("friends.online"); + const online = document.createElement("button"); + online.textContent = I18n.friends.online(); channelTopic.append(online); - const genOnline=()=>{ - this.localuser.relationshipsUpdate=genOnline; + const genOnline = () => { + this.localuser.relationshipsUpdate = genOnline; checkVoid(); - container.innerHTML=""; - container.append(I18n.getTranslation("friends.online:")); - for(const user of this.localuser.inrelation){ - if(user.relationshipType===1&&user.online){ - const buttonc=document.createElement("div"); - const button1=document.createElement("span"); - button1.classList.add("svg-frmessage","svgicon"); + container.innerHTML = ""; + container.append(I18n.friends["online:"]()); + for (const user of this.localuser.inrelation) { + if (user.relationshipType === 1 && user.online) { + const buttonc = document.createElement("div"); + const button1 = document.createElement("span"); + button1.classList.add("svg-frmessage", "svgicon"); buttonc.append(button1); buttonc.classList.add("friendlyButton"); - buttonc.onclick=(e)=>{ + buttonc.onclick = (e) => { e.stopImmediatePropagation(); user.opendm(); - } - container.append(genuserstrip(user,buttonc)); + }; + container.append(genuserstrip(user, buttonc)); } } - } - online.onclick=genOnline; + }; + online.onclick = genOnline; genOnline(); } { - const all=document.createElement("button"); - all.textContent=I18n.getTranslation("friends.all"); - const genAll=()=>{ - this.localuser.relationshipsUpdate=genAll; + const all = document.createElement("button"); + all.textContent = I18n.friends.all(); + const genAll = () => { + this.localuser.relationshipsUpdate = genAll; checkVoid(); - container.innerHTML=""; - container.append(I18n.getTranslation("friends.all:")); - for(const user of this.localuser.inrelation){ - if(user.relationshipType===1){ - const buttonc=document.createElement("div"); - const button1=document.createElement("span"); - button1.classList.add("svg-frmessage","svgicon"); + container.innerHTML = ""; + container.append(I18n.friends["all:"]()); + for (const user of this.localuser.inrelation) { + if (user.relationshipType === 1) { + const buttonc = document.createElement("div"); + const button1 = document.createElement("span"); + button1.classList.add("svg-frmessage", "svgicon"); buttonc.append(button1); buttonc.classList.add("friendlyButton"); - buttonc.onclick=(e)=>{ + buttonc.onclick = (e) => { e.stopImmediatePropagation(); user.opendm(); - } - container.append(genuserstrip(user,buttonc)); + }; + container.append(genuserstrip(user, buttonc)); } } - } - all.onclick=genAll; + }; + all.onclick = genAll; channelTopic.append(all); } { - const pending=document.createElement("button"); - pending.textContent=I18n.getTranslation("friends.pending"); - const genPending=()=>{ - this.localuser.relationshipsUpdate=genPending; + const pending = document.createElement("button"); + pending.textContent = I18n.friends.pending(); + const genPending = () => { + this.localuser.relationshipsUpdate = genPending; checkVoid(); - container.innerHTML=""; - container.append(I18n.getTranslation("friends.pending:")); - for(const user of this.localuser.inrelation){ - if(user.relationshipType===3||user.relationshipType===4){ - const buttons=document.createElement("div"); + container.innerHTML = ""; + container.append(I18n.friends["pending:"]()); + for (const user of this.localuser.inrelation) { + if (user.relationshipType === 3 || user.relationshipType === 4) { + const buttons = document.createElement("div"); buttons.classList.add("flexltr"); - const buttonc=document.createElement("div"); - const button1=document.createElement("span"); - button1.classList.add("svgicon","svg-x"); - if(user.relationshipType===3){ - const buttonc=document.createElement("div"); - const button2=document.createElement("span"); - button2.classList.add("svgicon","svg-x"); + const buttonc = document.createElement("div"); + const button1 = document.createElement("span"); + button1.classList.add("svgicon", "svg-x"); + if (user.relationshipType === 3) { + const buttonc = document.createElement("div"); + const button2 = document.createElement("span"); + button2.classList.add("svgicon", "svg-x"); button2.classList.add("svg-addfriend"); buttonc.append(button2); buttonc.classList.add("friendlyButton"); buttonc.append(button2); buttons.append(buttonc); - buttonc.onclick=(e)=>{ + buttonc.onclick = (e) => { e.stopImmediatePropagation(); user.changeRelationship(1); outerDiv.remove(); - } + }; } buttonc.append(button1); buttonc.classList.add("friendlyButton"); - buttonc.onclick=(e)=>{ + buttonc.onclick = (e) => { e.stopImmediatePropagation(); user.changeRelationship(0); outerDiv.remove(); - } + }; buttons.append(buttonc); - const outerDiv=genuserstrip(user,buttons); + const outerDiv = genuserstrip(user, buttons); container.append(outerDiv); } } - } - pending.onclick=genPending; + }; + pending.onclick = genPending; channelTopic.append(pending); } { - const blocked=document.createElement("button"); - blocked.textContent=I18n.getTranslation("friends.blocked"); + const blocked = document.createElement("button"); + blocked.textContent = I18n.friends.blocked(); - const genBlocked=()=>{ - this.localuser.relationshipsUpdate=genBlocked; + const genBlocked = () => { + this.localuser.relationshipsUpdate = genBlocked; checkVoid(); - container.innerHTML=""; - container.append(I18n.getTranslation("friends.blockedusers")); - for(const user of this.localuser.inrelation){ - if(user.relationshipType===2){ - const buttonc=document.createElement("div"); - const button1=document.createElement("span"); - button1.classList.add("svg-x","svgicon"); + container.innerHTML = ""; + container.append(I18n.friends.blockedusers()); + for (const user of this.localuser.inrelation) { + if (user.relationshipType === 2) { + const buttonc = document.createElement("div"); + const button1 = document.createElement("span"); + button1.classList.add("svg-x", "svgicon"); buttonc.append(button1); buttonc.classList.add("friendlyButton"); - buttonc.onclick=(e)=>{ + buttonc.onclick = (e) => { user.changeRelationship(0); e.stopImmediatePropagation(); outerDiv.remove(); - } - const outerDiv=genuserstrip(user,buttonc); + }; + const outerDiv = genuserstrip(user, buttonc); container.append(outerDiv); } } - } - blocked.onclick=genBlocked; + }; + blocked.onclick = genBlocked; channelTopic.append(blocked); } { - const add=document.createElement("button"); - add.textContent=I18n.getTranslation("friends.addfriend"); - add.onclick=()=>{ - this.localuser.relationshipsUpdate=()=>{}; - container.innerHTML=""; - const float=new Float(""); - const options=float.options; - const form=options.addForm("",(e:any)=>{ - console.log(e); - if(e.code===404){ - throw new FormError(text,I18n.getTranslation("friends.notfound")); - }else if(e.code===400){ - throw new FormError(text,e.message.split("Error: ")[1]); - }else{ - const box=text.input.deref(); - if(!box)return; - box.value=""; - } - },{ - method:"POST", - fetchURL:this.info.api+"/users/@me/relationships", - headers:this.headers - }); - const text=form.addTextInput(I18n.getTranslation("friends.addfriendpromt"),"username"); - form.addPreprocessor((obj:any)=>{ - const [username,discriminator]=obj.username.split("#"); - obj.username=username; - obj.discriminator=discriminator; - if(!discriminator){ - throw new FormError(text,I18n.getTranslation("friends.discnotfound")); + const add = document.createElement("button"); + add.textContent = I18n.friends.addfriend(); + add.onclick = () => { + this.localuser.relationshipsUpdate = () => {}; + container.innerHTML = ""; + const float = new Float(""); + const options = float.options; + const form = options.addForm( + "", + (e: any) => { + console.log(e); + if (e.code === 404) { + throw new FormError(text, I18n.friends.notfound()); + } else if (e.code === 400) { + throw new FormError(text, e.message.split("Error: ")[1]); + } else { + const box = text.input.deref(); + if (!box) return; + box.value = ""; + box.placeholder = I18n.friends.requestsent(); + } + }, + { + method: "POST", + fetchURL: this.info.api + "/users/@me/relationships", + headers: this.headers, + }, + ); + const text = form.addTextInput(I18n.friends.addfriendpromt(), "username"); + form.addPreprocessor((obj: any) => { + const [username, discriminator] = obj.username.split("#"); + obj.username = username; + obj.discriminator = discriminator; + if (!discriminator) { + throw new FormError(text, I18n.friends.discnotfound()); } }); container.append(float.generateHTML()); - } + }; channelTopic.append(add); } } - giveMember(_member: memberjson){ - console.error("not a real guild, can't give member object"); + get mentions() { + let mentions = 0; + for (const thing of this.localuser.inrelation) { + if (thing.relationshipType === 3) { + mentions += 1; + } + } + return mentions; } - getRole(/* ID: string */){ + giveMember(_member: memberjson) { + throw new Error("not a real guild, can't give member object"); + } + getRole() { return null; } - hasRole(/* r: string */){ + hasRole() { return false; } - isAdmin(){ + isAdmin() { return false; } - unreaddms(){ - for(const thing of this.channels){ + unreaddms() { + for (const thing of this.channels) { (thing as Group).unreads(); } } @@ -323,69 +393,218 @@ dmPermissions.setPermission("CONNECT", 1); dmPermissions.setPermission("SPEAK", 1); dmPermissions.setPermission("STREAM", 1); dmPermissions.setPermission("USE_VAD", 1); +class Group extends Channel { + users: User[]; + owner_id?: string; + static groupcontextmenu = new Contextmenu("channel menu"); + static groupMenu = this.makeGroupMenu(); + static makeGroupMenu() { + const menu = new Contextmenu("group menu", true); + menu.addButton( + function (user) { + return I18n.member.kick(user.name, this.name); + }, + function (user) { + fetch(this.info.api + `/channels/${this.id}/recipients/${user.id}`, { + method: "DELETE", + headers: this.headers, + }); + }, + { + group: "default", + visible: function (user) { + return this.localuser.user.id !== user.id && this.owner_id === this.localuser.user.id; + }, + color: "red", + }, + ); + return menu; + } -// @ts-ignore I need to look into this lol -class Group extends Channel{ - user: User; - static contextmenu = new Contextmenu("channel menu"); - static setupcontextmenu(){ - this.contextmenu.addbutton(()=>I18n.getTranslation("DMs.copyId"), function(this: Group){ - navigator.clipboard.writeText(this.id); - }); + static setupcontextmenu() { + this.groupcontextmenu.addButton( + () => I18n.DMs.markRead(), + function (this: Group) { + this.readbottom(); + }, + ); - this.contextmenu.addbutton(()=>I18n.getTranslation("DMs.markRead"), function(this: Group){ - this.readbottom(); - }); + this.groupcontextmenu.addSeperator(); - this.contextmenu.addbutton(()=>I18n.getTranslation("DMs.close"), function(this: Group){ - this.deleteChannel(); - }); + this.groupcontextmenu.addButton( + () => I18n.group.edit(), + function () { + this.edit(); + }, + { + visible: function () { + return this.type !== 1; + }, + }, + ); + + this.groupcontextmenu.addButton( + () => I18n.DMs.close(), + function (this: Group) { + this.deleteChannel(); + }, + { + color: "red", + }, + ); + + this.groupcontextmenu.addButton( + () => I18n.DMs.add(), + function (this: Group) { + this.addPerson(); + }, + ); + + this.groupcontextmenu.addSeperator(); - this.contextmenu.addbutton(()=>I18n.getTranslation("user.copyId"), function(){ - navigator.clipboard.writeText(this.user.id); + this.groupcontextmenu.addButton( + () => I18n.user.copyId(), + function () { + navigator.clipboard.writeText(this.users[0].id); + }, + { + visible: function () { + return this.type === 1; + }, + }, + ); + + this.groupcontextmenu.addButton( + () => I18n.DMs.copyId(), + function (this: Group) { + navigator.clipboard.writeText(this.id); + }, + ); + } + + async addPerson() { + const d = new Dialog(I18n.DMs.add()); + const options = [...this.localuser.inrelation] + .filter((user) => user.relationshipType === 1) + .filter((user) => !this.users.includes(user)); + d.show(); + const users = await User.makeSelector(d.options, "Add person", options, {single: true}); + d.hide(); + if (!users) return; + const [user] = [...users]; + if (!user) return; + await fetch(this.info.api + "/channels/" + this.id + "/recipients/" + user.id, { + headers: this.headers, + method: "PUT", }); } - constructor(json: dirrectjson, owner: Direct){ + edit() { + const dio = new Dialog(I18n.group.edit()); + const form = dio.options.addForm("", () => {}, { + fetchURL: this.info.api + "/channels/" + this.id, + headers: this.headers, + method: "PATCH", + }); + form.addTextInput(I18n.channel["name:"](), "name", { + initText: this.name === this.defaultName() ? "" : this.name, + }); + form.addImageInput(I18n.channel.icon(), "icon", { + initImg: this.icon ? this.iconUrl() : undefined, + }); + form.onSubmit = () => { + dio.hide(); + }; + dio.show(); + } + addRec(user: User) { + this.users.push(user); + this.users = [...new Set(this.users)]; + if (this.localuser.channelfocus === this) { + this.localuser.memberListQue(); + } + } + removeRec(user: User) { + this.users = this.users.filter((u) => u !== user); + if (this.localuser.channelfocus === this) { + this.localuser.memberListQue(); + } + } + updateChannel(json: channeljson): void { + super.updateChannel(json); + this.owner_id = json.owner_id; + this.icon = json.icon; + this.makeIcon(); + } + defaultName() { + return this.users.map((_) => _.name).join(", "); + } + constructor(json: dirrectjson, owner: Direct) { super(-1, owner, json.id); - this.owner = owner; - this.headers = this.guild.headers; - this.name = json.recipients[0]?.username; - if(json.recipients[0]){ - this.user = new User(json.recipients[0], this.localuser); - }else{ - this.user = this.localuser.user; + + this.icon = json.icon; + this.type = json.type; + this.owner_id = json.owner_id; + + json.recipients = json.recipients.filter((_) => _.id !== this.localuser.user.id); + const userSet = new Set(json.recipients.map((user) => new User(user, this.localuser))); + if (userSet.size === 0) { + userSet.add(this.localuser.user); } + + this.users = [...userSet]; + this.name = json.name || this.defaultName(); + + userSet.add(this.localuser.user); + this.name ??= this.localuser.user.username; this.parent_id!; this.parent!; this.children = []; this.guild_id = "@me"; this.permission_overwrites = new Map(); - this.lastmessageid = json.last_message_id; + if (json.last_message_id) { + this.setLastMessageId(json.last_message_id); + } else { + this.lastmessageid = undefined; + } this.mentions = 0; + this.setUpInfiniteScroller(); this.updatePosition(); } - updatePosition(){ - if(this.lastmessageid){ + updatePosition(time?: number) { + if (time) { + this.position = time; + } else if (this.lastmessage) { + this.position = this.lastmessage.getTimeStamp(); + } else if (this.lastmessageid) { this.position = SnowFlake.stringToUnixTime(this.lastmessageid); - }else{ + } else { this.position = 0; } this.position = -Math.max(this.position, this.getUnixTime()); + + const html = this.html?.deref(); + if (!html) return; + const parent = html.parentElement; + if (!parent) return; + parent.prepend(html); } - createguildHTML(){ + createguildHTML() { const div = document.createElement("div"); - Group.contextmenu.bindContextmenu(div, this,undefined); + Group.groupcontextmenu.bindContextmenu(div, this, undefined); this.html = new WeakRef(div); - div.classList.add("flexltr","liststyle"); + div.classList.add("flexltr", "liststyle"); const myhtml = document.createElement("span"); myhtml.classList.add("ellipsis"); - myhtml.textContent = this.name; - div.appendChild(this.user.buildpfp()); + myhtml.textContent = this.type === 1 && this.users[0] ? this.users[0].name : this.name; + this.nameSpan = new WeakRef(myhtml); + + div.appendChild(this.makeIcon()); + div.appendChild(myhtml); (div as any).myinfo = this; - div.onclick = _=>{ + div.onclick = (_) => { this.getHTML(); const toggle = document.getElementById("maintoggle") as HTMLInputElement; toggle.checked = true; @@ -393,116 +612,73 @@ class Group extends Channel{ return div; } - async getHTML(addstate=true){ - const id = ++Channel.genid; - if(this.localuser.channelfocus){ - this.localuser.channelfocus.infinite.delete(); - } - if(this.guild !== this.localuser.lookingguild){ - this.guild.loadGuild(); - } - this.guild.prevchannel = this; - this.localuser.channelfocus = this; - const prom = this.infinite.delete(); - if(addstate){ - history.pushState([this.guild_id,this.id], "", "/channels/" + this.guild_id + "/" + this.id); - } - this.localuser.pageTitle("@" + this.name); - (document.getElementById("channelTopic") as HTMLElement).setAttribute("hidden",""); - - const loading = document.getElementById("loadingdiv") as HTMLDivElement; - Channel.regenLoadingMessages(); - - loading.classList.add("loading"); - this.rendertyping(); - (document.getElementById("typebox") as HTMLDivElement).contentEditable ="" + true; - (document.getElementById("upload") as HTMLElement).style.visibility="visible"; - (document.getElementById("typediv") as HTMLElement).style.visibility="visible"; - (document.getElementById("typebox") as HTMLDivElement).focus(); - await this.putmessages(); - await prom; - this.localuser.getSidePannel(); - if(id !== Channel.genid){ - return; - } - this.buildmessages(); - + getname() { + return this.name; } - messageCreate(messagep: { d: messagejson }){ - const messagez = new Message(messagep.d, this); - if(this.lastmessageid){ - this.idToNext.set(this.lastmessageid, messagez.id); - this.idToPrev.set(messagez.id, this.lastmessageid); - } - this.lastmessageid = messagez.id; - if(messagez.author === this.localuser.user){ - this.lastreadmessageid = messagez.id; - if(this.myhtml){ - this.myhtml.classList.remove("cunread"); - } - }else{ - if(this.myhtml){ - this.myhtml.classList.add("cunread"); - } - } - this.unreads(); - this.updatePosition(); - this.infinite.addedBottom(); - this.guild.sortchannels(); - if(this.myhtml){ - const parrent = this.myhtml.parentElement as HTMLElement; - parrent.prepend(this.myhtml); - } - if(this === this.localuser.channelfocus){ - if(!this.infinitefocus){ - this.tryfocusinfinate(); - } - this.infinite.addedBottom(); - } - this.unreads(); - if(messagez.author === this.localuser.user){ - return; - } - if( - this.localuser.lookingguild?.prevchannel === this && - document.hasFocus() - ){ - return; - } - if(this.notification === "all"){ - this.notify(messagez); - }else if( - this.notification === "mentions" && - messagez.mentionsuser(this.localuser.user) - ){ - this.notify(messagez); + notititle(message: Message) { + if (this.users.length === 1) { + return message.author.username; + } else { + return this.getname() + " > " + message.author.username; } } - notititle(message: Message){ - return message.author.username; + readStateInfo(json: readyjson["d"]["read_state"]["entries"][0]): void { + super.readStateInfo(json); } - readbottom(){ + readbottom() { super.readbottom(); this.unreads(); } + get hasunreads() { + return this.mentions !== 0; + } all: WeakRef = new WeakRef(document.createElement("div")); - noti: WeakRef = new WeakRef(document.createElement("div")); - del(){ + noti?: WeakRef; + del() { const all = this.all.deref(); - if(all){ + if (all) { all.remove(); } - if(this.myhtml){ + if (this.myhtml) { this.myhtml.remove(); } } - unreads(){ + groupDmDiv = new WeakRef(document.createElement("div")); + makeIcon(): HTMLElement { + if (this.type === 1) { + return this.users[0].buildstatuspfp(this); + } else { + const div = this.groupDmDiv.deref() || document.createElement("div"); + div.innerHTML = ""; + div.classList.add("groupDmDiv"); + if (this.icon) { + const img = createImg( + this.info.cdn + + "/channel-icons/" + + this.id + + "/" + + this.icon + + ".png" + + new CDNParams({expectedSize: 32}), + ); + img.classList.add("pfp"); + div.append(img); + } else { + for (const user of this.users.slice(0, 5)) { + div.append(user.buildpfp(undefined)); + } + } + return div; + } + } + unreads() { const sentdms = document.getElementById("sentdms") as HTMLDivElement; //Need to change sometime - const current = this.all.deref(); - if(this.hasunreads){ + let current = this.all.deref(); + if (!current || !document.body.contains(current)) current = undefined; + if (this.mentions) { { - const noti = this.noti.deref(); - if(noti){ + const noti = this.noti?.deref(); + if (noti) { noti.textContent = this.mentions + ""; return; } @@ -513,30 +689,32 @@ class Group extends Channel{ noti.classList.add("unread", "notiunread", "pinged"); noti.textContent = "" + this.mentions; this.noti = new WeakRef(noti); - div.append(noti); - const buildpfp = this.user.buildpfp(); + const buildpfp = this.makeIcon(); this.all = new WeakRef(div); buildpfp.classList.add("mentioned"); - div.append(buildpfp); + div.append(buildpfp, noti); sentdms.append(div); - div.onclick = _=>{ + div.onclick = (_) => { this.guild.loadGuild(); this.getHTML(); const toggle = document.getElementById("maintoggle") as HTMLInputElement; toggle.checked = true; }; - }else if(current){ + } else if (current) { current.remove(); - }else{ + } else { } } - isAdmin(): boolean{ - return false; - } - hasPermission(name: string): boolean{ + + hasPermission(name: string): boolean { return dmPermissions.hasPermission(name); } + async messageCreate(messagep: messageCreateJson): Promise { + await super.messageCreate(messagep); + this.updatePosition(); + console.log(this); + } } -export{ Direct, Group }; +export {Direct, Group}; -Group.setupcontextmenu() +Group.setupcontextmenu(); diff --git a/src/webpage/discovery.ts b/src/webpage/discovery.ts new file mode 100644 index 00000000..53c7280a --- /dev/null +++ b/src/webpage/discovery.ts @@ -0,0 +1,241 @@ +import {Contextmenu} from "./contextmenu.js"; +import {Direct} from "./direct.js"; +import {I18n} from "./i18n.js"; +import {guildjson} from "./jsontypes.js"; +import {ReportMenu} from "./reporting/report.js"; +import {Dialog} from "./settings.js"; +import {CDNParams} from "./utils/cdnParams.js"; +import {getDeveloperSettings} from "./utils/storage/devSettings.js"; +import {createImg} from "./utils/utils.js"; + +export class Discovery { + owner: Direct; + get info() { + return this.owner.info; + } + get headers() { + return this.owner.headers; + } + get localuser() { + return this.owner.localuser; + } + constructor(owner: Direct) { + this.owner = owner; + this.context = this.makeContextMenu(); + } + context: Contextmenu HTMLElement>; + makeContextMenu() { + const menu = new Contextmenu HTMLElement>("discovery"); + const local = this.localuser; + menu.addButton( + () => I18n.guild.report(), + async function (elm) { + const menu = await ReportMenu.makeReport("guild_discovery", local, { + guild_id: this, + dyn_preview: elm, + }); + menu?.spawnMenu(); + }, + { + visible: function () { + const settings = getDeveloperSettings(); + return settings.reportSystem; + }, + color: "red", + }, + ); + return menu; + } + async makeMenu() { + if (this.owner instanceof Direct) { + this.owner.freindDiv?.classList.remove("viewChannel"); + } + if (this.localuser.channelfocus) { + this.localuser.channelfocus.collectBox(); + } + history.pushState(["@me", "discover"], "", "/channels/@me/discover"); + this.localuser.pageTitle(I18n.discovery()); + + const channelTopic = document.getElementById("channelTopic") as HTMLSpanElement; + channelTopic.removeAttribute("hidden"); + channelTopic.textContent = ""; + channelTopic.onclick = () => {}; + if (this.localuser.lookingguild) { + this.localuser.lookingguild.html.classList.remove("serveropen"); + } + + this.localuser.lookingguild = undefined; + this.localuser.channelfocus = undefined; + + const loading = document.getElementById("loadingdiv") as HTMLDivElement; + loading.classList.remove("loading"); + this.localuser.getSidePannel(); + + const messages = document.getElementById("scrollWrap") as HTMLDivElement; + for (const thing of Array.from(messages.getElementsByClassName("messagecontainer"))) { + thing.remove(); + } + + const channels = document.getElementById("channels") as HTMLDivElement; + channels.innerHTML = ""; + + const banner = document.getElementById("servertd") as HTMLDivElement; + banner.style.removeProperty("background-image"); + banner.classList.remove("Banner"); + banner.style.removeProperty("cursor"); + banner.onclick = () => {}; + + (document.getElementById("serverName") as HTMLElement).textContent = I18n.discovery(); + + const scrollWrap = document.getElementById("scrollWrap") as HTMLDivElement; + scrollWrap.innerHTML = ""; + + const content = document.createElement("div"); + content.classList.add("flexttb", "guildy", "messagecontainer"); + content.textContent = I18n.guild.loadingDiscovery(); + + scrollWrap.append(content); + + const guildsButton = document.createElement("button"); + guildsButton.textContent = I18n.guild.guilds(); + guildsButton.classList.add("discoverButton", "selected"); + channels.append(guildsButton); + + const res = await fetch(this.info.api + "/discoverable-guilds?limit=50", { + headers: this.headers, + }); + const json = await res.json(); + console.log([...json.guilds], json.guilds); + + content.innerHTML = ""; + const title = document.createElement("h2"); + title.textContent = I18n.guild.disoveryTitle(json.guilds.length + ""); + content.appendChild(title); + + const guilds = document.createElement("div"); + guilds.id = "discovery-guild-content"; + + json.guilds.forEach((guild: guildjson["properties"]) => { + const content = document.createElement("div"); + + this.context.bindContextmenu(content, guild.id, () => { + const div = document.createElement("div"); + div.classList.add("flexltr"); + const img = this.getIconURL(guild); + img.classList.add("icon"); + img.crossOrigin = "anonymous"; + + img.alt = ""; + div.appendChild(img); + + const name = document.createElement("h3"); + name.textContent = guild.name; + div.appendChild(name); + return div; + }); + content.classList.add("discovery-guild"); + const banner = this.getBannerURL(guild); + if (banner) { + banner.classList.add("banner"); + banner.crossOrigin = "anonymous"; + banner.alt = ""; + content.appendChild(banner); + } + + const nameContainer = document.createElement("div"); + nameContainer.classList.add("flex"); + const img = this.getIconURL(guild); + img.classList.add("icon"); + img.crossOrigin = "anonymous"; + + img.alt = ""; + nameContainer.appendChild(img); + + const name = document.createElement("h3"); + name.textContent = guild.name; + nameContainer.appendChild(name); + content.appendChild(nameContainer); + const desc = document.createElement("p"); + desc.textContent = guild.description; + content.appendChild(desc); + + content.addEventListener("click", async () => { + let guildObj = this.localuser.guildids.get(guild.id); + if (guildObj) { + guildObj.loadGuild(); + guildObj.loadChannel(); + return; + } + if (await this.confirmJoin(guild)) { + await this.join(guild); + } + }); + guilds.appendChild(content); + }); + content.appendChild(guilds); + } + getIconURL(guild: guildjson["properties"]) { + return createImg( + this.info.cdn + + (guild.icon + ? "/icons/" + guild.id + "/" + guild.icon + ".png" + new CDNParams({expectedSize: 48}) + : "/embed/avatars/3.png"), + ); + } + getBannerURL(guild: guildjson["properties"]) { + return ( + guild.banner && + createImg(this.info.cdn + "/banners/" + guild.id + "/" + guild.banner + ".png?size=256") + ); + } + async confirmJoin(guild: guildjson["properties"]) { + return new Promise((res) => { + const dio = new Dialog(I18n.guild.joinConfirm(guild.name)); + const opt = dio.options; + + const div = document.createElement("div"); + div.classList.add("flexltr", "guildPreview"); + const img = this.getIconURL(guild); + img.classList.add("icon"); + img.crossOrigin = "anonymous"; + img.alt = ""; + div.append(img); + const banner = this.getBannerURL(guild); + if (banner) { + banner.classList.add("banner"); + banner.crossOrigin = "anonymous"; + banner.alt = ""; + div.append(banner); + } + + opt.addHTMLArea(div); + opt.addText(I18n.guild.memberCount(guild.member_count + "")); + + if (guild.description) opt.addText(I18n.guild["description:"]() + "\n" + guild.description); + + const buttons = opt.addOptions("", {ltr: true}); + buttons.addButtonInput("", I18n.yes(), () => { + dio.hide(); + res(true); + }); + buttons.addButtonInput("", I18n.no(), () => { + dio.hide(); + res(false); + }); + dio.show(); + }); + } + async join(guild: guildjson["properties"]) { + await fetch(this.info.api + "/guilds/" + guild.id + "/members/@me", { + method: "PUT", + headers: this.headers, + }); + let guildObj = this.localuser.guildids.get(guild.id); + while (!guildObj) { + guildObj = this.localuser.guildids.get(guild.id); + await new Promise((res) => setTimeout(res, 100)); + } + guildObj.loadGuild(); + guildObj.loadChannel(); + } +} diff --git a/src/webpage/disimg.ts b/src/webpage/disimg.ts index a1d2a1d4..80cb72c9 100644 --- a/src/webpage/disimg.ts +++ b/src/webpage/disimg.ts @@ -1,37 +1,89 @@ -class ImagesDisplay{ - images:string[]; - index=0; - constructor(srcs:string[],index=0){ - this.images=srcs; - this.index=index; +import {File} from "./file.js"; +import {removeAni} from "./utils/utils.js"; + +class ImagesDisplay { + files: File[]; + index = 0; + constructor(files: File[], index = 0) { + this.files = files; + this.index = index; } - weakbg=new WeakRef(document.createElement("div")); - get background():HTMLElement|undefined{ + weakbg = new WeakRef(document.createElement("div")); + get background(): HTMLElement | undefined { return this.weakbg.deref(); } - set background(e:HTMLElement){ - this.weakbg=new WeakRef(e); + set background(e: HTMLElement) { + this.weakbg = new WeakRef(e); } - makeHTML():HTMLElement{ - //TODO this should be able to display more than one image at a time lol - const image= document.createElement("img"); - image.src=this.images[this.index]; - image.classList.add("imgfit","centeritem"); + makeHTML(): HTMLElement { + const image = this.files[this.index].getHTML(false, true); + image.classList.add("imgfit", "centeritem"); return image; } - show(){ + show() { this.background = document.createElement("div"); this.background.classList.add("background"); - this.background.appendChild(this.makeHTML()); - this.background.onclick = _=>{ + let cur = this.makeHTML(); + if (this.files.length !== 1) { + const right = document.createElement("span"); + right.classList.add("rightArrow", "svg-intoMenu"); + right.onclick = (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + this.index++; + this.index %= this.files.length; + cur.remove(); + cur = this.makeHTML(); + if (this.background) { + this.background.appendChild(cur); + } + }; + + const left = document.createElement("span"); + left.onclick = (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + this.index += this.files.length - 1; + this.index %= this.files.length; + cur.remove(); + cur = this.makeHTML(); + if (this.background) { + this.background.appendChild(cur); + } + }; + left.classList.add("leftArrow", "svg-leftArrow"); + this.background.append(right, left); + this.background.addEventListener("keydown", (e) => { + if (e.key === "ArrowRight") { + e.preventDefault(); + e.stopImmediatePropagation(); + right.click(); + } + if (e.key === "ArrowLeft") { + e.preventDefault(); + e.stopImmediatePropagation(); + right.click(); + } + }); + } + + this.background.appendChild(cur); + this.background.onclick = (_) => { this.hide(); }; + this.background.onkeydown = (e) => { + if (e.key === "Escape") { + this.hide(); + } + }; document.body.append(this.background); + this.background.setAttribute("tabindex", "0"); + this.background.focus(); } - hide(){ - if(this.background){ - this.background.remove(); + hide() { + if (this.background) { + removeAni(this.background); } } } -export{ImagesDisplay} +export {ImagesDisplay}; diff --git a/src/webpage/embed.ts b/src/webpage/embed.ts index e84105d2..bc7b867b 100644 --- a/src/webpage/embed.ts +++ b/src/webpage/embed.ts @@ -1,137 +1,139 @@ -import{ Message }from"./message.js"; -import{ MarkDown }from"./markdown.js"; -import{ embedjson, invitejson }from"./jsontypes.js"; -import{ getapiurls, getInstances }from"./login.js"; -import{ Guild }from"./guild.js"; -import { I18n } from "./i18n.js"; -import { ImagesDisplay } from "./disimg.js"; +import {Message} from "./message.js"; +import {MarkDown} from "./markdown.js"; +import {embedjson, invitejson} from "./jsontypes.js"; +import {createImg, getapiurls, getBulkUsers, getInstances, Specialuser} from "./utils/utils.js"; +import {Guild} from "./guild.js"; +import {I18n} from "./i18n.js"; +import {ImagesDisplay} from "./disimg.js"; +import {File} from "./file.js"; +import {CDNParams} from "./utils/cdnParams.js"; -class Embed{ +class Embed { type: string; owner: Message; json: embedjson; - constructor(json: embedjson, owner: Message){ + constructor(json: embedjson, owner: Message) { this.type = this.getType(json); this.owner = owner; this.json = json; } - getType(json: embedjson){ - const instances = getInstances(); - if( - instances && -json.type === "link" && -json.url && -URL.canParse(json.url) - ){ + getType(json: embedjson) { + const users = Object.values(getBulkUsers().users) as Specialuser[]; + const instances = getInstances() + ?.map((_) => _.url) + .filter((_) => undefined !== _) + .concat(users.map((_) => _.serverurls.wellknown)); + if (instances && json.type === "link" && json.url && URL.canParse(json.url)) { const Url = new URL(json.url); - for(const instance of instances){ - if(instance.url && URL.canParse(instance.url)){ - const IUrl = new URL(instance.url); + for (const instance of instances) { + if (instance && URL.canParse(instance)) { + const IUrl = new URL(instance); const params = new URLSearchParams(Url.search); let host: string; - if(params.has("instance")){ + if (params.has("instance")) { const url = params.get("instance") as string; - if(URL.canParse(url)){ + if (URL.canParse(url)) { host = new URL(url).host; - }else{ + } else { host = Url.host; } - }else{ + } else { host = Url.host; } - if(IUrl.host === host){ - const code = -Url.pathname.split("/")[Url.pathname.split("/").length - 1]; + if (IUrl.host === host) { + const code = Url.pathname.split("/")[Url.pathname.split("/").length - 1]; json.invite = { - url: instance.url, + url: instance, code, }; - return"invite"; + return "invite"; } } } } return json.type || "rich"; } - generateHTML(){ - switch(this.type){ - case"rich": - return this.generateRich(); - case"image": - return this.generateImage(); - case"invite": - return this.generateInvite(); - case"link": - return this.generateLink(); - case"video": - case"article": - return this.generateArticle(); - default: - console.warn( - `unsupported embed type ${this.type}, please add support dev :3`, - this.json - ); - return document.createElement("div"); //prevent errors by giving blank div + generateHTML() { + switch (this.type) { + case "rich": + return this.generateRich(); + case "image": + return this.generateImage(); + case "invite": + return this.generateInvite(); + case "link": + return this.generateLink(); + case "video": + case "article": + return this.generateArticle(); + default: + console.warn(`unsupported embed type ${this.type}, please add support dev :3`, this.json); + return document.createElement("div"); //prevent errors by giving blank div } } - get message(){ + get message() { return this.owner; } - get channel(){ + get channel() { return this.message.channel; } - get guild(){ + get guild() { return this.channel.guild; } - get localuser(){ + get localuser() { return this.guild.localuser; } - generateRich(){ + generateRich() { const div = document.createElement("div"); - if(this.json.color){ - div.style.backgroundColor = "#" + this.json.color.toString(16); - } - div.classList.add("embed-color"); + div.style.backgroundColor = + this.json.color !== undefined + ? "#" + this.json.color.toString(16).padStart(6, "0") + : "var(--embed-bg-side)"; + + div.classList.add("embed-color", "rich-embed"); const embed = document.createElement("div"); embed.classList.add("embed"); div.append(embed); - if(this.json.author){ + if (this.json.author) { const authorline = document.createElement("div"); - if(this.json.author.icon_url){ + if (this.json.author.icon_url) { const img = document.createElement("img"); - img.classList.add("embedimg"); - img.src = this.json.author.icon_url; + img.classList.add("authorEmbedImg"); + this.localuser.refreshIfNeeded(this.json.author.icon_url).then((url) => { + if (this.json.author) this.json.author.icon_url = url; + img.src = url; + }); authorline.append(img); } - const a = document.createElement("a"); + const a = this.json.author.url ? document.createElement("a") : document.createElement("span"); a.textContent = this.json.author.name as string; - if(this.json.author.url){ + if (this.json.author.url) { MarkDown.safeLink(a, this.json.author.url); + a.classList.add("username"); } - a.classList.add("username"); + authorline.append(a); embed.append(authorline); } - if(this.json.title){ + if (this.json.title) { const title = document.createElement("a"); title.append(new MarkDown(this.json.title, this.channel).makeHTML()); - if(this.json.url){ + if (this.json.url) { MarkDown.safeLink(title, this.json.url); } title.classList.add("embedtitle"); embed.append(title); } - if(this.json.description){ + if (this.json.description) { const p = document.createElement("p"); p.append(new MarkDown(this.json.description, this.channel).makeHTML()); embed.append(p); } - embed.append(document.createElement("br")); - if(this.json.fields){ - for(const thing of this.json.fields){ + if (this.json.fields) { + for (const thing of this.json.fields) { const div = document.createElement("div"); const b = document.createElement("b"); b.textContent = thing.name; @@ -141,31 +143,55 @@ Url.pathname.split("/")[Url.pathname.split("/").length - 1]; p.classList.add("embedp"); div.append(p); - if(thing.inline){ + if (thing.inline) { div.classList.add("inline"); } embed.append(div); } } - if(this.json.footer || this.json.timestamp){ + if (this.json.image) { + const img = document.createElement("img"); + img.classList.add("embedImg"); + if (this.json.image.width) { + img.width = this.json.image.width; + } + if (this.json.image.height) { + img.height = this.json.image.height; + } + this.localuser.refreshIfNeeded(this.json.image.url).then((url) => { + if (this.json.image) this.json.image.url = url; + if (!this.json.image?.proxy_url) img.src = url; + }); + if (this.json.image.proxy_url) { + this.localuser.refreshIfNeeded(this.json.image.url).then((url) => { + if (this.json.image) this.json.image.url = url; + img.src = url; + }); + } + embed.append(img); + } + if (this.json.footer || this.json.timestamp) { const footer = document.createElement("div"); - if(this.json?.footer?.icon_url){ + if (this.json?.footer?.icon_url) { const img = document.createElement("img"); - img.src = this.json.footer.icon_url; + this.localuser.refreshIfNeeded(this.json.footer.icon_url).then((url) => { + if (this.json.footer) this.json.footer.icon_url = url; + img.src = url; + }); img.classList.add("embedicon"); footer.append(img); } - if(this.json?.footer?.text){ + if (this.json?.footer?.text) { const span = document.createElement("span"); span.textContent = this.json.footer.text; footer.append(span); } - if(this.json?.footer && this.json?.timestamp){ + if (this.json?.footer && this.json?.timestamp) { const span = document.createElement("span"); span.textContent = " • "; footer.append(span); } - if(this.json?.timestamp){ + if (this.json?.timestamp) { const span = document.createElement("span"); span.textContent = new Date(this.json.timestamp).toLocaleString(); footer.append(span); @@ -174,15 +200,27 @@ Url.pathname.split("/")[Url.pathname.split("/").length - 1]; } return div; } - generateImage(){ - const img = document.createElement("img"); + generateImage() { + const div = document.createElement("div"); + div.classList.add("messageimgdiv"); + const img = createImg(this.json.thumbnail.proxy_url); img.classList.add("messageimg"); - img.onclick = function(){ - const full = new ImagesDisplay([img.src]); + img.onclick = () => { + const full = new ImagesDisplay([ + new File( + { + id: "", + filename: "", + url: this.json.thumbnail.proxy_url, + size: -1, + content_type: "image/", + }, + null, + ), + ]); full.show(); }; - img.src = this.json.thumbnail.proxy_url; - if(this.json.thumbnail.width){ + if (this.json.thumbnail.width) { let scale = 1; const max = 96 * 3; scale = Math.max(scale, this.json.thumbnail.width / max); @@ -193,28 +231,61 @@ Url.pathname.split("/")[Url.pathname.split("/").length - 1]; img.style.width = this.json.thumbnail.width + "px"; img.style.height = this.json.thumbnail.height + "px"; console.log(this.json, "Image fix"); - return img; + + img.isAnimated().then((animated) => { + if (!animated || !this.owner) return; + const url = + new URL(this.json.thumbnail.url).origin + new URL(this.json.thumbnail.url).pathname; + const span = document.createElement("span"); + span.classList.add("svg-gifstar"); + if (this.owner.localuser.favorites.hasGif(url)) { + span.classList.add("favorited"); + } + div.append(span); + + span.onclick = () => { + if (!this.owner) return; + const fav = this.owner.localuser.favorites; + + if (fav.hasGif(url)) { + span.classList.remove("favorited"); + fav.unfavoriteGif(url); + } else { + span.classList.add("favorited"); + fav.favoriteGif(url, { + src: url, + width: this.json.thumbnail.width, + height: this.json.thumbnail.height, + }); + } + }; + }); + div.append(img); + return div; } - generateLink(){ + generateLink() { const table = document.createElement("table"); table.classList.add("embed", "linkembed"); const trtop = document.createElement("tr"); table.append(trtop); - if(this.json.url && this.json.title){ + if (this.json.url && this.json.title) { const td = document.createElement("td"); const a = document.createElement("a"); MarkDown.safeLink(a, this.json.url); a.textContent = this.json.title; + a.classList.add("embedtitle"); td.append(a); trtop.append(td); } { const td = document.createElement("td"); const img = document.createElement("img"); - if(this.json.thumbnail){ + if (this.json.thumbnail) { img.classList.add("embedimg"); - img.onclick = function(){ - const full = new ImagesDisplay([img.src]); + img.onclick = function () { + const full = new ImagesDisplay([ + new File({id: "", filename: "", url: img.src, size: -1, content_type: "image/"}, null), + ]); full.show(); }; img.src = this.json.thumbnail.proxy_url; @@ -224,7 +295,7 @@ Url.pathname.split("/")[Url.pathname.split("/").length - 1]; } const bottomtr = document.createElement("tr"); const td = document.createElement("td"); - if(this.json.description){ + if (this.json.description) { const span = document.createElement("span"); span.textContent = this.json.description; td.append(span); @@ -233,59 +304,63 @@ Url.pathname.split("/")[Url.pathname.split("/").length - 1]; table.append(bottomtr); return table; } - invcache: [invitejson, { cdn: string; api: string }] | undefined; - generateInvite(){ - if(this.invcache && (!this.json.invite || !this.localuser)){ + invcache: [invitejson, {cdn: string; api: string}] | undefined; + generateInvite() { + if (this.invcache && (!this.json.invite || !this.localuser)) { return this.generateLink(); } const div = document.createElement("div"); div.classList.add("embed", "inviteEmbed", "flexttb"); const json1 = this.json.invite; - (async ()=>{ + (async () => { let json: invitejson; - let info: { cdn: string; api: string }; - if(!this.invcache){ - if(!json1){ - div.classList.remove("embed", "inviteEmbed", "flexttb") + let info: {cdn: string; api: string}; + if (!this.invcache) { + if (!json1) { + div.classList.remove("embed", "inviteEmbed", "flexttb"); div.append(this.generateLink()); return; } const tempinfo = await getapiurls(json1.url); - if(!tempinfo){ - div.classList.remove("embed", "inviteEmbed", "flexttb") + if (!tempinfo) { + div.classList.remove("embed", "inviteEmbed", "flexttb"); div.append(this.generateLink()); return; } info = tempinfo; const res = await fetch(info.api + "/invites/" + json1.code); - if(!res.ok){ - div.classList.remove("embed", "inviteEmbed", "flexttb") + if (!res.ok) { + div.classList.remove("embed", "inviteEmbed", "flexttb"); div.append(this.generateLink()); return; } json = (await res.json()) as invitejson; this.invcache = [json, info]; - }else{ + } else { [json, info] = this.invcache; } - if(!json){ + if (!json) { div.append(this.generateLink()); - div.classList.remove("embed", "inviteEmbed", "flexttb") + div.classList.remove("embed", "inviteEmbed", "flexttb"); return; } - if(json.guild.banner){ + if (json.guild.banner) { const banner = document.createElement("img"); - banner.src = this.localuser.info.cdn + "/icons/" + json.guild.id + "/" + json.guild.banner + ".png?size=256"; + banner.src = + info.cdn + + "/banners/" + + json.guild.id + + "/" + + json.guild.banner + + ".png" + + new CDNParams({expectedSize: 256}); banner.classList.add("banner"); div.append(banner); } - const guild: invitejson["guild"] & { info?: { cdn: string } } = -json.guild; + const guild: invitejson["guild"] & {info?: {cdn: string}} = json.guild; guild.info = info; - const icon = Guild.generateGuildIcon( -guild as invitejson["guild"] & { info: { cdn: string } } - ); + const icon = Guild.generateGuildIcon(guild as invitejson["guild"] & {info: {cdn: string}}); const iconrow = document.createElement("div"); iconrow.classList.add("flexltr"); iconrow.append(icon); @@ -305,30 +380,30 @@ guild as invitejson["guild"] & { info: { cdn: string } } div.append(iconrow); const h2 = document.createElement("h2"); - h2.textContent = I18n.getTranslation("invite.invitedBy",json.inviter.username); + h2.textContent = I18n.invite.invitedBy(json.inviter.username); div.append(h2); const button = document.createElement("button"); - button.textContent = I18n.getTranslation("invite.accept"); - if(this.localuser.info.api.startsWith(info.api) && this.localuser.guildids.has(guild.id)){ - button.textContent = I18n.getTranslation("invite.alreadyJoined"); + button.textContent = I18n.invite.accept(); + if (this.localuser.info.api.startsWith(info.api) && this.localuser.guildids.has(guild.id)) { + button.textContent = I18n.invite.alreadyJoined(); button.disabled = true; } button.classList.add("acceptinvbutton"); div.append(button); - button.onclick = _=>{ - if(this.localuser.info.api.startsWith(info.api)){ + button.onclick = (_) => { + if (this.localuser.info.api.startsWith(info.api)) { fetch(this.localuser.info.api + "/invites/" + json.code, { method: "POST", headers: this.localuser.headers, }) - .then(r=>r.json()) - .then(_=>{ - if(_.message){ + .then((r) => r.json()) + .then((_) => { + if (_.message) { alert(_.message); } }); - }else{ - if(this.json.invite){ + } else { + if (this.json.invite) { const params = new URLSearchParams(""); params.set("instance", this.json.invite.url); const encoded = params.toString(); @@ -340,33 +415,34 @@ guild as invitejson["guild"] & { info: { cdn: string } } })(); return div; } - generateArticle(){ + generateArticle() { const colordiv = document.createElement("div"); colordiv.style.backgroundColor = "#000000"; colordiv.classList.add("embed-color"); const div = document.createElement("div"); div.classList.add("embed"); - if(this.json.provider){ + if (this.json.provider) { const provider = document.createElement("p"); provider.classList.add("provider"); provider.textContent = this.json.provider.name; div.append(provider); } const a = document.createElement("a"); - if(this.json.url && this.json.url){ + if (this.json.url && this.json.url) { MarkDown.safeLink(a, this.json.url); - a.textContent = this.json.url; + a.textContent = this.json.title || this.json.url; + a.classList.add("embedtitle"); div.append(a); } - if(this.json.description){ + if (this.json.description) { const description = document.createElement("p"); description.textContent = this.json.description; div.append(description); } - if(this.json.thumbnail){ + if (this.json.thumbnail) { const img = document.createElement("img"); - if(this.json.thumbnail.width && this.json.thumbnail.width){ + if (this.json.thumbnail.width && this.json.thumbnail.width) { let scale = 1; const inch = 96; scale = Math.max(scale, this.json.thumbnail.width / inch / 4); @@ -377,22 +453,24 @@ guild as invitejson["guild"] & { info: { cdn: string } } img.style.height = this.json.thumbnail.height + "px"; } img.classList.add("bigembedimg"); - if(this.json.video){ - img.onclick = async ()=>{ - if(this.json.video){ + if (this.json.video) { + img.onclick = async () => { + if (this.json.video) { img.remove(); const iframe = document.createElement("iframe"); iframe.src = this.json.video.url + "?autoplay=1"; - if(this.json.thumbnail.width && this.json.thumbnail.width){ + if (this.json.thumbnail.width && this.json.thumbnail.height) { iframe.style.width = this.json.thumbnail.width + "px"; iframe.style.height = this.json.thumbnail.height + "px"; } div.append(iframe); } }; - }else{ - img.onclick = async ()=>{ - const full = new ImagesDisplay([img.src]); + } else { + img.onclick = async () => { + const full = new ImagesDisplay([ + new File({id: "", filename: "", url: img.src, size: -1, content_type: "image/"}, null), + ]); full.show(); }; } @@ -403,4 +481,4 @@ guild as invitejson["guild"] & { info: { cdn: string } } return colordiv; } } -export{ Embed }; +export {Embed}; diff --git a/src/webpage/emoji.ts b/src/webpage/emoji.ts index 03b1d429..4dbe63e3 100644 --- a/src/webpage/emoji.ts +++ b/src/webpage/emoji.ts @@ -1,114 +1,237 @@ -import{ Contextmenu }from"./contextmenu.js"; -import{ Guild }from"./guild.js"; -import { emojijson } from "./jsontypes.js"; -import{ Localuser }from"./localuser.js"; +import {Contextmenu} from "./contextmenu.js"; +import {Guild} from "./guild.js"; +import {Hover} from "./hover.js"; +import {I18n} from "./i18n.js"; +import {emojijson, emojiSource} from "./jsontypes.js"; +import {Localuser} from "./localuser.js"; +import {BinRead} from "./utils/binaryUtils.js"; +import {CDNParams} from "./utils/cdnParams.js"; +import {removeAni} from "./utils/utils.js"; //I need to recompile the emoji format for translation -class Emoji{ +class Emoji { static emojis: { name: string; emojis: { - name: string; - emoji: string; + name: string; + emoji: string; }[]; }[]; name: string; id?: string; - emoji?:string; + emoji?: string; animated: boolean; - owner: Guild | Localuser; - get guild(){ - if(this.owner instanceof Guild){ + owner?: Guild | Localuser; + get guild() { + if (this.owner instanceof Guild) { return this.owner; } return null; } - get localuser(){ - if(this.owner instanceof Guild){ + get localuser() { + if (this.owner instanceof Guild) { return this.owner.localuser; - }else{ + } else { return this.owner; } } - get info(){ - return this.owner.info; - } - constructor( - json: emojijson, - owner: Guild | Localuser - ){ + constructor(json: emojijson, owner?: Guild | Localuser) { this.name = json.name; this.id = json.id; - this.animated = json.animated||false; + this.animated = json.animated || false; this.owner = owner; - this.emoji=json.emoji; + this.emoji = json.emoji; + } + get humanName() { + if (this.id) { + const trim = this.name.split(":")[1]; + if (trim) { + return trim; + } + } + return this.name; + } + static emojiMap = new WeakMap>(); + static async lookupEmoji(id: string, localuser: Localuser): Promise { + const guild = localuser.guilds.find((guild) => guild.emojis.find((emoji) => emoji.id === id)); + if (guild) { + return { + type: "GUILD", + guild: { + id: guild.id, + name: guild.properties.name, + nsfw: guild.properties.nsfw, + icon: guild.properties.icon, + features: guild.properties.features, + description: guild.properties.description, + }, + }; + } + + const map = this.emojiMap.get(localuser) || new Map(); + this.emojiMap.set(localuser, map); + + if (map.has(id)) return map.get(id); + + const res = await fetch(localuser.info.api + `/emojis/${id}/source`, { + headers: localuser.headers, + }); + if (res.status === 403) { + map.set(id, undefined); + return undefined; + } + const json = (await res.json()) as emojiSource; + map.set(id, json); + return json; } - getHTML(bigemoji: boolean = false){ - if(this.id){ + getHTML(bigemoji: boolean = false, click = true) { + if (this.id) { + if (!this.owner) throw new Error("owner is missing for custom emoji!"); const emojiElem = document.createElement("img"); emojiElem.classList.add("md-emoji"); emojiElem.classList.add(bigemoji ? "bigemoji" : "smallemoji"); emojiElem.crossOrigin = "anonymous"; - emojiElem.src =this.info.cdn+"/emojis/"+this.id+"."+(this.animated ? "gif" : "png")+"?size=32"; + emojiElem.src = + this.owner.info.cdn + + "/emojis/" + + this.id + + "." + + (this.animated ? "gif" : "png") + + new CDNParams({expectedSize: 32, animated: this.animated}); emojiElem.alt = this.name; emojiElem.loading = "lazy"; + + const hover = new Hover(this.humanName); + hover.addEvent(emojiElem); + if (click) + emojiElem.onclick = async (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + const div = document.createElement("div"); + div.style.top = e.clientY + "px"; + div.style.left = e.clientX + "px"; + div.classList.add("flexttb", "EmojiGuildMenu"); + + const localuser = this.localuser as Localuser; + const lookup = await Emoji.lookupEmoji(this.id as string, localuser); + + const top = document.createElement("div"); + top.classList.add("flexltr", "GuildEmojiTop"); + + const toptext = document.createElement("div"); + toptext.classList.add("flexttb"); + div.append(toptext); + + const name = document.createElement("span"); + name.textContent = `:${this.humanName}:`; + + const desc = document.createElement("span"); + + toptext.append(name, desc); + top.append(this.getHTML(true, false), toptext); + + if (!lookup?.guild) { + desc.textContent = I18n.emoji.found.private(); + return; + } + const guild = localuser.guildids.get(lookup.guild?.id as string); + if (guild) { + if (localuser.lookingguild === guild) { + desc.textContent = I18n.emoji.found.this(); + } else { + desc.textContent = I18n.emoji.found.other(); + } + } else { + desc.textContent = I18n.emoji.found.not(); + } + + const h3 = document.createElement("h3"); + h3.textContent = I18n.emoji.from(); + + const guildRow = document.createElement("div"); + guildRow.classList.add("flexltr", "guildEmojiRow"); + + const guildText = document.createElement("div"); + guildText.classList.add("flexttb", "guildEmojiText"); + + const guildName = document.createElement("span"); + guildName.textContent = lookup.guild.name; + + const guildDesc = document.createElement("span"); + const discoverable = lookup.guild.features.find((_) => _ === "DISCOVERABLE"); + if (discoverable) { + if (lookup.guild.description) { + guildDesc.textContent = lookup.guild.description; + } + } else { + guildDesc.textContent = I18n.emoji.privateGuild(); + } + + guildText.append(guildName, guildDesc); + if (!guild && discoverable) { + const button = document.createElement("button"); + button.textContent = I18n.emoji.join(); + button.classList.add("emojiJoin"); + guildText.append(button); + button.onclick = async () => { + const joinRes = await fetch( + localuser.info.api + "/guilds/" + lookup.guild?.id + "/members/@me", + { + method: "PUT", + headers: localuser.headers, + }, + ); + if (joinRes.ok) { + removeAni(div); + } + }; + } + guildRow.append( + Guild.generateGuildIcon({...lookup.guild, info: localuser.info}, false) + .lastChild as HTMLElement, + guildText, + ); + div.append(top, h3, guildRow); + + document.body.append(div); + Contextmenu.keepOnScreen(div); + Contextmenu.declareMenu(div); + }; + return emojiElem; - }else if(this.emoji){ + } else if (this.emoji || this.name) { const emojiElem = document.createElement("span"); emojiElem.classList.add("md-emoji"); emojiElem.classList.add(bigemoji ? "bigemoji" : "smallemoji"); - emojiElem.textContent=this.emoji; + emojiElem.textContent = this.emoji || this.name; + + const hover = new Hover(this.humanName); + hover.addEvent(emojiElem); + return emojiElem; - }else{ + } else { throw new Error("This path should *never* be gone down, this means a malformed emoji"); } } - static decodeEmojiList(buffer: ArrayBuffer){ - const view = new DataView(buffer, 0); - let i = 0; - function read16(){ - const int = view.getUint16(i); - i += 2; - return int; - } - function read8(){ - const int = view.getUint8(i); - i += 1; - return int; - } - function readString8(){ - return readStringNo(read8()); - } - function readString16(){ - return readStringNo(read16()); - } - function readStringNo(length: number){ - const array = new Uint8Array(length); + static decodeEmojiList(buffer: ArrayBuffer) { + const reader = new BinRead(buffer); + const build: {name: string; emojis: {name: string; emoji: string}[]}[] = []; + let cats = reader.read16(); - for(let i = 0; i < length; i++){ - array[i] = read8(); - } - //console.log(array); - return new TextDecoder("utf8").decode(array.buffer as ArrayBuffer); - } - const build: { name: string; emojis: { name: string; emoji: string }[] }[] = - []; - let cats = read16(); - - for(; cats !== 0; cats--){ - const name = readString16(); + for (; cats !== 0; cats--) { + const name = reader.readString16(); const emojis: { name: string; skin_tone_support: boolean; emoji: string; }[] = []; - let emojinumber = read16(); - for(; emojinumber !== 0; emojinumber--){ + let emojinumber = reader.read16(); + for (; emojinumber !== 0; emojinumber--) { //console.log(emojis); - const name = readString8(); - const len = read8(); + const name = reader.readString8(); + const len = reader.read8(); const skin_tone_support = len > 127; - const emoji = readStringNo(len - Number(skin_tone_support) * 128); + const emoji = reader.readStringNo(len - Number(skin_tone_support) * 128); emojis.push({ name, skin_tone_support, @@ -122,172 +245,291 @@ class Emoji{ } this.emojis = build; } - static grabEmoji(){ + static grabEmoji() { fetch("/emoji.bin") - .then(e=>{ + .then((e) => { return e.arrayBuffer(); }) - .then(e=>{ + .then((e) => { Emoji.decodeEmojiList(e); }); } + static getEmojiFromIDOrString(idOrString: string, localuser: Localuser) { + for (const list of Emoji.emojis) { + const emj = list.emojis.find((_) => _.emoji === idOrString); + if (emj) { + return new Emoji(emj, localuser); + } + } + for (const guild of localuser.guilds) { + if (!guild.emojis) continue; + const emj = guild.emojis.find((_) => _.id === idOrString); + if (emj) { + return new Emoji(emj, localuser); + } + } + return new Emoji( + { + id: idOrString, + name: "", + }, + localuser, + ); + } static async emojiPicker( + this: typeof Emoji, x: number, y: number, - localuser: Localuser - ): Promise{ - let res: (r: Emoji | string) => void; - const promise: Promise = new Promise(r=>{ + localuser?: Localuser, + ): Promise { + let res: (r: Emoji) => void; + this; + const promise: Promise = new Promise((r) => { res = r; }); const menu = document.createElement("div"); menu.classList.add("flexttb", "emojiPicker"); - menu.style.top = y + "px"; - menu.style.left = x + "px"; + if (y > 0) { + menu.style.top = y + "px"; + } else { + menu.style.bottom = y * -1 + "px"; + } + if (x > 0) { + menu.style.left = x + "px"; + } else { + menu.style.right = x * -1 + "px"; + } + + const topBar = document.createElement("div"); + topBar.classList.add("flexltr", "emojiHeading"); const title = document.createElement("h2"); title.textContent = Emoji.emojis[0].name; title.classList.add("emojiTitle"); - menu.append(title); + topBar.append(title); + + const search = document.createElement("input"); + search.type = "text"; + topBar.append(search); + + let html: HTMLElement | undefined = undefined; + let topEmoji: undefined | Emoji = undefined; + function updateSearch(this: typeof Emoji) { + if (search.value === "") { + if (html) html.click(); + search.style.removeProperty("width"); + topEmoji = undefined; + return; + } + + search.style.setProperty("width", "3in"); + title.innerText = ""; + body.innerHTML = ""; + const searchResults = this.searchEmoji(search.value, localuser, 200); + if (searchResults[0]) { + topEmoji = searchResults[0][0]; + } + for (const [emoji] of searchResults) { + const emojiElem = document.createElement("div"); + emojiElem.classList.add("emojiSelect"); + + emojiElem.append(emoji.getHTML(false, false)); + body.append(emojiElem); + + emojiElem.addEventListener("click", () => { + res(emoji); + Contextmenu.declareMenu(); + }); + } + } + search.addEventListener("input", () => { + updateSearch.call(this); + }); + search.addEventListener("keyup", (e) => { + if (e.key === "Enter" && topEmoji) { + res(topEmoji); + Contextmenu.declareMenu(); + } + }); + + menu.append(topBar); + const selection = document.createElement("div"); selection.classList.add("flexltr", "emojirow"); const body = document.createElement("div"); body.classList.add("emojiBody"); let isFirst = true; - localuser.guilds - .filter(guild=>guild.id != "@me" && guild.emojis.length > 0) - .forEach(guild=>{ - const select = document.createElement("div"); - select.classList.add("emojiSelect"); - - if(guild.properties.icon){ - const img = document.createElement("img"); - img.classList.add("pfp", "servericon", "emoji-server"); - img.crossOrigin = "anonymous"; - img.src = localuser.info.cdn+"/icons/"+guild.properties.id+"/"+guild.properties.icon+".png?size=48"; - img.alt = "Server: " + guild.properties.name; - select.appendChild(img); - }else{ - const div = document.createElement("span"); - div.textContent = guild.properties.name - .replace(/'s /g, " ") - .replace(/\w+/g, word=>word[0]) - .replace(/\s/g, ""); - select.append(div); - } + if (localuser) { + [ + localuser.lookingguild, + ...localuser.guilds.filter((guild) => guild !== localuser.lookingguild), + ] + .filter((_) => _ !== undefined) + .filter((guild) => guild.id != "@me" && guild.emojis.length > 0) + .forEach((guild) => { + const select = document.createElement("div"); + select.classList.add("emojiSelect"); - selection.append(select); - - const clickEvent = ()=>{ - title.textContent = guild.properties.name; - body.innerHTML = ""; - for(const emojit of guild.emojis){ - const emojiElem = document.createElement("div"); - emojiElem.classList.add("emojiSelect"); - - const emojiClass = new Emoji( - { - id: emojit.id as string, - name: emojit.name, - animated: emojit.animated as boolean, - }, - localuser - ); - emojiElem.append(emojiClass.getHTML()); - body.append(emojiElem); - - emojiElem.addEventListener("click", ()=>{ - res(emojiClass); - if(Contextmenu.currentmenu !== ""){ - Contextmenu.currentmenu.remove(); - } - }); + if (guild.properties.icon) { + const img = document.createElement("img"); + img.classList.add("pfp", "servericon", "emoji-server"); + img.crossOrigin = "anonymous"; + img.src = + localuser.info.cdn + + "/icons/" + + guild.properties.id + + "/" + + guild.properties.icon + + ".png" + + new CDNParams({expectedSize: 48}); + img.alt = "Server: " + guild.properties.name; + select.appendChild(img); + } else { + const div = document.createElement("span"); + div.textContent = guild.properties.name + .replace(/'s /g, " ") + .replace(/\w+/g, (word) => word[0]) + .replace(/\s/g, ""); + select.append(div); } - }; - select.addEventListener("click", clickEvent); - if(isFirst){ - clickEvent(); - isFirst = false; - } - }); + selection.append(select); - setTimeout(()=>{ - if(Contextmenu.currentmenu != ""){ - Contextmenu.currentmenu.remove(); - } - document.body.append(menu); - Contextmenu.currentmenu = menu; - Contextmenu.keepOnScreen(menu); - }, 10); + const clickEvent = () => { + search.value = ""; + updateSearch.call(this); + title.textContent = guild.properties.name; + body.innerHTML = ""; + for (const emojit of guild.emojis) { + const emojiElem = document.createElement("div"); + emojiElem.classList.add("emojiSelect"); + + const emojiClass = new Emoji( + { + id: emojit.id as string, + name: emojit.name, + animated: emojit.animated as boolean, + }, + localuser, + ); + emojiElem.append(emojiClass.getHTML(false, false)); + body.append(emojiElem); + + emojiElem.addEventListener("click", () => { + res(emojiClass); + Contextmenu.declareMenu(); + }); + } + }; + + select.addEventListener("click", clickEvent); + if (isFirst) { + clickEvent(); + isFirst = false; + } + }); + } let i = 0; - for(const thing of Emoji.emojis){ + + Contextmenu.declareMenu(menu); + document.body.append(menu); + Contextmenu.keepOnScreen(menu); + const recent = localuser?.favorites.emojiFreq(); + if (recent && recent.length >= 15 && localuser) { + const select = document.createElement("div"); + select.classList.add("svg-history"); + selection.append(select); + console.log("appended"); + select.onclick = () => { + title.textContent = I18n.recentEmoji(); + body.innerHTML = ""; + for (const [emj] of recent) { + const emoji = Emoji.getEmojiFromIDOrString(emj, localuser); + const emojihtml = document.createElement("div"); + emojihtml.classList.add("emojiSelect"); + emojihtml.append(emoji.getHTML(false, false)); + body.append(emojihtml); + emojihtml.onclick = (_) => { + res(emoji); + Contextmenu.declareMenu(); + }; + } + }; + select.click(); + i++; + } + + for (const thing of Emoji.emojis) { const select = document.createElement("div"); select.textContent = thing.emojis[0].emoji; select.classList.add("emojiSelect"); selection.append(select); - const clickEvent = ()=>{ + const clickEvent = () => { + search.value = ""; + updateSearch.call(this); title.textContent = thing.name; body.innerHTML = ""; - for(const emojit of thing.emojis){ + for (const emojit of thing.emojis.map((_) => new Emoji(_, localuser))) { const emoji = document.createElement("div"); emoji.classList.add("emojiSelect"); - emoji.textContent = emojit.emoji; + emoji.append(emojit.getHTML(false, false)); body.append(emoji); - emoji.onclick = _=>{ - res(emojit.emoji); - if(Contextmenu.currentmenu !== ""){ - Contextmenu.currentmenu.remove(); - } + emoji.onclick = (_) => { + res(emojit); + Contextmenu.declareMenu(); }; } }; select.onclick = clickEvent; - if(i === 0){ + if (i === 0) { + html = select; clickEvent(); } i++; } menu.append(selection); menu.append(body); + search.focus(); return promise; } - static searchEmoji(search:string,localuser:Localuser,results=50):[Emoji,number][]{ - const ranked:[emojijson,number][]=[]; - function similar(json:emojijson){ - if(json.name.includes(search)){ - ranked.push([json,search.length/json.name.length]); + static searchEmoji(search: string, localuser?: Localuser, results = 50): [Emoji, number][] { + //NOTE this function is used for searching in the emoji picker for reactions, and the emoji auto-fill + const ranked: [emojijson, number][] = []; + function similar(json: emojijson) { + if (json.name.includes(search)) { + ranked.push([json, search.length / json.name.length]); return true; - }else if(json.name.toLowerCase().includes(search.toLowerCase())){ - ranked.push([json,search.length/json.name.length/1.4]); + } else if (json.name.toLowerCase().includes(search.toLowerCase())) { + ranked.push([json, search.length / json.name.length / 1.4]); return true; - }else{ + } else { return false; } } - for(const group of this.emojis){ - for(const emoji of group.emojis){ - similar(emoji) + for (const group of this.emojis) { + for (const emoji of group.emojis) { + similar(emoji); } } - const weakGuild=new WeakMap(); - for(const guild of localuser.guilds){ - if(guild.id!=="@me"&&guild.emojis.length!==0){ - for(const emoji of guild.emojis){ - if(similar(emoji)){ - weakGuild.set(emoji,guild); - }; + const weakGuild = new WeakMap(); + if (localuser) { + for (const guild of localuser.guilds) { + if (guild.id !== "@me" && guild.emojis.length !== 0) { + for (const emoji of guild.emojis) { + if (similar(emoji)) { + weakGuild.set(emoji, guild); + } + } } } } - ranked.sort((a,b)=>b[1]-a[1]); - return ranked.splice(0,results).map(a=>{ - return [new Emoji(a[0],weakGuild.get(a[0])||localuser),a[1]]; - - }) + ranked.sort((a, b) => b[1] - a[1]); + return ranked.splice(0, results).map((a) => { + return [new Emoji(a[0], weakGuild.get(a[0]) || localuser), a[1]]; + }); } } Emoji.grabEmoji(); -export{ Emoji }; +export {Emoji}; diff --git a/src/webpage/emoji/BlobmojiCompat.ttf b/src/webpage/emoji/BlobmojiCompat.ttf new file mode 100644 index 00000000..8d738903 Binary files /dev/null and b/src/webpage/emoji/BlobmojiCompat.ttf differ diff --git a/src/webpage/emoji/NotoColorEmoji-Regular.ttf b/src/webpage/emoji/NotoColorEmoji-Regular.ttf new file mode 100644 index 00000000..5d7a86f3 Binary files /dev/null and b/src/webpage/emoji/NotoColorEmoji-Regular.ttf differ diff --git a/src/webpage/emoji/OFL.txt b/src/webpage/emoji/OFL.txt new file mode 100644 index 00000000..979c943e --- /dev/null +++ b/src/webpage/emoji/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2021 Google Inc. All Rights Reserved. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/webpage/emoji/OpenMoji-color-glyf_colr_0.woff2 b/src/webpage/emoji/OpenMoji-color-glyf_colr_0.woff2 new file mode 100644 index 00000000..57623bd9 Binary files /dev/null and b/src/webpage/emoji/OpenMoji-color-glyf_colr_0.woff2 differ diff --git a/src/webpage/emoji/Twemoji-16.0.1.ttf b/src/webpage/emoji/Twemoji-16.0.1.ttf new file mode 100644 index 00000000..1942b04a Binary files /dev/null and b/src/webpage/emoji/Twemoji-16.0.1.ttf differ diff --git a/src/webpage/favorites.ts b/src/webpage/favorites.ts new file mode 100644 index 00000000..207e035f --- /dev/null +++ b/src/webpage/favorites.ts @@ -0,0 +1,494 @@ +import {favandfreq, freq} from "./jsontypes.js"; +import {Localuser} from "./localuser.js"; +const enum saveImportance { + no = 0, + verylow = 1, + low = 2, + medium = 3, + high = 4, +} +interface permStore { + current: { + gifs: favandfreq["favoriteGifs"]["gifs"]; + emojiFrecency: favandfreq["emojiFrecency"]["emojis"]; + emojiReactionFrecency: favandfreq["emojiReactionFrecency"]["emojis"]; + guildAndChannelFrecency: favandfreq["guildAndChannelFrecency"]["guildAndChannels"]; + favorite_stickers: string[]; + sticker_frecency: favandfreq["sticker_frecency"]["stickers"]; + favorite_emojis: string[]; + }; + needsSave: saveImportance; + lastSave: number; + lastDecay: number; + old: favandfreqimp; +} +type RecursivePartial = { + [P in keyof T]?: T[P] extends (infer U)[] + ? RecursivePartial[] + : T[P] extends object | undefined + ? RecursivePartial + : T[P]; +}; + +type favandfreqimp = Omit< + favandfreq, + | "application_command_frecency" + | "favorite_soundboard_sounds" + | "application_frecency" + | "heard_sound_frecency" + | "played_sound_frecency" +>; + +export class Favorites { + owner: Localuser; + // ----- stuff that needs to store ---- + private gifs: favandfreq["favoriteGifs"]["gifs"] = {}; + private emojiFrecency: favandfreq["emojiFrecency"]["emojis"] = {}; + private emojiReactionFrecency: favandfreq["emojiReactionFrecency"]["emojis"] = {}; + private guildAndChannelFrecency: favandfreq["guildAndChannelFrecency"]["guildAndChannels"] = {}; + private favorite_stickers: string[] = []; + private sticker_frecency: favandfreq["sticker_frecency"]["stickers"] = {}; + private favorite_emojis: string[] = []; + needsSave: saveImportance = 0; + lastSave = 0; + lastDecay = 0; + old!: favandfreqimp; + // ----- end of stuff that needs to store ---- + get info() { + return this.owner.info; + } + get headers() { + return this.owner.headers; + } + constructor(owner: Localuser) { + this.owner = owner; + this.setup(); + } + get store() { + return (this.owner.perminfo.favoriteStore || {}) as permStore; + } + hasGif(gif: string) { + return !!this.gifs[gif]; + } + loadFromLocal() { + const store: RecursivePartial = this.store; + store.current ||= {}; + store.current.gifs ||= {}; + store.current.emojiFrecency ||= {}; + store.current.emojiReactionFrecency ||= {}; + store.current.guildAndChannelFrecency ||= {}; + store.current.sticker_frecency ||= {}; + store.current.favorite_stickers = []; + store.current.favorite_emojis = []; + function deapClone(clone: any) { + const val = JSON.parse(JSON.stringify(clone)); + return val; + } + const old = deapClone((store.old ||= {})) as Partial; + old.favoriteGifs ||= { + gifs: {}, + hideTooltip: false, + }; + old.emojiFrecency ||= { + emojis: {}, + }; + old.emojiReactionFrecency ||= { + emojis: {}, + }; + old.guildAndChannelFrecency ||= { + guildAndChannels: {}, + }; + + old.favorite_emojis ||= {emojis: []}; + old.favorite_stickers ||= {sticker_ids: []}; + old.sticker_frecency ||= { + stickers: {}, + }; + store.old = old; + + store.needsSave ||= 0; + store.lastSave ||= Date.now(); + store.lastDecay ||= Date.now(); + + this.needsSave = store.needsSave; + this.lastSave = store.lastSave; + this.lastDecay = store.lastDecay; + + this.old = old as favandfreq; + + this.favorite_emojis = deapClone(store.current.favorite_emojis); + this.sticker_frecency = deapClone(store.current.sticker_frecency); + this.favorite_stickers = deapClone(store.current.favorite_stickers); + this.gifs = deapClone(store.current.gifs); + this.emojiFrecency = deapClone(store.current.emojiFrecency); + this.emojiReactionFrecency = deapClone(store.current.emojiReactionFrecency); + this.guildAndChannelFrecency = deapClone(store.current.guildAndChannelFrecency); + } + saveLocal() { + const save: permStore = { + old: this.old, + current: { + emojiFrecency: this.emojiFrecency, + gifs: this.gifs, + emojiReactionFrecency: this.emojiReactionFrecency, + guildAndChannelFrecency: this.guildAndChannelFrecency, + favorite_stickers: this.favorite_stickers, + sticker_frecency: this.sticker_frecency, + favorite_emojis: this.favorite_emojis, + }, + lastDecay: this.lastDecay, + lastSave: this.lastSave, + needsSave: this.needsSave, + }; + this.owner.perminfo.favoriteStore = save; + } + async saveNetwork() { + await this.startSync(false); + const body: {settings: favandfreqimp} = { + settings: { + favoriteGifs: { + gifs: this.gifs, + hideTooltip: false, + }, + emojiFrecency: { + emojis: this.emojiFrecency, + }, + emojiReactionFrecency: { + emojis: this.emojiReactionFrecency, + }, + guildAndChannelFrecency: { + guildAndChannels: this.guildAndChannelFrecency, + }, + favorite_stickers: {sticker_ids: this.favorite_stickers}, + sticker_frecency: { + stickers: this.sticker_frecency, + }, + favorite_emojis: {emojis: this.favorite_emojis}, + }, + }; + const res = await fetch(this.info.api + "/users/@me/settings-proto/2/json", { + method: "PATCH", + headers: this.headers, + body: JSON.stringify(body), + }); + if (res.ok) { + this.needsSave = saveImportance.no; + this.lastSave = Date.now(); + } + } + async startSync(save = true) { + const sat = fetch(this.info.api + "/users/@me/settings-proto/2/json", { + headers: this.headers, + }); + const res: {settings: Partial} = await (await sat).json(); + this.saveDifs(res.settings, save); + } + async setup() { + try { + this.loadFromLocal(); + await this.startSync(); + } catch (e) { + console.error(e); + } + } + getOld(): favandfreqimp { + return this.old; + } + setOld(newold: favandfreqimp) { + this.old = structuredClone(newold); + this.saveLocal(); + return; + } + mixNewOldNetwork( + newf: {[key: string]: freq}, + old: {[key: string]: freq}, + net: {[key: string]: freq}, + ) { + const oldKeys = new Set(Object.keys(old)); + const newKeys = new Set(Object.keys(newf)); + const removedKeys = oldKeys.difference(newKeys); + const addedKeys = newKeys.difference(oldKeys); + for (const key of removedKeys) { + delete net[key]; + } + for (const key of addedKeys) { + if (net[key]) { + const newEmj = newf[key]; + const netEmj = net[key]; + netEmj.totalUses += newEmj.totalUses; + netEmj.score += newEmj.score; + netEmj.recentUses = newEmj.recentUses + .concat(netEmj.recentUses) + .sort((a, b) => Number(BigInt(b) - BigInt(a))) + .splice(0, 20); + } else { + net[key] = newf[key]; + } + } + const sharedKeys = newKeys.intersection(oldKeys); + for (const key of sharedKeys) { + const oldEmj = old[key]; + const newEmj = newf[key]; + if (net[key]) { + const netEmj = net[key]; + netEmj.totalUses += newEmj.totalUses - oldEmj.totalUses; + netEmj.score += newEmj.score - oldEmj.score; + + netEmj.recentUses = newEmj.recentUses + .concat(netEmj.recentUses) + .sort((a, b) => Number(BigInt(b) - BigInt(a))) + .splice(0, 20); + } else { + net[key] = newEmj; + } + } + } + saveDifs(diffs: Partial, save = true) { + const old = this.getOld(); + if (diffs.favoriteGifs?.gifs) { + const oldKeys = new Set(Object.keys(old.favoriteGifs.gifs)); + const newKeys = new Set(Object.keys(this.gifs)); + const removedKeys = oldKeys.difference(newKeys); + const addedKeys = newKeys.difference(oldKeys); + for (const key of removedKeys) { + delete diffs.favoriteGifs.gifs[key]; + } + for (const key of addedKeys) { + diffs.favoriteGifs.gifs[key] = this.gifs[key]; + } + old.favoriteGifs.gifs = this.gifs = diffs.favoriteGifs.gifs; + } + + if (diffs.favorite_stickers?.sticker_ids) { + const oldKeys = new Set(Object.keys(old.favorite_stickers.sticker_ids)); + const newKeys = new Set(Object.keys(this.favorite_stickers)); + const removedKeys = oldKeys.difference(newKeys); + const addedKeys = newKeys.difference(oldKeys); + + diffs.favorite_stickers.sticker_ids = diffs.favorite_stickers.sticker_ids.filter( + (_) => !removedKeys.has(_), + ); + + diffs.favorite_stickers.sticker_ids = [ + ...new Set([...diffs.favorite_stickers.sticker_ids, ...addedKeys]), + ]; + + old.favorite_stickers.sticker_ids = this.favorite_stickers = + diffs.favorite_stickers.sticker_ids; + } + + if (diffs.favorite_emojis?.emojis) { + const oldKeys = new Set(Object.keys(old.favorite_emojis.emojis)); + const newKeys = new Set(Object.keys(this.favorite_emojis)); + const removedKeys = oldKeys.difference(newKeys); + const addedKeys = newKeys.difference(oldKeys); + + diffs.favorite_emojis.emojis = diffs.favorite_emojis.emojis.filter( + (_) => !removedKeys.has(_), + ); + + diffs.favorite_emojis.emojis = [...new Set([...diffs.favorite_emojis.emojis, ...addedKeys])]; + + old.favorite_emojis.emojis = this.favorite_emojis = diffs.favorite_emojis.emojis; + } + + if (diffs.emojiFrecency?.emojis) { + this.mixNewOldNetwork( + this.emojiFrecency, + old.emojiFrecency.emojis, + diffs.emojiFrecency.emojis, + ); + old.emojiFrecency.emojis = this.emojiFrecency = diffs.emojiFrecency.emojis; + } + if (diffs.emojiReactionFrecency?.emojis) { + this.mixNewOldNetwork( + this.emojiReactionFrecency, + old.emojiReactionFrecency.emojis, + diffs.emojiReactionFrecency.emojis, + ); + old.emojiReactionFrecency.emojis = this.emojiReactionFrecency = + diffs.emojiReactionFrecency.emojis; + } + if (diffs.guildAndChannelFrecency?.guildAndChannels) { + this.mixNewOldNetwork( + this.guildAndChannelFrecency, + old.guildAndChannelFrecency.guildAndChannels, + diffs.guildAndChannelFrecency.guildAndChannels, + ); + old.guildAndChannelFrecency.guildAndChannels = this.guildAndChannelFrecency = + diffs.guildAndChannelFrecency.guildAndChannels; + } + + if (diffs.sticker_frecency?.stickers) { + this.mixNewOldNetwork( + this.sticker_frecency, + old.sticker_frecency.stickers, + diffs.sticker_frecency.stickers, + ); + old.sticker_frecency.stickers = this.sticker_frecency = diffs.sticker_frecency.stickers; + } + + this.setOld(old); + + this.decayScore(save); + } + favoriteGifs() { + return structuredClone(Object.values(this.gifs).sort((a, b) => a.order - b.order)); + } + favoriteStickers() { + return structuredClone(this.favorite_stickers); + } + stickersFreq() { + return Object.entries(this.sticker_frecency).sort((a, b) => b[1].score - a[1].score); + } + favoriteEmojis() { + return structuredClone(this.favorite_emojis); + } + emojiFreq() { + return Object.entries(this.emojiFrecency).sort((a, b) => b[1].score - a[1].score); + } + emojiReactFreq() { + return Object.entries(this.emojiReactionFrecency).sort((a, b) => b[1].score - a[1].score); + } + async addEmoji(nameOrID: string) { + const obj = (this.emojiFrecency[nameOrID] ??= { + totalUses: 0, + recentUses: [], + frecency: -1, + score: 0, + }); + obj.totalUses++; + obj.recentUses.unshift(Math.floor(Date.now()) + ""); + obj.recentUses = obj.recentUses.splice(0, 20); + obj.score += 100; + await this.save(saveImportance.low); + } + async addStickerFreq(id: string) { + const obj = (this.sticker_frecency[id] ??= { + totalUses: 0, + recentUses: [], + frecency: -1, + score: 0, + }); + obj.totalUses++; + obj.recentUses.unshift(Math.floor(Date.now()) + ""); + obj.recentUses = obj.recentUses.splice(0, 20); + obj.score += 100; + await this.save(saveImportance.low); + } + async addReactEmoji(nameOrID: string) { + const obj = (this.emojiReactionFrecency[nameOrID] ??= { + totalUses: 0, + recentUses: [], + frecency: -1, + score: 0, + }); + obj.totalUses++; + obj.recentUses.unshift(Math.floor(Date.now()) + ""); + obj.recentUses = obj.recentUses.splice(0, 20); + obj.score += 100; + await this.save(saveImportance.low); + } + async addChannelGuild(ID: string) { + const obj = (this.guildAndChannelFrecency[ID] ??= { + totalUses: 0, + recentUses: [], + frecency: -1, + score: 0, + }); + obj.totalUses++; + obj.recentUses.unshift(Math.floor(Date.now()) + ""); + obj.recentUses = obj.recentUses.splice(0, 5); + obj.score += 100; + await this.save(saveImportance.verylow); + } + async unfavoriteGif(name: string) { + delete this.gifs[name]; + await this.save(saveImportance.high); + } + + async favoriteSticker(id: string) { + this.favorite_stickers.push(id); + await this.save(saveImportance.high); + } + async unfavoriteSticker(id: string) { + this.favorite_stickers = this.favorite_stickers.filter((_) => _ !== id); + await this.save(saveImportance.high); + } + + async favoriteEmoji(idorname: string) { + this.favorite_emojis.push(idorname); + await this.save(saveImportance.high); + } + async unfavoriteEmoji(idorname: string) { + this.favorite_emojis = this.favorite_emojis.filter((_) => _ !== idorname); + await this.save(saveImportance.high); + } + + async favoriteGif( + name: string, + gif: Omit, + ) { + this.gifs[name] = { + ...gif, + width: Math.round(gif.width), + height: Math.round(gif.height), + format: "GIF_TYPE_IMAGE", + order: Object.keys(this.gifs).length + 1, + }; + await this.save(saveImportance.high); + } + async removeFavoriteGif(name: string) { + delete this.gifs[name]; + await this.save(saveImportance.high); + } + + async decayScore(save = true) { + const delta = (Date.now() - this.lastDecay) / 1000 / 60 / 60 / 12; + if (delta < 1) { + //Don't decay scores if less than 12 hours since last decay + return; + } + // Did some math and I found I liked these values for g and n + // f\left(x\right)=\left(\frac{x}{g}+1\right)^{-n} + const decay = (delta / 20 + 1) ** -0.35; + Object.values(this.emojiFrecency).forEach((emoji) => { + emoji.score *= decay; + emoji.score ^= 0; + }); + Object.values(this.emojiReactionFrecency).forEach((emoji) => { + emoji.score *= decay; + emoji.score ^= 0; + }); + Object.values(this.guildAndChannelFrecency).forEach((thing) => { + thing.score *= decay; + thing.score ^= 0; + }); + this.lastDecay = Date.now(); + if (save) await this.save(saveImportance.low); + } + + async save(importance: saveImportance) { + this.saveLocal(); + let time = 0; + this.needsSave = Math.max(this.needsSave, importance); + switch (this.needsSave) { + case saveImportance.no: + case saveImportance.verylow: + return; + case saveImportance.low: + time = 24 * 60 * 60 * 1000; + break; + case saveImportance.medium: + time = 30 * 60 * 1000; + break; + case saveImportance.high: + await this.saveNetwork(); + return; + } + if (this.lastSave + time < Date.now()) { + await this.saveNetwork(); + return; + } + } +} diff --git a/src/webpage/file.ts b/src/webpage/file.ts index a16dcb57..ec234d40 100644 --- a/src/webpage/file.ts +++ b/src/webpage/file.ts @@ -1,9 +1,11 @@ -import{ Message }from"./message.js"; -import{ filejson }from"./jsontypes.js"; -import { ImagesDisplay } from "./disimg.js"; - -class File{ - owner: Message | null; +import {Message} from "./message.js"; +import {filejson} from "./jsontypes.js"; +import {ImagesDisplay} from "./disimg.js"; +import {makePlayBox, MediaPlayer} from "./media.js"; +import {I18n} from "./i18n.js"; +import {createImg} from "./utils/utils.js"; +class File { + readonly owner: Message | null; id: string; filename: string; content_type: string; @@ -12,7 +14,8 @@ class File{ proxy_url: string | undefined; url: string; size: number; - constructor(fileJSON: filejson, owner: Message | null){ + files?: File[]; + constructor(fileJSON: filejson, owner: Message | null) { this.owner = owner; this.id = fileJSON.id; this.filename = fileJSON.filename; @@ -24,9 +27,22 @@ class File{ this.content_type = fileJSON.content_type; this.size = fileJSON.size; } - getHTML(temp: boolean = false): HTMLElement{ + getHTML(temp: boolean = false, fullScreen = false, OSpoiler = false): HTMLElement { + function makeSpoilerHTML(): HTMLElement { + const spoil = document.createElement("div"); + spoil.classList.add("fSpoil"); + const stext = document.createElement("span"); + stext.textContent = I18n.spoiler(); + spoil.append(stext); + spoil.onclick = () => spoil.remove(); + return spoil; + } + OSpoiler ||= this.filename.startsWith("SPOILER_"); + const src = this.proxy_url || this.url; - if(this.width && this.height){ + const url = this.refreshURL(); + + if (this.width && this.height) { let scale = 1; const max = 96 * 3; scale = Math.max(scale, this.width / max); @@ -34,66 +50,183 @@ class File{ this.width /= scale; this.height /= scale; } - if(this.content_type.startsWith("image/")){ + if (this.height === null) { + this.height = 96 * 3; + } + + if (this.content_type.startsWith("image/")) { const div = document.createElement("div"); - const img = document.createElement("img"); - img.classList.add("messageimg"); - div.classList.add("messageimgdiv"); - img.onclick = function(){ - const full = new ImagesDisplay([img.src]); - full.show(); - }; - img.src = src; + const img = createImg(src, undefined, div); + if (!fullScreen) { + img.classList.add("messageimg"); + div.classList.add("messageimgdiv"); + img.onclick = () => { + if (this.owner) { + const full = new ImagesDisplay( + this.files || this.owner.attachments, + (this.files || this.owner.attachments).indexOf(this), + ); + full.show(); + } else { + const full = new ImagesDisplay([this]); + full.show(); + } + }; + } else { + img.onclick = (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + }; + } + + if (url) + url.then((src) => { + img.setSrcs(src); + }); div.append(img); - if(this.width){ + if (this.width && !fullScreen) { div.style.width = this.width + "px"; div.style.height = this.height + "px"; + } else if (!fullScreen) { + div.style.maxWidth = 96 * 3 + "px"; + div.style.maxHeight = 96 * 3 + "px"; + } + img.isAnimated().then((animated) => { + if (!animated || !this.owner || fullScreen) return; + const url = new URL(this.url).origin + new URL(this.url).pathname; + const span = document.createElement("span"); + span.classList.add("svg-gifstar"); + if (this.owner.localuser.favorites.hasGif(url)) { + span.classList.add("favorited"); + } + div.append(span); + + span.onclick = () => { + if (!this.owner || !this.width || !this.height) return; + const fav = this.owner.localuser.favorites; + + if (fav.hasGif(url)) { + span.classList.remove("favorited"); + fav.unfavoriteGif(url); + } else { + span.classList.add("favorited"); + fav.favoriteGif(url, { + src: url, + width: this.width, + height: this.height, + }); + } + }; + }); + if (!fullScreen) { + if (OSpoiler) { + div.append(makeSpoilerHTML()); + } } return div; - }else if(this.content_type.startsWith("video/")){ + } else if (this.content_type.startsWith("video/")) { const video = document.createElement("video"); const source = document.createElement("source"); source.src = src; + if (url) + url.then((src) => { + source.src = src; + }); video.append(source); - source.type = this.content_type; + //source.type = this.content_type; video.controls = !temp; - if(this.width && this.height){ - video.width = this.width; - video.height = this.height; + + if (this.width) video.width = this.width; + if (this.height) video.height = this.height; + + if (OSpoiler) { + const div = document.createElement("div"); + div.style.setProperty("position", "relative"); + div.append(video, makeSpoilerHTML()); + return div; } return video; - }else if(this.content_type.startsWith("audio/")){ - const audio = document.createElement("audio"); - const source = document.createElement("source"); - source.src = src; - audio.append(source); - source.type = this.content_type; - audio.controls = !temp; - return audio; - }else{ - return this.createunknown(); + } else if (this.content_type.startsWith("audio/")) { + const a = this.getAudioHTML(url); + if (OSpoiler) { + a.append(makeSpoilerHTML()); + } + return a; + } else { + const uk = this.createunknown(url); + if (OSpoiler) { + uk.append(makeSpoilerHTML()); + } + return uk; + } + } + refreshURL(url = this.proxy_url || this.url): Promise | void { + if (!this.owner) return; + const urlObj = new URL(url); + if (urlObj.host === new URL(this.owner.info.cdn).host) { + if (Number.parseInt(urlObj.searchParams.get("ex") || "", 16) >= Date.now() - 5000) { + return; + } + const newUrl = this.owner.localuser.refreshURL(url); + newUrl.then((_) => (this.proxy_url = _)); + return newUrl; } } - upHTML(files: Blob[], file: globalThis.File): HTMLElement{ + private getAudioHTML(url: Promise | void) { + const src = this.proxy_url || this.url; + return makePlayBox(src, player, 0, url); + } + upHTML(files: Blob[], map: WeakMap, file: globalThis.File): HTMLElement { const div = document.createElement("div"); - const contained = this.getHTML(true); + let contained = this.getHTML(true, false, file.name.startsWith("SPOILER_")); div.classList.add("containedFile"); div.append(contained); const controls = document.createElement("div"); + controls.classList.add("controls"); + const garbage = document.createElement("button"); const icon = document.createElement("span"); - icon.classList.add("svgicon","svg-delete"); + icon.classList.add("svgicon", "svg-delete"); garbage.append(icon); - garbage.onclick = _=>{ + garbage.onclick = (_) => { div.remove(); files.splice(files.indexOf(file), 1); }; - controls.classList.add("controls"); + + const spoiler = document.createElement("button"); + const sicon = document.createElement("span"); + sicon.classList.add( + "svgicon", + file.name.startsWith("SPOILER_") ? "svg-unspoiler" : "svg-spoiler", + ); + spoiler.append(sicon); + spoiler.onclick = (_) => { + if (file.name.startsWith("SPOILER_")) { + const name = file.name.split("SPOILER_"); + name.shift(); + file = files[files.indexOf(file)] = new globalThis.File([file], name.join("SPOILER_"), { + type: file.type, + }); + sicon.classList.add("svg-spoiler"); + sicon.classList.remove("svg-unspoiler"); + } else { + file = files[files.indexOf(file)] = new globalThis.File([file], "SPOILER_" + file.name, { + type: file.type, + }); + sicon.classList.add("svg-unspoiler"); + sicon.classList.remove("svg-spoiler"); + } + map.set(file, div); + contained.remove(); + contained = this.getHTML(true, false, file.name.startsWith("SPOILER_")); + div.append(contained); + }; + div.append(controls); - controls.append(garbage); + controls.append(spoiler, garbage); return div; } - static initFromBlob(file: globalThis.File){ + static initFromBlob(file: globalThis.File) { return new File( { filename: file.name, @@ -105,10 +238,10 @@ class File{ url: URL.createObjectURL(file), proxy_url: undefined, }, - null + null, ); } - createunknown(): HTMLElement{ + createunknown(url: Promise | void): HTMLElement { console.log("🗎"); const src = this.proxy_url || this.url; const div = document.createElement("table"); @@ -121,12 +254,16 @@ class File{ fileicon.classList.add("fileicon"); fileicon.rowSpan = 2; const nametd = document.createElement("td"); - if(src){ + if (src) { const a = document.createElement("a"); a.href = src; + if (url) + url.then((_) => { + a.href = _; + }); a.textContent = this.filename; nametd.append(a); - }else{ + } else { nametd.textContent = this.filename; } @@ -140,11 +277,15 @@ class File{ div.appendChild(sizetr); return div; } - static filesizehuman(fsize: number){ + static filesizehuman(fsize: number) { const i = fsize == 0 ? 0 : Math.floor(Math.log(fsize) / Math.log(1024)); - return( - Number((fsize / Math.pow(1024, i)).toFixed(2)) * 1 + " " + ["Bytes", "Kilobytes", "Megabytes", "Gigabytes", "Terabytes"][i] // I don't think this changes across languages, correct me if I'm wrong + return ( + Number((fsize / Math.pow(1024, i)).toFixed(2)) * 1 + + " " + + ["Bytes", "Kilobytes", "Megabytes", "Gigabytes", "Terabytes"][i] // I don't think this changes across languages, correct me if I'm wrong ); } } -export{ File }; + +const player = new MediaPlayer(); +export {File}; diff --git a/src/webpage/guild.ts b/src/webpage/guild.ts index d50274ea..c700c2c7 100644 --- a/src/webpage/guild.ts +++ b/src/webpage/guild.ts @@ -1,16 +1,179 @@ -import{ Channel }from"./channel.js"; -import{ Localuser }from"./localuser.js"; -import{ Contextmenu }from"./contextmenu.js"; -import{ Role, RoleList }from"./role.js"; -import{ Member }from"./member.js"; -import{ Dialog, Options, Settings }from"./settings.js"; -import{ Permissions }from"./permissions.js"; -import{ SnowFlake }from"./snowflake.js"; -import{channeljson,guildjson,emojijson,memberjson,invitejson,rolesjson,}from"./jsontypes.js"; -import{ User }from"./user.js"; -import { I18n } from "./i18n.js"; - -class Guild extends SnowFlake{ +import {Channel} from "./channel.js"; +import {Localuser} from "./localuser.js"; +import {Contextmenu} from "./contextmenu.js"; +import {Role, RoleList} from "./role.js"; +import {Member} from "./member.js"; +import {Dialog, FormError, Options, Settings} from "./settings.js"; +import {Permissions} from "./permissions.js"; +import {SnowFlake} from "./snowflake.js"; +import { + channeljson, + guildjson, + memberjson, + invitejson, + rolesjson, + emojipjson, + extendedProperties, + banObj, + templateSkim, + mute_config, + GuildOverrides, + commandJson, + applicationJson, + presencejson, + welcomeScreen, +} from "./jsontypes.js"; +import {User} from "./user.js"; +import {I18n} from "./i18n.js"; +import {Emoji} from "./emoji.js"; +import {webhookMenu} from "./webhooks.js"; +import {createImg} from "./utils/utils.js"; +import {Sticker} from "./sticker.js"; +import {ProgessiveDecodeJSON} from "./utils/progessiveLoad.js"; +import {MarkDown} from "./markdown.js"; +import {Command} from "./interactions/commands.js"; +import {Hover} from "./hover.js"; +import {ReportMenu} from "./reporting/report.js"; +import {getDeveloperSettings} from "./utils/storage/devSettings.js"; +import {CDNParams} from "./utils/cdnParams.js"; +export async function makeInviteMenu(inviteMenu: Options, guild: Guild, url: string) { + const invDiv = document.createElement("div"); + const bansp = ProgessiveDecodeJSON(url, { + headers: guild.headers, + }); + const createInviteHTML = (invite: invitejson) => { + const div = document.createElement("div"); + div.classList.add("templateMiniBox"); + + const edit = document.createElement("button"); + edit.textContent = I18n.edit(); + + const code = document.createElement("span"); + code.textContent = invite.code; + + const used = document.createElement("span"); + used.textContent = I18n.invite.used(invite.uses + ""); + + edit.onclick = () => { + const opt = inviteMenu.addSubOptions(invite.code); + const inviter = new User(invite.inviter, guild.localuser); + + opt.addMDText( + new MarkDown( + window.location.origin + + "/invite/" + + invite.code + + "?" + + new URLSearchParams([["instance", guild.info.wellknown]]), + undefined, + ), + ); + + opt.addText(I18n.invite.used(invite.uses + "")); + if (invite.max_uses !== 0) opt.addText(I18n.invite.maxUses(invite.max_uses + "")); + + const channel = guild.channels.find((_) => _.id == invite.channel_id); + if (channel) { + opt.addText(I18n.invite.forChannel(channel.name)); + } + + opt.addText(I18n.invite.createdAt(new Date(invite.created_at).toLocaleDateString(I18n.lang))); + + let expires = I18n.invite.never(); + if (invite.expires_at) { + expires = new Date(invite.expires_at).toLocaleDateString(I18n.lang); + } + opt.addText(I18n.invite.expires(expires)); + + opt.addText(I18n.webhooks.createdBy()); + opt.addHTMLArea(inviter.createWidget(guild)); + + opt.addButtonInput("", I18n.delete(), async () => { + if ( + ( + await fetch(guild.info.api + "/invites/" + invite.code, { + method: "DELETE", + headers: guild.headers, + }) + ).ok + ) { + invsArr = invsArr.filter((_) => _ !== invite); + inviteMenu.returnFromSub(); + loadPage(currentPage); + } + }); + }; + + div.append(used, code, edit); + return div; + }; + let invsArr: invitejson[] = []; + let onpage = 0; + + async function loadArr() { + let invsArr2: invitejson[] = []; + let waiting = false; + async function addHTML() { + if (waiting) return; + waiting = true; + await new Promise((res) => setTimeout(res, 0)); + waiting = false; + invDiv.append(...invsArr2.map((inv) => createInviteHTML(inv))); + invsArr2 = []; + } + while (!(await bansp).done) { + const inv = await (await (await bansp).getNext()).getWhole(); + invsArr.push(inv); + if (onpage < 50) { + invsArr2.push(inv); + addHTML(); + onpage++; + } else { + next.disabled = false; + } + } + } + + let currentPage = 0; + function loadPage(page = 0) { + invDiv.innerHTML = ""; + for (onpage = 0; onpage < 50; onpage++) { + const inv = invsArr[onpage + page * 50]; + if (!inv) break; + invDiv.append(createInviteHTML(inv)); + } + if (onpage === 50 && invsArr[onpage + page * 50]) { + next.disabled = false; + } else { + next.disabled = true; + } + } + + const pageNav = document.createElement("div"); + const back = document.createElement("button"); + back.textContent = I18n.search.back(); + back.disabled = !currentPage; + back.onclick = () => { + back.disabled = !(currentPage - 1); + next.disabled = false; + loadPage(--currentPage); + }; + + const next = document.createElement("button"); + next.textContent = I18n.search.next(); + next.disabled = true; + pageNav.append(back, next); + inviteMenu.addHTMLArea(pageNav); + next.onclick = () => { + loadPage(++currentPage); + back.disabled = false; + }; + + loadArr(); + loadPage(currentPage); + inviteMenu.addHTMLArea(invDiv); +} +class Guild extends SnowFlake { owner!: Localuser; headers!: Localuser["headers"]; channels!: Channel[]; @@ -26,161 +189,1195 @@ class Guild extends SnowFlake{ parent_id!: string; member!: Member; html!: HTMLElement; - emojis!: emojijson[]; + emojis: emojipjson[] = []; large!: boolean; - members=new Set(); + stickers!: Sticker[]; + members = new Set(); static contextmenu = new Contextmenu("guild menu"); - static setupcontextmenu(){ - Guild.contextmenu.addbutton(()=>I18n.getTranslation("guild.copyId"), function(this: Guild){ - navigator.clipboard.writeText(this.id); - }); + static setupcontextmenu() { + Guild.contextmenu.addButton( + () => I18n.guild.makeInvite(), + function (this: Guild) { + const d = new Dialog(""); + this.makeInviteMenu(d.options); + d.show(); + }, + { + enabled: function () { + return this.member.hasPermission("CREATE_INSTANT_INVITE"); + }, + color: "blue", + }, + ); + Guild.contextmenu.addSeperator(); - Guild.contextmenu.addbutton(()=>I18n.getTranslation("guild.markRead"), function(this: Guild){ - this.markAsRead(); - }); + Guild.contextmenu.addButton( + () => I18n.guild.markRead(), + function (this: Guild) { + this.markAsRead(); + }, + ); - Guild.contextmenu.addbutton(()=>I18n.getTranslation("guild.notifications"), function(this: Guild){ - this.setnotifcation(); - }); + Guild.contextmenu.addButton( + () => I18n.guild.notifications(), + function (this: Guild) { + this.setnotifcation(); + }, + ); + Guild.contextmenu.addSeperator(); + this.contextmenu.addButton( + () => I18n.user.editServerProfile(), + function () { + this.member.showEditProfile(); + }, + ); + Guild.contextmenu.addSeperator(); - Guild.contextmenu.addbutton( - ()=>I18n.getTranslation("guild.leave"), - function(this: Guild){ + Guild.contextmenu.addButton( + () => I18n.guild.leave(), + function (this: Guild) { this.confirmleave(); }, - null, - function(_){ - return this.properties.owner_id !== this.member.user.id; - } + { + visible: function (_) { + return this.properties.owner_id !== this.member.user.id; + }, + color: "red", + }, ); - Guild.contextmenu.addbutton( - ()=>I18n.getTranslation("guild.delete"), - function(this: Guild){ + Guild.contextmenu.addButton( + () => I18n.guild.delete(), + function (this: Guild) { this.confirmDelete(); }, - null, - function(_){ - return this.properties.owner_id === this.member.user.id; - } + { + visible: function (_) { + return this.properties.owner_id === this.member.user.id; + }, + color: "red", + icon: { + css: "svg-delete", + }, + }, ); - Guild.contextmenu.addbutton( - ()=>I18n.getTranslation("guild.makeInvite"), - function(this: Guild){ - const d=new Dialog(""); - this.makeInviteMenu(d.options); - d.show(); + Guild.contextmenu.addButton( + () => I18n.guild.settings(), + function (this: Guild) { + this.generateSettings(); }, - null, - _=>true, - function(){ - return this.member.hasPermission("CREATE_INSTANT_INVITE"); - } + { + visible: function () { + return ( + this.member.hasPermission("MANAGE_GUILD") || + this.member.hasPermission("MANAGE_WEBHOOKS") || + this.member.hasPermission("BAN_MEMBERS") || + this.member.hasPermission("MANAGE_GUILD_EXPRESSIONS") || + this.member.hasPermission("MANAGE_ROLES") + ); + }, + icon: { + css: "svg-settings", + }, + }, + ); + + //TODO make icon for this + Guild.contextmenu.addButton( + () => I18n.guild.admins(), + function (this: Guild) { + this.findAdmin(); + }, + ); + Guild.contextmenu.addSeperator(); + Guild.contextmenu.addButton( + () => I18n.channel.createChannel(), + function () { + this.createchannels(); + }, + { + visible: function () { + return this.member.hasPermission("MANAGE_CHANNELS"); + }, + }, + ); + Guild.contextmenu.addButton( + () => I18n.channel.createCatagory(), + function () { + this.createcategory(); + }, + { + visible: function () { + return this.member.hasPermission("MANAGE_CHANNELS"); + }, + }, + ); + + Guild.contextmenu.addSeperator(); + Guild.contextmenu.addButton( + () => I18n.guild.copyId(), + function (this: Guild) { + navigator.clipboard.writeText(this.id); + }, + ); + Guild.contextmenu.addButton( + () => I18n.guild.report(), + async function () { + const menu = await ReportMenu.makeReport("guild", this.localuser, {guild: this}); + menu?.spawnMenu(); + }, + { + visible: function () { + const settings = getDeveloperSettings(); + return this.properties.owner_id !== this.localuser.user.id && settings.reportSystem; + }, + color: "red", + }, + ); + //TODO mute guild button + } + get muted() { + return this.mute_config && new Date(this.mute_config.end_time).getTime() < Date.now(); + } + resolveMember(user: User) { + return Member.resolveMember(user, this); + } + getChannel(id: string) { + return this.channels.find((_) => _.id === id); + } + recalcPrivate() { + if (this !== this.localuser.lookingguild) return; + this.localuser.channelfocus?.slowmode(); + } + async findAdmin() { + const menu = new Dialog(I18n.guild.admins()); + menu.options.addText("Loading"); + menu.show(); + const roles = new Set( + Object.entries( + (await ( + await fetch(this.info.api + `/guilds/${this.id}/roles/member-counts/`, { + headers: this.headers, + }) + ).json()) as {[key: string]: number}, + ) + .map(([id, count]) => { + return [this.roleids.get(id), count] as [Role, number]; + }) + //Just in case, this should never fire + .filter((_) => _[0] !== undefined) + //Filter out those who have too many users + .filter((_) => _[1] > 1000) + .map((_) => _[0]), ); - Guild.contextmenu.addbutton(()=>I18n.getTranslation("guild.settings"), function(this: Guild){ - this.generateSettings(); - },null,function(){ - return this.member.hasPermission("MANAGE_GUILD"); + const everyone = this.roleids.get(this.id); + if (everyone) roles.add(everyone); + menu.options.removeAll(); + let owner = true; + let perms = [ + "ADMINISTRATOR", + "BAN_MEMBERS", + "KICK_MEMBERS", + "MANAGE_GUILD", + "MANAGE_CHANNELS", + "MODERATE_MEMBERS", + "MANAGE_ROLES", + "MANAGE_MESSAGES", + "MANAGE_NICKNAMES", + "MANAGE_WEBHOOKS", + "MANAGE_EVENTS", + "MANAGE_THREADS", + ].filter((_) => { + for (const role of roles) { + if (role.permissions.hasPermission(_, false)) { + return false; + } + } + return true; }); - /* -----things left for later----- - guild.contextmenu.addbutton("Leave Guild",function(){ - console.log(this) - this.deleteChannel(); - },null,_=>{return thisuser.isAdmin()}) - - guild.contextmenu.addbutton("Mute Guild",function(){ - editchannelf(this); - },null,_=>{return thisuser.isAdmin()}) - */ + + menu.options.addButtonInput("", I18n.guild.adminMenu.changePerms(), () => { + const d = new Dialog("", {noSubmit: false}); + const opt = d.options; + opt + .addCheckboxInput( + I18n.guild.adminMenu.owner(), + (b) => { + owner = b; + d.hide(); + queueMicrotask(() => loadResults()); + }, + { + initState: owner, + }, + ) + .watchForChange(() => opt.changed()); + for (const perm of Permissions.info().filter((_) => { + for (const role of roles) { + if (role.permissions.hasPermission(_.name, false)) { + return false; + } + } + return true; + })) { + opt.addHR(); + opt + .addCheckboxInput( + perm.readableName, + (b) => { + if (b) { + perms.push(perm.name); + } else { + perms = perms.filter((_) => _ !== perm.name); + } + }, + { + initState: perms.includes(perm.name), + }, + ) + .watchForChange(() => opt.changed()); + opt.addText(perm.description); + } + d.show().style.width = "80%"; + }); + + const retDiv = document.createElement("div"); + menu.options.addHTMLArea(retDiv); + const loadResults = async () => { + retDiv.textContent = I18n.guild.adminMenu.finding(); + const results = new Set( + ( + await Promise.all( + this.roles + .filter((_) => { + for (const perm of perms) { + if (_.permissions.hasPermission(perm, false)) { + return true; + } + } + return false; + }) + .map(async (_) => { + return ( + await fetch(`${this.info.api}/guilds/${this.id}/roles/${_.id}/member-ids`, { + headers: this.headers, + }) + ).json() as Promise; + }), + ) + ).flat(), + ); + if (owner) { + results.add(this.properties.owner_id); + } + const members = ( + await Promise.all( + [...results].map(async (_) => { + const json = await this.localuser.resolvemember(_, this.id); + return json ? Member.new(json, this) : undefined; + }), + ) + ).filter((_) => _ !== undefined); + members.sort((a, b) => { + return a.name < b.name ? 1 : -1; + }); + retDiv.innerHTML = ""; + retDiv.append( + ...members.map((memb) => { + const div = document.createElement("div"); + div.classList.add("flexltr", "adminList"); + const name = document.createElement("b"); + name.textContent = memb.name; + const nameBox = document.createElement("div"); + nameBox.classList.add("flexttb"); + + const roles = document.createElement("div"); + roles.classList.add("flexltr"); + roles.append( + ...perms + .filter((_) => memb.hasPermission(_, false)) + .map((perm) => { + const span = document.createElement("span"); + //@ts-ignore + span.textContent = I18n.permissions.readableNames[perm](); + return span; + }), + ); + if (owner && memb.id === this.properties.owner_id) { + const span = document.createElement("span"); + span.textContent = I18n.guild.adminMenu.ownName(); + roles.append(span); + } + + nameBox.append(name, roles); + + const pfp = memb.user.buildpfp(memb, div); + + div.append(pfp, nameBox); + + memb.user.bind(div, this, undefined); + return div; + }), + ); + console.log(members); + }; + loadResults(); + } + async searchMembers(limit: number, query: string): Promise { + if (this.id !== "@me") { + return new Promise((res) => { + const nonce = Math.floor(Math.random() * 10 ** 8) + ""; + this.localuser.ws!.send( + JSON.stringify({ + op: 8, + d: { + guild_id: [this.id], + query, + limit, + presences: true, + nonce, + }, + }), + ); + this.localuser.searchMap.set( + nonce, + async (e: { + chunk_index: number; + chunk_count: number; + nonce: string; + not_found?: string[]; + members?: memberjson[]; + presences: presencejson[]; + }) => { + console.log(e); + if (e.members && e.members[0]) { + if (e.members[0].user) { + res( + (await Promise.all(e.members.map(async (_) => await Member.new(_, this)))).filter( + (_) => _ !== undefined, + ), + ); + } else { + const prom1: Promise[] = []; + for (const thing of e.members) { + prom1.push(this.localuser.getUser(thing.id)); + } + await Promise.all(prom1); + res( + (await Promise.all(e.members.map(async (_) => await Member.new(_, this)))).filter( + (_) => _ !== undefined, + ), + ); + } + } + return []; + }, + ); + }); + } + return []; } - generateSettings(){ - const settings = new Settings(I18n.getTranslation("guild.settingsFor",this.properties.name)); - const textChannels=this.channels.filter(e=>{ + async showWelcome() { + this.welcomeScreen = await ( + await fetch(this.info.api + "/guilds/" + this.id + "/welcome-screen", { + headers: this.headers, + }) + ).json(); + if (!this.welcomeScreen?.enabled) return; + const dio = new Dialog(I18n.onboarding.title(this.properties.name)); + const opt = dio.options; + let i = 0; + for (const cha of this.welcomeScreen.welcome_channels) { + const channel = this.getChannel(cha.channel_id); + if (!channel) continue; + opt.addButtonInput("", channel.name, () => { + dio.hide(); + this.loadChannel(channel.id); + }); + opt.addText(cha.description); + i++; + if (i !== this.welcomeScreen.welcome_channels.length) + opt.addHTMLArea(document.createElement("hr")); + } + + dio.show(); + } + welcomeScreen?: welcomeScreen; + generateSettings() { + const settings = new Settings(I18n.guild.settingsFor(this.properties.name)); + const textChannels = this.channels.filter((e) => { //TODO there are almost certainly more types. is Voice valid? - return new Set([0,5]).has(e.type); + return new Set([0, 5]).has(e.type); }); - { - const overview = settings.addButton(I18n.getTranslation("guild.overview")); - const form = overview.addForm("", _=>{}, { + if (this.member.hasPermission("MANAGE_GUILD")) { + const overview = settings.addButton(I18n.guild.overview()); + const form = overview.addForm("", (_) => {}, { headers: this.headers, traditionalSubmit: true, fetchURL: this.info.api + "/guilds/" + this.id, method: "PATCH", }); - form.addTextInput(I18n.getTranslation("guild.name:"), "name", { initText: this.properties.name }); - form.addMDInput(I18n.getTranslation("guild.description:"), "description", { - initText: this.properties.description, + form.addTextInput(I18n.guild["name:"](), "name", { + initText: this.properties.name, }); - form.addFileInput(I18n.getTranslation("guild.banner:"), "banner", { clear: true }); - form.addFileInput(I18n.getTranslation("guild.icon:"), "icon", { clear: true }); + form.addImageInput(I18n.guild["banner:"](), "banner", { + clear: true, + width: 96 * 3, + initImg: this.banner + ? this.info.cdn + + "/banners/" + + this.id + + "/" + + this.banner + + ".png" + + new CDNParams({expectedSize: 256}) + : "", + objectFit: "cover", + }); + + form.addImageInput(I18n.guild["icon:"](), "icon", { + clear: true, + initImg: this.properties.icon + ? this.info.cdn + + "/icons/" + + this.id + + "/" + + this.properties.icon + + ".png" + + new CDNParams({expectedSize: 256}) + : "", + }); + + form.addImageInput(I18n.guild.splash(), "discovery_splash", { + clear: true, + initImg: this.properties.discovery_splash + ? `${this.info.cdn}/discovery-splashes/${this.id}/${this.properties.discovery_splash}.png` + + new CDNParams({expectedSize: 256}) + : "", + width: 96 * 2, + }); form.addHR(); - const sysmap=[null,...textChannels.map(e=>e.id)]; - form.addSelect(I18n.getTranslation("guild.systemSelect:"), "system_channel_id", - ["No system messages",...textChannels.map(e=>e.name)],{defaultIndex:sysmap.indexOf(this.properties.system_channel_id)} - ,sysmap); + console.log(textChannels, this.channels); + const options = ["DISCOVERABLE", "COMMUNITY", "INVITES_DISABLED"] as const; + const defaultIndex = options.findIndex((_) => this.properties.features.includes(_)); + form.addSelect( + I18n.guild.howJoin(), + "features", + options.map((_) => I18n.guild[_]()), + { + defaultIndex: defaultIndex == -1 ? 2 : defaultIndex, + }, + options, + ); - form.addCheckboxInput(I18n.getTranslation("guild.sendrandomwelcome?"),"s1",{ - initState:!(this.properties.system_channel_flags&1) + form.addCheckboxInput(I18n.guild.disableInvites(), "invDis", { + initState: !!this.properties.features.includes("INVITES_DISABLED"), }); - form.addCheckboxInput(I18n.getTranslation("guild.stickWelcomeReact?"),"s4",{ - initState:!(this.properties.system_channel_flags&8) + + form.addCheckboxInput(I18n.guild["sendrandomwelcome?"](), "s1", { + initState: !(this.properties.system_channel_flags & 1), + }); + form.addCheckboxInput(I18n.guild["stickWelcomeReact?"](), "s4", { + initState: !(this.properties.system_channel_flags & 8), }); - form.addCheckboxInput(I18n.getTranslation("guild.boostMessage?"),"s2",{ - initState:!(this.properties.system_channel_flags&2) + form.addCheckboxInput(I18n.guild["boostMessage?"](), "s2", { + initState: !(this.properties.system_channel_flags & 2), }); - form.addCheckboxInput(I18n.getTranslation("guild.helpTips?"),"s3",{ - initState:!(this.properties.system_channel_flags&4) + form.addCheckboxInput(I18n.guild["helpTips?"](), "s3", { + initState: !(this.properties.system_channel_flags & 4), }); - form.addPreprocessor((e:any)=>{ - let bits=0; - bits+=(1-e.s1)*1; + form.addPreprocessor((e: any) => { + let bits = 0; + bits += (1 - e.s1) * 1; delete e.s1; - bits+=(1-e.s2)*2; + bits += (1 - e.s2) * 2; delete e.s2; - bits+=(1-e.s3)*4; + bits += (1 - e.s3) * 4; delete e.s3; - bits+= (1-e.s4)*8; + bits += (1 - e.s4) * 8; delete e.s4; - e.system_channel_flags=bits; - }) + e.system_channel_flags = bits; + let temp = this.properties.features; + console.log([...temp]); + //@ts-ignore + temp = temp.filter((_) => !options.includes(_)); + temp.push(e.features); + if (e.features === "DISCOVERABLE") { + temp.push("COMMUNITY"); + } + if (e.invDis) { + if (!temp.includes("INVITES_DISABLED")) { + temp.push("INVITES_DISABLED"); + } + } else { + temp = temp.filter((_) => _ !== "INVITES_DISABLED"); + } + delete e.invDis; + if (temp.includes("COMMUNITY")) { + if (!com) { + this.addCommunity(settings, textChannels); + com = true; + } + } + e.features = temp; + }); form.addHR(); - form.addSelect(I18n.getTranslation("guild.defaultNoti"),"default_message_notifications", - [I18n.getTranslation("guild.onlyMentions"),I18n.getTranslation("guild.all")], - { - defaultIndex:[1,0].indexOf(this.properties.default_message_notifications), - radio:true - },[1,0]); + form.addSelect( + I18n.guild.defaultNoti(), + "default_message_notifications", + [I18n.guild.onlyMentions(), I18n.guild.all()], + { + defaultIndex: [1, 0].indexOf(this.properties.default_message_notifications), + radio: true, + }, + [1, 0], + ); form.addHR(); let region = this.properties.region; - if(!region){ + if (!region) { region = ""; } - form.addTextInput(I18n.getTranslation("guild.region:"), "region", { initText: region }); + form.addTextInput(I18n.guild["region:"](), "region", {initText: region}); } - this.makeInviteMenu(settings.addButton(I18n.getTranslation("invite.inviteMaker")),textChannels); - const s1 = settings.addButton(I18n.getTranslation("guild.roles")); - const permlist: [Role, Permissions][] = []; - for(const thing of this.roles){ - permlist.push([thing, thing.permissions]); + this.makeInviteMenu(settings.addButton(I18n.invite.inviteMaker()), textChannels); + if (this.member.hasPermission("MANAGE_ROLES")) { + const s1 = settings.addButton(I18n.guild.roles(), {optName: ""}); + const permlist: [Role, Permissions][] = []; + for (const thing of this.roles) { + permlist.push([thing, thing.permissions]); + } + s1.options.push(new RoleList(permlist, this, this.updateRolePermissions.bind(this), false)); + } + if (this.member.hasPermission("MANAGE_GUILD_EXPRESSIONS")) { + const emoji = settings.addButton(I18n.emoji.title()); + emoji.addButtonInput("", I18n.emoji.upload(), () => { + const popup = new Dialog(I18n.emoji.upload()); + const form = popup.options.addForm( + "", + () => { + popup.hide(); + }, + { + fetchURL: `${this.info.api}/guilds/${this.id}/emojis`, + method: "POST", + headers: this.headers, + }, + ); + form.addFileInput(I18n.emoji["image:"](), "image", {required: true}); + form.addTextInput(I18n.emoji["name:"](), "name", {required: true}); + popup.show(); + }); + const containdiv = document.createElement("div"); + const genDiv = () => { + containdiv.innerHTML = ""; + for (const emoji of this.emojis) { + const div = document.createElement("div"); + div.classList.add("flexltr", "emojiOption"); + const emojic = new Emoji(emoji, this); + + const text = document.createElement("input"); + text.type = "text"; + text.value = emoji.name; + text.addEventListener("change", () => { + fetch(`${this.info.api}/guilds/${this.id}/emojis/${emoji.id}`, { + method: "PATCH", + headers: this.headers, + body: JSON.stringify({name: text.value}), + }).then((e) => { + if (!e.ok) text.value = emoji.name; + }); //if not ok, undo + }); + + const del = document.createElement("span"); + del.classList.add("svgicon", "svg-x", "deleteEmoji"); + del.onclick = () => { + const diaolog = new Dialog(""); + diaolog.options.addTitle(I18n.emoji.confirmDel()); + const options = diaolog.options.addOptions("", {ltr: true}); + options.addButtonInput("", I18n.yes(), () => { + fetch(`${this.info.api}/guilds/${this.id}/emojis/${emoji.id}`, { + method: "DELETE", + headers: this.headers, + }); + diaolog.hide(); + }); + options.addButtonInput("", I18n.no(), () => { + diaolog.hide(); + }); + diaolog.show(); + }; + + div.append(emojic.getHTML(true), ":", text, ":", del); + + containdiv.append(div); + } + }; + this.onEmojiUpdate = () => { + if (!document.body.contains(containdiv)) { + this.onEmojiUpdate = () => {}; + return; + } + genDiv(); + }; + genDiv(); + emoji.addHTMLArea(containdiv); + } + if (this.member.hasPermission("MANAGE_GUILD")) { + const onboard = settings.addButton(I18n.onboarding.name()); + const genOnboard = async () => { + this.welcomeScreen = await ( + await fetch(this.info.api + "/guilds/" + this.id + "/welcome-screen", { + headers: this.headers, + }) + ).json(); + onboard.removeAll(); + if (this.welcomeScreen?.enabled) { + const welcomeScreen = this.welcomeScreen; + onboard.afterSubmit = () => { + fetch(this.info.api + "/guilds/" + this.id + "/welcome-screen", { + method: "PATCH", + headers: this.headers, + body: JSON.stringify(welcomeScreen), + }); + }; + onboard.addButtonInput("", I18n.onboarding.disable(), () => { + fetch(this.info.api + "/guilds/" + this.id + "/welcome-screen", { + method: "PATCH", + headers: this.headers, + body: JSON.stringify({enabled: false}), + }).then((r) => { + this.welcomeScreen = undefined; + if (r.ok) { + genOnboard(); + } + }); + }); + onboard.addButtonInput("", I18n.onboarding.addChannel(), () => { + const d = new Dialog(I18n.onboarding.addChannel()); + const form = d.options.addForm("", (o) => { + const obj = o as {description: string; channel_id: string}; + welcomeScreen.welcome_channels.push(obj); + fetch(this.info.api + "/guilds/" + this.id + "/welcome-screen", { + method: "PATCH", + headers: this.headers, + body: JSON.stringify(welcomeScreen), + }).then((r) => { + if (r.ok) { + d.hide(); + welcome(obj); + } + }); + }); + const channels = this.channels.filter((channel) => { + if (channel.isThread()) return false; + if (channel.type === 4) return false; + if (welcomeScreen.welcome_channels.find((c) => c.emoji_id === channel.id)) + return false; + return true; + }); + form.addSelect( + I18n.onboarding.channel(), + "channel_id", + channels.map(({name}) => name), + {}, + channels.map(({id}) => id), + ); + form.addTextInput(I18n.onboarding.desc(), "description"); + d.show(); + }); + onboard.addMDInput( + I18n.onboarding.desc(), + (desc) => { + welcomeScreen.description = desc; + }, + { + initText: welcomeScreen.description, + }, + ); + const welcome = (obj: {channel_id: string; description: string}) => { + const {channel_id, description} = obj; + const opt = onboard.addOptions(""); + const channel = this.getChannel(channel_id); + if (!channel) return; + opt.addTitle(channel.name); + opt.addTextInput( + I18n.onboarding.desc(), + (desc) => { + obj.description = desc; + }, + {initText: description}, + ); + opt.addButtonInput("", I18n.onboarding.deleteChannel(), () => { + welcomeScreen.welcome_channels = welcomeScreen.welcome_channels.filter( + ({channel_id}) => channel_id !== channel_id, + ); + fetch(this.info.api + "/guilds/" + this.id + "/welcome-screen", { + method: "PATCH", + headers: this.headers, + body: JSON.stringify(welcomeScreen), + }).then((r) => { + if (r.ok) { + opt.removeAll(); + } + }); + }); + }; + welcomeScreen.welcome_channels.forEach(welcome); + } else { + onboard.addButtonInput("", I18n.onboarding.enable(), () => { + const d = new Dialog(""); + const form = d.options.addForm("", (o) => { + const obj = o as {description: string}; + const welcome_screen = { + description: obj.description, + enabled: true, + welcome_channels: [], + } satisfies extendedProperties["welcome_screen"]; + fetch(this.info.api + "/guilds/" + this.id + "/welcome-screen", { + method: "PATCH", + headers: this.headers, + body: JSON.stringify(welcome_screen), + }).then((r) => { + this.welcomeScreen = welcome_screen; + if (r.ok) { + d.hide(); + genOnboard(); + } + }); + }); + form.addTextInput(I18n.onboarding.desc(), "description"); + d.show(); + }); + } + }; + genOnboard(); + } + if (this.member.hasPermission("MANAGE_GUILD_EXPRESSIONS")) { + const emoji = settings.addButton(I18n.sticker.title()); + emoji.addButtonInput("", I18n.sticker.upload(), () => { + const popup = new Dialog(I18n.sticker.upload()); + const form = popup.options.addForm("", async () => { + const body = new FormData(); + body.set("name", name.value); + if (!filei.value) throw new FormError(filei, I18n.sticker.errFileMust()); + const file = filei.value.item(0); + if (!file) throw new FormError(filei, I18n.sticker.errFileMust()); + body.set("file", file); + if (!tags.value) throw new FormError(tags, I18n.sticker.errEmjMust()); + if (tags.value.id) { + body.set("tags", tags.value.id); + } else if (tags.value.emoji) { + body.set("tags", tags.value.emoji); + } else { + throw new FormError(tags, I18n.sticker.errEmjMust()); + } + const res = await fetch(this.info.api + "/guilds/" + this.id + "/stickers", { + method: "POST", + headers: { + Authorization: this.headers.Authorization, + }, + body, + }); + if (res.ok) { + popup.hide(); + } else { + const json = await res.json(); + if ("message" in json && typeof json.message === "string") { + throw new FormError(filei, json.message); + } + } + }); + const filei = form.addFileInput(I18n.sticker.image(), "file", {required: true}); + const name = form.addTextInput(I18n.sticker.name(), "name", {required: true}); + const tags = form.addEmojiInput(I18n.sticker.tags(), "tags", this.localuser, { + required: true, + }); + popup.show(); + }); + const containdiv = document.createElement("div"); + containdiv.classList.add("stickersDiv"); + const genDiv = () => { + containdiv.innerHTML = ""; + for (const sticker of this.stickers) { + const div = document.createElement("div"); + div.classList.add("flexttb", "stickerOption"); + + const text = document.createElement("span"); + text.textContent = sticker.name; + + div.onclick = () => { + const form = emoji.addSubForm(emoji.name, () => {}, { + fetchURL: this.info.api + "/guilds/" + this.id + "/stickers/" + sticker.id, + method: "PATCH", + headers: this.headers, + traditionalSubmit: true, + }); + + form.addHTMLArea(sticker.getHTML()); + form.addTextInput(I18n.sticker.name(), "name", { + initText: sticker.name, + }); + + form.addMDInput(I18n.sticker.desc(), "description", { + initText: sticker.description, + }); + + let initEmoji = Emoji.getEmojiFromIDOrString(sticker.tags, this.localuser); + form.addEmojiInput(I18n.sticker.tags(), "tags", this.localuser, { + initEmoji, + required: false, + }); + + form.addButtonInput("", I18n.sticker.del(), () => { + const diaolog = new Dialog(""); + diaolog.options.addTitle(I18n.sticker.confirmDel()); + const options = diaolog.options.addOptions("", {ltr: true}); + options.addButtonInput("", I18n.yes(), () => { + fetch(`${this.info.api}/guilds/${this.id}/stickers/${sticker.id}`, { + method: "DELETE", + headers: this.headers, + }); + diaolog.hide(); + }); + options.addButtonInput("", I18n.no(), () => { + diaolog.hide(); + }); + diaolog.show(); + }); + }; + + div.append(sticker.getHTML(), text); + + containdiv.append(div); + } + }; + this.onStickerUpdate = () => { + emoji.returnFromSub(); + if (!document.body.contains(containdiv)) { + this.onStickerUpdate = () => {}; + return; + } + genDiv(); + }; + genDiv(); + emoji.addHTMLArea(containdiv); + } + if (this.member.hasPermission("MANAGE_GUILD")) { + const inviteMenu = settings.addButton(I18n.guild.invites()); + makeInviteMenu(inviteMenu, this, this.info.api + `/guilds/${this.id}/invites`); + } + + if (this.member.hasPermission("BAN_MEMBERS")) { + const banMenu = settings.addButton(I18n.guild.bans()); + const makeBanMenu = () => { + const banDiv = document.createElement("div"); + const bansp = ProgessiveDecodeJSON( + this.info.api + "/guilds/" + this.id + "/bans", + { + headers: this.headers, + }, + ); + const createBanHTML = (ban: banObj) => { + const div = document.createElement("div"); + div.classList.add("flexltr", "bandiv"); + let src: string; + if (ban.user.avatar !== null) { + src = + `${this.info.cdn}/avatars/${ban.user.id}/${ban.user.avatar}.png` + + new CDNParams({expectedSize: 96}); + } else { + const int = Number((BigInt(ban.user.id) >> 22n) % 6n); + src = `${this.info.cdn}/embed/avatars/${int}.png`; + } + const img = createImg(src); + img.classList.add("pfp"); + const divUserRes = document.createElement("div"); + divUserRes.classList.add("flexttb"); + + const username = document.createElement("span"); + username.textContent = ban.user.username; + + divUserRes.append(username); + if (ban.reason) { + const reason = document.createElement("span"); + reason.innerText = ban.reason; + divUserRes.append(I18n.guild.banReason(ban.reason)); + } + div.append(img, divUserRes); + div.onclick = async (_) => { + const opt = banMenu.addSubOptions(ban.user.username); + + opt.addHTMLArea(img.cloneNode(true) as HTMLElement); + opt.addText(ban.user.username); + if (ban.reason) opt.addText(I18n.guild.banReason(ban.reason)); + //FIXME the API sends back the wrong response, so I don't have this info + /* + const moreInfo = (await ( + await fetch(this.info.api + "/guilds/" + this.id + "/bans/" + ban.user.id, { + headers: this.headers, + }) + ).json()) as addInfoBan; + const userWhoBanned = await User.resolve(moreInfo.executor_id, this.localuser); + opt.addHTMLArea(userWhoBanned.createWidget(this)); + //*/ + opt.addButtonInput("", I18n.user.unban(ban.user.username), async () => { + bansArr = bansArr.filter((_) => _ !== ban); + + await fetch(this.info.api + "/guilds/" + this.id + "/bans/" + ban.user.id, { + headers: this.headers, + method: "DELETE", + }); + loadPage(currentPage); + banMenu.returnFromSub(); + }); + }; + return div; + }; + let bansArr: banObj[] = []; + let onpage = 0; + async function loadArr() { + let bansArr2: banObj[] = []; + let waiting = false; + async function addHTML() { + if (waiting) return; + waiting = true; + await new Promise((res) => setTimeout(res, 0)); + waiting = false; + banDiv.append(...bansArr2.map((ban) => createBanHTML(ban))); + bansArr2 = []; + } + while (!(await bansp).done) { + const ban = await (await (await bansp).getNext()).getWhole(); + bansArr.push(ban); + if (onpage < 50) { + bansArr2.push(ban); + addHTML(); + onpage++; + } else { + next.disabled = false; + } + } + } + let currentPage = 0; + function loadPage(page = 0) { + banDiv.innerHTML = ""; + for (onpage = 0; onpage < 50; onpage++) { + const ban = bansArr[onpage + page * 50]; + if (!ban) break; + banDiv.append(createBanHTML(ban)); + } + if (onpage === 50 && bansArr[onpage + page * 50]) { + next.disabled = false; + } else { + next.disabled = true; + } + } + + const pageNav = document.createElement("div"); + const back = document.createElement("button"); + back.textContent = I18n.search.back(); + back.disabled = !currentPage; + back.onclick = () => { + back.disabled = !(currentPage - 1); + next.disabled = false; + loadPage(--currentPage); + }; + + const next = document.createElement("button"); + next.textContent = I18n.search.next(); + next.disabled = true; + pageNav.append(back, next); + banMenu.addHTMLArea(pageNav); + next.onclick = () => { + loadPage(++currentPage); + back.disabled = false; + }; + + loadArr(); + loadPage(currentPage); + return banDiv; + }; + banMenu.addHTMLArea(makeBanMenu); + } + if (this.member.hasPermission("MANAGE_GUILD")) { + const widgetMenu = settings.addButton(I18n.widget()); + (async () => { + const cur = (await ( + await fetch(this.info.api + "/guilds/" + this.id + "/widget", { + headers: this.headers, + }) + ).json()) as { + enabled: boolean; + channel_id?: null | string; + }; + const form = widgetMenu.addForm("", () => {}, { + traditionalSubmit: true, + fetchURL: this.info.api + "/guilds/" + this.id + "/widget", + headers: this.headers, + method: "PATCH", + }); + form.addCheckboxInput(I18n.widgetEnabled(), "enabled", {initState: cur.enabled}); + const channels = this.channels.filter((_) => _.type !== 4); + form.addSelect( + I18n.channel.name(), + "channel_id", + channels.map((_) => _.name), + { + defaultIndex: channels.findIndex((_) => _.id == cur.channel_id), + }, + channels.map((_) => _.id), + ); + })(); + } + if (this.member.hasPermission("MANAGE_WEBHOOKS")) { + const webhooks = settings.addButton(I18n.webhooks.base()); + webhookMenu(this, this.info.api + `/guilds/${this.id}/webhooks`, webhooks); + const template = settings.addButton(I18n.guild.templates()); + (async () => { + template.addText(I18n.guild.templcateMetaDesc()); + const generateTemplateArea = (temp: templateSkim) => { + const div = document.createElement("div"); + div.classList.add("flexltr", "templateMiniBox"); + const code = document.createElement("span"); + + code.textContent = temp.code + ` (${temp.name})`; + + const edit = document.createElement("button"); + edit.textContent = I18n.edit(); + edit.onclick = () => { + const form = template.addSubForm( + I18n.guild.editingTemplate(temp.name), + (tempy) => { + const template = tempy as templateSkim; + temp.name = template.name; + temp.description = template.description; + }, + { + fetchURL: this.info.api + "/guilds/" + this.id + "/templates/" + temp.code, + method: "PATCH", + headers: this.headers, + }, + ); + const search = new URLSearchParams([["instance", this.info.wellknown]]); + form.addMDText( + new MarkDown( + I18n.guild.templateURL( + window.location.origin + "/template/" + temp.code + "?" + search, + ), + undefined, + ), + ); + + const name = form.addTextInput(I18n.guild.templateName(), "name", { + initText: temp.name, + }); + form.addMDInput(I18n.guild.templateDesc(), "description", { + initText: temp.description, + }); + User.resolve(temp.creator_id, this.localuser).then((_) => { + form.addText(I18n.guild.tempCreatedBy()); + form.addHTMLArea(_.createWidget(this)); + }); + form.addText(I18n.guild.tempUseCount((temp.usage_count || 0) + "")); + form.addPreprocessor(() => { + if (name.value.length < 2) { + throw new FormError(name, I18n.guild.templateNameShort()); + } + }); + }; + + div.append(code, edit); + template.addHTMLArea(div); + }; + template.addButtonInput("", I18n.guild.createNewTemplate(), () => { + const form = template.addSubForm( + I18n.guild.createNewTemplate(), + (code) => { + template.returnFromSub(); + generateTemplateArea(code as templateSkim); + }, + { + fetchURL: this.info.api + "/guilds/" + this.id + "/templates", + method: "POST", + headers: this.headers, + }, + ); + form.addText(I18n.guild.templcateMetaDesc()); + const name = form.addTextInput(I18n.guild.templateName(), "name"); + form.addMDInput(I18n.guild.templateDesc(), "description"); + form.addPreprocessor(() => { + if (name.value.length < 2) { + throw new FormError(name, I18n.guild.templateNameShort()); + } + }); + }); + const templates = (await ( + await fetch(this.info.api + "/guilds/" + this.id + "/templates", {headers: this.headers}) + ).json()) as templateSkim[]; + for (const temp of templates.reverse()) { + generateTemplateArea(temp); + } + })(); + } + let com = false; + if (this.properties.features.includes("COMMUNITY")) { + if (this.member.hasPermission("MANAGE_GUILD")) this.addCommunity(settings, textChannels); + com = true; } - s1.options.push( - new RoleList(permlist, this, this.updateRolePermissions.bind(this),false) - ); settings.show(); } - makeInviteMenu(options:Options,valid:void|(Channel[])){ - if(!valid){ - valid=this.channels.filter(e=>{ + onStickerUpdate = (_stickers: Sticker[]) => {}; + addCommunity(settings: Settings, textChannels: Channel[]) { + const com = settings.addButton(I18n.guild.community()).addForm("", () => {}, { + fetchURL: this.info.api + "/guilds/" + this.id, + method: "PATCH", + headers: this.headers, + traditionalSubmit: true, + }); + { + com.addMDInput(I18n.guild["description:"](), "description", { + initText: this.properties.description, + }); + } + { + let defaultIndex = textChannels.findIndex((_) => this.properties.rules_channel_id == _.id); + if (defaultIndex === -1) { + defaultIndex = textChannels.length; + } + com.addSelect( + I18n.guild.ruleId(), + "rules_channel_id", + [...textChannels.map((_) => _.name), "none"], + { + defaultIndex, + }, + [...textChannels.map((_) => _.id), undefined], + ); + } + { + const sysmap = [null, ...textChannels.map((e) => e.id)]; + com.addSelect( + I18n.guild["systemSelect:"](), + "system_channel_id", + ["No system messages", ...textChannels.map((e) => e.name)], + {defaultIndex: sysmap.indexOf(this.properties.system_channel_id)}, + sysmap, + ); + } + } + makeInviteMenu(options: Options, valid: void | Channel[]) { + if (!valid) { + valid = this.channels.filter((e) => { //TODO there are almost certainly more types. is Voice valid? - return new Set([0,5]).has(e.type); + return new Set([0, 5]).has(e.type); }); } - let channel=valid[0]; + let channel = valid[0]; const div = document.createElement("div"); div.classList.add("invitediv"); const text = document.createElement("span"); @@ -193,13 +1390,13 @@ class Guild extends SnowFlake{ const copy = document.createElement("span"); copy.classList.add("copybutton", "svgicon", "svg-copy"); copycontainer.append(copy); - copycontainer.onclick = _=>{ - if(text.textContent){ + copycontainer.onclick = (_) => { + if (text.textContent) { navigator.clipboard.writeText(text.textContent); } }; div.append(copycontainer); - const update = ()=>{ + const update = () => { fetch(`${this.info.api}/channels/${channel.id}/invites`, { method: "POST", headers: this.headers, @@ -209,11 +1406,11 @@ class Guild extends SnowFlake{ target_user_id: null, max_age: expires + "", max_uses: uses, - temporary: uses !== 0 + temporary: uses !== 0, }), }) - .then(_=>_.json()) - .then(json=>{ + .then((_) => _.json()) + .then((json) => { const params = new URLSearchParams(""); params.set("instance", this.info.wellknown); const encoded = params.toString(); @@ -221,259 +1418,314 @@ class Guild extends SnowFlake{ }); }; - options.addTitle(I18n.getTranslation("inviteOptions.title")); - const text2=options.addText(""); - options.addSelect(I18n.getTranslation("invite.channel:"),()=>{},valid.map(e=>e.name)) - .watchForChange((e)=>{ - channel=valid[e]; - text2.setText(I18n.getTranslation("invite.subtext",channel.name,this.properties.name)); - }) - + options.addTitle(I18n.inviteOptions.title()); + const text2 = options.addText(""); + options + .addSelect( + I18n.invite["channel:"](), + () => {}, + valid.map((e) => e.name), + ) + .watchForChange((e) => { + channel = valid[e]; + text2.setText(I18n.invite.subtext(channel.name, this.properties.name)); + }); - options.addSelect(I18n.getTranslation("invite.expireAfter"),()=>{}, - ["30m","1h","6h","12h","1d","7d","30d","never"].map((e)=>I18n.getTranslation("inviteOptions."+e)) - ).onchange=(e)=>{expires=[1800, 3600, 21600, 43200, 86400, 604800, 2592000, 0][e];}; + options.addSelect( + I18n.invite.expireAfter(), + () => {}, + (["30m", "1h", "6h", "12h", "1d", "7d", "30d", "never"] as const).map((e) => + I18n.inviteOptions[e](), + ), + ).onchange = (e) => { + expires = [1800, 3600, 21600, 43200, 86400, 604800, 2592000, 0][e]; + }; - const timeOptions=["1","5","10","25","50","100"].map((e)=>I18n.getTranslation("inviteOptions.limit",e)) - timeOptions.unshift(I18n.getTranslation("inviteOptions.noLimit")) - options.addSelect(I18n.getTranslation("invite.expireAfter"),()=>{},timeOptions) - .onchange=(e)=>{uses=[0, 1, 5, 10, 25, 50, 100][e];}; + const timeOptions = ["1", "5", "10", "25", "50", "100"].map((e) => I18n.inviteOptions.limit(e)); + timeOptions.unshift(I18n.inviteOptions.noLimit()); + options.addSelect(I18n.invite.expireAfter(), () => {}, timeOptions).onchange = (e) => { + uses = [0, 1, 5, 10, 25, 50, 100][e]; + }; - options.addButtonInput("",I18n.getTranslation("invite.createInvite"),()=>{ + options.addButtonInput("", I18n.invite.createInvite(), () => { update(); - }) + }); options.addHTMLArea(div); } - roleUpdate:(role:Role,added:-1|0|1)=>unknown=()=>{}; - sortRoles(){ - this.roles.sort((a,b)=>(b.position-a.position)); + roleUpdate: (role: Role, added: -1 | 0 | 1) => unknown = () => {}; + sortRoles() { + this.roles.sort((a, b) => b.position - a.position); } - async recalcRoles(){ - let position=this.roles.length; - const map=this.roles.map(_=>{ + async recalcRoles() { + let position = this.roles.length; + const map = this.roles.map((_) => { position--; - return {id:_.id,position}; - }) - await fetch(this.info.api+"/guilds/"+this.id+"/roles",{ - method:"PATCH", - body:JSON.stringify(map), - headers:this.headers - }) + return {id: _.id, position}; + }); + await fetch(this.info.api + "/guilds/" + this.id + "/roles", { + method: "PATCH", + body: JSON.stringify(map), + headers: this.headers, + }); } - newRole(rolej:rolesjson){ - const role=new Role(rolej,this); + newRole(rolej: rolesjson) { + const role = new Role(rolej, this); this.roles.push(role); this.roleids.set(role.id, role); this.sortRoles(); - this.roleUpdate(role,1); + this.roleUpdate(role, 1); } - updateRole(rolej:rolesjson){ - const role=this.roleids.get(rolej.id) as Role; + updateRole(rolej: rolesjson) { + const role = this.roleids.get(rolej.id) as Role; role.newJson(rolej); - this.roleUpdate(role,0); + this.roleUpdate(role, 0); } - memberupdate(json:memberjson){ - let member:undefined|Member=undefined; - for(const thing of this.members){ - if(thing.id===json.id){ - member=thing; + memberupdate(json: memberjson) { + let member: undefined | Member = undefined; + for (const thing of this.members) { + if (thing.id === json.id) { + member = thing; break; } } - if(!member) return; + if (!member) return; member.update(json); - if(member===this.member){ + if (member === this.member) { console.log(member); this.loadGuild(); } } - deleteRole(id:string){ + deleteRole(id: string) { const role = this.roleids.get(id); - if(!role) return; + if (!role) return; this.roleids.delete(id); - this.roles.splice(this.roles.indexOf(role),1); - this.roleUpdate(role,-1); - } - constructor( - json: guildjson | -1, - owner: Localuser, - member: memberjson | User | null - ){ - if(json === -1 || member === null){ - super("@me"); + this.roles.splice(this.roles.indexOf(role), 1); + this.roleUpdate(role, -1); + } + onEmojiUpdate = (_: emojipjson[]) => {}; + update(json: extendedProperties) { + this.large = json.large; + this.member_count = json.member_count; + this.emojis = json.emojis || []; + this.headers = this.owner.headers; + this.welcomeScreen = json.welcome_screen; + this.properties.features = json.features; + if (this.properties.icon !== json.icon) { + this.properties.icon = json.icon; + if (this.HTMLicon) { + const divy = this.generateGuildIcon(); + this.HTMLicon.replaceWith(divy); + this.HTMLicon = divy; + } + } + this.roleids = new Map(); + this.banner = json.banner; + } + constructor(json: guildjson | -1, owner: Localuser, member: memberjson | User | null) { + super(typeof json === "number" ? "@me" : json.id); + this.owner = owner; + this.perminfo ??= {channels: {}}; + this.headers = this.owner.headers; + + if (json === -1 || member === null) { return; } - if(json.stickers.length){ + if (json.stickers.length) { console.log(json.stickers, ":3"); } - super(json.id); + this.large = json.large; this.member_count = json.member_count; - this.emojis = json.emojis; - this.owner = owner; - this.headers = this.owner.headers; + this.emojis = json.emojis || []; this.channels = []; - this.properties = json.properties; + if (json.properties) { + this.properties = json.properties; + } this.roles = []; this.roleids = new Map(); + this.banner = json.properties.banner; + this.welcomeScreen = json.properties.welcome_screen; + if (json.roles) { + for (const roley of json.roles) { + const roleh = new Role(roley, this); + this.roles.push(roleh); + this.roleids.set(roleh.id, roleh); + } + } this.message_notifications = 0; - for(const roley of json.roles){ - const roleh = new Role(roley, this); - this.roles.push(roleh); - this.roleids.set(roleh.id, roleh); - } this.sortRoles(); - if(member instanceof User){ + if (member instanceof User) { console.warn(member); - Member.resolveMember(member, this).then(_=>{ - if(_){ + Member.resolveMember(member, this).then((_) => { + if (_) { this.member = _; - }else{ + } else { console.error("Member was unable to resolve"); } }); - }else{ - Member.new(member, this).then(_=>{ - if(_){ + } else { + Member.new(member, this).then((_) => { + if (_) { this.member = _; } }); } - this.perminfo ??= { channels: {} }; - for(const thing of json.channels){ + + for (const thing of json.channels) { const temp = new Channel(thing, this); this.channels.push(temp); this.localuser.channelids.set(temp.id, temp); } + this.headchannels = []; - for(const thing of this.channels){ + for (const thing of this.channels) { const parent = thing.resolveparent(this); - if(!parent){ + if (!parent) { this.headchannels.push(thing); } } + for (const thread of json.threads) { + if (this.localuser.channelids.has(thread.id)) continue; + const temp = new Channel(thread, this); + this.localuser.channelids.set(temp.id, temp); + temp.resolveparent(this); + } this.prevchannel = this.localuser.channelids.get(this.perminfo.prevchannel); + this.stickers = json.stickers.map((_) => new Sticker(_, this)) || []; } - get perminfo(){ + get perminfo() { return this.localuser.perminfo.guilds[this.id]; } - set perminfo(e){ + set perminfo(e) { this.localuser.perminfo.guilds[this.id] = e; } - notisetting(settings: { - channel_overrides: {message_notifications: number,muted: boolean,mute_config: {selected_time_window: number,end_time: number},channel_id: string}[]; - message_notifications: any; - flags?: number; - hide_muted_channels?: boolean; - mobile_push?: boolean; - mute_config?: null; - mute_scheduled_events?: boolean; - muted?: boolean; - notify_highlights?: number; - suppress_everyone?: boolean; - suppress_roles?: boolean; - version?: number; - guild_id?: string; - }){ + mute_config!: mute_config | null; + notisetting(settings: GuildOverrides) { + this.mute_config = this.mute_config; this.message_notifications = settings.message_notifications; - for(const override of settings.channel_overrides){ - const channel=this.localuser.channelids.get(override.channel_id); - if(!channel) continue; + for (const override of settings.channel_overrides) { + const channel = this.localuser.channelids.get(override.channel_id); + if (!channel) continue; channel.handleUserOverrides(override); } } - setnotifcation(){ - - const options=["all", "onlyMentions", "none"].map(e=>I18n.getTranslation("guild."+e)); - const notiselect=new Dialog(""); - const form=notiselect.options.addForm("",(_,sent:any)=>{ - notiselect.hide(); - this.message_notifications = sent.message_notifications; - },{ - fetchURL:`${this.info.api}/users/@me/guilds/${this.id}/settings/`, - method:"PATCH", - headers:this.headers - }); - form.addSelect(I18n.getTranslation("guild.selectnoti"),"message_notifications",options,{ - radio:true, - defaultIndex:this.message_notifications - },[0,1,2]); + setnotifcation() { + const options = (["all", "onlyMentions", "none"] as const).map((e) => I18n.guild[e](e)); + const notiselect = new Dialog(""); + const form = notiselect.options.addForm( + "", + (_, sent: any) => { + notiselect.hide(); + this.message_notifications = sent.message_notifications; + }, + { + fetchURL: `${this.info.api}/users/@me/guilds/${this.id}/settings/`, + method: "PATCH", + headers: this.headers, + }, + ); + form.addSelect( + I18n.guild.selectnoti(), + "message_notifications", + options, + { + radio: true, + defaultIndex: this.message_notifications, + }, + [0, 1, 2], + ); notiselect.show(); } - confirmleave(){ + confirmleave() { const full = new Dialog(""); - full.options.addTitle(I18n.getTranslation("guild.confirmLeave")) - const options=full.options.addOptions("",{ltr:true}); - options.addButtonInput("",I18n.getTranslation("guild.yesLeave"),()=>{ - this.leave().then(_=>{ + full.options.addTitle(I18n.guild.confirmLeave()); + const options = full.options.addOptions("", {ltr: true}); + options.addButtonInput("", I18n.guild.yesLeave(), () => { + this.leave().then((_) => { full.hide(); }); }); - options.addButtonInput("",I18n.getTranslation("guild.noLeave"),()=>{ + options.addButtonInput("", I18n.guild.noLeave(), () => { full.hide(); }); full.show(); } - async leave(){ + async leave() { return fetch(this.info.api + "/users/@me/guilds/" + this.id, { method: "DELETE", headers: this.headers, }); } - printServers(){ + printServers() { let build = ""; - for(const thing of this.headchannels){ + for (const thing of this.headchannels) { build += thing.name + ":" + thing.position + "\n"; - for(const thingy of thing.children){ + console.log(thing.children); + for (const thingy of thing.children) { build += " " + thingy.name + ":" + thingy.position + "\n"; } } console.log(build); } - calculateReorder(){ - let position = -1; - const build: { - id: string; - position: number | undefined; - parent_id: string | undefined; - }[] = []; - for(const thing of this.headchannels){ + calculateReorder(movedId?: string) { + let position = 0; + type buildtype = { + id: string; + position: number | undefined; + parent_id: string | undefined | null; + }[]; + const build: buildtype = []; + for (const thing of this.headchannels) { const thisthing: { - id: string; - position: number | undefined; - parent_id: string | undefined; - } = { id: thing.id, position: undefined, parent_id: undefined }; - if(thing.position <= position){ - thing.position = thisthing.position = position + 1; - } - position = thing.position; + id: string; + position: number | undefined; + parent_id: string | undefined; + } = {id: thing.id, position: undefined, parent_id: undefined}; + if (thing.position != position) { + thisthing.position = thing.position = position; + } + console.log(position); - if(thing.move_id && thing.move_id !== thing.parent_id){ + if (thing.move_id && thing.move_id !== thing.parent_id) { thing.parent_id = thing.move_id; thisthing.parent_id = thing.parent?.id; thing.move_id = undefined; } - if(thisthing.position || thisthing.parent_id){ + if (thisthing.position !== undefined || thisthing.parent_id) { build.push(thisthing); } - if(thing.children.length > 0){ - const things = thing.calculateReorder(); - for(const thing of things){ + position++; + if (thing.children.length > 0 && thing.type === 4) { + let things: buildtype; + [things, position] = thing.calculateReorder(position); + for (const thing of things) { build.push(thing); } } } + const find = build.find((_) => _.id === movedId); + const channel = this.channels.find((_) => _.id === movedId); + if (!find) { + if (channel) + build.push({ + id: channel.id, + position: channel.position, + parent_id: channel.parent?.id || null, + }); + } else { + if (channel) find.parent_id = channel.parent?.id || null; + } console.log(build); this.printServers(); - if(build.length === 0){ + if (build.length === 0) { return; } const serverbug = false; - if(serverbug){ - for(const thing of build){ + if (serverbug) { + for (const thing of build) { console.log(build, thing); fetch(this.info.api + "/guilds/" + this.id + "/channels", { method: "PATCH", @@ -481,7 +1733,7 @@ class Guild extends SnowFlake{ body: JSON.stringify([thing]), }); } - }else{ + } else { fetch(this.info.api + "/guilds/" + this.id + "/channels", { method: "PATCH", headers: this.headers, @@ -489,159 +1741,220 @@ class Guild extends SnowFlake{ }); } } - get localuser(){ + get localuser() { return this.owner; } - get info(){ + get info() { return this.owner.info; } - sortchannels(){ - this.headchannels.sort((a, b)=>{ + sortchannels() { + this.headchannels.sort((a, b) => { return a.position - b.position; }); } - static generateGuildIcon(guild: Guild | (invitejson["guild"] & { info: { cdn: string } })){ + HTMLicon?: HTMLElement; + static generateGuildIcon( + guild: Guild | {id: string; name: string; icon: string | null; info: {cdn: string}}, + autoLink = true, + ) { const divy = document.createElement("div"); divy.classList.add("servernoti"); const noti = document.createElement("div"); noti.classList.add("unread"); divy.append(noti); - if(guild instanceof Guild){ + if (guild instanceof Guild && autoLink) { guild.localuser.guildhtml.set(guild.id, divy); - guild.html=divy; + guild.html = divy; } let icon: string | null; - if(guild instanceof Guild){ + if (guild instanceof Guild) { icon = guild.properties.icon; - }else{ + } else { icon = guild.icon; } - if(icon !== null){ - const img = document.createElement("img"); + const onclick = + guild instanceof Guild + ? () => { + guild.loadGuild(); + guild.loadChannel(); + } + : null; + const hover = new Hover(guild instanceof Guild ? guild.properties.name : "", { + side: "right", + weak: true, + }); + if (icon !== null) { + const img = createImg( + guild.info.cdn + + "/icons/" + + guild.id + + "/" + + icon + + ".png" + + new CDNParams({expectedSize: 96}), + ); img.classList.add("pfp", "servericon"); - img.src = guild.info.cdn + "/icons/" + guild.id + "/" + icon + ".png"; divy.appendChild(img); - if(guild instanceof Guild){ - img.onclick = ()=>{ - console.log(guild.loadGuild); - guild.loadGuild(); - guild.loadChannel(); - }; - Guild.contextmenu.bindContextmenu(img, guild,undefined); + if (guild instanceof Guild && autoLink) { + img.onclick = onclick; + Guild.contextmenu.bindContextmenu(img, guild, undefined); + hover.addEvent(img); } - }else{ + } else { const div = document.createElement("div"); let name: string; - if(guild instanceof Guild){ + if (guild instanceof Guild) { name = guild.properties.name; - }else{ + } else { name = guild.name; } const build = name .replace(/'s /g, " ") - .replace(/\w+/g, word=>word[0]) + .replace(/[^\s]+/g, (word) => word[0]) .replace(/\s/g, ""); div.textContent = build; div.classList.add("blankserver", "servericon"); divy.appendChild(div); - if(guild instanceof Guild){ - div.onclick = ()=>{ - guild.loadGuild(); - guild.loadChannel(); - }; - Guild.contextmenu.bindContextmenu(div, guild,undefined); + if (guild instanceof Guild) { + div.onclick = onclick; + Guild.contextmenu.bindContextmenu(div, guild, undefined); + hover.addEvent(div); } } return divy; } - generateGuildIcon(){ - return Guild.generateGuildIcon(this); + generateGuildIcon(autoLink = true) { + return Guild.generateGuildIcon(this, autoLink); } - confirmDelete(){ + confirmDelete() { let confirmname = ""; const full = new Dialog(""); - full.options.addTitle(I18n.getTranslation("guild.confirmDelete",this.properties.name)); - full.options.addTextInput(I18n.getTranslation("guild.serverName"),()=>{}).onchange=(e)=>confirmname=e; - - const options=full.options.addOptions("",{ltr:true}); - options.addButtonInput("",I18n.getTranslation("guild.yesDelete"),()=>{ - if(confirmname !== this.properties.name){ - //TODO maybe some sort of form error? idk - alert("names don't match"); + full.options.addTitle(I18n.guild.confirmDelete(this.properties.name)); + const form = full.options.addForm("", () => {}, {submitText: ""}); + const txt = form.addTextInput(I18n.guild.serverName(), ""); + txt.onchange = (e) => (confirmname = e); + + const options = form.addOptions("", {ltr: true}); + options.addButtonInput("", I18n.guild.yesDelete(), () => { + if (confirmname !== this.properties.name) { + form.handleError(new FormError(txt, I18n.guild.nameNoMatch())); return; } - this.delete().then(_=>{ + this.delete().then((_) => { full.hide(); }); }); - options.addButtonInput("",I18n.getTranslation("guild.noDelete"),()=>{ + options.addButtonInput("", I18n.guild.noDelete(), () => { full.hide(); }); full.show(); } - async delete(){ + async delete() { return fetch(this.info.api + "/guilds/" + this.id + "/delete", { method: "POST", headers: this.headers, }); } - unreads(html?: HTMLElement | undefined){ - if(html){ + get mentions() { + let mentions = 0; + for (const thing of this.channels) { + if (thing.visible) mentions += thing.mentions; + else if (thing.mentions) console.error("Hidden Channel has pings:", thing); + } + return mentions; + } + unreads(html?: HTMLElement | undefined) { + if (html) { this.html = html; - }else{ + } else { html = this.html; } + if (!html) { + return; + } let read = true; - for(const thing of this.channels){ - if(thing.hasunreads){ - console.log(thing); + let mentions = this.mentions; + for (const thing of this.channels) { + if (thing.hasunreads && (!thing.isThread() || thing.owner)) { read = false; break; } } - if(!html){ - return; + const noti = html.children[0]; + if (mentions !== 0) { + noti.classList.add("pinged"); + noti.textContent = "" + mentions; + } else { + noti.textContent = ""; + noti.classList.remove("pinged"); } - if(read){ - html.children[0].classList.remove("notiunread"); - }else{ - html.children[0].classList.add("notiunread"); + if (read) { + noti.classList.remove("notiunread"); + } else { + noti.classList.add("notiunread"); } } - getHTML(){ + async goToThread(threadId: string) { + if (!this.localuser.channelids.has(threadId)) { + const channelJson = await ( + await fetch(this.info.api + "/channels/" + threadId, { + headers: this.headers, + }) + ).json(); + if (channelJson.code == 200) { + const channel = new Channel(channelJson as channeljson, this); + this.localuser.channelids.set(channel.id, channel); + channel.resolveparent(this); + const par = this.localuser.channelids.get(channel.parent_id as string); + par?.createguildHTML(); + } else { + this.loadChannel(); + } + } + this.localuser.goToChannel(threadId); + } + getHTML() { + const sideContainDiv = document.getElementById("sideContainDiv"); + if (sideContainDiv) { + sideContainDiv.classList.remove("searchDiv"); + sideContainDiv.classList.remove("hideSearchDiv"); + } + const searchBox = document.getElementById("searchBox"); + if (searchBox) searchBox.textContent = ""; + //this.printServers(); this.sortchannels(); this.printServers(); const build = document.createElement("div"); - for(const thing of this.headchannels){ + for (const thing of this.headchannels) { build.appendChild(thing.createguildHTML(this.isAdmin())); } return build; } - isAdmin(){ + isAdmin() { return this.member.isAdmin(); } - async markAsRead(){ + async markAsRead() { const build: { - read_states: { - channel_id: string; - message_id: string | null | undefined; - read_state_type: number; - }[]; - } = { read_states: [] }; - for(const thing of this.channels){ - if(thing.hasunreads){ + read_states: { + channel_id: string; + message_id: string | null | undefined; + read_state_type: number; + }[]; + } = {read_states: []}; + for (const thing of this.channels) { + if (thing.hasunreads) { build.read_states.push({ channel_id: thing.id, - message_id: thing.lastmessageid, + message_id: thing.trueLastMessageid, read_state_type: 0, }); - thing.lastreadmessageid = thing.lastmessageid; - if(!thing.myhtml)continue; + thing.lastreadmessageid = thing.trueLastMessageid; + if (!thing.myhtml) continue; thing.myhtml.classList.remove("cunread"); } } @@ -652,30 +1965,33 @@ class Guild extends SnowFlake{ body: JSON.stringify(build), }); } - hasRole(r: Role | string){ + hasRole(r: Role | string) { console.log("this should run"); - if(r instanceof Role){ + if (r instanceof Role) { r = r.id; } return this.member.hasRole(r); } - loadChannel(ID?: string | undefined| null,addstate=true){ - if(ID){ + async loadChannel(ID?: string | undefined | null, addstate = true, message?: string) { + if (ID) { const channel = this.localuser.channelids.get(ID); - if(channel){ - channel.getHTML(addstate); + if (channel) { + await channel.getHTML(addstate, undefined, message); + return; + } else { + await this.goToThread(ID); return; } } - if(this.prevchannel&&ID!==null){ + if (this.prevchannel && ID !== null && this.prevchannel.visible) { console.log(this.prevchannel); - this.prevchannel.getHTML(addstate); + await this.prevchannel.getHTML(addstate, undefined, message); return; } - if(this.id!=="@me"){ - for(const thing of this.channels){ - if(thing.type!==4){ - thing.getHTML(addstate); + if (this.id !== "@me") { + for (const thing of this.channels) { + if (thing.type !== 4 && thing.visible) { + await thing.getHTML(addstate, undefined, message); return; } } @@ -683,14 +1999,14 @@ class Guild extends SnowFlake{ this.removePrevChannel(); this.noChannel(addstate); } - removePrevChannel(){ - if(this.localuser.channelfocus){ + removePrevChannel() { + if (this.localuser.channelfocus) { this.localuser.channelfocus.infinite.delete(); } - if(this !== this.localuser.lookingguild){ + if (this !== this.localuser.lookingguild) { this.loadGuild(); } - if(this.localuser.channelfocus && this.localuser.channelfocus.myhtml){ + if (this.localuser.channelfocus && this.localuser.channelfocus.myhtml) { this.localuser.channelfocus.myhtml.classList.remove("viewChannel"); } this.prevchannel = undefined; @@ -699,16 +2015,22 @@ class Guild extends SnowFlake{ const typebox = document.getElementById("typebox") as HTMLElement; replybox.classList.add("hideReplyBox"); typebox.classList.remove("typeboxreplying"); - (document.getElementById("typebox") as HTMLDivElement).contentEditable ="false"; - (document.getElementById("upload") as HTMLElement).style.visibility="hidden"; - (document.getElementById("typediv") as HTMLElement).style.visibility="hidden"; - (document.getElementById("sideDiv") as HTMLElement).innerHTML=""; + (document.getElementById("typebox") as HTMLDivElement).contentEditable = "false"; + (document.getElementById("upload") as HTMLElement).style.visibility = "hidden"; + (document.getElementById("typediv") as HTMLElement).style.visibility = "hidden"; + (document.getElementById("sideDiv") as HTMLElement).innerHTML = ""; } - noChannel(addstate:boolean){ - if(addstate){ - history.pushState([this.id,undefined], "", "/channels/" + this.id); + noChannel(addstate: boolean) { + for (const c of this.channels) { + if (c.visible) { + this.loadChannel(c.id, addstate); + return; + } + } + if (addstate) { + history.pushState([this.id, undefined], "", "/channels/" + this.id); } - this.localuser.pageTitle(I18n.getTranslation("guild.emptytitle")); + this.localuser.pageTitle(I18n.guild.emptytitle()); const channelTopic = document.getElementById("channelTopic") as HTMLSpanElement; channelTopic.setAttribute("hidden", ""); @@ -716,83 +2038,97 @@ class Guild extends SnowFlake{ loading.classList.remove("loading"); this.localuser.getSidePannel(); - const messages = document.getElementById("channelw") as HTMLDivElement; - for(const thing of Array.from(messages.getElementsByClassName("messagecontainer"))){ + const messages = document.getElementById("scrollWrap") as HTMLDivElement; + for (const thing of Array.from(messages.getElementsByClassName("messagecontainer"))) { thing.remove(); } - const h1=document.createElement("h1"); - h1.classList.add("messagecontainer") - h1.textContent=I18n.getTranslation("guild.emptytext"); + const h1 = document.createElement("h1"); + h1.classList.add("messagecontainer"); + h1.textContent = I18n.guild.emptytext(); messages.append(h1); } - loadGuild(){ + loadGuild() { this.localuser.loadGuild(this.id); } - updateChannel(json: channeljson){ + updateChannel(json: channeljson) { const channel = this.localuser.channelids.get(json.id); - if(channel){ + if (channel) { + const parent = channel.parent; channel.updateChannel(json); - this.headchannels = []; - for(const thing of this.channels){ - thing.children = []; - } - this.headchannels = []; - for(const thing of this.channels){ - const parent = thing.resolveparent(this); - if(!parent){ - this.headchannels.push(thing); + this.channels = this.channels.sort((a, b) => a.position - b.position); + if (parent !== channel.parent) { + if (parent) parent.children.filter((_) => _ !== channel); + channel.resolveparent(); + } else if (parent && !json.parent_id) { + this.headchannels.push(channel); + for (const thing of this.channels) { + thing.children = thing.children.filter((_) => _ !== channel); } } + + for (const channel of this.channels) { + channel.children = channel.children.sort((a, b) => a.position - b.position); + } this.printServers(); } } - createChannelpac(json: channeljson){ + createChannelpac(json: channeljson) { const thischannel = new Channel(json, this); this.localuser.channelids.set(json.id, thischannel); this.channels.push(thischannel); thischannel.resolveparent(this); - if(!thischannel.parent){ + if (!thischannel.parent) { this.headchannels.push(thischannel); } this.calculateReorder(); this.printServers(); return thischannel; } - createchannels(func = this.createChannel){ - const options=["text", "announcement","voice"].map(e=>I18n.getTranslation("channel."+e)); + goToChannelDelay(id: string) { + const channel = this.channels.find((_) => _.id == id); + if (channel) { + this.loadChannel(channel.id); + } else { + this.localuser.gotoid = id; + } + } + createchannels(func = this.createChannel.bind(this)) { + const options = (["text", "announcement", "voice", "forum"] as const).map((e) => + I18n.channel[e](), + ); - const channelselect=new Dialog(""); - const form=channelselect.options.addForm("",(e:any)=>{ - func(e.name,e.type); + const channelselect = new Dialog(""); + const form = channelselect.options.addForm("", (e: any) => { + func(e.name, e.type); channelselect.hide(); }); - form.addSelect(I18n.getTranslation("channel.selectType"),"type",options,{radio:true},[0,5,2]); - form.addTextInput(I18n.getTranslation("channel.selectName"),"name"); + form.addSelect(I18n.channel.selectType(), "type", options, {radio: true}, [0, 5, 2, 15]); + form.addTextInput(I18n.channel.selectName(), "name"); channelselect.show(); } - createcategory(){ + createcategory() { const category = 4; - const channelselect=new Dialog(""); - const options=channelselect.options; - const form=options.addForm("",(e:any)=>{ + const channelselect = new Dialog(""); + const options = channelselect.options; + const form = options.addForm("", (e: any) => { this.createChannel(e.name, category); channelselect.hide(); }); - form.addTextInput(I18n.getTranslation("channel.selectCatName"),"name"); + form.addTextInput(I18n.channel.selectCatName(), "name"); channelselect.show(); } - delChannel(json: channeljson){ + delChannel(json: channeljson) { const channel = this.localuser.channelids.get(json.id); this.localuser.channelids.delete(json.id); - if(!channel)return; + if (!channel) return; this.channels.splice(this.channels.indexOf(channel), 1); const indexy = this.headchannels.indexOf(channel); - if(indexy !== -1){ + if (indexy !== -1) { this.headchannels.splice(indexy, 1); } - if(channel===this.prevchannel){ - this.prevchannel=undefined; + if (channel === this.prevchannel) { + this.prevchannel = undefined; } /* const build=[]; @@ -811,35 +2147,34 @@ class Guild extends SnowFlake{ */ this.printServers(); } - createChannel(name: string, type: number){ + createChannel(name: string, type: number) { fetch(this.info.api + "/guilds/" + this.id + "/channels", { method: "POST", headers: this.headers, - body: JSON.stringify({ name, type }), - }); + body: JSON.stringify({name, type}), + }) + .then((_) => _.json()) + .then((_) => this.goToChannelDelay(_.id)); } - async createRole(name: string){ - const fetched = await fetch( - this.info.api + "/guilds/" + this.id + "roles", - { - method: "POST", - headers: this.headers, - body: JSON.stringify({ - name, - color: 0, - permissions: "0", - }), - } - ); + async createRole(name: string) { + const fetched = await fetch(this.info.api + "/guilds/" + this.id + "roles", { + method: "POST", + headers: this.headers, + body: JSON.stringify({ + name, + color: 0, + permissions: "0", + }), + }); const json = await fetched.json(); const role = new Role(json, this); this.roleids.set(role.id, role); this.roles.push(role); return role; } - async updateRolePermissions(id: string, perms: Permissions){ + async updateRolePermissions(id: string, perms: Permissions) { const role = this.roleids.get(id); - if(!role){ + if (!role) { return; } role.permissions.allow = perms.allow; @@ -859,6 +2194,51 @@ class Guild extends SnowFlake{ }), }); } + commands?: Command[]; + commandProm?: Promise; + apps?: applicationJson[]; + async getApps() { + if (this.commandProm) { + await this.commandProm; + } + if (this.apps) { + return this.commands; + } else { + const prom = this.getCommandsFetch(); + this.commandProm = prom; + const {apps, commands} = await prom; + this.commands = commands; + this.apps = apps; + return apps; + } + } + async getCommands() { + if (this.commandProm) { + await this.commandProm; + } + if (this.commands) { + return this.commands; + } else { + const prom = this.getCommandsFetch(); + this.commandProm = prom; + const {apps, commands} = await prom; + this.commands = commands; + this.apps = apps; + return commands; + } + } + + async getCommandsFetch() { + const json = (await ( + await fetch(this.info.api + `/guilds/${this.id}/application-command-index`, { + headers: this.headers, + }) + ).json()) as {application_commands: commandJson[]; applications: applicationJson[]}; + return { + apps: json.applications, + commands: json.application_commands.map((_) => new Command(_, this.localuser)), + }; + } } Guild.setupcontextmenu(); -export{ Guild }; +export {Guild}; diff --git a/src/webpage/home.html b/src/webpage/home.html deleted file mode 100644 index 8189617a..00000000 --- a/src/webpage/home.html +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - Jank Client - - - - - - - - - - - -
- -

Welcome to Jank Client

-
-

Jank Client is a Spacebar-compatible client seeking to be as good as it can be with many features including:

-
    -
  • Direct Messaging
  • -
  • Reactions support
  • -
  • Invites
  • -
  • Account switching
  • -
  • User settings
  • -
  • Developer portal
  • -
  • Bot invites
  • -
  • Translation support
  • -
-
-
-

Spacebar-Compatible Instances:

-
-
-
-
-

Contribute to Jank Client

-

We always appreciate some help, whether that be in the form of bug reports, code, help translate, or even just pointing out some typos.


- - Github - -
-
- - - - diff --git a/src/webpage/home.ts b/src/webpage/home.ts index a04772b4..ac5c6959 100644 --- a/src/webpage/home.ts +++ b/src/webpage/home.ts @@ -1,120 +1,198 @@ -import { I18n } from "./i18n.js"; -import{ mobile }from"./login.js"; -console.log(mobile); -const serverbox = document.getElementById("instancebox") as HTMLDivElement; +import {I18n} from "./i18n.js"; +import {makeRegister} from "./register.js"; +import {mobile} from "./utils/utils.js"; +if (window.location.pathname === "/" || window.location.pathname.startsWith("/index")) { + console.log(mobile); + const serverbox = document.getElementById("instancebox") as HTMLDivElement; -(async ()=>{ - await I18n.done; - const openClient=document.getElementById("openClient") - const welcomeJank=document.getElementById("welcomeJank") - const box1title=document.getElementById("box1title") - const box1Items=document.getElementById("box1Items") - const compatableInstances=document.getElementById("compatableInstances") - const box3title=document.getElementById("box3title") - const box3description=document.getElementById("box3description") - if(openClient&&welcomeJank&&compatableInstances&&box3title&&box3description&&box1title&&box1Items){ - openClient.textContent=I18n.getTranslation("htmlPages.openClient"); - welcomeJank.textContent=I18n.getTranslation("htmlPages.welcomeJank"); - box1title.textContent=I18n.getTranslation("htmlPages.box1title"); + (async () => { + await I18n.done; + const box1Items = document.getElementById("box1Items"); + I18n.translatePage(); - compatableInstances.textContent=I18n.getTranslation("htmlPages.compatableInstances"); - box3title.textContent=I18n.getTranslation("htmlPages.box3title"); - box3description.textContent=I18n.getTranslation("htmlPages.box3description"); - - const items=I18n.getTranslation("htmlPages.box1Items").split("|"); - let i=0; - //@ts-ignore ts is being dumb here - for(const item of box1Items.children){ - (item as HTMLElement).textContent=items[i]; - i++; + if (box1Items) { + const items = I18n.htmlPages.box1Items().split("|"); + let i = 0; + //@ts-ignore ts is being dumb here + for (const item of box1Items.children) { + (item as HTMLElement).textContent = items[i]; + i++; + } } - }else{ - console.error(openClient,welcomeJank,compatableInstances,box3title,box3description,box1title,box1Items) + })(); + const recent = document.getElementById("recentBlog"); + if (recent) { + fetch("https://blog.fermi.chat/feed_json_created.json") + .then((_) => _.json()) + .then( + (json: { + items: { + url: string; + title: string; + content_html: string; + }[]; + }) => { + for (const thing of json.items.slice(0, 5)) { + const a = document.createElement("a"); + a.href = thing.url; + a.textContent = thing.title; + recent.append(a); + } + }, + ); } -})() - -fetch("/instances.json") - .then(_=>_.json()) - .then( - async ( - json: { - name: string; - description?: string; - descriptionLong?: string; - image?: string; - url?: string; - display?: boolean; - online?: boolean; - uptime: { alltime: number; daytime: number; weektime: number }; - urls: { - wellknown: string; - api: string; - cdn: string; - gateway: string; - login?: string; - }; - }[] - )=>{ - await I18n.done; - console.warn(json); - for(const instance of json){ - if(instance.display === false){ - continue; - } - const div = document.createElement("div"); - div.classList.add("flexltr", "instance"); - if(instance.image){ - const img = document.createElement("img"); - img.src = instance.image; - div.append(img); - } - const statbox = document.createElement("div"); - statbox.classList.add("flexttb","flexgrow"); - - { - const textbox = document.createElement("div"); - textbox.classList.add("flexttb", "instancetextbox"); - const title = document.createElement("h2"); - title.innerText = instance.name; - if(instance.online !== undefined){ - const status = document.createElement("span"); - status.innerText = instance.online ? "Online" : "Offline"; - status.classList.add("instanceStatus"); - title.append(status); + fetch("/instances.json") + .then((_) => _.json()) + .then( + async ( + json: { + name: string; + description?: string; + descriptionLong?: string; + image?: string; + url?: string; + display?: boolean; + online?: boolean; + uptime: {alltime: number; daytime: number; weektime: number}; + urls: { + wellknown: string; + api: string; + cdn: string; + gateway: string; + login?: string; + }; + }[], + ) => { + await I18n.done; + console.warn(json); + for (const instance of json) { + if (instance.display === false) { + continue; + } + const div = document.createElement("div"); + div.classList.add("flexltr", "instance"); + if (instance.image) { + const img = document.createElement("img"); + img.alt = I18n.home.icon(instance.name); + img.src = instance.image; + div.append(img); } - textbox.append(title); - if(instance.description || instance.descriptionLong){ - const p = document.createElement("p"); - if(instance.descriptionLong){ - p.innerText = instance.descriptionLong; - }else if(instance.description){ - p.innerText = instance.description; + const statbox = document.createElement("div"); + statbox.classList.add("flexttb", "flexgrow"); + + { + const textbox = document.createElement("div"); + textbox.classList.add("flexttb", "instancetextbox"); + const title = document.createElement("h2"); + title.innerText = instance.name; + if (instance.online !== undefined) { + const status = document.createElement("span"); + status.innerText = instance.online ? "Online" : "Offline"; + status.classList.add("instanceStatus"); + title.append(status); + } + textbox.append(title); + if (instance.description || instance.descriptionLong) { + const p = document.createElement("p"); + if (instance.descriptionLong) { + p.innerText = instance.descriptionLong; + } else if (instance.description) { + p.innerText = instance.description; + } + textbox.append(p); } - textbox.append(p); + statbox.append(textbox); } - statbox.append(textbox); + if (instance.uptime) { + const stats = document.createElement("div"); + stats.classList.add("flexltr"); + const span = document.createElement("span"); + span.innerText = I18n.home.uptimeStats( + Math.round(instance.uptime.alltime * 100) + "", + Math.round(instance.uptime.weektime * 100) + "", + Math.round(instance.uptime.daytime * 100) + "", + ); + stats.append(span); + statbox.append(stats); + } + div.append(statbox); + div.onclick = (_) => { + if (instance.online !== false) { + makeRegister(true, instance.name); + } else { + alert(I18n.home.warnOffiline()); + } + }; + serverbox.append(div); + } + }, + ); + + const slides = document.getElementById("ScreenshotSlides"); + if (slides) { + const images = Array.from(slides.getElementsByTagName("img")); + const left = slides.getElementsByClassName("leftArrow").item(0) as HTMLElement; + const right = slides.getElementsByClassName("rightArrow").item(0) as HTMLElement; + let index = 0; + let timeout: NodeJS.Timeout | undefined = setTimeout(() => {}); + function slideShow() { + let cleared = false; + if (timeout !== undefined) { + cleared = true; + clearTimeout(timeout); + } + let i = 0; + for (const img of images) { + if (i !== index) { + img.classList.add("hidden"); + } else { + img.classList.remove("hidden"); } - if(instance.uptime){ - const stats = document.createElement("div"); - stats.classList.add("flexltr"); - const span = document.createElement("span"); - span.innerText = I18n.getTranslation("home.uptimeStats",Math.round( - instance.uptime.alltime * 100 - )+"",Math.round( - instance.uptime.weektime * 100 - )+"",Math.round(instance.uptime.daytime * 100)+"") - stats.append(span); - statbox.append(stats); + i++; + } + const count = document.getElementById("slideCount"); + if (count) { + if (count.children.length !== images.length) { + count.innerHTML = ""; + for (let i = 0; i < images.length; i++) { + const dot = document.createElement("span"); + const outer = document.createElement("div"); + outer.onclick = () => { + index = i; + slideShow(); + }; + outer.append(dot); + count.append(outer); + } } - div.append(statbox); - div.onclick = _=>{ - if(instance.online){ - window.location.href = "/register.html?instance=" + encodeURI(instance.name); - }else{ - alert(I18n.getTranslation("home.warnOffiline")); + let i = 0; + for (const child of Array.from(count.children)) { + if (i === index) { + child.classList.add("selected"); + } else { + child.classList.remove("selected"); } - }; - serverbox.append(div); + i++; + } } + + timeout = setTimeout( + () => { + index = (index + 1) % images.length; + timeout = undefined; + slideShow(); + }, + cleared ? 15000 : 30000, + ); } - ); + slideShow(); + left.onclick = () => { + index = (index - 1 + images.length) % images.length; + slideShow(); + }; + right.onclick = () => { + index = (index + 1) % images.length; + slideShow(); + }; + } +} diff --git a/src/webpage/home/SS1.webp b/src/webpage/home/SS1.webp new file mode 100644 index 00000000..2aa141fa Binary files /dev/null and b/src/webpage/home/SS1.webp differ diff --git a/src/webpage/home/SS2.webp b/src/webpage/home/SS2.webp new file mode 100644 index 00000000..bc09be5d Binary files /dev/null and b/src/webpage/home/SS2.webp differ diff --git a/src/webpage/home/SS3.webp b/src/webpage/home/SS3.webp new file mode 100644 index 00000000..84c065b0 Binary files /dev/null and b/src/webpage/home/SS3.webp differ diff --git a/src/webpage/hover.ts b/src/webpage/hover.ts index fcd5982a..a5359ec8 100644 --- a/src/webpage/hover.ts +++ b/src/webpage/hover.ts @@ -1,43 +1,114 @@ -import { Contextmenu } from "./contextmenu.js"; -import { MarkDown } from "./markdown.js"; +import {Contextmenu} from "./contextmenu.js"; +import {MarkDown} from "./markdown.js"; +type sides = "right" | "bottom"; +class Hover { + str: string | MarkDown | (() => Promise | MarkDown | string); + customHTML?: () => HTMLElement; + weak: boolean; + side: sides; + constructor( + txt: string | MarkDown | (() => Promise | MarkDown | string), + {customHTML, weak, side}: {customHTML?: () => HTMLElement; weak: boolean; side?: sides} = { + weak: true, + }, + ) { + this.customHTML = customHTML; + this.weak = weak; + this.str = txt; + this.side = side || "bottom"; + } + static map = new WeakMap void>(); + static elm: HTMLElement = document.createElement("div"); + static bound: HTMLElement = document.createElement("div"); + static timeout: NodeJS.Timeout; + static watchForGone() { + clearInterval(this.timeout); + this.timeout = setInterval(() => { + if (!document.contains(this.bound)) { + this.elm.remove(); + clearInterval(this.timeout); + } + }, 100); + } + get elm2() { + return Hover.elm; + } + set elm2(elm: HTMLElement) { + Hover.elm = elm; + } + addEvent(elm: HTMLElement) { + let timeOut = setTimeout(() => {}, 0); + const RM = () => { + this.elm2.remove(); + }; -class Hover{ - str:string|MarkDown - constructor(txt:string|MarkDown){ - this.str=txt; - } - addEvent(elm:HTMLElement){ - let timeOut=setTimeout(()=>{},0); - let elm2=document.createElement("div"); - elm.addEventListener("mouseover",()=>{ - timeOut=setTimeout(()=>{ - elm2=this.makeHover(elm); - },750) - }); - elm.addEventListener("mouseout",()=>{ - clearTimeout(timeOut); - elm2.remove(); - }) - } - makeHover(elm:HTMLElement){ - const div=document.createElement("div"); - if(this.str instanceof MarkDown){ - div.append(this.str.makeHTML({stdsize:true})) - }else{ - div.append(this.str); - } - const box=elm.getBoundingClientRect(); - div.style.top=(box.bottom+4)+"px"; - div.style.left=Math.floor(box.left+box.width/2)+"px"; - div.classList.add("hoverthing"); - div.style.opacity="0"; - setTimeout(()=>{ - div.style.opacity="1"; - },10) - document.body.append(div); - Contextmenu.keepOnScreen(div); - console.log(div,elm); - return div; - } + elm.addEventListener("mouseover", () => { + clearTimeout(timeOut); + timeOut = setTimeout(async () => { + RM(); + this.elm2 = await this.makeHover(elm); + Hover.bound = elm; + Hover.watchForGone(); + }, 300); + }); + elm.addEventListener("mouseout", () => { + clearTimeout(timeOut); + RM(); + }); + new MutationObserver((e) => { + if (e[0].removedNodes.length) { + clearTimeout(timeOut); + RM(); + } + }).observe(elm, {childList: true}); + Hover.map.get(elm)?.(); + Hover.map.set(elm, () => { + alert("happened"); + clearTimeout(timeOut); + this.elm2.remove(); + }); + } + async makeHover(elm: HTMLElement) { + if (!document.contains(elm)) return document.createElement("div"); + const div = document.createElement("div"); + + if (this.customHTML) { + div.append(this.customHTML()); + } else { + if (this.str instanceof MarkDown) { + div.append(this.str.makeHTML()); + } else if (this.str instanceof Function) { + const hover = await this.str(); + if (hover instanceof MarkDown) { + div.append(hover.makeHTML()); + } else { + div.innerText = hover; + } + } else { + div.innerText = this.str; + } + } + document.body.append(div); + div.classList.add("hoverthing"); + + const box = elm.getBoundingClientRect(); + const box2 = div.getBoundingClientRect(); + if (this.side === "bottom") { + div.style.top = box.bottom + 4 + "px"; + div.style.left = Math.floor((box.left + box.right - box2.width) / 2) + "px"; + } else if (this.side === "right") { + div.style.left = box.right + 4 + "px"; + div.style.top = Math.floor(box.top + box.height / 4) + "px"; + } + + if (this.weak) { + div.addEventListener("mouseover", () => { + div.remove(); + }); + } + + Contextmenu.keepOnScreen(div); + return div; + } } -export{Hover} +export {Hover}; diff --git a/src/webpage/i18n.ts b/src/webpage/i18n.ts index 742ba328..88cadd0d 100644 --- a/src/webpage/i18n.ts +++ b/src/webpage/i18n.ts @@ -1,122 +1,168 @@ //@ts-ignore -import {langs} from "./translations/langs.js"; -const langmap=new Map(); -for(const lang of Object.keys(langs) as string[]){ - langmap.set(lang,langs[lang]); +import {langs} from "/translations/langs.js"; +const langmap = new Map(); +for (const lang of Object.keys(langs) as string[]) { + langmap.set(lang, langs[lang]); } console.log(langs); -type translation={ - [key:string]:string|translation +type translation = { + [key: string]: string | translation; }; -let res:()=>unknown=()=>{}; -class I18n{ - static lang:string; - static translations:translation[]=[]; - static done=new Promise((res2,_reject)=>{ - res=res2; - }); - static async create(lang:string){ +let res: () => unknown = () => {}; +class I18n { + static lang: string; + static translations: translation[] = []; + static done = new Promise((res2, _reject) => { + res = res2; + }); + static async create(lang: string) { + if (!(lang + ".json" in langs)) { + if (lang.includes("-")) lang = lang.split("-")[0]; + if (!(lang + ".json" in langs)) { + console.warn("Language " + lang + " not found, defaulting to en"); + lang = "en"; + } + } - const json=await (await fetch("/translations/"+lang+".json")).json() as translation; - const translations:translation[]=[]; - translations.push(json); - if(lang!=="en"){ - translations.push(await (await fetch("/translations/en.json")).json() as translation); - } - this.lang=lang; - this.translations=translations; + const json = (await (await fetch("/translations/" + lang + ".json")).json()) as translation; + const translations: translation[] = []; + translations.push(json); + if (lang !== "en") { + translations.push((await (await fetch("/translations/en.json")).json()) as translation); + } + const en = translations.find( + (_) => (_["@metadata"] as translation)?.locale === "en", + ) as translation; + const weirdObj = transForm(en); + Object.assign(this, weirdObj); + this.lang = lang; + this.translations = translations; - res(); - } - static getTranslation(msg:string,...params:string[]):string{ - let str:string|undefined; - const path=msg.split("."); - for(const json of this.translations){ - let jsont:string|translation=json; - for(const thing of path){ - if(typeof jsont !== "string" && jsont!==undefined){ - jsont=jsont[thing]; + res(); + } + static translatePage() { + const elms = document.querySelectorAll("[i18n]"); + for (const elm of Array.from(elms)) { + const t = elm.getAttribute("i18n") as string; + try { + elm.textContent = this.getTranslation(t); + } catch { + console.error("Couldn't get " + t + "'s translation"); + } + } + } + static getTranslation(msg: string, ...params: string[]): string { + let str: string | undefined; + const path = msg.split("."); + for (const json of this.translations) { + let jsont: string | translation = json; + for (const thing of path) { + if (typeof jsont !== "string" && jsont !== undefined) { + jsont = jsont[thing]; + } else { + jsont = json; + break; + } + } - }else{ - jsont=json; - break; - } - } - - if(typeof jsont === "string"){ - str=jsont; - break; - } - } - if(str){ - return this.fillInBlanks(str,params); - }else{ - throw new Error(msg+" not found") - } - } - static fillInBlanks(msg:string,params:string[]):string{ - //thanks to geotale for the regex - msg=msg.replace(/\$\d+/g,(match) => { - const number=Number(match.slice(1)); - if(params[number-1]){ - return params[number-1]; - }else{ - return match; - } - }); - msg=msg.replace(/{{(.+?)}}/g, - (str, match:string) => { - const [op,strsSplit]=this.fillInBlanks(match,params).split(":"); - const [first,...strs]=strsSplit.split("|"); - switch(op.toUpperCase()){ - case "PLURAL":{ - const numb=Number(first); - if(numb===0){ - return strs[strs.length-1]; - } - return strs[Math.min(strs.length-1,numb-1)]; - } - case "GENDER":{ - if(first==="male"){ - return strs[0]; - }else if(first==="female"){ - return strs[1]; - }else if(first==="neutral"){ - if(strs[2]){ - return strs[2]; - }else{ - return strs[0]; - } - } - } - } - return str; - } - ); - - return msg; - } - static options(){ - return [...langmap.keys()].map(e=>e.replace(".json","")); - } - static setLanguage(lang:string){ - if(this.options().indexOf(userLocale)!==-1){ - localStorage.setItem("lang",lang); - I18n.create(lang); - } - } + if (typeof jsont === "string") { + str = jsont; + break; + } + } + if (str) { + return this.fillInBlanks(str, params); + } else { + throw new Error(msg + " not found"); + } + } + static fillInBlanks(msg: string, params: string[]): string { + msg = msg.replace(/(\$\d+)|({{(.+?)}})/g, (_, dolar, str, match) => { + if (dolar) { + const number = Number(dolar.slice(1)); + if (params[number - 1] !== undefined) { + return params[number - 1]; + } else { + return dolar; + } + } else { + const [op, strsSplit] = this.fillInBlanks(match, params).split(":"); + const [first, ...strs] = strsSplit.split("|"); + switch (op.toUpperCase()) { + case "PLURAL": { + const numb = Number(first); + if (numb === 0) { + return strs[strs.length - 1]; + } + return strs[Math.min(strs.length - 1, numb - 1)]; + } + case "GENDER": { + if (first === "male") { + return strs[0]; + } else if (first === "female") { + return strs[1]; + } else if (first === "neutral") { + if (strs[2]) { + return strs[2]; + } else { + return strs[0]; + } + } + } + } + return str; + } + }); + return msg; + } + static options() { + return [...langmap.keys()].map((e) => e.replace(".json", "")); + } + static setLanguage(lang: string) { + if (this.options().indexOf(lang) !== -1) { + getPreferences().then(async (prefs) => { + prefs.locale = lang; + await I18n.create(lang); + await setPreferences(prefs); + }); + } + } } console.log(langmap); -let userLocale = navigator.language.slice(0,2) || "en"; -if(I18n.options().indexOf(userLocale)===-1){ - userLocale="en"; +let userLocale = navigator.language.slice(0, 2) || "en"; +if (I18n.options().indexOf(userLocale) === -1) { + userLocale = "en"; } -const storage=localStorage.getItem("lang"); -if(storage){ - userLocale=storage; -}else{ - localStorage.setItem("lang",userLocale) +const prefs = await getPreferences(); +const storage = prefs.locale; +if (storage) { + userLocale = storage; +} else { + prefs.locale = userLocale; + await setPreferences(prefs); } I18n.create(userLocale); +function transForm(inobj: translation, path: string = "") { + const obj: Record = {}; + for (const [key, value] of Object.entries(inobj)) { + if (typeof value === "string") { + obj[key] = (...args: string[]) => { + return I18n.getTranslation((path ? path + "." : path) + key, ...args); + }; + } else { + obj[key] = transForm(value, (path ? path + "." : path) + key); + } + } + return obj; +} + +import jsonType from "./../../translations/en.json"; +import {getPreferences, setPreferences} from "./utils/storage/userPreferences"; +type beforeType = typeof jsonType; + +type DoTheThing = { + [K in keyof T]: T[K] extends string ? (...args: string[]) => string : DoTheThing; +}; -export{I18n,langmap}; +const cast = I18n as unknown as typeof I18n & DoTheThing; +export {cast as I18n, langmap}; diff --git a/src/webpage/icons/announcensfw.svg b/src/webpage/icons/announcensfw.svg new file mode 100644 index 00000000..2e3864b0 --- /dev/null +++ b/src/webpage/icons/announcensfw.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/call.svg b/src/webpage/icons/call.svg new file mode 100644 index 00000000..063b494d --- /dev/null +++ b/src/webpage/icons/call.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/channelnsfw.svg b/src/webpage/icons/channelnsfw.svg new file mode 100644 index 00000000..55258d15 --- /dev/null +++ b/src/webpage/icons/channelnsfw.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/crown.svg b/src/webpage/icons/crown.svg new file mode 100644 index 00000000..a91c7897 --- /dev/null +++ b/src/webpage/icons/crown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/folder.svg b/src/webpage/icons/folder.svg new file mode 100644 index 00000000..0c54a979 --- /dev/null +++ b/src/webpage/icons/folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/forum.svg b/src/webpage/icons/forum.svg new file mode 100644 index 00000000..4c196c89 --- /dev/null +++ b/src/webpage/icons/forum.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/forumnsfw.svg b/src/webpage/icons/forumnsfw.svg new file mode 100644 index 00000000..4ae8a932 --- /dev/null +++ b/src/webpage/icons/forumnsfw.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/gif.svg b/src/webpage/icons/gif.svg new file mode 100644 index 00000000..703f442d --- /dev/null +++ b/src/webpage/icons/gif.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/gifstar.svg b/src/webpage/icons/gifstar.svg new file mode 100644 index 00000000..7aaa36d7 --- /dev/null +++ b/src/webpage/icons/gifstar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/hangup.svg b/src/webpage/icons/hangup.svg new file mode 100644 index 00000000..807cff69 --- /dev/null +++ b/src/webpage/icons/hangup.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/history.svg b/src/webpage/icons/history.svg new file mode 100644 index 00000000..2b13557a --- /dev/null +++ b/src/webpage/icons/history.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + diff --git a/src/webpage/icons/intoMenu.svg b/src/webpage/icons/intoMenu.svg new file mode 100644 index 00000000..c534cd2c --- /dev/null +++ b/src/webpage/icons/intoMenu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/leftArrow.svg b/src/webpage/icons/leftArrow.svg new file mode 100644 index 00000000..186769b9 --- /dev/null +++ b/src/webpage/icons/leftArrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/mic.svg b/src/webpage/icons/mic.svg new file mode 100644 index 00000000..533378f7 --- /dev/null +++ b/src/webpage/icons/mic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/micmute.svg b/src/webpage/icons/micmute.svg new file mode 100644 index 00000000..e41993b5 --- /dev/null +++ b/src/webpage/icons/micmute.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/mobileSend.svg b/src/webpage/icons/mobileSend.svg new file mode 100644 index 00000000..a0e00713 --- /dev/null +++ b/src/webpage/icons/mobileSend.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/noSelect.svg b/src/webpage/icons/noSelect.svg new file mode 100644 index 00000000..23a1fc41 --- /dev/null +++ b/src/webpage/icons/noSelect.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/novideo.svg b/src/webpage/icons/novideo.svg new file mode 100644 index 00000000..4a5e722f --- /dev/null +++ b/src/webpage/icons/novideo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/pause.svg b/src/webpage/icons/pause.svg new file mode 100644 index 00000000..ab767c67 --- /dev/null +++ b/src/webpage/icons/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/pin.svg b/src/webpage/icons/pin.svg new file mode 100644 index 00000000..ea8e6429 --- /dev/null +++ b/src/webpage/icons/pin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/plainx.svg b/src/webpage/icons/plainx.svg new file mode 100644 index 00000000..e7406fdd --- /dev/null +++ b/src/webpage/icons/plainx.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/play.svg b/src/webpage/icons/play.svg new file mode 100644 index 00000000..6bac363b --- /dev/null +++ b/src/webpage/icons/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/rules.svg b/src/webpage/icons/rules.svg new file mode 100644 index 00000000..3af5d732 --- /dev/null +++ b/src/webpage/icons/rules.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/sad.svg b/src/webpage/icons/sad.svg new file mode 100644 index 00000000..f516f0bf --- /dev/null +++ b/src/webpage/icons/sad.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/search.svg b/src/webpage/icons/search.svg new file mode 100644 index 00000000..42ccd12c --- /dev/null +++ b/src/webpage/icons/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/select.svg b/src/webpage/icons/select.svg new file mode 100644 index 00000000..43e23b5b --- /dev/null +++ b/src/webpage/icons/select.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/soundMore.svg b/src/webpage/icons/soundMore.svg new file mode 100644 index 00000000..2012a2be --- /dev/null +++ b/src/webpage/icons/soundMore.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/spoiler.svg b/src/webpage/icons/spoiler.svg new file mode 100644 index 00000000..f45543fb --- /dev/null +++ b/src/webpage/icons/spoiler.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/sticker.svg b/src/webpage/icons/sticker.svg new file mode 100644 index 00000000..d7e20909 --- /dev/null +++ b/src/webpage/icons/sticker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/stopstream.svg b/src/webpage/icons/stopstream.svg new file mode 100644 index 00000000..83c8b6ec --- /dev/null +++ b/src/webpage/icons/stopstream.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/stream.svg b/src/webpage/icons/stream.svg new file mode 100644 index 00000000..aacd09d6 --- /dev/null +++ b/src/webpage/icons/stream.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/timeout.svg b/src/webpage/icons/timeout.svg new file mode 100644 index 00000000..bd3c594c --- /dev/null +++ b/src/webpage/icons/timeout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/unspoiler.svg b/src/webpage/icons/unspoiler.svg new file mode 100644 index 00000000..032375fe --- /dev/null +++ b/src/webpage/icons/unspoiler.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/update.svg b/src/webpage/icons/update.svg new file mode 100644 index 00000000..aacbfb13 --- /dev/null +++ b/src/webpage/icons/update.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/video.svg b/src/webpage/icons/video.svg new file mode 100644 index 00000000..f0e4653e --- /dev/null +++ b/src/webpage/icons/video.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/icons/voicensfw.svg b/src/webpage/icons/voicensfw.svg new file mode 100644 index 00000000..9ba44ba3 --- /dev/null +++ b/src/webpage/icons/voicensfw.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/index.html b/src/webpage/index.html index 13f62766..8c90cac4 100644 --- a/src/webpage/index.html +++ b/src/webpage/index.html @@ -1,108 +1,109 @@ - + - - - - Jank Client - - - - - - - - + + + Fermi: A Spacebar Client + + + + + + + + - - - -
-
- -

Jank Client is loading

-

This shouldn't take long

-

Switch Accounts

-
+ + -
-
-
-
-

Server Name

-
-
-
-
- -
-
-
- +
+

Welcome to Fermi

+
+ + + Screenshot 1 + + +
+
+
+

+ Spacebar-Compatible Instances: +

+
+
+
+

+ Fermi is a Spacebar-compatible client seeking to be as good as it can be with many + features including: +

+
    +
  • Direct Messaging
  • +
  • Reactions support
  • +
  • Invites
  • +
  • Account switching
  • +
  • User settings
  • +
  • Developer portal
  • +
  • Bot invites
  • +
  • Translation support
  • +
+
-
-

USERNAME

-

STATUS

-
-
+
+

Fermi Blog

+

+ Read or subscribe to the blog for updates on Fermi! +

+
+ Blog +
+
-
- -
-
-
+
+

Translate Fermi

+

+ You can help translate Fermi into your own language! +

+
+ + Translate +
-
-
- - - - Channel name - - - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
- -
-
-
-
+ +
+

Contribute to Fermi

+

+ We always appreciate some help, whether that be in the form of bug reports, code, help + translate, or even just pointing out some typos. +

+
+ Github
- diff --git a/src/webpage/index.ts b/src/webpage/index.ts index 6499fcbc..4be6d409 100644 --- a/src/webpage/index.ts +++ b/src/webpage/index.ts @@ -1,306 +1,475 @@ -import{ Localuser }from"./localuser.js"; -import{ Contextmenu }from"./contextmenu.js"; -import{ mobile, getBulkUsers, setTheme, Specialuser }from"./login.js"; -import{ MarkDown }from"./markdown.js"; -import{ Message }from"./message.js"; -import{File}from"./file.js"; -import { I18n } from "./i18n.js"; -(async ()=>{ - await I18n.done - const users = getBulkUsers(); - if(!users.currentuser){ - window.location.href = "/login.html"; - return; - } - { - const loadingText=document.getElementById("loadingText"); - const loaddesc=document.getElementById("load-desc"); - const switchaccounts=document.getElementById("switchaccounts"); - const filedroptext=document.getElementById("filedroptext"); - if(loadingText&&loaddesc&&switchaccounts&&filedroptext){ - loadingText.textContent=I18n.getTranslation("htmlPages.loadingText"); - loaddesc.textContent=I18n.getTranslation("htmlPages.loaddesc"); - switchaccounts.textContent=I18n.getTranslation("htmlPages.switchaccounts"); - filedroptext.textContent=I18n.getTranslation("uploadFilesText"); - } - } - I18n - function showAccountSwitcher(): void{ - const table = document.createElement("div"); - table.classList.add("flexttb","accountSwitcher"); - - for(const user of Object.values(users.users)){ - const specialUser = user as Specialuser; - const userInfo = document.createElement("div"); - userInfo.classList.add("flexltr", "switchtable"); - - const pfp = document.createElement("img"); - pfp.src = specialUser.pfpsrc; - pfp.classList.add("pfp"); - userInfo.append(pfp); - - const userDiv = document.createElement("div"); - userDiv.classList.add("userinfo"); - userDiv.textContent = specialUser.username; - userDiv.append(document.createElement("br")); - - const span = document.createElement("span"); - span.textContent = specialUser.serverurls.wellknown - .replace("https://", "") - .replace("http://", ""); - span.classList.add("serverURL"); - userDiv.append(span); - - userInfo.append(userDiv); - table.append(userInfo); - - userInfo.addEventListener("click", ()=>{ - thisUser.unload(); - thisUser.swapped = true; - const loading = document.getElementById("loading") as HTMLDivElement; - loading.classList.remove("doneloading"); - loading.classList.add("loading"); - - thisUser = new Localuser(specialUser); - users.currentuser = specialUser.uid; - localStorage.setItem("userinfos", JSON.stringify(users)); +import {Localuser} from "./localuser.js"; +import {Contextmenu} from "./contextmenu.js"; +import {mobile, Specialuser} from "./utils/utils.js"; +import {setTheme} from "./utils/utils.js"; +import {MarkDown} from "./markdown.js"; +import {Message} from "./message.js"; +import {File} from "./file.js"; +import {I18n} from "./i18n.js"; +import "./utils/pollyfills.js"; +import {makeLogin} from "./login.js"; +import {Hover} from "./hover.js"; +import "./templatePage.js"; +import "./more.js"; +import "./recover.js"; +import "./home.js"; +import "./invite.js"; +import "./oauth2/auth.js"; +import "./audio/page.js"; +import "./404.js"; +import {Channel} from "./channel.js"; - thisUser.initwebsocket().then(()=>{ - thisUser.loaduser(); - thisUser.init(); - loading.classList.add("doneloading"); - loading.classList.remove("loading"); - console.log("done loading"); - }); +if (window.location.pathname === "/app") { + window.location.pathname = "/channels/@me"; +} +export interface CustomHTMLDivElement extends HTMLDivElement { + markdown: MarkDown; +} +if (window.location.pathname.startsWith("/channels")) { + let templateID = new URLSearchParams(window.location.search).get("templateID"); + await I18n.done; + Localuser.loadFont(); - userInfo.remove(); - }); - } - - const switchAccountDiv = document.createElement("div"); - switchAccountDiv.classList.add("switchtable"); - switchAccountDiv.textContent = I18n.getTranslation("switchAccounts"); - switchAccountDiv.addEventListener("click", ()=>{ - window.location.href = "/login.html"; - }); - table.append(switchAccountDiv); - - if(Contextmenu.currentmenu){ - Contextmenu.currentmenu.remove(); - } - Contextmenu.currentmenu = table; - document.body.append(table); - } + I18n.translatePage(); const userInfoElement = document.getElementById("userinfo") as HTMLDivElement; - userInfoElement.addEventListener("click", event=>{ + userInfoElement.addEventListener("click", (event) => { event.stopImmediatePropagation(); - showAccountSwitcher(); + const rect = userInfoElement.getBoundingClientRect(); + Localuser.userMenu.makemenu(rect.x, rect.top - 10 - window.innerHeight, thisUser); }); const switchAccountsElement = document.getElementById("switchaccounts") as HTMLDivElement; - switchAccountsElement.addEventListener("click", event=>{ + switchAccountsElement.addEventListener("click", async (event) => { event.stopImmediatePropagation(); - showAccountSwitcher(); + Localuser.showAccountSwitcher(thisUser); }); let thisUser: Localuser; - try{ - console.log(users.users, users.currentuser); - thisUser = new Localuser(users.users[users.currentuser]); - thisUser.initwebsocket().then(()=>{ + function regSwap(l: Localuser) { + l.onswap = (l) => { + thisUser = l; + regSwap(l); + }; + l.fileExtange = (img, html) => { + const blobArr: Blob[] = []; + const htmlArr = imagesHtml; + let i = 0; + for (const file of images) { + const img = imagesHtml.get(file); + if (!img) continue; + if (pasteImageElement.contains(img)) { + pasteImageElement.removeChild(img); + blobArr.push(images[i]); + } else { + i++; + } + } + images = img; + imagesHtml = html; + for (const file of images) { + const img = imagesHtml.get(file); + if (!img) throw new Error("Image without HTML, exiting"); + pasteImageElement.append(img); + } + return [blobArr, htmlArr]; + }; + } + const loaddesc = document.getElementById("load-desc") as HTMLSpanElement; + try { + const current = sessionStorage.getItem("currentuser") || Localuser.users.currentuser; + if (!Localuser.users.users[current]) { + thisUser = new Localuser(await new Promise((res) => makeLogin(true, "", res))); + } else { + thisUser = new Localuser(Localuser.users.users[current]); + } + + regSwap(thisUser); + thisUser.initwebsocket().then(async () => { thisUser.loaduser(); - thisUser.init(); + console.warn("huh"); + await thisUser.init(); + console.warn("huh2"); const loading = document.getElementById("loading") as HTMLDivElement; loading.classList.add("doneloading"); loading.classList.remove("loading"); + loaddesc.textContent = I18n.loaded(); console.log("done loading"); + if (templateID) { + thisUser.passTemplateID(templateID); + } }); - }catch(e){ + } catch (e) { + debugger; console.error(e); - (document.getElementById("load-desc") as HTMLSpanElement).textContent = I18n.getTranslation("accountNotStart"); + loaddesc.textContent = I18n.accountNotStart(); thisUser = new Localuser(-1); } - - const menu = new Contextmenu("create rightclick"); - menu.addbutton( - I18n.getTranslation("channel.createChannel"), - ()=>{ - if(thisUser.lookingguild){ + //TODO move this to the channel/guild class, this is a weird spot + const menu = new Contextmenu("create rightclick"); + menu.addButton( + I18n.channel.createChannel(), + () => { + if (thisUser.lookingguild) { thisUser.lookingguild.createchannels(); } }, - null, - ()=>thisUser.isAdmin() + { + visible: function () { + return thisUser.lookingguild?.member.hasPermission("MANAGE_CHANNELS") || false; + }, + }, ); - menu.addbutton( - I18n.getTranslation("channel.createCatagory"), - ()=>{ - if(thisUser.lookingguild){ + menu.addButton( + I18n.channel.createCatagory(), + () => { + if (thisUser.lookingguild) { thisUser.lookingguild.createcategory(); } }, - null, - ()=>thisUser.isAdmin() + { + visible: function () { + return thisUser.lookingguild?.member.hasPermission("MANAGE_CHANNELS") || false; + }, + }, ); - + const channelw = document.getElementById("channelw"); + if (channelw) + channelw.addEventListener("keypress", (e) => { + if (e.ctrlKey || e.altKey || e.metaKey || e.metaKey) return; + let owner = e.target as HTMLElement; + while (owner !== channelw) { + if (owner.tagName === "input" || owner.contentEditable !== "false") { + return; + } + owner = owner.parentElement as HTMLElement; + } + typebox.markdown.boxupdate(Infinity); + }); menu.bindContextmenu(document.getElementById("channels") as HTMLDivElement); const pasteImageElement = document.getElementById("pasteimage") as HTMLDivElement; let replyingTo: Message | null = null; - window.addEventListener("popstate",(e)=>{ - if(e.state instanceof Object){ - thisUser.goToChannel(e.state[1],false); + window.addEventListener("popstate", (e) => { + if (e.state instanceof Object) { + thisUser.goToState(e.state); } //console.log(e.state,"state:3") - }) - async function handleEnter(event: KeyboardEvent): Promise{ - if(thisUser.keyup(event)){return} - const channel = thisUser.channelfocus; - if(!channel)return; - if(markdown.rawString===""&&event.key==="ArrowUp"){ - channel.editLast(); + }); + let nonceMap = new Map(); + //@ts-expect-error unused right now, not needed + function getNonce(id: string) { + const nonce = nonceMap.get(id) || Math.floor(Math.random() * 1000000000) + ""; + nonceMap.set(id, nonce); + return nonce; + } + const markdown = new MarkDown("", thisUser); + async function sendMessage(channel: Channel, content: string) { + if (!channel.canMessageRightNow()) return; + if (channel.curCommand) { + channel.submitCommand(); return; } - channel.typingstart(); + markdown.onUpdate("", false); - if(event.key === "Enter" && !event.shiftKey){ - event.preventDefault(); - replyingTo = thisUser.channelfocus? thisUser.channelfocus.replyingto: null; - if(replyingTo?.div){ - replyingTo.div.classList.remove("replying"); + replyingTo = thisUser.channelfocus ? thisUser.channelfocus.replyingto : null; + if (replyingTo?.div) { + replyingTo.div.classList.remove("replying"); + } + if (thisUser.channelfocus) { + thisUser.channelfocus.replyingto = null; + thisUser.channelfocus.makereplybox(); + } + const attachments = images.filter((_) => document.contains(imagesHtml.get(_) || null)); + while (images.length) { + const elm = imagesHtml.get(images.pop() as Blob) as HTMLElement; + if (pasteImageElement.contains(elm)) pasteImageElement.removeChild(elm); + } + typebox.innerHTML = ""; + typebox.markdown.txt = []; + try { + await new Promise((mres, rej) => + channel.sendMessage( + content, + { + attachments, + embeds: [], // Add an empty array for the embeds property + replyingto: replyingTo, + sticker_ids: [], + //nonce: getNonce(channel.id), + }, + (res) => { + if (res === "Ok") { + mres(); + } else { + rej(); + } + }, + ), + ); + } catch { + images = attachments; + for (const file of images) { + const img = imagesHtml.get(file); + if (!img) continue; + pasteImageElement.append(img); } - if(thisUser.channelfocus){ - thisUser.channelfocus.replyingto = null; + channel.replyingto = replyingTo; + channel.makereplybox(); + typebox.textContent = content; + typebox.markdown.txt = content.split(""); + typebox.markdown.boxupdate(Infinity); + } + nonceMap.delete(channel.id); + } + const mobileSend = document.getElementById("mobileSend"); + if (mobileSend) { + mobileSend.onclick = () => { + const channel = thisUser.channelfocus; + if (!channel) return; + const content = MarkDown.gatherBoxText(typebox); + sendMessage(channel, content); + }; + } + async function handleEnter(event: KeyboardEvent): Promise { + if (event.isComposing) return; + if (event.key === "Escape" && (images.length || thisUser.channelfocus?.replyingto)) { + while (images.length) { + const elm = imagesHtml.get(images.pop() as Blob) as HTMLElement; + if (pasteImageElement.contains(elm)) pasteImageElement.removeChild(elm); } - channel.sendMessage(markdown.rawString, { - attachments: images, - // @ts-ignore This is valid according to the API - embeds: [], // Add an empty array for the embeds property - replyingto: replyingTo, - }); - if(thisUser.channelfocus){ + if (thisUser.channelfocus) { + thisUser.channelfocus?.replyingto?.div?.classList.remove("replying"); + thisUser.channelfocus.replyingto = null; thisUser.channelfocus.makereplybox(); } - while(images.length){ - images.pop(); - pasteImageElement.removeChild(imagesHtml.pop() as HTMLElement); - } + return; + } + if (thisUser.handleKeyUp(event)) { + return; + } - typebox.innerHTML = ""; + const channel = thisUser.channelfocus; + if (!channel) return; + const content = MarkDown.gatherBoxText(typebox); + if (content === "" && event.key === "ArrowUp") { + channel.editLast(); + return; } - } + channel.typingstart(); - interface CustomHTMLDivElement extends HTMLDivElement {markdown: MarkDown;} + if (event.key === "Enter" && !event.shiftKey && window.innerWidth > 600) { + event.preventDefault(); + await sendMessage(channel, content); + } + } const typebox = document.getElementById("typebox") as CustomHTMLDivElement; - const markdown = new MarkDown("", thisUser); + typebox.markdown = markdown; typebox.addEventListener("keyup", handleEnter); - typebox.addEventListener("keydown", event=>{ - thisUser.keydown(event) - if(event.key === "Enter" && !event.shiftKey) event.preventDefault(); + typebox.addEventListener("keydown", (event) => { + if (event.isComposing) return; + thisUser.keydown(event); + if (event.key === "Enter" && !event.shiftKey && window.innerWidth > 600) { + event.preventDefault(); + event.stopImmediatePropagation(); + } }); markdown.giveBox(typebox); + { + const searchBox = document.getElementById("searchBox") as CustomHTMLDivElement; + const markdown = new MarkDown("", thisUser); + searchBox.markdown = markdown; + const searchX = document.getElementById("searchX") as HTMLElement; + searchBox.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + thisUser.mSearch(markdown.rawString); + } + }); + searchBox.addEventListener("keyup", () => { + if (searchBox.textContent === "") { + setTimeout(() => (searchBox.innerHTML = ""), 0); + searchX.classList.add("svg-search"); + searchX.classList.remove("svg-plainx"); + searchBox.parentElement!.classList.remove("searching"); + } else { + searchX.classList.remove("svg-search"); + searchX.classList.add("svg-plainx"); + searchBox.parentElement!.classList.add("searching"); + } + }); + const sideContainDiv = document.getElementById("sideContainDiv") as HTMLElement; + searchBox.onclick = () => { + sideContainDiv.classList.remove("hideSearchDiv"); + }; + searchX.onclick = () => { + if (searchX.classList.contains("svg-plainx")) { + markdown.txt = []; + searchBox.innerHTML = ""; + searchX.classList.add("svg-search"); + searchBox.parentElement!.classList.remove("searching"); + searchX.classList.remove("svg-plainx"); + thisUser.mSearch(""); + } else { + searchBox.parentElement!.classList.add("searching"); + } + }; - const images: Blob[] = []; - const imagesHtml: HTMLElement[] = []; + markdown.giveBox(searchBox); + markdown.setCustomBox((e) => { + const span = document.createElement("span"); + span.textContent = e.replace("\n", ""); + return span; + }); + } + let images: Blob[] = []; + let imagesHtml = new WeakMap(); - document.addEventListener("paste", async (e: ClipboardEvent)=>{ - if(!e.clipboardData)return; + document.addEventListener("paste", async (e: ClipboardEvent) => { + if (!thisUser.channelfocus) return; + if (!e.clipboardData) return; - for(const file of Array.from(e.clipboardData.files)){ + for (const file of Array.from(e.clipboardData.files)) { const fileInstance = File.initFromBlob(file); e.preventDefault(); - const html = fileInstance.upHTML(images, file); + const html = fileInstance.upHTML(images, imagesHtml, file); pasteImageElement.appendChild(html); images.push(file); - imagesHtml.push(html); + imagesHtml.set(file, html); } }); - setTheme(); + await setTheme(); - function userSettings(): void{ + function userSettings(): void { thisUser.showusersettings(); } - (document.getElementById("settings") as HTMLImageElement).onclick = - userSettings; - - if(mobile){ + (document.getElementById("settings") as HTMLImageElement).onclick = userSettings; + const memberListToggle = document.getElementById("memberlisttoggle") as HTMLInputElement; + memberListToggle.checked = !localStorage.getItem("memberNotChecked"); + memberListToggle.onchange = () => { + if (!memberListToggle.checked) { + localStorage.setItem("memberNotChecked", "true"); + } else { + localStorage.removeItem("memberNotChecked"); + } + }; + if (mobile) { const channelWrapper = document.getElementById("channelw") as HTMLDivElement; - channelWrapper.onclick = ()=>{ + channelWrapper.onclick = () => { const toggle = document.getElementById("maintoggle") as HTMLInputElement; toggle.checked = true; }; - const memberListToggle = document.getElementById("memberlisttoggle") as HTMLInputElement; memberListToggle.checked = false; } - let dragendtimeout=setTimeout(()=>{}) - document.addEventListener("dragover",(e)=>{ + let dragendtimeout = setTimeout(() => {}); + document.addEventListener("dragover", (e) => { clearTimeout(dragendtimeout); const data = e.dataTransfer; - const bg=document.getElementById("gimmefile") as HTMLDivElement; + const bg = document.getElementById("gimmefile") as HTMLDivElement; - if(data){ - const isfile=data.types.includes("Files")||data.types.includes("application/x-moz-file"); - if(!isfile){ - bg.hidden=true; + if (data) { + const isfile = data.types.includes("Files") || data.types.includes("application/x-moz-file"); + if (!isfile) { + bg.hidden = true; return; } e.preventDefault(); - bg.hidden=false; + bg.hidden = false; //console.log(data.types,data) - }else{ - bg.hidden=true; + } else { + bg.hidden = true; } }); - document.addEventListener("dragleave",(_)=>{ - dragendtimeout=setTimeout(()=>{ - const bg=document.getElementById("gimmefile") as HTMLDivElement; - bg.hidden=true; - },1000) + document.addEventListener("dragleave", (_) => { + dragendtimeout = setTimeout(() => { + const bg = document.getElementById("gimmefile") as HTMLDivElement; + bg.hidden = true; + }, 1000); }); - document.addEventListener("dragenter",(e)=>{ + document.addEventListener("dragenter", (e) => { e.preventDefault(); - }) - document.addEventListener("drop",e=>{ + }); + document.addEventListener("drop", (e) => { const data = e.dataTransfer; - const bg=document.getElementById("gimmefile") as HTMLDivElement; - bg.hidden=true; - if(data){ - const isfile=data.types.includes("Files")||data.types.includes("application/x-moz-file"); - if(isfile){ + const bg = document.getElementById("gimmefile") as HTMLDivElement; + bg.hidden = true; + if (!thisUser.channelfocus) { + e.preventDefault(); + return; + } + if (data) { + const isfile = data.types.includes("Files") || data.types.includes("application/x-moz-file"); + if (isfile) { e.preventDefault(); console.log(data.files); - for(const file of Array.from(data.files)){ + for (const file of Array.from(data.files)) { const fileInstance = File.initFromBlob(file); - const html = fileInstance.upHTML(images, file); + const html = fileInstance.upHTML(images, imagesHtml, file); pasteImageElement.appendChild(html); images.push(file); - imagesHtml.push(html); + imagesHtml.set(file, html); } } } }); - (document.getElementById("upload") as HTMLElement).onclick=()=>{ - const input=document.createElement("input"); - input.type="file"; + const pinnedM = document.getElementById("pinnedM") as HTMLElement; + pinnedM.onclick = (e) => { + thisUser.pinnedClick(pinnedM.getBoundingClientRect()); + e.preventDefault(); + e.stopImmediatePropagation(); + }; + (document.getElementById("upload") as HTMLElement).onclick = () => { + const input = document.createElement("input"); + input.type = "file"; input.click(); - console.log("clicked") - input.onchange=(() => { - if(input.files){ - for(const file of Array.from(input.files)){ + input.multiple = true; + console.log("clicked"); + if (!thisUser.channelfocus) return; + input.onchange = () => { + if (input.files) { + for (const file of Array.from(input.files)) { const fileInstance = File.initFromBlob(file); - const html = fileInstance.upHTML(images, file); + const html = fileInstance.upHTML(images, imagesHtml, file); pasteImageElement.appendChild(html); images.push(file); - imagesHtml.push(html); + imagesHtml.set(file, html); } } - }) - } + }; + }; + const emojiTB = document.getElementById("emojiTB") as HTMLElement; + emojiTB.onmousedown = (e) => e.stopImmediatePropagation(); + emojiTB.onclick = (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + thisUser.TBEmojiMenu(emojiTB.getBoundingClientRect()); + }; -})(); + const gifTB = document.getElementById("gifTB") as HTMLElement; + gifTB.onmousedown = (e) => e.stopImmediatePropagation(); + gifTB.onclick = (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + thisUser.makeGifBox(gifTB.getBoundingClientRect()); + }; + + const stickerTB = document.getElementById("stickerTB") as HTMLElement; + stickerTB.onmousedown = (e) => e.stopImmediatePropagation(); + stickerTB.onclick = (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + thisUser.makeStickerBox(stickerTB.getBoundingClientRect()); + }; + const updateIcon = document.getElementById("updateIcon"); + if (updateIcon) { + new Hover(() => updateIcon.textContent || "").addEvent(updateIcon); + updateIcon.onclick = () => { + window.location.reload(); + }; + } +} diff --git a/src/webpage/infiniteScroller.ts b/src/webpage/infiniteScroller.ts index 5332bb42..e11e85e1 100644 --- a/src/webpage/infiniteScroller.ts +++ b/src/webpage/infiniteScroller.ts @@ -1,350 +1,487 @@ -class InfiniteScroller{ - readonly getIDFromOffset: ( -ID: string, -offset: number -) => Promise; - readonly getHTMLFromID: (ID: string) => Promise; - readonly destroyFromID: (ID: string) => Promise; - readonly reachesBottom: () => void; - private readonly minDist = 2000; - private readonly fillDist = 3000; - private readonly maxDist = 6000; - HTMLElements: [HTMLElement, string][] = []; - div: HTMLDivElement | null = null; - timeout: ReturnType | null = null; - beenloaded = false; - scrollBottom = 0; - scrollTop = 0; - needsupdate = true; - averageheight = 60; - watchtime = false; - changePromise: Promise | undefined; - scollDiv!: { scrollTop: number; scrollHeight: number; clientHeight: number }; - - resetVars(){ - this.scrollTop=0; - this.scrollBottom=0; - this.averageheight=60; - this.watchtime=false; - this.needsupdate=true; - this.beenloaded=false; - this.changePromise=undefined; - if(this.timeout){ - clearTimeout(this.timeout); - this.timeout=null; +function fragAppend(div: HTMLElement, pre = false) { + let qued = false; + const par = div.parentElement as Element; + if (!par) throw new Error("parrent is missing"); + function appendFrag() { + const elms = Array.from(frag.children) as HTMLElement[]; + let didForce = false; + if (pre) { + if (supports) { + if (par.scrollTop === 0) { + par.scrollTop += 3; + didForce = true; + } + } + } + div[pre ? "prepend" : "append"](frag); + + if (didForce) { + par.scrollTop -= 3; } - for(const thing of this.HTMLElements){ - this.destroyFromID(thing[1]); + if (pre && !supports) { + let top = -Infinity; + let bottom = Infinity; + elms.forEach((_) => { + const rec = _.getBoundingClientRect(); + top = Math.max(top, rec.top); + bottom = Math.min(bottom, rec.bottom); + }); + const h = top - bottom; + const p = div.parentNode; + if (p instanceof HTMLElement) { + p.scrollTop += h; + } } - this.HTMLElements=[]; - this.div=null; } + const frag = document.createDocumentFragment(); + let count = 0; + return (elm: HTMLElement) => + new Promise((res) => { + count++; + frag[pre ? "prepend" : "append"](elm); + if (!qued) { + let lcount = count; + function wait(t = 0) { + if (count !== lcount) { + lcount = count; + t = 0; + } + if (t === 10) { + appendFrag(); + qued = false; + res(); + return; + } + queueMicrotask(() => { + wait(t + 1); + }); + } + wait(); + qued = true; + } else { + res(); + } + }); +} + +const supports = CSS.supports("overflow-anchor", "auto"); +class InfiniteScroller { + readonly getIDFromOffset: (ID: string, offset: number) => Promise; + readonly getHTMLFromID: (ID: string) => HTMLElement; + readonly destroyFromID: (ID: string) => Promise; + readonly reachesBottom: () => void; + + private weakDiv = new WeakRef(document.createElement("div")); + private curFocID?: string; + private curElms = new Map(); + private backElm = new Map(); + private forElm = new Map(); + private weakElmId = new WeakMap(); + + get div() { + return this.weakDiv.deref(); + } + set div(div: HTMLDivElement | undefined) { + this.weakDiv = new WeakRef(div || document.createElement("div")); + } + get scroller() { + return this.div?.children[0] as HTMLDivElement | undefined; + } + constructor( getIDFromOffset: InfiniteScroller["getIDFromOffset"], getHTMLFromID: InfiniteScroller["getHTMLFromID"], destroyFromID: InfiniteScroller["destroyFromID"], - reachesBottom: InfiniteScroller["reachesBottom"] = ()=>{} - ){ + reachesBottom: InfiniteScroller["reachesBottom"] = () => {}, + ) { this.getIDFromOffset = getIDFromOffset; this.getHTMLFromID = getHTMLFromID; this.destroyFromID = destroyFromID; this.reachesBottom = reachesBottom; } + observer: IntersectionObserver = new IntersectionObserver(console.log); - async getDiv(initialId: string): Promise{ - if(this.div){ - throw new Error("Div already exists, exiting."); + private heightMap = new WeakMap(); + private createObserver(root: HTMLDivElement) { + const scroller = root.children[0]; + function sorted() { + return Array.from(scroller.children).filter((_) => visable.has(_)) as HTMLElement[]; } - this.resetVars(); - const scroll = document.createElement("div"); - scroll.classList.add("scroller"); - this.div = scroll; + if ("ResizeObserver" in globalThis) { + let height = 0; + new ResizeObserver((e) => { + const nh = e[0].target.getBoundingClientRect().height; + if (height) { + if (this.scrollBottom - height + nh < 2) root.scrollTop = root.scrollHeight; + else if (root.scrollTop + nh + 6 > root.scrollHeight) root.scrollTop += -height + nh; + else root.scrollTop += height - nh; + console.log(root.scrollTop + height + 6 - root.scrollHeight); + } + height = nh; + }).observe(root); + } + const heights = new WeakMap(); + const re = + "ResizeObserver" in globalThis + ? new ResizeObserver((e) => { + for (const elm of e) { + const nh = elm.target.getBoundingClientRect().height; + const height = heights.get(elm.target); + if (height && nh) { + if (!this.atBottom()) root.scrollTop -= height - nh; + } + heights.set(elm.target, nh); + } + }) + : undefined; + //TODO maybe a workarround? + const visable = new Set(); + this.observer = new IntersectionObserver( + (obvs) => { + for (const obv of obvs) { + if (obv.target instanceof HTMLElement) { + if (obv.isIntersecting) { + visable.add(obv.target); + re?.observe(obv.target); + } else { + visable.delete(obv.target); + re?.unobserve(obv.target); + } - this.div.addEventListener("scroll", ()=>{ - this.checkscroll(); - if(this.scrollBottom < 5){ - this.scrollBottom = 5; - } - if(this.timeout === null){ - this.timeout = setTimeout(this.updatestuff.bind(this), 300); - } - this.watchForChange(); - }); + this.heightMap.set(obv.target, obv.boundingClientRect.height); + } + } + for (const obv of obvs) { + if (obv.target instanceof HTMLElement) { + const id = this.weakElmId.get(obv.target); + if (id && !obv.isIntersecting && id === this.curFocID) { + const elms = sorted(); - let oldheight = 0; - new ResizeObserver(()=>{ - this.checkscroll(); - const func = this.snapBottom(); - this.updatestuff(); - const change = oldheight - scroll.offsetHeight; - if(change > 0 && this.div){ - this.div.scrollTop += change; + const middle = elms[(elms.length / 2) | 0]; + const id = this.weakElmId.get(middle); + if (!id) continue; + this.curFocID = id; + this.fillIn(true); + } else if (!id) console.log("uh..."); + } + } + }, + {root, threshold: 0.1}, + ); + let time = Date.now(); + const handleScroll = async () => { + await new Promise((res) => requestAnimationFrame(res)); + if (this.scrollBottom < 5) { + const scroll = this.scroller; + if (!scroll) return; + const last = this.weakElmId.get(Array.from(scroll.children).at(-1) as HTMLElement); + if (!last) return; + if (this.backElm.get(last) || !this.backElm.has(last)) return; + this.reachesBottom(); + } + }; + let last = 0; + root.addEventListener("scroll", async () => { + const now = Date.now(); + const thisid = ++last; + if (now - time < 500) { + await new Promise((res) => setTimeout(res, 500)); + if (thisid !== last) return; } - oldheight = scroll.offsetHeight; - this.watchForChange(); - func(); - }).observe(scroll); - - new ResizeObserver(this.watchForChange.bind(this)).observe(scroll); - - await this.firstElement(initialId); - this.updatestuff(); - await this.watchForChange().then(()=>{ - this.updatestuff(); - this.beenloaded = true; + time = now; + handleScroll(); }); - - return scroll; } - checkscroll(): void{ - if(this.beenloaded && this.div && !document.body.contains(this.div)){ - console.warn("not in document"); - this.div = null; - } - } + async getDiv(initialId: string, flash = false): Promise { + const div = document.createElement("div"); + div.classList.add("scroller"); + this.div = div; - async updatestuff(): Promise{ - this.timeout = null; - if(!this.div)return; + const scroll = document.createElement("div"); + div.append(scroll); - this.scrollBottom = - this.div.scrollHeight - this.div.scrollTop - this.div.clientHeight; - this.averageheight = this.div.scrollHeight / this.HTMLElements.length; - if(this.averageheight < 10){ - this.averageheight = 60; - } - this.scrollTop = this.div.scrollTop; + this.createObserver(div); - if(!this.scrollBottom && !(await this.watchForChange())){ - this.reachesBottom(); - } - if(!this.scrollTop){ - await this.watchForChange(); + await this.focus(initialId, flash, true); + return div; + } + private get scrollBottom() { + if (this.div) { + return this.div.scrollHeight - this.div.scrollTop - this.div.clientHeight; + } else { + return 0; } - this.needsupdate = false; } - async firstElement(id: string): Promise{ - if(!this.div)return; - const html = await this.getHTMLFromID(id); - this.div.appendChild(html); - this.HTMLElements.push([html, id]); + async addedBottom(): Promise { + const snap = this.snapBottom(); + const scroll = this.scroller; + if (!scroll) return; + const last = this.weakElmId.get(Array.from(scroll.children).at(-1) as HTMLElement); + if (!last) return; + this.backElm.delete(last); + await this.fillIn(); + snap(); } - async addedBottom(): Promise{ - await this.updatestuff(); - const func = this.snapBottom(); - await this.watchForChange(); - func(); + atBottom() { + const scroll = this.scroller; + if (!scroll) return false; + const last = this.weakElmId.get(Array.from(scroll.children).at(-1) as HTMLElement); + if (!last) return false; + if (this.backElm.get(last) || !this.backElm.has(last)) return false; + return this.scrollBottom < 4; } - - snapBottom(): () => void{ - const scrollBottom = this.scrollBottom; - return()=>{ - if(this.div && scrollBottom < 4){ - this.div.scrollTop = this.div.scrollHeight; - } - }; + toBottom() { + if (this.div) this.div.scrollTop = this.div.scrollHeight; } - private async watchForTop( - already = false, - fragment = new DocumentFragment() - ): Promise{ - if(!this.div)return false; - try{ - let again = false; - if(this.scrollTop < (already ? this.fillDist : this.minDist)){ - let nextid: string | undefined; - const firstelm = this.HTMLElements.at(0); - if(firstelm){ - const previd = firstelm[1]; - nextid = await this.getIDFromOffset(previd, 1); - } + snapBottom(): () => void { + if (this.div && this.atBottom()) { + const trigger = this.scrollBottom < 4; + return () => { + if (this.div && trigger) this.toBottom(); + }; + } else { + return () => {}; + } + } - if(nextid){ - const html = await this.getHTMLFromID(nextid); + async deleteId(id: string) { + const prev = this.backElm.get(id) || (this.backElm.has(id) ? null : undefined); + const next = this.forElm.get(id) || (this.forElm.has(id) ? null : undefined); + await this.removeElm(id); + if (prev && next !== null) this.forElm.set(prev, next); + if (next && prev !== null) this.backElm.set(next, prev); + } - if(!html){ - this.destroyFromID(nextid); - return false; - } - again = true; - fragment.prepend(html); - this.HTMLElements.unshift([html, nextid]); - this.scrollTop += this.averageheight; + private async clearElms() { + await Promise.all(this.curElms.keys().map((id) => this.destroyFromID(id))); + this.curElms.clear(); + this.backElm.clear(); + this.forElm.clear(); + const scroller = this.scroller; + if (!scroller) return; + scroller.innerHTML = ""; + } - } - } - if(this.scrollTop > this.maxDist){ - const html = this.HTMLElements.shift(); - if(html){ - again = true; - await this.destroyFromID(html[1]); + private async removeElm(id: string) { + const back = this.backElm.get(id); + if (back) this.forElm.delete(back); + this.backElm.delete(id); - this.scrollTop -= this.averageheight; + const forward = this.forElm.get(id); + if (forward) this.backElm.delete(forward); + this.forElm.delete(id); - } - } - if(again){ - await this.watchForTop(true, fragment); - } - return again; - }finally{ - if(!already){ - if(this.div.scrollTop === 0){ - this.scrollTop = 1; - this.div.scrollTop = 10; - } - this.div.prepend(fragment, fragment); + const elm = this.curElms.get(id); + this.curElms.delete(id); + await this.destroyFromID(id); + elm?.remove(); + } + private getFromID(id: string) { + if (this.curElms.has(id)) { + return this.curElms.get(id) as HTMLElement; + } + const elm = this.getHTMLFromID(id); + this.curElms.set(id, elm); + this.weakElmId.set(elm, id); + this.observer.observe(elm); + return elm; + } + //@ts-ignore-error + private checkIDs() { + const scroll = this.scroller; + if (!scroll) return; + const kids = Array.from(scroll.children) + .map((_) => this.weakElmId.get(_ as HTMLElement)) + .filter((_) => _ !== undefined); + let last = null; + for (const kid of kids) { + if (last === null) { + last = kid; + } else { + if (this.backElm.get(last) !== kid) + console.log("back is wrong", kid, this.backElm.get(kid)); + if (this.forElm.get(kid) !== last) + console.log("for is wrong", last, this.backElm.get(last)); + last = kid; } } + const e = new Set(this.curElms.keys()); + if (e.symmetricDifference(new Set(kids)).size) + console.log("cur elms is wrong", e.symmetricDifference(new Set(kids))); + } + private addLink(prev: string | undefined, next: string | undefined) { + if (prev) this.forElm.set(prev, next); + if (next) this.backElm.set(next, prev); } + private async fillInTop() { + const scroll = this.scroller; + if (!scroll) return; + let top = this.curFocID; + let count = 0; + let limit = 50; + const app = fragAppend(scroll, true); + const proms: Promise[] = []; - async watchForBottom( - already = false, - fragment = new DocumentFragment() - ): Promise{ - let func: Function | undefined; - if(!already) func = this.snapBottom(); - if(!this.div)return false; - try{ - let again = false; - const scrollBottom = this.scrollBottom; - if(scrollBottom < (already ? this.fillDist : this.minDist)){ - let nextid: string | undefined; - const lastelm = this.HTMLElements.at(-1); - if(lastelm){ - const previd = lastelm[1]; - nextid = await this.getIDFromOffset(previd, -1); + while (top) { + count++; + if (count > 100) { + const list: string[] = []; + while (top) { + list.push(top); + top = this.forElm.get(top); } - if(nextid){ - again = true; - const html = await this.getHTMLFromID(nextid); - fragment.appendChild(html); - this.HTMLElements.push([html, nextid]); - this.scrollBottom += this.averageheight; + if (!supports) { + const heights = list + .map((_) => this.curElms.get(_)) + .map((_) => this.heightMap.get(_ as HTMLElement)) + .filter((_) => _ !== undefined) + .reduce((a, b) => a + b, 0); + this.div!.scrollTop -= heights; } + list.forEach((_) => this.removeElm(_)); + break; } - if(scrollBottom > this.maxDist){ - const html = this.HTMLElements.pop(); - if(html){ - await this.destroyFromID(html[1]); - this.scrollBottom -= this.averageheight; - again = true; + if (this.forElm.has(top) && this.curElms.has(top)) { + top = this.forElm.get(top); + } else if (count > limit) { + break; + } else { + limit = 75; + const id = await this.getIDFromOffset(top, 1); + this.addLink(top, id); + + if (id) { + proms.push(app(this.getFromID(id))); } + top = id; } - if(again){ - await this.watchForBottom(true, fragment); + } + + await Promise.all(proms); + } + private async fillInBottom() { + const scroll = this.scroller; + if (!scroll) return; + let bottom = this.curFocID; + let count = 0; + let limit = 50; + const app = fragAppend(scroll); + const proms: Promise[] = []; + while (bottom) { + count++; + if (count > 100) { + const list: string[] = []; + while (bottom) { + list.push(bottom); + bottom = this.backElm.get(bottom); + } + list.forEach((_) => this.removeElm(_)); + break; } - return again; - }finally{ - if(!already){ - this.div.append(fragment); - if(func){ - func(); + if (this.backElm.has(bottom) && this.curElms.has(bottom)) { + if (limit === 75) console.error("patchy?"); + bottom = this.backElm.get(bottom); + } else if (count > limit) { + break; + } else { + limit = 75; + const id = await this.getIDFromOffset(bottom, -1); + this.addLink(id, bottom); + + if (id) { + proms.push(app(this.getFromID(id))); } + bottom = id; } } + + await Promise.all(proms); } - async watchForChange(): Promise{ - if(this.changePromise){ - this.watchtime = true; - return await this.changePromise; - }else{ - this.watchtime = false; + private filling?: Promise; + private async fillIn(refill = false) { + if (this.filling && !refill) { + return this.filling; } - this.changePromise = new Promise(async res=>{ - try{ - if(!this.div){ - res(false); - } - const out = (await Promise.allSettled([ - this.watchForTop(), - this.watchForBottom(), - ])) as { value: boolean }[]; - const changed = out[0].value || out[1].value; - if(this.timeout === null && changed){ - this.timeout = setTimeout(this.updatestuff.bind(this), 300); - } - res(Boolean(changed)); - }catch(e){ - console.error(e); - res(false); - }finally{ - setTimeout(()=>{ - this.changePromise = undefined; - if(this.watchtime){ - this.watchForChange(); - } - }, 300); + await this.filling; + if (this.filling) return; + + const fill = new Promise(async (res) => { + await Promise.all([this.fillInTop(), this.fillInBottom()]); + if (this.filling === fill) { + this.filling = undefined; } + res(); }); + this.filling = fill; + await fill; - return await this.changePromise; - } - async focus(id: string, flash = true): Promise{ - let element: HTMLElement | undefined; - for(const thing of this.HTMLElements){ - if(thing[1] === id){ - element = thing[0]; + (async () => { + while (true) { + if (this.div && this.div.parentElement) { + if (this.div.parentElement.clientHeight !== this.div.clientHeight) this.reachesBottom(); + break; + } + await new Promise((res) => setTimeout(res, 100)); } + })(); + + return; + } + + async focus(id: string, flash = true, sec = false): Promise { + // debugger; + const scroller = this.scroller; + if (!scroller) return; + + let div = this.curElms.get(id); + if (div && !document.contains(div)) div = undefined; + let had = true; + this.curFocID = id; + + if (!div) { + await this.clearElms(); + had = false; + const obj = this.getFromID(id); + scroller.append(obj); + div = obj; } - if(element){ - if(flash){ - element.scrollIntoView({ - behavior: "smooth", - block: "center", - }); - await new Promise(resolve=>{ - setTimeout(resolve, 1000); - }); - element.classList.remove("jumped"); - await new Promise(resolve=>{ - setTimeout(resolve, 100); - }); - element.classList.add("jumped"); - }else{ - element.scrollIntoView(); - } - }else{ - this.resetVars(); - //TODO may be a redundant loop, not 100% sure :P - for(const thing of this.HTMLElements){ - await this.destroyFromID(thing[1]); - } - this.HTMLElements = []; - await this.firstElement(id); - this.updatestuff(); - await this.watchForChange(); - await new Promise(resolve=>{ + await this.fillIn(true); + if (had && !sec) { + div.scrollIntoView({ + behavior: "smooth", + inline: "center", + block: "center", + }); + } else { + div.scrollIntoView({ + block: "center", + }); + } + + if (flash) { + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + div.classList.remove("jumped"); + await new Promise((resolve) => { setTimeout(resolve, 100); }); - await this.focus(id, true); + div.classList.add("jumped"); } } - async delete(): Promise{ - if(this.div){ + async delete(): Promise { + if (this.div) { this.div.remove(); - this.div = null; - } - this.resetVars(); - try{ - for(const thing of this.HTMLElements){ - await this.destroyFromID(thing[1]); - } - }catch(e){ - console.error(e); - } - this.HTMLElements = []; - if(this.timeout){ - clearTimeout(this.timeout); } + this.clearElms(); } } -export{ InfiniteScroller }; +export {InfiniteScroller}; diff --git a/src/webpage/infiniteScrollerOld.ts b/src/webpage/infiniteScrollerOld.ts new file mode 100644 index 00000000..7c73cfcc --- /dev/null +++ b/src/webpage/infiniteScrollerOld.ts @@ -0,0 +1,406 @@ +class InfiniteScroller { + readonly getIDFromOffset: (ID: string, offset: number) => Promise; + readonly getHTMLFromID: (ID: string) => Promise; + readonly destroyFromID: (ID: string) => Promise; + readonly reachesBottom: () => void; + private readonly minDist = 2000; + private readonly fillDist = 3000; + private readonly maxDist = 6000; + HTMLElements: [HTMLElement, string][] = []; + div: HTMLDivElement | null = null; + timeout: ReturnType | null = null; + beenloaded = false; + scrollBottom = 0; + scrollTop = 0; + needsupdate = true; + averageheight = 60; + watchtime = false; + changePromise: Promise | undefined; + scollDiv!: {scrollTop: number; scrollHeight: number; clientHeight: number}; + + resetVars() { + this.scrollTop = 0; + this.scrollBottom = 0; + this.averageheight = 60; + this.watchtime = false; + this.needsupdate = true; + this.beenloaded = false; + this.changePromise = undefined; + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = null; + } + for (const thing of this.HTMLElements) { + this.destroyFromID(thing[1]); + } + this.HTMLElements = []; + } + constructor( + getIDFromOffset: InfiniteScroller["getIDFromOffset"], + getHTMLFromID: InfiniteScroller["getHTMLFromID"], + destroyFromID: InfiniteScroller["destroyFromID"], + reachesBottom: InfiniteScroller["reachesBottom"] = () => {}, + ) { + this.getIDFromOffset = getIDFromOffset; + this.getHTMLFromID = getHTMLFromID; + this.destroyFromID = destroyFromID; + this.reachesBottom = reachesBottom; + } + + async getDiv(initialId: string): Promise { + if (this.div) { + return this.div; + } + this.resetVars(); + const scroll = document.createElement("div"); + scroll.classList.add("scroller"); + this.div = scroll; + + this.div.addEventListener("scroll", () => { + this.checkscroll(); + if (this.scrollBottom < 5) { + this.scrollBottom = 5; + } + if (this.timeout === null) { + this.timeout = setTimeout(this.updatestuff.bind(this), 300); + } + this.watchForChange(); + }); + + let oldheight = 0; + new ResizeObserver(() => { + this.checkscroll(); + const func = this.snapBottom(); + this.updatestuff(); + const change = oldheight - scroll.offsetHeight; + if (change > 0 && this.div) { + this.div.scrollTop += change; + } + oldheight = scroll.offsetHeight; + this.watchForChange(); + func(); + }).observe(scroll); + + new ResizeObserver(() => this.watchForChange()).observe(scroll); + + await this.firstElement(initialId); + this.updatestuff(); + await this.watchForChange().then(() => { + this.updatestuff(); + this.beenloaded = true; + }); + + return scroll; + } + + checkscroll(): void { + if (this.beenloaded && this.div && !document.body.contains(this.div)) { + console.warn("not in document"); + this.div = null; + } + } + + async updatestuff(): Promise { + this.timeout = null; + if (!this.div) return; + + this.scrollBottom = this.div.scrollHeight - this.div.scrollTop - this.div.clientHeight; + this.averageheight = this.div.scrollHeight / this.HTMLElements.length; + if (this.averageheight < 10) { + this.averageheight = 60; + } + this.scrollTop = this.div.scrollTop; + + if (this.scrollBottom < 5 && !(await this.watchForChange())) { + this.reachesBottom(); + } + if (!this.scrollTop) { + await this.watchForChange(); + } + this.needsupdate = false; + } + + async firstElement(id: string): Promise { + if (!this.div) return; + const html = await this.getHTMLFromID(id); + this.div.appendChild(html); + this.HTMLElements.push([html, id]); + } + + async addedBottom(): Promise { + await this.updatestuff(); + const func = this.snapBottom(); + if (this.changePromise) { + while (this.changePromise) { + await new Promise((res) => setTimeout(res, 30)); + } + } else { + await this.watchForChange(); + } + func(); + } + + snapBottom(): () => void { + const scrollBottom = this.scrollBottom; + return () => { + if (this.div && scrollBottom < 4) { + this.div.scrollTop = this.div.scrollHeight; + } + }; + } + whenFrag: (() => number)[] = []; + private async watchForTop(already = false, fragment = new DocumentFragment()): Promise { + const supports = CSS.supports("overflow-anchor", "auto"); + if (!this.div) return false; + const div = this.div; + try { + let again = false; + if (this.scrollTop < (already ? this.fillDist : this.minDist)) { + let nextid: string | undefined; + const firstelm = this.HTMLElements.at(0); + if (firstelm) { + const previd = firstelm[1]; + nextid = await this.getIDFromOffset(previd, 1); + } + + if (nextid) { + const html = await this.getHTMLFromID(nextid); + + if (!html) { + this.destroyFromID(nextid); + return false; + } + if (!supports) { + this.whenFrag.push(() => { + const box = html.getBoundingClientRect(); + return box.height; + }); + } + again = true; + fragment.prepend(html); + this.HTMLElements.unshift([html, nextid]); + this.scrollTop += this.averageheight; + } + } + if (this.scrollTop > this.maxDist && this.remove) { + const html = this.HTMLElements.shift(); + if (html) { + let dec = 0; + if (!supports) { + const box = html[0].getBoundingClientRect(); + dec = box.height; + } + again = true; + await this.destroyFromID(html[1]); + + div.scrollTop -= dec; + this.scrollTop -= this.averageheight; + } + } + if (again) { + await this.watchForTop(true, fragment); + } + return again; + } finally { + if (!already) { + if (this.div.scrollTop === 0) { + this.scrollTop = 1; + this.div.scrollTop = 10; + } + let height = 0; + + this.div.prepend(fragment); + this.whenFrag.forEach((_) => (height += _())); + this.div.scrollTop += height; + + this.whenFrag = []; + } + } + } + deleteId(id: string) { + this.HTMLElements = this.HTMLElements.filter(([elm, elmid]) => { + if (id === elmid) { + elm.remove(); + return false; + } else { + return true; + } + }); + } + + async watchForBottom(already = false, fragment = new DocumentFragment()): Promise { + let func: Function | undefined; + if (!already) func = this.snapBottom(); + if (!this.div) return false; + try { + let again = false; + const scrollBottom = this.scrollBottom; + if (scrollBottom < (already ? this.fillDist : this.minDist)) { + let nextid: string | undefined; + const lastelm = this.HTMLElements.at(-1); + if (lastelm) { + const previd = lastelm[1]; + nextid = await this.getIDFromOffset(previd, -1); + } + if (nextid) { + again = true; + const html = await this.getHTMLFromID(nextid); + fragment.appendChild(html); + this.HTMLElements.push([html, nextid]); + this.scrollBottom += this.averageheight; + } + } + if (scrollBottom > this.maxDist && this.remove) { + const html = this.HTMLElements.pop(); + if (html) { + await this.destroyFromID(html[1]); + this.scrollBottom -= this.averageheight; + again = true; + } + } + if (again) { + await this.watchForBottom(true, fragment); + } + return again; + } finally { + if (!already) { + this.div.append(fragment); + if (func) { + func(); + } + } + } + } + + async watchForChange(stop = false): Promise { + if (!this.remove) return false; + if (stop == true) { + let prom = this.changePromise; + while (this.changePromise) { + prom = this.changePromise; + await this.changePromise; + if (prom === this.changePromise) { + this.changePromise = undefined; + break; + } + } + } + if (this.changePromise) { + this.watchtime = true; + return await this.changePromise; + } else { + this.watchtime = false; + } + + this.changePromise = new Promise(async (res) => { + try { + if (!this.div) { + res(false); + } + const out = (await Promise.allSettled([this.watchForTop(), this.watchForBottom()])) as { + value: boolean; + }[]; + const changed = out[0].value || out[1].value; + if (this.timeout === null && changed) { + this.timeout = setTimeout(this.updatestuff.bind(this), 300); + } + res(Boolean(changed)); + } catch (e) { + console.error(e); + res(false); + } finally { + if (stop === true) { + this.changePromise = undefined; + return; + } + setTimeout(() => { + this.changePromise = undefined; + if (this.watchtime) { + this.watchForChange(); + } + }, 300); + } + }); + + return await this.changePromise; + } + remove = true; + async focus(id: string, flash = true, sec = false): Promise { + let element: HTMLElement | undefined; + for (const thing of this.HTMLElements) { + if (thing[1] === id) { + element = thing[0]; + } + } + if (sec && element && document.contains(element)) { + if (flash) { + element.scrollIntoView({ + behavior: "smooth", + inline: "center", + block: "center", + }); + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + element.classList.remove("jumped"); + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + element.classList.add("jumped"); + } else { + element.scrollIntoView({ + block: "center", + }); + } + } else if (!sec) { + this.resetVars(); + //TODO may be a redundant loop, not 100% sure :P + for (const thing of this.HTMLElements) { + await this.destroyFromID(thing[1]); + } + this.HTMLElements = []; + await this.firstElement(id); + this.changePromise = new Promise(async (resolve) => { + try { + await this.updatestuff(); + this.remove = false; + await Promise.all([this.watchForBottom(), this.watchForTop()]); + + await new Promise((res) => queueMicrotask(res)); + await this.focus(id, !element && flash, true); + this.remove = true; + //TODO figure out why this fixes it and fix it for real :P + await new Promise(requestAnimationFrame); + await new Promise(requestAnimationFrame); + this.changePromise = undefined; + } finally { + resolve(true); + } + }); + await this.changePromise; + } else { + console.warn("elm not exist"); + } + } + + async delete(): Promise { + if (this.div) { + this.div.remove(); + this.div = null; + } + this.resetVars(); + try { + for (const thing of this.HTMLElements) { + await this.destroyFromID(thing[1]); + } + } catch (e) { + console.error(e); + } + this.HTMLElements = []; + if (this.timeout) { + clearTimeout(this.timeout); + } + } +} + +export {InfiniteScroller}; diff --git a/src/webpage/instances.json b/src/webpage/instances.json index 4949fbc9..a5d49c56 100644 --- a/src/webpage/instances.json +++ b/src/webpage/instances.json @@ -1,34 +1,28 @@ [ - { - "name": "Spacebar", - "description": "The official Spacebar instance.", - "image": "https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/png/Spacebar__Icon-Discord.png", - "urls":{ - "wellknown": "https://spacebar.chat/", - "api": "https://old.server.spacebar.chat/api", - "cdn": "https://cdn.old.server.spacebar.chat", - "gateway": "wss://gateway.old.server.spacebar.chat" - }, - "url": "https://spacebar.chat" - }, - { - "name": "Fastbar", - "description": "The best Spacebar instance with 95% uptime, running under on a NVME drive running with bleeding edge stuff <3", - "image": "https://spacebar.greysilly7.xyz/logo.png", - "url": "https://greysilly7.xyz", - "language": "en", - "country": "US", - "display": true, - "urls": { - "wellknown": "https://greysilly7.xyz", - "api": "https://api-spacebar.greysilly7.xyz/api", - "cdn": "https://cdn-spacebar.greysilly7.xyz", - "gateway": "wss://gateway-spacebar.greysilly7.xyz" - }, - "contactInfo": { - "dicord": "greysilly7", - "github": "https://github.com/greysilly7", - "email": "greysilly7@gmail.com" - } - } + { + "name": "Spacebar", + "description": "The official Spacebar instance.", + "image": "https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/png/Spacebar__Icon-Discord.png", + "url": "https://spacebar.chat" + }, + { + "name": "Fastbar", + "description": "The best Spacebar instance with 95% uptime, running under on a NVME drive running with bleeding edge stuff <3", + "image": "https://spacebar.greysilly7.xyz/logo.png", + "url": "https://greysilly7.xyz", + "language": "en", + "country": "US", + "display": true, + "urls": { + "wellknown": "https://greysilly7.xyz", + "api": "https://api-spacebar.greysilly7.xyz/api", + "cdn": "https://cdn-spacebar.greysilly7.xyz", + "gateway": "wss://gateway-spacebar.greysilly7.xyz" + }, + "contactInfo": { + "discord": "greysilly7", + "github": "https://github.com/greysilly7", + "email": "greysilly7@gmail.com" + } + } ] diff --git a/src/webpage/interactions/commands.ts b/src/webpage/interactions/commands.ts new file mode 100644 index 00000000..6c3a15f0 --- /dev/null +++ b/src/webpage/interactions/commands.ts @@ -0,0 +1,503 @@ +import {Channel} from "../channel.js"; +import {Guild} from "../guild.js"; +import {I18n} from "../i18n.js"; +import {commandJson, commandOptionJson} from "../jsontypes.js"; +import {Localuser} from "../localuser.js"; +import {SnowFlake} from "../snowflake.js"; +import {removeAni} from "../utils/utils.js"; +function focusInput(html: HTMLElement) { + const input = html.getElementsByTagName("input")[0]; + if (input) input.focus(); +} +function focusElm(node: HTMLElement | Text, before = true) { + const selection = window.getSelection(); + if (!selection) return; + var range = document.createRange(); + if (before) { + range.setStartBefore(node); + } else { + range.setStartAfter(node); + } + range.collapse(true); + selection.removeAllRanges(); + selection.addRange(range); +} +export class Command extends SnowFlake { + owner: Localuser | Guild; + type: 1 | 2 | 3 | 4; + applicationId: string; + name: string; + nameLocalizations: Record; + descriptionLocalizations: Record; + description: string; + defaultMemberPerms: BigInt; + permissions: { + user: boolean; + roles: Record; + channels: Record; + }; + nsfw: boolean; + gpr: number; + version: string; + handler: 1 | 2 | 3; + options: Option[]; + readonly rawJson: Readonly; + get localuser() { + if (this.owner instanceof Localuser) { + return this.owner; + } else { + return this.owner.owner; + } + } + constructor(command: commandJson, owner: Localuser | Guild) { + super(command.id); + this.rawJson = Object.freeze(structuredClone(command)); + this.owner = owner; + this.type = command.type; + this.applicationId = command.application_id; + this.name = command.name; + this.nameLocalizations = command.name_localizations || {}; + this.description = command.description; + this.descriptionLocalizations = command.description_localizations || {}; + this.defaultMemberPerms = BigInt(command.default_member_permissions || "0"); + this.permissions = { + user: command.permissions?.user || true, + roles: command.permissions?.roles || {}, + channels: command.permissions?.channels || {}, + }; + command.options ||= []; + this.options = command.options.map((_) => Option.toOption(_, this)); + this.nsfw = command.nsfw; + this.gpr = command.global_popularity_rank || 0; + this.version = command.version; + this.handler = command.handler || 1; + } + get localizedName() { + return this.nameLocalizations[I18n.lang] || this.name; + } + get localizedDescription() { + return this.descriptionLocalizations[I18n.lang] || this.description; + } + similar(search: string) { + if (search.length === 0) { + return 0.1; + } + const similar = (str: string) => { + if (str.includes(search)) { + return search.length / str.length; + } else if (str.toLowerCase().includes(search.toLowerCase())) { + return str.length / str.length / 1.4; + } else { + return 0; + } + }; + return Math.max( + similar(this.name), + similar(this.description), + similar(this.localizedDescription), + similar(this.localizedName), + ); + } + state = new WeakMap< + Channel, + ( + | { + option: Option; + state: string; + } + | string + )[] + >(); + collect(html: HTMLElement, channel: Channel, node?: Node): boolean { + const states = this.state.get(channel); + const build: ( + | { + option: Option; + state: string; + } + | string + )[] = []; + if (!states) return false; + + let gotname = false; + for (const elm of Array.from(html.childNodes)) { + if (elm instanceof HTMLElement) { + if (elm.classList.contains("commandFront")) { + gotname = true; + continue; + } + const name = elm.getAttribute("commandName"); + const state = states.find((_) => _ instanceof Object && _.option.match(name || "")); + if (state) { + build.push(state); + } else { + const option = this.options.find((_) => _.match(name || "")); + if (option) { + build.push({option, state: ""}); + } + } + } else if (elm instanceof Text) { + build.push(elm.textContent || ""); + } + } + + if (node instanceof Text) { + this.searchAtr(node, channel, html); + } + + if (gotname) { + this.state.set(channel, build); + } else { + this.state.delete(channel); + } + + return gotname; + } + searchAtr(textNode: Text, channel: Channel, Divhtml: HTMLElement) { + const text = (textNode.textContent || "").trim(); + const states = this.state.get(channel); + if (!states) { + this.localuser.MDSearchOptions( + [], + "", + document.getElementById("searchOptions") as HTMLDivElement, + ); + return; + } + const opts = this.options + .filter((obj) => !states.find((_) => _ instanceof Object && _.option === obj)) + .map((opt) => [opt, opt.similar(text)] as const) + .filter((_) => _[1]) + .sort((a, b) => a[1] - b[1]) + .slice(0, 6) + .map((_) => _[0]); + this.localuser.MDSearchOptions( + opts.map((opt) => { + return [ + opt.localizedName, + "", + void 0, + () => { + const html = opt.toHTML("", channel); + textNode.after(html); + textNode.remove(); + this.collect(Divhtml, channel); + console.log(this.state.get(channel)); + focusInput(html); + return true; + }, + ]; + }), + "", + document.getElementById("searchOptions") as HTMLDivElement, + ); + } + render(html: HTMLElement, channel: Channel) { + console.warn(this.rawJson); + html.innerHTML = ""; + let state = this.state.get(channel); + if (!state) { + const req = this.options.filter((_) => _.required); + state = req.map((option) => ({option, state: ""})); + this.state.set(channel, state); + } + const command = document.createElement("span"); + command.classList.add("commandFront"); + command.textContent = `/${this.localizedName}`; + command.contentEditable = "false"; + html.append(command); + let lastElm: HTMLElement | undefined = undefined; + for (const thing of state) { + if (typeof thing === "string") { + html.append(thing); + continue; + } + const {option, state} = thing; + const opt = option.toHTML(state, channel); + lastElm = opt; + html.append(opt); + } + if (lastElm) { + focusInput(lastElm); + } else { + const node = new Text(); + node.textContent = ""; + html.append(node); + focusElm(node, false); + } + } + stateChange(option: Option, channel: Channel, state: string) { + const states = this.state.get(channel); + if (!states) return; + const stateObj = states.find((_) => _ instanceof Object && _.option === option); + if (stateObj && stateObj instanceof Object) { + stateObj.state = state; + } + } + getState(option: Option, channel: Channel) { + const states = this.state.get(channel); + if (!states) return; + const stateObj = states.find((_) => _ instanceof Object && _.option === option); + if (stateObj && stateObj instanceof Object) { + return stateObj.state; + } + return; + } + get info() { + return this.owner.info; + } + + get headers() { + return this.owner.headers; + } + + async submit(html: HTMLElement, channel: Channel) { + try { + const nonce = Math.floor(Math.random() * 10 ** 9) + ""; + const states = this.state.get(channel); + if (!states) { + return true; + } + const opts = states.filter((_) => typeof _ !== "string"); + const options = opts.map(({option, state}) => { + return option.toJson(state); + }); + const madeit = new Set(opts.map((_) => _.option)); + for (const thing of this.options) { + if (thing.required && !madeit.has(thing)) { + throw new OptionError(I18n.commands.required(thing.localizedName)); + } + } + + await fetch(this.info.api + "/interactions", { + method: "POST", + headers: this.headers, + body: JSON.stringify({ + type: 2, + nonce: nonce, + guild_id: channel.owner.id, + channel_id: channel.id, + application_id: this.applicationId, + session_id: this.localuser.session_id, + data: { + application_command: this.rawJson, + attachments: [], + id: this.id, + name: this.name, + options, + type: 1, + version: this.version, + }, + }), + }); + this.state.delete(channel); + } catch (e) { + if (e instanceof OptionError) { + const message = e.message; + const error = document.createElement("span"); + error.classList.add("commandError"); + error.textContent = message; + html.parentElement?.append(error); + removeAni(error, 25000); + } + return false; + } + return true; + } +} +abstract class Option { + type: number; + required: boolean; + private name: string; + private description: string; + private nameLocalizations: Record; + private descriptionLocalizations: Record; + constructor(optionjson: commandOptionJson) { + this.required = optionjson.required || false; + this.name = optionjson.name; + this.nameLocalizations = optionjson.name_localizations || {}; + this.description = optionjson.description; + this.descriptionLocalizations = optionjson.description_localizations || {}; + this.type = optionjson.type; + } + match(str: string) { + return str === this.name; + } + get localizedName() { + return this.nameLocalizations[I18n.lang] || this.name; + } + get localizedDescription() { + return this.descriptionLocalizations[I18n.lang] || this.description; + } + static toOption(optionjson: commandOptionJson, owner: Command): Option { + switch (optionjson.type) { + case 3: + return new StringOption(optionjson, owner); + default: + return new ErrorOption(optionjson); + } + } + abstract toHTML(state: string, channel: Channel): HTMLElement; + imprintName(html: HTMLElement) { + html.setAttribute("commandName", this.name); + } + similar(search: string) { + if (search.length === 0) { + return 0.1; + } + const similar = (str: string) => { + if (str.includes(search)) { + return search.length / str.length; + } else if (str.toLowerCase().includes(search.toLowerCase())) { + return str.length / str.length / 1.4; + } else { + return 0; + } + }; + return Math.max( + similar(this.name), + similar(this.description), + similar(this.localizedDescription), + similar(this.localizedName), + ); + } + toJson(state: string) { + return { + value: this.getValue(state), + type: this.type, + name: this.name, + }; + } + getValue(state: string): string | number { + return state; + } +} +class ErrorOption extends Option { + constructor(optionjson: commandOptionJson) { + super(optionjson); + this.required = false; + } + toHTML(): HTMLElement { + const span = document.createElement("span"); + this.imprintName(span); + span.textContent = "Fermi doesn't impl this yet"; + return span; + } +} +class OptionError extends Error { + constructor(reason: string) { + super(reason); + } +} +class StringOption extends Option { + minLeng: number; + maxLeng: number; + choices: commandOptionJson["choices"]; + autocomplete: boolean; + owner: Command; + constructor(optionjson: commandOptionJson, owner: Command) { + super(optionjson); + this.owner = owner; + this.minLeng = optionjson.min_length || 0; + this.maxLeng = optionjson.min_length || 6000; + this.choices = optionjson.choices; + this.autocomplete = optionjson.autocomplete || false; + } + + toHTML(state: string, channel: Channel): HTMLElement { + const div = document.createElement("div"); + div.contentEditable = "false"; + div.classList.add("flexltr", "commandinput"); + this.imprintName(div); + + const label = document.createElement("span"); + label.textContent = this.localizedName + ":"; + + const input = document.createElement("input"); + input.type = "text"; + input.value = state; + input.onkeydown = (e) => { + if (input.selectionStart === 0 && e.key === "Backspace") { + const before = !!div.nextSibling; + const sib = div.nextSibling || div.previousSibling; + div.remove(); + focusElm(sib as HTMLElement, before); + e.preventDefault(); + e.stopImmediatePropagation(); + } + }; + input.onkeyup = (e) => { + if (input.selectionStart === input.value.length && e.key === "ArrowRight") { + focusElm(div, false); + } + const last = this.owner.getState(this, channel); + this.owner.stateChange(this, channel, input.value); + if (this.choices?.length && last !== input.value) { + this.displayChoices(input, channel); + } + }; + + div.append(label, input); + return div; + } + displayChoices(input: HTMLInputElement, channel: Channel) { + const value = input.value; + if (!this.choices) return; + const similar = (str?: string | null) => { + if (str === null || str === undefined) return 0; + if (str.includes(value)) { + return value.length / str.length; + } else if (str.toLowerCase().includes(value.toLowerCase())) { + return str.length / str.length / 1.4; + } else { + return 0; + } + }; + + const options = ( + value + ? this.choices + .map( + (_) => + [_, Math.max(similar(_.name), similar(_.name_localizations?.[I18n.lang]))] as const, + ) + .filter((_) => _[1] !== 0) + .sort((a, b) => a[1] - b[1]) + .map((_) => _[0]) + : this.choices + ).slice(0, 10); + + this.owner.localuser.MDSearchOptions( + options.map((elm) => { + return [ + `${elm.name_localizations?.[I18n.lang] || elm.name}`, + "", + undefined, + () => { + input.value = elm.name_localizations?.[I18n.lang] || elm.name; + this.owner.stateChange(this, channel, input.value); + return true; + }, + ] as const; + }), + "", + ); + } + getValue(state: string) { + if (this.choices?.length) { + const choice = this.choices.find((choice) => { + if (choice.name === state) { + return true; + } else if (choice.name_localizations?.[I18n.lang] === state) { + return true; + } + return false; + }); + if (choice) { + return choice.value; + } + throw new OptionError(I18n.commands.errorNotValid(state || '""', this.localizedName)); + } + return state; + } +} diff --git a/src/webpage/interactions/compontents.ts b/src/webpage/interactions/compontents.ts new file mode 100644 index 00000000..d1f1e30d --- /dev/null +++ b/src/webpage/interactions/compontents.ts @@ -0,0 +1,407 @@ +import {Channel} from "../channel.js"; +import {I18n} from "../i18n.js"; +import { + actionRow, + button, + component, + container, + mediaGallery, + MessageComponentType, + section, + select, + seperator, + textDisp, + thumbnail, +} from "../jsontypes.js"; +import {MarkDown} from "../markdown"; +import {Message} from "../message.js"; +import {FancySelect} from "../utils/fancySelect.js"; +import {File} from "../file.js"; + +abstract class compObj { + abstract owner: Components; + abstract getHTML(): HTMLElement; + get info() { + return this.owner.owner.info; + } + get message() { + return this.owner.owner instanceof Message ? this.owner.owner : undefined; + } + get channel() { + return this.owner.owner instanceof Message ? this.owner.owner.channel : this.owner.owner; + } + get guild() { + return this.owner.owner.guild; + } + get headers() { + return this.owner.owner.headers; + } + get bot() { + return this.message!.author; + } + get localuser() { + return this.owner.owner.localuser; + } +} +export class Components { + components: compObj[]; + owner: Message | Channel; + constructor(components: component[], message: Message | Channel) { + this.owner = message; + this.components = components.map((comp) => this.toComp(comp)); + } + toComp(comp: component): compObj { + switch (comp.type) { + case MessageComponentType.ActionRow: + return new ActionRow(comp, this); + case MessageComponentType.Button: + return new Button(comp, this); + case MessageComponentType.StringSelect: + return new Select(comp, this); + case MessageComponentType.Container: + return new Container(comp, this); + case MessageComponentType.TextDisplay: + return new TextDisplay(comp, this); + case MessageComponentType.Separator: + return new Seperator(comp, this); + case MessageComponentType.MediaGallery: + return new MediaGallery(comp, this); + case MessageComponentType.Section: + return new Section(comp, this); + case MessageComponentType.Thumbnail: + return new Thumbnail(comp, this); + default: + return new ErrorElm(comp, this); + } + } + getHTML() { + const div = document.createElement("div"); + div.classList.add("flexttb"); + div.append(...this.components.map((_) => _.getHTML())); + return div; + } +} +class MediaGallery extends compObj { + items: { + media: File; + description?: string; + spoiler?: boolean; + }[]; + owner: Components; + accentColor?: number; + constructor(comp: mediaGallery, owner: Components) { + super(); + this.owner = owner; + + this.items = comp.items.map((elm) => { + return { + media: new File( + { + ...elm.media, + filename: "", + size: NaN, + }, + this.message ?? null, + ), + description: elm.description, + spoiler: elm.spoiler, + }; + }); + const files = this.items.map(({media}) => media); + files.forEach((file) => (file.files = files)); + } + getHTML() { + //TODO handle spoiler + const div = document.createElement("div"); + div.classList.add("flexttb", "mediaDisp"); + const items = this.items.map((elm) => { + return elm.media.getHTML(); + }); + const row = document.createElement("div"); + row.classList.add("flexltr"); + row.append(...items.slice(0, 2)); + div.append(row); + if (items.length > 2) { + const row = document.createElement("div"); + row.classList.add("flexltr"); + row.append(...items.slice(2, 5)); + div.append(row); + } + if (items.length > 5) { + const row = document.createElement("div"); + row.classList.add("flexltr"); + row.append(...items.slice(5, 10)); + div.append(row); + } + + div.append(); + return div; + } +} +class Seperator extends compObj { + owner: Components; + divider: boolean; + spacing: 1 | 2; + constructor(comp: seperator, owner: Components) { + super(); + this.owner = owner; + this.divider = comp.divider ?? true; + this.spacing = comp.spacing ?? 1; + } + getHTML() { + if (this.divider) { + const hr = document.createElement("hr"); + if (this.spacing === 1) { + hr.style.margin = "4px"; + } else { + hr.style.margin = "8px"; + } + return hr; + } else { + const hr = document.createElement("span"); + + if (this.spacing === 1) { + hr.style.height = "16px"; + } else { + hr.style.height = "32px"; + } + + return hr; + } + } +} +class Thumbnail extends compObj { + owner: Components; + file: File; + constructor(comp: thumbnail, owner: Components) { + super(); + this.owner = owner; + //TODO deal with more + this.file = new File( + { + ...comp.media, + filename: "", + size: NaN, + content_type: "image/", + }, + this.message ?? null, + ); + this.file.files = [this.file]; + } + getHTML() { + return this.file.getHTML(); + } +} +class TextDisplay extends compObj { + owner: Components; + content: string; + constructor(comp: textDisp, owner: Components) { + super(); + this.owner = owner; + this.content = comp.content; + } + getHTML() { + return new MarkDown(this.content, this.channel).makeHTML(); + } +} +class Section extends compObj { + components: compObj[]; + owner: Components; + button: compObj; + constructor(comp: section, owner: Components) { + super(); + this.owner = owner; + this.button = owner.toComp(comp.accessory); + this.components = comp.components.map((comp) => owner.toComp(comp)); + } + getHTML() { + //TODO handle spoiler + const div = document.createElement("div"); + div.classList.add("flexltr"); + const div2 = document.createElement("div"); + div2.classList.add("flexttb"); + div2.append(...this.components.map((_) => _.getHTML())); + const b = this.button.getHTML(); + b.style.marginLeft = "auto"; + + div.append(div2, b); + return div; + } +} +class Container extends compObj { + components: compObj[]; + owner: Components; + accentColor?: number; + constructor(comp: container, owner: Components) { + super(); + this.owner = owner; + this.accentColor = comp.accent_color; + this.components = comp.components.map((comp) => owner.toComp(comp)); + } + getHTML() { + //TODO handle spoiler + const div = document.createElement("div"); + div.classList.add("flexttb", "displayComp"); + if (this.accentColor !== undefined) + div.style.setProperty("--accent-color", "#" + this.accentColor.toString(16).padStart(6, "0")); + div.append(...this.components.map((_) => _.getHTML())); + return div; + } +} +class ActionRow extends compObj { + components: compObj[]; + owner: Components; + constructor(comp: actionRow, owner: Components) { + super(); + this.owner = owner; + this.components = comp.components.map((comp) => owner.toComp(comp)); + } + getHTML() { + const div = document.createElement("div"); + div.classList.add("flexltr"); + div.append(...this.components.map((_) => _.getHTML())); + return div; + } +} +class Button extends compObj { + custom_id: string; + label?: string; + url?: string; + style: 1 | 2 | 3 | 4 | 5 | 6; + owner: Components; + disabled: boolean; + constructor(comp: button, owner: Components) { + super(); + console.warn(comp); + this.custom_id = comp.custom_id; + this.label = comp.label; + this.style = comp.style; + this.owner = owner; + this.disabled = comp.disabled || false; + this.url = comp.url; + } + get styleVar() { + return [ + "buttonPrimary", + "buttonSecondary", + "buttonSuccess", + "buttonDanger", + "buttonLink", + "buttonPremium", + ][this.style - 1]; + } + async clickEvent() { + const nonce = Math.floor(Math.random() * 10 ** 9) + ""; + if (this.message) this.localuser.registerInterNonce(nonce, this.message); + await fetch(this.info.api + "/interactions", { + method: "POST", + headers: this.headers, + body: JSON.stringify({ + type: this.message ? 3 : 5, + nonce: nonce, + guild_id: this.guild.id, + channel_id: this.channel.id, + message_flags: this.message?.flags, + message_id: this.message?.id, + application_id: this.bot.id, + session_id: this.localuser.session_id, + data: { + component_type: 2, + custom_id: this.custom_id, + }, + }), + }); + } + getHTML() { + const button = document.createElement("button"); + if (this.label) { + button.textContent = this.label; + } + if (!this.disabled) { + if (this.url) { + MarkDown.safeLink(button, this.url, this.localuser); + } else { + button.onclick = () => { + this.clickEvent(); + }; + } + } + button.disabled = this.disabled; + button.classList.add(this.styleVar, "interButton"); + return button; + } +} +class ErrorElm extends compObj { + comp: component; + owner: Components; + constructor(comp: component, owner: Components) { + super(); + this.comp = comp; + this.owner = owner; + } + getHTML(): HTMLElement { + const span = document.createElement("span"); + if (this.comp.type < 1 && this.comp.type > 20) { + span.textContent = I18n.interactions.notImpl(this.comp.type + ""); + } else { + span.textContent = I18n.interactions.nonsence(this.comp.type + ""); + } + return span; + } +} +class Select extends compObj { + custom_id: string; + owner: Components; + options: select["options"]; + maxValues: number; + minValues: number; + constructor(comp: select, owner: Components) { + super(); + this.owner = owner; + this.custom_id = comp.custom_id; + this.options = comp.options; + this.maxValues = comp.max_values || 1; + this.minValues = comp.min_values || 1; + } + async submit(values: string[]) { + const nonce = Math.floor(Math.random() * 10 ** 9) + ""; + if (this.message) this.localuser.registerInterNonce(nonce, this.message); + await fetch(this.info.api + "/interactions", { + method: "POST", + headers: this.headers, + body: JSON.stringify({ + type: this.message ? 3 : 5, + nonce: nonce, + guild_id: this.guild.id, + channel_id: this.channel.id, + message_flags: this.message?.flags, + message_id: this.message?.id, + application_id: this.bot.id, + session_id: this.localuser.session_id, + data: { + component_type: 3, + custom_id: this.custom_id, + values, + }, + }), + }); + } + getHTML() { + const fancy = new FancySelect( + this.options.map((_) => { + return { + label: _.label, + value: _.value, + description: _.description, + default: _.default || false, + }; + }), + {max: this.maxValues, min: this.minValues}, + ); + fancy.onSubmit = (list) => { + this.submit(list); + }; + + return fancy.getHTML(); + } +} diff --git a/src/webpage/invite.html b/src/webpage/invite.html index 5a2c3d77..165d7c93 100644 --- a/src/webpage/invite.html +++ b/src/webpage/invite.html @@ -1,27 +1,37 @@ - + - - - Jank Client - - - - - - - - + + + Fermi + + + + + + + +
+

Server Name

Someone invited you to Server Name

- + - \ No newline at end of file + diff --git a/src/webpage/invite.ts b/src/webpage/invite.ts index 15ebecaa..6bab2476 100644 --- a/src/webpage/invite.ts +++ b/src/webpage/invite.ts @@ -1,147 +1,139 @@ -import { I18n } from "./i18n.js"; -import{ getBulkUsers, Specialuser, getapiurls }from"./login.js"; - -(async ()=>{ - const users = getBulkUsers(); - const well = new URLSearchParams(window.location.search).get("instance"); - const joinable: Specialuser[] = []; - - for(const key in users.users){ - if(Object.prototype.hasOwnProperty.call(users.users, key)){ - const user: Specialuser = users.users[key]; - if(well && user.serverurls.wellknown.includes(well)){ - joinable.push(user); +import {I18n} from "./i18n.js"; +import {AccountSwitcher} from "./utils/switcher.js"; +import {createImg, getapiurls} from "./utils/utils.js"; +import {getBulkUsers, Specialuser} from "./utils/utils.js"; +if (window.location.pathname.startsWith("/invite")) + (async () => { + const users = getBulkUsers(); + const well = new URLSearchParams(window.location.search).get("instance") || "Spacebar"; + const joinable: Specialuser[] = []; + + for (const key in users.users) { + if (Object.prototype.hasOwnProperty.call(users.users, key)) { + const user: Specialuser = users.users[key]; + if (well && user.serverurls.wellknown.includes(well)) { + joinable.push(user); + } + console.log(user); } - console.log(user); } - } - - let urls: { api: string; cdn: string } | undefined; - if(!joinable.length && well){ - const out = await getapiurls(well); - if(out){ - urls = out; - for(const key in users.users){ - if(Object.prototype.hasOwnProperty.call(users.users, key)){ - const user: Specialuser = users.users[key]; - if(user.serverurls.api.includes(out.api)){ - joinable.push(user); + let urls: {api: string; cdn: string} | undefined; + + if (!joinable.length && well) { + const out = await getapiurls(well); + if (out) { + urls = out; + for (const key in users.users) { + if (Object.prototype.hasOwnProperty.call(users.users, key)) { + const user: Specialuser = users.users[key]; + if (user.serverurls.api.includes(out.api)) { + joinable.push(user); + } + console.log(user); } - console.log(user); } + } else { + throw new Error("Someone needs to handle the case where the servers don't exist"); } - }else{ - throw new Error( - "Someone needs to handle the case where the servers don't exist" - ); + } else { + urls = joinable[0].serverurls; + } + await I18n.done; + if (!joinable.length) { + document.getElementById("AcceptInvite")!.textContent = I18n.htmlPages.noAccount(); } - }else{ - urls = joinable[0].serverurls; - } - await I18n.done; - if(!joinable.length){ -document.getElementById("AcceptInvite")!.textContent = I18n.getTranslation("noAccount"); - } - - const code = window.location.pathname.split("/")[2]; - let guildinfo: any; - - fetch(`${urls!.api}/invites/${code}`, { - method: "GET", - }) - .then(response=>response.json()) - .then(json=>{ - const guildjson = json.guild; - guildinfo = guildjson; -document.getElementById("invitename")!.textContent = guildjson.name; -document.getElementById( - "invitedescription" -)!.textContent = I18n.getTranslation("invite.longInvitedBy",json.inviter.username,guildjson.name) -if(guildjson.icon){ - const img = document.createElement("img"); - img.src = `${urls!.cdn}/icons/${guildjson.id}/${guildjson.icon}.png`; - img.classList.add("inviteGuild"); -document.getElementById("inviteimg")!.append(img); -}else{ - const txt = guildjson.name - .replace(/'s /g, " ") - .replace(/\w+/g, (word: any[])=>word[0]) - .replace(/\s/g, ""); - const div = document.createElement("div"); - div.textContent = txt; - div.classList.add("inviteGuild"); -document.getElementById("inviteimg")!.append(div); -} - }); - - function showAccounts(): void{ - const table = document.createElement("dialog"); - for(const user of joinable){ - console.log(user.pfpsrc); - - const userinfo = document.createElement("div"); - userinfo.classList.add("flexltr", "switchtable"); - - const pfp = document.createElement("img"); - pfp.src = user.pfpsrc; - pfp.classList.add("pfp"); - userinfo.append(pfp); - - const userDiv = document.createElement("div"); - userDiv.classList.add("userinfo"); - userDiv.textContent = user.username; - userDiv.append(document.createElement("br")); - - const span = document.createElement("span"); - span.textContent = user.serverurls.wellknown - .replace("https://", "") - .replace("http://", ""); - span.classList.add("serverURL"); - userDiv.append(span); - userinfo.append(userDiv); - table.append(userinfo); + const code = window.location.pathname.split("/")[2]; + let guildinfo: any; + + fetch(`${urls!.api}/invites/${code}`, { + method: "GET", + }) + .then((response) => response.json()) + .then((json) => { + if (json.code === 404) { + document.getElementById("AcceptInvite")?.remove(); + document.getElementById("invitename")!.textContent = I18n.invite.notFound(); + document.getElementById("invitedescription")!.textContent = ""; + return; + } + const guildjson = json.guild; + guildinfo = guildjson; + document.getElementById("invitename")!.textContent = guildjson.name; + document.getElementById("invitedescription")!.textContent = I18n.invite.longInvitedBy( + json.inviter.username, + guildjson.name, + ); + if (guildjson.discovery_splash) { + const img = createImg( + `${urls!.cdn}/discovery-splashes/${guildjson.id}/${guildjson.discovery_splash}.png`, + ); + img.classList.add("inviteBG"); + document.body.prepend(img); + document.getElementById("invitebody")?.classList.add("moreShadow"); + } + if (guildjson.banner) { + const img = createImg(`${urls!.cdn}/banners/${guildjson.id}/${guildjson.banner}.png`); + document.getElementById("inviteBanner")!.append(img); + } + if (guildjson.icon) { + const img = document.createElement("img"); + img.src = `${urls!.cdn}/icons/${guildjson.id}/${guildjson.icon}.png`; + img.classList.add("inviteGuild"); + document.getElementById("inviteimg")!.append(img); + } else { + const txt = guildjson.name + .replace(/'s /g, " ") + .replace(/\w+/g, (word: any[]) => word[0]) + .replace(/\s/g, ""); + const div = document.createElement("div"); + div.textContent = txt; + div.classList.add("inviteGuild"); + document.getElementById("inviteimg")!.append(div); + } + }); - userinfo.addEventListener("click", ()=>{ - console.log(user); - fetch(`${urls!.api}/invites/${code}`, { - method: "POST", - headers: { - Authorization: user.token, - }, - }).then(()=>{ - users.currentuser = user.uid; - localStorage.setItem("userinfos", JSON.stringify(users)); - window.location.href = "/channels/" + guildinfo.id; - }); + async function showAccounts() { + console.log("showing!"); + const user = await new AccountSwitcher( + (user) => { + return !!(well && user.serverurls.wellknown.includes(well)); + }, + { + loginText: () => I18n.login.login(), + createOpt: true, + loginurl: + "/login?" + + new URLSearchParams([ + ["goback", window.location.href], + ["instance", well], + ["invite", code], + ]), + registerurl: + "/register?" + + new URLSearchParams([ + ["goback", window.location.href], + ["instance", well], + ["invite", code], + ]), + }, + ).show(); + fetch(`${urls!.api}/invites/${code}`, { + method: "POST", + headers: { + Authorization: user.token, + }, + }).then(() => { + users.currentuser = user.uid; + sessionStorage.setItem("currentuser", user.uid); + localStorage.setItem("userinfos", JSON.stringify(users)); + window.location.href = "/channels/" + guildinfo.id; }); } - const td = document.createElement("div"); - td.classList.add("switchtable"); - td.textContent = I18n.getTranslation("invite.loginOrCreateAccount"); - td.addEventListener("click", ()=>{ - const l = new URLSearchParams("?"); - l.set("goback", window.location.href); - l.set("instance", well!); - window.location.href = "/login?" + l.toString(); + document.getElementById("AcceptInvite")!.addEventListener("click", (e) => { + e.stopImmediatePropagation(); + showAccounts(); }); - - if(!joinable.length){ - const l = new URLSearchParams("?"); - l.set("goback", window.location.href); - l.set("instance", well!); - window.location.href = "/login?" + l.toString(); - } - - table.append(td); - table.classList.add("flexttb","accountSwitcher"); - console.log(table); - document.body.append(table); - } - -document - .getElementById("AcceptInvite")! - .addEventListener("click", showAccounts); -})(); + })(); diff --git a/src/webpage/jsontypes.ts b/src/webpage/jsontypes.ts index 767e91b1..b89cce50 100644 --- a/src/webpage/jsontypes.ts +++ b/src/webpage/jsontypes.ts @@ -1,8 +1,9 @@ -type readyjson = { +interface readyjson { op: 0; t: "READY"; s: number; d: { + _trace?: string[]; v: number; user: mainuserjson; user_settings: { @@ -26,7 +27,7 @@ type readyjson = { }; //might be missing things here gateway_connected: boolean; gif_auto_play: boolean; - guild_folders: []; //need an example of this not empty + guild_folders: guildFolder[]; guild_positions: []; //need an example of this not empty inline_attachment_media: boolean; inline_embed_media: boolean; @@ -43,40 +44,16 @@ type readyjson = { timezone_offset: number; view_nsfw_guilds: boolean; }; + auth_token?: string; guilds: guildjson[]; - relationships: { - id: string; - type: 0 | 1 | 2 | 3 | 4; - nickname: string | null; - user: userjson; - }[]; + relationships: relationJson[]; read_state: { - entries: { - id: string; - channel_id: string; - last_message_id: string; - last_pin_timestamp: string; - mention_count: number; //in theory, the server doesn't actually send this as far as I'm aware - }[]; + entries: readStateEntry[]; partial: boolean; version: number; }; user_guild_settings: { - entries: { - channel_overrides: {message_notifications: number,muted: boolean,mute_config: {selected_time_window: number,end_time: number},channel_id: string}[]; - message_notifications: number; - flags: number; - hide_muted_channels: boolean; - mobile_push: boolean; - mute_config: null; - mute_scheduled_events: boolean; - muted: boolean; - notify_highlights: number; - suppress_everyone: boolean; - suppress_roles: boolean; - version: number; - guild_id: string; - }[]; + entries: GuildOverrides[]; partial: boolean; version: number; }; @@ -85,15 +62,7 @@ type readyjson = { country_code: string; users: userjson[]; merged_members: [memberjson][]; - sessions: { - active: boolean; - activities: []; //will need to find example of this - client_info: { - version: number; - }; - session_id: string; - status: string; - }[]; + sessions: sessionJson[]; resume_gateway_url: string; consents: { personalization: { @@ -115,7 +84,299 @@ type readyjson = { flags: number; }; }; -}; +} +export interface readStateEntry { + id: string; + channel_id: string; + last_message_id: string; + last_pin_timestamp: string; + mention_count: number; //in theory, the server doesn't actually send this as far as I'm aware +} +export interface sessionJson { + active: boolean; + activities: []; //will need to find example of this + client_info: { + version: number; + }; + session_id: string; + status: string; +} +export interface sesLocation { + is_eu: boolean; + city: string | null; + region: string | null; + region_code: string | null; + country_name: string; + country_code: string; + continent_name: string; + continent_code: string; + latitude: number; + longitude: number; + postal: string | null; + calling_code: string; + flag: string; + emoji_flag: string; + emoji_unicode: string; +} +export interface expSessionJson { + id: string; + id_hash: string; + status: string; + activities: []; + client_status: {}; + approx_last_used_time: string; + client_info: { + platform?: string; + location?: string; + os?: string; + version?: number; + }; + last_seen: string; + last_seen_ip: string; + last_seen_location: string | null; + last_seen_location_info: sesLocation | null; +} +export interface GuildOverrides { + channel_overrides: { + message_notifications: number; + muted: boolean; + mute_config: mute_config | null; + channel_id: string; + }[]; + message_notifications: number; + flags: number; + hide_muted_channels: boolean; + mobile_push: boolean; + mute_config: mute_config | null; + mute_scheduled_events: boolean; + muted: boolean; + notify_highlights: number; + suppress_everyone: boolean; + suppress_roles: boolean; + version: number; + guild_id: string; +} +export interface mute_config { + selected_time_window: number; + end_time: number; +} +export interface guildFolder { + color?: number | null; + guild_ids: string[]; + id?: number | null; + name?: string | null; +} +export interface freq { + totalUses: number; + recentUses: string[]; + frecency: -1; + score: number; +} +//https://docs.discord.food/resources/user-settings-proto#frecency-user-settings-object +export interface favandfreq { + versions?: { + client_version: number; + server_version: number; + data_version: number; + }; + favoriteGifs: { + gifs: { + [key: string]: { + format: "GIF_TYPE_IMAGE"; + src: string; + width: number; + height: number; + order: number; + }; + }; + hideTooltip: boolean; + }; + emojiFrecency: { + emojis: { + [key: string]: freq; + }; + }; + guildAndChannelFrecency: { + guildAndChannels: { + [key: string]: freq; + }; + }; + emojiReactionFrecency: { + emojis: { + [key: string]: freq; + }; + }; + favorite_stickers: { + sticker_ids: string[]; + }; + sticker_frecency: { + stickers: { + [key: string]: freq; + }; + }; + favorite_emojis: { + emojis: string[]; + }; + application_command_frecency: { + application_commands: { + [key: string]: freq; + }; + }; + favorite_soundboard_sounds: string[]; + application_frecency: { + applications: { + [key: string]: freq; + }; + }; + heard_sound_frecency: { + heard_sounds: { + [key: string]: freq; + }; + }; + played_sound_frecency: { + played_sounds: { + [key: string]: freq; + }; + }; +} +export interface applicationJson { + description: string; + flags: number; + icon: null | string; + id: string; + name: string; +} +export interface commandOptionJson { + type: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11; + name: string; + name_localized?: string; + name_localizations?: null | Record; + description: string; + description_localizations?: null | Record; + description_localized?: string; + choices?: + | { + name: string; + name_localizations?: null | Record; + name_localized?: string | null; + value: string | number; + }[] + | null; + options?: commandJson[]; + channel_types?: number[]; + min_value?: number; + max_value?: number; + min_length?: number; + max_length?: number; + autocomplete?: boolean; + required?: boolean; +} +export interface commandJson { + id: string; + type: 1 | 2 | 3 | 4; + application_id: string; + guild_id?: null | string; + name: string; + name_localized?: string; + name_localizations?: null | {[key: string]: string}; + description: string; + description_localizations?: null | {[key: string]: string}; + options?: commandOptionJson[]; + default_member_permissions?: null | string; + dm_permission: boolean; + permissions?: null | { + user?: boolean; + roles?: {[key: string]: boolean}; + channels?: {[key: string]: boolean}; + }; + nsfw: boolean; + integration_types?: null | number[]; + global_popularity_rank: number; + contexts?: null | number[]; + version: string; + handler: 0 | 1 | 2 | 3; //0 really shouldn't be here, but it's a bug and should be treated like 1. +} + +interface readySuplemental { + op: 0; + t: "READY_SUPPLEMENTAL"; + s: number; + d: { + merged_presences: { + guilds: []; + friends: []; + }; + merged_members: []; + lazy_private_channels: []; + guilds: { + voice_states: { + user_id: string; + suppress: boolean; + session_id: string; + self_video: boolean; + self_mute: boolean; + self_deaf: boolean; + self_stream: boolean; + request_to_speak_timestamp: null; + mute: boolean; + deaf: boolean; + channel_id: string; //weird reasons, don't question it too much + guild_id: string; + }[]; + id: string; + embedded_activities: []; + }[]; + disclose: []; + }; +} +interface banObj { + reason: string | null; + user: { + username: string; + discriminator: string; + id: string; + avatar: string | null; + public_flags: number; + }; +} +interface templateSkim { + id: string; + code: string; + name: string; + description: string; + usage_count: null | number; + creator_id: string; + created_at: string; + updated_at: string; + source_guild_id: string; + serialized_source_guild: { + id: string; + afk_channel_id: null | string; + afk_timeout: number; + default_message_notifications: number; + description: null | "string"; + explicit_content_filter: number; + features: string[]; + icon: null | string; + large: boolean; + name: string; + preferred_locale: string; + region: string; + system_channel_id: null | string; + system_channel_flags: number; + verification_level: number; + widget_enabled: boolean; + nsfw: boolean; + premium_progress_bar_enabled: boolean; + }; +} +interface addInfoBan { + id: string; + user_id: string; + guild_id: string; + executor_id: string; + reason?: string | undefined; +} type mainuserjson = userjson & { flags: number; mfa_enabled?: boolean; @@ -140,19 +401,28 @@ type userjson = { bot: boolean; premium_since: string; premium_type: number; - theme_colors: string; - pronouns: string; + theme_colors: [number, number] | null; + pronouns?: string; badge_ids: string[]; + webhook?: webhookInfo; + uid?: string; + avatar_decoration_data?: { + asset: string; + sku_id: string; + } | null; }; type memberjson = { index?: number; + bio?: string; id: string; user: userjson | null; guild_id: string; + avatar?: string; + banner?: string; guild: { id: string; } | null; - presence?:presencejson + presence?: presencejson; nick?: string; roles: string[]; joined_at: string; @@ -160,20 +430,39 @@ type memberjson = { deaf: boolean; mute: boolean; pending: boolean; + communication_disabled_until?: string; last_message_id?: boolean; //What??? }; +export type highMemberJSON = mainuserjson & { + mutual_guilds: { + id: string; + nick: null | string; + }[]; + //Only reason this is optional is due to the fact that this is really new and I want to make sure the type checker checks this for me :3 + mutual_friends?: userjson[]; +}; type emojijson = { name: string; id?: string; animated?: boolean; - emoji?:string + emoji?: string; +}; +type emojipjson = emojijson & { + available: boolean; + guild_id: string; + user_id: string; + managed: boolean; + require_colons: boolean; + roles: string[]; + groups: null; //TODO figure out what this means lol }; type guildjson = { - application_command_counts: { [key: string]: number }; + application_command_counts: {[key: string]: number}; channels: channeljson[]; + threads: channeljson[]; data_mode: string; - emojis: emojijson[]; + emojis: emojipjson[]; guild_scheduled_events: []; id: string; large: boolean; @@ -216,15 +505,39 @@ type guildjson = { max_stage_video_channel_users: number; nsfw: boolean; safety_alerts_channel_id: string; + welcome_screen?: welcomeScreen; }; + //TODO implement onboarding too roles: rolesjson[]; stage_instances: []; - stickers: []; - threads: []; + stickers: stickerJson[]; version: string; guild_hashes: {}; joined_at: string; }; +export interface welcomeScreen { + enabled: boolean; + description: string; + welcome_channels: { + description: string; + emoji_id?: string; + emoji_name?: string; + channel_id: string; + }[]; +} +interface stickerJson { + id: string; + name: string; + tags: string; + type: number; + format_type: number; + description?: string; + guild_id?: string; +} +type extendedProperties = guildjson["properties"] & { + emojis: emojipjson[]; + large: boolean; +}; type startTypingjson = { d: { channel_id: string; @@ -234,8 +547,35 @@ type startTypingjson = { member?: memberjson; }; }; +export interface threadMember { + id: string; + user_id: string; + join_timestamp: string; + flags: number; + muted: boolean; + mute_config?: { + end_time?: string; + selected_time_window?: number; + }; + member?: memberjson; +} +export interface tagjson { + name: string; + id: string; + moderated: boolean; + emoji_id?: string; + emoji_name?: string; +} type channeljson = { + member_count?: number; + message_count?: number; + applied_tags?: string[]; + available_tags?: tagjson[]; + total_message_sent?: number; + member: threadMember; id: string; + rate_limit_per_user?: number; + owner_id?: string; created_at: string; name: string; icon: string; @@ -245,6 +585,7 @@ type channeljson = { parent_id: string; last_pin_timestamp: string; default_auto_archive_duration: number; + thread_metadata?: threadMetadata; permission_overwrites: { id: string; allow: string; @@ -258,6 +599,21 @@ type channeljson = { default_thread_rate_limit_per_user: number; position: number; }; +export interface emojiSource { + type: "GUILD" | "APPLICATION"; + guild?: { + id: string; + name: string; + icon: string | null; + description?: string; + features: string[]; + nsfw: boolean; + }; + application?: { + id: string; + name: string; + }; +} type rolesjson = { id: string; guild_id: string; @@ -271,15 +627,170 @@ type rolesjson = { icon: string; unicode_emoji: string; flags: number; + colors?: { + primary_color: number; + secondary_color?: number | null; + tertiary_color?: number | null; + }; }; type dirrectjson = { id: string; flags: number; + name?: string | null; + icon?: string; last_message_id: string; type: number; recipients: userjson[]; is_spam: boolean; + owner_id?: string; }; +type webhookType = { + application_id: null | string; + avatar: null | string; + channel_id: string; + guild_id: string; + id: string; + name: string; + type: 1 | 2 | 3; + user: userjson; + token: string; + url: string; +}; +type webhookInfo = { + id: string; + type: 1; + name: string; + avatar: null | string; + guild_id: string; + channel_id: string; + application_id: null | string; + user_id: string; + source_guild_id: string; + source_channel_id: string; +}; +export enum MessageComponentType { + ActionRow = 1, + Button = 2, + StringSelect = 3, + TextInput = 4, + UserSelect = 5, + RoleSelect = 6, + MentionableSelect = 7, + ChannelSelect = 8, + Section = 9, + + TextDisplay = 10, + Thumbnail = 11, + MediaGallery = 12, + File = 13, + Separator = 14, + // 15 is unknown? + ContentInventoryEntry = 16, // activity feed entry + Container = 17, + Label = 18, + FileUpload = 19, + CheckpointCard = 20, // year in review 2026 + RadioGroup = 21, + CheckboxGroup = 22, + Checkbox = 23, +} +export interface actionRow { + type: MessageComponentType.ActionRow; + components: component[]; +} +export interface button { + type: MessageComponentType.Button; + id?: number; + custom_id: string; + label?: string; + sku_id?: string; + url?: string; + disabled?: boolean; + emoji?: emojijson; + style: 1 | 2 | 3 | 4 | 5 | 6; +} +export interface section { + type: MessageComponentType.Section; + id?: number; + components: component[]; + accessory: button | thumbnail; +} +export interface select { + type: MessageComponentType.StringSelect; + id?: number; + custom_id: string; + options: { + label: string; + value: string; + description?: string; + emoji?: emojijson; + default?: boolean; + }[]; + placeholder?: string; + min_values?: number; + max_values?: number; + required?: boolean; + disabled?: boolean; +} +export interface container { + type: MessageComponentType.Container; + id?: number; + components: component[]; + accent_color?: number; + spoiler?: boolean; +} +export interface textDisp { + type: MessageComponentType.TextDisplay; + id?: number; + content: string; +} +export interface UnfurledMediaItem { + id: string; + url: string; + proxy_url?: string; + height?: number; + width?: number; + flags?: number; + content_type: string; + content_scan_metadata?: unknown; //TODO deal with this lol + placeholder_version?: number; + placeholder?: string; + loading_state?: number; + attachment_id?: string; +} +export interface mediaGallery { + type: MessageComponentType.MediaGallery; + id?: number; + items: { + media: UnfurledMediaItem; + description?: string; + spoiler?: boolean; + }[]; +} +export interface seperator { + type: MessageComponentType.Separator; + id?: number; + divider?: boolean; + spacing?: 1 | 2; +} +export interface thumbnail { + type: MessageComponentType.Thumbnail; + id?: number; + media: UnfurledMediaItem; + description?: string; + spoiler?: string; +} +export type component = + | actionRow + | button + | select + | container + | textDisp + | seperator + | mediaGallery + | section + | thumbnail; + type messagejson = { id: string; channel_id: string; @@ -288,29 +799,58 @@ type messagejson = { member?: memberjson; content: string; timestamp: string; - edited_timestamp: string; + edited_timestamp: string | null; tts: boolean; mention_everyone: boolean; mentions: []; //need examples to fix - mention_roles: []; //need examples to fix + mention_roles?: []; //need examples to fix attachments: filejson[]; embeds: embedjson[]; - reactions: { + components?: component[] | null; //TODO remove this once spacebar fixes the null bug + reactions?: { count: number; emoji: emojijson; //very likely needs expanding me: boolean; }[]; + thread?: channeljson; + interaction?: { + id: string; + type: 2 | 3; + user: userjson; + }; + interaction_metadata?: { + authorizing_integration_owners: { + //User ids + [key: string]: string; + }; + id: string; + type: 2 | 3; + user: userjson; + }; nonce: string; pinned: boolean; type: number; + webhook?: webhookInfo; + sticker_items: stickerJson[]; + message_reference?: string; + referenced_message?: messagejson; }; + +export interface threadMetadata { + archived: boolean; + auto_archive_duration: number; + archive_timestamp: string; + locked: boolean; + invitable?: boolean; + create_timestamp: string; //Discord docs say this is optional, but it's only for after a certain date so it's not +} type filejson = { id: string; filename: string; content_type: string; width?: number; height?: number; - proxy_url: string | undefined; + proxy_url?: string; url: string; size: number; }; @@ -336,6 +876,12 @@ type embedjson = { text?: string; thumbnail?: string; }; + image?: { + proxy_url?: string; + url: string; + width?: number; + height?: number; + }; timestamp?: string; thumbnail: { proxy_url: string; @@ -361,10 +907,10 @@ type invitejson = { code: string; temporary: boolean; uses: number; - max_use: number; + max_uses: number; max_age: number; created_at: string; - expires_at: string; + expires_at: string | null; guild_id: string; channel_id: string; inviter_id: string; @@ -392,158 +938,281 @@ type messageCreateJson = { s: number; t: "MESSAGE_CREATE"; }; -type roleCreate={ - op: 0, - t: "GUILD_ROLE_CREATE", - d: { - guild_id: string, - role: rolesjson - }, - s: 6 -} -type wsjson = -roleCreate | { - op: 0; - d: any; - s: number; - t: - | "TYPING_START" - | "USER_UPDATE" - | "CHANNEL_UPDATE" - | "CHANNEL_CREATE" - | "CHANNEL_DELETE" - | "GUILD_DELETE" - | "GUILD_CREATE" - | "MESSAGE_REACTION_REMOVE_ALL" - | "MESSAGE_REACTION_REMOVE_EMOJI"; - } -| { - op: 0; - t: "GUILD_MEMBERS_CHUNK"; - d: memberChunk; - s: number; +export interface relationJson { + id: string; + type: 0 | 1 | 2 | 3 | 4; + nickname: string | null; + user: userjson; } -| { +type roleCreate = { op: 0; + t: "GUILD_ROLE_CREATE"; d: { - id: string; - guild_id?: string; - channel_id: string; + guild_id: string; + role: rolesjson; }; - s: number; - t: "MESSAGE_DELETE"; -} -| { + s: 6; +}; +type wsjson = + | readySuplemental + | roleCreate + | { + op: 0; + d: any; + s: number; + t: + | "TYPING_START" + | "USER_UPDATE" + | "CHANNEL_UPDATE" + | "CHANNEL_CREATE" + | "CHANNEL_DELETE" + | "GUILD_DELETE" + | "GUILD_CREATE" + | "MESSAGE_REACTION_REMOVE_ALL" + | "MESSAGE_REACTION_REMOVE_EMOJI" + | "THREAD_CREATE"; + } + | { + op: 0; + t: "GUILD_MEMBERS_CHUNK"; + d: memberChunk; + s: number; + } + | { + op: 0; + d: { + id: string; + guild_id?: string; + channel_id: string; + }; + s: number; + t: "MESSAGE_DELETE"; + } + | { + op: 0; + t: "THREAD_MEMBERS_UPDATE"; + d: { + guild_id: string; + id: string; + member_count: number; + added_members?: [threadMember]; + removed_member_ids?: string[]; + }; + s: 3; + } + | { + op: 0; + d: { + guild_id?: string; + channel_id: string; + } & messagejson; + s: number; + t: "MESSAGE_UPDATE"; + } + | messageCreateJson + | readyjson + | { + op: 11; + s: undefined; + d: {}; + } + | { + op: 10; + s: undefined; + d: { + heartbeat_interval: number; + }; + } + | { + op: 0; + t: "MESSAGE_REACTION_ADD"; + d: { + user_id: string; + channel_id: string; + message_id: string; + guild_id?: string; + emoji: emojijson; + member?: memberjson; + }; + s: number; + } + | { + op: 0; + t: "MESSAGE_REACTION_REMOVE"; + d: { + user_id: string; + channel_id: string; + message_id: string; + guild_id: string; + emoji: emojijson; + }; + s: number; + } + | { + op: 0; + t: "GUILD_ROLE_UPDATE"; + d: { + guild_id: string; + role: rolesjson; + }; + s: number; + } + | { + op: 0; + t: "GUILD_ROLE_DELETE"; + d: { + guild_id: string; + role_id: string; + }; + s: number; + } + | { + op: 0; + t: "GUILD_MEMBER_UPDATE"; + d: memberjson; + s: 3; + } + | { + op: 9; + d: boolean; + s: number; + } + | memberlistupdatejson + | voiceupdate + | voiceserverupdate + | { + op: 0; + t: "RELATIONSHIP_ADD"; + d: relationJson; + s: number; + } + | { + op: 0; + t: "RELATIONSHIP_REMOVE"; + d: relationJson; + s: number; + } + | { + op: 0; + t: "RELATIONSHIP_UPDATE"; + d: relationJson; + s: number; + } + | { + op: 0; + t: "PRESENCE_UPDATE"; + d: presencejson; + s: number; + } + | { + op: 0; + t: "GUILD_MEMBER_ADD"; + d: memberjson; + s: number; + } + | { + op: 0; + t: "GUILD_MEMBER_REMOVE"; + d: { + guild_id: string; + user: userjson; + }; + s: number; + } + | { + op: 0; + t: "GUILD_EMOJIS_UPDATE"; + d: { + guild_id: string; + emojis: emojipjson[]; + }; + s: number; + } + | { + op: 0; + t: "GUILD_UPDATE"; + d: extendedProperties; + s: number; + } + | { + op: 0; + t: "CHANNEL_PINS_UPDATE"; + d: { + channel_id: string; + guild_id: string; + }; + s: number; + } + | { + op: 0; + t: "GUILD_STICKERS_UPDATE"; + d: { + guild_id: string; + stickers: stickerJson[]; + }; + s: number; + } + | streamServerUpdate + | streamCreate + | interactionEvents + | { + op: 0; + t: "CHANNEL_RECIPIENT_ADD"; + d: { + channel_id: string; + user: userjson; + }; + s: number; + } + | { + op: 0; + t: "CHANNEL_RECIPIENT_REMOVE"; + d: { + channel_id: string; + user: userjson; + }; + s: number; + } + | { + op: 0; + t: "MESSAGE_ACK"; + d: { + channel_id: string; + message_id: string; + version: number; //I don't think this really matters lol + }; + s: number; + }; + +export interface interactionCreate { op: 0; + t: "INTERACTION_CREATE"; d: { - guild_id?: string; - channel_id: string; - } & messagejson; - s: number; - t: "MESSAGE_UPDATE"; -} -| messageCreateJson -| readyjson -| { - op: 11; - s: undefined; - d: {}; -} -| { - op: 10; - s: undefined; - d: { - heartbeat_interval: number; + id: string; + nonce: string; }; + s: number; } -| { +export interface interactionSuccess { op: 0; - t: "MESSAGE_REACTION_ADD"; + t: "INTERACTION_SUCCESS"; d: { - user_id: string; - channel_id: string; - message_id: string; - guild_id?: string; - emoji: emojijson; - member?: memberjson; + id: string; + nonce: string; }; s: number; } -| { +export interface interactionFailure { op: 0; - t: "MESSAGE_REACTION_REMOVE"; + t: "INTERACTION_FAILURE"; d: { - user_id: string; - channel_id: string; - message_id: string; - guild_id: string; - emoji: emojijson; + id: string; + nonce: string; + reason_code: number; }; s: number; -}|{ - op: 0, - t: "GUILD_ROLE_UPDATE", - d: { - guild_id: string, - role: rolesjson - }, - "s": number -}|{ - op: 0, - t: "GUILD_ROLE_DELETE", - d: { - guild_id: string, - role_id: string - }, - s:number -}|{ - op: 0, - t: "GUILD_MEMBER_UPDATE", - d: memberjson, - s: 3 -}|{ - op:9, - d:boolean, - s:number -}|memberlistupdatejson|voiceupdate|voiceserverupdate|{ - op: 0, - t: "RELATIONSHIP_ADD", - d: { - id: string, - type: 0|1|2|3|4|5|6, - user: userjson - }, - s: number -}|{ - op: 0, - t: "RELATIONSHIP_REMOVE", - d: { - id: string, - type: number, - nickname: null - }, - s: number -}|{ - op: 0, - t: "PRESENCE_UPDATE", - d: presencejson, - s:number -}|{ - op:0, - t:"GUILD_MEMBER_ADD", - d:memberjson, - s:number -}|{ - op:0, - t:"GUILD_MEMBER_REMOVE", - d:{ - guild_id:string, - user:userjson - }, - s:number -}; - - +} +export type interactionEvents = interactionCreate | interactionSuccess | interactionFailure; type memberChunk = { guild_id: string; nonce: string; @@ -553,134 +1222,166 @@ type memberChunk = { chunk_count: number; not_found: string[]; }; -type voiceupdate={ - op: 0, - t: "VOICE_STATE_UPDATE", +export type voiceStatus = { + guild_id: string; + channel_id: string; + user_id: string; + member?: memberjson; + session_id: string; + deaf: boolean; + mute: boolean; + self_deaf: boolean; + self_mute: boolean; + self_video: boolean; + self_stream: boolean; + suppress: boolean; +}; +export interface streamCreate { + op: 0; + t: "STREAM_CREATE"; d: { - guild_id: string, - channel_id: string, - user_id: string, - member: memberjson, - session_id: string, - token: string, - deaf: boolean, - mute: boolean, - self_deaf: boolean, - self_mute: boolean, - self_video: boolean, - suppress: boolean - }, - s: number + stream_key: string; + rtc_server_id: string; + viewer_ids: string[]; + region: "spacebar"; + paused: boolean; + }; + s: number; +} +export interface streamServerUpdate { + op: 0; + t: "STREAM_SERVER_UPDATE"; + d: { + token: string; + stream_key: string; + guild_id: null; //There is no way this ain't a server bug lol + endpoint: string; + }; + s: number; +} +type voiceupdate = { + op: 0; + t: "VOICE_STATE_UPDATE"; + d: voiceStatus; + s: number; }; -type voiceserverupdate={ - op: 0, - t: "VOICE_SERVER_UPDATE", +type voiceserverupdate = { + op: 0; + t: "VOICE_SERVER_UPDATE"; d: { - token: string, - guild_id: string, - endpoint: string - }, - s: 6 + token: string; + guild_id: string; + endpoint: string; + }; + s: 6; }; -type memberlistupdatejson={ - op: 0, - s: number, - t: "GUILD_MEMBER_LIST_UPDATE", +type memberlistupdatejson = { + op: 0; + s: number; + t: "GUILD_MEMBER_LIST_UPDATE"; d: { ops: [ { - items:({ - group:{ - count:number, - id:string - } - }|{ - member:memberjson - })[] - op: "SYNC", - range: [ - number, - number - ] - } - ], - online_count: number, - member_count: number, - id: string, - guild_id: string, + items: ( + | { + group: { + count: number; + id: string; + }; + } + | { + member: memberjson; + } + )[]; + op: "SYNC"; + range: [number, number]; + }, + ]; + online_count: number; + member_count: number; + id: string; + guild_id: string; groups: { - count: number, - id: string - }[] - } -} -type webRTCSocket= { - op: 8, - d: { - heartbeat_interval: number - } -}|{ - op:6, - d:{t: number} -}|{ - op: 2, - d: { - ssrc: number, - "streams": { - type: "video",//probally more options, but idk - rid: string, - quality: number, - ssrc: number, - rtx_ssrc:number - }[], - ip: number, - port: number, - "modes": [],//no clue - "experiments": []//no clue - } -}|sdpback|opRTC12|{ - op: 5, - d: { - user_id: string, - speaking: 0, - ssrc: 940464811 - } + count: number; + id: string; + }[]; + }; }; -type sdpback={ - op: 4, +type webRTCSocket = + | { + op: 8; + d: { + heartbeat_interval: number; + }; + } + | { + op: 6; + d: {t: number}; + } + | { + op: 2; + d: { + ssrc: number; + streams: { + type: "video"; //probally more options, but idk + rid: string; + quality: number; + ssrc: number; + rtx_ssrc: number; + }[]; + ip: number; + port: number; + modes: []; //no clue + experiments: []; //no clue + }; + } + | sdpback + | opRTC12 + | { + op: 5; + d: { + user_id: string; + speaking: 0; + ssrc: 940464811; + }; + }; + +type sdpback = { + op: 4; d: { - audioCodec: string, - videoCodec: string, - media_session_id: string, - sdp: string - } + audioCodec: string; + videoCodec: string; + media_session_id: string; + sdp: string; + }; }; -type opRTC12={ - op: 12, +type opRTC12 = { + op: 12; d: { - user_id: string, - audio_ssrc: number, - video_ssrc: number, + user_id: string; + audio_ssrc: number; + video_ssrc: number; streams: [ { - type: "video", - rid: "100", - ssrc: number, - active: boolean, - quality: 100, - rtx_ssrc: number, - max_bitrate: 2500000, - max_framerate: number, + type: "video"; + rid: "100"; + ssrc: number; + active: boolean; + quality: 100; + rtx_ssrc: number; + max_bitrate: 2500000; + max_framerate: number; max_resolution: { - type: "fixed", - width: number, - height: number - } - } - ] - } -} -export{ + type: "fixed"; + width: number; + height: number; + }; + }, + ]; + }; +}; + +export { readyjson, dirrectjson, startTypingjson, @@ -704,5 +1405,13 @@ export{ voiceserverupdate, webRTCSocket, sdpback, - opRTC12 + opRTC12, + emojipjson, + extendedProperties, + webhookInfo, + webhookType, + stickerJson, + banObj, + addInfoBan, + templateSkim, }; diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index e61e01fd..0e52adf2 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -1,35 +1,94 @@ -import{ Guild }from"./guild.js"; -import{ Channel }from"./channel.js"; -import{ Direct }from"./direct.js"; -import{ AVoice }from"./audio.js"; -import{ User }from"./user.js"; -import{ getapiurls, getBulkInfo, setTheme, Specialuser, SW }from"./login.js"; -import{channeljson,guildjson,mainuserjson,memberjson,memberlistupdatejson,messageCreateJson,presencejson,readyjson,startTypingjson,wsjson,}from"./jsontypes.js"; -import{ Member }from"./member.js"; -import{ Dialog, Form, FormError, Options, Settings }from"./settings.js"; -import{ getTextNodeAtPosition, MarkDown }from"./markdown.js"; -import { Bot } from "./bot.js"; -import { Role } from "./role.js"; -import { VoiceFactory } from "./voice.js"; -import { I18n, langmap } from "./i18n.js"; -import { Emoji } from "./emoji.js"; - -const wsCodesRetry = new Set([4000,4001,4002, 4003, 4005, 4007, 4008, 4009]); - -class Localuser{ - badges: Map = new Map(); +import {Guild} from "./guild.js"; +import {Channel} from "./channel.js"; +import {Direct, Group} from "./direct.js"; +import {User} from "./user.js"; +import {createImg, getapiurls, getBulkUsers, installPGet, SW} from "./utils/utils.js"; +import {getBulkInfo, setTheme, Specialuser} from "./utils/utils.js"; +import { + channeljson, + expSessionJson, + guildFolder, + mainuserjson, + memberjson, + memberlistupdatejson, + messageCreateJson, + messagejson, + presencejson, + readStateEntry, + readyjson, + startTypingjson, + wsjson, +} from "./jsontypes.js"; +import {Member} from "./member.js"; +import {Dialog, Form, FormError, Options, Settings} from "./settings.js"; +import {getTextNodeAtPosition, MarkDown, saveCaretPosition} from "./markdown.js"; +import {Bot} from "./bot.js"; +import {Role} from "./role.js"; +import {VoiceFactory, voiceStatusStr} from "./voice.js"; +import {I18n, langmap} from "./i18n.js"; +import {Emoji} from "./emoji.js"; +import {Play} from "./audio/play.js"; +import {Message} from "./message.js"; +import {badgeArr} from "./Dbadges.js"; +import {Rights} from "./rights.js"; +import {Contextmenu} from "./contextmenu.js"; +import {Sticker} from "./sticker.js"; +import {Hover} from "./hover.js"; +import {AccountSwitcher} from "./utils/switcher.js"; +import {Favorites} from "./favorites.js"; +import { + AnimateTristateValues, + getPreferences, + setPreferences, + ThemeOption, +} from "./utils/storage/userPreferences"; +import {getDeveloperSettings, setDeveloperSettings} from "./utils/storage/devSettings"; +import {getLocalSettings, ServiceWorkerModeValues} from "./utils/storage/localSettings.js"; +import {PromiseLock} from "./utils/promiseLock.js"; +import {CDNParams} from "./utils/cdnParams.js"; +type traceObj = { + micros: number; + calls?: (string | traceObj)[]; +}; +type trace = [string, traceObj]; +const wsCodesRetry = new Set([4000, 4001, 4002, 4003, 4005, 4007, 4008, 4009]); +interface CustomHTMLDivElement extends HTMLDivElement { + markdown: MarkDown; +} + +MarkDown.emoji = Emoji; +class Localuser { + badges = new Map< + string, + {id: string; description: string; icon: string; link?: string; translate?: boolean} + >( + badgeArr as [ + string, + {id: string; description: string; icon: string; link?: string; translate?: boolean}, + ][], + ); lastSequence: number | null = null; - token!: string; + get token() { + return this.headers.Authorization; + } userinfo!: Specialuser; serverurls!: Specialuser["serverurls"]; initialized!: boolean; info!: Specialuser["serverurls"]; - headers!: { "Content-type": string; Authorization: string }; + headers!: {"Content-type": string; Authorization: string}; ready!: readyjson; guilds!: Guild[]; guildids: Map = new Map(); user!: User; - status!: string; + idToPrev: Map = new Map(); + idToNext: Map = new Map(); + messages: Map = new Map(); + get status() { + return this.user.status; + } + set status(status: string) { + this.user.setstatus(status); + } channelfocus: Channel | undefined; lookingguild: Guild | undefined; guildhtml: Map = new Map(); @@ -38,63 +97,304 @@ class Localuser{ errorBackoff = 0; channelids: Map = new Map(); readonly userMap: Map = new Map(); - voiceFactory?:VoiceFactory; + voiceFactory?: VoiceFactory; + play?: Play; instancePing = { name: "Unknown", }; mfa_enabled!: boolean; - get perminfo(){ + get perminfo() { return this.userinfo.localuserStore; } - set perminfo(e){ + set perminfo(e) { this.userinfo.localuserStore = e; } - constructor(userinfo: Specialuser | -1){ - if(userinfo === -1){ + static users = getBulkUsers(); + static async showAccountSwitcher(thisUser?: Localuser) { + const specialUser = await new AccountSwitcher().show(); + + const onswap = thisUser?.onswap; + thisUser?.unload(); + if (thisUser) thisUser.swapped = true; + const loading = document.getElementById("loading") as HTMLDivElement; + loading.classList.remove("doneloading"); + loading.classList.add("loading"); + + thisUser = new Localuser(specialUser); + Localuser.users.currentuser = specialUser.uid; + sessionStorage.setItem("currentuser", specialUser.uid); + localStorage.setItem("userinfos", JSON.stringify(Localuser.users)); + + thisUser.initwebsocket().then(async () => { + const loaddesc = document.getElementById("load-desc") as HTMLElement; + thisUser.loaduser(); + await thisUser.init(); + loading.classList.add("doneloading"); + loaddesc.textContent = I18n.loaded(); + loading.classList.remove("loading"); + console.log("done loading"); + }); + + onswap?.(thisUser); + } + static userMenu = this.generateUserMenu(); + userResMap = new Map>(); + async getUser(id: string) { + let user = this.userMap.get(id); + if (user) return user; + const cache = this.userResMap.get(id); + if (cache) return cache; + const prom = User.resolve(id, this); + this.userResMap.set(id, prom); + await prom; + this.userResMap.delete(id); + return prom; + } + static generateUserMenu() { + const menu = new Contextmenu(""); + menu.addButton( + () => I18n.localuser.addStatus(), + function () { + const d = new Dialog(I18n.localuser.status()); + const opt = d.float.options.addForm( + "", + () => { + const status = cust.value; + sessionStorage.setItem("cstatus", JSON.stringify({text: status})); + //this.user.setstatus(status); + d.hide(); + }, + { + fetchURL: this.info.api + "/users/@me/settings", + method: "PATCH", + headers: this.headers, + }, + ); + opt.addText(I18n.localuser.customStatusWarn()); + opt.addPreprocessor((obj) => { + if ("custom_status" in obj) { + obj.custom_status = {text: obj.custom_status}; + } + }); + const cust = opt.addTextInput(I18n.localuser.status(), "custom_status", {}); + d.show(); + }, + ); + menu.addButton( + () => I18n.localuser.status(), + function () { + const d = new Dialog(I18n.localuser.status()); + const opt = d.float.options; + const selection = ["online", "invisible", "dnd", "idle"] as const; + const smap = selection.map((_) => I18n.user[_]()); + let index = selection.indexOf(this.status as "online" | "invisible" | "dnd" | "idle"); + if (index === -1) { + index = 0; + } + opt + .addSelect("", () => {}, smap, { + defaultIndex: index, + }) + .watchForChange(async (i) => { + const status = selection[i]; + await fetch(this.info.api + "/users/@me/settings", { + body: JSON.stringify({ + status, + }), + headers: this.headers, + method: "PATCH", + }); + sessionStorage.setItem("status", status); + this.user.setstatus(status); + }); + d.show(); + }, + ); + menu.addButton( + () => I18n.switchAccounts(), + function () { + Localuser.showAccountSwitcher(this); + }, + ); + return menu; + } + onswap?: (l: Localuser) => void; + constructor(userinfo: Specialuser | -1) { + Play.playURL("/audio/sounds.jasf").then((_) => { + this.play = _; + }); + + //TODO get rid of this garbage + if (userinfo === -1) { + this.rights = new Rights(""); return; } - this.token = userinfo.token; this.userinfo = userinfo; this.perminfo.guilds ??= {}; + this.perminfo.user ??= {}; + this.perminfo.user.decorations ??= true; this.serverurls = this.userinfo.serverurls; this.initialized = false; this.info = this.serverurls; + SW.postMessage({ + code: "canRefresh", + host: new URL(this.info.cdn).host, + }); + SW.captureEvent("refreshURL", async (e) => { + SW.postMessage({ + code: "refreshedUrl", + url: await this.refreshURL(e.url), + oldurl: e.url, + }); + }); this.headers = { "Content-type": "application/json; charset=UTF-8", Authorization: this.userinfo.token, }; - } - async gottenReady(ready: readyjson): Promise{ + this.favorites = new Favorites(this); + const rights = this.perminfo.user.rights || "875069521787904"; + this.rights = new Rights(rights); + if (this.perminfo.user.disableColors === undefined) this.perminfo.user.disableColors = true; + this.updateTranslations(); + } + favorites!: Favorites; + readysup = false; + get voiceAllowed() { + return this.readysup; + } + mute = true; + deaf = false; + updateOtherMic = () => {}; + updateMic(updateVoice: boolean = true) { + this.updateOtherMic(); + const mic = document.getElementById("mic") as HTMLElement; + mic.classList.remove("svg-mic", "svg-micmute"); + if (this.voiceFactory && updateVoice) this.voiceFactory.mute = this.mute; + if (this.mute) { + mic.classList.add("svg-micmute"); + } else { + mic.classList.add("svg-mic"); + } + } + channelByID(id: string): Channel | void { + let channel: Channel | void = undefined; + this.guilds.forEach((_) => { + _.channels.forEach((_) => { + if (_.id === id) { + channel = _; + } + }); + }); + return channel; + } + trace: {trace: trace; time: Date}[] = []; + handleTrace(str: string[]) { + const json = str.map((_) => JSON.parse(_)) as trace[]; + console.log(json); + this.trace.push( + ...json.map((trace) => { + return {trace, time: new Date()}; + }), + ); + } + async queryBlog() { + this.perminfo.localuser ??= {}; + const prefs = await getPreferences(); + const bstate = prefs.showBlogUpdates; + if (bstate === undefined) { + const pop = new Dialog(""); + pop.options.addText(I18n.blog.wantUpdates()); + const opts = pop.options.addOptions("", {ltr: true}); + opts.addButtonInput("", I18n.yes(), async () => { + prefs.showBlogUpdates = true; + await setPreferences(prefs); + this.queryBlog(); + pop.hide(); + }); + opts.addButtonInput("", I18n.no(), async () => { + prefs.showBlogUpdates = false; + await setPreferences(prefs); + this.queryBlog(); + pop.hide(); + }); + pop.show(); + } else if (bstate) { + const post = (await this.getPosts()).items[0]; + if (this.perminfo.localuser.mostRecent !== post.url) { + this.perminfo.localuser.mostRecent = post.url; + const pop = new Dialog(post.title); + //TODO implement images for the rendering of this + pop.options.addText(post.content_html); + pop.options.addButtonInput("", I18n.blog.gotoPost(), () => { + window.open(post.url); + pop.hide(); + }); + pop.show(); + } + } + } + guildFolders: guildFolder[] = []; + unknownRead = new Map(); + async gottenReady(ready: readyjson): Promise { await I18n.done; + this.errorBackoff = 0; + this.channelids.clear(); + this.inrelation.clear(); + this.userMap.clear(); + this.queryBlog(); + this.guildFolders = ready.d.user_settings.guild_folders; + document.body.style.setProperty("--view-rest", I18n.message.viewrest()); this.initialized = true; this.ready = ready; this.guilds = []; this.guildids = new Map(); this.user = new User(ready.d.user, this); - this.user.setstatus("online"); - this.resume_gateway_url=ready.d.resume_gateway_url; - this.session_id=ready.d.session_id; + this.user.setstatus(sessionStorage.getItem("status") || "online"); + this.resume_gateway_url = ready.d.resume_gateway_url; + this.session_id = ready.d.session_id; this.mdBox(); - this.voiceFactory=new VoiceFactory({id:this.user.id}); + this.voiceFactory = new VoiceFactory( + {id: this.user.id}, + (g) => { + if (this.ws) { + this.ws.send(JSON.stringify(g)); + } + }, + this.info.api.startsWith("https://"), + ); this.handleVoice(); this.mfa_enabled = ready.d.user.mfa_enabled as boolean; this.userinfo.username = this.user.username; this.userinfo.id = this.user.id; this.userinfo.pfpsrc = this.user.getpfpsrc(); + + if (ready.d.auth_token) { + this.userinfo.token = ready.d.auth_token; + this.userinfo.json.token = ready.d.auth_token; + this.headers.Authorization = ready.d.auth_token; + this.userinfo.updateLocal(); + } + this.status = this.ready.d.user_settings.status; this.channelfocus = undefined; this.lookingguild = undefined; this.guildhtml = new Map(); - const members: { [key: string]: memberjson } = {}; - if(ready.d.merged_members){ - for(const thing of ready.d.merged_members){ + const members: {[key: string]: memberjson} = {}; + if (ready.d.merged_members) { + for (const thing of ready.d.merged_members) { members[thing[0].guild_id] = thing[0]; } } - for(const thing of ready.d.guilds){ + this.updateMic(); + const mic = document.getElementById("mic") as HTMLElement; + mic.onclick = () => { + this.mute = !this.mute; + this.updateMic(); + }; + for (const thing of ready.d.guilds) { const temp = new Guild(thing, this, members[thing.id]); this.guilds.push(temp); this.guildids.set(temp.id, temp); @@ -104,94 +404,104 @@ class Localuser{ this.guilds.push(temp); this.guildids.set(temp.id, temp); } - console.log(ready.d.user_guild_settings.entries); + if (ready.d.user_guild_settings) { + console.log(ready.d.user_guild_settings.entries); - for(const thing of ready.d.user_guild_settings.entries){ - (this.guildids.get(thing.guild_id) as Guild).notisetting(thing); + for (const thing of ready.d.user_guild_settings.entries) { + (this.guildids.get(thing.guild_id) as Guild).notisetting(thing); + } } - - for(const thing of ready.d.read_state.entries){ - const channel = this.channelids.get(thing.channel_id); - if(!channel){ - continue; + if (ready.d.read_state) { + for (const thing of ready.d.read_state.entries) { + const channel = this.channelids.get(thing.channel_id); + if (!channel) { + this.unknownRead.set(thing.channel_id, thing); + continue; + } + channel.readStateInfo(thing); } - channel.readStateInfo(thing); } - for(const thing of ready.d.relationships){ - const user = new User(thing.user, this); - user.nickname = thing.nickname; - user.relationshipType = thing.type; - this.inrelation.add(user); + for (const relationship of ready.d.relationships) { + const user = new User(relationship.user, this); + user.handleRelationship(relationship); } this.pingEndpoint(); - this.userinfo.updateLocal(); - } - inrelation=new Set(); - outoffocus(): void{ + inrelation = new Set(); + outoffocus(): void { const servers = document.getElementById("servers") as HTMLDivElement; servers.innerHTML = ""; const channels = document.getElementById("channels") as HTMLDivElement; channels.innerHTML = ""; - if(this.channelfocus){ + if (this.channelfocus) { this.channelfocus.infinite.delete(); } this.lookingguild = undefined; this.channelfocus = undefined; } - unload(): void{ + giveMessage(m: messagejson) { + const c = this.channelids.get(m.channel_id); + if (!c) return; + new Message(m, c); + } + unload(): void { this.initialized = false; this.outoffocus(); this.guilds = []; this.guildids = new Map(); - if(this.ws){ + if (this.ws) { this.ws.close(4040); } } swapped = false; - resume_gateway_url?:string; - session_id?:string; - async initwebsocket(resume=false): Promise{ + resume_gateway_url?: string; + session_id?: string; + async initwebsocket(resume = false): Promise { let returny: () => void; - if(!this.resume_gateway_url||!this.session_id){ - resume=false; + if (!this.resume_gateway_url || !this.session_id) { + resume = false; + } + if (!resume) { + this.messages.clear(); + this.idToPrev.clear(); + this.idToNext.clear(); } + const doComp = DecompressionStream && !getDeveloperSettings().gatewayCompression; const ws = new WebSocket( - (resume?this.resume_gateway_url:this.serverurls.gateway.toString()) - +"?encoding=json&v=9" + - (DecompressionStream ? "&compress=zlib-stream" : "") + (resume ? this.resume_gateway_url : this.serverurls.gateway.toString()) + + "?encoding=json&v=9" + + (doComp ? "&compress=zlib-stream" : ""), ); this.ws = ws; let ds: DecompressionStream; let w: WritableStreamDefaultWriter; - let r: ReadableStreamDefaultReader; let arr: Uint8Array; - let build = ""; - if(DecompressionStream){ + + if (DecompressionStream) { ds = new DecompressionStream("deflate"); w = ds.writable.getWriter(); - r = ds.readable.getReader(); + arr = new Uint8Array(); } - const promise = new Promise(res=>{ + const promise = new Promise((res) => { returny = res; - ws.addEventListener("open", _event=>{ + ws.addEventListener("open", (_event) => { console.log("WebSocket connected"); - if(resume){ + if (resume) { ws.send( JSON.stringify({ op: 6, d: { token: this.token, session_id: this.session_id, - seq: this.lastSequence - } - }) + seq: this.lastSequence, + }, + }), ); - this.resume_gateway_url=undefined; - this.session_id=undefined; - }else{ + this.resume_gateway_url = undefined; + this.session_id = undefined; + } else { ws.send( JSON.stringify({ op: 2, @@ -199,52 +509,58 @@ class Localuser{ token: this.token, capabilities: 16381, properties: { - browser: "Jank Client", + browser: "Fermi", client_build_number: 0, //might update this eventually lol release_channel: "Custom", browser_user_agent: navigator.userAgent, }, compress: Boolean(DecompressionStream), presence: { - status: "online", + status: sessionStorage.getItem("status") || "online", since: null, //new Date().getTime() activities: [], afk: false, - }, + }, //TODO think this through, it's just a stupid large number to fix op 8 requests + large_threshold: 100000000, }, - }) + }), ); } }); - const textdecode = new TextDecoder(); - if(DecompressionStream){ - (async ()=>{ - while(true){ - const read = await r.read(); - const data = textdecode.decode(read.value); + + if (DecompressionStream) { + (async () => { + let build = ""; + for await (const data of ds.readable.tee()[0].pipeThrough(new TextDecoderStream())) { build += data; - try{ + try { const temp = JSON.parse(build); build = ""; await this.handleEvent(temp); - if(temp.op === 0 && temp.t === "READY"){ + + if (temp.op === 0 && temp.t === "READY") { + console.log("in here?"); returny(); } - }catch{} + } catch (e) { + if (!(e instanceof SyntaxError)) { + console.error(e); + } + } } })(); } }); - let order = new Promise(res=>res()); + let order = new Promise((res) => res()); - ws.addEventListener("message", async event=>{ + ws.addEventListener("message", async (event) => { const temp2 = order; - order = new Promise(async res=>{ + order = new Promise(async (res) => { await temp2; - let temp: { op: number; t: string }; - try{ - if(event.data instanceof Blob){ + let temp: {op: number; t: string}; + try { + if (event.data instanceof Blob) { const buff = await event.data.arrayBuffer(); const array = new Uint8Array(buff); @@ -254,41 +570,44 @@ class Localuser{ arr = temparr; const len = array.length; - if( + if ( !( array[len - 1] === 255 && array[len - 2] === 255 && array[len - 3] === 0 && array[len - 4] === 0 ) - ){ + ) { return; } w.write(arr.buffer); arr = new Uint8Array(); return; //had to move the while loop due to me being dumb - }else{ + } else { temp = JSON.parse(event.data); } await this.handleEvent(temp as readyjson); - if(temp.op === 0 && temp.t === "READY"){ + if (temp.op === 0 && temp.t === "READY") { returny(); } - }catch(e){ + } catch (e) { console.error(e); - }finally{ + } finally { res(); } }); }); - ws.addEventListener("close", async event=>{ + ws.addEventListener("close", async (event) => { this.ws = undefined; console.log("WebSocket closed with code " + event.code); - if((event.code > 1000 && event.code < 1016) || (wsCodesRetry.has(event.code)&&this.errorBackoff===0)){ + if ( + (event.code > 1000 && event.code < 1016 && this.errorBackoff === 0) || + (wsCodesRetry.has(event.code) && this.errorBackoff === 0) + ) { this.errorBackoff++; - this.initwebsocket(true).then(()=>{ + this.initwebsocket(true).then(() => { this.loaduser(); }); return; @@ -296,543 +615,1097 @@ class Localuser{ this.unload(); (document.getElementById("loading") as HTMLElement).classList.remove("doneloading"); (document.getElementById("loading") as HTMLElement).classList.add("loading"); - this.fetchingmembers = new Map(); - this.noncemap = new Map(); - this.noncebuild = new Map(); - if((event.code > 1000 && event.code < 1016) || wsCodesRetry.has(event.code)||event.code==4041){ - if(this.connectionSucceed !== 0 && Date.now() > this.connectionSucceed + 20000){ + this.fetchingmembers.clear(); + this.noncemap.clear(); + this.noncebuild.clear(); + const loaddesc = document.getElementById("load-desc") as HTMLElement; + if ( + (event.code > 1000 && event.code < 1016) || + wsCodesRetry.has(event.code) || + event.code == 4041 + ) { + if (this.connectionSucceed !== 0 && Date.now() > this.connectionSucceed + 20000) { this.errorBackoff = 0; - }else this.errorBackoff++; + } else this.errorBackoff++; this.connectionSucceed = 0; - const loaddesc=document.getElementById("load-desc") as HTMLElement; - - loaddesc.innerHTML =""; - loaddesc.append(new MarkDown(I18n.getTranslation("errorReconnect",Math.round(0.2 + this.errorBackoff * 2.8)+"")).makeHTML()); - switch(this.errorBackoff){//try to recover from bad domain - case 3: - const newurls = await getapiurls(this.info.wellknown); - if(newurls){ - this.info = newurls; - this.serverurls = newurls; - this.userinfo.json.serverurls = this.info; - this.userinfo.updateLocal(); + + loaddesc.innerHTML = ""; + loaddesc.append( + new MarkDown( + I18n.errorReconnect(Math.round(0.2 + this.errorBackoff * 2.8) + ""), + ).makeHTML(), + ); + switch ( + this.errorBackoff //try to recover from bad domain + ) { + case 3: + const newurls = await getapiurls(this.info.wellknown); + if (newurls) { + this.info = newurls; + this.serverurls = newurls; + this.userinfo.json.serverurls = this.info; + break; + } break; - } - break; - case 4: { - const newurls = await getapiurls( - new URL(this.info.wellknown).origin - ); - if(newurls){ - this.info = newurls; - this.serverurls = newurls; - this.userinfo.json.serverurls = this.info; - this.userinfo.updateLocal(); + case 4: { + const newurls = await getapiurls(new URL(this.info.wellknown).origin); + if (newurls) { + this.info = newurls; + this.serverurls = newurls; + this.userinfo.json.serverurls = this.info; + break; + } break; } - break; - } - case 5: { - const breakappart = new URL(this.info.wellknown).origin.split("."); - const url = - "https://" + breakappart.at(-2) + "." + breakappart.at(-1); - const newurls = await getapiurls(url); - if(newurls){ - this.info = newurls; - this.serverurls = newurls; - this.userinfo.json.serverurls = this.info; - this.userinfo.updateLocal(); + case 5: { + const breakappart = new URL(this.info.wellknown).host.split("."); + const url = "https://" + breakappart.at(-2) + "." + breakappart.at(-1); + const newurls = await getapiurls(url); + if (newurls) { + this.info = newurls; + this.serverurls = newurls; + this.userinfo.json.serverurls = this.info; + } + break; } - break; - } } - setTimeout(()=>{ - if(this.swapped)return; - (document.getElementById("load-desc") as HTMLElement).textContent =I18n.getTranslation("retrying"); - this.initwebsocket().then(()=>{ - this.loaduser(); - this.init(); - const loading = document.getElementById("loading") as HTMLElement; - loading.classList.add("doneloading"); - loading.classList.remove("loading"); - console.log("done loading"); - }); - }, 200 + this.errorBackoff * 2800); - }else - (document.getElementById("load-desc") as HTMLElement).textContent = I18n.getTranslation("unableToConnect") + setTimeout( + () => { + if (this.swapped) return; + loaddesc.textContent = I18n.retrying(); + this.initwebsocket().then(async () => { + console.log("FINE ME"); + this.loaduser(); + await this.init(); + const loading = document.getElementById("loading") as HTMLElement; + loading.classList.add("doneloading"); + loading.classList.remove("loading"); + loaddesc.textContent = I18n.loaded(); + console.log("done loading"); + }); + }, + 200 + this.errorBackoff * 2800, + ); + } else loaddesc.textContent = I18n.unableToConnect(); }); + console.log("here?"); await promise; + console.warn("huh"); + } + interNonceMap = new Map(); + registerInterNonce(nonce: string, thing: Message) { + this.interNonceMap.set(nonce, thing); + } + relationshipsUpdate = () => {}; + rights: Rights; + updateRights(rights: string | number) { + if (this.rights.isSameAs(rights)) return; + this.rights.update(rights); + this.perminfo.user.rights = rights; } - relationshipsUpdate=()=>{}; - async handleEvent(temp: wsjson){ - console.debug(temp); - if(temp.s)this.lastSequence = temp.s; - if(temp.op ===9&&this.ws){ - this.errorBackoff=0; + traceSub() { + SW.captureEvent("trace", (e) => { + this.handleTrace(e.trace); + }); + } + async handleEvent(temp: wsjson) { + if (temp.d._trace) this.handleTrace(temp.d._trace); + if (getDeveloperSettings().gatewayLogging) console.debug(temp); + if (temp.s) this.lastSequence = temp.s; + if (temp.op === 9 && this.ws) { + this.errorBackoff = 0; this.ws.close(4041); } - if(temp.op == 0){ - switch(temp.t){ - case"MESSAGE_CREATE": - if(this.initialized){ + if (temp.op == 0) { + switch (temp.t) { + case "THREAD_MEMBERS_UPDATE": { + const channel = this.channelids.get(temp.d.id); + if (!channel) return; + if (temp.d.added_members) { + for (const memb of temp.d.added_members) { + if (memb.user_id === this.user.id) { + channel.member = memb; + channel.parent?.createguildHTML(); + } else { + //TODO store these somewhere + } + } + } + if (temp.d.removed_member_ids) { + for (const id of temp.d.removed_member_ids) { + if (id === this.user.id) { + channel.member = undefined; + channel.parent?.createguildHTML(); + } else { + //TODO unstore these somewhere + } + } + } + break; + } + case "INTERACTION_FAILURE": + case "INTERACTION_CREATE": + case "INTERACTION_SUCCESS": + const m = this.interNonceMap.get(temp.d.nonce); + if (m) { + //Punt the events off to the message class + m.interactionEvents(temp); + } + break; + case "MESSAGE_CREATE": + if (this.initialized) { this.messageCreate(temp); } break; - case"MESSAGE_DELETE": { + case "MESSAGE_DELETE": { temp.d.guild_id ??= "@me"; const channel = this.channelids.get(temp.d.channel_id); - if(!channel)break; + if (!channel) break; const message = channel.messages.get(temp.d.id); - if(!message)break; + if (!message) break; message.deleteEvent(); break; } - case"READY": + case "READY": await this.gottenReady(temp as readyjson); break; - case"MESSAGE_UPDATE": { + case "MESSAGE_UPDATE": { temp.d.guild_id ??= "@me"; const channel = this.channelids.get(temp.d.channel_id); - if(!channel)break; + if (!channel) break; const message = channel.messages.get(temp.d.id); - if(!message)break; + if (!message) break; message.giveData(temp.d); break; } - case"TYPING_START": - if(this.initialized){ + case "TYPING_START": + if (this.initialized) { this.typingStart(temp); } break; - case"USER_UPDATE": - if(this.initialized){ + case "USER_UPDATE": + if (this.initialized) { const users = this.userMap.get(temp.d.id); - if(users){ + if (users) { users.userupdate(temp.d); } } break; - case"CHANNEL_UPDATE": - if(this.initialized){ + case "CHANNEL_PINS_UPDATE": + temp.d.guild_id ??= "@me"; + const channel = this.channelids.get(temp.d.channel_id); + if (!channel) break; + delete channel.pinnedMessages; + channel.lastpin = new Date() + ""; + const pinnedM = document.getElementById("pinnedMDiv"); + if (pinnedM) { + pinnedM.classList.add("unreadPin"); + } + break; + case "CHANNEL_UPDATE": + if (this.initialized) { this.updateChannel(temp.d); } break; - case"CHANNEL_CREATE": - if(this.initialized){ + case "CHANNEL_CREATE": + case "THREAD_CREATE": + if (this.initialized) { this.createChannel(temp.d); } break; - case"CHANNEL_DELETE": - if(this.initialized){ + case "CHANNEL_DELETE": + if (this.initialized) { this.delChannel(temp.d); } break; - case"GUILD_DELETE": { + case "GUILD_DELETE": { const guildy = this.guildids.get(temp.d.id); - if(guildy){ + if (guildy) { this.guildids.delete(temp.d.id); this.guilds.splice(this.guilds.indexOf(guildy), 1); guildy.html.remove(); + if (guildy === this.lookingguild) { + this.guildids.get("@me")?.loadGuild(); + this.guildids.get("@me")?.loadChannel(); + } } break; } - case"GUILD_CREATE": (async()=>{ - const guildy = new Guild(temp.d, this, this.user); - this.guilds.push(guildy); - this.guildids.set(guildy.id, guildy); - (document.getElementById("servers") as HTMLDivElement).insertBefore( - guildy.generateGuildIcon(), - document.getElementById("bottomseparator") - ); - - })(); - break; - case"MESSAGE_REACTION_ADD": + case "GUILD_UPDATE": { + const guildy = this.guildids.get(temp.d.id); + if (guildy) { + guildy.update(temp.d); + } + break; + } + case "GUILD_CREATE": + (async () => { + const guildy = new Guild(temp.d, this, this.user); + this.guilds.push(guildy); + this.guildids.set(guildy.id, guildy); + const divy = this.makeGuildIcon(guildy); + guildy.HTMLicon = divy; + (document.getElementById("servers") as HTMLDivElement).insertBefore( + divy, + document.getElementById("bottomseparator"), + ); + guildy.message_notifications = guildy.properties.default_message_notifications; + guildy.showWelcome(); + })(); + break; + case "MESSAGE_REACTION_ADD": { temp.d.guild_id ??= "@me"; const guild = this.guildids.get(temp.d.guild_id); - if(!guild)break; + if (!guild) break; const channel = this.channelids.get(temp.d.channel_id); - if(!channel)break; + if (!channel) break; const message = channel.messages.get(temp.d.message_id); - if(!message)break; - let thing: Member | { id: string }; - if(temp.d.member){ + if (!message) break; + let thing: Member | {id: string}; + if (temp.d.member) { thing = (await Member.new(temp.d.member, guild)) as Member; - }else{ - thing = { id: temp.d.user_id }; + } else { + thing = {id: temp.d.user_id}; } message.reactionAdd(temp.d.emoji, thing); } break; - case"MESSAGE_REACTION_REMOVE": + case "MESSAGE_REACTION_REMOVE": { temp.d.guild_id ??= "@me"; const channel = this.channelids.get(temp.d.channel_id); - if(!channel)break; + if (!channel) break; + const message = channel.messages.get(temp.d.message_id); - if(!message)break; + if (!message) break; + message.reactionRemove(temp.d.emoji, temp.d.user_id); } break; - case"MESSAGE_REACTION_REMOVE_ALL": + case "MESSAGE_REACTION_REMOVE_ALL": { temp.d.guild_id ??= "@me"; const channel = this.channelids.get(temp.d.channel_id); - if(!channel)break; + if (!channel) break; const message = channel.messages.get(temp.d.message_id); - if(!message)break; + if (!message) break; message.reactionRemoveAll(); } break; - case"MESSAGE_REACTION_REMOVE_EMOJI": + case "MESSAGE_REACTION_REMOVE_EMOJI": { temp.d.guild_id ??= "@me"; const channel = this.channelids.get(temp.d.channel_id); - if(!channel)break; + if (!channel) break; const message = channel.messages.get(temp.d.message_id); - if(!message)break; + if (!message) break; message.reactionRemoveEmoji(temp.d.emoji); } break; - case"GUILD_MEMBERS_CHUNK": + case "GUILD_MEMBERS_CHUNK": this.gotChunk(temp.d); break; - case"GUILD_MEMBER_LIST_UPDATE": - { - this.memberListUpdate(temp) + case "GUILD_MEMBER_LIST_UPDATE": { + this.memberListUpdate(temp); break; } + case "READY_SUPPLEMENTAL": + { + temp.d.guilds.forEach((_) => + _.voice_states.forEach((status) => { + if (this.voiceFactory && status.channel_id) { + this.voiceFactory.voiceStateUpdate(status); + console.log(status); + } + }), + ); + this.readysup = temp.d.guilds.length !== 0; + } + break; case "VOICE_STATE_UPDATE": - if(this.voiceFactory){ - this.voiceFactory.voiceStateUpdate(temp) + if (this.user.id === temp.d.user_id) { + this.mute = temp.d.self_mute; + this.updateMic(false); + } + if (this.voiceFactory) { + this.voiceFactory.voiceStateUpdate(temp.d); } break; + case "STREAM_SERVER_UPDATE": { + if (this.voiceFactory) { + this.voiceFactory.streamServerUpdate(temp); + } + break; + } + case "STREAM_CREATE": { + if (this.voiceFactory) { + this.voiceFactory.streamCreate(temp); + } + break; + } case "VOICE_SERVER_UPDATE": - if(this.voiceFactory){ - this.voiceFactory.voiceServerUpdate(temp) + if (this.voiceFactory) { + this.voiceFactory.voiceServerUpdate(temp); } break; - case "GUILD_ROLE_CREATE":{ - const guild=this.guildids.get(temp.d.guild_id); - if(!guild) break; + case "GUILD_ROLE_CREATE": { + const guild = this.guildids.get(temp.d.guild_id); + if (!guild) break; guild.newRole(temp.d.role); break; } - case "GUILD_ROLE_UPDATE":{ - const guild=this.guildids.get(temp.d.guild_id); - if(!guild) break; + case "GUILD_ROLE_UPDATE": { + const guild = this.guildids.get(temp.d.guild_id); + if (!guild) break; guild.updateRole(temp.d.role); break; } - case "GUILD_ROLE_DELETE":{ - const guild=this.guildids.get(temp.d.guild_id); - if(!guild) break; + case "GUILD_ROLE_DELETE": { + const guild = this.guildids.get(temp.d.guild_id); + if (!guild) break; guild.deleteRole(temp.d.role_id); break; } - case "GUILD_MEMBER_UPDATE":{ - const guild=this.guildids.get(temp.d.guild_id); - if(!guild) break; - guild.memberupdate(temp.d) - break + case "GUILD_MEMBER_UPDATE": { + const guild = this.guildids.get(temp.d.guild_id); + if (!guild) break; + guild.memberupdate(temp.d); + break; } - case "RELATIONSHIP_ADD":{ - const user = new User(temp.d.user, this); - user.nickname = null; - user.relationshipType = temp.d.type; - this.inrelation.add(user); - this.relationshipsUpdate(); + case "RELATIONSHIP_UPDATE": + case "RELATIONSHIP_ADD": { + (async () => { + const user = temp.d.user ? new User(temp.d.user, this) : await this.getUser(temp.d.id); + user.handleRelationship(temp.d); + this.relationshipsUpdate(); + const me = this.guildids.get("@me"); + if (!me) return; + me.unreads(); + })(); break; } - case "RELATIONSHIP_REMOVE":{ + case "RELATIONSHIP_REMOVE": { const user = this.userMap.get(temp.d.id); - if(!user) return; - user.nickname = null; - user.relationshipType = 0; - this.inrelation.delete(user); + if (!user) return; + user.removeRelation(); this.relationshipsUpdate(); break; } - case "PRESENCE_UPDATE":{ - if(temp.d.user){ + case "PRESENCE_UPDATE": { + if (temp.d.user) { + const user = new User(temp.d.user, this); this.presences.set(temp.d.user.id, temp.d); + user.setstatus(temp.d.status); + if (user === this.user) this.loaduser(); } break; } - case "GUILD_MEMBER_ADD":{ - const guild=this.guildids.get(temp.d.guild_id); - if(!guild) break; - Member.new(temp.d,guild); + case "GUILD_MEMBER_ADD": { + const guild = this.guildids.get(temp.d.guild_id); + if (!guild) break; + Member.new(temp.d, guild); break; } - case "GUILD_MEMBER_REMOVE":{ - const guild=this.guildids.get(temp.d.guild_id); - if(!guild) break; - const user=new User(temp.d.user,this); - const member=user.members.get(guild); - if(!(member instanceof Member)) break; + case "GUILD_MEMBER_REMOVE": { + const guild = this.guildids.get(temp.d.guild_id); + if (!guild) break; + const user = new User(temp.d.user, this); + const member = user.members.get(guild); + if (!(member instanceof Member)) break; member.remove(); break; } - default :{ - //@ts-ignore - console.warn("Unhandled case "+temp.t,temp); + case "GUILD_EMOJIS_UPDATE": { + const guild = this.guildids.get(temp.d.guild_id); + if (!guild) break; + guild.emojis = temp.d.emojis; + guild.onEmojiUpdate(guild.emojis); + break; + } + case "GUILD_STICKERS_UPDATE": { + const guild = this.guildids.get(temp.d.guild_id); + if (!guild) break; + guild.stickers = temp.d.stickers.map((_) => new Sticker(_, guild)); + guild.onStickerUpdate(guild.stickers); + break; + } + case "CHANNEL_RECIPIENT_REMOVE": { + const guild = this.guildids.get("@me") as Direct; + const channel = guild.channels.find(({id}) => id == temp.d.channel_id) as Group; + if (!channel) break; + channel.removeRec(new User(temp.d.user, this)); + break; + } + case "CHANNEL_RECIPIENT_ADD": { + const guild = this.guildids.get("@me") as Direct; + const channel = guild.channels.find(({id}) => id == temp.d.channel_id) as Group; + if (!channel) break; + channel.addRec(new User(temp.d.user, this)); + break; + } + case "MESSAGE_ACK": { + const channel = this.channelByID(temp.d.channel_id); + if (!channel) break; + channel.lastreadmessageid = temp.d.message_id; + channel.mentions = 0; + channel.unreads(); + channel.guild.unreads(); + break; } - } - - - }else if(temp.op === 10){ - if(!this.ws)return; + default: { + //@ts-expect-error + console.warn("Unhandled case " + temp.t, temp); + } + } + this.generateFavicon(); + } else if (temp.op === 10) { + if (!this.ws) return; console.log("heartbeat down"); this.heartbeat_interval = temp.d.heartbeat_interval; - this.ws.send(JSON.stringify({ op: 1, d: this.lastSequence })); - }else if(temp.op === 11){ - setTimeout((_: any)=>{ - if(!this.ws)return; - if(this.connectionSucceed === 0)this.connectionSucceed = Date.now(); - this.ws.send(JSON.stringify({ op: 1, d: this.lastSequence })); + this.ws.send(JSON.stringify({op: 1, d: this.lastSequence})); + } else if (temp.op === 11) { + setTimeout((_: any) => { + if (!this.ws) return; + const reasons = this.generateReasons(); + + if (this.connectionSucceed === 0) this.connectionSucceed = Date.now(); + this.ws.send( + JSON.stringify({ + op: 40, + d: {seq: this.lastSequence, qos: {ver: 27, active: !!reasons.length, reasons}}, + }), + ); }, this.heartbeat_interval); - }else{ - console.log("Unhandled case "+temp.d,temp); + } else { + console.log("Unhandled case " + temp.d, temp); + } + } + generateReasons() { + const reasons: string[] = []; + if (!document.hidden) reasons.push("foregrounded"); + return reasons; + } + setUserAudio(id: string, vol: number) { + if (!this.perminfo.userAudio) this.perminfo.userAudio = {}; + this.perminfo.userAudio[id] = vol; + const v = this.voiceFactory?.currentVoice; + if (v) { + const u = v.uVolMap.get(id); + if (u) u.volume = vol / 100; } } - get currentVoice(){ + getUserAudio(id: string) { + if (!this.perminfo.userAudio) this.perminfo.userAudio = {}; + + return (this.perminfo.userAudio[id] as number) ?? 100; + } + get currentVoice() { return this.voiceFactory?.currentVoice; } - async joinVoice(channel:Channel){ - if(!this.voiceFactory) return; - if(!this.ws) return; - this.ws.send(JSON.stringify(this.voiceFactory.joinVoice(channel.id,channel.guild.id))); - return undefined; + async getAudioDeviceList() { + return (await navigator.mediaDevices.enumerateDevices()).filter( + (dev) => dev.kind === "audioinput", + ); } - changeVCStatus(status:string){ - const statuselm=document.getElementById("VoiceStatus"); - if(!statuselm) throw new Error("Missing status element"); - statuselm.textContent=status; + async setNewDefualtDevice(id: string) { + const devices = await this.getAudioDeviceList(); + const d = devices.find((_) => _.deviceId === id); + if (!d) return; + const device = await navigator.mediaDevices.getUserMedia({ + audio: { + deviceId: {exact: d.deviceId}, + }, + }); + if (!device) return; + await this.voiceFactory?.currentVoice?.giveMicTrack(device); + this.perminfo.localuser.defaultAudio = id; } - handleVoice(){ - if(this.voiceFactory){ - this.voiceFactory.onJoin=voice=>{ - voice.onSatusChange=status=>{ - this.changeVCStatus(status); - } + getDefaultAudio() { + return this.perminfo.localuser.defaultAudio || "default"; + } + async joinVoice(channel: Channel) { + if (!this.voiceFactory) return; + if (!this.ws) return; + const v = this.voiceFactory.joinVoice(channel.id, channel.guild.id, this.mute); + if (this.perminfo.localuser.defaultAudio) { + const devices = await this.getAudioDeviceList(); + const d = devices.find((_) => _.deviceId === this.perminfo.localuser.defaultAudio); + if (d) { + navigator.mediaDevices + .getUserMedia({ + audio: { + deviceId: {exact: d.deviceId}, + }, + }) + .then((_) => v.giveMicTrack(_)); + return; + } + } + const d = await navigator.mediaDevices.getUserMedia({video: false, audio: true}); + this.perminfo.localuser.defaultAudio = d.getAudioTracks()[0]?.id; + v.giveMicTrack(d); + return undefined; + } + regenVoiceIcons = () => {}; + changeVCStatus(status: voiceStatusStr, channel: Channel) { + const statuselm = document.getElementById("VoiceStatus"); + const VoiceGuild = document.getElementById("VoiceGuild"); + const VoiceButtons = document.getElementById("VoiceButtons"); + if (!statuselm || !VoiceGuild || !VoiceButtons) throw new Error("Missing status element"); + + statuselm.textContent = I18n.Voice.status[status](); + const guildName = document.createElement("span"); + guildName.textContent = channel.guild.properties.name; + const channelName = document.createElement("span"); + channelName.textContent = channel.name; + VoiceGuild.innerHTML = ``; + VoiceGuild.append(guildName, " / ", channelName); + VoiceGuild.onclick = () => { + this.goToChannel(channel.id); + }; + + VoiceButtons.innerHTML = ""; + + const leave = document.createElement("div"); + const leaveIcon = document.createElement("span"); + leaveIcon.classList.add("svg-hangup"); + leave.append(leaveIcon); + + leave.onclick = () => { + channel.voice?.leave(); + }; + + const screenShare = document.createElement("div"); + const screenShareIcon = document.createElement("span"); + const updateStreamIcon = () => { + screenShareIcon.classList.remove("svg-stopstream", "svg-stream"); + if (channel.voice?.isLive()) { + screenShareIcon.classList.add("svg-stopstream"); + } else { + screenShareIcon.classList.add("svg-stream"); + } + }; + updateStreamIcon(); + + screenShare.append(screenShareIcon); + + screenShare.onclick = async () => { + if (channel.voice?.isLive()) { + channel.voice.stopStream(); + } else { + const stream = await navigator.mediaDevices.getDisplayMedia(); + await channel.voice?.createLive(stream); + } + updateStreamIcon(); + }; + + const video = document.createElement("div"); + const videoIcon = document.createElement("span"); + const updateVideoIconIcon = () => { + videoIcon.classList.remove("svg-novideo", "svg-video"); + if (this.voiceFactory?.video) { + videoIcon.classList.add("svg-video"); + } else { + videoIcon.classList.add("svg-novideo"); + } + }; + updateVideoIconIcon(); + + video.append(videoIcon); + + video.onclick = async () => { + if (this.voiceFactory?.video) { + channel.voice?.stopVideo(); + } else { + const cam = await navigator.mediaDevices.getUserMedia({ + video: { + advanced: [ + { + aspectRatio: 1.75, + }, + ], + }, + }); + if (!cam) return; + channel.voice?.startVideo(cam); } + updateVideoIconIcon(); + }; + + this.regenVoiceIcons = () => { + updateStreamIcon(); + updateVideoIconIcon(); + }; + VoiceButtons.append(leave, video, screenShare); + + const clear = () => { + statuselm.textContent = ""; + VoiceGuild.textContent = ""; + VoiceButtons.innerHTML = ""; + }; + const conSet = new Set(["notconnected"]); + if (conSet.has(status)) { + setTimeout(() => { + if (statuselm.textContent === I18n.Voice.status[status]()) { + clear(); + } + }, 2000); + } else if (status === "left") { + clear(); + } + } + handleVoice() { + if (this.voiceFactory) { + this.voiceFactory.onJoin = (voice) => { + voice.onSatusChange = (status) => { + let channel: Channel | undefined = undefined; + for (const guild of this.guilds) { + channel ||= guild.channels.find((_) => _.voice === voice); + } + if (channel) this.changeVCStatus(status, channel); + else console.error("Uh, no channel found?"); + }; + }; } } heartbeat_interval: number = 0; - updateChannel(json: channeljson): void{ - const guild = this.guildids.get(json.guild_id); - if(guild){ + updateChannel(json: channeljson): void { + const guild = this.guildids.get(json.guild_id || "@me"); + if (guild) { guild.updateChannel(json); - if(json.guild_id === this.lookingguild?.id){ + if (json.guild_id === this.lookingguild?.id) { this.loadGuild(json.guild_id); } } } - createChannel(json: channeljson): undefined | Channel{ + createChannel(json: channeljson): undefined | Channel { + const c = this.channelids.get(json.id); + if (c) { + c.updateChannel(json); + return c; + } json.guild_id ??= "@me"; const guild = this.guildids.get(json.guild_id); - if(!guild)return; + if (!guild) return; + if (guild.channels.find((_) => _.id === json.id)) return; const channel = guild.createChannelpac(json); - if(json.guild_id === this.lookingguild?.id){ - this.loadGuild(json.guild_id,true); + if (json.guild_id === this.lookingguild?.id) { + this.loadGuild(json.guild_id, true); } - if(channel.id === this.gotoid){ + if (channel.id === this.gotoid) { guild.loadGuild(); - guild.loadChannel(channel.id); - this.gotoid = undefined; + guild.loadChannel(channel.id).then(() => { + this.gotoRes(); + this.gotoRes = () => {}; + this.gotoid = undefined; + }); } return channel; // Add this line to return the 'channel' variable } - async memberListUpdate(list:memberlistupdatejson|void){ - const div=document.getElementById("sideDiv") as HTMLDivElement; - div.innerHTML=""; - if(!list) return; - const counts=new Map(); - const guild=this.lookingguild; - if(!guild) return; - const channel=this.channelfocus; - if(!channel) return; - for(const thing of list.d.ops[0].items){ - if("member" in thing){ - await Member.new(thing.member,guild); - }else{ - counts.set(thing.group.id,thing.group.count); - } - } - - const elms:Map=new Map([]); - for(const role of guild.roles){ - if(role.hoist){ - elms.set(role,[]); - } - } - elms.set("online",[]); - elms.set("offline",[]) - const members=new Set(guild.members); - members.forEach((member)=>{ - if(!channel.hasPermission("VIEW_CHANNEL",member)){ + listque = false; + memberListQue() { + if (this.listque) { + return; + } + this.listque = true; + setTimeout(async () => { + await this.memberListUpdate(); + this.listque = false; + }, 100); + } + async memberListUpdate(list: memberlistupdatejson | void) { + if (this.searching) return; + const guild = this.lookingguild; + if (!guild) return; + + const channel = this.channelfocus; + if (!channel) return; + if (channel.voice && this.voiceAllowed) { + const div = document.getElementById("sideDiv") as HTMLDivElement; + div.textContent = ""; + return; + } + if (guild.id === "@me" && (channel as Group).type === 1) { + const div = document.getElementById("sideDiv") as HTMLDivElement; + div.textContent = ""; + return; + } + + if (list) { + if (list.d.guild_id !== guild.id) { + return; + } + const counts = new Map(); + for (const thing of list.d.ops[0].items) { + if ("member" in thing) { + if (this.userMap.get(thing.member.id)?.members.has(guild)) continue; + await Member.new(thing.member, guild); + } else { + counts.set(thing.group.id, thing.group.count); + } + } + } + + const elms: Map = new Map([]); + for (const role of guild.roles) { + if (role.hoist) { + elms.set(role, []); + } + } + elms.set("online", []); + elms.set("offline", []); + let members = new Set(guild.members); + if (channel instanceof Group) { + members = new Set(channel.users); + members.add(this.user); + } + members.forEach((member) => { + if (member instanceof User) { + return; + } + if (!channel.hasPermission("VIEW_CHANNEL", member)) { members.delete(member); - console.log(member) + console.log(member, "can't see"); return; } - }) - for(const [role, list] of elms){ - members.forEach((member)=>{ - if(role === "offline"){ - if(member.user.status === "offline"){ + }); + + for (const [role, list] of elms) { + members.forEach((member) => { + const user = member instanceof Member ? member.user : member; + if (role === "offline") { + if (user.getStatus() === "offline" || user.getStatus() === "invisible") { list.push(member); members.delete(member); } return; } - if(member.user.status === "offline"){ + if (user.getStatus() === "offline" || user.getStatus() === "invisible") { return; } - if(role !== "online"&&member.hasRole(role.id)){ - list.push(member); - members.delete(member); + if (member instanceof Member) { + if (role !== "online" && member.hasRole(role.id)) { + list.push(member); + members.delete(member); + } } }); - if(!list.length) continue; - list.sort((a,b)=>{ - return (a.name.toLowerCase()>b.name.toLowerCase())?1:-1; + if (!list.length) continue; + list.sort((a, b) => { + return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1; }); } - const online=[...members]; - online.sort((a,b)=>{ - return (a.name.toLowerCase()>b.name.toLowerCase())?1:-1; + const online = [...members]; + online.sort((a, b) => { + return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1; }); - elms.set("online",online); - for(const [role, list] of elms){ - if(!list.length) continue; - const category=document.createElement("div"); - category.classList.add("memberList"); - let title=document.createElement("h3"); - if(role==="offline"){ - title.textContent=I18n.getTranslation("user.offline"); - category.classList.add("offline"); - }else if(role==="online"){ - title.textContent=I18n.getTranslation("user.online"); - }else{ - title.textContent=role.name; - } - category.append(title); - const membershtml=document.createElement("div"); - membershtml.classList.add("flexttb"); - - for(const member of list){ - const memberdiv=document.createElement("div"); - const pfp=await member.user.buildstatuspfp(); - const username=document.createElement("span"); - username.classList.add("ellipsis"); - username.textContent=member.name; - member.bind(username) - member.user.bind(memberdiv,member.guild,false); - memberdiv.append(pfp,username); - memberdiv.classList.add("flexltr","liststyle"); - membershtml.append(memberdiv); - } - category.append(membershtml); - div.append(category); - } - - console.log(elms); - } - async getSidePannel(){ - - if(this.ws&&this.channelfocus){ - console.log(this.channelfocus.guild.id); - if(this.channelfocus.guild.id==="@me"){ - this.memberListUpdate(); - return; - } - this.ws.send(JSON.stringify({ - d:{ - channels:{[this.channelfocus.id]:[[0,99]]}, - guild_id:this.channelfocus.guild.id - }, - op:14 - })) - }else{ - console.log("false? :3") - } + elms.set("online", online); + this.generateListHTML(elms, channel); } - gotoid: string | undefined; - async goToChannel(id: string,addstate=true){ - const channel = this.channelids.get(id); - if(channel){ - const guild = channel.guild; - guild.loadGuild(); - guild.loadChannel(id,addstate); - }else{ - this.gotoid = id; + roleListMap = new WeakMap< + HTMLDivElement, + { + role: Role | "offline" | "online"; + memberListMap: Map; } - } - delChannel(json: channeljson): void{ - let guild_id = json.guild_id; - guild_id ??= "@me"; - const guild = this.guildids.get(guild_id); - if(guild){ - guild.delChannel(json); + >(); + listGuild?: Guild; + generateListHTML(elms: Map, channel: Channel) { + const div = document.getElementById("sideDiv") as HTMLDivElement; + let roleMap = new Map< + Role | "offline" | "online", + {elm: HTMLDivElement; memberListMap: Map} + >(); + if (channel.guild !== this.listGuild) { + this.listGuild = channel.guild; + div.innerHTML = ""; } - - if(json.guild_id === this.lookingguild?.id){ - this.loadGuild(json.guild_id,true); + Array.from(div.children) + .map((_) => [this.roleListMap.get(_ as HTMLDivElement), _ as HTMLDivElement] as const) + .forEach(([role, elm]) => { + if (role && elms.get(role.role)?.length) { + if (document.contains(elm)) + roleMap.set(role.role, {elm, memberListMap: role.memberListMap}); + } else if (elm) { + elm.remove(); + } + }); + div.classList.remove("searchDiv"); + div.classList.remove("hideSearchDiv"); + let lastDiv: HTMLDivElement | void = undefined; + for (const [role, list] of elms) { + if (!list.length) continue; + + let category: HTMLDivElement; + let memberMap: Map; + const getF = roleMap.get(role); + roleMap.delete(role); + if (getF) { + category = getF.elm; + memberMap = getF.memberListMap; + if (lastDiv) { + const nextElm = lastDiv.nextElementSibling as HTMLElement | null; + if (nextElm !== category) { + lastDiv.after(category); + } + } else { + const first = div.firstElementChild; + if (first !== category) { + div.prepend(category); + } + } + } else { + category = document.createElement("div"); + category.classList.add("memberList"); + let title = document.createElement("h3"); + if (role === "offline") { + title.textContent = I18n.user.offline(); + category.classList.add("offline"); + } else if (role === "online") { + title.textContent = I18n.user.online(); + } else { + title.textContent = role.name; + } + category.append(title); + const membershtml = document.createElement("div"); + membershtml.classList.add("flexttb"); + category.append(membershtml); + memberMap = new Map(); + if (lastDiv) { + lastDiv.after(category); + } else { + div.prepend(category); + } + this.roleListMap.set(category, { + role, + memberListMap: memberMap, + }); + } + lastDiv = category; + const membershtml = category.getElementsByTagName("div")[0]; + const cur = new Set( + list.map((member) => { + return member instanceof Member ? member.user : member; + }), + ); + const userToHTMLMap = new Map(); + Array.from(membershtml.children) + .map((_) => [_ as HTMLElement, memberMap.get(_ as HTMLElement)] as const) + .forEach(([elm, memb]) => { + if (!memb || !cur.has(memb)) { + memberMap.delete(elm); + elm.remove(); + } else { + userToHTMLMap.set(memb, elm); + } + }); + const makeMemberDiv = (user: User, member: Member | User) => { + user.localstatusUpdate = () => { + this.memberListQue(); + }; + const memberdiv = document.createElement("div"); + memberMap.set(memberdiv, user); + const pfp = user.buildstatuspfp(channel); + const username = document.createElement("span"); + username.classList.add("ellipsis"); + username.textContent = member.name; + member.subName(username); + if (user.bot) { + const bot = document.createElement("span"); + bot.classList.add("bot"); + bot.textContent = I18n.bot(); + username.appendChild(bot); + } + + memberdiv.append(pfp, username); + if (channel instanceof Group) { + Group.groupMenu.bindContextmenu(memberdiv, channel, user); + if (channel.owner_id === user.id) { + const crown = document.createElement("span"); + crown.classList.add("svg-crown"); + memberdiv.append(crown); + } + } + member.bind(username); + user.bind(memberdiv, member instanceof Member ? member.guild : undefined, false); + + memberdiv.classList.add("flexltr", "liststyle", "memberListStyle"); + return memberdiv; + }; + + let lastElm: void | HTMLElement = void 0; + for (const member of list) { + const user = member instanceof Member ? member.user : member; + let elm = userToHTMLMap.get(user); + if (!elm) { + elm = makeMemberDiv(user, member); + if (!lastElm) { + membershtml.append(elm); + } else { + lastElm.after(elm); + } + } else if (lastElm) { + //@ts-expect-error TS Bug, let me know when it's fixed :3 + // https://github.com/microsoft/TypeScript/issues/62872 + const nextElm = lastElm.nextElementSibling; + if (nextElm !== elm) { + lastElm.after(elm); + } + } else { + const first = membershtml.firstElementChild; + if (first !== elm) { + membershtml.prepend(elm); + } + } + + lastElm = elm; + } + } + } + emojiPicker(x: number, y: number, guildEmojis = true) { + return Emoji.emojiPicker(x, y, guildEmojis ? this : undefined); + } + async getSidePannel() { + if (this.ws && this.channelfocus) { + console.log(this.channelfocus.guild.id); + this.memberListQue(); + if (this.channelfocus.guild.id === "@me") { + return; + } + if (!this.channelfocus.visible) return; + this.ws.send( + JSON.stringify({ + d: { + channels: {[this.channelfocus.id]: [[0, 99]]}, + guild_id: this.channelfocus.guild.id, + }, + op: 14, + }), + ); + } else { + console.log("false? :3"); + } + } + async goToState(state: [string, string | undefined, string | undefined]) { + const [guildid, channelid, messageid] = state; + if (!channelid) { + if (guildid === "@me") { + const dir = this.guildids.get("@me") as Direct; + dir.loadChannel(null, false); + } + return; + } + this.goToChannel(channelid, false, messageid); + } + + gotoid: string | undefined; + gotoRes = () => {}; + async goToChannel(channelid: string, addstate = true, messageid: undefined | string = undefined) { + const channel = this.channelids.get(channelid); + if (channel) { + const guild = channel.guild; + guild.loadGuild(); + await guild.loadChannel(channelid, addstate, messageid); + } else { + this.gotoid = channelid; + return new Promise((res) => (this.gotoRes = res)); + } + } + delChannel(json: channeljson): void { + let guild_id = json.guild_id; + guild_id ??= "@me"; + const guild = this.guildids.get(guild_id); + if (guild) { + guild.delChannel(json); + } + + if (json.guild_id === this.lookingguild?.id) { + this.loadGuild(json.guild_id, true); } } - init(): void{ + async init() { const location = window.location.href.split("/"); this.buildservers(); - if(location[3] === "channels"){ + this.generateFavicon(); + if (location[3] === "channels") { const guild = this.loadGuild(location[4]); - if(!guild){ + if (!guild) { return; } - guild.loadChannel(location[5]); + await guild.loadChannel(location[5], true, location[6]); this.channelfocus = this.channelids.get(location[5]); } } - loaduser(): void{ + loaduser(): void { (document.getElementById("username") as HTMLSpanElement).textContent = this.user.username; (document.getElementById("userpfp") as HTMLImageElement).src = this.user.getpfpsrc(); (document.getElementById("status") as HTMLSpanElement).textContent = this.status; } - isAdmin(): boolean{ - if(this.lookingguild){ + isAdmin(): boolean { + if (this.lookingguild) { return this.lookingguild.isAdmin(); - }else{ + } else { return false; } } - loadGuild(id: string,forceReload=false): Guild | undefined{ + + loadGuild(id: string, forceReload = false): Guild | undefined { + this.searching = false; let guild = this.guildids.get(id); - if(!guild){ + if (!guild) { guild = this.guildids.get("@me"); } console.log(forceReload); - if((!forceReload)&&(this.lookingguild === guild)){ + if (!forceReload && this.lookingguild === guild) { return guild; } - if(this.channelfocus){ + if (this.channelfocus && this.lookingguild !== guild) { this.channelfocus.infinite.delete(); this.channelfocus = undefined; } - if(this.lookingguild){ + if (this.lookingguild) { this.lookingguild.html.classList.remove("serveropen"); } - if(!guild)return; - if(guild.html){ + if (!guild) return; + if (guild.html) { guild.html.classList.add("serveropen"); } this.lookingguild = guild; (document.getElementById("serverName") as HTMLElement).textContent = guild.properties.name; + const banner = document.getElementById("servertd"); + console.log(guild.banner, banner); + if (banner) { + if (guild.banner) { + //https://cdn.discordapp.com/banners/677271830838640680/fab8570de5bb51365ba8f36d7d3627ae.webp?size=240 + banner.style.setProperty( + "background-image", + `linear-gradient(rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0) 40%), url(${this.info.cdn}/banners/${guild.id}/${guild.banner + new CDNParams({expectedSize: 128})})`, + ); + banner.classList.add("Banner"); + //background-image: + } else { + banner.style.removeProperty("background-image"); + banner.classList.remove("Banner"); + } + if (guild.id !== "@me") { + banner.style.setProperty("cursor", `pointer`); + banner.onclick = (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + const box = banner.getBoundingClientRect(); + Guild.contextmenu.makemenu(box.left + 16, box.bottom + 5, guild, undefined); + }; + } else { + banner.style.removeProperty("cursor"); + banner.onclick = () => {}; + } + } //console.log(this.guildids,id) const channels = document.getElementById("channels") as HTMLDivElement; channels.innerHTML = ""; @@ -840,7 +1713,302 @@ class Localuser{ channels.appendChild(html); return guild; } - buildservers(): void{ + dragMap = new WeakMap< + HTMLElement, + | Guild + | { + guilds: Guild[]; + color?: number | null; + name: string; + id: number; + } + >(); + dragged?: HTMLElement; + makeGuildDragable( + elm: HTMLElement, + thing: + | Guild + | { + guilds: Guild[]; + color?: number | null; + name: string; + id: number; + }, + ) { + this.dragMap.set(elm, thing); + elm.addEventListener("dragstart", (e) => { + this.dragged = elm; + e.stopImmediatePropagation(); + }); + elm.addEventListener("dragend", () => { + delete this.dragged; + }); + + elm.style.position = "relative"; + elm.draggable = true; + elm.addEventListener("dragenter", (event) => { + console.log("enter"); + event.preventDefault(); + event.stopImmediatePropagation(); + }); + const guildWithin = (guild?: Guild) => { + return !this.guildOrder.find((_) => _ === guild); + }; + elm.addEventListener("dragover", (event) => { + event.stopImmediatePropagation(); + const thingy = this.dragMap.get(this.dragged as HTMLElement); + if (!thingy) return; + if (this.dragged === elm) return; + if (!(thingy instanceof Guild) && guildWithin(thing as Guild)) return; + const height = elm.getBoundingClientRect().height; + if (event.offsetY < 0.3 * 48) { + elm.classList.add("dragTopView"); + elm.classList.remove("dragFolderView"); + elm.classList.remove("dragBottomView"); + } else if (height - event.offsetY < 0.3 * 48) { + elm.classList.remove("dragTopView"); + elm.classList.remove("dragFolderView"); + elm.classList.add("dragBottomView"); + } else { + elm.classList.remove("dragTopView"); + elm.classList.remove("dragBottomView"); + if (thingy instanceof Guild && (!(thing instanceof Guild) || !guildWithin(thing))) { + elm.classList.add("dragFolderView"); + } + } + event.preventDefault(); + }); + elm.addEventListener("dragleave", () => { + elm.classList.remove("dragFolderView"); + elm.classList.remove("dragTopView"); + elm.classList.remove("dragBottomView"); + }); + elm.addEventListener("drop", async (event) => { + event.stopImmediatePropagation(); + if (!this.dragged) return; + const drag = this.dragged; + const thingy = this.dragMap.get(this.dragged); + if (!thingy) return; + elm.classList.remove("dragFolderView"); + elm.classList.remove("dragTopView"); + elm.classList.remove("dragBottomView"); + + if (!(thingy instanceof Guild) && guildWithin(thing as Guild)) return; + if (this.dragged === elm) return; + + const height = elm.getBoundingClientRect().height; + let arr = this.guildOrder; + console.warn(arr === this.guildOrder); + let found = arr.find((elm) => { + if (elm === thing) { + return true; + } + if (!(elm instanceof Guild) && elm.guilds.find((_) => _ === thing)) { + return true; + } + return false; + }); + if (found && "guilds" in found && found !== thing) arr = found.guilds; + console.warn(arr === this.guildOrder); + const removeThingy = (thing = thingy) => { + const index = this.guildOrder.indexOf(thing); + if (-1 !== index) this.guildOrder.splice(index, 1); + this.guildOrder.forEach((_) => { + if (!(_ instanceof Guild)) { + const index = _.guilds.indexOf(thing as Guild); + if (-1 !== index) _.guilds.splice(index, 1); + } + }); + }; + console.log(arr, found); + if (event.offsetY < 0.3 * 48) { + removeThingy(); + elm.before(this.dragged); + const index = arr.indexOf(thing); + arr.splice(index, 0, thingy); + } else if (height - event.offsetY < 0.3 * 48) { + removeThingy(); + elm.after(this.dragged); + arr.splice(arr.indexOf(thing) + 1, 0, thingy); + } else if (thingy instanceof Guild && (!(thing instanceof Guild) || !guildWithin(thing))) { + if (thing instanceof Guild) { + await new Promise((res) => { + const dia = new Dialog(I18n.folder.create()); + const opt = dia.options; + const color = opt.addColorInput(I18n.folder.color(), () => {}); + const name = opt.addTextInput(I18n.folder.name(), () => {}); + opt.addButtonInput("", I18n.submit(), () => { + removeThingy(); + + let id = 1; + while (this.guildOrder.find((_) => _.id === id)) { + id++; + } + const folder = { + color: +("0x" + (color.value || "#0").split("#")[1]), + name: name.value, + guilds: [thing, thingy], + id, + }; + + this.guildOrder.splice(this.guildOrder.indexOf(thing), 1, folder); + const hold = document.createElement("hr"); + elm.after(hold); + hold.after( + this.makeFolder( + folder, + new Map([ + [thing, elm], + [thingy, drag], + ]), + ), + ); + hold.remove(); + + dia.hide(); + res(); + }); + dia.show(); + }); + } else { + removeThingy(); + thing.guilds.push(thingy); + elm.append(drag); + } + } + console.log(this.guildOrder); + this.guildOrder = this.guildOrder.filter((folder) => { + if (folder instanceof Guild) return true; + if (folder.guilds.length === 0) { + const servers = document.getElementById("servers"); + if (servers) + Array.from(servers.children).forEach((_) => { + const html = _ as HTMLElement; + if (this.dragMap.get(html) === folder) html.remove(); + }); + return false; + } + return true; + }); + await this.saveGuildOrder(); + }); + } + async saveGuildOrder() { + const guild_folders: guildFolder[] = this.guildOrder.map((elm) => { + if (elm instanceof Guild) { + return { + id: null, + name: null, + guild_ids: [elm.id], + color: null, + }; + } else { + return { + id: elm.id, + name: elm.name, + guild_ids: elm.guilds.map((guild) => guild.id), + color: elm.color, + }; + } + }); + + await fetch(this.info.api + "/users/@me/settings", { + method: "PATCH", + headers: this.headers, + body: JSON.stringify({guild_folders}), + }); + } + makeGuildIcon(guild: Guild) { + const divy = guild.generateGuildIcon(); + guild.HTMLicon = divy; + this.makeGuildDragable(divy, guild); + return divy; + } + makeFolder( + folder: {color?: number | null; id: number; name: string; guilds: Guild[]}, + icons = new Map(), + ) { + const folderDiv = document.createElement("div"); + folderDiv.classList.add("folder-div"); + const iconDiv = document.createElement("div"); + iconDiv.classList.add("folder-icon-div"); + const icon = document.createElement("span"); + icon.classList.add("svg-folder"); + + const menu = new Contextmenu(""); + menu.addButton(I18n.folder.edit(), () => { + const dio = new Dialog(I18n.folder.edit()); + const opt = dio.options; + const name = opt.addTextInput(I18n.folder.name(), () => {}, { + initText: folder.name, + }); + const color = opt.addColorInput(I18n.folder.color(), () => {}, { + initColor: "#" + (folder.color || 0).toString(16), + }); + opt.addButtonInput("", I18n.submit(), async () => { + folder.name = name.value; + folder.color = +("0x" + (color.value || "#0").split("#")[1]); + icon.style.setProperty("--folder-color", "#" + folder.color.toString(16).padStart(6, "0")); + if (!folder.color) icon.style.removeProperty("--folder-color"); + await this.saveGuildOrder(); + dio.hide(); + }); + dio.show(); + }); + + menu.bindContextmenu(iconDiv); + if (folder.color !== null && folder.color !== undefined) { + icon.style.setProperty("--folder-color", "#" + folder.color.toString(16).padStart(6, "0")); + if (!folder.color) icon.style.removeProperty("--folder-color"); + } + iconDiv.append(icon); + const divy = document.createElement("div"); + divy.append( + ...folder.guilds.map((guild) => { + const icon = icons.get(guild); + if (icon) return icon; + return this.makeGuildIcon(guild); + }), + ); + divy.classList.add("guilds-div-folder"); + folderDiv.append(iconDiv, divy); + let height = -1; + const toggle = async (fast = false) => { + if (height === -1) { + divy.style.overflow = "clip"; + height = divy.getBoundingClientRect().height; + divy.style.height = height + "px"; + await new Promise((res) => requestAnimationFrame(res)); + divy.style.height = "0px"; + this.perminfo.folderStates[folder.id].state = true; + } else { + divy.style.height = height + "px"; + if (!fast) await new Promise((res) => setTimeout(res, 200)); + divy.style.height = "unset"; + height = -1; + divy.style.overflow = "unset"; + this.perminfo.folderStates[folder.id].state = false; + } + }; + iconDiv.onclick = () => toggle(); + this.perminfo.folderStates ??= {}; + this.perminfo.folderStates[folder.id] ??= {}; + if (this.perminfo.folderStates[folder.id].state === true) { + toggle(true); + } + this.makeGuildDragable(folderDiv, folder); + return folderDiv; + } + guildOrder: ( + | Guild + | { + guilds: Guild[]; + color?: number | null; + name: string; + id: number; + } + )[] = []; + buildservers(): void { const serverlist = document.getElementById("servers") as HTMLDivElement; // const outdiv = document.createElement("div"); const home: any = document.createElement("span"); @@ -848,7 +2016,6 @@ class Localuser{ div.classList.add("home", "servericon"); home.classList.add("svgicon", "svg-home"); - home.all = this.guildids.get("@me"); (this.guildids.get("@me") as Guild).html = outdiv; const unread = document.createElement("div"); unread.classList.add("unread"); @@ -858,9 +2025,11 @@ class Localuser{ outdiv.classList.add("servernoti"); serverlist.append(outdiv); - home.onclick = function(){ - this.all.loadGuild(); - this.all.loadChannel(); + home.onclick = () => { + const guild = this.guildids.get("@me"); + if (!guild) return; + guild.loadGuild(); + guild.loadChannel(); }; const sentdms = document.createElement("div"); sentdms.classList.add("sentdms"); @@ -870,14 +2039,57 @@ class Localuser{ const br = document.createElement("hr"); br.classList.add("lightbr"); serverlist.appendChild(br); - for(const thing of this.guilds){ - if(thing instanceof Direct){ - (thing as Direct).unreaddms(); - continue; + const guilds = new Set(this.guilds); + const dirrect = this.guilds.find((_) => _ instanceof Direct) as Direct; + + guilds.delete(dirrect); + const folders = this.guildFolders + .map((folder) => { + return { + guilds: folder.guild_ids + .map((id) => { + const guild = this.guildids.get(id); + if (!guild) { + console.error(`guild ${id} does not exist`); + return; + } + if (!guilds.has(guild)) { + console.error(`guild ${id} is already in a folder`); + return; + } + guilds.delete(guild); + return guild; + }) + .filter((_) => _ !== undefined), + color: folder.color, + name: folder.name || "", + id: folder.id || 0, + }; + }) + .filter((_) => { + if (_.guilds.length === 0) { + console.error("empty folder depected"); + return false; + } + return true; + }) + .map((folder) => { + if (!folder.id && folder.guilds.length === 1) { + return folder.guilds[0]; + } + return folder; + }); + const guildOrder = [...guilds, ...folders]; + this.guildOrder = guildOrder; + for (const thing of guildOrder) { + if (thing instanceof Guild) { + serverlist.append(this.makeGuildIcon(thing)); + } else { + const folderDiv = this.makeFolder(thing); + serverlist.append(folderDiv); } - const divy = thing.generateGuildIcon(); - serverlist.append(divy); } + { const br = document.createElement("hr"); br.classList.add("lightbr"); @@ -890,7 +2102,7 @@ class Localuser{ div.classList.add("home", "servericon"); div.appendChild(plus); serverlist.appendChild(div); - div.onclick = _=>{ + div.onclick = (_) => { this.createGuild(); }; const guilddsdiv = document.createElement("div"); @@ -899,54 +2111,127 @@ class Localuser{ guilddsdiv.classList.add("home", "servericon"); guilddsdiv.appendChild(guildDiscoveryContainer); serverlist.appendChild(guilddsdiv); - guildDiscoveryContainer.addEventListener("click", ()=>{ + guildDiscoveryContainer.addEventListener("click", () => { this.guildDiscovery(); }); } this.unreads(); + dirrect.unreaddms(); } - createGuild(){ - - const full=new Dialog(""); - const buttons=full.options.addButtons("",{top:true}); - const viacode=buttons.add(I18n.getTranslation("invite.joinUsing")); + passTemplateID(id: string) { + this.createGuild(id); + } + createGuild(templateID?: string) { + const full = new Dialog(""); + const buttons = full.options.addButtons("", {top: true}); + const viacode = buttons.add(I18n.invite.joinUsing()); { - const form=viacode.addForm("",async (e: any)=>{ + const form = viacode.addForm("", async (e: any) => { let parsed = ""; - if(e.code.includes("/")){ + if (e.code.includes("/")) { parsed = e.code.split("/")[e.code.split("/").length - 1]; - }else{ + } else { parsed = e.code; } - const json=await (await fetch(this.info.api + "/invites/" + parsed, { - method: "POST", - headers: this.headers, - })).json() - if(json.message){ - throw new FormError(text,json.message); + const json = await ( + await fetch(this.info.api + "/invites/" + parsed, { + method: "POST", + headers: this.headers, + }) + ).json(); + if (json.message) { + throw new FormError(text, json.message); } full.hide(); }); - const text=form.addTextInput(I18n.getTranslation("invite.inviteLinkCode"),"code"); + const text = form.addTextInput(I18n.invite.inviteLinkCode(), "code"); } - const guildcreate=buttons.add(I18n.getTranslation("guild.create")); + const guildcreate = buttons.add(I18n.guild.create()); { - const form=guildcreate.addForm("",(fields:any)=>{ - this.makeGuild(fields).then(_=>{ - if(_.message){ + const form = guildcreate.addForm("", (fields: any) => { + this.makeGuild(fields).then((_) => { + if (_.message) { + loading.hide(); + full.show(); alert(_.errors.name._errors[0].message); - }else{ + } else { + loading.hide(); full.hide(); } }); }); - form.addFileInput(I18n.getTranslation("guild.icon:"),"icon",{files:"one"}); - form.addTextInput(I18n.getTranslation("guild.name:"),"name",{required:true}); + form.addImageInput(I18n.guild["icon:"](), "icon", { + clear: true, + }); + form.addTextInput(I18n.guild["name:"](), "name", {required: true}); + const loading = new Dialog(""); + loading.float.options.addTitle(I18n.guild.creating()); + form.onFormError = () => { + loading.hide(); + full.show(); + }; + form.addPreprocessor(() => { + loading.show(); + full.hide(); + }); + } + const guildcreateFromTemplate = buttons.add(I18n.guild.createFromTemplate()); + { + const form = guildcreateFromTemplate.addForm( + "", + (_: any) => { + if (_.message) { + loading.hide(); + full.show(); + alert(_.message); + const htmlarea = buttons.htmlarea.deref(); + if (htmlarea) buttons.generateHTMLArea(guildcreateFromTemplate, htmlarea); + } else { + loading.hide(); + full.hide(); + } + }, + { + method: "POST", + headers: this.headers, + }, + ); + const template = form.addTextInput(I18n.guild.template(), "template", { + initText: templateID || "", + }); + form.addImageInput(I18n.guild["icon:"](), "icon", {files: "one", clear: true}); + form.addTextInput(I18n.guild["name:"](), "name", {required: true}); + const loading = new Dialog(""); + loading.float.options.addTitle(I18n.guild.creating()); + form.onFormError = () => { + loading.hide(); + full.show(); + }; + form.addPreprocessor((e) => { + loading.show(); + full.hide(); + if ("template" in e) delete e.template; + let code: string; + if (URL.canParse(template.value) && new URL(template.value).protocol.startsWith("http")) { + const url = new URL(template.value); + code = url.pathname.split("/").at(-1) as string; + if (url.host === "discord.com" || url.host === "discord.new") { + code = "discord:" + code; + } + } else { + code = template.value; + } + form.fetchURL = this.info.api + "/guilds/templates/" + code; + }); } full.show(); + if (templateID) { + const htmlarea = buttons.htmlarea.deref(); + if (htmlarea) buttons.generateHTMLArea(guildcreateFromTemplate, htmlarea); + } } - async makeGuild(fields: { name: string; icon: string | null }){ + async makeGuild(fields: {name: string; icon: string | null}) { return await ( await fetch(this.info.api + "/guilds", { method: "POST", @@ -955,101 +2240,97 @@ class Localuser{ }) ).json(); } - async guildDiscovery(){ - const content = document.createElement("div"); - content.classList.add("flexttb","guildy"); - content.textContent = I18n.getTranslation("guild.loadingDiscovery"); - const full = new Dialog(""); - full.options.addHTMLArea(content); - full.show(); - - const res = await fetch(this.info.api + "/discoverable-guilds?limit=50", { - headers: this.headers, - }); - const json = await res.json(); - - content.innerHTML = ""; - const title = document.createElement("h2"); - title.textContent = I18n.getTranslation("guild.disoveryTitle",json.guilds.length+""); - content.appendChild(title); - - const guilds = document.createElement("div"); - guilds.id = "discovery-guild-content"; - - json.guilds.forEach((guild: guildjson["properties"])=>{ - const content = document.createElement("div"); - content.classList.add("discovery-guild"); - - if(guild.banner){ - const banner = document.createElement("img"); - banner.classList.add("banner"); - banner.crossOrigin = "anonymous"; - banner.src =this.info.cdn +"/icons/" +guild.id +"/" +guild.banner +".png?size=256"; - banner.alt = ""; - content.appendChild(banner); - } - - const nameContainer = document.createElement("div"); - nameContainer.classList.add("flex"); - const img = document.createElement("img"); - img.classList.add("icon"); - img.crossOrigin = "anonymous"; - img.src = - this.info.cdn + - (guild.icon - ? "/icons/" + guild.id + "/" + guild.icon + ".png?size=48" - : "/embed/avatars/3.png"); - img.alt = ""; - nameContainer.appendChild(img); - - const name = document.createElement("h3"); - name.textContent = guild.name; - nameContainer.appendChild(name); - content.appendChild(nameContainer); - const desc = document.createElement("p"); - desc.textContent = guild.description; - content.appendChild(desc); - - content.addEventListener("click", async ()=>{ - const joinRes = await fetch( - this.info.api + "/guilds/" + guild.id + "/members/@me", - { - method: "PUT", - headers: this.headers, - } - ); - if(joinRes.ok) full.hide(); - }); - guilds.appendChild(content); - }); - content.appendChild(guilds); + async guildDiscovery() { + this.guildids.get("@me")?.loadChannel("discover"); } - messageCreate(messagep: messageCreateJson): void{ + messageCreate(messagep: messageCreateJson): void { messagep.d.guild_id ??= "@me"; const channel = this.channelids.get(messagep.d.channel_id); - if(channel){ + if (channel) { channel.messageCreate(messagep); this.unreads(); } } - unreads(): void{ - for(const thing of this.guilds){ - if(thing.id === "@me"){ + unreads(): void { + for (const thing of this.guilds) { + if (thing.id === "@me") { + thing.unreads(); continue; } const html = this.guildhtml.get(thing.id); thing.unreads(html); } } - async typingStart(typing: startTypingjson): Promise{ + static favC = document.createElement("canvas"); + static favCTX = this.favC.getContext("2d") as CanvasRenderingContext2D; + static favImg = this.getFaviconImg(); + static getFaviconImg() { + const img = document.createElement("img"); + img.src = "/logo.webp"; + return img; + } + last = "-1"; + generateFavicon() { + const make = () => { + const favicon = document.getElementById("favicon") as HTMLLinkElement; + + let text = this.totalMentions() + ""; + if (this.last === text) return; + this.last = text; + if (text === "0") { + favicon.href = "/favicon.ico"; + return; + } + if (+text > 99) text = "+99"; + + const c = Localuser.favC; + c.width = 256; + c.height = 256; + const ctx = Localuser.favCTX; + ctx.drawImage(Localuser.favImg, 0, 0, c.width, c.height); + ctx.fillStyle = "#F00"; + const pos = 0.675; + + ctx.beginPath(); + ctx.arc(c.width * pos, c.height * pos, c.width * (1 - pos), 0, 2 * Math.PI); + ctx.fill(); + + ctx.fillStyle = "#FFF"; + + ctx.font = `bolder ${text.length === 1 ? 150 : 100}px sans-serif`; + + const messure = ctx.measureText(text); + const height = messure.fontBoundingBoxAscent + messure.fontBoundingBoxDescent; + ctx.fillText(text, c.width * pos - messure.width / 2, c.height * pos + height / 2 - 25); + + favicon.href = c.toDataURL("image/x-icon"); + }; + if (Localuser.favImg.complete) { + make(); + } + Localuser.favImg.onload = () => { + make(); + }; + } + totalMentions() { + let sum = 0; + for (const guild of this.guilds) { + sum += guild.mentions; + } + for (const channel of (this.guildids.get("@me") as Direct).channels) { + sum += channel.mentions; + } + return sum; + } + async typingStart(typing: startTypingjson): Promise { const channel = this.channelids.get(typing.d.channel_id); - if(!channel)return; + if (!channel) return; channel.typingStart(typing); } - updatepfp(file: Blob): void{ + updatepfp(file: Blob): void { const reader = new FileReader(); reader.readAsDataURL(file); - reader.onload = ()=>{ + reader.onload = () => { fetch(this.info.api + "/users/@me", { method: "PATCH", headers: this.headers, @@ -1059,11 +2340,11 @@ class Localuser{ }); }; } - updatebanner(file: Blob | null): void{ - if(file){ + updatebanner(file: Blob | null): void { + if (file) { const reader = new FileReader(); reader.readAsDataURL(file); - reader.onload = ()=>{ + reader.onload = () => { fetch(this.info.api + "/users/@me", { method: "PATCH", headers: this.headers, @@ -1072,7 +2353,7 @@ class Localuser{ }), }); }; - }else{ + } else { fetch(this.info.api + "/users/@me", { method: "PATCH", headers: this.headers, @@ -1082,28 +2363,38 @@ class Localuser{ }); } } - updateProfile(json: { - bio?: string; - pronouns?: string; - accent_color?: number; - }){ + updateProfile(json: {bio?: string; pronouns?: string; accent_color?: number}) { fetch(this.info.api + "/users/@me/profile", { method: "PATCH", headers: this.headers, body: JSON.stringify(json), }); } - async showusersettings(){ - const settings = new Settings(I18n.getTranslation("localuser.settings")); + async getPosts() { + return (await (await fetch("https://blog.fermi.chat/feed_json_created.json")).json()) as { + items: { + url: string; + title: string; + content_html: string; + image: null | string; + }[]; + }; + } + async showusersettings() { + const prefs = await getPreferences(); + const localSettings = getLocalSettings(); + const settings = new Settings(I18n.localuser.settings()); { - const userOptions = settings.addButton(I18n.getTranslation("localuser.userSettings"), { ltr: true }); + const userOptions = settings.addButton(I18n.localuser.userSettings(), { + ltr: true, + }); const hypotheticalProfile = document.createElement("div"); let file: undefined | File | null; let newpronouns: string | undefined; let newbio: string | undefined; const hypouser = this.user.clone(); let color: string; - async function regen(){ + async function regen() { hypotheticalProfile.textContent = ""; const hypoprofile = await hypouser.buildprofile(-1, -1); @@ -1114,24 +2405,25 @@ class Localuser{ const settingsRight = userOptions.addOptions(""); settingsRight.addHTMLArea(hypotheticalProfile); - const finput = settingsLeft.addFileInput( - I18n.getTranslation("uploadPfp"), - _=>{ - if(file){ + const finput = settingsLeft.addImageInput( + I18n.uploadPfp(), + (_) => { + if (file) { this.updatepfp(file); } }, - { clear: true } + {clear: true, initImg: this.user.getpfpsrc()}, ); - finput.watchForChange(_=>{ - if(!_){ + finput.img.classList.add("pfp"); + finput.watchForChange((_) => { + if (!_) { file = null; hypouser.avatar = null; hypouser.hypotheticalpfp = true; regen(); return; } - if(_.length){ + if (_.length) { file = _[0]; const blob = URL.createObjectURL(file); hypouser.avatar = blob; @@ -1140,24 +2432,29 @@ class Localuser{ } }); let bfile: undefined | File | null; - const binput = settingsLeft.addFileInput( - I18n.getTranslation("uploadBanner"), - _=>{ - if(bfile !== undefined){ + const binput = settingsLeft.addImageInput( + I18n.uploadBanner(), + (_) => { + if (bfile !== undefined) { this.updatebanner(bfile); } }, - { clear: true } + { + clear: true, + width: 96 * 3, + initImg: this.user.banner ? this.user.getBannerUrl() : "", + objectFit: "cover", + }, ); - binput.watchForChange(_=>{ - if(!_){ + binput.watchForChange((_) => { + if (!_) { bfile = null; hypouser.banner = undefined; hypouser.hypotheticalbanner = true; regen(); return; } - if(_.length){ + if (_.length) { bfile = _[0]; const blob = URL.createObjectURL(bfile); hypouser.banner = blob; @@ -1167,133 +2464,225 @@ class Localuser{ }); let changed = false; const pronounbox = settingsLeft.addTextInput( - I18n.getTranslation("pronouns"), - _=>{ - if(newpronouns || newbio || changed){ + I18n.pronouns(), + (_) => { + if (newpronouns !== undefined || newbio !== undefined || changed !== undefined) { this.updateProfile({ pronouns: newpronouns, bio: newbio, - accent_color: Number.parseInt("0x" + color.substr(1), 16), + accent_color: Number.parseInt("0x" + color.substring(1), 16), }); } }, - { initText: this.user.pronouns } + {initText: this.user.pronouns}, ); - pronounbox.watchForChange(_=>{ + pronounbox.watchForChange((_) => { hypouser.pronouns = _; newpronouns = _; regen(); }); - const bioBox = settingsLeft.addMDInput(I18n.getTranslation("bio"), _=>{}, { + const bioBox = settingsLeft.addMDInput(I18n.bio(), (_) => {}, { initText: this.user.bio.rawString, }); - bioBox.watchForChange(_=>{ + bioBox.watchForChange((_) => { newbio = _; hypouser.bio = new MarkDown(_, this); regen(); }); - if(this.user.accent_color){ + if (this.user.accent_color) { color = "#" + this.user.accent_color.toString(16); - }else{ + } else { color = "transparent"; } - const colorPicker = settingsLeft.addColorInput( - I18n.getTranslation("profileColor"), - _=>{}, - { initColor: color } - ); - colorPicker.watchForChange(_=>{ + const colorPicker = settingsLeft.addColorInput(I18n.profileColor(), (_) => {}, { + initColor: color, + }); + + colorPicker.watchForChange((_) => { console.log(); color = _; - hypouser.accent_color = Number.parseInt("0x" + _.substr(1), 16); + hypouser.accent_color = Number.parseInt("0x" + _.substring(1), 16); changed = true; regen(); }); } { - const tas = settings.addButton(I18n.getTranslation("localuser.themesAndSounds")); + const prefs = await getPreferences(); + const tas = settings.addButton(I18n.localuser.themesAndSounds()); { const themes = ["Dark", "WHITE", "Light", "Dark-Accent"]; tas.addSelect( - I18n.getTranslation("localuser.theme:"), - _=>{ - localStorage.setItem("theme", themes[_]); - setTheme(); + I18n.localuser["theme:"](), + async (_) => { + prefs.theme = themes[_] as ThemeOption; + + await setTheme(prefs.theme); }, themes, { - defaultIndex: themes.indexOf( - localStorage.getItem("theme") as string - ), - } + defaultIndex: themes.indexOf(prefs.theme), + }, ); } { - const sounds = AVoice.sounds; - tas - .addSelect( - I18n.getTranslation("localuser.notisound"), - _=>{ - AVoice.setNotificationSound(sounds[_]); - }, - sounds, - { defaultIndex: sounds.indexOf(AVoice.getNotificationSound()) } - ) - .watchForChange(_=>{ - AVoice.noises(sounds[_]); - }); + const initArea = (index: number) => { + if (index === sounds.length - 1) { + const input = document.createElement("input"); + input.type = "file"; + input.accept = "audio/*"; + input.addEventListener("change", () => { + if (input.files?.length === 1) { + const file = input.files[0]; + + let reader = new FileReader(); + reader.onload = () => { + let dataUrl = reader.result; + if (typeof dataUrl !== "string") return; + this.perminfo.sound = {}; + try { + this.perminfo.sound.cSound = dataUrl; + console.log(this.perminfo.sound.cSound); + this.playSound("custom"); + } catch (_) { + alert(I18n.localuser.soundTooLarge()); + } + }; + reader.readAsDataURL(file); + } + }); + area.append(input); + } else { + area.innerHTML = ""; + } + }; + const sounds = [...(this.play?.tracks || []), I18n.localuser.customSound()]; + const initIndex = sounds.indexOf(this.getNotificationSound()); + const select = tas.addSelect( + I18n.localuser.notisound(), + (index) => { + this.setNotificationSound(sounds[index]); + }, + sounds, + {defaultIndex: initIndex}, + ); + select.watchForChange((index) => { + initArea(index); + this.playSound(sounds[index]); + }); + const input = document.createElement("input"); + input.type = "range"; + input.value = this.getNotiVolume() + ""; + input.min = "0"; + input.max = "100"; + input.onchange = () => { + this.setNotificationVolume(+input.value); + this.playSound(sounds[select.index]); + }; + + const area = document.createElement("div"); + initArea(initIndex); + tas.addHTMLArea(area); + tas.addText(I18n.notiVolume()); + tas.addHTMLArea(input); } { - let userinfos = getBulkInfo(); tas.addColorInput( - I18n.getTranslation("localuser.accentColor"), - _=>{ - userinfos = getBulkInfo(); - userinfos.accent_color = _; - localStorage.setItem("userinfos", JSON.stringify(userinfos)); - document.documentElement.style.setProperty( - "--accent-color", - userinfos.accent_color - ); + I18n.localuser.accentColor(), + async (_) => { + prefs.accentColor = _; + await setPreferences(prefs); + + document.documentElement.style.setProperty("--accent-color", prefs.accentColor); }, - { initColor: userinfos.accent_color } + {initColor: prefs.accentColor}, ); } + { + const options = [[null, I18n.noEmojiFont()], ...Localuser.fonts] as const; + const cur = prefs.emojiFont; + let index = options.findIndex((_) => _[1] == cur); + if (index === -1) index = 0; + tas.addSelect( + I18n.emojiSelect(), + async (index) => { + if (options[index][0]) { + prefs.emojiFont = options[index][1]; + } else { + prefs.emojiFont = undefined; + } + await setPreferences(prefs); + Localuser.loadFont(); + }, + options.map((font) => font[1]), + { + defaultIndex: index, + }, + ); + } + { + const cur = prefs.renderJoinAvatars; + tas.addCheckboxInput( + I18n.renderJoinAvatars(), + async (v) => { + prefs.renderJoinAvatars = v; + await setPreferences(prefs); + }, + {initState: cur}, + ); + } } { - const update=settings.addButton(I18n.getTranslation("localuser.updateSettings")) - const sw=update.addSelect(I18n.getTranslation("localuser.swSettings"),()=>{},["SWOff","SWOffline","SWOn"].map(e=>I18n.getTranslation("localuser."+e)),{ - defaultIndex:["false","offlineOnly","true"].indexOf(localStorage.getItem("SWMode") as string) - }); - sw.onchange=(e)=>{ - SW.setMode(["false","offlineOnly","true"][e] as "false"|"offlineOnly"|"true") + const update = settings.addButton(I18n.localuser.updateSettings()); + let index = ServiceWorkerModeValues.indexOf(localSettings.serviceWorkerMode); + if (index === -1) { + index = 2; } - update.addButtonInput("",I18n.getTranslation("localuser.CheckUpdate"),()=>{ - SW.checkUpdate(); + const sw = update.addSelect( + I18n.settings.updates.serviceWorkerMode.title(), + () => {}, + ServiceWorkerModeValues.map((e) => I18n.settings.updates.serviceWorkerMode[e]()), + { + defaultIndex: index, + }, + ); + sw.onchange = (e) => { + SW.setMode(ServiceWorkerModeValues[e]); + }; + update.addButtonInput("", I18n.localuser.CheckUpdate(), async () => { + const update = await SW.checkUpdates(); + const text = update ? I18n.localuser.updatesYay() : I18n.localuser.noUpdates(); + const d = new Dialog(""); + d.options.addTitle(text); + if (update) { + d.options.addButtonInput("", I18n.localuser.refreshPage(), () => { + window.location.reload(); + }); + } + d.show(); }); - update.addButtonInput("",I18n.getTranslation("localuser.clearCache"),()=>{ + update.addButtonInput("", I18n.localuser.clearCache(), () => { SW.forceClear(); }); } { - const security = settings.addButton(I18n.getTranslation("localuser.accountSettings")); - const genSecurity = ()=>{ + const security = settings.addButton(I18n.localuser.accountSettings()); + const genSecurity = () => { security.removeAll(); - if(this.mfa_enabled){ - security.addButtonInput("", I18n.getTranslation("localuser.2faDisable"), ()=>{ + if (this.mfa_enabled) { + security.addButtonInput("", I18n.localuser["2faDisable"](), () => { const form = security.addSubForm( - I18n.getTranslation("localuser.2faDisable"), - (_: any)=>{ - if(_.message){ - switch(_.code){ - case 60008: - form.error("code", I18n.getTranslation("badCode")); - break; + I18n.localuser["2faDisable"](), + (_: any) => { + if (_.message) { + switch (_.code) { + case 60008: + form.error("code", I18n.localuser.badCode()); + break; } - }else{ + } else { this.mfa_enabled = false; security.returnFromSub(); genSecurity(); @@ -1302,31 +2691,29 @@ class Localuser{ { fetchURL: this.info.api + "/users/@me/mfa/totp/disable", headers: this.headers, - } + }, ); - form.addTextInput(I18n.getTranslation("localuser.2faCode"), "code", { required: true }); + form.addTextInput(I18n.localuser["2faCode:"](), "code", {required: true}); }); - }else{ - security.addButtonInput("", I18n.getTranslation("localuser.2faEnable"), async ()=>{ + } else { + security.addButtonInput("", I18n.localuser["2faEnable"](), async () => { let secret = ""; - for(let i = 0; i < 18; i++){ - secret += "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"[ - Math.floor(Math.random() * 32) - ]; + for (let i = 0; i < 18; i++) { + secret += "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"[Math.floor(Math.random() * 32)]; } const form = security.addSubForm( - I18n.getTranslation("localuser.setUp2fa"), - (_: any)=>{ - if(_.message){ - switch(_.code){ - case 60008: - form.error("code", I18n.getTranslation("localuser.badCode")); - break; - case 400: - form.error("password", I18n.getTranslation("localuser.badPassword")); - break; + I18n.localuser.setUp2fa(), + (_: any) => { + if (_.message) { + switch (_.code) { + case 60008: + form.error("code", I18n.localuser.badCode()); + break; + case 400: + form.error("password", I18n.localuser.badPassword()); + break; } - }else{ + } else { genSecurity(); this.mfa_enabled = true; security.returnFromSub(); @@ -1335,170 +2722,301 @@ class Localuser{ { fetchURL: this.info.api + "/users/@me/mfa/totp/enable/", headers: this.headers, - } - ); - form.addTitle( - I18n.getTranslation("localuser.setUp2faInstruction") - ); - form.addText( - I18n.getTranslation("localuser.2faCodeGive",secret) + }, ); - form.addTextInput(I18n.getTranslation("localuser.password:"), "password", { + form.addTitle(I18n.localuser.setUp2faInstruction()); + form.addText(I18n.localuser["2faCodeGive"](secret)); + form.addTextInput(I18n.localuser["password:"](), "password", { required: true, password: true, }); - form.addTextInput(I18n.getTranslation("localuser.2faCode"), "code", { required: true }); + form.addTextInput(I18n.localuser["2faCode:"](), "code", {required: true}); form.setValue("secret", secret); }); } - security.addButtonInput("", I18n.getTranslation("localuser.changeDiscriminator"), ()=>{ + { + security.addButtonInput("", I18n.webauth.manage(), () => { + const keyMenu = security.addSubOptions("Manage Keys"); + const addKey = (key: {name: string; id: string}) => { + keyMenu.addButtonInput("", key.name, () => { + const opt = keyMenu.addSubOptions(key.name); + const button = opt.addButtonInput("", I18n.delete(), async () => { + await fetch(this.info.api + "/users/@me/mfa/webauthn/credentials/" + key.id, { + headers: this.headers, + method: "DELETE", + }); + keyMenu.returnFromSub(); + keyMenu.deleteElm(button); + }); + }); + }; + keyMenu.addButtonInput("", I18n.webauth.addKey(), () => { + const form = keyMenu.addSubForm( + I18n.webauth.addKey(), + async (obj) => { + const body = obj as {ticket: string; challenge: string}; + const challenge = JSON.parse(body.challenge) + .publicKey as PublicKeyCredentialCreationOptionsJSON; + console.log(challenge.challenge); + challenge.challenge = challenge.challenge + .split("=")[0] + .replaceAll("+", "-") + .replaceAll("/", "_"); + console.log(challenge.challenge); + const options = PublicKeyCredential.parseCreationOptionsFromJSON(challenge); + const credential = (await navigator.credentials.create({ + publicKey: options, + })) as unknown as { + rawId: ArrayBuffer; + response: { + attestationObject: ArrayBuffer; + clientDataJSON: ArrayBuffer; + }; + }; + if (!credential) return; + function toBase64(buf: ArrayBuffer) { + return btoa(String.fromCharCode(...new Uint8Array(buf))); + } + const res = { + rawId: toBase64(credential.rawId), + response: { + clientDataJSON: toBase64(credential.response.clientDataJSON), + attestationObject: toBase64(credential.response.attestationObject), + }, + }; + const key = await ( + await fetch(this.info.api + "/users/@me/mfa/webauthn/credentials", { + headers: this.headers, + method: "POST", + body: JSON.stringify({ + ticket: body.ticket, + credential: JSON.stringify(res), + name: name.value, + }), + }) + ).json(); + addKey(key); + keyMenu.returnFromSub(); + }, + { + fetchURL: this.info.api + "/users/@me/mfa/webauthn/credentials", + method: "POST", + headers: this.headers, + tfaCheck: false, + }, + ); + form.addTextInput(I18n.htmlPages.pwField(), "password", { + password: true, + }); + const name = form.options.addTextInput(I18n.webauth.keyname(), () => {}, { + initText: "Key", + }); + }); + fetch(this.info.api + "/users/@me/mfa/webauthn/credentials", { + headers: this.headers, + }) + .then((_) => _.json()) + .then((keys: {id: string; name: string}[]) => { + for (const key of keys) { + addKey(key); + } + }); + }); + } + security.addButtonInput("", I18n.localuser.changeDiscriminator(), () => { const form = security.addSubForm( - I18n.getTranslation("localuser.changeDiscriminator"), - _=>{ + I18n.localuser.changeDiscriminator(), + (_) => { security.returnFromSub(); }, { fetchURL: this.info.api + "/users/@me/", headers: this.headers, method: "PATCH", - } + }, ); - form.addTextInput(I18n.getTranslation("localuser.newDiscriminator"), "discriminator"); + form.addTextInput(I18n.localuser.newDiscriminator(), "discriminator"); }); - security.addButtonInput("", I18n.getTranslation("localuser.changeEmail"), ()=>{ + security.addButtonInput("", I18n.localuser.changeEmail(), () => { const form = security.addSubForm( - I18n.getTranslation("localuser.changeEmail"), - _=>{ + I18n.localuser.changeEmail(), + (_) => { security.returnFromSub(); }, { fetchURL: this.info.api + "/users/@me/", headers: this.headers, method: "PATCH", - } + }, ); - form.addTextInput(I18n.getTranslation("localuser.password:"), "password", { password: true }); - if(this.mfa_enabled){ - form.addTextInput(I18n.getTranslation("localuser.2faCode"), "code"); + form.addTextInput(I18n.localuser["password:"](), "password", { + password: true, + }); + if (this.mfa_enabled) { + form.addTextInput(I18n.localuser["2faCode:"](), "code"); } - form.addTextInput(I18n.getTranslation("localuser.newEmail:"), "email"); + form.addTextInput(I18n.localuser["newEmail:"](), "email"); }); - security.addButtonInput("", I18n.getTranslation("localuser.changeUsername"), ()=>{ + security.addButtonInput("", I18n.localuser.changeUsername(), () => { const form = security.addSubForm( - I18n.getTranslation("localuser.changeUsername"), - _=>{ + I18n.localuser.changeUsername(), + (_) => { security.returnFromSub(); }, { fetchURL: this.info.api + "/users/@me/", headers: this.headers, method: "PATCH", - } + }, ); - form.addTextInput(I18n.getTranslation("localuser.password:"), "password", { password: true }); - if(this.mfa_enabled){ - form.addTextInput(I18n.getTranslation("localuser.2faCode"), "code"); + form.addTextInput(I18n.localuser["password:"](), "password", { + password: true, + }); + if (this.mfa_enabled) { + form.addTextInput(I18n.localuser["2faCode:"](), "code"); } - form.addTextInput(I18n.getTranslation("localuser.newUsername"), "username"); + form.addTextInput(I18n.localuser.newUsername(), "username"); }); - security.addButtonInput("", I18n.getTranslation("localuser.changePassword"), ()=>{ + security.addButtonInput("", I18n.localuser.changePassword(), () => { const form = security.addSubForm( - I18n.getTranslation("localuser.changePassword"), - _=>{ + I18n.localuser.changePassword(), + (_) => { security.returnFromSub(); }, { fetchURL: this.info.api + "/users/@me/", headers: this.headers, method: "PATCH", - } + }, ); - form.addTextInput(I18n.getTranslation("localuser.oldPassword:"), "password", { password: true }); - if(this.mfa_enabled){ - form.addTextInput(I18n.getTranslation("localuser.2faCode"), "code"); + form.addTextInput(I18n.localuser["oldPassword:"](), "password", { + password: true, + }); + if (this.mfa_enabled) { + form.addTextInput(I18n.localuser["2faCode:"](), "code"); } let in1 = ""; let in2 = ""; - form.addTextInput(I18n.getTranslation("localuser.newPassword:"), "").watchForChange(text=>{ - in1 = text; - }); - const copy = form.addTextInput("New password again:", ""); - copy.watchForChange(text=>{ + form + .addTextInput(I18n.localuser["newPassword:"](), "", {password: true}) + .watchForChange((text) => { + in1 = text; + }); + const copy = form.addTextInput("New password again:", "", {password: true}); + copy.watchForChange((text) => { in2 = text; }); - form.setValue("new_password", ()=>{ - if(in1 === in2){ + form.setValue("new_password", () => { + if (in1 === in2) { return in1; - }else{ - throw new FormError(copy, I18n.getTranslation("localuser.PasswordsNoMatch")); + } else { + throw new FormError(copy, I18n.localuser.PasswordsNoMatch()); } }); }); - security.addSelect(I18n.getTranslation("localuser.language"),(e)=>{ - I18n.setLanguage(I18n.options()[e]); - },[...langmap.values()],{ - defaultIndex:I18n.options().indexOf(I18n.lang) - }); + security.addSelect( + I18n.localuser.language(), + (e) => { + I18n.setLanguage(I18n.options()[e]); + this.updateTranslations(); + }, + [...langmap.values()], + {defaultIndex: I18n.options().indexOf(I18n.lang)}, + ); + { - const box=security.addCheckboxInput(I18n.getTranslation("localuser.enableEVoice"),()=>{},{initState:Boolean(localStorage.getItem("Voice enabled"))}); - box.onchange=(e)=>{ - if(e){ - if(confirm(I18n.getTranslation("localuser.VoiceWarning"))){ - localStorage.setItem("Voice enabled","true") - - }else{ - box.value=true; - const checkbox=box.input.deref(); - if(checkbox){ - checkbox.checked=false; - } - } - }else{ - localStorage.removeItem("Voice enabled"); - } + security.addButtonInput("", I18n.logout.logout(), async () => { + if (await this.userinfo.logout()) window.location.href = "/"; + }); } - } }; genSecurity(); } { - const connections = settings.addButton(I18n.getTranslation("localuser.connections")); + const accessibility = settings.addButton(I18n.accessibility.name()); + accessibility.addCheckboxInput( + I18n.accessibility.roleColors(), + (t) => { + console.log(t); + this.perminfo.user.disableColors = !t; + }, + {initState: !this.perminfo.user.disableColors}, + ); + accessibility.addCheckboxInput( + I18n.accessibility.gradientColors(), + (t) => { + console.log(t); + this.perminfo.user.gradientColors = t; + }, + {initState: this.perminfo.user.gradientColors}, + ); + accessibility.addCheckboxInput( + I18n.channel.allowIcons(), + (t) => { + console.log(t); + this.perminfo.user.disableIcons = !t; + }, + {initState: !this.perminfo.user.disableIcons}, + ); + + accessibility.addCheckboxInput( + I18n.accessibility.decorations(), + (t) => { + this.perminfo.user.decorations = t; + }, + {initState: this.perminfo.user.decorations}, + ); + accessibility.addSelect( + I18n.accessibility.playGif(), + async (i) => { + prefs.animateGifs = AnimateTristateValues[i]; + await setPreferences(prefs); + }, + AnimateTristateValues.map((_) => I18n.accessibility.gifSettings[_]()), + {defaultIndex: AnimateTristateValues.indexOf(prefs.animateGifs)}, + ); + accessibility.addSelect( + I18n.accessibility.playIcon(), + async (i) => { + prefs.animateIcons = AnimateTristateValues[i]; + await setPreferences(prefs); + }, + AnimateTristateValues.map((_) => I18n.accessibility.gifSettings[_]()), + {defaultIndex: AnimateTristateValues.indexOf(prefs.animateIcons)}, + ); + } + { + const connections = settings.addButton(I18n.localuser.connections()); const connectionContainer = document.createElement("div"); connectionContainer.id = "connection-container"; fetch(this.info.api + "/connections", { headers: this.headers, }) - .then(r=>r.json()) - .then(json=>{ + .then((r) => r.json() as Promise<{[key: string]: {enabled: boolean}}>) + .then((json) => { Object.keys(json) - .sort(key=>(json[key].enabled ? -1 : 1)) - .forEach(key=>{ + .sort((key) => (json[key].enabled ? -1 : 1)) + .forEach((key) => { const connection = json[key]; const container = document.createElement("div"); - container.textContent = - key.charAt(0).toUpperCase() + key.slice(1); + container.textContent = key.charAt(0).toUpperCase() + key.slice(1); - if(connection.enabled){ - container.addEventListener("click", async ()=>{ + if (connection.enabled) { + container.addEventListener("click", async () => { const connectionRes = await fetch( this.info.api + "/connections/" + key + "/authorize", { headers: this.headers, - } + }, ); const connectionJSON = await connectionRes.json(); - window.open( - connectionJSON.url, - "_blank", - "noopener noreferrer" - ); + window.open(connectionJSON.url, "_blank", "noopener noreferrer"); }); - }else{ + } else { container.classList.add("disabled"); - container.title = I18n.getTranslation("localuser.PasswordsNoMatch"); } connectionContainer.appendChild(container); @@ -1507,38 +3025,38 @@ class Localuser{ connections.addHTMLArea(connectionContainer); } { - const devPortal = settings.addButton(I18n.getTranslation("localuser.devPortal")); + const devPortal = settings.addButton(I18n.localuser.devPortal()); fetch(this.info.api + "/teams", { headers: this.headers, - }).then(async (teamsRes)=>{ + }).then(async (teamsRes) => { const teams = await teamsRes.json(); - devPortal.addButtonInput("", I18n.getTranslation("localuser.createApp"), ()=>{ + const button = devPortal.addButtonInput("", I18n.localuser.createApp(), () => { const form = devPortal.addSubForm( - I18n.getTranslation("localuser.createApp"), - (json: any)=>{ - if(json.message) form.error("name", json.message); - else{ + I18n.localuser.createApp(), + (json: any) => { + if (json.message) form.error("name", json.message); + else { devPortal.returnFromSub(); - this.manageApplication(json.id,devPortal); + this.manageApplication(json.id, devPortal, () => { + form.options.deleteElm(button); + }); } }, { fetchURL: this.info.api + "/applications", headers: this.headers, method: "POST", - } + }, ); - form.addTextInput("Name:", "name", { required: true }); + form.addTextInput("Name:", "name", {required: true}); form.addSelect( - I18n.getTranslation("localuser.team:"), + I18n.localuser["team:"](), "team_id", - ["Personal", ...teams.map((team: { name: string })=>team.name)], - { - defaultIndex: 0, - } + ["Personal", ...teams.map((team: {name: string}) => team.name)], + {defaultIndex: 0}, ); }); @@ -1547,8 +3065,8 @@ class Localuser{ fetch(this.info.api + "/applications", { headers: this.headers, }) - .then(r=>r.json()) - .then(json=>{ + .then((r) => r.json()) + .then((json) => { json.forEach( (application: { cover_image: any; @@ -1556,19 +3074,19 @@ class Localuser{ id: string | undefined; name: string | number; bot: any; - })=>{ + }) => { const container = document.createElement("div"); - if(application.cover_image || application.icon){ - const cover = document.createElement("img"); - cover.crossOrigin = "anonymous"; - cover.src = - this.info.cdn + - "/app-icons/" + - application.id + - "/" + - (application.cover_image || application.icon) + - ".png?size=256"; + if (application.cover_image || application.icon) { + const cover = createImg( + this.info.cdn + + "/app-icons/" + + application.id + + "/" + + (application.cover_image || application.icon) + + ".png" + + new CDNParams({expectedSize: 256}), + ); cover.alt = ""; cover.loading = "lazy"; container.appendChild(cover); @@ -1578,391 +3096,1535 @@ class Localuser{ name.textContent = application.name + (application.bot ? " (Bot)" : ""); container.appendChild(name); - container.addEventListener("click", async ()=>{ - this.manageApplication(application.id,devPortal); + container.addEventListener("click", async () => { + this.manageApplication(application.id, devPortal, () => { + appListContainer.remove(); + }); }); appListContainer.appendChild(container); - } + }, ); }); devPortal.addHTMLArea(appListContainer); }); } + + { + const manageSessions = settings.addButton(I18n.deviceManage.title()); + (async () => { + const json = (await ( + await fetch(this.info.api + "/auth/sessions?extended=true", {headers: this.headers}) + ).json()) as {user_sessions: expSessionJson[]}; + for (const session of json.user_sessions.sort( + (a, b) => +new Date(a.last_seen) - +new Date(b.last_seen), + )) { + const div = document.createElement("div"); + div.classList.add("flexltr", "sessionDiv"); + + const info = document.createElement("div"); + info.classList.add("flexttb"); + div.append(info); + + let line2 = ""; + const last = session.last_seen_location_info; + if (last) { + line2 += last.country_name; + if (last.region) line2 += ", " + last.region; + if (last.city) line2 += ", " + last.city; + } + if (line2) { + line2 += " • "; + } + const format = new Intl.RelativeTimeFormat(I18n.lang, {style: "short"}); + const time = (Date.now() - +new Date(session.last_seen)) / 1000; + if (time < 60) { + line2 += format.format(-Math.floor(time), "seconds"); + } else if (time < 60 * 60) { + line2 += format.format(-Math.floor(time / 60), "minutes"); + } else if (time < 60 * 60 * 24) { + line2 += format.format(-Math.floor(time / 60 / 60), "hours"); + } else if (time < 60 * 60 * 24 * 7) { + line2 += format.format(-Math.floor(time / 60 / 60 / 24), "days"); + } else if (time < 60 * 60 * 24 * 365) { + line2 += format.format(-Math.floor(time / 60 / 60 / 24 / 7), "weeks"); + } else { + line2 += format.format(-Math.floor(time / 60 / 60 / 24 / 365), "years"); + } + const loc = document.createElement("span"); + loc.textContent = line2; + info.append(loc); + const r = manageSessions.addHTMLArea(div); + div.onclick = () => { + const sub = manageSessions.addSubOptions(I18n.deviceManage.manageDev()); + sub.addText(I18n.deviceManage.ip(session.last_seen_ip)); + sub.addText(I18n.deviceManage.last(session.approx_last_used_time)); + if (last) { + sub.addText(I18n.deviceManage.estimateWarn()); + sub.addText(I18n.deviceManage.continent(last.continent_name)); + sub.addText(I18n.deviceManage.country(last.country_name)); + if (last.region) sub.addText(I18n.deviceManage.region(last.region)); + if (last.city) sub.addText(I18n.deviceManage.city(last.city)); + if (last.postal) sub.addText(I18n.deviceManage.postal(last.postal)); + sub.addText(I18n.deviceManage.longitude(last.longitude + "")); + sub.addText(I18n.deviceManage.latitude(last.latitude + "")); + } + if (session.id !== this.session_id) { + sub.addButtonInput("", I18n.deviceManage.logout(), () => { + div.remove(); + r.html = document.createElement("div"); + manageSessions.returnFromSub(); + fetch(this.info.api + "/auth/sessions/logout", { + method: "POST", + headers: this.headers, + body: JSON.stringify({ + session_id_hashes: [session.id_hash], + }), + }); + }); + } else sub.addText(I18n.deviceManage.curSes()); + }; + } + })(); + } + + { + const deleteAccount = settings.addButton(I18n.localuser.deleteAccount()).addForm( + "", + (e) => { + if ("message" in e) { + if (typeof e.message === "string") { + throw new FormError(password, e.message); + } + } else { + this.userinfo.remove(); + window.location.href = "/"; + } + }, + { + headers: this.headers, + method: "POST", + fetchURL: this.info.api + "/users/@me/delete/", + traditionalSubmit: false, + submitText: I18n.localuser.deleteAccountButton(), + }, + ); + const shrek = deleteAccount.addTextInput( + I18n.localuser.areYouSureDelete(I18n.localuser.sillyDeleteConfirmPhrase()), + "shrek", + ); + const password = deleteAccount.addTextInput(I18n.localuser["password:"](), "password", { + password: true, + }); + deleteAccount.addPreprocessor((obj) => { + if ("shrek" in obj) { + if (obj.shrek !== I18n.localuser.sillyDeleteConfirmPhrase()) { + throw new FormError(shrek, I18n.localuser.mustTypePhrase()); + } + delete obj.shrek; + } else { + throw new FormError(shrek, I18n.localuser.mustTypePhrase()); + } + }); + } + if ( + this.rights.hasPermission("OPERATOR") || + this.rights.hasPermission("CREATE_REGISTRATION_TOKENS") + ) { + const manageInstance = settings.addButton(I18n.localuser.manageInstance()); + if (this.rights.hasPermission("OPERATOR")) { + manageInstance.addButtonInput("", I18n.manageInstance.stop(), () => { + const menu = new Dialog(""); + const options = menu.float.options; + options.addTitle(I18n.manageInstance.AreYouSureStop()); + const yesno = options.addOptions("", {ltr: true}); + yesno.addButtonInput("", I18n.yes(), () => { + fetch(this.info.api + "/stop", {headers: this.headers, method: "POST"}); + menu.hide(); + }); + yesno.addButtonInput("", I18n.no(), () => { + menu.hide(); + }); + menu.show(); + }); + } + if (this.rights.hasPermission("CREATE_REGISTRATION_TOKENS")) { + manageInstance.addButtonInput("", I18n.manageInstance.createTokens(), () => { + const tokens = manageInstance.addSubOptions(I18n.manageInstance.createTokens(), { + noSubmit: true, + }); + const count = tokens.addTextInput(I18n.manageInstance.count(), () => {}, { + initText: "1", + }); + const length = tokens.addTextInput(I18n.manageInstance.length(), () => {}, { + initText: "32", + }); + const format = tokens.addSelect( + I18n.manageInstance.format(), + () => {}, + [ + I18n.manageInstance.TokenFormats.JSON(), + I18n.manageInstance.TokenFormats.plain(), + I18n.manageInstance.TokenFormats.URLs(), + ], + { + defaultIndex: 2, + }, + ); + format.watchForChange((e) => { + if (e !== 2) { + urlOption.removeAll(); + } else { + makeURLMenu(); + } + }); + const urlOption = tokens.addOptions(""); + const urlOptionsJSON = { + url: window.location.origin, + type: "Jank", + }; + function makeURLMenu() { + urlOption + .addTextInput(I18n.manageInstance.clientURL(), () => {}, { + initText: urlOptionsJSON.url, + }) + .watchForChange((str) => { + urlOptionsJSON.url = str; + }); + urlOption + .addSelect( + I18n.manageInstance.regType(), + () => {}, + ["Jank", I18n.manageInstance.genericType()], + { + defaultIndex: ["Jank", "generic"].indexOf(urlOptionsJSON.type), + }, + ) + .watchForChange((i) => { + urlOptionsJSON.type = ["Jank", "generic"][i]; + }); + } + makeURLMenu(); + tokens.addButtonInput("", I18n.manageInstance.create(), async () => { + const params = new URLSearchParams(); + params.set("count", count.value); + params.set("length", length.value); + const json = (await ( + await fetch( + this.info.api + "/auth/generate-registration-tokens?" + params.toString(), + { + headers: this.headers, + }, + ) + ).json()) as {tokens: string[]}; + if (format.index === 0) { + pre.textContent = JSON.stringify(json.tokens); + } else if (format.index === 1) { + pre.textContent = json.tokens.join("\n"); + } else if (format.index === 2) { + if (urlOptionsJSON.type === "Jank") { + const options = new URLSearchParams(); + options.set("instance", this.info.wellknown); + pre.textContent = json.tokens + .map((token) => { + options.set("token", token); + return `${urlOptionsJSON.url}/register?` + options.toString(); + }) + .join("\n"); + } else { + const options = new URLSearchParams(); + pre.textContent = json.tokens + .map((token) => { + options.set("token", token); + return `${urlOptionsJSON.url}/register?` + options.toString(); + }) + .join("\n"); + } + } + }); + tokens.addButtonInput("", I18n.manageInstance.copy(), async () => { + try { + if (pre.textContent) { + await navigator.clipboard.writeText(pre.textContent); + } + } catch (err) { + console.error(err); + } + }); + const pre = document.createElement("pre"); + tokens.addHTMLArea(pre); + }); + } + } + (async () => { + const jankInfo = settings.addButton(I18n.jankInfo()); + const img = document.createElement("img"); + img.src = "/logo.svg"; + jankInfo.addHTMLArea(img); + img.width = 128; + img.height = 128; + const ver = await (await fetch("/getupdates")).text(); + jankInfo.addMDText( + new MarkDown( + I18n.clientDesc(ver, window.location.origin, this.rights.allow + ""), + undefined, + ), + ); + })(); + const installP = installPGet(); + if (installP) { + const c = settings.addButton(I18n.localuser.install()); + c.addText(I18n.localuser.installDesc()); + c.addButtonInput("", I18n.localuser.installJank(), async () => { + //@ts-expect-error have to do this :3 + await installP.prompt(); + }); + } + { + const trusted = settings.addButton(I18n.localuser.trusted()); + trusted.addMDText(new MarkDown(I18n.localuser.trustedDesc())); + for (const thing of MarkDown.trustedDomains) { + const div = document.createElement("div"); + div.classList.add("flexltr", "trustedDomain"); + + const name = document.createElement("span"); + name.textContent = thing; + + const remove = document.createElement("button"); + remove.textContent = I18n.remove(); + remove.onclick = () => { + MarkDown.saveTrusted(); + MarkDown.trustedDomains.delete(thing); + MarkDown.saveTrusted(true); + div.remove(); + }; + + div.append(name, remove); + trusted.addHTMLArea(div); + } + } + { + const blog = settings.addButton(I18n.blog.blog()); + blog.addCheckboxInput( + I18n.blog.blogUpdates(), + async (check) => { + prefs.showBlogUpdates = check; + await setPreferences(prefs); + }, + {initState: prefs.showBlogUpdates}, + ); + (async () => { + const posts = await this.getPosts(); + for (const post of posts.items) { + const div = document.createElement("div"); + div.classList.add("flexltr", "blogDiv"); + if (post.image) { + //TODO handle this case, no blog posts currently do this + } + const titleStuff = document.createElement("div"); + titleStuff.classList.add("flexttb"); + + const h2 = document.createElement("h2"); + h2.textContent = post.title; + + const p = document.createElement("p"); + p.textContent = post.content_html; + titleStuff.append(h2, p); + div.append(titleStuff); + blog.addHTMLArea(div); + MarkDown.safeLink(div, post.url); + } + })(); + } + { + const devSettings = settings.addButton(I18n.devSettings.name(), {noSubmit: true}); + devSettings.addText(I18n.devSettings.description()); + devSettings.addHR(); + const box1 = devSettings.addCheckboxInput(I18n.devSettings.logGateway(), () => {}, { + initState: getDeveloperSettings().gatewayLogging, + }); + box1.onchange = (e) => { + const settings = getDeveloperSettings(); + settings.gatewayLogging = e; + setDeveloperSettings(settings); + }; + + const box2 = devSettings.addCheckboxInput(I18n.devSettings.badUser(), () => {}, { + initState: getDeveloperSettings().logBannedFields, + }); + box2.onchange = (e) => { + const settings = getDeveloperSettings(); + settings.logBannedFields = e; + setDeveloperSettings(settings); + }; + + const box3 = devSettings.addCheckboxInput(I18n.devSettings.traces(), () => {}, { + initState: getDeveloperSettings().showTraces, + }); + box3.onchange = (e) => { + const settings = getDeveloperSettings(); + settings.showTraces = e; + setDeveloperSettings(settings); + }; + + const box4 = devSettings.addCheckboxInput(I18n.devSettings.cache(), () => {}, { + initState: getDeveloperSettings().cacheSourceMaps, + }); + box4.onchange = (e) => { + const settings = getDeveloperSettings(); + settings.cacheSourceMaps = e; + setDeveloperSettings(settings); + SW.postMessage({code: "isDev", dev: e}); + }; + devSettings.addText(I18n.devSettings.cacheDesc()); + + const box5 = devSettings.addCheckboxInput(I18n.devSettings.captureTrace(), () => {}, { + initState: getDeveloperSettings().interceptApiTraces, + }); + box5.onchange = (e) => { + const settings = getDeveloperSettings(); + settings.interceptApiTraces = e; + setDeveloperSettings(settings); + SW.traceInit(); + }; + + const box6 = devSettings.addCheckboxInput(I18n.devSettings.gatewayComp(), () => {}, { + initState: getDeveloperSettings().gatewayCompression, + }); + box6.onchange = (e) => { + const settings = getDeveloperSettings(); + settings.gatewayCompression = e; + setDeveloperSettings(settings); + SW.traceInit(); + }; + + const box7 = devSettings.addCheckboxInput(I18n.devSettings.reportSystem(), () => {}, { + initState: getDeveloperSettings().reportSystem, + }); + box7.onchange = (e) => { + const settings = getDeveloperSettings(); + settings.reportSystem = e; + setDeveloperSettings(settings); + SW.traceInit(); + }; + + devSettings.addButtonInput("", I18n.devSettings.clearWellKnowns(), async () => { + const currentUserInfos = JSON.parse(localStorage.getItem("userinfos")!); + await Promise.all( + Object.keys(currentUserInfos.users).map(async (user) => { + const key = + currentUserInfos.users[user].serverurls.value ?? + currentUserInfos.users[user].serverurls.wellknown ?? + currentUserInfos.users[user].serverurls.api; + currentUserInfos.users[user].serverurls = await getapiurls(key); + console.log(key, currentUserInfos.users[user].serverurls); + localStorage.setItem("userinfos", JSON.stringify(currentUserInfos)); + }), + ); + + localStorage.removeItem("instanceinfo"); + await SW.postMessage({ + code: "clearCdnCache", + }); + + // @ts-ignore - chromium is smelly for not supporting the `forceGet` option (aka skip cache) + window.location.reload(true); + }); + } + if (this.trace.length && getDeveloperSettings().showTraces) { + const traces = settings.addButton(I18n.localuser.trace(), { + noSubmit: true, + }); + const traceArr = this.trace; + + const sel = traces.addSelect( + "", + () => {}, + this.trace.map((_) => + I18n.trace.traces( + _.trace[0], + _.trace[1].micros / 1000 + "", + _.time.getHours() + ":" + _.time.getMinutes(), + ), + ), + ); + function generateTraceHTML(trace: trace, indent: number): HTMLElement { + const div = document.createElement("div"); + div.classList.add("traceDiv", "flexttb"); + + const head = document.createElement("div"); + div.append(head); + + const title = document.createElement("h3"); + title.textContent = I18n.trace.totalTime(trace[1].micros / 1000 + "", trace[0]); + const indents = document.createElement("span"); + indents.classList.add("visually-hidden"); + indents.textContent = " ".repeat(indent); + title.prepend(indents); + head.append(title); + + if (!trace[1].calls) return div; + + let objs: {name: string; val: traceObj}[] = []; + { + const names = trace[1].calls.filter((_) => typeof _ === "string"); + const vals = trace[1].calls.filter((_) => _ instanceof Object); + let i = 0; + for (const name of names) { + const val = vals[i]; + objs.push({name, val}); + i++; + } + } + + const bars = document.createElement("div"); + bars.classList.add("flexltr", "traceBars"); + + const colors = ["red", "orange", "yellow", "lime", "blue", "indigo", "violet"]; + let i = 0; + for (const thing of objs) { + const bar = document.createElement("div"); + bar.style.setProperty( + "flex-grow", + Math.ceil((thing.val.micros / trace[1].micros) * 1000) + "", + ); + bar.style.setProperty("background", colors[i % colors.length]); + bars.append(bar); + new Hover(I18n.trace.totalTime(thing.val.micros / 1000 + "", thing.name)).addEvent(bar); + i++; + } + const body = document.createElement("div"); + div.append(body); + head.append(bars); + let dropped = false; + head.onclick = () => { + if (!trace[1].calls) return; + if (dropped) { + dropped = false; + body.innerHTML = ""; + return; + } + + let i = 0; + for (const obj of objs) { + body.append(generateTraceHTML([obj.name, obj.val], indent + 1)); + i++; + } + dropped = true; + }; + + div.classList.add("dropDownTrace"); + head.classList.add("traceHead"); + return div; + } + const blank = document.createElement("div"); + traces.addHTMLArea(blank); + const updateInfo = () => { + const trace = traceArr[sel.index]; + blank.innerHTML = ""; + blank.append(generateTraceHTML(trace.trace, 0)); + }; + sel.onchange = () => { + updateInfo(); + }; + updateInfo(); + } + { + const instanceInfo = settings.addButton(I18n.instanceInfo.name()); + fetch(this.info.api + "/policies/instance/") + .then((_) => _.json()) + .then((body) => { + const json = body as { + instanceName: string; + instanceDescription: string | null; + frontPage: string | null; + tosPage: string | null; + correspondenceEmail: string | null; + correspondenceUserID: string | null; + image: string | null; + instanceId: string; + autoCreateBotUsers: false; + publicUrl: string | null; + }; + instanceInfo.addTitle(json.instanceName); + if (json.correspondenceEmail) { + const a = document.createElement("a"); + a.target = "_blank"; + a.rel = "noreferrer"; + a.href = "mailto:" + json.correspondenceEmail; + a.textContent = I18n.instanceInfo.contact(); + instanceInfo.addHTMLArea(a); + } + if (json.tosPage) + instanceInfo.addMDText(new MarkDown(I18n.instanceInfo.tosPage(json.tosPage))); + if (json.publicUrl) + instanceInfo.addMDText(new MarkDown(I18n.instanceInfo.publicUrl(json.publicUrl))); + if (json.frontPage) + instanceInfo.addMDText(new MarkDown(I18n.instanceInfo.frontPage(json.frontPage))); + instanceInfo.addButtonInput("", I18n.instInfo(), () => { + this.instanceStats(); + }); + }); + } settings.show(); } - readonly botTokens:Map=new Map(); - async manageApplication(appId = "", container:Options){ - if(this.perminfo.applications){ - for(const item of Object.keys(this.perminfo.applications)){ - this.botTokens.set(item,this.perminfo.applications[item]); + readonly botTokens: Map = new Map(); + async manageApplication(appId = "", container: Options, deleteButton: () => void) { + if (this.perminfo.applications) { + for (const item of Object.keys(this.perminfo.applications)) { + this.botTokens.set(item, this.perminfo.applications[item]); } } const res = await fetch(this.info.api + "/applications/" + appId, { headers: this.headers, }); const json = await res.json(); - const form=container.addSubForm(json.name,()=>{},{ - fetchURL:this.info.api + "/applications/" + appId, - method:"PATCH", - headers:this.headers, - traditionalSubmit:true + console.error(json); + const form = container.addSubForm(json.name, () => {}, { + fetchURL: this.info.api + "/applications/" + appId, + method: "PATCH", + headers: this.headers, + traditionalSubmit: true, + }); + form.addTextInput(I18n.localuser.appName(), "name", {initText: json.name}); + form.addMDInput(I18n.localuser.description(), "description", { + initText: json.description, + }); + form.addImageInput("Icon:", "icon", { + clear: true, + initImg: json.icon + ? this.info.cdn + + "/app-icons/" + + appId + + "/" + + json.icon + + new CDNParams({expectedSize: 96}) + : "", + }); + form.addTextInput(I18n.localuser.privacyPolcyURL(), "privacy_policy_url", { + initText: json.privacy_policy_url, + }); + form.addText(I18n.localuser.appID(appId)); + form.addButtonInput("", I18n.localuser.showSecret(), () => { + const opt = form.addSubOptions(I18n.localuser.secret()); + opt.addText(I18n.localuser.clientSecret(json.verify_key)); + }); + form.addTextInput(I18n.localuser.TOSURL(), "terms_of_service_url", { + initText: json.terms_of_service_url, }); - form.addTextInput(I18n.getTranslation("localuser.appName"),"name",{initText:json.name}); - form.addMDInput(I18n.getTranslation("localuser.description"),"description",{initText:json.description}); - form.addFileInput("Icon:","icon"); - form.addTextInput(I18n.getTranslation("localuser.privacyPolcyURL"),"privacy_policy_url",{initText:json.privacy_policy_url}); - form.addTextInput(I18n.getTranslation("localuser.TOSURL"),"terms_of_service_url",{initText:json.terms_of_service_url}); - form.addCheckboxInput(I18n.getTranslation("localuser.publicAvaliable"),"bot_public",{initState:json.bot_public}); - form.addCheckboxInput(I18n.getTranslation("localuser.requireCode"),"bot_require_code_grant",{initState:json.bot_require_code_grant}); - form.addButtonInput("",I18n.getTranslation("localuser."+(json.bot?"manageBot":"addBot")),async ()=>{ - if(!json.bot){ - if(!confirm(I18n.getTranslation("localuser.confirmAddBot"))){ + form.addCheckboxInput(I18n.localuser.publicAvaliable(), "bot_public", { + initState: json.bot_public, + }); + form.addCheckboxInput(I18n.localuser.requireCode(), "bot_require_code_grant", { + initState: json.bot_require_code_grant, + }); + form.addButtonInput("", I18n.localuser[json.bot ? "manageBot" : "addBot"](), async () => { + if (!json.bot) { + if (!confirm(I18n.localuser.confirmAddBot())) { return; } - const updateRes = await fetch( - this.info.api + "/applications/" + appId + "/bot", - { - method: "POST", - headers: this.headers, - } - ); + const updateRes = await fetch(this.info.api + "/applications/" + appId + "/bot", { + method: "POST", + headers: this.headers, + }); const updateJSON = await updateRes.json(); - this.botTokens.set(appId,updateJSON.token); + this.botTokens.set(appId, updateJSON.token); } - this.manageBot(appId,form); - }) + this.manageBot(appId, form); + }); + form.addButtonInput("", I18n.applications.delete(), () => { + const sub = form.addSubForm( + I18n.applications.delete(), + () => { + deleteButton(); + container.returnFromSub(); + }, + { + fetchURL: this.info.api + "/applications/" + appId + "/delete", + method: "POST", + headers: this.headers, + submitText: I18n.delete(), + }, + ); + sub.addText(I18n.applications.sure(json.name)); + }); } - async manageBot(appId = "",container:Form){ + async manageBot(appId = "", container: Form) { const res = await fetch(this.info.api + "/applications/" + appId, { - headers: this.headers + headers: this.headers, }); const json = await res.json(); - if(!json.bot){ - return alert(I18n.getTranslation("localuser.confuseNoBot")); - } - const bot:mainuserjson=json.bot; - const form=container.addSubForm(I18n.getTranslation("localuser.editingBot",bot.username),out=>{console.log(out)},{ - method:"PATCH", - fetchURL:this.info.api + "/applications/" + appId + "/bot", - headers:this.headers, - traditionalSubmit:true + if (!json.bot) { + return alert(I18n.localuser.confuseNoBot()); + } + const bot: User = new User(json.bot, this); + const form = container.addSubForm( + I18n.localuser.editingBot(bot.username), + (out) => { + console.log(out); + }, + { + method: "PATCH", + fetchURL: this.info.api + "/applications/" + appId + "/bot", + headers: this.headers, + traditionalSubmit: true, + }, + ); + form.addTextInput(I18n.localuser.botUsername(), "username", { + initText: bot.username, + }); + form.addImageInput(I18n.localuser.botAvatar(), "avatar", { + initImg: bot.getpfpsrc(), + clear: true, }); - form.addTextInput(I18n.getTranslation("localuser.botUsername"),"username",{initText:bot.username}); - form.addFileInput(I18n.getTranslation("localuser.botAvatar"),"avatar"); - form.addButtonInput("",I18n.getTranslation("localuser.resetToken"),async ()=>{ - if(!confirm(I18n.getTranslation("localuser.confirmReset"))){ + form.addButtonInput("", I18n.localuser.resetToken(), async () => { + if (!confirm(I18n.localuser.confirmReset())) { return; } - const updateRes = await fetch( - this.info.api + "/applications/" + appId + "/bot/reset", - { - method: "POST", - headers: this.headers, - } - ); + const updateRes = await fetch(this.info.api + "/applications/" + appId + "/bot/reset", { + method: "POST", + headers: this.headers, + }); const updateJSON = await updateRes.json(); - text.setText(I18n.getTranslation("localuser.tokenDisplay",updateJSON.token)); - this.botTokens.set(appId,updateJSON.token); - if(this.perminfo.applications[appId]){ - this.perminfo.applications[appId]=updateJSON.token; - this.userinfo.updateLocal(); + text.setText(I18n.localuser.tokenDisplay(updateJSON.token)); + this.botTokens.set(appId, updateJSON.token); + if (this.perminfo.applications[appId]) { + this.perminfo.applications[appId] = updateJSON.token; } }); - const text=form.addText(I18n.getTranslation("localuser.tokenDisplay",this.botTokens.has(appId)?this.botTokens.get(appId) as string:"*****************") ); - const check=form.addOptions("",{noSubmit:true}); - if(!this.perminfo.applications){ - this.perminfo.applications={}; - this.userinfo.updateLocal(); + const text = form.addText( + I18n.localuser.tokenDisplay( + this.botTokens.has(appId) ? (this.botTokens.get(appId) as string) : "*****************", + ), + ); + const check = form.addOptions("", {noSubmit: true}); + if (!this.perminfo.applications) { + this.perminfo.applications = {}; } - const checkbox=check.addCheckboxInput(I18n.getTranslation("localuser.saveToken"),()=>{},{initState:!!this.perminfo.applications[appId]}); - checkbox.watchForChange(_=>{ - if(_){ - if(this.botTokens.has(appId)){ - this.perminfo.applications[appId]=this.botTokens.get(appId); - this.userinfo.updateLocal(); - }else{ - alert(I18n.getTranslation("localuser.noToken")); + const checkbox = check.addCheckboxInput(I18n.localuser.saveToken(), () => {}, { + initState: !!this.perminfo.applications[appId], + }); + checkbox.watchForChange((_) => { + if (_) { + if (this.botTokens.has(appId)) { + this.perminfo.applications[appId] = this.botTokens.get(appId); + } else { + alert(I18n.localuser.noToken()); checkbox.setState(false); } - }else{ + } else { delete this.perminfo.applications[appId]; - this.userinfo.updateLocal(); } }); - form.addButtonInput("",I18n.getTranslation("localuser.advancedBot"),()=>{ - const token=this.botTokens.get(appId); - if(token){ - const botc=new Bot(bot,token,this); + form.addButtonInput("", I18n.localuser.advancedBot(), () => { + const token = this.botTokens.get(appId); + if (token) { + //TODO check if this is actually valid or not + const botc = new Bot(bot as unknown as mainuserjson, token, this); botc.settings(); } }); - form.addButtonInput("",I18n.getTranslation("localuser.botInviteCreate"),()=>{ - Bot.InviteMaker(appId,form,this.info); - }) + form.addButtonInput("", I18n.localuser.botInviteCreate(), () => { + Bot.InviteMaker(appId, form, this.info); + }); } - readonly autofillregex=Object.freeze(/[@#:]([a-z0-9 ]*)$/i); - mdBox(){ - interface CustomHTMLDivElement extends HTMLDivElement {markdown: MarkDown;} + //TODO make this an option + readonly autofillregex = Object.freeze(/(^|\s|\n)[@#:]([a-zA-Z0-9]*)$/i); + mdBox() { + const typebox = document.getElementById("typebox") as CustomHTMLDivElement; + const typeMd = typebox.markdown; + typeMd.owner = this; + typeMd.onUpdate = (str, pre) => { + this.search(document.getElementById("searchOptions") as HTMLDivElement, typeMd, str, pre); + if (str && str !== "\n") { + typebox.parentElement!.classList.remove("noConent"); + } else { + typebox.parentElement!.classList.add("noConent"); + } + }; + } + async pinnedClick(rect: DOMRect) { + if (!this.channelfocus) return; + await this.channelfocus.pinnedClick(rect); + } + async makeStickerBox(rect: DOMRect) { + const sticker = await Sticker.stickerPicker( + -0 + rect.right - window.innerWidth, + -20 + rect.top - window.innerHeight, + this, + ); + this.favorites.addStickerFreq(sticker.id); + console.log(sticker); + if (this.channelfocus) { + this.channelfocus.sendMessage("", { + embeds: [], + attachments: [], + sticker_ids: [sticker.id], + replyingto: this.channelfocus.replyingto, + }); + this.channelfocus.replyingto = null; + } + } + + async makeGifBox(rect: DOMRect) { + interface fullgif { + id: string; + title: string; + url: string; + src: string; + gif_src: string; + width: number; + height: number; + preview: string; + } + const menu = document.createElement("div"); + menu.classList.add("flexttb", "gifmenu"); + menu.style.bottom = window.innerHeight - rect.top + 15 + "px"; + menu.style.right = window.innerWidth - rect.right + "px"; + document.body.append(menu); + Contextmenu.keepOnScreen(menu); + Contextmenu.declareMenu(menu); + const trending = (await ( + await fetch( + this.info.api + "/gifs/trending?" + new URLSearchParams([["locale", I18n.lang]]), + {headers: this.headers}, + ) + ).json()) as { + categories: { + name: string; + src: string; + }[]; + gifs: [fullgif]; + }; + const gifbox = document.createElement("div"); + gifbox.classList.add("gifbox"); + const search = document.createElement("input"); + let gifs = gifbox; + const placeGifs = ( + gifs: HTMLDivElement, + gifReturns: {src: string; width: number; height: number; title?: string}[], + ) => { + const width = menu.getBoundingClientRect().width; + let left = 0; + let right = width < 370 ? Infinity : 0; + console.warn(right, width); + for (const gif of gifReturns) { + const div = document.createElement("div"); + div.classList.add("gifBox"); + const img = createImg(gif.src); + this.refreshIfNeeded(gif.src).then((url) => { + if (url === gif.src) return; + img.setSrcs(url); + }); + if (gif.title) img.alt = gif.title; + const scale = gif.width / 196; + + img.width = gif.width / scale; + img.height = gif.height / scale; + div.append(img); + + if (left <= right) { + div.style.top = left + "px"; + left += Math.ceil(img.height) + 10; + div.style.left = "5px"; + } else { + div.style.top = right + "px"; + right += Math.ceil(img.height) + 10; + div.style.left = "210px"; + } + + gifs.append(div); + + div.onclick = () => { + if (this.channelfocus) { + this.channelfocus.sendMessage(gif.src, { + embeds: [], + attachments: [], + sticker_ids: [], + replyingto: this.channelfocus.replyingto, + }); + menu.remove(); + this.channelfocus.replyingto = null; + } + }; + } + gifs.style.height = (right == Infinity ? left : Math.max(left, right)) + "px"; + }; + const searchBox = async () => { + gifs.remove(); + if (search.value === "") { + menu.append(gifbox); + gifs = gifbox; + return; + } + gifs = document.createElement("div"); + gifs.classList.add("gifbox"); + menu.append(gifs); + const sValue = search.value; + const gifReturns = (await ( + await fetch( + this.info.api + + "/gifs/search?" + + new URLSearchParams([ + ["locale", I18n.lang], + ["q", sValue], + ["limit", "500"], + ]), + {headers: this.headers}, + ) + ).json()) as fullgif[]; + if (sValue !== search.value) { + return; + } + placeGifs( + gifs, + gifReturns.map((gif) => { + return {src: gif.gif_src, width: gif.width, height: gif.height, title: gif.title}; + }), + ); + }; + let last = ""; + search.onkeyup = () => { + if (last === search.value) { + return; + } + last = search.value; + searchBox(); + }; + search.classList.add("searchGifBar"); + //TODO fix this once we swap over + search.placeholder = I18n.searchGifs("Tenor"); + const favs = this.favorites.favoriteGifs(); + if (favs.length) { + favs.forEach(async (_) => (_.src = await this.refreshIfNeeded(_.src))); + + const div = document.createElement("div"); + div.classList.add("gifPreviewBox"); + const img = document.createElement("img"); + img.src = favs[0].src; + img.src = await this.refreshIfNeeded(img.src); + const title = document.createElement("span"); + title.textContent = I18n.favoriteGifs(); + div.append(img, title); + gifbox.append(div); + div.onclick = (e) => { + e.stopImmediatePropagation(); + search.remove(); + gifs.remove(); + gifs = document.createElement("div"); + gifs.classList.add("gifbox"); + + const div = document.createElement("div"); + div.classList.add("flexltr", "title"); + + const back = document.createElement("span"); + back.classList.add("svg-leftArrow"); + back.onclick = (e) => { + e.stopImmediatePropagation(); + div.remove(); + gifs.remove(); + gifs = gifbox; + menu.append(search, gifbox); + }; + const title = document.createElement("h3"); + title.textContent = I18n.favoriteGifs(); + div.append(back, title); + + menu.append(div, gifs); + placeGifs(gifs, favs); + }; + } + for (const category of trending.categories) { + const div = document.createElement("div"); + div.classList.add("gifPreviewBox"); + const img = document.createElement("img"); + img.src = category.src; + const title = document.createElement("span"); + title.textContent = category.name; + div.append(img, title); + gifbox.append(div); + div.onclick = (e) => { + e.stopImmediatePropagation(); + search.value = category.name; + searchBox(); + }; + } + menu.append(search, gifbox); + search.focus(); + } + async TBEmojiMenu(rect: DOMRect) { const typebox = document.getElementById("typebox") as CustomHTMLDivElement; - const typeMd=typebox.markdown; - typeMd.owner=this; - typeMd.onUpdate=(str,pre)=>{ - this.search(document.getElementById("searchOptions") as HTMLDivElement,typeMd,str,pre); - } - } - MDReplace(replacewith:string,original:string,typebox:MarkDown){ - let raw=typebox.rawString; - raw=raw.split(original)[1]; - if(raw===undefined) return; - raw=original.replace(this.autofillregex,"")+replacewith+raw; - console.log(raw); - console.log(replacewith); - console.log(original); + const p = saveCaretPosition(typebox); + if (!p) return; + const original = MarkDown.getText(); + + const emoji = await Emoji.emojiPicker( + -0 + rect.right - window.innerWidth, + -20 + rect.top - window.innerHeight, + this, + ); + this.favorites.addEmoji(emoji.id || (emoji.emoji as string)); + p(); + const md = typebox.markdown; + this.MDReplace( + emoji.id + ? `<${emoji.animated ? "a" : ""}:${emoji.name}:${emoji.id}>` + : (emoji.emoji as string), + original, + md, + null, + ); + } + MDReplace( + replacewith: string, + original: string, + typebox: MarkDown, + start: RegExp | null = this.autofillregex, + ) { + let raw = typebox.rawString; + let empty = raw.length === 0; + raw = original !== "" ? raw.split(original)[1] : raw; + if (raw === undefined && !empty) return; + if (empty) { + raw = ""; + } + raw = (start ? original.replace(start, "") : original) + " " + replacewith + raw; + typebox.txt = raw.split(""); - const match=original.match(this.autofillregex); - if(match){ - typebox.boxupdate(replacewith.length-match[0].length); - } - } - MDSearchOptions(options:[string,string,void|HTMLElement][],original:string,div:HTMLDivElement,typebox:MarkDown){ - if(!div)return; - div.innerHTML=""; - let i=0; - const htmloptions:HTMLSpanElement[]=[]; - for(const thing of options){ - if(i==8){ + const match = start ? original.match(start) : true; + if (match) { + typebox.boxupdate( + replacewith.length - (match === true ? 0 : match[0].length) + 1, + false, + original.length, + ); + } + } + fileExtange!: ( + files: Blob[], + html: WeakMap, + ) => [Blob[], WeakMap]; + MDSearchOptions( + options: ( + | [string, string, void | HTMLElement] + | [string, string, void | HTMLElement, () => void | boolean] + )[], + original: string, + div: HTMLDivElement = document.getElementById("searchOptions") as HTMLDivElement, + typebox?: MarkDown, + ) { + if (!div) return; + div.innerHTML = ""; + let i = 0; + const htmloptions: HTMLSpanElement[] = []; + for (const [name, replace, elm, func] of options) { + if (i == 8) { break; } i++; - const span=document.createElement("span"); + const span = document.createElement("span"); htmloptions.push(span); - if(thing[2]){ - span.append(thing[2]); + if (elm) { + span.append(elm); } - span.append(thing[0]); - span.onclick=(e)=>{ - - if(e){ - const selection = window.getSelection() as Selection; - const box=typebox.box.deref(); - if(!box) return; - if(selection){ - console.warn(original); - - const pos = getTextNodeAtPosition(box, original.length-(original.match(this.autofillregex) as RegExpMatchArray)[0].length+thing[1].length); - selection.removeAllRanges(); - const range = new Range(); - range.setStart(pos.node, pos.position); - selection.addRange(range); + span.append(name); + span.onclick = (e) => { + if (e) { + if (replace) { + const selection = window.getSelection() as Selection; + const box = typebox?.box.deref(); + if (!box) return; + if (selection) { + const pos = getTextNodeAtPosition( + box, + original.length - + (original.match(this.autofillregex) as RegExpMatchArray)[0].length + + replace.length, + ); + selection.removeAllRanges(); + const range = new Range(); + range.setStart(pos.node, pos.position); + selection.addRange(range); + } + box.focus(); } e.preventDefault(); - box.focus(); } - this.MDReplace(thing[1],original,typebox); - div.innerHTML=""; + if (!func?.() && typebox) { + this.MDReplace(replace, original, typebox); + } + div.innerHTML = ""; remove(); - } + }; div.prepend(span); } - const remove=()=>{ - if(div&&div.innerHTML===""){ - this.keyup=()=>false; - this.keydown=()=>{}; + const remove = () => { + if (div && div.innerHTML === "") { + this.keyup = () => false; + this.keydown = () => {}; return true; } return false; - } - if(htmloptions[0]){ - let curindex=0; - let cur=htmloptions[0]; + }; + if (htmloptions[0]) { + let curindex = 0; + let cur = htmloptions[0]; cur.classList.add("selected"); - const cancel=new Set(["ArrowUp","ArrowDown","Enter","Tab"]); - this.keyup=(event)=>{ - if(remove()) return false; - if(cancel.has(event.key)){ - switch(event.key){ + const cancel = new Set(["ArrowUp", "ArrowDown", "Enter", "Tab"]); + this.keyup = (event) => { + if (remove()) return false; + + if (cancel.has(event.key)) { + switch (event.key) { case "ArrowUp": - if(htmloptions[curindex+1]){ + if (htmloptions[curindex + 1]) { cur.classList.remove("selected"); curindex++; - cur=htmloptions[curindex]; + cur = htmloptions[curindex]; cur.classList.add("selected"); } break; case "ArrowDown": - if(htmloptions[curindex-1]){ + if (htmloptions[curindex - 1]) { cur.classList.remove("selected"); curindex--; - cur=htmloptions[curindex]; + cur = htmloptions[curindex]; cur.classList.add("selected"); } break; case "Enter": case "Tab": - //@ts-ignore - cur.onclick(); + cur.click(); break; } return true; } return false; - } - this.keydown=(event)=>{ - if(remove()) return; - if(cancel.has(event.key)){ + }; + this.keydown = (event) => { + if (remove()) return; + if (cancel.has(event.key)) { event.preventDefault(); } - } - }else{ + }; + } else { remove(); } } - MDFindChannel(name:string,orginal:string,box:HTMLDivElement,typebox:MarkDown){ - const maybe:[number,Channel][]=[]; - if(this.lookingguild&&this.lookingguild.id!=="@me"){ - for(const channel of this.lookingguild.channels){ - const confidence=channel.similar(name); - if(confidence>0){ - maybe.push([confidence,channel]); + MDFindChannel(name: string, original: string, box: HTMLDivElement, typebox: MarkDown) { + const maybe: [number, Channel][] = []; + if (this.lookingguild && this.lookingguild.id !== "@me") { + for (const channel of this.lookingguild.channels) { + const confidence = channel.similar(name); + if (confidence > 0) { + maybe.push([confidence, channel]); } } } - maybe.sort((a,b)=>b[0]-a[0]); - this.MDSearchOptions(maybe.map((a)=>["# "+a[1].name,`<#${a[1].id}> `,undefined]),orginal,box,typebox); - } - async getUser(id:string){ - if(this.userMap.has(id)){ - return this.userMap.get(id) as User; - } - return new User(await (await fetch(this.info.api+"/users/"+id)).json(),this); + maybe.sort((a, b) => b[0] - a[0]); + this.MDSearchOptions( + maybe.map((a) => ["# " + a[1].name, `<#${a[1].id}> `, undefined]), + original, + box, + typebox, + ); } - MDFineMentionGen(name:string,original:string,box:HTMLDivElement,typebox:MarkDown){ - let members:[Member,number][]=[]; - if(this.lookingguild){ - for(const member of this.lookingguild.members){ - const rank=member.compare(name); - if(rank>0){ - members.push([member,rank]) + MDFineMentionGen(name: string, original: string, box: HTMLDivElement, typebox: MarkDown) { + let members: [Member | Role | User | "@everyone" | "@here", number][] = []; + if (this.lookingguild && name !== "everyone" && name !== "here") { + if (this.lookingguild.id === "@me") { + const dirrect = this.channelfocus as Group; + + for (const user of dirrect.users) { + const rank = user.compare(name); + if (rank > 0) { + members.push([user, rank]); + } } - } - } - members.sort((a,b)=>b[1]-a[1]); - this.MDSearchOptions(members.map((a)=>["@"+a[0].name,`<@${a[0].id}> `,undefined]),original,box,typebox); - } - MDFindMention(name:string,original:string,box:HTMLDivElement,typebox:MarkDown){ - if(this.ws&&this.lookingguild){ - this.MDFineMentionGen(name,original,box,typebox); - const nonce=Math.floor(Math.random()*10**8)+""; - if(this.lookingguild.member_count<=this.lookingguild.members.size) return; - this.ws.send(JSON.stringify( - {op:8, - d:{ - guild_id:[this.lookingguild.id], - query:name, - limit:8, - presences:true, - nonce + } else { + for (const member of this.lookingguild.members) { + const rank = member.compare(name); + if (rank > 0) { + members.push([member, rank]); } } - )); - this.searchMap.set(nonce,async (e)=>{ - console.log(e); - if(e.members&&e.members[0]){ - if(e.members[0].user){ - for(const thing of e.members){ - await Member.new(thing,this.lookingguild as Guild) - } - }else{ - const prom1:Promise[]=[]; - for(const thing of e.members){ - prom1.push(this.getUser(thing.id)); - } - Promise.all(prom1); - for(const thing of e.members){ - if(!this.userMap.has(thing.id)){ - console.warn("Dumb server bug for this member",thing); - continue; - } - await Member.new(thing,this.lookingguild as Guild) - } + for (const role of this.lookingguild.roles.filter((_) => _.id !== this.lookingguild?.id)) { + const rank = role.compare(name); + if (rank > 0) { + members.push([role, rank]); } - this.MDFineMentionGen(name,original,box,typebox); } - }) + } + function similar(str2: string | null | undefined) { + if (!str2) return 0; + const strl = Math.max(name.length, 1); + if (str2.includes(name)) { + return strl / str2.length; + } else if (str2.toLowerCase().includes(name.toLowerCase())) { + return strl / str2.length / 1.2; + } + return 0; + } + const everyoneScore = similar("everyone"); + if (everyoneScore) members.push(["@everyone", everyoneScore]); + const hereScore = similar("here"); + if (hereScore) members.push(["@here", hereScore]); } + members.sort((a, b) => b[1] - a[1]); + this.MDSearchOptions( + members.map((a) => [ + typeof a[0] === "string" ? a[0] : "@" + a[0].name, + a[0] instanceof Role + ? `<@&${a[0].id}> ` + : typeof a[0] === "string" + ? a[0] + " " + : `<@${a[0].id}> `, + undefined, + ]), + original, + box, + typebox, + ); } - findEmoji(search:string,orginal:string,box:HTMLDivElement,typebox:MarkDown){ - const emj=Emoji.searchEmoji(search,this,10); - const map=emj.map(([emoji]):[string,string,HTMLElement]=>{ - return [emoji.name,emoji.id?`<${emoji.animated?"a":""}:${emoji.name}:${emoji.id}>`:emoji.emoji as string,emoji.getHTML()] - }) - this.MDSearchOptions(map,orginal,box,typebox); + MDFindMention(name: string, original: string, box: HTMLDivElement, typebox: MarkDown) { + if (this.ws && this.lookingguild) { + this.MDFineMentionGen(name, original, box, typebox); + if (this.lookingguild.member_count <= this.lookingguild.members.size) return; + if (this.lookingguild.id !== "@me") { + this.lookingguild.searchMembers(8, name).then(async () => { + if (!typebox.rawString.startsWith(original)) return; + this.MDFineMentionGen(name, original, box, typebox); + }); + } + } + } + findEmoji(search: string, original: string, box: HTMLDivElement, typebox: MarkDown) { + const emj = Emoji.searchEmoji(search, this, 10); + const map = emj.map(([emoji]): [string, string, HTMLElement, () => void] => { + return [ + emoji.name, + emoji.id + ? `<${emoji.animated ? "a" : ""}:${emoji.name}:${emoji.id}>` + : (emoji.emoji as string), + emoji.getHTML(), + () => { + this.favorites.addEmoji(emoji.id || (emoji.emoji as string)); + }, + ]; + }); + this.MDSearchOptions(map, original, box, typebox); } - search(box:HTMLDivElement,md:MarkDown,str:string,pre:boolean){ - if(!pre){ - const match=str.match(this.autofillregex); + async findCommands(search: string, box: HTMLDivElement, md: MarkDown) { + const guild = this.lookingguild; + if (!guild) return; + const commands = await guild.getCommands(); + const sorted = commands + .map((_) => [_, _.similar(search)] as const) + .filter((_) => _[1] !== 0) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10); - if(match){ - const [type, search]=[match[0][0],match[0].split(/@|#|:/)[1]]; - switch(type){ + this.MDSearchOptions( + sorted.map(([elm]) => { + return [ + `/${elm.localizedName}`, + "", + undefined, + () => { + this.channelfocus?.startCommand(elm); + return true; + }, + ] as const; + }), + "", + box, + md, + ); + console.log(sorted, search); + } + search(box: HTMLDivElement, md: MarkDown, str: string, pre: boolean) { + if (!pre) { + const match = str.match(this.autofillregex); + + if (match) { + const trim = match[0].trim(); + const [type, search] = [trim[0], trim.split(/@|#|:/)[1]]; + switch (type) { case "#": - this.MDFindChannel(search,str,box,md); + this.MDFindChannel(search, str, box, md); break; case "@": - this.MDFindMention(search,str,box,md); + this.MDFindMention(search, str, box, md); break; case ":": - if(search.length>=2){ - this.findEmoji(search,str,box,md) - }else{ - this.MDSearchOptions([],"",box,md); + if (search.length >= 2) { + this.findEmoji(search, str, box, md); + } else { + this.MDSearchOptions([], "", box, md); } break; + default: + return; } - return + return; + } + const command = str.match(/^\/((\s*[\w\d]+)*)$/); + if (command) { + const search = command[1]; + this.findCommands(search, box, md); } } - box.innerHTML=""; + box.innerHTML = ""; + } + searching = false; + updateTranslations() { + const searchBox = document.getElementById("searchBox") as HTMLDivElement; + searchBox.style.setProperty("--hint-text", JSON.stringify(I18n.search.search())); + } + curSearch?: Symbol; + mSearch(query: string) { + const searchy = Symbol("search"); + this.curSearch = searchy; + const p = new URLSearchParams("?"); + this.searching = true; + p.set("content", query.trim()); + p.set("sort_by", "timestamp"); + p.set("sort_order", "desc"); + let maxpage: undefined | number = undefined; + const sideDiv = document.getElementById("sideDiv"); + const sideContainDiv = document.getElementById("sideContainDiv"); + if (!sideDiv || !sideContainDiv) return; + const genPage = (page: number) => { + p.set("offset", page * 50 + ""); + fetch(this.info.api + `/guilds/${this.lookingguild?.id}/messages/search/?` + p.toString(), { + headers: this.headers, + }) + .then((_) => _.json()) + .then((json: {messages: [messagejson][]; total_results: number}) => { + if (this.curSearch !== searchy) { + return; + } + //FIXME total_results shall be ignored as it's known to be bad, spacebar bug. + const messages = json.messages + .map(([m]) => { + const c = this.channelids.get(m.channel_id); + if (!c) return; + if (c.messages.get(m.id)) { + return c.messages.get(m.id); + } + return new Message(m, c, true); + }) + .filter((_) => _ !== undefined); + sideDiv.innerHTML = ""; + if (messages.length == 0 && page !== 0) { + maxpage = page - 1; + genPage(page - 1); + return; + } else if (messages.length !== 50) { + maxpage = page; + } + const sortBar = document.createElement("div"); + sortBar.classList.add("flexltr", "sortBar"); + + const newB = document.createElement("button"); + const old = document.createElement("button"); + [newB.textContent, old.textContent] = [I18n.search.new(), I18n.search.old()]; + old.onclick = () => { + p.set("sort_order", "asc"); + deleteMessages(); + genPage(0); + }; + newB.onclick = () => { + p.set("sort_order", "desc"); + deleteMessages(); + genPage(0); + }; + if (p.get("sort_order") === "asc") { + old.classList.add("selectedB"); + } else { + newB.classList.add("selectedB"); + } + + const spaceElm = document.createElement("div"); + spaceElm.classList.add("spaceElm"); + + sortBar.append(I18n.search.page(page + 1 + ""), spaceElm, newB, old); + + sideDiv.append(sortBar); + + sideContainDiv.classList.add("searchDiv"); + let channel: Channel | undefined = undefined; + function deleteMessages() { + for (const elm of htmls) elm.remove(); + } + const htmls: HTMLElement[] = []; + sideContainDiv.classList.remove("hideSearchDiv"); + for (const message of messages) { + if (channel !== message.channel) { + channel = message.channel; + const h3 = document.createElement("h3"); + h3.textContent = channel.name; + h3.classList.add("channelSTitle"); + sideDiv.append(h3); + htmls.push(h3); + } + const html = message.buildhtml(undefined, true); + if (message.div) console.error(message.div); + html.addEventListener("click", async () => { + try { + sideContainDiv.classList.add("hideSearchDiv"); + await message.channel.focus(message.id); + } catch (e) { + console.error(e); + } + }); + sideDiv.append(html); + htmls.push(html); + } + if (messages.length === 0) { + const noMs = document.createElement("h3"); + noMs.textContent = I18n.search.nofind(); + sideDiv.append(noMs); + } + const bottombuttons = document.createElement("div"); + bottombuttons.classList.add("flexltr", "searchNavButtons"); + const next = document.createElement("button"); + if (page == maxpage) next.disabled = true; + next.onclick = () => { + deleteMessages(); + genPage(page + 1); + }; + const prev = document.createElement("button"); + prev.onclick = () => { + deleteMessages(); + genPage(page - 1); + }; + if (page == 0) prev.disabled = true; + [next.textContent, prev.textContent] = [I18n.search.next(), I18n.search.back()]; + bottombuttons.append(prev, next); + sideDiv.append(bottombuttons); + sideDiv.scrollTo({top: 0, behavior: "instant"}); + }); + }; + if (query === "") { + sideContainDiv.classList.remove("searchDiv"); + sideContainDiv.classList.remove("hideSearchDiv"); + sideDiv.innerHTML = ""; + this.searching = false; + this.getSidePannel(); + return; + } + genPage(0); + } + + keydown: (event: KeyboardEvent) => unknown = () => {}; + keyup: (event: KeyboardEvent) => boolean = () => false; + handleKeyUp(event: KeyboardEvent): boolean { + if (this.keyup(event)) { + return true; + } + if (event.key === "Escape") { + if (event.ctrlKey) { + this.lookingguild?.markAsRead(); + } else { + this.channelfocus?.readbottom(); + this.channelfocus?.goToBottom(); + } + return true; + } + + return false; } - keydown:(event:KeyboardEvent)=>unknown=()=>{}; - keyup:(event:KeyboardEvent)=>boolean=()=>false; //---------- resolving members code ----------- - readonly waitingmembers = new Map void>>(); + readonly waitingmembers = new Map< + string, + Map void> + >(); readonly presences: Map = new Map(); - async resolvemember( - id: string, - guildid: string - ): Promise{ - if(guildid === "@me"){ + static font?: FontFace; + static async loadFont() { + const prefs = await getPreferences(); + const fontName = prefs.emojiFont; + + if (this.font) { + //TODO see when/if this can be removed + //@ts-ignore this is stupid. it's been here since 2020 + document.fonts.delete(this.font); + } + + const realname = this.fonts.find((_) => _[1] === fontName)?.[0]; + if (realname) { + const font = new FontFace("emojiFont", `url("/emoji/${realname}")`); + await font.load(); + console.error("Loaded font:", fontName, "/", realname); + //TODO see when/if this can be removed + //@ts-ignore this is stupid. it's been here since 2020 + document.fonts.add(font); + console.log(font); + this.font = font; + } + } + static get fonts() { + return [ + ["NotoColorEmoji-Regular.ttf", "Noto Color Emoji"], + ["OpenMoji-color-glyf_colr_0.woff2", "OpenMoji"], + ["Twemoji-16.0.1.ttf", "Twemoji"], + ["BlobmojiCompat.ttf", "Blobmoji"], + ] as const; + } + getMemberMap = new Map>(); + async getMember(id: string, guildid: string): Promise { + const user = this.userMap.get(id); + const guild = this.guildids.get(guildid) as Guild; + if (user) { + const memb = user.members.get(guild); + if (memb) return memb; + } + const uid = id + "-" + guildid; + const prom = this.getMemberMap.get(uid); + if (prom) return prom; + const prom2 = new Promise(async (res) => { + const json = await this.resolvemember(id, guildid); + if (!json) { + res(undefined); + return; + } + res(Member.new(json, guild)); + }); + this.getMemberMap.set(uid, prom2); + return prom2; + } + memberLock = new PromiseLock(); + async resolvemember(id: string, guildid: string): Promise { + if (guildid === "@me") { return undefined; } const guild = this.guildids.get(guildid); - const borked = true; - if( !guild || borked && guild.member_count > 250){ - //sorry puyo, I need to fix member resolving while it's broken on large guilds - try{ - const req = await fetch( - this.info.api + "/guilds/" + guildid + "/members/" + id, - { - headers: this.headers, - } - ); - if(req.status !== 200){ + //TODO well, maybe think this over, I set the member count and whatnot, maybe for "large enough" guilds I return a member that's filled out blankly unless its needed to be fully resolved, added stuff to identify to make it work + const borked = false; + if (!guild || (borked && guild.member_count > 250)) { + const unlock = await this.memberLock.acquireLock(); + try { + const req = await fetch(this.info.api + "/guilds/" + guildid + "/members/" + id, { + headers: this.headers, + }); + if (req.status !== 200) { return undefined; } return await req.json(); - }catch{ + } catch { return undefined; + } finally { + unlock(); } } let guildmap = this.waitingmembers.get(guildid); - if(!guildmap){ + if (!guildmap) { guildmap = new Map(); this.waitingmembers.set(guildid, guildmap); } - const promise: Promise = new Promise(res=>{ + const promise: Promise = new Promise((res) => { guildmap.set(id, res); this.getmembers(); }); @@ -1971,14 +4633,17 @@ class Localuser{ fetchingmembers: Map = new Map(); noncemap: Map void> = new Map(); noncebuild: Map = new Map(); - searchMap=new Mapunknown>(); + searchMap = new Map< + string, + (arg: { + chunk_index: number; + chunk_count: number; + nonce: string; + not_found?: string[]; + members?: memberjson[]; + presences: presencejson[]; + }) => unknown + >(); async gotChunk(chunk: { chunk_index: number; chunk_count: number; @@ -1986,85 +4651,83 @@ class Localuser{ not_found?: string[]; members?: memberjson[]; presences: presencejson[]; - }){ - for(const thing of chunk.presences){ - if(thing.user){ + }) { + for (const thing of chunk.presences) { + if (thing.user) { this.presences.set(thing.user.id, thing); } } - if(this.searchMap.has(chunk.nonce)){ - const func=this.searchMap.get(chunk.nonce); + if (this.searchMap.has(chunk.nonce)) { + const func = this.searchMap.get(chunk.nonce); this.searchMap.delete(chunk.nonce); - if(func){ + if (func) { func(chunk); return; } } chunk.members ??= []; const arr = this.noncebuild.get(chunk.nonce); - if(!arr)return; + if (!arr) return; arr[0] = arr[0].concat(chunk.members); - if(chunk.not_found){ + if (chunk.not_found) { arr[1] = chunk.not_found; } arr[2].push(chunk.chunk_index); - if(arr[2].length === chunk.chunk_count){ + if (arr[2].length === chunk.chunk_count) { this.noncebuild.delete(chunk.nonce); const func = this.noncemap.get(chunk.nonce); - if(!func)return; + if (!func) return; func([arr[0], arr[1]]); this.noncemap.delete(chunk.nonce); } } - async getmembers(){ - const promise = new Promise(res=>{ + async getmembers() { + const promise = new Promise((res) => { setTimeout(res, 10); }); await promise; //allow for more to be sent at once :P - if(this.ws){ - this.waitingmembers.forEach(async (value, guildid)=>{ + if (this.ws) { + this.waitingmembers.forEach(async (value, guildid) => { const keys = value.keys(); - if(this.fetchingmembers.has(guildid)){ + if (this.fetchingmembers.has(guildid)) { return; } const build: string[] = []; - for(const key of keys){ + for (const key of keys) { build.push(key); - if(build.length === 100){ + if (build.length === 100) { break; } } - if(!build.length){ + if (!build.length) { this.waitingmembers.delete(guildid); return; } - const promise: Promise<[memberjson[], string[]]> = new Promise( - res=>{ - const nonce = "" + Math.floor(Math.random() * 100000000000); - this.noncemap.set(nonce, res); - this.noncebuild.set(nonce, [[], [], []]); - if(!this.ws)return; - this.ws.send( - JSON.stringify({ - op: 8, - d: { - user_ids: build, - guild_id: guildid, - limit: 100, - nonce, - presences: true, - }, - }) - ); - this.fetchingmembers.set(guildid, true); - } - ); + const promise: Promise<[memberjson[], string[]]> = new Promise((res) => { + const nonce = "" + Math.floor(Math.random() * 100000000000); + this.noncemap.set(nonce, res); + this.noncebuild.set(nonce, [[], [], []]); + if (!this.ws) return; + this.ws.send( + JSON.stringify({ + op: 8, + d: { + user_ids: build, + guild_id: guildid, + limit: 100, + nonce, + presences: true, + }, + }), + ); + this.fetchingmembers.set(guildid, true); + }); const prom = await promise; const data = prom[0]; - for(const thing of data){ - if(value.has(thing.id)){ + for (const thing of data) { + if (value.has(thing.id)) { const func = value.get(thing.id); - if(!func){ + if (!func) { value.delete(thing.id); continue; } @@ -2072,10 +4735,10 @@ class Localuser{ value.delete(thing.id); } } - for(const thing of prom[1]){ - if(value.has(thing)){ + for (const thing of prom[1]) { + if (value.has(thing)) { const func = value.get(thing); - if(!func){ + if (!func) { value.delete(thing); continue; } @@ -2088,11 +4751,11 @@ class Localuser{ }); } } - async pingEndpoint(){ + async pingEndpoint() { const userInfo = getBulkInfo(); - if(!userInfo.instances) userInfo.instances = {}; + if (!userInfo.instances) userInfo.instances = {}; const wellknown = this.info.wellknown; - if(!userInfo.instances[wellknown]){ + if (!userInfo.instances[wellknown]) { const pingRes = await fetch(this.info.api + "/ping"); const pingJSON = await pingRes.json(); userInfo.instances[wellknown] = pingJSON; @@ -2102,22 +4765,104 @@ class Localuser{ this.pageTitle("Loading..."); } - pageTitle(channelName = "", guildName = ""){ + pageTitle(channelName = "", guildName = "") { (document.getElementById("channelname") as HTMLSpanElement).textContent = channelName; - (document.getElementsByTagName("title")[0] as HTMLTitleElement).textContent = channelName + (guildName ? " | " + guildName : "") + " | " + this.instancePing.name +" | Jank Client"; + (document.getElementsByTagName("title")[0] as HTMLTitleElement).textContent = + channelName + + (guildName ? " | " + guildName : "") + + " | " + + this.instancePing.name + + " | Fermi"; } - async instanceStats(){ + async instanceStats() { + const dialog = new Dialog(""); + dialog.options.addTitle(I18n.instanceStats.name(this.instancePing.name)); + dialog.show(); const res = await fetch(this.info.api + "/policies/stats", { headers: this.headers, }); const json = await res.json(); - const dialog = new Dialog(""); - dialog.options.addTitle(I18n.getTranslation("instanceStats.name",this.instancePing.name)); - dialog.options.addText(I18n.getTranslation("instanceStats.users",json.counts.user)); - dialog.options.addText(I18n.getTranslation("instanceStats.servers",json.counts.guild)); - dialog.options.addText(I18n.getTranslation("instanceStats.messages",json.counts.message)); - dialog.options.addText(I18n.getTranslation("instanceStats.members",json.counts.members)); - dialog.show(); + dialog.options.addText(I18n.instanceStats.users(json.counts.user)); + dialog.options.addText(I18n.instanceStats.servers(json.counts.guild)); + dialog.options.addText(I18n.instanceStats.messages(json.counts.message)); + dialog.options.addText(I18n.instanceStats.members(json.counts.members)); + } + + async refreshIfNeeded(url: string) { + const urlObj = new URL(url); + if (urlObj.host === new URL(this.info.cdn).host) { + if (urlObj.searchParams.get("ex")) { + if (Number.parseInt(urlObj.searchParams.get("ex") || "", 16) >= Date.now() - 5000) { + return url; + } + } + const newUrl = this.refreshURL(url); + newUrl.then((_) => (url = _)); + return newUrl; + } + return url; + } + + refreshTimeOut?: NodeJS.Timeout; + urlsToRefresh: [string, (arg: string) => void][] = []; + refreshURL(url: string): Promise { + if (!this.refreshTimeOut) { + this.refreshTimeOut = setTimeout(async () => { + const refreshes = this.urlsToRefresh; + this.urlsToRefresh = []; + delete this.refreshTimeOut; + const res = await fetch(this.info.api + "/attachments/refresh-urls", { + method: "POST", + body: JSON.stringify({attachment_urls: refreshes.map((_) => _[0])}), + headers: this.headers, + }); + const body: { + refreshed_urls: string[]; + } = await res.json(); + let i = 0; + for (const url of body.refreshed_urls) { + refreshes[i][1](url); + i++; + } + }, 100); + } + return new Promise((res) => { + this.urlsToRefresh.push([url, res]); + }); + } + getNotiVolume(): number { + const userinfos = getBulkInfo(); + return userinfos.preferences.volume ?? 20; + } + setNotificationVolume(volume: number) { + const userinfos = getBulkInfo(); + userinfos.preferences.volume = volume; + localStorage.setItem("userinfos", JSON.stringify(userinfos)); + } + setNotificationSound(sound: string) { + const userinfos = getBulkInfo(); + userinfos.preferences.notisound = sound; + localStorage.setItem("userinfos", JSON.stringify(userinfos)); + } + playSound(name = this.getNotificationSound()) { + const volume = this.getNotiVolume(); + if (this.play) { + const voice = this.play.tracks.includes(name); + if (voice) { + this.play.play(name, volume); + } else if (this.perminfo.sound && this.perminfo.sound.cSound) { + const audio = document.createElement("audio"); + audio.volume = volume / 100; + audio.src = this.perminfo.sound.cSound; + audio.play().catch(); + } + } else { + console.error("play object is missing"); + } + } + getNotificationSound() { + const userinfos = getBulkInfo(); + return userinfos.preferences.notisound; } } -export{ Localuser }; +export {Localuser}; diff --git a/src/webpage/login.html b/src/webpage/login.html index 836e06b0..41836334 100644 --- a/src/webpage/login.html +++ b/src/webpage/login.html @@ -1,61 +1,27 @@ - + - - - Jank Client - - - - - - - + + + Fermi + + + + + + + -
-

Login

-
- -

- - - - - - - -

- -
- -
- Don't have an account? -
- - + diff --git a/src/webpage/login.ts b/src/webpage/login.ts index a85a36af..d2f9fa66 100644 --- a/src/webpage/login.ts +++ b/src/webpage/login.ts @@ -1,674 +1,125 @@ -import { I18n } from "./i18n.js"; -import { Dialog, FormError } from "./settings.js"; - -const mobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); -const iOS = /iPhone|iPad|iPod/i.test(navigator.userAgent); - -let instances: -| { -name: string; -description?: string; -descriptionLong?: string; -image?: string; -url?: string; -display?: boolean; -online?: boolean; -uptime: { alltime: number; daytime: number; weektime: number }; -urls: { -wellknown: string; -api: string; -cdn: string; -gateway: string; -login?: string; -}; -}[] -| null; -const datalist = document.getElementById("instances"); -console.warn(datalist); -const instancefetch=fetch("/instances.json") - .then(res=>res.json()) - .then( - (json: { - name: string; - description?: string; - descriptionLong?: string; - image?: string; - url?: string; - display?: boolean; - online?: boolean; - uptime: { alltime: number; daytime: number; weektime: number }; - urls: { - wellknown: string; - api: string; - cdn: string; - gateway: string; - login?: string; - } - }[] - )=>{ - instances = json; - if(datalist){ - console.warn(json); - if(instancein && instancein.value === ""){ - instancein.value = json[0].name; - } - for(const instance of json){ - if(instance.display === false){ - continue; - } - const option = document.createElement("option"); - option.disabled = !instance.online; - option.value = instance.name; - if(instance.url){ - stringURLMap.set(option.value, instance.url); - if(instance.urls){ - stringURLsMap.set(instance.url, instance.urls); - } - }else if(instance.urls){ - stringURLsMap.set(option.value, instance.urls); - }else{ - option.disabled = true; - } - if(instance.description){ - option.label = instance.description; - }else{ - option.label = instance.name; - } - datalist.append(option); +import {InstanceInfo, adduser, Specialuser} from "./utils/utils.js"; +import {I18n} from "./i18n.js"; +import {Dialog, FormError} from "./settings.js"; +import {makeRegister} from "./register.js"; +import {trimTrailingSlashes} from "./utils/netUtils"; +function generateRecArea(recover = document.getElementById("recover")) { + if (!recover) return; + recover.innerHTML = ""; + const can = localStorage.getItem("canRecover"); + if (can) { + const a = document.createElement("a"); + a.textContent = I18n.login.recover(); + a.href = "/reset" + window.location.search; + recover.append(a); + } +} +const recMap = new Map>(); +async function recover(e: InstanceInfo, recover = document.getElementById("recover")) { + const prom = new Promise(async (res) => { + if (!recover) { + res(false); + return; + } + recover.innerHTML = ""; + try { + if (!(await recMap.get(e.api))) { + if (recMap.has(e.api)) { + throw Error("can't recover"); } - checkInstance(""); - } - } - ); -setTheme(); -await I18n.done -function setTheme(){ - let name = localStorage.getItem("theme"); - if(!name){ - localStorage.setItem("theme", "Dark"); - name = "Dark"; - } - document.body.className = name + "-theme"; -} - - -(async ()=>{ - await I18n.done - const instanceField=document.getElementById("instanceField"); - const emailField= document.getElementById("emailField"); - const pwField= document.getElementById("pwField"); - const loginButton=document.getElementById("loginButton"); - const noAccount=document.getElementById("switch") - if(instanceField&&emailField&&pwField&&loginButton&&noAccount){ - instanceField.textContent=I18n.getTranslation("htmlPages.instanceField"); - emailField.textContent=I18n.getTranslation("htmlPages.emailField"); - pwField.textContent=I18n.getTranslation("htmlPages.pwField"); - loginButton.textContent=I18n.getTranslation("htmlPages.loginButton"); - noAccount.textContent=I18n.getTranslation("htmlPages.noAccount"); - } -})() - -function getBulkUsers(){ - const json = getBulkInfo(); - for(const thing in json.users){ - json.users[thing] = new Specialuser(json.users[thing]); - } - return json; -} -function trimswitcher(){ - const json = getBulkInfo(); - const map = new Map(); - for(const thing in json.users){ - const user = json.users[thing]; - let wellknown = user.serverurls.wellknown; - if(wellknown.at(-1) !== "/"){ - wellknown += "/"; - } - wellknown =(user.id||user.email)+"@"+wellknown; - if(map.has(wellknown)){ - const otheruser = map.get(wellknown); - if(otheruser[1].serverurls.wellknown.at(-1) === "/"){ - delete json.users[otheruser[0]]; - map.set(wellknown, [thing, user]); - }else{ - delete json.users[thing]; + recMap.set(e.api, prom); + const json = (await (await fetch(e.api + "/policies/instance/config")).json()) as { + can_recover_account: boolean; + }; + if (!json || !json.can_recover_account) throw Error("can't recover account"); } - }else{ - map.set(wellknown, [thing, user]); + res(true); + localStorage.setItem("canRecover", "true"); + generateRecArea(recover); + } catch { + res(false); + localStorage.removeItem("canRecover"); + generateRecArea(recover); + } finally { + res(false); } - } - for(const thing in json.users){ - if(thing.at(-1) === "/"){ - const user = json.users[thing]; - delete json.users[thing]; - json.users[thing.slice(0, -1)] = user; - } - } - localStorage.setItem("userinfos", JSON.stringify(json)); - console.log(json); + }); } -function getBulkInfo(){ - return JSON.parse(localStorage.getItem("userinfos") as string); -} -function setDefaults(){ - let userinfos = getBulkInfo(); - if(!userinfos){ - localStorage.setItem( - "userinfos", - JSON.stringify({ - currentuser: null, - users: {}, - preferences: { - theme: "Dark", - notifications: false, - notisound: "three", - }, - }) - ); - userinfos = getBulkInfo(); - } - if(userinfos.users === undefined){ - userinfos.users = {}; - } - if(userinfos.accent_color === undefined){ - userinfos.accent_color = "#3096f7"; - } - document.documentElement.style.setProperty( - "--accent-color", - userinfos.accent_color +export async function makeLogin( + trasparentBg = false, + instance = "", + handle?: (user: Specialuser) => void, +) { + const dialog = new Dialog(""); + const opt = dialog.options; + opt.addTitle(I18n.login.login()); + const picker = opt.addInstancePicker( + (info) => { + form.fetchURL = trimTrailingSlashes(info.api) + "/auth/login"; + recover(info, rec); + }, + { + instance, + }, ); - if(userinfos.preferences === undefined){ - userinfos.preferences = { - theme: "Dark", - notifications: false, - notisound: "three", - }; - } - if(userinfos.preferences && userinfos.preferences.notisound === undefined){ - console.warn("uhoh") - userinfos.preferences.notisound = "three"; - } - localStorage.setItem("userinfos", JSON.stringify(userinfos)); -} -setDefaults(); -class Specialuser{ - serverurls: { - api: string; - cdn: string; - gateway: string; - wellknown: string; - login: string; - }; - email: string; - token: string; - loggedin; - json; - constructor(json: any){ - if(json instanceof Specialuser){ - console.error("specialuser can't construct from another specialuser"); - } - this.serverurls = json.serverurls; - let apistring = new URL(json.serverurls.api).toString(); - apistring = apistring.replace(/\/(v\d+\/?)?$/, "") + "/v9"; - this.serverurls.api = apistring; - this.serverurls.cdn = new URL(json.serverurls.cdn) - .toString() - .replace(/\/$/, ""); - this.serverurls.gateway = new URL(json.serverurls.gateway) - .toString() - .replace(/\/$/, ""); - this.serverurls.wellknown = new URL(json.serverurls.wellknown) - .toString() - .replace(/\/$/, ""); - this.serverurls.login = new URL(json.serverurls.login) - .toString() - .replace(/\/$/, ""); - this.email = json.email; - this.token = json.token; - this.loggedin = json.loggedin; - this.json = json; - this.json.localuserStore ??= {}; - if(!this.serverurls || !this.email || !this.token){ - console.error( - "There are fundamentally missing pieces of info missing from this user" - ); - } - } - set pfpsrc(e){ - this.json.pfpsrc = e; - this.updateLocal(); - } - get pfpsrc(){ - return this.json.pfpsrc; - } - set username(e){ - this.json.username = e; - this.updateLocal(); - } - get username(){ - return this.json.username; - } - set localuserStore(e){ - this.json.localuserStore = e; - this.updateLocal(); - } - get localuserStore(){ - return this.json.localuserStore; - } - set id(e){ - this.json.id = e; - this.updateLocal(); - } - get id(){ - return this.json.id; - } - get uid(){ - return this.email + this.serverurls.wellknown; - } - toJSON(){ - return this.json; - } - updateLocal(){ - const info = getBulkInfo(); - info.users[this.uid] = this.toJSON(); - localStorage.setItem("userinfos", JSON.stringify(info)); - } -} -function adduser(user: typeof Specialuser.prototype.json){ - user = new Specialuser(user); - const info = getBulkInfo(); - info.users[user.uid] = user; - info.currentuser = user.uid; - localStorage.setItem("userinfos", JSON.stringify(info)); - return user; -} -const instancein = document.getElementById("instancein") as HTMLInputElement; -let timeout: ReturnType | string | number | undefined | null = null; -// let instanceinfo; -const stringURLMap = new Map(); - -const stringURLsMap = new Map< - string, - { - wellknown: string; - api: string; - cdn: string; - gateway: string; - login?: string; - } - >(); -async function getapiurls(str: string): Promise< - { - api: string; - cdn: string; - gateway: string; - wellknown: string; - login: string; - } - | false - >{ - function appendApi(str:string){ - return str.includes("api")?"" : (str.endsWith("/")? "api" : "/api"); - } - if(!URL.canParse(str)){ - const val = stringURLMap.get(str); - if(stringURLMap.size===0){ - await new Promise(res=>{ - setInterval(()=>{ - if(stringURLMap.size!==0){ - res(); - } - },100); - }); - } - if(val){ - str = val; - }else{ - const val = stringURLsMap.get(str); - if(val){ - const responce = await fetch( - val.api + (val.api.endsWith("/") ? "" : "/") + "ping" - ); - if(responce.ok){ - if(val.login){ - return val as { - wellknown: string; - api: string; - cdn: string; - gateway: string; - login: string; - }; - }else{ - val.login = val.api; - return val as { - wellknown: string; - api: string; - cdn: string; - gateway: string; - login: string; - }; - } + dialog.show(trasparentBg).parentElement!.style.zIndex = "200"; + + const form = opt.addForm( + "", + (res) => { + if ("token" in res && typeof res.token == "string") { + const u = adduser({ + serverurls: JSON.parse(localStorage.getItem("instanceinfo") as string), + email: email.value, + token: res.token, + }); + u.username = email.value; + if (handle) { + handle(u); + dialog.hide(); + return; } - } - } - } - if(str.at(-1) !== "/"){ - str += "/"; - } - let api: string; - try{ - const info = await fetch(`${str}.well-known/spacebar`).then(x=>x.json() - ); - api = info.api; - }catch{ - api=str; - } - if(!URL.canParse(api)){ - return false; - } - const url = new URL(api); - try{ - const info = await fetch( - `${api}${ - url.pathname.includes("api") ? "" : "api" - }/policies/instance/domains` - ).then(x=>x.json()); - const apiurl = new URL(info.apiEndpoint); - return{ - api: info.apiEndpoint+appendApi(apiurl.pathname), - gateway: info.gateway, - cdn: info.cdn, - wellknown: str, - login: info.apiEndpoint+appendApi(apiurl.pathname), - }; - }catch{ - const val = stringURLsMap.get(str); - if(val){ - const responce = await fetch( - val.api + (val.api.endsWith("/") ? "" : "/") + "ping" - ); - if(responce.ok){ - if(val.login){ - return val as { - wellknown: string; - api: string; - cdn: string; - gateway: string; - login: string; - }; - }else{ - val.login = val.api; - return val as { - wellknown: string; - api: string; - cdn: string; - gateway: string; - login: string; - }; + const redir = new URLSearchParams(window.location.search).get("goback"); + if (redir && (!URL.canParse(redir) || new URL(redir).host === window.location.host)) { + window.location.href = redir; + } else { + window.location.href = "/channels/@me"; } + } else { + //@ts-ignore + //TODO just type this to get rid of the ignore :P + const message = res.errors.at(0)._errors[0].message; + throw new FormError(password, message); } - } - return false; - } -} -async function checkInstance(instance?: string){ - await instancefetch; - const verify = document.getElementById("verify"); - try{ - verify!.textContent = I18n.getTranslation("login.checking"); - const instanceValue = instance || (instancein as HTMLInputElement).value; - const instanceinfo = (await getapiurls(instanceValue)) as { - wellknown: string; - api: string; - cdn: string; - gateway: string; - login: string; - value: string; - }; - if(instanceinfo){ - instanceinfo.value = instanceValue; - localStorage.setItem("instanceinfo", JSON.stringify(instanceinfo)); - verify!.textContent = I18n.getTranslation("login.allGood"); - // @ts-ignore - if(checkInstance.alt){ - // @ts-ignore - checkInstance.alt(); - } - setTimeout((_: any)=>{ - console.log(verify!.textContent); - verify!.textContent = ""; - }, 3000); - }else{ - verify!.textContent = I18n.getTranslation("login.invalid"); - } - }catch{ - console.log("catch"); - verify!.textContent = I18n.getTranslation("login.invalid"); - } -} - -if(instancein){ - console.log(instancein); - instancein.addEventListener("keydown", ()=>{ - const verify = document.getElementById("verify"); - verify!.textContent = I18n.getTranslation("login.waiting"); - if(timeout !== null && typeof timeout !== "string"){ - clearTimeout(timeout); - } - timeout = setTimeout(()=>checkInstance(), 1000); - }); - if(localStorage.getItem("instanceinfo")){ - const json = JSON.parse(localStorage.getItem("instanceinfo")!); - if(json.value){ - (instancein as HTMLInputElement).value = json.value; - }else{ - (instancein as HTMLInputElement).value = json.wellknown; - } - }else{ - checkInstance("https://spacebar.chat/"); - } -} - -async function login(username: string, password: string, captcha: string){ - if(captcha === ""){ - captcha = ""; - } - const options = { - method: "POST", - body: JSON.stringify({ - login: username, - password, - undelete: false, - captcha_key: captcha, - }), - headers: { - "Content-type": "application/json; charset=UTF-8", }, - }; - try{ - const info = JSON.parse(localStorage.getItem("instanceinfo")!); - const api = info.login + (info.login.startsWith("/") ? "/" : ""); - return await fetch(api + "/auth/login", options) - .then(response=>response.json()) - .then(response=>{ - console.log(response, response.message); - if(response.message === "Invalid Form Body"){ - return response.errors.login._errors[0].message; - console.log("test"); - } - //this.serverurls||!this.email||!this.token - console.log(response); - - if(response.captcha_sitekey){ - const capt = document.getElementById("h-captcha"); - if(capt!.children.length){ - eval("hcaptcha.reset()"); - }else{ - const capty = document.createElement("div"); - capty.classList.add("h-captcha"); - - capty.setAttribute("data-sitekey", response.captcha_sitekey); - const script = document.createElement("script"); - script.src = "https://js.hcaptcha.com/1/api.js"; - capt!.append(script); - capt!.append(capty); - } - }else{ - console.log(response); - if(response.ticket){ - const better=new Dialog(""); - const form=better.options.addForm("",(res:any)=>{ - if(res.message){ - throw new FormError(ti,res.message); - }else{ - console.warn(res); - if(!res.token)return; - adduser({ - serverurls: JSON.parse(localStorage.getItem("instanceinfo") as string), - email: username, - token: res.token, - }).username = username; - const redir = new URLSearchParams( - window.location.search - ).get("goback"); - if(redir){ - window.location.href = redir; - }else{ - window.location.href = "/channels/@me"; - } - } - },{ - fetchURL:api + "/auth/mfa/totp", - method:"POST", - headers:{ - "Content-Type": "application/json", - } - }); - form.addTitle(I18n.getTranslation("2faCode")); - const ti=form.addTextInput("","code"); - better.show() - }else{ - console.warn(response); - if(!response.token)return; - adduser({ - serverurls: JSON.parse(localStorage.getItem("instanceinfo")!), - email: username, - token: response.token, - }).username = username; - const redir = new URLSearchParams(window.location.search).get( - "goback" - ); - if(redir){ - window.location.href = redir; - }else{ - window.location.href = "/channels/@me"; - } - return""; - } - } - }); - }catch(error){ - console.error("Error:", error); - } -} - -async function check(e: SubmitEvent){ - e.preventDefault(); - const target = e.target as HTMLFormElement; - const h = await login( - (target[1] as HTMLInputElement).value, - (target[2] as HTMLInputElement).value, - (target[3] as HTMLInputElement).value + { + submitText: I18n.login.login(), + method: "POST", + headers: { + "Content-type": "application/json; charset=UTF-8", + }, + vsmaller: true, + }, ); - const wrongElement = document.getElementById("wrong"); - if(wrongElement){ - wrongElement.textContent = h; - } - console.log(h); -} -if(document.getElementById("form")){ - const form = document.getElementById("form"); - if(form){ - form.addEventListener("submit", (e: SubmitEvent)=>check(e)); - } -} -//this currently does not work, and need to be implemented better at some time. -if(!localStorage.getItem("SWMode")){ - localStorage.setItem("SWMode","true"); -} -class SW{ - static worker:undefined|ServiceWorker; - static setMode(mode:"false"|"offlineOnly"|"true"){ - localStorage.setItem("SWMode",mode); - if(this.worker){ - this.worker.postMessage({data:mode,code:"setMode"}); - } - } - static checkUpdate(){ - if(this.worker){ - this.worker.postMessage({code:"CheckUpdate"}); - } - } - static forceClear(){ - if(this.worker){ - this.worker.postMessage({code:"ForceClear"}); - } - } -} -export {SW}; -if ("serviceWorker" in navigator){ - navigator.serviceWorker.register("/service.js", { - scope: "/", - }).then((registration) => { - let serviceWorker:ServiceWorker|undefined; - if (registration.installing) { - serviceWorker = registration.installing; - console.log("installing"); - } else if (registration.waiting) { - serviceWorker = registration.waiting; - console.log("waiting"); - } else if (registration.active) { - serviceWorker = registration.active; - console.log("active"); - } - SW.worker=serviceWorker; - SW.setMode(localStorage.getItem("SWMode") as "false"|"offlineOnly"|"true"); - if (serviceWorker) { - console.log(serviceWorker.state); - serviceWorker.addEventListener("statechange", (_) => { - console.log(serviceWorker.state); - }); - } - }) -} - -const switchurl = document.getElementById("switch") as HTMLAreaElement; -if(switchurl){ - switchurl.href += window.location.search; - const instance = new URLSearchParams(window.location.search).get("instance"); - console.log(instance); - if(instance){ - instancein.value = instance; - checkInstance(""); - } + const button = form.button.deref(); + picker.giveButton(button); + button?.classList.add("createAccount"); + + const email = form.addTextInput(I18n.htmlPages.emailField(), "login"); + const password = form.addTextInput(I18n.htmlPages.pwField(), "password", {password: true}); + form.addCaptcha(); + const a = document.createElement("a"); + a.onclick = () => { + dialog.hide(); + makeRegister(trasparentBg, "", handle); + }; + a.textContent = I18n.htmlPages.noAccount(); + const rec = document.createElement("div"); + form.addHTMLArea(rec); + form.addHTMLArea(a); } -export{ checkInstance }; -trimswitcher(); -export{ - mobile, - iOS, - getBulkUsers, - getBulkInfo, - setTheme, - Specialuser, - getapiurls, - adduser, -}; - - -export function getInstances(){ - return instances; +await I18n.done; +if (window.location.pathname.startsWith("/login")) { + makeLogin(); } - - diff --git a/src/webpage/manifest.json b/src/webpage/manifest.json index f78e7cd8..9c36a32d 100644 --- a/src/webpage/manifest.json +++ b/src/webpage/manifest.json @@ -1,13 +1,17 @@ { - "name": "Jank Client", - "icons": [ - { - "src": "/logo.webp", - "sizes": "512x512" - } - ], - "start_url": "/channels/@me", - "display": "standalone", - "theme_color": "#05050a", - "offline_enabled": true + "name": "Fermi", + "icons": [ + { + "src": "/logo.webp", + "sizes": "512x512" + } + ], + "start_url": "/app", + "display": "standalone", + "scope": "/", + "theme_color": "#05050a", + "categories":["social"], + "description": "A lightweight chat app for all of your spacebar needs!", + "background_color": "#05050a", + "offline_enabled": true } diff --git a/src/webpage/markdown.ts b/src/webpage/markdown.ts index 5072e5c1..5087b825 100644 --- a/src/webpage/markdown.ts +++ b/src/webpage/markdown.ts @@ -1,99 +1,166 @@ -import{ Channel }from"./channel.js"; -import{ Emoji }from"./emoji.js"; -import{ Guild }from"./guild.js"; -import { I18n } from "./i18n.js"; -import{ Localuser }from"./localuser.js"; -import{ Member }from"./member.js"; -import { Dialog } from "./settings.js"; - -class MarkDown{ +import {Localuser} from "./localuser.js"; +import {Channel} from "./channel.js"; +import {Emoji} from "./emoji.js"; +import {Guild} from "./guild.js"; +import {I18n} from "./i18n.js"; +import {Dialog} from "./settings.js"; +import {Contextmenu} from "./contextmenu.js"; +const linkMenu = new Contextmenu("copyLink", true); +linkMenu.addButton( + () => I18n.copyRegLink(), + function () { + navigator.clipboard.writeText(this); + }, + {group: "copyLink"}, +); +class MarkDown { + static emoji?: typeof Emoji; txt: string[]; keep: boolean; stdsize: boolean; - owner: Localuser | Channel|void; - info: Localuser["info"]|void=undefined; + owner: Localuser | Channel | void; + info: Localuser["info"] | void = undefined; constructor( text: string | string[], owner: MarkDown["owner"], - { keep = false, stdsize = false } = {} - ){ - if(typeof text === typeof ""){ + {keep = false, stdsize = false} = {}, + ) { + if (typeof text === typeof "") { this.txt = (text as string).split(""); - }else{ + } else { this.txt = text as string[]; } - if(this.txt === undefined){ + if (this.txt === undefined) { this.txt = []; } - if(owner){ + if (owner) { this.info = owner.info; } this.keep = keep; this.owner = owner; this.stdsize = stdsize; } - get localuser(){ - if(this.owner instanceof Localuser){ + get channel() { + if (!this.owner) return; + if ("user" in this.owner) { + return; + } else if (this.owner) { return this.owner; - }else if(this.owner){ + } + return null; + } + get localuser() { + if (!this.owner) return; + if ("user" in this.owner) { + return this.owner; + } else if (this.owner) { return this.owner.localuser; } return null; } - get rawString(){ + get rawString() { return this.txt.join(""); } - get textContent(){ + get textContent() { return this.makeHTML().textContent; } - makeHTML({ keep = this.keep, stdsize = this.stdsize } = {}){ - return this.markdown(this.txt, { keep, stdsize }); + static getText() { + return text; + } + makeHTML({keep = this.keep, stdsize = this.stdsize} = {}) { + return this.markdown(this.txt, {keep, stdsize}); } - markdown(text: string | string[], { keep = false, stdsize = false } = {}){ + markdown(text: string | string[], {keep = false, stdsize = false} = {}) { + if (!keep && !stdsize) { + let str: string; + if (text instanceof Array) { + str = text.join(""); + } else { + str = text; + } + const span = document.createElement("span"); + span.classList.add("md-emoji", "bigemojiUni"); + + const matched = str.match(/^((|([^\da-zA-Z <>])) *){1,3}$/u); + if (matched) { + const map = [...str.matchAll(/|[^\da-zA-Z <>]+/gu).map(([_]) => _)]; + const seg = new Intl.Segmenter("en-US", {granularity: "grapheme"}); + const invalid = map.find((str) => { + if (str.length > 10) return false; + if (Array.from(seg.segment(str)).length !== 1) return true; + return false; + }); + if (!invalid) { + for (const match of map) { + if (match.length > 10) { + const parts = match.match(/^<(a)?:\w+:(\d{10,30})>$/); + if (parts && parts[2]) { + const owner = this.channel ? this.channel.guild : this.localuser; + if (!owner) continue; + const emoji = new Emoji( + {name: match, id: parts[2], animated: Boolean(parts[1])}, + owner, + ); + span.appendChild(emoji.getHTML(true, !keep)); + + continue; + } + } else { + span.append(match); + } + } + return span; + } + } + } let txt: string[]; - if(typeof text === typeof ""){ + if (typeof text === typeof "") { txt = (text as string).split(""); - }else{ + } else { txt = text as string[]; } - if(txt === undefined){ + if (txt === undefined) { txt = []; } const span = document.createElement("span"); let current = document.createElement("span"); - function appendcurrent(){ - if(current.innerHTML !== ""){ + function appendcurrent() { + if (current.innerHTML !== "") { span.append(current); current = document.createElement("span"); } } - for(let i = 0; i < txt.length; i++){ - if(txt[i] === "\n" || i === 0){ - const first = i === 0; - if(first){ + function getCurLast(): Node | undefined { + return Array.from(span.childNodes).at(-1); + } + for (let i = 0; i < txt.length; i++) { + const isSpace = /^\s$/; + if (txt[i] === "\n" || i === 0) { + let first = i === 0; + if (first) { i--; } let element: HTMLElement = document.createElement("span"); let keepys = ""; - if(txt[i + 1] === "#"){ - if(txt[i + 2] === "#"){ - if(txt[i + 3] === "#" && txt[i + 4] === " "){ + if (txt[i + 1] === "#") { + if (txt[i + 2] === "#") { + if (txt[i + 3] === "#" && txt[i + 4]?.match(isSpace)) { element = document.createElement("h3"); keepys = "### "; i += 5; - }else if(txt[i + 3] === " "){ + } else if (txt[i + 3]?.match(isSpace)) { element = document.createElement("h2"); element.classList.add("h2md"); keepys = "## "; i += 4; } - }else if(txt[i + 2] === " "){ + } else if (txt[i + 2]?.match(isSpace)) { element = document.createElement("h1"); keepys = "# "; i += 3; } - }else if(txt[i + 1] === ">" && txt[i + 2] === " "){ + } else if (txt[i + 1] === ">" && txt[i + 2]?.match(isSpace)) { element = document.createElement("div"); const line = document.createElement("div"); line.classList.add("quoteline"); @@ -101,81 +168,161 @@ class MarkDown{ element.classList.add("quote"); keepys = "> "; i += 3; + } else if (txt[i + 1] === "-" && txt[i + 2] === "#" && txt[i + 3]?.match(isSpace)) { + element = document.createElement("small"); + keepys = "-# "; + i += 4; } - if(keepys){ + if (keepys) { appendcurrent(); - if(!first && !stdsize){ + if (!first && !stdsize) { span.appendChild(document.createElement("br")); } const build: string[] = []; - for(; txt[i] !== "\n" && txt[i] !== undefined; i++){ + for (; txt[i] !== "\n" && txt[i] !== undefined; i++) { build.push(txt[i]); } - try{ - if(stdsize){ + try { + if (stdsize) { element = document.createElement("span"); } - if(keep){ + if (keep) { element.append(keepys); - //span.appendChild(document.createElement("br")); } - element.appendChild(this.markdown(build, { keep, stdsize })); + element.appendChild(this.markdown(build, {keep, stdsize})); span.append(element); - }finally{ + } finally { i -= 1; continue; } } - if(first){ + const bullet = new Set("*+- "); + if (bullet.has(txt[i + 1])) { + let list = document.createElement("ul"); + let depth = 0; + while (true) { + let j = i + 1; + let build = ""; + for (; txt[j] === " "; j++) { + build += txt[j]; + } + build += txt[j]; + j++; + build += txt[j]; + j++; + const match = build.match(/( *)[+*-] $/); + if (match) { + const arr: string[] = []; + for (; txt[j] && txt[j] !== "\n"; j++) { + arr.push(txt[j]); + } + i = j; + const line = this.markdown(arr); + if (keep) { + if (!first) { + current.textContent += "\n"; + } else { + first = false; + } + current.textContent += build; + appendcurrent(); + span.append(line); + depth = 2; + } else { + const curDepth = 0 | (match[1].length / 2); + if (curDepth > depth) { + depth++; + const newlist = document.createElement("ul"); + list.append(newlist); + list = newlist; + } else { + while (curDepth < depth) { + depth--; + list = list.parentElement as HTMLUListElement; + } + } + const li = document.createElement("li"); + li.append(line); + list.append(li); + } + } else { + break; + } + if (!txt[j]) { + break; + } + } + if (depth !== 0 || list.children.length) { + if (!keep) { + while (0 < depth) { + depth--; + list = list.parentElement as HTMLUListElement; + } + appendcurrent(); + span.append(list); + } + i--; + continue; + } + } + if (first) { i++; } } - if(txt[i] === "\\"){ - const chatset=new Set("\\`{}[]()<>*_#+-.!|".split("")); - if(chatset.has(txt[i+1])){ - if(keep){ + if (txt[i] === "\\") { + const chatset = new Set("\\`{}[]()<>*_#+-.!|@".split("")); + if (chatset.has(txt[i + 1])) { + if (keep) { current.textContent += txt[i]; } - current.textContent += txt[i+1]; + current.textContent += txt[i + 1]; i++; continue; } } - if(txt[i] === "\n"){ - if(!stdsize){ + if (txt[i] === "\n") { + if (!stdsize) { + const last = getCurLast(); + if (last instanceof HTMLElement && last.contentEditable === "false") { + span.append(document.createElement("span")); + } appendcurrent(); - span.append(document.createElement("br")); + if (keep) { + span.append(new Text("\n")); + } else { + span.append(document.createElement("br")); + } } continue; } - if(txt[i] === "`"){ + if (txt[i] === "`") { let count = 1; - if(txt[i + 1] === "`"){ + if (txt[i + 1] === "`") { count++; - if(txt[i + 2] === "`"){ + if (txt[i + 2] === "`") { count++; } } let build = ""; - if(keep){ + if (keep) { build += "`".repeat(count); } let find = 0; let j = i + count; let init = true; - for(;txt[j] !== undefined &&(txt[j] !== "\n" || count === 3) &&find !== count;j++){ - if(txt[j] === "`"){ + for (; txt[j] !== undefined && (txt[j] !== "\n" || count === 3) && find !== count; j++) { + if (txt[j] === "`") { find++; - }else{ - if(find !== 0){ + } else { + if (find !== 0) { build += "`".repeat(find); find = 0; } - if(init && count === 3){ - if(txt[j] === " " || txt[j] === "\n"){ + if (init && count === 3) { + if (txt[j] === " " || txt[j] === "\n") { init = false; } - if(keep){ + if (keep) { build += txt[j]; } continue; @@ -183,25 +330,25 @@ class MarkDown{ build += txt[j]; } } - if(stdsize){ + if (stdsize) { build = build.replaceAll("\n", ""); } - if(find === count){ + if (find === count) { appendcurrent(); i = j; - if(keep){ + if (keep) { build += "`".repeat(find); } - if(count !== 3 && !stdsize){ + if (count !== 3 && !stdsize) { const samp = document.createElement("samp"); samp.textContent = build; span.appendChild(samp); - }else{ + } else { const pre = document.createElement("pre"); - if(build.at(-1) === "\n"){ + if (build.at(-1) === "\n") { build = build.substring(0, build.length - 1); } - if(txt[i] === "\n"){ + if (txt[i] === "\n") { i++; } pre.textContent = build; @@ -212,61 +359,61 @@ class MarkDown{ } } - if(txt[i] === "*"){ + if (txt[i] === "*") { let count = 1; - if(txt[i + 1] === "*"){ + if (txt[i + 1] === "*") { count++; - if(txt[i + 2] === "*"){ + if (txt[i + 2] === "*") { count++; } } let build: string[] = []; let find = 0; let j = i + count; - for(; txt[j] !== undefined && find !== count; j++){ - if(txt[j] === "*"){ + for (; txt[j] !== undefined && find !== count; j++) { + if (txt[j] === "*") { find++; - }else{ + } else { build.push(txt[j]); - if(find !== 0){ + if (find !== 0) { build = build.concat(new Array(find).fill("*")); find = 0; } } } - if(find === count && (count != 1 || txt[i + 1] !== " ")){ + if (find === count && (count != 1 || txt[i + 1] !== " ")) { appendcurrent(); i = j; const stars = "*".repeat(count); - if(count === 1){ + if (count === 1) { const i = document.createElement("i"); - if(keep){ + if (keep) { i.append(stars); } - i.appendChild(this.markdown(build, { keep, stdsize })); - if(keep){ + i.appendChild(this.markdown(build, {keep, stdsize})); + if (keep) { i.append(stars); } span.appendChild(i); - }else if(count === 2){ + } else if (count === 2) { const b = document.createElement("b"); - if(keep){ + if (keep) { b.append(stars); } - b.appendChild(this.markdown(build, { keep, stdsize })); - if(keep){ + b.appendChild(this.markdown(build, {keep, stdsize})); + if (keep) { b.append(stars); } span.appendChild(b); - }else{ + } else { const b = document.createElement("b"); const i = document.createElement("i"); - if(keep){ + if (keep) { b.append(stars); } - b.appendChild(this.markdown(build, { keep, stdsize })); - if(keep){ + b.appendChild(this.markdown(build, {keep, stdsize})); + if (keep) { b.append(stars); } i.appendChild(b); @@ -277,66 +424,63 @@ class MarkDown{ } } - if(txt[i] === "_"){ + if (txt[i] === "_") { let count = 1; - if(txt[i + 1] === "_"){ + if (txt[i + 1] === "_") { count++; - if(txt[i + 2] === "_"){ + if (txt[i + 2] === "_") { count++; } } let build: string[] = []; let find = 0; let j = i + count; - for(; txt[j] !== undefined && find !== count; j++){ - if(txt[j] === "_"){ + for (; txt[j] !== undefined && find !== count; j++) { + if (txt[j] === "_") { find++; - }else{ + } else { build.push(txt[j]); - if(find !== 0){ + if (find !== 0) { build = build.concat(new Array(find).fill("_")); find = 0; } } } - if( + if ( find === count && -(count != 1 || -txt[j + 1] === " " || -txt[j + 1] === "\n" || -txt[j + 1] === undefined) - ){ + (count != 1 || txt[j] === " " || txt[j] === "\n" || txt[j] === undefined) + ) { appendcurrent(); i = j; const underscores = "_".repeat(count); - if(count === 1){ + if (count === 1) { const i = document.createElement("i"); - if(keep){ + if (keep) { i.append(underscores); } - i.appendChild(this.markdown(build, { keep, stdsize })); - if(keep){ + i.appendChild(this.markdown(build, {keep, stdsize})); + if (keep) { i.append(underscores); } span.appendChild(i); - }else if(count === 2){ + } else if (count === 2) { const u = document.createElement("u"); - if(keep){ + if (keep) { u.append(underscores); } - u.appendChild(this.markdown(build, { keep, stdsize })); - if(keep){ + u.appendChild(this.markdown(build, {keep, stdsize})); + if (keep) { u.append(underscores); } span.appendChild(u); - }else{ + } else { const u = document.createElement("u"); const i = document.createElement("i"); - if(keep){ + if (keep) { i.append(underscores); } - i.appendChild(this.markdown(build, { keep, stdsize })); - if(keep){ + i.appendChild(this.markdown(build, {keep, stdsize})); + if (keep) { i.append(underscores); } u.appendChild(i); @@ -347,33 +491,33 @@ txt[j + 1] === undefined) } } - if(txt[i] === "~" && txt[i + 1] === "~"){ + if (txt[i] === "~" && txt[i + 1] === "~") { const count = 2; let build: string[] = []; let find = 0; let j = i + 2; - for(; txt[j] !== undefined && find !== count; j++){ - if(txt[j] === "~"){ + for (; txt[j] !== undefined && find !== count; j++) { + if (txt[j] === "~") { find++; - }else{ + } else { build.push(txt[j]); - if(find !== 0){ + if (find !== 0) { build = build.concat(new Array(find).fill("~")); find = 0; } } } - if(find === count){ + if (find === count) { appendcurrent(); i = j - 1; const tildes = "~~"; - if(count === 2){ + if (count === 2) { const s = document.createElement("s"); - if(keep){ + if (keep) { s.append(tildes); } - s.appendChild(this.markdown(build, { keep, stdsize })); - if(keep){ + s.appendChild(this.markdown(build, {keep, stdsize})); + if (keep) { s.append(tildes); } span.appendChild(s); @@ -381,35 +525,38 @@ txt[j + 1] === undefined) continue; } } - if(txt[i] === "|" && txt[i + 1] === "|"){ + if (txt[i] === "|" && txt[i + 1] === "|") { const count = 2; let build: string[] = []; let find = 0; let j = i + 2; - for(; txt[j] !== undefined && find !== count; j++){ - if(txt[j] === "|"){ + for (; txt[j] !== undefined && find !== count; j++) { + if (txt[j] === "|") { find++; - }else{ + } else { build.push(txt[j]); - if(find !== 0){ + if (find !== 0) { build = build.concat(new Array(find).fill("~")); find = 0; } } } - if(find === count){ + if (find === count) { appendcurrent(); i = j - 1; const pipes = "||"; - if(count === 2){ + if (count === 2) { const j = document.createElement("j"); - if(keep){ + if (keep) { j.append(pipes); } - j.appendChild(this.markdown(build, { keep, stdsize })); + j.appendChild(this.markdown(build, {keep, stdsize})); j.classList.add("spoiler"); j.onclick = MarkDown.unspoil; - if(keep){ + if (keep) { + j.click(); + } + if (keep) { j.append(pipes); } span.appendChild(j); @@ -417,111 +564,216 @@ txt[j + 1] === undefined) continue; } } - if( + if ( !keep && txt[i] === "h" && txt[i + 1] === "t" && txt[i + 2] === "t" && txt[i + 3] === "p" - ){ + ) { let build = "http"; let j = i + 4; - const endchars = new Set(["\\", "<", ">", "|", "]", " ","\n"]); - for(; txt[j] !== undefined; j++){ + const endchars = new Set("\\<>|[] \n(){}"); + for (; txt[j] !== undefined; j++) { const char = txt[j]; - if(endchars.has(char)){ + if (endchars.has(char)) { break; } build += char; } - if(URL.canParse(build)){ + if (URL.canParse(build)) { appendcurrent(); const a = document.createElement("a"); + linkMenu.bindContextmenu(a, build); //a.href=build; - MarkDown.safeLink(a, build); + a.textContent = build; - a.target = "_blank"; + if (!stdsize) { + const replace = MarkDown.safeLink(a, build, this.localuser); + if (replace) { + a.textContent = replace; + a.classList.add("mentionMD"); + } + a.target = "_blank"; + } + i = j - 1; span.appendChild(a); continue; } } - if((txt[i] === "<" && (txt[i + 1] === "@" || txt[i + 1] === "#"))&&this.localuser){ - let id = ""; - let j = i + 2; - const numbers = new Set(["0","1","2","3","4","5","6","7","8","9",]); - for(; txt[j] !== undefined; j++){ - const char = txt[j]; - if(!numbers.has(char)){ + if (txt[i] === "@") { + let j = i + 1; + let everyone = true; + for (const char of "everyone") { + if (char !== txt[j]) { + everyone = false; break; } - id += char; + j++; } - - if(txt[j] === ">"){ - appendcurrent(); + let here = false; + if (!everyone) { + here = true; + for (const char of "here") { + if (char !== txt[j]) { + here = false; + break; + } + j++; + } + } + if (everyone || here) { + i = j - 1; const mention = document.createElement("span"); mention.classList.add("mentionMD"); mention.contentEditable = "false"; - const char = txt[i + 1]; - i = j; - switch(char){ - case"@": - const user = this.localuser.userMap.get(id); - if(user){ - mention.textContent = `@${user.name}`; - let guild: null | Guild = null; - if(this.owner instanceof Channel){ - guild = this.owner.guild; - } - if(!keep){ - user.bind(mention, guild); - } - if(guild){ - Member.resolveMember(user, guild).then(member=>{ - if(member){ - mention.textContent = `@${member.name}`; + mention.textContent = everyone ? "@everyone" : "@here"; + appendcurrent(); + span.appendChild(mention); + mention.setAttribute("real", everyone ? `@everyone` : "@here"); + continue; + } + } + if (txt[i] === "<") { + if ((txt[i + 1] === "@" || txt[i + 1] === "#") && this.localuser) { + let id = ""; + const role = txt[i + 1] === "@" && txt[i + 2] === "&"; + let j = i + 2 + +role; + const numbers = new Set(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]); + for (; txt[j] !== undefined; j++) { + const char = txt[j]; + if (!numbers.has(char)) { + break; + } + id += char; + } + + if (txt[j] === ">") { + appendcurrent(); + const last = getCurLast(); + if ( + last instanceof HTMLBRElement || + (last instanceof HTMLElement && last.contentEditable === "false") + ) { + span.append(document.createElement("span")); + } + const mention = document.createElement("span"); + mention.classList.add("mentionMD"); + mention.contentEditable = "false"; + const char = txt[i + 1]; + i = j; + switch (char) { + case "@": + if (role) { + if (this.channel) { + const role = this.channel.guild.roleids.get(id); + if (role) { + mention.textContent = `@${role.name}`; + mention.style.color = `var(--role-${role.id})`; + } else { + mention.textContent = I18n.guild.unknownRole(); + } } - }); - } - }else{ - mention.textContent = "@unknown"; + } else { + (async () => { + mention.textContent = I18n.userping.resolving(); + const user = await this.localuser?.getUser(id); + if (user) { + mention.textContent = `@${user.name}`; + let guild: null | Guild = null; + if (this.channel) { + guild = this.channel.guild; + } + if (!keep) { + user.bind(mention, guild, true, stdsize ? "none" : "left"); + } + if (guild) { + guild.resolveMember(user).then((member) => { + if (member) { + mention.textContent = `@${member.name}`; + } + }); + } + } else { + mention.textContent = I18n.userping.unknown(); + } + })(); + } + break; + case "#": + const channel = this.localuser.channelids.get(id); + if (channel) { + mention.textContent = `#${channel.name}`; + if (!keep && !stdsize) { + mention.onclick = (_) => { + if (!this.localuser) return; + this.localuser.goToChannel(id); + }; + } + } else { + mention.textContent = "#unknown"; + } + break; } - break; - case"#": - const channel = this.localuser.channelids.get(id); - if(channel){ - mention.textContent = `#${channel.name}`; - if(!keep){ - mention.onclick = _=>{ - if(!this.localuser) return; - this.localuser.goToChannel(id); - }; + span.appendChild(mention); + mention.setAttribute("real", `<${char}${role ? "&" : ""}${id}>`); + continue; + } + } else { + let j = i + 1; + let build = ""; + const invalid = new Set([">", "<"]); + for (; txt[j] !== undefined; j++) { + const char = txt[j]; + if (invalid.has(char)) { + break; + } + build += char; + } + if (URL.canParse(build) && txt[j] === ">") { + const url = new URL(build); + const allowedprotocols = new Set(["https:", "http:"]); + if (allowedprotocols.has(url.protocol)) { + i = j; + + if (keep) { + current.textContent += `<${build}>`; + } else { + appendcurrent(); + const a = document.createElement("a"); + linkMenu.bindContextmenu(a, build); + if (!stdsize) { + const text = MarkDown.safeLink(a, build, this.localuser); + if (text) { + a.textContent = text; + a.classList.add("mentionMD"); + } else { + a.textContent = build; + } + a.target = "_blank"; + } + span.appendChild(a); } - }else{ - mention.textContent = "#unknown"; + continue; } - break; } - span.appendChild(mention); - mention.setAttribute("real", `<${char}${id}>`); - continue; } } - if(txt[i] === "<" && txt[i + 1] === "t" && txt[i + 2] === ":"){ + if (txt[i] === "<" && txt[i + 1] === "t" && txt[i + 2] === ":") { let found = false; const build = ["<", "t", ":"]; let j = i + 3; - for(; txt[j] !== void 0; j++){ + for (; txt[j] !== void 0; j++) { build.push(txt[j]); - if(txt[j] === ">"){ + if (txt[j] === ">") { found = true; break; } } - if(found){ + if (found) { appendcurrent(); i = j; const parts = build @@ -529,58 +781,59 @@ txt[j + 1] === undefined) .match(/^$/) as RegExpMatchArray; const dateInput = new Date(Number.parseInt(parts[1]) * 1000); let time = ""; - if(Number.isNaN(dateInput.getTime())) time = build.join(""); - else{ - if(parts[3] === "d") + if (Number.isNaN(dateInput.getTime())) time = build.join(""); + else { + if (parts[3] === "d") time = dateInput.toLocaleString(void 0, { day: "2-digit", month: "2-digit", year: "numeric", }); - else if(parts[3] === "D") + else if (parts[3] === "D") time = dateInput.toLocaleString(void 0, { day: "numeric", month: "long", year: "numeric", }); - else if(!parts[3] || parts[3] === "f") + else if (!parts[3] || parts[3] === "f") time = - dateInput.toLocaleString(void 0, { - day: "numeric", - month: "long", - year: "numeric", - }) + - " " + - dateInput.toLocaleString(void 0, { - hour: "2-digit", - minute: "2-digit", - }); - else if(parts[3] === "F") + dateInput.toLocaleString(void 0, { + day: "numeric", + month: "long", + year: "numeric", + }) + + " " + + dateInput.toLocaleString(void 0, { + hour: "2-digit", + minute: "2-digit", + }); + else if (parts[3] === "F") time = - dateInput.toLocaleString(void 0, { - day: "numeric", - month: "long", - year: "numeric", - weekday: "long", - }) + - " " + - dateInput.toLocaleString(void 0, { - hour: "2-digit", - minute: "2-digit", - }); - else if(parts[3] === "t") + dateInput.toLocaleString(void 0, { + day: "numeric", + month: "long", + year: "numeric", + weekday: "long", + }) + + " " + + dateInput.toLocaleString(void 0, { + hour: "2-digit", + minute: "2-digit", + }); + else if (parts[3] === "t") time = dateInput.toLocaleString(void 0, { hour: "2-digit", minute: "2-digit", }); - else if(parts[3] === "T") + else if (parts[3] === "T") time = dateInput.toLocaleString(void 0, { hour: "2-digit", minute: "2-digit", second: "2-digit", }); - else if(parts[3] === "R") - time =Math.round((Date.now() - Number.parseInt(parts[1]) * 1000) / 1000 / 60) + " minutes ago"; + else if (parts[3] === "R") + //TODO make this a little less bad + time = MarkDown.relTime(new Date(Number.parseInt(parts[1]) * 1000)); } const timeElem = document.createElement("span"); @@ -591,83 +844,92 @@ txt[j + 1] === undefined) } } - if(txt[i] === "<" && (txt[i + 1] === ":" || (txt[i + 1] === "a" && txt[i + 2] === ":")&&this.owner)){ + if ( + txt[i] === "<" && + (txt[i + 1] === ":" || (txt[i + 1] === "a" && txt[i + 2] === ":" && this.owner)) + ) { + const Emoji = MarkDown.emoji; let found = false; const build = txt[i + 1] === "a" ? ["<", "a", ":"] : ["<", ":"]; let j = i + build.length; - for(; txt[j] !== void 0; j++){ + for (; txt[j] !== void 0; j++) { build.push(txt[j]); - if(txt[j] === ">"){ + if (txt[j] === ">") { found = true; break; } } - if(found){ + if (found && Emoji) { const buildjoin = build.join(""); const parts = buildjoin.match(/^<(a)?:\w+:(\d{10,30})>$/); - if(parts && parts[2]){ + if (parts && parts[2]) { appendcurrent(); i = j; - const isEmojiOnly = txt.join("").trim() === buildjoin.trim(); - const owner = this.owner instanceof Channel ? this.owner.guild : this.owner; - if(!owner) continue; + const isEmojiOnly = txt.join("").trim() === buildjoin.trim() && !stdsize; + const owner = this.channel ? this.channel.guild : this.localuser; + if (!owner) continue; const emoji = new Emoji( - { name: buildjoin, id: parts[2], animated: Boolean(parts[1]) }, - owner + {name: buildjoin, id: parts[2], animated: Boolean(parts[1])}, + owner, ); - span.appendChild(emoji.getHTML(isEmojiOnly)); + span.appendChild(emoji.getHTML(isEmojiOnly, !keep)); continue; } } } - if(txt[i] == "[" && !keep){ + if (txt[i] == "[" && !keep) { let partsFound = 0; let j = i + 1; const build = ["["]; - for(; txt[j] !== void 0; j++){ + for (; txt[j] !== void 0; j++) { build.push(txt[j]); - if(partsFound === 0 && txt[j] === "]"){ - if( - txt[j + 1] === "(" && - txt[j + 2] === "h" && - txt[j + 3] === "t" && - txt[j + 4] === "t" && - txt[j + 5] === "p" && - (txt[j + 6] === "s" || txt[j + 6] === ":") - ){ + if (partsFound === 0 && txt[j] === "]") { + if ( + (txt[j + 1] === "(" && + txt[j + 2] === "h" && + txt[j + 3] === "t" && + txt[j + 4] === "t" && + txt[j + 5] === "p" && + (txt[j + 6] === "s" || txt[j + 6] === ":")) || + (txt[j + 1] === "(" && + txt[j + 2] === "<" && + txt[j + 3] === "h" && + txt[j + 4] === "t" && + txt[j + 5] === "t" && + txt[j + 6] === "p" && + (txt[j + 7] === "s" || txt[j + 7] === ":")) + ) { partsFound++; - }else{ + } else { break; } - }else if(partsFound === 1 && txt[j] === ")"){ + } else if (partsFound === 1 && txt[j] === ")") { partsFound++; break; } } - - if(partsFound === 2){ + if (partsFound === 2) { appendcurrent(); - const parts = build - .join("") - .match(/^\[(.+)\]\((https?:.+?)( ('|").+('|"))?\)$/); - if(parts){ + const parts = build.join("").match(/^\[(.+)\]\(?( ('|").+('|"))?\)$/); + if (parts) { const linkElem = document.createElement("a"); - if(URL.canParse(parts[2])){ + + if (URL.canParse(parts[2])) { i = j; - MarkDown.safeLink(linkElem, parts[2]); - linkElem.textContent = parts[1]; - linkElem.target = "_blank"; - linkElem.rel = "noopener noreferrer"; + if (!stdsize) { + MarkDown.safeLink(linkElem, parts[2]); + linkElem.append(this.markdown(parts[1], {keep, stdsize})); + linkElem.target = "_blank"; + linkElem.rel = "noopener noreferrer"; + } linkElem.title = - (parts[3] - ? parts[3].substring(2, parts[3].length - 1) + "\n\n" - : "") + parts[2]; + (parts[3] ? parts[3].substring(2, parts[3].length - 1) + "\n\n" : "") + parts[2]; span.appendChild(linkElem); continue; @@ -675,130 +937,328 @@ txt[j + 1] === undefined) } } } - current.textContent += txt[i]; } appendcurrent(); + const last = getCurLast(); + if (last && last instanceof Text && last.textContent === "\n" && Error.prototype.stack === "") { + span.append(current); + } + if ( + last && + last instanceof HTMLElement && + (last.contentEditable === "false" || last instanceof HTMLBRElement) + ) { + span.append(current); + } return span; } - static unspoil(e: any): void{ + static relTime(date: Date, nextUpdate?: () => void): string { + const time = Date.now() - +date; + + let seconds = Math.round(time / 1000); + const round = time % 1000; + let minutes = Math.floor(seconds / 60); + seconds -= minutes * 60; + let hours = Math.floor(minutes / 60); + minutes -= hours * 60; + let days = Math.floor(hours / 24); + hours -= days * 24; + let years = Math.floor(days / 24); + days -= years * 365; + + const formatter = new Intl.RelativeTimeFormat(I18n.lang, {style: "short"}); + if (years) { + if (nextUpdate) + setTimeout( + nextUpdate, + round + (seconds + (minutes + (hours + days * 24) * 60) * 60) * 1000, + ); + return formatter.format(-years, "year"); + } else if (days) { + if (nextUpdate) + setTimeout(nextUpdate, round + (seconds + (minutes + hours * 60) * 60) * 1000); + return formatter.format(-days, "days"); + } else if (hours) { + if (nextUpdate) setTimeout(nextUpdate, round + (seconds + minutes * 60) * 1000); + return formatter.format(-hours, "hours"); + } else if (minutes) { + if (nextUpdate) setTimeout(nextUpdate, round + seconds * 1000); + return formatter.format(-minutes, "minutes"); + } else { + if (nextUpdate) setTimeout(nextUpdate, round); + return formatter.format(-seconds, "seconds"); + } + } + static unspoil(e: any): void { e.target.classList.remove("spoiler"); e.target.classList.add("unspoiled"); } - onUpdate:(upto:string,pre:boolean)=>unknown=()=>{}; - box=new WeakRef(document.createElement("div")); - giveBox(box: HTMLDivElement,onUpdate:(upto:string,pre:boolean)=>unknown=()=>{}){ - this.box=new WeakRef(box); - this.onUpdate=onUpdate; - box.onkeydown = _=>{ - //console.log(_); - }; + onUpdate: (upto: string, pre: boolean) => unknown = () => {}; + box = new WeakRef(document.createElement("div")); + giveBox(box: HTMLDivElement, onUpdate: (upto: string, pre: boolean) => unknown = () => {}) { + this.box = new WeakRef(box); + this.onUpdate = onUpdate; + box.addEventListener("keydown", (_) => { + if (_.isComposing) return; + if (Error.prototype.stack !== "") return; + if (_.key === "Enter") { + const selection = window.getSelection() as Selection; + if (!selection) return; + const range = selection.getRangeAt(0); + const node = new Text("\n"); + range.insertNode(node); + const g = node.nextSibling; + if (g) range.setStart(g, 0); + _.preventDefault(); + return; + } + }); let prevcontent = ""; - box.onkeyup = _=>{ - const content = MarkDown.gatherBoxText(box); - if(content !== prevcontent){ + const gatherBoxContents = (isBackSpace: boolean) => { + let content = MarkDown.gatherBoxText(box); + if (content === "\n") content = ""; + if (content !== prevcontent) { prevcontent = content; this.txt = content.split(""); - this.boxupdate(); + this.boxupdate(undefined, undefined, undefined, isBackSpace); MarkDown.gatherBoxText(box); } - }; - box.onpaste = _=>{ - if(!_.clipboardData)return; - console.log(_.clipboardData.types); - const data = _.clipboardData.getData("text"); - - document.execCommand("insertHTML", false, data); - _.preventDefault(); - if(!box.onkeyup)return; - box.onkeyup(new KeyboardEvent("_")); + box.onkeyup = (_) => { + if (_.isComposing) return; + gatherBoxContents(_.key === "Backspace"); + }; + box.addEventListener("compositionend", (_) => { + gatherBoxContents(false); + }); + box.onpaste = (_) => { + if (!_.clipboardData) return; + const types = _.clipboardData.types; + console.log(types); + if (types.includes("Files")) { + _.preventDefault(); + return; + } + const selection = window.getSelection() as Selection; + + if (types.includes("text/html")) { + const data = _.clipboardData.getData("text/html"); + const html = new DOMParser().parseFromString(data, "text/html"); + const txt = MarkDown.gatherBoxText(html.body); + console.log(txt); + const rstr = selection.toString(); + saveCaretPosition(box)?.(); + const content = this.textContent; + if (content) { + const [_first, end] = content.split(text); + if (rstr) { + const tw = text.split(rstr); + tw.pop(); + text = tw.join(""); + } + const boxText = text + txt + (end ?? ""); + box.textContent = boxText; + const len = text.length + txt.length; + text = boxText; + this.txt = text.split(""); + this.boxupdate(len, false, 0); + } else { + box.textContent = txt; + text = txt; + this.txt = text.split(""); + this.boxupdate(txt.length, false, 0); + } + _.preventDefault(); + } else if (types.includes("text/plain")) { + //Allow the paste like normal + } else { + _.preventDefault(); + } }; } - boxupdate(offset=0){ - const box=this.box.deref(); - if(!box) return; - const restore = saveCaretPosition(box,offset); - box.innerHTML = ""; - box.append(this.makeHTML({ keep: true })); - if(restore){ - restore(); - const test=saveCaretPosition(box); - if(test) test(); - } - this.onUpdate(text,formatted); + customBox?: [(arg1: string) => HTMLElement, (arg1: HTMLElement) => string]; + clearCustom() { + this.customBox = undefined; + } + setCustomBox( + stringToHTML: (arg1: string) => HTMLElement, + HTMLToString = MarkDown.gatherBoxText.bind(MarkDown), + ) { + this.customBox = [stringToHTML, HTMLToString]; + } + boxEnabled = true; + boxupdate( + offset = 0, + allowLazy = true, + computedLength: void | number = undefined, + backspace = false, + ) { + if (!this.boxEnabled) return; + const box = this.box.deref(); + if (!box) return; + let restore: undefined | ((backspace: boolean) => void); + if (this.customBox) { + restore = saveCaretPosition(box, offset, this.customBox[1], computedLength); + } else { + restore = saveCaretPosition( + box, + offset, + MarkDown.gatherBoxText.bind(MarkDown), + computedLength, + ); + } + + if (this.customBox) { + //TODO maybe the custom logic applies here as well, but that's a later thing + box.innerHTML = ""; + box.append(this.customBox[0](this.rawString)); + } else { + //console.time(); + const html = this.makeHTML({keep: true}); + const condition = + html.childNodes.length == 1 && + html.childNodes[0].childNodes.length === 1 && + html.childNodes[0].childNodes[0]; + //console.log(box.cloneNode(true), html.cloneNode(true)); + //TODO this may be slow, may want to check in on this in the future if it is + if ((!box.hasChildNodes() || html.isEqualNode(Array.from(box.childNodes)[0])) && allowLazy) { + //console.log("no replace needed"); + } else { + if ( + !(box.childNodes.length === 1 && box.childNodes[0] instanceof Text && condition) || + !allowLazy + ) { + box.innerHTML = ""; + box.append(html); + } else { + //console.log("empty replace not happened"); + } + } + //console.timeEnd(); + } + if (restore) { + restore(backspace); + } + this.onUpdate(text, formatted); } - static gatherBoxText(element: HTMLElement): string{ - if(element.tagName.toLowerCase() === "img"){ - return(element as HTMLImageElement).alt; + static gatherBoxText(element: HTMLElement): string { + if (element.tagName.toLowerCase() === "img") { + return (element as HTMLImageElement).alt; } - if(element.tagName.toLowerCase() === "br"){ - return"\n"; + if (element.tagName.toLowerCase() === "br") { + return "\n"; } - if(element.hasAttribute("real")){ + if (element.hasAttribute("real")) { return element.getAttribute("real") as string; } - if(element.tagName.toLowerCase() === "pre"||element.tagName.toLowerCase() === "samp"){ - formatted=true; - }else{ - formatted=false; + if (element.tagName.toLowerCase() === "pre" || element.tagName.toLowerCase() === "samp") { + formatted = true; + } else { + formatted = false; } let build = ""; - for(const thing of Array.from(element.childNodes)){ - if(thing instanceof Text){ + const arr = Array.from(element.childNodes); + for (const thing of arr) { + if (thing instanceof Text) { const text = thing.textContent; build += text; continue; } const text = this.gatherBoxText(thing as HTMLElement); - if(text){ + if (text) { build += text; } } return build; } - static readonly trustedDomains = new Set([location.host]); - static safeLink(elm: HTMLElement, url: string){ - if(URL.canParse(url)){ + static trustedDomains = this.getTrusted(); + static getTrusted() { + const domains = localStorage.getItem("trustedDomains"); + if (domains) { + return new Set(JSON.parse(domains) as string[]); + } + return new Set([location.host, "fermi.chat", "blog.fermi.chat"]); + } + static saveTrusted(remove = false) { + if (!remove) { + this.trustedDomains = this.trustedDomains.union(this.getTrusted()); + } + const domains = JSON.stringify([...this.trustedDomains]); + + localStorage.setItem("trustedDomains", domains); + } + static safeLink( + elm: HTMLElement, + url: string, + localuser: Localuser | null = null, + ): string | void { + if (elm instanceof HTMLAnchorElement) { + elm.rel = "noopener noreferrer"; + } + if (URL.canParse(url)) { const Url = new URL(url); - if(elm instanceof HTMLAnchorElement && this.trustedDomains.has(Url.host)){ + if (localuser) { + const [_, _2, ...path] = Url.pathname.split("/"); + + const guild = localuser.guildids.get(path[0]); + const channel = guild?.getChannel(path[1]); + if (channel) { + const message = isNaN(+path[2]) ? undefined : path[2]; + elm.onmouseup = (_) => { + channel.getHTML(true, true, message); + }; + if (message) { + return I18n.messageLink(channel.name); + } else { + return I18n.channelLink(channel.name); + } + } + } + if (elm instanceof HTMLAnchorElement && this.trustedDomains.has(Url.host)) { elm.href = url; elm.target = "_blank"; return; } - elm.onmouseup = _=>{ - if(_.button === 2)return; - console.log(":3"); - function open(){ + elm.onmouseup = (_) => { + let parent: HTMLElement | null = elm; + while (parent) { + if (parent.classList.contains("unspoiled")) break; + if (parent.classList.contains("spoiler")) return; + parent = parent.parentElement; + } + if (_.button === 2) return; + function open() { const proxy = window.open(url, "_blank"); - if(proxy && _.button === 1){ + if (proxy && _.button === 1) { proxy.focus(); - }else if(proxy){ + } else if (proxy) { window.focus(); } } - if(this.trustedDomains.has(Url.host)){ + if (this.trustedDomains.has(Url.host)) { open(); - }else{ - const full=new Dialog(""); - full.options.addTitle(I18n.getTranslation("leaving")); - full.options.addText(I18n.getTranslation("goingToURL",Url.host)); - const options=full.options.addOptions("",{ltr:true}); - options.addButtonInput("",I18n.getTranslation("nevermind"),()=>full.hide()); - options.addButtonInput("",I18n.getTranslation("goThere"),()=>{ + } else { + const full = new Dialog(""); + full.options.addTitle(I18n.leaving()); + full.options.addText(I18n.goingToURL(Url.host)); + const options = full.options.addOptions("", {ltr: true}); + options.addButtonInput("", I18n.nevermind(), () => full.hide()); + options.addButtonInput("", I18n.goThere(), () => { open(); full.hide(); }); - options.addButtonInput("",I18n.getTranslation("goThereTrust"),()=>{ + options.addButtonInput("", I18n.goThereTrust(), () => { open(); full.hide(); this.trustedDomains.add(Url.host); + this.saveTrusted(); }); full.show(); } }; - }else{ + } else { throw new Error(url + " is not a valid URL"); } } @@ -815,156 +1275,220 @@ txt[j + 1] === undefined) //solution from https://stackoverflow.com/questions/4576694/saving-and-restoring-caret-position-for-contenteditable-div let text = ""; -let formatted=false; -function saveCaretPosition(context: HTMLElement,offset=0){ +let formatted = false; +function saveCaretPosition( + context: HTMLElement, + offset = 0, + txtLengthFunc = MarkDown.gatherBoxText.bind(MarkDown), + computedLength: void | number = undefined, +) { const selection = window.getSelection() as Selection; - if(!selection)return; - try{ + if (!selection) return; + try { const range = selection.getRangeAt(0); - let base=selection.anchorNode as Node; + let base = selection.anchorNode as Node; range.setStart(base, 0); - let baseString:string; - if(!(base instanceof Text)){ - let i=0; - const index=selection.focusOffset; - //@ts-ignore - for(const thing of base.childNodes){ - if(i===index){ - base=thing; - break; - } - i++; + let baseString: string; + let i = 0; + const index = selection.focusOffset; + + for (const thing of Array.from(base.childNodes)) { + if (i === index) { + base = thing; + break; } - if(base instanceof HTMLElement){ - baseString=MarkDown.gatherBoxText(base) - }else{ - baseString=base.textContent as string; + i++; + } + const prev = base.previousSibling; + let len = 0; + if ((!prev || prev instanceof HTMLBRElement) && base instanceof HTMLBRElement) { + len--; + } + if ( + !(base instanceof Text) && + !( + base instanceof HTMLSpanElement && + base.className === "" && + base.children.length == 0 && + !(base instanceof HTMLBRElement) + ) + ) { + if (base instanceof HTMLElement) { + baseString = txtLengthFunc(base); + } else { + baseString = base.textContent || ""; } - }else{ - baseString=selection.toString(); + } else { + baseString = selection.toString(); } - - range.setStart(context, 0); - let build=""; + let build = ""; //I think this is working now :3 - function crawlForText(context:Node){ + function crawlForText(context: Node) { //@ts-ignore - const children=[...context.childNodes]; - if(children.length===1&&children[0] instanceof Text){ - if(selection.containsNode(context,false)){ - build+=MarkDown.gatherBoxText(context as HTMLElement); - }else if(selection.containsNode(context,true)){ - if(context.contains(base)||context===base||base.contains(context)){ - build+=baseString; - }else{ - build+=context.textContent; - } - }else{ + const children = [...context.childNodes]; + if (children.length === 1 && children[0] instanceof Text) { + if (selection.containsNode(context, false)) { + build += txtLengthFunc(context as HTMLElement); + } else if (selection.containsNode(context, true)) { + if (context.contains(base) || context === base || base.contains(context)) { + build += baseString; + } else { + build += context.textContent; + } + } else { console.error(context); } return; } - for(const node of children as Node[]){ - - if(selection.containsNode(node,false)){ - if(node instanceof HTMLElement){ - build+=MarkDown.gatherBoxText(node); - }else{ - build+=node.textContent; + for (const node of children as Node[]) { + if (selection.containsNode(node, false)) { + if (node instanceof HTMLElement) { + build += txtLengthFunc(node); + } else { + build += node.textContent; } - }else if(selection.containsNode(node,true)){ - if(node instanceof HTMLElement){ + } else if (selection.containsNode(node, true)) { + if (node instanceof HTMLElement) { crawlForText(node); - }else{ - console.error(node,"This shouldn't happen") + } else { + build += node.textContent; } - }else{ + } else { //console.error(node,"This shouldn't happen"); } } } crawlForText(context); - if(baseString==="\n"){ - build+=baseString; - } - text=build; - let len=build.length+offset; - len=Math.min(len,MarkDown.gatherBoxText(context).length) - return function restore(){ - if(!selection)return; - const pos = getTextNodeAtPosition(context, len); + if (baseString === "\n") { + build += baseString; + } + text = build; + len += build.length; + if (computedLength !== undefined) { + len = computedLength; + } + len = Math.min(len, txtLengthFunc(context).length); + len += offset; + + return function restore(backspace = false) { + if (!selection) return; + const pos = getTextNodeAtPosition(context, len, txtLengthFunc); + if ( + pos.node instanceof Text && + pos.node.textContent === "\n" && + pos.node.nextSibling && + Error.prototype.stack === "" && + !backspace + ) { + if (pos.node.nextSibling instanceof Text && pos.node.nextSibling.textContent === "\n") { + pos.position = 1; + } else { + pos.node = pos.node.nextSibling; + pos.position = 0; + } + } selection.removeAllRanges(); const range = new Range(); range.setStart(pos.node, pos.position); selection.addRange(range); }; - }catch{ + } catch { return undefined; } } -function getTextNodeAtPosition(root: Node, index: number):{ - node: Node, - position: number, - }{ - if(root instanceof Text){ - return{ +function getTextNodeAtPosition( + root: Node, + index: number, + txtLengthFunc = MarkDown.gatherBoxText.bind(MarkDown), +): { + node: Node; + position: number; +} { + if (index === 0) { + return { + node: root, + position: 0, + }; + } + if (root instanceof Text) { + return { node: root, position: index, }; - }else if(root instanceof HTMLBRElement){ - return{ + } else if (root instanceof HTMLBRElement) { + return { node: root, position: 0, }; - }else if(root instanceof HTMLElement&&root.hasAttribute("real")){ - return{ + } else if (root instanceof HTMLElement && root.hasAttribute("real")) { + return { node: root, position: -1, }; } - let lastElm:Node=root; - for(const node of root.childNodes as unknown as Node[]){ - lastElm=node; - let len:number - if(node instanceof HTMLElement){ - len=MarkDown.gatherBoxText(node).length; - }else{ - len=(node.textContent as string).length - } - if(len<=index&&(len("media"); +menu.addButton( + () => I18n.media.download(), + function () { + const a = document.createElement("a"); + a.href = this.src; + a.download = this.filename; + a.click(); + }, +); +menu.addButton( + () => I18n.media.moreInfo(), + async function () { + const di = new Dialog(this.title); + const options = di.float.options; + if (this.img) { + const img = document.createElement("img"); + img.classList.add("media-medium"); + img.src = this.img.url; + if (this.img.description) img.alt = this.img.description; + options.addHTMLArea(img); + } + if (this.artist) { + options.addText(I18n.media.artist(this.artist)); + } + if (this.composer) { + options.addText(I18n.media.composer(this.composer)); + } + { + const mins = Math.floor((await this.length) / 60000); + const seconds = Math.round(((await this.length) - mins * 60000) / 1000); + options.addText(I18n.media.length(mins + "", seconds + "")); + } + + di.show(); + if (this.copyright) { + const text = options.addText(this.copyright); + const txt = text.elm.deref(); + if (txt) { + txt.classList.add("timestamp"); + } + } + }, +); +type mediaEvents = + | { + type: "play"; + } + | { + type: "pause"; + } + | { + type: "playing"; + time: number; + } + | { + type: "done"; + } + | { + type: "audio"; + t: "start" | "stop"; + } + | { + type: "audio"; + t: "skip"; + time: number; + } + | { + type: "end"; + }; + +function makePlayBox( + mor: string | media, + player: MediaPlayer, + ctime = 0, + url: Promise | void, +) { + const div = document.createElement("div"); + div.classList.add("flexltr", "Mplayer"); + + const button = document.createElement("img"); + button.classList.add("svg-mediaButton", "svg-play"); + + const vDiv = document.createElement("div"); + vDiv.classList.add("flexttb"); + + const title = document.createElement("span"); + title.textContent = I18n.media.loading(); + + const barDiv = document.createElement("div"); + barDiv.classList.add("flexltr"); + + const bar = document.createElement("input"); + bar.type = "range"; + bar.disabled = true; + bar.value = "" + ctime; + bar.min = "0"; + + const time = document.createElement("span"); + time.textContent = "0:00/..:.."; + + const more = document.createElement("span"); + more.classList.add("svg-soundMore", "svg-mediaSettings"); + + barDiv.append(bar, time); + vDiv.append(title, barDiv); + div.append(button, vDiv, more); + (async () => { + if (url) mor = await url; + MediaPlayer.IdentifyFile(mor).then((thing) => { + let audio: HTMLAudioElement | undefined = undefined; + + if (!thing) { + const span = document.createElement("span"); + span.textContent = I18n.media.notFound(); + return; + } + menu.bindContextmenu( + more, + thing, + undefined, + () => {}, + () => {}, + "left", + ); + player.addListener(thing.src, followUpdates, div); + let int = setInterval((_) => {}, 1000); + if (typeof mor !== "string") { + const cmor = mor; + const audioo = new Audio(cmor.src); + audioo.load(); + audioo.autoplay = true; + audioo.currentTime = ctime / 1000; + int = setInterval(() => { + if (button.classList.contains("svg-pause")) { + player.addUpdate(cmor.src, {type: "playing", time: audioo.currentTime * 1000}); + } + }, 100) as unknown as number; + audioo.onplay = () => { + player.addUpdate(cmor.src, {type: "play"}); + }; + audioo.onpause = () => { + player.addUpdate(thing.src, {type: "pause"}); + }; + audioo.onloadeddata = () => { + audio = audioo; + }; + audioo.onended = () => { + player.addUpdate(cmor.src, {type: "end"}); + }; + } + button.onclick = () => { + if (!player.isPlaying(thing.src)) { + player.setToTopList(thing, +bar.value * 1000); + } else { + player.addUpdate(thing.src, { + type: "audio", + t: button.classList.contains("svg-play") ? "start" : "stop", + }); + } + }; + function followUpdates(cur: mediaEvents) { + if (audio && cur.type !== "playing") { + } + if (cur.type == "audio" && audio) { + if (cur.t == "start") { + audio.play(); + } else if (cur.t == "stop") { + audio.pause(); + } else if (cur.t == "skip" && audio) { + audio.currentTime = cur.time / 1000; + } + } + if (cur.type == "audio" && cur.t == "skip") { + bar.value = "" + cur.time / 1000; + } + if (cur.type == "playing") { + regenTime(cur.time); + bar.value = "" + cur.time / 1000; + button.classList.add("svg-pause"); + button.classList.remove("svg-play"); + } else if (cur.type === "play") { + button.classList.add("svg-pause"); + button.classList.remove("svg-play"); + } else if (cur.type === "pause") { + button.classList.add("svg-play"); + button.classList.remove("svg-pause"); + } else if (cur.type === "end") { + clearInterval(int); + if (audio) { + audio.pause(); + audio.src = ""; + player.end(); + } + button.classList.add("svg-play"); + button.classList.remove("svg-pause"); + regenTime(); + } + } + + const med = thing; + if (med.img) { + let img: HTMLImageElement; + if (mor instanceof Object) { + img = button; + } else { + img = document.createElement("img"); + div.append(img); + } + img.classList.add("media-small"); + img.src = med.img.url; + } + function timeToString(time: number) { + const minutes = Math.floor(time / 1000 / 60); + const seconds = Math.round(time / 1000 - minutes * 60) + ""; + return `${minutes}:${seconds.padStart(2, "0")}`; + } + bar.oninput = () => { + player.addUpdate(thing.src, { + type: "audio", + t: "skip", + time: +bar.value * 1000, + }); + regenTime(+bar.value * 1000); + }; + async function regenTime(curTime: number = 0) { + const len = await med.length; + bar.disabled = false; + bar.max = "" + len / 1000; + + time.textContent = `${timeToString(curTime)}/${timeToString(len)}`; + } + regenTime(); + title.textContent = thing.title; + }); + })(); + return div; +} + +interface media { + src: string; + filename: string; + img?: { + url: string; + description?: string; + }; + title: string; + artist?: string; + composer?: string; + sourcy?: string; + year?: number; + copyright?: string; + length: number | Promise; +} +class MediaPlayer { + lists: media[] = []; + curAudio?: HTMLElement; + private listeners: [(e: mediaEvents) => void, string][] = []; + elm: HTMLDivElement; + cur = 0; + constructor() { + this.elm = document.getElementById("player") as HTMLDivElement; + } + addElmToList(audio: media) { + this.lists.unshift(audio); + } + addUpdate(e: string, update: mediaEvents) { + for (const thing of this.listeners) { + if (thing[1] === e) { + thing[0](update); + } + } + } + addListener(audio: string, updates: (e: mediaEvents) => void, elm: HTMLElement) { + this.listeners.push([updates, audio]); + const int = setInterval(() => { + if (!document.contains(elm)) { + clearInterval(int); + this.listeners = this.listeners.filter((_) => _[0] !== updates); + } + }, 1000); + } + isPlaying(str: string) { + const med = this.lists[this.cur]; + if (!med) return false; + return med.src === str; + } + setToTopList(audio: media, time: number) { + const med = this.lists[this.cur]; + if (med) { + this.addUpdate(med.src, {type: "end"}); + } + this.lists.splice(this.cur, 0, audio); + this.regenPlayer(time); + } + end() { + if (this.curAudio) { + this.curAudio.remove(); + this.cur++; + this.regenPlayer(0); + } + } + regenPlayer(time: number) { + this.elm.innerHTML = ""; + if (this.lists.length > this.cur) { + const audio = this.lists[this.cur]; + this.elm.append((this.curAudio = makePlayBox(audio, this, time))); + } + } + static cache = new Map>(); + static async IdentifyFile(url: string | media): Promise { + if (url instanceof Object) { + return url; + } + const cValue = this.cache.get(url); + if (cValue) { + return cValue; + } + let resMedio = (_: media) => {}; + this.cache.set(url, new Promise((res) => (resMedio = res))); + const prog = new ProgressiveArray(url, {method: "get"}); + await prog.ready; + + const output: Partial = { + src: url, + }; + try { + const head = String.fromCharCode(await prog.next(), await prog.next(), await prog.next()); + if (head === "ID3") { + const version = (await prog.next()) + (await prog.next()) * 256; + + if (version === 2) { + //TODO I'm like 90% I can ignore *all* of the flags, but I need to check more sometime + await prog.next(); + //debugger; + const sizes = await prog.get8BitArray(4); + prog.sizeLeft = (sizes[0] << 21) + (sizes[1] << 14) + (sizes[2] << 7) + sizes[3]; + const mappy = new Map>(); + while (prog.sizeLeft > 0) { + const Identify = String.fromCharCode( + await prog.next(), + await prog.next(), + await prog.next(), + ); + const sizeArr = await prog.get8BitArray(3); + const size = (sizeArr[0] << 16) + (sizeArr[1] << 8) + sizeArr[2]; + if (Identify === String.fromCharCode(0, 0, 0)) { + break; + } + if (!size) { + throw Error("weirdness"); + } + if (!Identify.match(/[A-Z0-9]/gm)) { + console.error(`tried to get ${Identify} which doesn't exist`); + break; + } + if (mappy.has(Identify)) { + await prog.get8BitArray(size); + //console.warn("Got dupe", Identify); + } else { + mappy.set(Identify, await prog.get8BitArray(size)); + } + } + const pic = mappy.get("PIC"); + if (pic) { + let i = 5; //skipping info I don't need right now + const desc: number[] = []; + for (; pic[i]; i++) { + desc.push(pic[i]); + } + const description = new TextDecoder().decode(new Uint8Array(desc)); + i++; + const blob = new Blob([pic.slice(i, pic.length).buffer], {type: "image/jpeg"}); + const urlmaker = window.URL || window.webkitURL; + const url = urlmaker.createObjectURL(blob); + output.img = {url, description}; + } + function decodeText(buf: ArrayBuffer) { + let str = new TextDecoder().decode(buf); + if (str.startsWith("\u0000")) { + str = str.slice(1, str.length); + } + if (str.endsWith("\u0000")) { + str = str.slice(0, str.length - 1); + } + return str; + } + const mapmap = { + TT2: "title", + TP1: "artist", + TCM: "composer", + TAL: "sourcy", + TCO: "type", + TEN: "copyright", + } as const; + for (const [key, ind] of Object.entries(mapmap)) { + const temp = mappy.get(key); + if (temp) { + //@ts-ignore TS is being weird about this idk why + output[ind] = decodeText(temp); + } + } + const tye = mappy.get("TYE"); + if (tye) { + output.year = +decodeText(tye.buffer); + } + //TODO more thoroughly check if these two are the same format + } else if (version === 3 || version === 4) { + const flags = await prog.next(); + if (flags & 0b01000000) { + //TODO deal with the extended header + } + //debugger; + const sizes = await prog.get8BitArray(4); + prog.sizeLeft = (sizes[0] << 21) + (sizes[1] << 14) + (sizes[2] << 7) + sizes[3]; + const mappy = new Map>(); + while (prog.sizeLeft > 0) { + const Identify = String.fromCharCode( + await prog.next(), + await prog.next(), + await prog.next(), + await prog.next(), + ); + const sizeArr = await prog.get8BitArray(4); + const size = (sizeArr[0] << 24) + (sizeArr[1] << 16) + (sizeArr[2] << 8) + sizeArr[3]; + + const flags = await prog.get8BitArray(2); + const compression = !!(flags[1] & 0b10000000); + if (compression) { + //TODO Honestly, I don't know if I can do this with normal JS + continue; + } + const encyption = !!(flags[1] & 0b01000000); + if (encyption) { + //TODO not sure what this would even do + continue; + } + + if (Identify === String.fromCharCode(0, 0, 0, 0)) { + break; + } + if (!size) { + //throw Error("weirdness"); + } + if (!Identify.match(/[A-Z0-9]/gm)) { + console.error(`tried to get ${Identify} which doesn't exist`); + break; + } + if (mappy.has(Identify)) { + await prog.get8BitArray(size); + //console.warn("Got dupe", Identify); + } else { + mappy.set(Identify, await prog.get8BitArray(size)); + } + } + const pic = mappy.get("APIC"); + if (pic) { + //const encoding = pic[0]; + let i = 1; //skipping info I don't need right now + for (; pic[i]; i++) {} + i += 2; + let desc: number[] = []; + for (; pic[i]; i++) { + desc.push(pic[i]); + } + const description = new TextDecoder().decode(new Uint8Array(desc)); + i++; + const blob = new Blob([pic.slice(i, pic.length).buffer], {type: "image/jpeg"}); + const urlmaker = window.URL || window.webkitURL; + const url = urlmaker.createObjectURL(blob); + output.img = {url, description}; + } + function decodeText(buf: ArrayBuffer) { + let str = new TextDecoder().decode(buf); + if (str.startsWith("\u0000")) { + str = str.slice(1, str.length); + } + if (str.endsWith("\u0000")) { + str = str.slice(0, str.length - 1); + } + return str; + } + const mapmap = { + TIT2: "title", + TPE1: "artist", + TCOM: "composer", + TALB: "sourcy", + TMED: "type", + TENC: "copyright", + } as const; + for (const [key, ind] of Object.entries(mapmap)) { + const temp = mappy.get(key); + if (temp) { + //@ts-ignore TS is being weird about this idk why + output[ind] = decodeText(temp); + } + } + const TYER = mappy.get("TYER"); + if (TYER) { + output.year = +decodeText(TYER.buffer); + } + const TLEN = mappy.get("TLEN"); + if (TLEN) { + output.length = +decodeText(TLEN.buffer); + } + } + } //TODO implement more metadata types + } catch (e) { + console.error(e); + } finally { + output.filename = new URL(url).pathname.split("/").at(-1); + prog.close(); + if (!output.length) { + output.length = new Promise(async (res) => { + const audio = document.createElement("audio"); + audio.src = url; + audio.onloadeddata = (_) => { + output.length = audio.duration * 1000; + res(audio.duration * 1000); + }; + audio.load(); + }); + } + if (!output.title) { + output.title = output.filename; + } + } + resMedio(output as media); + return output as media; + } +} +export {MediaPlayer, media, makePlayBox}; diff --git a/src/webpage/member.ts b/src/webpage/member.ts index 7fed07c0..0277bc06 100644 --- a/src/webpage/member.ts +++ b/src/webpage/member.ts @@ -1,219 +1,569 @@ -import{ User }from"./user.js"; -import{ Role }from"./role.js"; -import{ Guild }from"./guild.js"; -import{ SnowFlake }from"./snowflake.js"; -import{ memberjson, presencejson }from"./jsontypes.js"; -import { I18n } from "./i18n.js"; -import { Dialog } from "./settings.js"; +import {User} from "./user.js"; +import {Role} from "./role.js"; +import {Guild} from "./guild.js"; +import {SnowFlake} from "./snowflake.js"; +import {highMemberJSON, memberjson, presencejson} from "./jsontypes.js"; +import {I18n} from "./i18n.js"; +import {Dialog, Options, Settings} from "./settings.js"; +import {CDNParams} from "./utils/cdnParams.js"; -class Member extends SnowFlake{ +class Member extends SnowFlake { static already = {}; owner: Guild; user: User; roles: Role[] = []; nick!: string; - - private constructor(memberjson: memberjson, owner: Guild){ + avatar: void | string = undefined; + banner: void | string = undefined; + communication_disabled_until?: Date; + private constructor(memberjson: memberjson, owner: Guild) { super(memberjson.id); this.owner = owner; - if(this.localuser.userMap.has(memberjson.id)){ + if (this.localuser.userMap.has(memberjson.id)) { this.user = this.localuser.userMap.get(memberjson.id) as User; - }else if(memberjson.user){ + } else if (memberjson.user) { this.user = new User(memberjson.user, owner.localuser); - }else{ + } else { throw new Error("Missing user object of this member"); } - if(this.localuser.userMap.has(this?.id)){ + if (this.localuser.userMap.has(this?.id)) { this.user = this.localuser.userMap.get(this?.id) as User; } - for(const key of Object.keys(memberjson)){ - if(key === "guild" || key === "owner" || key === "user"){ + this.update(memberjson); + } + elms = new Set>(); + subName(elm: HTMLElement) { + this.elms.add(new WeakRef(elm)); + } + nameChange() { + for (const ref of this.elms) { + const elm = ref.deref(); + if (!elm || !document.contains(elm)) { + this.elms.delete(ref); continue; } + elm.textContent = this.name; + } + } + commuicationDisabledLeft() { + return this.communication_disabled_until + ? Math.max(+this.communication_disabled_until - Date.now(), 0) + : 0; + } + remove() { + this.user.members.delete(this.guild); + this.guild.members.delete(this); + } + getpfpsrc(): string { + if (this.hypotheticalpfp && this.avatar) { + return this.avatar; + } + if (this.avatar !== undefined && this.avatar !== null) { + return ( + `${this.info.cdn}/guilds/${this.guild.id}/users/${this.id}/avatars/${ + this.avatar + }.${this.avatar.startsWith("a_") ? "gif" : "png"}` + + new CDNParams({expectedSize: 96, animated: this.avatar.startsWith("a_")}) + ); + } + return this.user.getpfpsrc(); + } + getBannerUrl(): string | undefined { + if (this.hypotheticalbanner && this.banner) { + return this.banner; + } + if (this.banner) { + return ( + `${this.info.cdn}/guilds/${this.guild.id}/users/${this.id}/banners/${ + this.banner + }.${this.banner.startsWith("a_") ? "gif" : "png"}` + + new CDNParams({expectedSize: 96, animated: this.banner.startsWith("a_")}) + ); + } else { + return undefined; + } + } + joined_at!: string; + premium_since!: string; + deaf!: boolean; + mute!: boolean; + pending!: boolean; + clone() { + return new Member( + { + id: this.id + "#clone", + user: this.user.tojson(), + guild_id: this.guild.id, + guild: {id: this.guild.id}, + avatar: this.avatar as string | undefined, + banner: this.banner as string | undefined, + //TODO presence + nick: this.nick, + roles: this.roles.map((_) => _.id), + joined_at: this.joined_at, + premium_since: this.premium_since, + deaf: this.deaf, + mute: this.mute, + pending: this.pending, + }, + this.owner, + ); + } + pronouns?: string; + bio?: string; + hypotheticalpfp = false; + hypotheticalbanner = false; + accent_color?: number; + get headers() { + return this.owner.headers; + } - if(key === "roles"){ - for(const strrole of memberjson.roles){ - const role = this.guild.roleids.get(strrole); - if(!role)continue; - this.roles.push(role); - } - continue; - } - if(key === "presence"){ - this.getPresence(memberjson.presence); - continue; - } - (this as any)[key] = (memberjson as any)[key]; + updatepfp(file: Blob): void { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + fetch(this.info.api + `/guilds/${this.guild.id}/members/${this.id}/`, { + method: "PATCH", + headers: this.headers, + body: JSON.stringify({ + avatar: reader.result, + }), + }); + }; + } + updatebanner(file: Blob | null): void { + if (file) { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + fetch(this.info.api + `/guilds/${this.guild.id}/profile/${this.id}`, { + method: "PATCH", + headers: this.headers, + body: JSON.stringify({ + banner: reader.result, + }), + }); + }; + } else { + fetch(this.info.api + `/guilds/${this.guild.id}/profile/${this.id}`, { + method: "PATCH", + headers: this.headers, + body: JSON.stringify({ + banner: null, + }), + }); } - if(!this.user.bot){ - const everyone=this.guild.roleids.get(this.guild.id); - if(everyone&&(this.roles.indexOf(everyone)===-1)){ - this.roles.push(everyone) - } + } + + updateProfile(json: {bio?: string | null; pronouns?: string | null; nick?: string | null}) { + console.log(JSON.stringify(json)); + /* + if(json.bio===""){ + json.bio=null; } - this.roles.sort((a, b)=>{ - return this.guild.roles.indexOf(a) - this.guild.roles.indexOf(b); + if(json.pronouns===""){ + json.pronouns=null; + } + if(json.nick===""){ + json.nick=null; + } + */ + fetch(this.info.api + `/guilds/${this.guild.id}/profile/${this.id}`, { + method: "PATCH", + headers: this.headers, + body: JSON.stringify(json), }); } - remove(){ - this.user.members.delete(this.guild); - this.guild.members.delete(this); + showEditProfile() { + const settings = new Settings("", true); + this.editProfile(settings.addButton(I18n.user.editServerProfile(), {ltr: true})); + settings.show(); } - update(memberjson: memberjson){ - this.roles=[]; - for(const key of Object.keys(memberjson)){ - if(key === "guild" || key === "owner" || key === "user"){ + + showEditNick() { + const dio = new Dialog(""); + const form = dio.options.addForm( + "", + () => { + dio.hide(); + }, + { + fetchURL: this.info.api + `/guilds/${this.guild.id}/members/${this.id}`, + method: "PATCH", + headers: this.headers, + }, + ); + form.addTextInput(I18n.member["nick:"](), "nick", { + initText: this.nick, + }); + dio.show(); + } + editProfile(options: Options) { + if (this.hasPermission("CHANGE_NICKNAME")) { + const hypotheticalProfile = document.createElement("div"); + let file: undefined | File | null; + let newpronouns: string | undefined; + let newbio: string | undefined; + let nick: string | undefined; + const hypomember = this.clone(); + + let color: string; + async function regen() { + hypotheticalProfile.textContent = ""; + const hypoprofile = await hypomember.user.buildprofile(-1, -1, hypomember); + + hypotheticalProfile.appendChild(hypoprofile); + } + regen(); + const settingsLeft = options.addOptions(""); + const settingsRight = options.addOptions(""); + settingsRight.addHTMLArea(hypotheticalProfile); + + const nicky = settingsLeft.addTextInput(I18n.member["nick:"](), () => {}, { + initText: this.nick || "", + }); + nicky.watchForChange((_) => { + hypomember.nick = _; + nick = _; + regen(); + }); + + const finput = settingsLeft.addFileInput( + I18n.uploadPfp(), + (_) => { + if (file) { + this.updatepfp(file); + } + }, + {clear: true}, + ); + finput.watchForChange((_) => { + if (!_) { + file = null; + hypomember.avatar = undefined; + hypomember.hypotheticalpfp = true; + regen(); + return; + } + if (_.length) { + file = _[0]; + const blob = URL.createObjectURL(file); + hypomember.avatar = blob; + hypomember.hypotheticalpfp = true; + regen(); + } + }); + let bfile: undefined | File | null; + const binput = settingsLeft.addFileInput( + I18n.uploadBanner(), + (_) => { + if (bfile !== undefined) { + this.updatebanner(bfile); + } + }, + {clear: true}, + ); + binput.watchForChange((_) => { + if (!_) { + bfile = null; + hypomember.banner = undefined; + hypomember.hypotheticalbanner = true; + regen(); + return; + } + if (_.length) { + bfile = _[0]; + const blob = URL.createObjectURL(bfile); + hypomember.banner = blob; + hypomember.hypotheticalbanner = true; + regen(); + } + }); + let changed = false; + const pronounbox = settingsLeft.addTextInput( + I18n.pronouns(), + (_) => { + if (newpronouns !== undefined || newbio !== undefined || changed !== undefined) { + this.updateProfile({ + pronouns: newpronouns, + bio: newbio, + //accent_color: Number.parseInt("0x" + color.substr(1), 16), + nick, + }); + } + }, + {initText: this.pronouns}, + ); + pronounbox.watchForChange((_) => { + hypomember.pronouns = _; + newpronouns = _; + regen(); + }); + const bioBox = settingsLeft.addMDInput(I18n.bio(), (_) => {}, { + initText: this.bio, + }); + bioBox.watchForChange((_) => { + newbio = _; + hypomember.bio = _; + regen(); + }); + color = (this.accent_color ? "#" + this.accent_color.toString(16) : "transparent") as string; + + const colorPicker = settingsLeft.addColorInput(I18n.profileColor(), (_) => {}, { + initColor: color, + }); + colorPicker.watchForChange((_) => { + console.log(); + color = _; + hypomember.accent_color = Number.parseInt("0x" + _.substring(1)); + changed = true; + regen(); + }); + } + } + update(memberjson: memberjson) { + const changeNick = this.nick !== memberjson.nick; + for (const key of Object.keys(memberjson)) { + if (key === "guild" || key === "owner" || key === "user") { + continue; + } + if (key === "communication_disabled_until") { + this.communication_disabled_until = new Date( + memberjson.communication_disabled_until as string, + ); + if (this.id === this.localuser.user.id) { + this.guild.recalcPrivate(); + } continue; } - if(key === "roles"){ - for(const strrole of memberjson.roles){ + if (key === "roles") { + if ((memberjson.roles[0] as unknown) instanceof Object) { + memberjson.roles = (memberjson.roles as any[]).map((_) => _.id); + console.error("Member role is incorrectly sent as role object instead of role ID"); + } + this.roles = []; + for (const strrole of memberjson.roles) { const role = this.guild.roleids.get(strrole); - if(!role)continue; + if (!role) { + console.warn(strrole + " is not in ", this.guild.roleids); + continue; + } this.roles.push(role); } + + if (!this.user.bot) { + const everyone = this.guild.roleids.get(this.guild.id); + if (everyone && this.roles.indexOf(everyone) === -1) { + this.roles.push(everyone); + } + } continue; } - if(key === "presence"){ + if (key === "presence") { this.getPresence(memberjson.presence); continue; } (this as any)[key] = (memberjson as any)[key]; } - this.roles.sort((a, b)=>{ + const everyone = this.guild.roleids.get(this.guild.id); + if (everyone && this.roles.indexOf(everyone) === -1) { + this.roles.push(everyone); + } + + this.roles.sort((a, b) => { return this.guild.roles.indexOf(a) - this.guild.roles.indexOf(b); }); + if (changeNick) { + this.nameChange(); + } } - get guild(){ + get guild() { return this.owner; } - get localuser(){ + get localuser() { return this.guild.localuser; } - get info(){ + get info() { return this.owner.info; } - static async new( - memberjson: memberjson, - owner: Guild - ): Promise{ + static async new(memberjson: memberjson, owner: Guild): Promise { let user: User; - if(owner.localuser.userMap.has(memberjson.id)){ + if (owner.localuser.userMap.has(memberjson.id)) { + if (memberjson.user) { + new User(memberjson.user, owner.localuser); + } user = owner.localuser.userMap.get(memberjson.id) as User; - }else if(memberjson.user){ + } else if (memberjson.user) { user = new User(memberjson.user, owner.localuser); - }else{ + } else { throw new Error("missing user object of this member"); } - if(user.members.has(owner)){ + if (user.members.has(owner)) { let memb = user.members.get(owner); - if(memb === undefined){ + if (memb === undefined) { memb = new Member(memberjson, owner); user.members.set(owner, memb); owner.members.add(memb); + user.localuser.memberListUpdate(); return memb; - }else if(memb instanceof Promise){ - return await memb; //I should do something else, though for now this is "good enough" - }else{ - if(memberjson.presence){ + } else if (memb instanceof Promise) { + const member = await memb; //I should do something else, though for now this is "good enough"; + if (member) { + member.update(memberjson); + } + return member; + } else { + if (memberjson.presence) { memb.getPresence(memberjson.presence); } + memb.update(memberjson); return memb; } - }else{ + } else { const memb = new Member(memberjson, owner); user.members.set(owner, memb); owner.members.add(memb); + memb.localuser.memberListUpdate(); return memb; } } - compare(str:string){ - function similar(str2:string|null|undefined){ - if(!str2) return 0; - const strl=Math.max(str.length,1) - if(str2.includes(str)){ - return strl/str2.length; - }else if(str2.toLowerCase().includes(str.toLowerCase())){ - return strl/str2.length/1.2; + compare(str: string) { + function similar(str2: string | null | undefined) { + if (!str2) return 0; + const strl = Math.max(str.length, 1); + if (str2.includes(str)) { + return strl / str2.length; + } else if (str2.toLowerCase().includes(str.toLowerCase())) { + return strl / str2.length / 1.2; } return 0; } - return Math.max(similar(this.user.name),similar(this.user.nickname),similar(this.nick),similar(this.user.username),similar(this.id)/1.5); + return Math.max( + similar(this.user.name), + similar(this.user.nickname), + similar(this.nick), + similar(this.user.username), + similar(this.id) / 1.5, + ); } - static async resolveMember( - user: User, - guild: Guild - ): Promise{ + static async resolveMember(user: User, guild: Guild): Promise { + if (guild.id === "@me") return; + if (user.webhook) return undefined; const maybe = user.members.get(guild); - if(!user.members.has(guild)){ + if (!user.members.has(guild)) { const membpromise = guild.localuser.resolvemember(user.id, guild.id); - const promise = new Promise(async res=>{ + const promise = new Promise(async (res) => { const membjson = await membpromise; - if(membjson === undefined){ + if (membjson === undefined) { return res(undefined); - }else{ + } else { const member = new Member(membjson, guild); const map = guild.localuser.presences; member.getPresence(map.get(member.id)); map.delete(member.id); res(member); + guild.localuser.memberListQue(); return member; } }); user.members.set(guild, promise); - return await promise; + const member = await promise; + if (member) { + guild.members.add(member); + } + user.members.set(guild, member); + return member; } - if(maybe instanceof Promise){ + if (maybe instanceof Promise) { return await maybe; - }else{ + } else { return maybe; } } - public getPresence(presence: presencejson | undefined){ + getRoleIcon() { + for (const role of this.roles) { + const icon = role.getIcon(); + if (icon) { + return icon; + } + } + return; + } + public getPresence(presence: presencejson | undefined) { this.user.getPresence(presence); } /** - * @todo - */ - highInfo(){ - fetch( - this.info.api + - "/users/" + - this.id + - "/profile?with_mutual_guilds=true&with_mutual_friends_count=true&guild_id=" + - this.guild.id, - { headers: this.guild.headers } - ); + * @todo + */ + async highInfo() { + return (await ( + await fetch( + this.info.api + + "/users/" + + this.id + + "/profile?with_mutual_guilds=true&with_mutual_friends=true&guild_id=" + + this.guild.id, + {headers: this.guild.headers}, + ) + ).json()) as highMemberJSON; } - hasRole(ID: string){ - for(const thing of this.roles){ - if(thing.id === ID){ + hasRole(ID: string) { + for (const thing of this.roles) { + if (thing.id === ID) { return true; } } return false; } - getColor(){ - for(const thing of this.roles){ + getTopColor() { + if (!this.localuser.perminfo.user.disableColors) { + return ""; + } + for (const thing of this.roles) { const color = thing.getColor(); - if(color){ + if (color) { + return thing.id; + } + } + return ""; + } + getColor() { + if (!this.localuser.perminfo.user.disableColors) { + return ""; + } + for (const thing of this.roles) { + const color = thing.getColor(); + if (color) { return color; } } - return""; + return ""; + } + getColorStyle() { + if (!this.localuser.perminfo.user.disableColors) { + return undefined; + } + for (const thing of this.roles) { + const color = thing.getColor(); + if (color) { + return `var(--role-${thing.id})`; + } + } + return undefined; } - isAdmin(){ - for(const role of this.roles){ - if(role.permissions.getPermission("ADMINISTRATOR")){ + isAdmin() { + for (const role of this.roles) { + if (role.permissions.getPermission("ADMINISTRATOR")) { return true; } } return this.guild.properties.owner_id === this.user.id; } - bind(html: HTMLElement){ - if(html.tagName === "SPAN"){ - if(!this){ + bind(html: HTMLElement) { + if (html.tagName === "SPAN") { + if (!this) { return; } /* @@ -221,28 +571,68 @@ class Member extends SnowFlake{ } */ - html.style.color = this.getColor(); + const id = this.getTopColor(); + if (id) html.style.setProperty("--userbg", `var(--role-${id})`); } //this.profileclick(html); } - profileclick(/* html: HTMLElement */){ + profileclick(/* html: HTMLElement */) { //to be implemented } - get name(){ - return this.nick || this.user.username; + get name() { + return this.nick || this.user.name; } - kick(){ + kick() { const menu = new Dialog(""); - const form=menu.options.addForm("",((e:any)=>{ + const form = menu.options.addForm("", (e: any) => { this.kickAPI(e.reason); menu.hide(); - })); - form.addTitle(I18n.getTranslation("member.kick",this.name,this.guild.properties.name)); - form.addTextInput(I18n.getTranslation("member.reason:"),"reason"); + }); + form.addTitle(I18n.member.kick(this.name, this.guild.properties.name)); + form.addTextInput(I18n.member["reason:"](), "reason"); menu.show(); } - kickAPI(reason: string){ + timeout() { + const menu = new Dialog(""); + const form = menu.options.addForm("", (e: any) => { + this.timeoutAPI(e.reason, e.time); + menu.hide(); + }); + form.addTitle(I18n.member.timeout(this.name)); + form.addTextInput(I18n.member["reason:"](), "reason"); + //TODO make this custom :3 + form.addSelect( + I18n.member.timeoutTime(), + "time", + ["60s", "5m", "10m", "1h", "1d", "1w"], + {}, + [60, 300, 600, 60 * 60, 60 * 60 * 24, 60 * 60 * 24 * 7].map((_) => _ * 1000), + ); + menu.show(); + } + removeTimeout() { + const headers = structuredClone(this.guild.headers); + fetch(`${this.info.api}/guilds/${this.guild.id}/members/${this.id}`, { + method: "PATCH", + headers, + body: JSON.stringify({ + communication_disabled_until: null, + }), + }); + } + timeoutAPI(reason: string, length: number) { + const headers = structuredClone(this.guild.headers); + (headers as any)["x-audit-log-reason"] = reason; + fetch(`${this.info.api}/guilds/${this.guild.id}/members/${this.id}`, { + method: "PATCH", + headers, + body: JSON.stringify({ + communication_disabled_until: new Date(length + Date.now()) + "", + }), + }); + } + kickAPI(reason: string) { const headers = structuredClone(this.guild.headers); (headers as any)["x-audit-log-reason"] = reason; fetch(`${this.info.api}/guilds/${this.guild.id}/members/${this.id}`, { @@ -250,35 +640,35 @@ class Member extends SnowFlake{ headers, }); } - ban(){ + ban() { const menu = new Dialog(""); - const form=menu.options.addForm("",((e:any)=>{ + const form = menu.options.addForm("", (e: any) => { this.banAPI(e.reason); menu.hide(); - })); - form.addTitle(I18n.getTranslation("member.ban",this.name,this.guild.properties.name)); - form.addTextInput(I18n.getTranslation("member.reason:"),"reason"); + }); + form.addTitle(I18n.member.ban(this.name, this.guild.properties.name)); + form.addTextInput(I18n.member["reason:"](), "reason"); menu.show(); } - addRole(role:Role){ - const roles=this.roles.map(_=>_.id) + addRole(role: Role) { + const roles = this.roles.map((_) => _.id); roles.push(role.id); - fetch(this.info.api+"/guilds/"+this.guild.id+"/members/"+this.id,{ - method:"PATCH", - headers:this.guild.headers, - body:JSON.stringify({roles}) - }) - } - removeRole(role:Role){ - let roles=this.roles.map(_=>_.id) - roles=roles.filter(_=>_!==role.id); - fetch(this.info.api+"/guilds/"+this.guild.id+"/members/"+this.id,{ - method:"PATCH", - headers:this.guild.headers, - body:JSON.stringify({roles}) - }) - } - banAPI(reason: string){ + fetch(this.info.api + "/guilds/" + this.guild.id + "/members/" + this.id, { + method: "PATCH", + headers: this.guild.headers, + body: JSON.stringify({roles}), + }); + } + removeRole(role: Role) { + let roles = this.roles.map((_) => _.id); + roles = roles.filter((_) => _ !== role.id); + fetch(this.info.api + "/guilds/" + this.guild.id + "/members/" + this.id, { + method: "PATCH", + headers: this.guild.headers, + body: JSON.stringify({roles}), + }); + } + banAPI(reason: string) { const headers = structuredClone(this.guild.headers); (headers as any)["x-audit-log-reason"] = reason; fetch(`${this.info.api}/guilds/${this.guild.id}/bans/${this.id}`, { @@ -286,16 +676,22 @@ class Member extends SnowFlake{ headers, }); } - hasPermission(name: string): boolean{ - if(this.isAdmin()){ + hasPermission(name: string, adminOver = true): boolean { + if (this.isAdmin() && adminOver) { return true; } - for(const thing of this.roles){ - if(thing.permissions.getPermission(name)){ + if (this.guild.member.commuicationDisabledLeft()) { + const allowSet = new Set(["READ_MESSAGE_HISTORY", "VIEW_CHANNEL"]); + if (!allowSet.has(name)) { + return false; + } + } + for (const thing of this.roles) { + if (thing.permissions.getPermission(name)) { return true; } } return false; } } -export{ Member }; +export {Member}; diff --git a/src/webpage/message.ts b/src/webpage/message.ts index 843a0cbd..ee105425 100644 --- a/src/webpage/message.ts +++ b/src/webpage/message.ts @@ -1,36 +1,48 @@ -import{ Contextmenu }from"./contextmenu.js"; -import{ User }from"./user.js"; -import{ Member }from"./member.js"; -import{ MarkDown, saveCaretPosition }from"./markdown.js"; -import{ Embed }from"./embed.js"; -import{ Channel }from"./channel.js"; -import{ Localuser }from"./localuser.js"; -import{ Role }from"./role.js"; -import{ File }from"./file.js"; -import{ SnowFlake }from"./snowflake.js"; -import{ memberjson, messagejson }from"./jsontypes.js"; -import{ Emoji }from"./emoji.js"; -import{ mobile }from"./login.js"; -import { I18n } from "./i18n.js"; -import { Hover } from "./hover.js"; -import { Dialog } from "./settings.js"; - -class Message extends SnowFlake{ - static contextmenu = new Contextmenu("message menu"); +import {Contextmenu} from "./contextmenu.js"; +import {User} from "./user.js"; +import {Member} from "./member.js"; +import {MarkDown, saveCaretPosition} from "./markdown.js"; +import {Embed} from "./embed.js"; +import {Channel} from "./channel.js"; +import {Localuser} from "./localuser.js"; +import {Role} from "./role.js"; +import {File} from "./file.js"; +import {SnowFlake} from "./snowflake.js"; +import { + channeljson, + emojijson, + interactionEvents, + memberjson, + messagejson, + userjson, +} from "./jsontypes.js"; +import {Emoji} from "./emoji.js"; +import {mobile} from "./utils/utils.js"; +import {I18n} from "./i18n.js"; +import {Hover} from "./hover.js"; +import {Dialog} from "./settings.js"; +import {Sticker} from "./sticker.js"; +import {Components} from "./interactions/compontents.js"; +import {ImagesDisplay} from "./disimg"; +import {ReportMenu} from "./reporting/report.js"; +import {getDeveloperSettings} from "./utils/storage/devSettings.js"; +class Message extends SnowFlake { + static contextmenu = new Contextmenu("message menu"); + stickers!: Sticker[]; owner: Channel; headers: Localuser["headers"]; - embeds!: Embed[]; + embeds: Embed[] = []; author!: User; - mentions!: User[]; + mentions: userjson[] = []; mention_roles!: Role[]; - attachments!: File[]; //probably should be its own class tbh, should be Attachments[] - message_reference!: { - guild_id: string, - channel_id: string, - message_id: string + attachments: File[] = []; //probably should be its own class tbh, should be Attachments[] + message_reference?: { + guild_id: string; + channel_id: string; + message_id: string; }; type!: number; - timestamp!: number; + private timestamp!: number | string; content!: MarkDown; static del: Promise; static resolve: Function; @@ -46,564 +58,1073 @@ class Message extends SnowFlake{ get div(){ return this.weakdiv?.deref(); } - //*/ - div:(HTMLDivElement & { pfpparent?: Message | undefined; txt?: HTMLElement })| undefined; + */ + div: HTMLDivElement | undefined; member: Member | undefined; - reactions!: messagejson["reactions"]; - static setup(){ - this.del = new Promise(_=>{ + reactions: { + count: number; + emoji: emojijson; + me: boolean; + }[] = []; + pinned!: boolean; + flags: number = 0; + getTimeStamp() { + return new Date(this.timestamp).getTime(); + } + static setup() { + this.del = new Promise((_) => { this.resolve = _; }); Message.setupcmenu(); } - static setupcmenu(){ - Message.contextmenu.addbutton(()=>I18n.getTranslation("copyrawtext"), function(this: Message){ - navigator.clipboard.writeText(this.content.rawString); - }); - Message.contextmenu.addbutton(()=>I18n.getTranslation("reply"), function(this: Message){ - this.channel.setReplying(this); - }); - Message.contextmenu.addbutton(()=>I18n.getTranslation("copymessageid"), function(this: Message){ - navigator.clipboard.writeText(this.id); - }); - Message.contextmenu.addsubmenu( - ()=>I18n.getTranslation("message.reactionAdd"), - function(this: Message, _, e: MouseEvent){ - Emoji.emojiPicker(e.x, e.y, this.localuser).then(_=>{ + static setupcmenu() { + const editTypes = new Set([0, 19]); + Message.contextmenu.addButton( + () => I18n.message.edit(), + function (this: Message) { + this.setEdit(); + }, + { + visible: function () { + return this.author.id === this.localuser.user.id && editTypes.has(this.type); + }, + + icon: { + css: "svg-edit", + }, + }, + ); + Message.contextmenu.addButton( + () => I18n.reply(), + function (this: Message) { + this.channel.setReplying(this); + }, + { + icon: { + css: "svg-reply", + }, + visible: function () { + return !this.ephemeral && this.channel.hasPermission("SEND_MESSAGES"); + }, + }, + ); + + Message.contextmenu.addButton( + () => I18n.message.reactionAdd(), + function (this: Message, _, e: MouseEvent) { + Emoji.emojiPicker(e.x, e.y, this.localuser).then((_) => { this.reactionToggle(_); }); - } + }, + { + icon: { + css: "svg-emoji", + }, + visible: function () { + return this.channel.hasPermission("ADD_REACTIONS"); + }, + }, ); - Message.contextmenu.addbutton( - ()=>I18n.getTranslation("message.edit"), - function(this: Message){ - this.setEdit(); + + Message.contextmenu.addButton( + () => I18n.message.createThread(), + function () { + const dio = new Dialog(I18n.message.threadOptions()); + const opt = dio.options.addForm( + "", + (body) => { + const channelJson = body as channeljson; + this.guild.goToThread(channelJson.id); + dio.hide(); + }, + { + fetchURL: + this.info.api + "/channels/" + this.channel.id + "/messages/" + this.id + "/threads", + headers: this.headers, + }, + ); + opt.addTextInput(I18n.threads.name(), "name"); + dio.show(); + }, + { + visible: function () { + return this.channel.hasPermission("CREATE_PUBLIC_THREADS"); + }, + }, + ); + Message.contextmenu.addButton( + () => I18n.message.reactions(), + function (this: Message) { + this.viewReactions(); + }, + { + visible: function () { + return !!this.reactions.length; + }, + }, + ); + + Message.contextmenu.addSeperator(); + Message.contextmenu.addButton( + () => I18n.copyrawtext(), + function (this: Message) { + navigator.clipboard.writeText(this.content.rawString); + }, + { + icon: { + css: "svg-copy", + }, + }, + ); + Message.contextmenu.addGroup("copyLink"); + Message.contextmenu.addButton( + () => I18n.copyLink(), + function (this: Message) { + navigator.clipboard.writeText( + `${window.location.origin}/channels/${this.guild.id}/${this.channel.id}/${this.id}`, + ); + }, + { + //TODO make icon + }, + ); + Message.contextmenu.addButton( + () => I18n.pinMessage(), + async function (this: Message) { + const f = await fetch(`${this.info.api}/channels/${this.channel.id}/pins/${this.id}`, { + method: "PUT", + headers: this.headers, + }); + if (!f.ok) alert(I18n.unableToPin()); + }, + { + icon: { + css: "svg-pin", + }, + visible: function () { + if (this.pinned) return false; + if (this.channel.guild.id === "@me") return true; + return this.channel.hasPermission("MANAGE_MESSAGES"); + }, + }, + ); + + Message.contextmenu.addButton( + () => I18n.unpinMessage(), + async function (this: Message) { + const f = await fetch(`${this.info.api}/channels/${this.channel.id}/pins/${this.id}`, { + method: "DELETE", + headers: this.headers, + }); + if (!f.ok) alert(I18n.unableToPin()); + }, + { + icon: { + css: "svg-pin", + }, + visible: function () { + if (!this.pinned) return false; + if (this.channel.guild.id === "@me") return true; + return this.channel.hasPermission("MANAGE_MESSAGES"); + }, }, - null, - function(){ - return this.author.id === this.localuser.user.id; - } ); - Message.contextmenu.addbutton( - ()=>I18n.getTranslation("message.delete"), - function(this: Message){ + + Message.contextmenu.addButton( + () => I18n.copymessageid(), + function (this: Message) { + navigator.clipboard.writeText(this.id); + }, + ); + + Message.contextmenu.addSeperator(); + Message.contextmenu.addButton( + () => I18n.message.delete(), + function (this: Message) { this.confirmDelete(); }, - null, - function(){ - return this.canDelete(); - } + { + visible: function () { + return this.canDelete(); + }, + icon: { + css: "svg-delete", + }, + color: "red", + }, + ); + Message.contextmenu.addButton( + () => I18n.message.report(), + async function () { + const menu = await ReportMenu.makeReport("message", this.localuser, {message: this}); + menu?.spawnMenu(); + }, + { + visible: function () { + const settings = getDeveloperSettings(); + return this.author.id !== this.localuser.user.id && settings.reportSystem; + }, + color: "red", + }, + ); + + Message.contextmenu.addSeperator(); + Message.contextmenu.addButton( + () => I18n.usedFermi(), + () => {}, + { + visible: function () { + return !!this.nonce && this.nonce.length <= 9 && this.nonce.length !== 0; + }, + enabled: () => false, + }, ); } - setEdit(){ - const prev=this.channel.editing; + viewReactions() { + const dio = new Dialog(I18n.message.reactionsTitle()); + const div = document.createElement("div"); + div.classList.add("flexltr"); + const reactions = document.createElement("div"); + reactions.classList.add("flexttb", "reactionList"); + + const list = document.createElement("div"); + list.classList.add("flexttb", "reactionUserList"); + let curSelect = document.createElement("div"); + reactions.append( + ...this.reactions.map((reaction) => { + const button = document.createElement("div"); + + console.log(reaction); + const emoji = new Emoji(reaction.emoji, this.guild); + button.append(emoji.getHTML(), `(${reaction.count})`); + let users: User[] | undefined = undefined; + button.onclick = async () => { + curSelect.classList.remove("current"); + + curSelect = button; + curSelect.classList.add("current"); + if (!users) { + const f = await fetch( + `${this.info.api}/channels/${this.channel.id}/messages/${this.id}/reactions/${reaction.emoji.name}?limit=50&type=0`, + {headers: this.headers}, + ); + users = ((await f.json()) as userjson[]).map((_) => new User(_, this.localuser)); + } + list.innerHTML = ""; + list.append( + ...users.map((user) => { + return user.createWidget(this.guild); + }), + ); + }; + + return button; + }), + ); + //@ts-ignore + [...reactions.children][0].click(); + div.append(reactions, list); + dio.options.addHTMLArea(div); + dio.show(); + } + nonce: string = ""; + setEdit() { + const prev = this.channel.editing; this.channel.editing = this; - if(prev) prev.generateMessage(); - this.generateMessage(undefined,false) + if (prev) prev.generateMessage(); + this.generateMessage(undefined, false); } - constructor(messagejson: messagejson, owner: Channel){ + constructor(messagejson: messagejson, owner: Channel, dontStore = false) { super(messagejson.id); this.owner = owner; this.headers = this.owner.headers; this.giveData(messagejson); - this.owner.messages.set(this.id, this); + if (!dontStore) { + this.owner.messages.set(this.id, this); + if (messagejson.referenced_message) + this.localuser.giveMessage(messagejson.referenced_message); + } } - reactionToggle(emoji: string | Emoji){ - let remove = false; - for(const thing of this.reactions){ - if(thing.emoji.name === emoji){ - remove = thing.me; - break; - } + reactionToggle(emoji: string | Emoji) { + if (emoji instanceof Emoji && !emoji.id && emoji.emoji) { + emoji = emoji.emoji; } + let remove = !!this.reactions.find((_) => _.emoji.name === emoji)?.me; + let reactiontxt: string; - if(emoji instanceof Emoji){ + if (emoji instanceof Emoji) { reactiontxt = `${emoji.name}:${emoji.id}`; - }else{ + } else { reactiontxt = encodeURIComponent(emoji); } + if (!remove) { + this.localuser.favorites.addReactEmoji( + emoji instanceof Emoji ? emoji.id || (emoji.emoji as string) : emoji, + ); + } fetch( `${this.info.api}/channels/${this.channel.id}/messages/${this.id}/reactions/${reactiontxt}/@me`, { method: remove ? "DELETE" : "PUT", headers: this.headers, - } + }, ); } - edited_timestamp:string|null=null; - giveData(messagejson: messagejson){ + components?: Components; + edited_timestamp: string | null = null; + thread?: Channel; + giveData(messagejson: messagejson) { const func = this.channel.infinite.snapBottom(); - for(const thing of Object.keys(messagejson)){ - if(thing === "attachments"){ + for (const thing of Object.keys(messagejson)) { + if (thing === "attachments") { this.attachments = []; - for(const thing of messagejson.attachments){ + for (const thing of messagejson.attachments) { this.attachments.push(new File(thing, this)); } continue; - }else if(thing === "content"){ - this.content = new MarkDown(messagejson[thing], this.channel); + } else if (thing === "content") { + this.content = new MarkDown(messagejson[thing] || "", this.channel); continue; - }else if(thing === "id"){ + } else if (thing === "id") { continue; - }else if(thing === "member"){ - Member.new(messagejson.member as memberjson, this.guild).then(_=>{ + } else if (thing === "member") { + Member.new(messagejson.member as memberjson, this.guild).then((_) => { this.member = _ as Member; }); continue; - }else if(thing === "embeds"){ + } else if (thing === "embeds") { this.embeds = []; - for(const thing in messagejson.embeds){ + for (const thing in messagejson.embeds) { this.embeds[thing] = new Embed(messagejson.embeds[thing], this); } continue; + } else if (thing === "author") { + continue; + } else if (thing === "sticker_items") { + this.stickers = messagejson.sticker_items.map((_) => { + const guild = this.localuser.guildids.get(_.guild_id as string); + return new Sticker(_, guild || this.localuser); + }); + } else if (thing === "components" && messagejson.components) { + this.components = new Components(messagejson.components, this); + continue; + } else if (thing === "thread" && messagejson.thread) { + let thread = this.localuser.channelids.get(messagejson.thread.id); + if (!thread) { + thread = new Channel(messagejson.thread, this.guild); + thread.resolveparent(); + this.localuser.channelids.set(thread.id, thread); + } + this.thread = thread; + continue; } (this as any)[thing] = (messagejson as any)[thing]; } - if(messagejson.reactions?.length){ + this.stickers ||= []; + if (messagejson.reactions?.length) { console.log(messagejson.reactions, ":3"); } - - this.author = new User(messagejson.author, this.localuser); - for(const thing in messagejson.mentions){ - this.mentions[thing] = new User( - messagejson.mentions[thing], - this.localuser - ); + if (messagejson.webhook) { + messagejson.author.webhook = messagejson.webhook; } - if(!this.member && this.guild.id !== "@me"){ - this.author.resolvemember(this.guild).then(_=>{ + if (messagejson.author.id) { + this.author = new User(messagejson.author, this.localuser, false); + } + if (messagejson.mentions) this.mentions = messagejson.mentions; + + this.mention_roles = (messagejson.mention_roles || []) + .map((role: string | {id: string}) => { + return this.guild.roleids.get(role instanceof Object ? role.id : role); + }) + .filter((_) => _ !== undefined); + + if (!this.member && this.guild.id !== "@me") { + this.author.resolvemember(this.guild).then((_) => { this.member = _; }); } - if(this.mentions.length || this.mention_roles.length){ - //currently mention_roles isn't implemented on the spacebar servers - console.log(this.mentions, this.mention_roles); - } - if(this.mentionsuser(this.localuser.user)){ - console.log(this); - } - if(this.div){ + if (this.div) { this.generateMessage(); + return; + } + if (+this.id > +(this.channel.lastmessageid || "0")) { + func(); } - func(); } - canDelete(){ - return( - this.channel.hasPermission("MANAGE_MESSAGES") || - this.author === this.localuser.user - ); + canDelete() { + return this.channel.hasPermission("MANAGE_MESSAGES") || this.author === this.localuser.user; } - get channel(){ + get channel() { return this.owner; } - get guild(){ + get guild() { return this.owner.guild; } - get localuser(){ + get localuser() { return this.owner.localuser; } - get info(){ + get info() { return this.owner.info; } - messageevents(obj: HTMLDivElement){ - let drag=false; - Message.contextmenu.bindContextmenu(obj, this, undefined,(x)=>{ - //console.log(x,y); - if(!drag&&x<20){ - return - } - drag=true; - this.channel.moveForDrag(Math.max(x,0)); - - },(x,y)=>{ - drag=false; - console.log(x,y); - this.channel.moveForDrag(-1); - if(x>60){ - console.log("In here?") - const toggle = document.getElementById("maintoggle") as HTMLInputElement; - toggle.checked = false; - console.log(toggle); - } + interactionDiv?: HTMLDivElement; + interactionEvents(event: interactionEvents) { + if (!this.interactionDiv) return; + this.interactionDiv.classList.remove("failed"); + switch (event.t) { + case "INTERACTION_CREATE": + this.interactionDiv.textContent = I18n.interactions.started(); + break; + case "INTERACTION_SUCCESS": + this.interactionDiv.textContent = I18n.interactions.worked(); + setTimeout(() => { + if (this.interactionDiv?.textContent === I18n.interactions.worked()) { + this.interactionDiv.textContent = ""; + } + }, 1000); + break; + case "INTERACTION_FAILURE": + this.interactionDiv.textContent = I18n.interactions.failed(); + this.interactionDiv.classList.add("failed"); + setTimeout(() => { + if (this.interactionDiv?.textContent === I18n.interactions.failed()) { + this.interactionDiv.textContent = ""; + } + }, 5000); + break; + } + } + messageevents(obj: HTMLDivElement) { + let drag = false; + Message.contextmenu.bindContextmenu( + obj, + this, + undefined, + (x) => { + //console.log(x); + if (x < -20) { + obj.style.translate = x + 20 + "px 0px"; + } else obj.style.translate = 0 + "px"; - },); + if (!drag && x < 20) { + return; + } + + drag = true; + this.channel.moveForDrag(Math.max(x, 0)); + }, + (x, y) => { + drag = false; + console.log(x, y); + obj.style.translate = 0 + "px"; + this.channel.moveForDrag(-1); + if (x > 60) { + console.log("In here?"); + const toggle = document.getElementById("maintoggle") as HTMLInputElement; + toggle.checked = false; + console.log(toggle); + } + if (x < -40) { + this.channel.setReplying(this); + } + }, + ); this.div = obj; obj.classList.add("messagediv"); } - deleteDiv(){ - if(!this.div)return; - try{ + deleteDiv() { + if (!this.div) return; + try { this.div.remove(); this.div = undefined; - }catch(e){ + } catch (e) { console.error(e); } } - mentionsuser(userd: User | Member){ - if(userd instanceof User){ - return this.mentions.includes(userd); - }else if(userd instanceof Member){ - return this.mentions.includes(userd.user); - }else{ + mention_everyone!: boolean; + mentionsuser(userd: User | Member) { + if (this.mention_everyone) return true; + if (userd instanceof User) { + return !!this.mentions.find(({id}) => id == userd.id); + } else if (userd instanceof Member) { + if (!!this.mentions.find(({id}) => id == userd.id)) { + return true; + } else { + return !new Set(this.mention_roles).isDisjointFrom(new Set(userd.roles)); //if the message mentions a role the user has + } + } else { return false; } } - getimages(){ + getimages() { const build: File[] = []; - for(const thing of this.attachments){ - if(thing.content_type.startsWith("image/")){ + for (const thing of this.attachments) { + if (thing.content_type.startsWith("image/")) { build.push(thing); } } return build; } - async edit(content: string){ - return await fetch( - this.info.api + "/channels/" + this.channel.id + "/messages/" + this.id, - { - method: "PATCH", - headers: this.headers, - body: JSON.stringify({ content }), - } - ); + getUnixTime(): number { + return new Date(this.timestamp).getTime(); + } + async edit(content: string) { + if (content === this.content.textContent) { + return; + } + return await fetch(this.info.api + "/channels/" + this.channel.id + "/messages/" + this.id, { + method: "PATCH", + headers: this.headers, + body: JSON.stringify({content}), + }); } - delete(){ - fetch(`${this.info.api}/channels/${this.channel.id}/messages/${this.id}`, { + async delete() { + await fetch(`${this.info.api}/channels/${this.channel.id}/messages/${this.id}`, { headers: this.headers, method: "DELETE", }); } - deleteEvent(){ + deleteEvent() { + if (!this.channel.messages.has(this.id)) return; + if (!this.id.includes("fake") && this.channel.messageCount !== undefined) + this.channel.messageCount--; console.log("deleted"); - if(this.div){ + this.channel.infinite.deleteId(this.id); + if (this.div) { this.div.remove(); this.div.innerHTML = ""; this.div = undefined; } const prev = this.channel.idToPrev.get(this.id); const next = this.channel.idToNext.get(this.id); - this.channel.idToPrev.delete(this.id); - this.channel.idToNext.delete(this.id); + this.channel.messages.delete(this.id); - if(prev && next){ - this.channel.idToPrev.set(next, prev); - this.channel.idToNext.set(prev, next); - }else if(prev){ + if (this.channel.idToPrev.has(this.id) && this.channel.idToNext.has(this.id)) { + if (next) this.channel.idToPrev.set(next, prev); + if (prev) this.channel.idToNext.set(prev, next); + } else if (prev) { this.channel.idToNext.delete(prev); - }else if(next){ + } else if (next) { this.channel.idToPrev.delete(next); } - if(prev){ - const prevmessage = this.channel.messages.get(prev); - if(prevmessage){ - prevmessage.generateMessage(); + if (prev) { + const prevMessage = this.channel.messages.get(prev); + if (prevMessage) { + prevMessage.generateMessage(); + } + } + if (next) { + const nextMessage = this.channel.messages.get(next); + if (nextMessage) { + nextMessage.generateMessage(); } } - if( - this.channel.lastmessage === this || - this.channel.lastmessageid === this.id - ){ - if(prev){ + if (this.channel.lastmessage === this || this.channel.lastmessageid === this.id) { + if (prev) { this.channel.lastmessage = this.channel.messages.get(prev); this.channel.lastmessageid = prev; - }else{ + } else { this.channel.lastmessage = undefined; this.channel.lastmessageid = undefined; } } - if(this.channel.lastreadmessageid === this.id){ - if(prev){ + if (this.channel.lastreadmessageid === this.id) { + if (prev) { this.channel.lastreadmessageid = prev; - }else{ + } else { this.channel.lastreadmessageid = undefined; } } console.log("deleted done"); } reactdiv!: WeakRef; - blockedPropigate(){ + blockedPropigate() { const previd = this.channel.idToPrev.get(this.id); - if(!previd){ + if (!previd) { this.generateMessage(); return; } const premessage = this.channel.messages.get(previd); - if(premessage?.author === this.author){ + if (premessage?.author === this.author) { premessage.blockedPropigate(); - }else{ + } else { this.generateMessage(); } } - generateMessage(premessage?: Message | undefined, ignoredblock = false){ - if(!this.div)return; - const editmode=this.channel.editing===this; - if(!premessage){ - premessage = this.channel.messages.get( - this.channel.idToPrev.get(this.id) as string - ); + interaction: messagejson["interaction"]; + get ephemeral() { + return !!(this.flags & (1 << 6)); + } + generateMessage( + premessage?: Message | undefined, + ignoredblock = false, + dupe: false | HTMLDivElement = false, + ) { + const div = dupe || this.div; + if (!div) return; + if (div === this.div) { + this.div.classList.add("messagediv"); } - const div = this.div; - for(const user of this.mentions){ - if(user === this.localuser.user){ - div.classList.add("mentioned"); - } + if (this.type === 21) { + this.channel.parent?.getmessage(this.message_reference?.message_id as string).then((_) => { + const gen = _?.generateMessage(undefined, undefined, div); + if (!gen) { + //idk + } + }); + return div; } - if(this === this.channel.replyingto){ + + const editmode = this.channel.editing === this; + if (!premessage && !dupe) { + premessage = this.channel.messages.get(this.channel.idToPrev.get(this.id) as string); + } + if ( + this.mentionsuser(this.guild.member) || + this.ephemeral || + this.interaction?.user.id === this.localuser.user.id + ) { + div.classList.add("mentioned"); + } + div.style.setProperty( + "--time-text", + JSON.stringify( + new Date(this.getUnixTime()).toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}), + ), + ); + + if (this === this.channel.replyingto) { div.classList.add("replying"); } div.innerHTML = ""; const build = document.createElement("div"); build.classList.add("flexltr", "message"); + + if (this.interaction) { + const replyline = document.createElement("div"); + + const minipfp = document.createElement("img"); + minipfp.classList.add("replypfp"); + replyline.appendChild(minipfp); + + const username = document.createElement("span"); + username.classList.add("username"); + replyline.appendChild(username); + + const reply = document.createElement("div"); + reply.classList.add("replytext", "ellipsis"); + reply.textContent = I18n.interactions.replyline(); + replyline.appendChild(reply); + + const user = new User(this.interaction.user, this.localuser); + + minipfp.src = user.getpfpsrc(); + Member.resolveMember(user, this.guild).then((member) => { + if (member) { + minipfp.src = member.getpfpsrc(); + username.textContent = member.name; + member.subName(username); + } else { + user.subName(username); + } + }); + user.bind(minipfp, this.guild); + username.textContent = user.name; + user.bind(username, this.guild); + + replyline.classList.add("flexltr", "replyflex"); + + div.appendChild(replyline); + } + div.classList.remove("zeroheight"); - if(this.author.relationshipType === 2){ - if(ignoredblock){ - if(premessage?.author !== this.author){ + if (this.author.relationshipType === 2) { + if (ignoredblock) { + if (premessage?.author !== this.author) { const span = document.createElement("span"); - span.textContent = I18n.getTranslation("hideBlockedMessages"); + span.textContent = I18n.hideBlockedMessages(); div.append(span); span.classList.add("blocked"); - span.onclick = _=>{ - const scroll = this.channel.infinite.scrollTop; + span.onclick = (_) => { let next: Message | undefined = this; - while(next?.author === this.author){ + while (next?.author === this.author) { next.generateMessage(); - next = this.channel.messages.get( - this.channel.idToNext.get(next.id) as string - ); - } - if(this.channel.infinite.scollDiv && scroll){ - this.channel.infinite.scollDiv.scrollTop = scroll; + next = this.channel.messages.get(this.channel.idToNext.get(next.id) as string); } }; } - }else{ + } else { div.classList.remove("topMessage"); - if(premessage?.author === this.author){ + if (premessage?.author === this.author) { div.classList.add("zeroheight"); premessage.blockedPropigate(); div.appendChild(build); return div; - }else{ + } else { build.classList.add("blocked", "topMessage"); const span = document.createElement("span"); let count = 1; - let next = this.channel.messages.get( - this.channel.idToNext.get(this.id) as string - ); - while(next?.author === this.author){ + let next = this.channel.messages.get(this.channel.idToNext.get(this.id) as string); + while (next?.author === this.author) { count++; - next = this.channel.messages.get( - this.channel.idToNext.get(next.id) as string - ); + next = this.channel.messages.get(this.channel.idToNext.get(next.id) as string); } - span.textContent = I18n.getTranslation("showBlockedMessages",count+""); + span.textContent = I18n.showBlockedMessages(count + ""); build.append(span); - span.onclick = _=>{ - const scroll = this.channel.infinite.scrollTop; + span.onclick = (_) => { const func = this.channel.infinite.snapBottom(); let next: Message | undefined = this; - while(next?.author === this.author){ + while (next?.author === this.author) { next.generateMessage(undefined, true); - next = this.channel.messages.get( - this.channel.idToNext.get(next.id) as string - ); + next = this.channel.messages.get(this.channel.idToNext.get(next.id) as string); console.log("loopy"); } - if(this.channel.infinite.scollDiv && scroll){ - func(); - this.channel.infinite.scollDiv.scrollTop = scroll; - } + func(); }; div.appendChild(build); return div; } } } - if(this.message_reference){ + if (this.message_reference && this.type !== 6 && this.type !== 18) { const replyline = document.createElement("div"); + const minipfp = document.createElement("img"); minipfp.classList.add("replypfp"); replyline.appendChild(minipfp); + const username = document.createElement("span"); replyline.appendChild(username); + const reply = document.createElement("div"); username.classList.add("username"); - reply.classList.add("replytext","ellipsis"); + reply.classList.add("replytext", "ellipsis"); replyline.appendChild(reply); - const line2 = document.createElement("hr"); - replyline.appendChild(line2); - line2.classList.add("reply"); - replyline.classList.add("flexltr","replyflex"); + + replyline.classList.add("flexltr", "replyflex"); // TODO: Fix this - this.channel.getmessage(this.message_reference.message_id).then(message=>{ - if(message.author.relationshipType === 2){ + this.channel.getmessage(this.message_reference.message_id).then((message) => { + if (!message) { + minipfp.remove(); + username.textContent = I18n.message.deleted(); + username.classList.remove("username"); + return; + } + if (message.author.relationshipType === 2) { username.textContent = "Blocked user"; return; } + if (message.attachments?.length || message.embeds?.length || message.stickers.length) { + const b = document.createElement("b"); + b.innerText = I18n.message.attached(); + b.style.paddingRight = "4px"; + reply.append(b); + } const author = message.author; - reply.appendChild(message.content.makeHTML({ stdsize: true })); + reply.appendChild(message.content.makeHTML({stdsize: true})); minipfp.src = author.getpfpsrc(); + author.bind(minipfp, this.guild); - username.textContent = author.username; + username.textContent = author.name; author.bind(username, this.guild); - Member.resolveMember(author, this.guild).then(_=>{ - if(_){ - username.textContent=_.name; + Member.resolveMember(author, this.guild).then((member) => { + if (member) { + username.textContent = member.name; + minipfp.src = member.getpfpsrc(); + member.subName(username); + } else { + author.subName(username); } - }) + }); }); - reply.onclick = _=>{ + reply.onclick = (_) => { + if (!this.message_reference) return; // TODO: FIX this - this.channel.infinite.focus(this.message_reference.message_id); + this.channel.focus(this.message_reference.message_id); }; div.appendChild(replyline); } div.appendChild(build); - if({ 0: true, 19: true }[this.type] || this.attachments.length !== 0){ + const text = document.createElement("div"); + text.classList.add("commentrow", "flexttb"); + + const messageTypes = new Set([0, 19, 20]); + if (messageTypes.has(this.type) || this.attachments.length !== 0) { const pfpRow = document.createElement("div"); - let pfpparent, current; - if(premessage != null){ - pfpparent ??= premessage; - // @ts-ignore - // TODO: type this - let pfpparent2 = pfpparent.all; - pfpparent2 ??= pfpparent; - const old = new Date(pfpparent2.timestamp).getTime() / 1000; + let current = true; + if (premessage !== undefined) { + const old = new Date(premessage.timestamp).getTime() / 1000; const newt = new Date(this.timestamp).getTime() / 1000; current = newt - old > 600; } - const combine = premessage?.author != this.author || current || this.message_reference; - if(combine){ - const pfp = this.author.buildpfp(); + const combine = + premessage?.author != this.author || + current || + this.message_reference || + !messageTypes.has(premessage.type) || + this.interaction; + if (combine) { + const pfp = this.author.buildpfp(this.guild, div); this.author.bind(pfp, this.guild, false); pfpRow.appendChild(pfp); - }else{ - div.pfpparent = pfpparent; } pfpRow.classList.add("pfprow"); build.appendChild(pfpRow); - const text = document.createElement("div"); - text.classList.add("commentrow", "flexttb"); - if(combine){ + + if (combine) { const username = document.createElement("span"); - username.classList.add("username"); + username.classList.add("username", "ellipsis"); this.author.bind(username, this.guild); - Member.resolveMember(this.author, this.guild).then(_=>{ - if(_){ - username.textContent=_.name; + const membProm = Member.resolveMember(this.author, this.guild); + membProm.then((member) => { + if (member) { + username.textContent = member.name; + member.subName(username); + const icon = member.getRoleIcon(); + if (icon) username.after(icon); + } else { + this.author.subName(username); } - }) + }); div.classList.add("topMessage"); - username.textContent = this.author.username; + username.textContent = this.author.name; const userwrap = document.createElement("div"); + userwrap.classList.add("userwrap"); userwrap.appendChild(username); - if(this.author.bot){ + if (this.author.bot) { const username = document.createElement("span"); username.classList.add("bot"); - username.textContent = "BOT"; + username.textContent = this.author.webhook ? I18n.webhook() : I18n.bot(); userwrap.appendChild(username); } const time = document.createElement("span"); time.textContent = " " + formatTime(new Date(this.timestamp)); time.classList.add("timestamp"); userwrap.appendChild(time); - const hover=new Hover(new Date(this.timestamp).toString()); + const hover = new Hover(new Date(this.timestamp).toString()); hover.addEvent(time); - if(this.edited_timestamp){ - const edit=document.createElement("span"); + if (this.edited_timestamp) { + const edit = document.createElement("span"); edit.classList.add("timestamp"); - edit.textContent=I18n.getTranslation("message.edited"); - const hover=new Hover(new Date(this.edited_timestamp).toString()); + edit.textContent = I18n.message.edited(); + const hover = new Hover(new Date(this.edited_timestamp).toString()); hover.addEvent(edit); userwrap.append(edit); } + membProm.then((memb) => { + if (memb) { + if (memb.commuicationDisabledLeft()) { + const icon = document.createElement("span"); + icon.classList.add("svg-timeout"); + username.after(icon); + const date = memb.communication_disabled_until as Date; + new Hover(I18n.channel.timedOutUntil(date.toLocaleString())).addEvent(icon); + } + } + }); text.appendChild(userwrap); - }else{ + } else { div.classList.remove("topMessage"); } const messagedwrap = document.createElement("div"); - if(editmode){ - const box=document.createElement("div"); + if (editmode) { + const box = document.createElement("div"); box.classList.add("messageEditContainer"); - const area=document.createElement("div"); - const sb=document.createElement("div"); - sb.style.position="absolute"; - sb.style.width="100%"; - const search=document.createElement("div"); - search.classList.add("searchOptions","flexttb"); + const area = document.createElement("div"); + const sb = document.createElement("div"); + sb.style.position = "absolute"; + sb.style.width = "100%"; + const search = document.createElement("div"); + search.classList.add("searchOptions", "flexttb"); area.classList.add("editMessage"); - area.contentEditable="true"; - const md=new MarkDown(this.content.rawString,this.owner,{keep:true}); + try { + area.contentEditable = "plaintext-only"; + } catch { + area.contentEditable = "true"; + } + const md = new MarkDown(this.content.rawString, this.owner, {keep: true}); area.append(md.makeHTML()); - area.addEventListener("keyup", (event)=>{ - if(this.localuser.keyup(event)) return; - if(event.key === "Enter" && !event.shiftKey){ + area.addEventListener("keyup", (event) => { + if (this.localuser.keyup(event)) return; + if (event.key === "Enter" && !event.shiftKey) { this.edit(md.rawString); - this.channel.editing=null; + this.channel.editing = null; this.generateMessage(); } }); - area.addEventListener("keydown", event=>{ + area.addEventListener("keydown", (event) => { this.localuser.keydown(event); - if(event.key === "Enter" && !event.shiftKey) event.preventDefault(); - if(event.key === "Escape"){ - this.channel.editing=null; + if (event.key === "Enter" && !event.shiftKey) event.preventDefault(); + if (event.key === "Escape") { + this.channel.editing = null; this.generateMessage(); } }); - md.giveBox(area,(str,pre)=>{ - this.localuser.search(search,md,str,pre) - }) + md.giveBox(area, (str, pre) => { + this.localuser.search(search, md, str, pre); + }); sb.append(search); - box.append(sb,area); + box.append(sb, area); messagedwrap.append(box); - setTimeout(()=>{ + setTimeout(() => { area.focus(); - const fun=saveCaretPosition(area,Infinity); - if(fun) fun(); - }) - }else{ - this.content.onUpdate=()=>{}; + const fun = saveCaretPosition(area, Infinity); + if (fun) fun(); + }); + box.oncontextmenu = (e) => { + e.stopImmediatePropagation(); + }; + } else { + this.content.onUpdate = () => {}; const messaged = this.content.makeHTML(); - (div as any).txt = messaged; - messagedwrap.classList.add("flexttb"); - messagedwrap.appendChild(messaged); - + if (!this.embeds.find((_) => _.json.url === messaged.textContent)) { + messagedwrap.classList.add("flexttb"); + messagedwrap.appendChild(messaged); + if (!combine && this.edited_timestamp) { + const edit = document.createElement("span"); + edit.classList.add("timestamp"); + edit.textContent = I18n.message.edited(); + const hover = new Hover(new Date(this.edited_timestamp).toString()); + hover.addEvent(edit); + messaged.append(edit); + } + } } text.appendChild(messagedwrap); - build.appendChild(text); - if(this.attachments.length){ - console.log(this.attachments); + + if (this.attachments.length) { const attach = document.createElement("div"); - attach.classList.add("flexltr","attachments"); - for(const thing of this.attachments){ + attach.classList.add("flexltr", "attachments"); + for (const thing of this.attachments) { attach.appendChild(thing.getHTML()); } messagedwrap.appendChild(attach); } - if(this.embeds.length){ + if (this.embeds.length) { const embeds = document.createElement("div"); - for(const thing of this.embeds){ + for (const thing of this.embeds) { embeds.appendChild(thing.generateHTML()); } messagedwrap.appendChild(embeds); } // - }else if(this.type === 7){ + } else if (this.type === 7) { + const messages = I18n.welcomeMessages("|||").split("\n"); + const message = messages[Number(BigInt(this.id) % BigInt(messages.length))]; + const [first, second] = message.split("|||"); + const welcome = document.createElement("div"); + text.appendChild(welcome); + + const firstspan = document.createElement("span"); + firstspan.textContent = first; + welcome.appendChild(firstspan); + + // TODO: settings how? + if (false) { + const img = document.createElement("img"); + img.classList.add("avatar"); + img.style.height = "1em"; + img.style.width = "1em"; + img.style.objectFit = "cover"; + img.src = this.author.getpfpsrc(this.guild) + "?size=1"; + img.loading = "lazy"; + img.decoding = "async"; + img.addEventListener( + "load", + () => { + img.src = this.author.getpfpsrc(this.guild) + "?size=" + firstspan.clientHeight; + }, + {once: true}, + ); + img.onclick = () => { + const full = new ImagesDisplay([ + new File( + { + content_type: "image/webp", + filename: "0", + id: "0", + size: 0, + url: this.author.getpfpsrc(this.guild), + }, + this, + ), + ]); + full.show(); + }; + + welcome.appendChild(img); + } + + const username = document.createElement("span"); + username.textContent = this.author.name; + //this.author.profileclick(username); + this.author.bind(username, this.guild); + welcome.appendChild(username); + username.classList.add("username"); + + const secondspan = document.createElement("span"); + secondspan.textContent = second; + welcome.appendChild(secondspan); + + const time = document.createElement("span"); + time.textContent = " " + formatTime(new Date(this.timestamp)); + time.classList.add("timestamp"); + welcome.append(time); + div.classList.add("topMessage"); + } else if (this.type === 6) { + const pinText = document.createElement("div"); + text.appendChild(pinText); + + const m = I18n.message.pin("||").split("||"); + if (m.length === 2) pinText.append(m.shift() as string); + + const username = document.createElement("span"); + username.textContent = this.author.name; + //this.author.profileclick(username); + this.author.bind(username, this.guild); + pinText.appendChild(username); + username.classList.add("username"); + + const afterText = document.createElement("span"); + afterText.textContent = m[0]; + afterText.onclick = (_) => { + if (!this.message_reference) return; + this.channel.infinite.focus(this.message_reference.message_id); + }; + afterText.classList.add("pinText"); + pinText.append(afterText); + + const time = document.createElement("span"); + time.textContent = " " + formatTime(new Date(this.timestamp)); + time.classList.add("timestamp"); + pinText.append(time); + div.classList.add("topMessage"); + } else if (this.type === 18) { const text = document.createElement("div"); build.appendChild(text); - const messaged = document.createElement("span"); - div.txt = messaged; - messaged.textContent = "welcome: "; - text.appendChild(messaged); + + const m = I18n.message.thread("|||", "???").split("|||"); + if (m.length === 2) text.append(m.shift() as string); const username = document.createElement("span"); - username.textContent = this.author.username; + username.textContent = this.author.name; //this.author.profileclick(username); this.author.bind(username, this.guild); text.appendChild(username); username.classList.add("username"); + const midText = m[0].split("???"); + + const afterText = document.createElement("span"); + afterText.textContent = midText[0]; + text.append(afterText); + + const threadName = document.createElement("span"); + threadName.textContent = this.content.rawString; + threadName.classList.add("pinText"); + text.append(threadName); + threadName.onclick = () => { + this.guild.goToThread(this.message_reference?.channel_id as string); + }; const time = document.createElement("span"); time.textContent = " " + formatTime(new Date(this.timestamp)); @@ -611,122 +1132,273 @@ class Message extends SnowFlake{ text.append(time); div.classList.add("topMessage"); } - const reactions = document.createElement("div"); - reactions.classList.add("flexltr", "reactiondiv"); - this.reactdiv = new WeakRef(reactions); - this.updateReactions(); - div.append(reactions); + build.appendChild(text); + const stickerArea = document.createElement("div"); + stickerArea.classList.add("flexltr", "stickerMArea"); + for (const sticker of this.stickers) { + stickerArea.append(sticker.getHTML()); + } + div.append(stickerArea); + if (!dupe) { + if (this.components && this.components.components.length) { + const cdiv = this.components.getHTML(); + cdiv.classList.add("messageComps"); + text.append(cdiv); + + const ndiv = document.createElement("div"); + ndiv.classList.add("compAppStatus"); + this.interactionDiv = ndiv; + text.append(ndiv); + } + const reactions = document.createElement("div"); + reactions.classList.add("flexltr", "reactiondiv"); + this.reactdiv = new WeakRef(reactions); + this.updateReactions(); + div.append(reactions); + } + if (this.ephemeral) { + const ephemeral = document.createElement("div"); + ephemeral.classList.add("flexltr", "ephemeralDiv"); + const span = document.createElement("span"); + span.textContent = I18n.interactions.onlyYou(); + + const a = document.createElement("a"); + a.onclick = () => { + this.deleteEvent(); + }; + a.textContent = I18n.interactions.ephemeralDismiss(); + ephemeral.append(span, a); + div.append(ephemeral); + } + const unreadLine = premessage && premessage.id === this.channel.lastreadmessageid; + let datelineNeeded = false; + if ((premessage || unreadLine) && !dupe) { + const thisTime = new Date(this.getUnixTime()); + if (premessage && !unreadLine) { + const prevTime = new Date(premessage.getUnixTime()); + datelineNeeded = + thisTime.getDay() !== prevTime.getDay() || + thisTime.getMonth() !== prevTime.getMonth() || + thisTime.getFullYear() !== prevTime.getFullYear(); + } else { + datelineNeeded = true; + } + if (datelineNeeded) { + const dateline = document.createElement("div"); + if (unreadLine) { + dateline.classList.add("unreadDateline"); + } + dateline.classList.add("flexltr", "dateline"); + dateline.append(document.createElement("hr")); + const span = document.createElement("span"); + span.innerText = Intl.DateTimeFormat(I18n.lang, { + year: "numeric", + month: "long", + day: "2-digit", + }).format(thisTime); + dateline.append(span); + dateline.append(document.createElement("hr")); + const messageDiv = document.createElement("div"); + messageDiv.append(...Array.from(div.children)); + messageDiv.classList = div.classList + ""; + div.classList = ""; + div.append(dateline, messageDiv); + } + } this.bindButtonEvent(); + + if (!dupe && this.thread) { + const thread = this.thread; + const threadBox = document.createElement("div"); + threadBox.classList.add("flexttb", "threadBox"); + const topRow = document.createElement("div"); + topRow.classList.add("flexltr"); + const title = document.createElement("span"); + title.textContent = thread.name; + topRow.onclick = () => { + this.localuser.goToChannel(thread.id); + }; + const messages = document.createElement("span"); + messages.classList.add("clickable"); + messages.textContent = I18n.message.messages(thread.messageCount + ""); + console.log(thread); + topRow.append(title, messages); + threadBox.append(topRow); + div.append(threadBox); + } + return div; } - bindButtonEvent(){ - if(this.div){ - let buttons: HTMLDivElement | undefined; - this.div.onmouseenter = _=>{ - if(mobile)return; - if(buttons){ - buttons.remove(); - buttons = undefined; - } - if(this.div){ - buttons = document.createElement("div"); - buttons.classList.add("messageButtons", "flexltr"); - if(this.channel.hasPermission("SEND_MESSAGES")){ - const container = document.createElement("button"); - const reply = document.createElement("span"); - reply.classList.add("svg-reply", "svgicon"); - container.append(reply); - buttons.append(container); - container.onclick = _=>{ - this.channel.setReplying(this); - }; - } - if(this.channel.hasPermission("ADD_REACTIONS")){ - const container = document.createElement("button"); - const reply = document.createElement("span"); - reply.classList.add("svg-emoji", "svgicon"); - container.append(reply); - buttons.append(container); - container.onclick = e=>{ - Emoji.emojiPicker(e.x, e.y, this.localuser).then(_=>{ - this.reactionToggle(_); - }); - }; - } - if(this.author === this.localuser.user){ + bindButtonEvent() { + if (!this.div) return; + const div = this.div.classList.contains("messagediv") + ? this.div + : (this.div.getElementsByClassName("messagediv")[0] as HTMLDivElement); + if (!div) return; + let buttons: HTMLDivElement | undefined; + div.onmouseenter = (_) => { + if (mobile) return; + if (buttons) { + buttons.remove(); + buttons = undefined; + } + if (div) { + buttons = document.createElement("div"); + buttons.classList.add("messageButtons", "flexltr"); + let addedRec = false; + if (this.channel.hasPermission("ADD_REACTIONS")) { + const favs = this.localuser.favorites + .emojiReactFreq() + .slice(0, 6) + .filter(([emoji]) => { + return !this.reactions.find( + (_) => _.emoji.id === emoji || _.emoji.emoji === emoji || _.emoji.name === emoji, + )?.me; + }) + .slice(0, 3); + for (const [emoji] of favs) { + addedRec = true; const container = document.createElement("button"); - const edit = document.createElement("span"); - edit.classList.add("svg-edit", "svgicon"); - container.append(edit); - buttons.append(container); - container.onclick = _=>{ - this.setEdit(); + if (isNaN(+emoji)) { + container.append(emoji); + } else { + const emj = Emoji.getEmojiFromIDOrString(emoji, this.localuser); + container.append(emj.getHTML(false, false)); + } + container.onclick = () => { + this.reactionToggle(emoji); }; - } - if(this.canDelete()){ - const container = document.createElement("button"); - const reply = document.createElement("span"); - reply.classList.add("svg-delete", "svgicon"); - container.append(reply); buttons.append(container); - container.onclick = _=>{ - if(_.shiftKey){ - this.delete(); - return; - } - this.confirmDelete(); - }; - } - if(buttons.childNodes.length !== 0){ - this.div.append(buttons); } } - }; - this.div.onmouseleave = _=>{ - if(buttons){ - buttons.remove(); - buttons = undefined; + if (addedRec) { + Array.from(buttons.children).at(-1)?.classList.add("vr-message"); } - }; - } + if (this.channel.hasPermission("SEND_MESSAGES") && !this.ephemeral) { + const container = document.createElement("button"); + const reply = document.createElement("span"); + reply.classList.add("svg-reply", "svgicon"); + container.append(reply); + buttons.append(container); + container.onclick = (_) => { + this.channel.setReplying(this); + }; + } + if (this.channel.hasPermission("ADD_REACTIONS")) { + const container = document.createElement("button"); + const reply = document.createElement("span"); + reply.classList.add("svg-emoji", "svgicon"); + container.append(reply); + buttons.append(container); + container.onclick = (e) => { + e.stopImmediatePropagation(); + e.preventDefault(); + Emoji.emojiPicker(e.x, e.y, this.localuser).then((_) => { + this.reactionToggle(_); + }); + }; + } + if (this.author === this.localuser.user) { + const container = document.createElement("button"); + const edit = document.createElement("span"); + edit.classList.add("svg-edit", "svgicon"); + container.append(edit); + buttons.append(container); + container.onclick = (_) => { + this.setEdit(); + }; + } + if (this.canDelete()) { + const container = document.createElement("button"); + const reply = document.createElement("span"); + reply.classList.add("svg-delete", "svgicon"); + container.append(reply); + buttons.append(container); + container.onclick = (_) => { + if (_.shiftKey) { + this.delete(); + return; + } + this.confirmDelete(); + }; + } + if (buttons.childNodes.length !== 0) { + div.append(buttons); + } + } + }; + div.onmouseleave = (_) => { + if (buttons) { + buttons.remove(); + buttons = undefined; + } + }; } - confirmDelete(){ - const diaolog=new Dialog(""); - diaolog.options.addTitle(I18n.getTranslation("deleteConfirm")); - const options=diaolog.options.addOptions("",{ltr:true}); - options.addButtonInput("",I18n.getTranslation("yes"),()=>{ + confirmDelete() { + const diaolog = new Dialog(""); + diaolog.options.addTitle(I18n.deleteConfirm()); + const options = diaolog.options.addOptions("", {ltr: true}); + options.addButtonInput("", I18n.yes(), () => { this.delete(); diaolog.hide(); }); - options.addButtonInput("",I18n.getTranslation("no"),()=>{ + options.addButtonInput("", I18n.no(), () => { diaolog.hide(); - }) + }); diaolog.show(); } - updateReactions(){ + updateReactions() { const reactdiv = this.reactdiv.deref(); - if(!reactdiv)return; + if (!reactdiv) return; const func = this.channel.infinite.snapBottom(); reactdiv.innerHTML = ""; - for(const thing of this.reactions){ + for (const thing of this.reactions) { const reaction = document.createElement("div"); reaction.classList.add("reaction"); - if(thing.me){ + if (thing.me) { reaction.classList.add("meReacted"); } let emoji: HTMLElement; - if(thing.emoji.id || /\d{17,21}/.test(thing.emoji.name)){ - if(/\d{17,21}/.test(thing.emoji.name)) + if (thing.emoji.id || /\d{17,21}/.test(thing.emoji.name)) { + if (/\d{17,21}/.test(thing.emoji.name)) { thing.emoji.id = thing.emoji.name; //Should stop being a thing once the server fixes this bug + } const emo = new Emoji( - thing.emoji as { name: string; id: string; animated: boolean }, - this.guild + thing.emoji as {name: string; id: string; animated: boolean}, + this.guild, ); - emoji = emo.getHTML(false); - }else{ + emoji = emo.getHTML(false, false); + } else { emoji = document.createElement("p"); emoji.textContent = thing.emoji.name; } + const h = new Hover(async () => { + //TODO this can't be real, name conflicts must happen, but for now it's fine + const f = await fetch( + `${this.info.api}/channels/${this.channel.id}/messages/${this.id}/reactions/${thing.emoji.name}?limit=3&type=0`, + {headers: this.headers}, + ); + const json = (await f.json()) as userjson[]; + let build = ""; + let users = json.map((_) => new User(_, this.localuser)); + //FIXME this is a spacebar bug, I can't fix this the api ignores limit and just sends everything. + users = users.splice(0, 3); + let first = true; + for (const user of users) { + if (!first) { + build += ", "; + } + build += user.name; + first = false; + } + if (thing.count > 3) { + build = I18n.message.andMore(build); + } else { + } + build += "\n" + I18n.message.reactedWith(thing.emoji.name); + return build; + }); + h.addEvent(reaction); const count = document.createElement("p"); count.textContent = "" + thing.count; count.classList.add("reactionCount"); @@ -734,23 +1406,24 @@ class Message extends SnowFlake{ reaction.append(emoji); reactdiv.append(reaction); - reaction.onclick = _=>{ + reaction.onclick = (_) => { this.reactionToggle(thing.emoji.name); }; } func(); } - reactionAdd(data: { name: string }, member: Member | { id: string }){ - for(const thing of this.reactions){ - if(thing.emoji.name === data.name){ + reactionAdd(data: {name: string}, member: Member | {id: string}) { + for (const thing of this.reactions) { + if (thing.emoji.name === data.name) { thing.count++; - if(member.id === this.localuser.user.id){ + if (member.id === this.localuser.user.id) { thing.me = true; - this.updateReactions(); - return; } + this.updateReactions(); + return; } } + console.log(data, this.reactions); this.reactions.push({ count: 1, emoji: data, @@ -758,19 +1431,19 @@ class Message extends SnowFlake{ }); this.updateReactions(); } - reactionRemove(data: { name: string }, id: string){ + reactionRemove(data: {name: string}, id: string) { console.log("test"); - for(const i in this.reactions){ + for (const i in this.reactions) { const thing = this.reactions[i]; console.log(thing, data); - if(thing.emoji.name === data.name){ + if (thing.emoji.name === data.name) { thing.count--; - if(thing.count === 0){ + if (thing.count === 0) { this.reactions.splice(Number(i), 1); this.updateReactions(); return; } - if(id === this.localuser.user.id){ + if (id === this.localuser.user.id) { thing.me = false; this.updateReactions(); return; @@ -778,34 +1451,41 @@ class Message extends SnowFlake{ } } } - reactionRemoveAll(){ + reactionRemoveAll() { this.reactions = []; this.updateReactions(); } - reactionRemoveEmoji(emoji: Emoji){ - for(const i in this.reactions){ + reactionRemoveEmoji(emoji: Emoji) { + for (const i in this.reactions) { const reaction = this.reactions[i]; - if( + if ( (reaction.emoji.id && reaction.emoji.id == emoji.id) || (!reaction.emoji.id && reaction.emoji.name == emoji.name) - ){ + ) { this.reactions.splice(Number(i), 1); this.updateReactions(); break; } } } - buildhtml(premessage?: Message | undefined): HTMLElement{ - if(this.div){ + buildhtml(premessage?: Message | undefined, dupe = false): HTMLElement { + const id = this.channel.nonceMap.get(this.nonce); + if (id && !dupe) { + this.channel.destroyFakeMessage(id); + } + if (dupe) { + return this.generateMessage(premessage, false, document.createElement("div")) as HTMLElement; + } + if (this.div) { console.error(`HTML for ${this.id} already exists, aborting`); return this.div; } - try{ + try { const div = document.createElement("div"); this.div = div; this.messageevents(div); return this.generateMessage(premessage) as HTMLElement; - }catch(e){ + } catch (e) { console.error(e); } return this.div as HTMLElement; @@ -814,23 +1494,24 @@ class Message extends SnowFlake{ let now: string; let yesterdayStr: string; -function formatTime(date: Date){ +function formatTime(date: Date) { updateTimes(); const datestring = date.toLocaleDateString(); - const formatTime = (date: Date)=>date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); + const formatTime = (date: Date) => + date.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); - if(datestring === now){ - return I18n.getTranslation("todayAt",formatTime(date)); - }else if(datestring === yesterdayStr){ - return I18n.getTranslation("yesterdayAt",formatTime(date)); - }else{ - return I18n.getTranslation("otherAt",formatTime(date),date.toLocaleDateString(),formatTime(date)); + if (datestring === now) { + return I18n.todayAt(formatTime(date)); + } else if (datestring === yesterdayStr) { + return I18n.yesterdayAt(formatTime(date)); + } else { + return I18n.otherAt(date.toLocaleDateString(), formatTime(date)); } } let tomorrow = 0; updateTimes(); -function updateTimes(){ - if(tomorrow < Date.now()){ +function updateTimes() { + if (tomorrow < Date.now()) { const d = new Date(); tomorrow = d.setHours(24, 0, 0, 0); now = new Date().toLocaleDateString(); @@ -840,4 +1521,4 @@ function updateTimes(){ } } Message.setup(); -export{ Message }; +export {Message}; diff --git a/src/webpage/more.ts b/src/webpage/more.ts new file mode 100644 index 00000000..d5990fc1 --- /dev/null +++ b/src/webpage/more.ts @@ -0,0 +1 @@ +import "./utils/utils.js"; diff --git a/src/webpage/notificationHandler.ts b/src/webpage/notificationHandler.ts new file mode 100644 index 00000000..b7496ee3 --- /dev/null +++ b/src/webpage/notificationHandler.ts @@ -0,0 +1,82 @@ +import {Channel} from "./channel.js"; +import {I18n} from "./i18n.js"; +import {Message} from "./message.js"; + +declare global { + interface NotificationOptions { + image?: string | null | undefined; + } +} +export class NotificationHandler { + static makeIcon(message: Message) { + return message.author.getpfpsrc(message.guild); + } + static channelMap = new Map>(); + static async sendMessageNotification(message: Message) { + const html = message.content.makeHTML(); + await new Promise((res) => res()); + let noticontent: string | undefined | null = html.textContent; + if (message.embeds[0]) { + noticontent ||= message.embeds[0]?.json.title; + noticontent ||= message.content.textContent; + } + noticontent ||= I18n.blankMessage(); + + const image = message.getimages()[0]; + const imgurl = image?.proxy_url || image?.url || undefined; + if (this.groupNotifs(message)) return; + const notification = new Notification(message.channel.notititle(message), { + body: noticontent, + icon: this.makeIcon(message), + image: imgurl, + silent: true, + }); + + notification.addEventListener("click", (_) => { + window.focus(); + message.channel.getHTML(true, true); + }); + + const channelSet = this.channelMap.get(message.channel) || new Set(); + this.channelMap.set(message.channel, channelSet); + channelSet.add(notification); + + notification.addEventListener("show", () => { + setTimeout(() => { + channelSet.delete(notification); + }, 4000); + }); + } + static channelSuperMap = new Map(); + static groupNotifs(message: Message): boolean { + let sup = this.channelSuperMap.get(message.channel); + + const notiSet = this.channelMap.get(message.channel); + if (!notiSet) return false; + if (!sup) { + if (notiSet.size < 4) return false; + sup = [notiSet.size - 1, 0 as any, Math.random()]; + this.channelSuperMap.set(message.channel, sup); + } + + [...notiSet].forEach((_) => _.close()); + + let [count, cancel, rand] = sup; + sup[0]++; + clearInterval(cancel); + new Notification(message.channel.notititle(message), { + body: I18n.notiClump(count + "", message.channel.name), + icon: this.makeIcon(message), + silent: true, + tag: message.channel.id + rand, + }).addEventListener("click", (_) => { + window.focus(); + message.channel.getHTML(true, true); + this.channelSuperMap.delete(message.channel); + }); + setTimeout(() => { + this.channelSuperMap.delete(message.channel); + }, 3000); + return true; + } +} diff --git a/src/webpage/oauth2/auth.ts b/src/webpage/oauth2/auth.ts index 2345f157..faf0032d 100644 --- a/src/webpage/oauth2/auth.ts +++ b/src/webpage/oauth2/auth.ts @@ -1,156 +1,157 @@ -import { I18n } from "../i18n.js"; -import{ getBulkUsers, Specialuser, getapiurls }from"../login.js"; -import { Permissions } from "../permissions.js"; -type botjsonfetch={ - guilds:{ - id: string, - name: string, - icon: string, - mfa_level: number, - permissions: string - }[], - "user": { - id: string, - username: string, - avatar: string, - avatar_decoration?: string, - discriminator: string, - public_flags: number - }, - application: { - id: string, - name: string, - icon: string|null, - description: string, - summary: string, - type: null,//not sure what this means :P - hook: boolean, - guild_id: null|string, - bot_public: boolean, - bot_require_code_grant: boolean, - verify_key: "IMPLEMENTME",//no clue what this is meant to be :P - flags: number - }, - bot: { - id: string, - username: string, - avatar: string|null, - avatar_decoration: null|string, - discriminator: string, - public_flags: number, - bot: boolean, - approximated_guild_count: number - }, - authorized: boolean -} -(async ()=>{ +import {I18n} from "../i18n.js"; +import {getapiurls} from "../utils/utils.js"; +import {getBulkUsers, Specialuser} from "../utils/utils.js"; +import {Permissions} from "../permissions.js"; +type botjsonfetch = { + guilds: { + id: string; + name: string; + icon: string; + mfa_level: number; + permissions: string; + }[]; + user: { + id: string; + username: string; + avatar: string; + avatar_decoration?: string; + discriminator: string; + public_flags: number; + }; + application: { + id: string; + name: string; + icon: string | null; + description: string; + summary: string; + type: null; //not sure what this means :P + hook: boolean; + guild_id: null | string; + bot_public: boolean; + bot_require_code_grant: boolean; + verify_key: "IMPLEMENTME"; //no clue what this is meant to be :P + flags: number; + }; + bot: { + id: string; + username: string; + avatar: string | null; + avatar_decoration: null | string; + discriminator: string; + public_flags: number; + bot: boolean; + approximated_guild_count: number; + }; + authorized: boolean; +}; +if (window.location.pathname.startsWith("/oauth2")) { const users = getBulkUsers(); - const params=new URLSearchParams(window.location.search); - const well = params.get("instance"); - const permstr=params.get("permissions"); + const params = new URLSearchParams(window.location.search); + const well = params.get("instance") || "https://spacebar.chat"; + const permstr = params.get("permissions"); const joinable: Specialuser[] = []; - for(const key in users.users){ - if(Object.prototype.hasOwnProperty.call(users.users, key)){ + for (const key in users.users) { + if (Object.prototype.hasOwnProperty.call(users.users, key)) { const user: Specialuser = users.users[key]; - if(well && user.serverurls.wellknown.includes(well)){ + if (well && user.serverurls.wellknown.includes(well)) { joinable.push(user); } console.log(user); } } - let urls: { api: string; cdn: string } | undefined; + let urls: {api: string; cdn: string} | undefined; - if(!joinable.length && well){ + if (!joinable.length && well) { const out = await getapiurls(well); - if(out){ + if (out) { urls = out; - for(const key in users.users){ - if(Object.prototype.hasOwnProperty.call(users.users, key)){ + for (const key in users.users) { + if (Object.prototype.hasOwnProperty.call(users.users, key)) { const user: Specialuser = users.users[key]; - if(user.serverurls.api.includes(out.api)){ + if (user.serverurls.api.includes(out.api)) { joinable.push(user); } console.log(user); } } - }else{ - throw new Error( - "Someone needs to handle the case where the servers don't exist" - ); + } else { + throw new Error("Someone needs to handle the case where the servers don't exist"); } - }else{ + } else { urls = joinable[0].serverurls; } - if(!joinable.length){ - document.getElementById("AcceptInvite")!.textContent = "Create an account to invite the bot"; + if (!joinable.length) { + document.getElementById("AcceptInvite")!.textContent = "Create an account to invite the bot"; } await I18n.done; - function showGuilds(user:Specialuser){ - if(!urls) return; - fetch(urls.api+"/oauth2/authorize/"+window.location.search,{ - headers:{ - Authorization:user.token - } - }).then(_=>_.json()).then((json:botjsonfetch)=>{ - const guilds:botjsonfetch["guilds"]=[]; - for(const guild of json.guilds){ - const permisions=new Permissions(guild.permissions) - if(permisions.hasPermission("MANAGE_GUILD")){ - guilds.push(guild); - } - } - const dialog=document.createElement("dialog"); - dialog.classList.add("flexttb","accountSwitcher"); - const h1=document.createElement("h1"); - dialog.append(h1); - h1.textContent="Invite to server:"; - const select=document.createElement("select"); - const selectSpan=document.createElement("span"); - selectSpan.classList.add("selectspan"); - const selectArrow = document.createElement("span"); - selectArrow.classList.add("svgicon","svg-category","selectarrow"); - for(const guild of guilds){ - const option=document.createElement("option"); - option.textContent=guild.name; - option.value=guild.id; - select.append(option); - } - selectSpan.append(select); - selectSpan.append(selectArrow); - dialog.append(selectSpan); - const button=document.createElement("button"); - button.textContent="Invite"; - dialog.append(button); - button.onclick=()=>{ - const id=select.value; - const params2=new URLSearchParams(""); - params2.set("client_id",params.get("client_id") as string) - fetch(urls.api+"/oauth2/authorize?"+params2.toString(),{ - method:"POST", - body:JSON.stringify({ - authorize:true, - guild_id:id, - permissions:permstr - }), - headers:{ - "Content-type": "application/json; charset=UTF-8", - Authorization:user.token, - } - }).then(req=>{ - if(req.ok){ - alert("Bot added successfully"); - } - }) - } - document.body.append(dialog); + function showGuilds(user: Specialuser) { + if (!urls) return; + fetch(urls.api + "/oauth2/authorize/" + window.location.search, { + headers: { + Authorization: user.token, + }, }) + .then((_) => _.json()) + .then((json: botjsonfetch) => { + const guilds: botjsonfetch["guilds"] = []; + for (const guild of json.guilds) { + const permisions = new Permissions(guild.permissions); + if (permisions.hasPermission("MANAGE_GUILD")) { + guilds.push(guild); + } + } + const dialog = document.createElement("dialog"); + dialog.classList.add("flexttb", "accountSwitcher"); + const h1 = document.createElement("h1"); + dialog.append(h1); + h1.textContent = "Invite to server:"; + const select = document.createElement("select"); + const selectSpan = document.createElement("span"); + selectSpan.classList.add("selectspan"); + const selectArrow = document.createElement("span"); + selectArrow.classList.add("svgicon", "svg-category", "selectarrow"); + for (const guild of guilds) { + const option = document.createElement("option"); + option.textContent = guild.name; + option.value = guild.id; + select.append(option); + } + selectSpan.append(select); + selectSpan.append(selectArrow); + dialog.append(selectSpan); + const button = document.createElement("button"); + button.textContent = "Invite"; + dialog.append(button); + button.onclick = () => { + const id = select.value; + const params2 = new URLSearchParams(""); + params2.set("client_id", params.get("client_id") as string); + fetch(urls.api + "/oauth2/authorize?" + params2.toString(), { + method: "POST", + body: JSON.stringify({ + authorize: true, + guild_id: id, + permissions: permstr, + }), + headers: { + "Content-type": "application/json; charset=UTF-8", + Authorization: user.token, + }, + }).then((req) => { + if (req.ok) { + alert("Bot added successfully"); + } + }); + }; + document.body.append(dialog); + }); } - function showAccounts(): void{ + function showAccounts(): void { const table = document.createElement("dialog"); - for(const user of joinable){ + for (const user of joinable) { console.log(user.pfpsrc); const userinfo = document.createElement("div"); @@ -167,16 +168,14 @@ type botjsonfetch={ userDiv.append(document.createElement("br")); const span = document.createElement("span"); - span.textContent = user.serverurls.wellknown - .replace("https://", "") - .replace("http://", ""); + span.textContent = user.serverurls.wellknown.replace("https://", "").replace("http://", ""); span.classList.add("serverURL"); userDiv.append(span); userinfo.append(userDiv); table.append(userinfo); - userinfo.addEventListener("click", ()=>{ + userinfo.addEventListener("click", () => { table.remove(); showGuilds(user); }); @@ -185,14 +184,14 @@ type botjsonfetch={ const td = document.createElement("div"); td.classList.add("switchtable"); td.textContent = "Login or create an account ⇌"; - td.addEventListener("click", ()=>{ + td.addEventListener("click", () => { const l = new URLSearchParams("?"); l.set("goback", window.location.href); l.set("instance", well!); window.location.href = "/login?" + l.toString(); }); - if(!joinable.length){ + if (!joinable.length) { const l = new URLSearchParams("?"); l.set("goback", window.location.href); l.set("instance", well!); @@ -200,54 +199,55 @@ type botjsonfetch={ } table.append(td); - table.classList.add("flexttb","accountSwitcher"); + table.classList.add("flexttb", "accountSwitcher"); console.log(table); document.body.append(table); } - const user=joinable[0]; - if(!user){ - return; - } - fetch(urls.api+"/oauth2/authorize/"+window.location.search,{ - headers:{ - Authorization:user.token - } - }).then(_=>_.json()).then((json:botjsonfetch)=>{ - const title=document.getElementById("invitename"); - if(title){ - title.textContent=`Invite ${json.bot.username} to your servers` - } - const desc=document.getElementById("invitedescription"); - if(desc){ - desc.textContent=json.application.description; - } - const pfp=document.getElementById("inviteimg") as HTMLImageElement; - if(json.bot.avatar !== null){ - pfp.src=`${urls.cdn}/avatars/${json.bot.id}/${json.bot.avatar}.png`; - }else{ - const int = Number((BigInt(json.bot.id) >> 22n) % 6n); - pfp.src=`${urls.cdn}/embed/avatars/${int}.png`; - } - const perms=document.getElementById("permissions") as HTMLDivElement; + const user = joinable[0]; + if (user) { + fetch(urls.api + "/oauth2/authorize/" + window.location.search, { + headers: { + Authorization: user.token, + }, + }) + .then((_) => _.json()) + .then((json: botjsonfetch) => { + const title = document.getElementById("invitename"); + if (title) { + title.textContent = `Invite ${json.bot.username} to your servers`; + } + const desc = document.getElementById("invitedescription"); + if (desc) { + desc.textContent = json.application.description; + } + const pfp = document.getElementById("inviteimg") as HTMLImageElement; + if (json.bot.avatar !== null) { + pfp.src = `${urls.cdn}/avatars/${json.bot.id}/${json.bot.avatar}.png`; + } else { + const int = Number((BigInt(json.bot.id) >> 22n) % 6n); + pfp.src = `${urls.cdn}/embed/avatars/${int}.png`; + } + const perms = document.getElementById("permissions") as HTMLDivElement; - if(perms&&permstr){ - perms.children[0].textContent=I18n.getTranslation("htmlPages.idpermissions") - const permisions=new Permissions(permstr) - for(const perm of Permissions.info()){ - if(permisions.hasPermission(perm.name,false)){ - const div=document.createElement("div"); - const h2=document.createElement("h2"); - h2.textContent=perm.readableName; - div.append(h2,perm.description); - div.classList.add("flexttb"); - perms.append(div); + if (perms && permstr) { + perms.children[0].textContent = I18n.htmlPages.idpermissions(); + const permisions = new Permissions(permstr); + for (const perm of Permissions.info()) { + if (permisions.hasPermission(perm.name, false)) { + const div = document.createElement("div"); + const h2 = document.createElement("h2"); + h2.textContent = perm.readableName; + div.append(h2, perm.description); + div.classList.add("flexttb"); + perms.append(div); + } + } } - } - } - }) - const AcceptInvite=document.getElementById("AcceptInvite"); - if(AcceptInvite){ + }); + } + const AcceptInvite = document.getElementById("AcceptInvite"); + if (AcceptInvite) { AcceptInvite.addEventListener("click", showAccounts); - AcceptInvite.textContent=I18n.getTranslation("htmlPages.addBot") + AcceptInvite.textContent = I18n.htmlPages.addBot(); } -})(); +} diff --git a/src/webpage/oauth2/authorize.html b/src/webpage/oauth2/authorize.html index b2d61d77..c63d6c79 100644 --- a/src/webpage/oauth2/authorize.html +++ b/src/webpage/oauth2/authorize.html @@ -1,27 +1,36 @@ - + - - - Jank Client - - - - - - - + + + Fermi + + + + + + +
- +

Bot Name

Add Bot

This will allow the bot to:

- + - \ No newline at end of file + diff --git a/src/webpage/permissions.ts b/src/webpage/permissions.ts index 6f3f6643..e5ece1e8 100644 --- a/src/webpage/permissions.ts +++ b/src/webpage/permissions.ts @@ -1,40 +1,40 @@ -import { I18n } from "./i18n.js"; +import {I18n} from "./i18n.js"; -class Permissions{ +class Permissions { allow: bigint; deny: bigint; readonly hasDeny: boolean; - constructor(allow: string, deny: string = ""){ + constructor(allow: string, deny: string = "") { this.hasDeny = Boolean(deny); - try{ + try { this.allow = BigInt(allow); this.deny = BigInt(deny); - }catch{ + } catch { this.allow = 0n; this.deny = 0n; console.error( - `Something really stupid happened with a permission with allow being ${allow} and deny being, ${deny}, execution will still happen, but something really stupid happened, please report if you know what caused this.` + `Something really stupid happened with a permission with allow being ${allow} and deny being, ${deny}, execution will still happen, but something really stupid happened, please report if you know what caused this.`, ); } } - getPermissionbit(b: number, big: bigint): boolean{ + getPermissionbit(b: number, big: bigint): boolean { return Boolean((big >> BigInt(b)) & 1n); } - setPermissionbit(b: number, state: boolean, big: bigint): bigint{ + setPermissionbit(b: number, state: boolean, big: bigint): bigint { const bit = 1n << BigInt(b); - return(big & ~bit) | (BigInt(state) << BigInt(b)); //thanks to geotale for this code :3 + return (big & ~bit) | (BigInt(state) << BigInt(b)); //thanks to geotale for this code :3 } //private static info: { name: string; readableName: string; description: string }[]; - static *info():Generator<{ name: string; readableName: string; description: string }>{ - for(const thing of this.permisions){ + static *info(): Generator<{name: string; readableName: string; description: string}> { + for (const thing of this.permisions) { yield { - name:thing, - readableName:I18n.getTranslation("permissions.readableNames."+thing), - description:I18n.getTranslation("permissions.descriptions."+thing), - } + name: thing, + readableName: I18n.permissions.readableNames[thing](), + description: I18n.permissions.descriptions[thing](thing), + }; } } - static permisions=[ + static permisions = [ "CREATE_INSTANT_INVITE", "KICK_MEMBERS", "BAN_MEMBERS", @@ -83,57 +83,52 @@ class Permissions{ "USE_EXTERNAL_SOUNDS", "SEND_VOICE_MESSAGES", "SEND_POLLS", - "USE_EXTERNAL_APPS" - ]; - getPermission(name: string): number{ - if(undefined===Permissions.permisions.indexOf(name)){ - console.error(name +" is not found in map",Permissions.permisions); + "USE_EXTERNAL_APPS", + "PIN_MESSAGES", + "BYPASS_SLOWMODE", + ] as const; + getPermission(name: string): number { + if (undefined === Permissions.permisions.indexOf(name as any)) { + console.error(name + " is not found in map", Permissions.permisions); } - if(this.getPermissionbit(Permissions.permisions.indexOf(name), this.allow)){ + if (this.getPermissionbit(Permissions.permisions.indexOf(name as any), this.allow)) { return 1; - }else if( - this.getPermissionbit(Permissions.permisions.indexOf(name), this.deny) - ){ - return-1; - }else{ + } else if (this.getPermissionbit(Permissions.permisions.indexOf(name as any), this.deny)) { + return -1; + } else { return 0; } } - hasPermission(name: string,adminOverride=true): boolean{ - if(this.deny){ + hasPermission(name: string, adminOverride = true): boolean { + if (this.deny) { console.warn( - "This function may of been used in error, think about using getPermision instead" + "This function may of been used in error, think about using getPermision instead", ); } - if(this.getPermissionbit(Permissions.permisions.indexOf(name), this.allow)) - return true; - if(name !== "ADMINISTRATOR"&&adminOverride)return this.hasPermission("ADMINISTRATOR"); + if (this.getPermissionbit(Permissions.permisions.indexOf(name as any), this.allow)) return true; + if (name !== "ADMINISTRATOR" && adminOverride) return this.hasPermission("ADMINISTRATOR"); return false; } - setPermission(name: string, setto: number): void{ - const bit = Permissions.permisions.indexOf(name); - if(bit===undefined){ + setPermission(name: string, setto: number): void { + const bit = Permissions.permisions.indexOf(name as any); + if (bit === undefined) { return console.error( - "Tried to set permission to " + -setto + -" for " + -name + -" but it doesn't exist" + "Tried to set permission to " + setto + " for " + name + " but it doesn't exist", ); } - if(setto === 0){ + if (setto === 0) { this.deny = this.setPermissionbit(bit, false, this.deny); this.allow = this.setPermissionbit(bit, false, this.allow); - }else if(setto === 1){ + } else if (setto === 1) { this.deny = this.setPermissionbit(bit, false, this.deny); this.allow = this.setPermissionbit(bit, true, this.allow); - }else if(setto === -1){ + } else if (setto === -1) { this.deny = this.setPermissionbit(bit, true, this.deny); this.allow = this.setPermissionbit(bit, false, this.allow); - }else{ + } else { console.error("invalid number entered:" + setto); } } } -export{ Permissions }; +export {Permissions}; diff --git a/src/webpage/recover.ts b/src/webpage/recover.ts new file mode 100644 index 00000000..562e4e56 --- /dev/null +++ b/src/webpage/recover.ts @@ -0,0 +1,100 @@ +import {I18n} from "./i18n.js"; +import {Dialog, FormError} from "./settings.js"; +await I18n.done; +const info = JSON.parse(localStorage.getItem("instanceinfo") as string); + +function makeMenu2(email: string | void) { + const d2 = new Dialog(""); + d2.options.addTitle(I18n.login.recovery()); + const headers = { + "Content-Type": "application/json", + }; + const opt = d2.float.options.addForm( + "", + async (obj) => { + if ("token" in obj && typeof obj.token === "string") { + window.location.href = "/login" + window.location.search; + } + }, + { + fetchURL: info.api + "/auth/reset", + method: "POST", + headers, + }, + ); + if (email !== undefined) { + opt.addTextInput(I18n.login.pasteInfo(), "token"); + } + opt.addTextInput(I18n.login.newPassword(), "password", {password: true}); + const p2 = opt.addTextInput(I18n.login.enterPAgain(), "password2", {password: true}); + opt.addPreprocessor((e) => { + const obj = e as unknown as {password: string; password2?: string; token?: string}; + const token = obj.token || window.location.href; + if (URL.canParse(token)) { + obj.token = new URLSearchParams(token.split("#")[1]).get("token") as string; + } + + if (obj.password !== obj.password2) { + throw new FormError(p2, I18n.localuser.PasswordsNoMatch()); + } + delete obj.password2; + }); + d2.show(false); +} +function makeMenu1() { + const d = new Dialog(""); + d.options.addTitle(I18n.login.recovery()); + let area: HTMLElement | undefined = undefined; + const opt = d.float.options.addForm( + "", + (e) => { + if (Object.keys(e).length === 0) { + d.hide(); + makeMenu2(email.value); + } else if ("captcha_sitekey" in e && typeof e.captcha_sitekey === "string") { + if (area) { + eval("hcaptcha.reset()"); + } else { + area = document.createElement("div"); + opt.addHTMLArea(area); + const capty = document.createElement("div"); + capty.classList.add("h-captcha"); + + capty.setAttribute("data-sitekey", e.captcha_sitekey); + const script = document.createElement("script"); + script.src = "https://js.hcaptcha.com/1/api.js"; + area.append(script); + area.append(capty); + } + } + }, + { + fetchURL: info.api + "/auth/forgot", + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }, + ); + const email = opt.addTextInput(I18n.htmlPages.emailField(), "login"); + opt.addPreprocessor((e) => { + if (area) { + try { + //@ts-expect-error + e.captcha_key = area.children[1].children[1].value; + } catch (e) { + console.error(e); + } + } + }); + d.show(false); +} +if (window.location.pathname.startsWith("/reset")) + if ( + window.location.href.split("#").length == 2 && + new URLSearchParams(window.location.href.split("#")[1]).has("token") + ) { + makeMenu2(); + } else { + makeMenu1(); + } diff --git a/src/webpage/register.html b/src/webpage/register.html index e6727e8f..3c80b7b9 100644 --- a/src/webpage/register.html +++ b/src/webpage/register.html @@ -1,64 +1,29 @@ - + - - - Jank Client - - - - - - - + + + Fermi + + + + + + + + -
-

Create an account

-
-
- -

- -
-
- - -
- -
- - -
-
- - -
- -
- - -
- -
- - -
- -
- I agree to the Terms of Service: - -
- -

-
- -
- -
- Already have an account? -
- - +

Register for a Spacebar instance!

+ diff --git a/src/webpage/register.ts b/src/webpage/register.ts index 7a9555cb..5af7fb35 100644 --- a/src/webpage/register.ts +++ b/src/webpage/register.ts @@ -1,164 +1,119 @@ -import { I18n } from "./i18n.js"; -import{ checkInstance, adduser }from"./login.js"; -import { MarkDown } from "./markdown.js"; -await I18n.done -const registerElement = document.getElementById("register"); -if(registerElement){ - registerElement.addEventListener("submit", registertry); -} -(async ()=>{ - await I18n.done; - const userField=document.getElementById("userField"); - const pw2Field=document.getElementById("pw2Field"); - const dobField=document.getElementById("dobField"); - const createAccount=document.getElementById("createAccount"); - const alreadyHave=document.getElementById("alreadyHave"); - if(userField&&pw2Field&&alreadyHave&&createAccount&&dobField){ - userField.textContent=I18n.getTranslation("htmlPages.userField") - pw2Field.textContent=I18n.getTranslation("htmlPages.pw2Field") - dobField.textContent=I18n.getTranslation("htmlPages.dobField") - createAccount.textContent=I18n.getTranslation("htmlPages.createAccount") - alreadyHave.textContent=I18n.getTranslation("htmlPages.alreadyHave") - } -})() -async function registertry(e: Event){ - e.preventDefault(); - const elements = (e.target as HTMLFormElement) - .elements as HTMLFormControlsCollection; - const email = (elements[1] as HTMLInputElement).value; - const username = (elements[2] as HTMLInputElement).value; - const password = (elements[3] as HTMLInputElement).value; - const confirmPassword = (elements[4] as HTMLInputElement).value; - const dateofbirth = (elements[5] as HTMLInputElement).value; - const consent = (elements[6] as HTMLInputElement).checked; - const captchaKey = (elements[7] as HTMLInputElement)?.value; +import {I18n} from "./i18n.js"; +import {adduser, Specialuser} from "./utils/utils.js"; +import {makeLogin} from "./login.js"; +import {MarkDown} from "./markdown.js"; +import {Dialog, FormError} from "./settings.js"; +import {trimTrailingSlashes} from "./utils/netUtils"; +export async function makeRegister( + trasparentBg = false, + instance = "", + handle?: (user: Specialuser) => void, +) { + const dialog = new Dialog(""); + const opt = dialog.options; + opt.addTitle(I18n.htmlPages.createAccount()); + const picker = opt.addInstancePicker( + (info) => { + form.fetchURL = trimTrailingSlashes(info.api) + "/auth/register"; + tosLogic(md); + }, + {instance}, + ); + dialog.show(trasparentBg).parentElement!.style.zIndex = "200"; + const invite = new URLSearchParams(window.location.search).get("invite"); - if(password !== confirmPassword){ - (document.getElementById("wrong") as HTMLElement).textContent = I18n.getTranslation("localuser.PasswordsNoMatch"); - return; - } - - const instanceInfo = JSON.parse(localStorage.getItem("instanceinfo") ?? "{}"); - const apiurl = new URL(instanceInfo.api); - - try{ - const response = await fetch(apiurl + "/auth/register", { - body: JSON.stringify({ - date_of_birth: dateofbirth, - email, - username, - password, - consent, - captcha_key: captchaKey, - }), + const form = opt.addForm( + "", + (res) => { + if ("token" in res && typeof res.token == "string") { + const u = adduser({ + serverurls: JSON.parse(localStorage.getItem("instanceinfo") as string), + email: email.value, + token: res.token, + }); + u.username = user.value; + if (handle) { + dialog.hide(); + handle(u); + return; + } + const redir = new URLSearchParams(window.location.search).get("goback"); + if ( + redir && + (!URL.canParse(redir) || new URL(redir).host === window.location.host) && + !invite + ) { + window.location.href = redir; + } else { + window.location.href = "/channels/@me"; + } + } + }, + { + submitText: I18n.htmlPages.createAccount(), + method: "POST", headers: { - "content-type": "application/json", + "Content-type": "application/json; charset=UTF-8", + Referrer: window.location.href, }, - method: "POST", - }); - - const data = await response.json(); + vsmaller: true, + }, + ); + const button = form.button.deref(); + picker.giveButton(button); + button?.classList.add("createAccount"); - if(data.captcha_sitekey){ - const capt = document.getElementById("h-captcha"); - if(capt && !capt.children.length){ - const capty = document.createElement("div"); - capty.classList.add("h-captcha"); - capty.setAttribute("data-sitekey", data.captcha_sitekey); - const script = document.createElement("script"); - script.src = "https://js.hcaptcha.com/1/api.js"; - capt.append(script); - capt.append(capty); - }else{ - eval("hcaptcha.reset()"); - } - return; - } + const email = form.addTextInput(I18n.htmlPages.emailField(), "email"); + const user = form.addTextInput(I18n.htmlPages.userField(), "username"); + const p1 = form.addTextInput(I18n.htmlPages.pwField(), "password", {password: true}); + const p2 = form.addTextInput(I18n.htmlPages.pw2Field(), "password2", {password: true}); - if(!data.token){ - handleErrors(data.errors, elements); - }else{ - adduser({ - serverurls: instanceInfo, - email, - token: data.token, - }).username = username; - localStorage.setItem("token", data.token); - const redir = new URLSearchParams(window.location.search).get("goback"); - window.location.href = redir ? redir : "/channels/@me"; + form.addDateInput(I18n.htmlPages.dobField(), "date_of_birth"); + form.addPreprocessor((e) => { + if (p1.value !== p2.value) { + throw new FormError(p2, I18n.localuser.PasswordsNoMatch()); } - }catch(error){ - console.error("Registration failed:", error); - } -} + //@ts-expect-error it's there + delete e.password2; + //@ts-expect-error it's allowed + e.invite = invite; + if (!check.checked) throw new FormError(checkbox, I18n.register.tos()); + //@ts-expect-error it's there + e.consent = check.checked; + }); + const toshtml = document.createElement("div"); + const md = document.createElement("span"); + const check = document.createElement("input"); + check.type = "checkbox"; -function handleErrors(errors: any, elements: HTMLFormControlsCollection){ - if(errors.consent){ - showError(elements[6] as HTMLElement, errors.consent._errors[0].message); - }else if(errors.password){ - showError( -elements[3] as HTMLElement, -I18n.getTranslation("register.passwordError",errors.password._errors[0].message) - ); - }else if(errors.username){ - showError( -elements[2] as HTMLElement, -I18n.getTranslation("register.usernameError",errors.username._errors[0].message) - ); - }else if(errors.email){ - showError( -elements[1] as HTMLElement, -I18n.getTranslation("register.emailError",errors.email._errors[0].message) - ); - }else if(errors.date_of_birth){ - showError( -elements[5] as HTMLElement, -I18n.getTranslation("register.DOBError",errors.date_of_birth._errors[0].message) - ); - }else{ - (document.getElementById("wrong") as HTMLElement).textContent = -errors[Object.keys(errors)[0]]._errors[0].message; - } -} - -function showError(element: HTMLElement, message: string){ - const parent = element.parentElement!; - let errorElement = parent.getElementsByClassName( - "suberror" - )[0] as HTMLElement; - if(!errorElement){ - const div = document.createElement("div"); - div.classList.add("suberror", "suberrora"); - parent.append(div); - errorElement = div; - }else{ - errorElement.classList.remove("suberror"); - setTimeout(()=>{ - errorElement.classList.add("suberror"); - }, 100); - } - errorElement.textContent = message; + toshtml.append(md, check); + const checkbox = form.addHTMLArea(toshtml); + form.addCaptcha(); + const a = document.createElement("a"); + a.onclick = () => { + dialog.hide(); + makeLogin(trasparentBg); + }; + a.textContent = I18n.htmlPages.alreadyHave(); + form.addHTMLArea(a); } - -async function tosLogic(){ +async function tosLogic(box: HTMLElement) { const instanceInfo = JSON.parse(localStorage.getItem("instanceinfo") ?? "{}"); const apiurl = new URL(instanceInfo.api); - const urlstr=apiurl.toString(); + const urlstr = apiurl.toString(); const response = await fetch(urlstr + (urlstr.endsWith("/") ? "" : "/") + "ping"); const data = await response.json(); const tosPage = data.instance.tosPage; - - if(tosPage){ - const box=document.getElementById("TOSbox"); - if(!box) return; - box.innerHTML =""; - box.append(new MarkDown(I18n.getTranslation("register.agreeTOS",tosPage)).makeHTML()); - }else{ - document.getElementById("TOSbox")!.textContent =I18n.getTranslation("register.noTOS"); + if (!box) return; + if (tosPage) { + box.innerHTML = ""; + box.append(new MarkDown(I18n.register.agreeTOS(tosPage)).makeHTML()); + } else { + box.textContent = I18n.register.noTOS(); } console.log(tosPage); } - -tosLogic(); - -(checkInstance as any).alt = tosLogic; +if (window.location.pathname.startsWith("/register")) { + await I18n.done; + makeRegister(); +} diff --git a/src/webpage/reporting/report.ts b/src/webpage/reporting/report.ts new file mode 100644 index 00000000..275543e3 --- /dev/null +++ b/src/webpage/reporting/report.ts @@ -0,0 +1,568 @@ +import {Guild} from "../guild.js"; +import {I18n} from "../i18n.js"; +import {Localuser} from "../localuser.js"; +import {MarkDown} from "../markdown.js"; +import {Member} from "../member.js"; +import {Message} from "../message.js"; +import {User} from "../user.js"; +import {removeAni} from "../utils/utils.js"; +import { + buttonTypes, + report, + reportElements, + reportMessagePut, + reportNode, + reportPut, + reportTypes, + reportUserPut, + reportGuildPut, + reportGuildDiscovery, + reportApplicationPut, +} from "./types.js"; +interface InfoMap { + message?: Message; + user?: User; + member?: Member; + failMessage?: string; + guild?: Guild; + guild_id?: string; + dyn_preview?: () => HTMLElement; + application_id?: string; +} +export class ReportMenu { + variant: string; + name: reportTypes; + owner: Localuser; + postbackUrl: URL; + rootNodeId: number; + successNodeId: number; + failNodeId: number; + reportNodes: Record; + get localuser() { + return this.owner; + } + get info() { + return this.localuser.info; + } + nodes: ReportNode[]; + options: string[] = []; + infoMap: InfoMap; + node?: ReportNode; + constructor(json: report, localuser: Localuser, infoMap: InfoMap) { + if (json.version !== "1.0") throw new Error("uh oh"); + this.name = json.name; + this.variant = json.variant; + this.owner = localuser; + this.postbackUrl = new URL(json.postback_url, this.info.api); + this.rootNodeId = json.root_node_id; + this.successNodeId = json.success_node_id; + this.failNodeId = json.fail_node_id; + this.reportNodes = {}; + for (const [id, nodejson] of Object.entries(json.nodes)) { + this.reportNodes[id] = new ReportNode(nodejson, this); + } + const first = this.reportNodes[this.rootNodeId]; + if (!first) throw new Error("unable to find first node"); + this.nodes = []; + this.infoMap = infoMap; + } + div?: HTMLDivElement; + async spawnMenu() { + const background = document.createElement("div"); + background.classList.add("background"); + background.onkeydown = (e) => { + if (e.key == "Escape") { + removeAni(background); + } + }; + background.onclick = () => { + removeAni(background); + }; + + const div = document.createElement("div"); + div.classList.add("flexttb", "reportMenu"); + background.append(div); + this.div = div; + div.onclick = (e) => { + e.stopImmediatePropagation(); + }; + const first = this.reportNodes[this.rootNodeId]; + first.render(); + + document.body.append(background); + } + static async makeReport(type: reportTypes, localuser: Localuser, infoMap: InfoMap = {}) { + const res = await fetch(localuser.info.api + "/reporting/menu/" + type, { + headers: localuser.headers, + }); + if (!res.ok) return; + const json = (await res.json()) as report; + return new ReportMenu(json, localuser, infoMap); + } + async submit(takeToScreen = true) { + const obj: Omit = { + version: "1.0", + variant: this.variant, + language: I18n.lang, + breadcrumbs: [...this.nodes.map((_) => _.id), this.node?.id as number], + elements: this.gatherElements(), + }; + let realBody: any; + switch (this.name) { + case "message": { + const message = this.infoMap.message; + if (!message) throw new Error("Message expected"); + const m: reportMessagePut = { + ...obj, + name: "message", + message_id: message.id, + channel_id: message.channel.id, + }; + realBody = m; + break; + } + case "user": { + const user = this.infoMap.user; + if (!user) throw new Error("User expected"); + const m: reportUserPut = { + ...obj, + name: "user", + user_id: user.id, + guild_id: this.infoMap.member?.guild.id || "@me", + }; + realBody = m; + break; + } + case "guild": { + const guild = this.infoMap.guild; + if (!guild) throw new Error("Guild expected"); + const m: reportGuildPut = { + ...obj, + name: "guild", + guild_id: guild.id, + }; + realBody = m; + break; + } + case "guild_discovery": { + const id = this.infoMap.guild_id; + if (!id) throw new Error("id expected"); + const m: reportGuildDiscovery = { + ...obj, + name: "guild_discovery", + guild_id: id, + }; + realBody = m; + break; + } + case "application": { + const id = this.infoMap.application_id; + if (!id) throw new Error("id expected"); + const m: reportApplicationPut = { + ...obj, + name: "application", + application_id: id, + }; + realBody = m; + break; + } + } + const res = await fetch(this.postbackUrl, { + method: "POST", + headers: this.localuser.headers, + body: JSON.stringify(realBody), + }); + if (res.ok) { + if (takeToScreen) { + const suc = this.reportNodes[this.successNodeId]; + if (!suc) throw new Error("unable to find suc node"); + suc.render(); + } + } else { + const json = await res.json(); + this.errorNode(json?.message); + } + } + errorNode(message?: string) { + const error = this.reportNodes[this.failNodeId]; + this.infoMap.failMessage = message; + if (!error) throw new Error("unable to find suc node"); + error.render(); + } + gatherElements() { + let elms: Record = {}; + for (const node of this.nodes) { + elms = { + ...node.gatherElements(), + ...elms, + }; + } + return elms; + } +} +class ReportNode { + owner: ReportMenu; + key: string; + header: string; + subheader: string | null; + info: string | null; + buttonType?: buttonTypes; + buttonTarget?: number; + elements: ReportElement[]; + reportType: string | null; + children: [string, number][]; + isMultiSelectRequired: boolean; + isAutoSubmit: boolean; + id: number; + constructor(json: reportNode, owner: ReportMenu) { + this.owner = owner; + this.header = json.header; + this.subheader = json.subheader; + this.key = json.key; + this.buttonType = json.button?.type; + this.buttonTarget = json.button?.target || undefined; + this.elements = json.elements.map((_) => new ReportElement(_, this)); + this.reportType = json.report_type; + this.children = json.children; + this.isMultiSelectRequired = json.is_multi_select_required; + this.isAutoSubmit = json.is_auto_submit; + this.id = json.id; + this.info = json.info; + } + div?: HTMLDivElement; + render() { + if (this.owner.node) { + if (this.owner.node.div) removeAni(this.owner.node.div); + } + this.owner.node = this; + const div = document.createElement("div"); + div.classList.add("flexttb"); + this.div = div; + const title = document.createElement("h2"); + title.textContent = this.header; + div.append(title); + + if (this.subheader) { + const sub = document.createElement("h3"); + sub.append(new MarkDown(this.subheader).makeHTML()); + div.append(sub); + } + div.append(document.createElement("hr")); + if (this.info) { + const info = document.createElement("span"); + info.textContent = this.info; + div.append(info); + } + div.append(...this.elements.map((e) => e.makeHTML())); + const children = document.createElement("div"); + children.classList.add("reportChildren", "flexttb"); + children.append( + ...this.children.map(([name, id]) => { + const button = document.createElement("button"); + button.textContent = name; + button.onclick = () => { + this.jumpPannel(id, true, name); + }; + return button; + }), + ...this.elements + .filter((_) => _.json.type === "external_link") + .map(({json}) => { + if (json.type !== "external_link") return; + if (json.skip_if_unlocalized && !json.is_localized) return; + const data = json.data; + const button = document.createElement("button"); + button.textContent = data.link_text; + button.onclick = () => { + window.open(data.url, "_blank")?.focus(); + }; + return button; + }) + .filter((_) => _ !== undefined), + ); + + const buttonDiv = document.createElement("div"); + buttonDiv.classList.add("flexltr", "reportButtonDiv"); + console.log(this.buttonType); + switch (this.buttonType) { + case "next": { + const next = document.createElement("button"); + next.textContent = I18n.report.next(); + buttonDiv.append(next); + next.onclick = () => { + if (this.isMultiSelectRequired) { + for (const elm of this.elements) { + if (elm.notFilled()) { + return; + } + } + } + if (this.buttonTarget !== undefined) this.jumpPannel(this.buttonTarget, true); + }; + break; + } + case "submit": { + const submit = document.createElement("button"); + submit.textContent = I18n.report.submit(); + buttonDiv.append(submit); + submit.onclick = () => { + this.owner.submit(); + }; + break; + } + case "cancel": { + const cancel = document.createElement("button"); + cancel.textContent = I18n.report.cancel(); + buttonDiv.prepend(cancel); + cancel.onclick = () => { + const div = this.owner.div?.parentElement; + if (div) removeAni(div); + }; + break; + } + + default: + console.log(this.buttonType); + } + if ( + this.buttonType !== "cancel" && + this.buttonType !== "done" && + !this.elements.find((e) => e.json.type === "skip") && + this.owner.nodes.length + ) { + const back = document.createElement("button"); + back.textContent = I18n.report.back(); + buttonDiv.prepend(back); + back.onclick = () => { + const pop = this.owner.nodes.pop(); + this.owner.options.pop(); + if (pop) { + this.jumpPannel(pop.id); + } + }; + } + + div.append(children, buttonDiv); + if (this.isAutoSubmit) this.owner.submit(false); + + this.owner.div?.append(div); + } + gatherElements() { + const elms: Record = {}; + for (const thing of this.elements) { + if (thing.options?.length) elms[thing.json.name] = thing.options; + } + return elms; + } + + jumpPannel(id: number, push = false, option = "") { + if (push) { + this.owner.nodes.push(this); + this.owner.options.push(option); + } + const next = this.owner.reportNodes[id]; + if (!next) throw new Error("node doesn't exist"); + next.render(); + } + getSelectStr() { + let comb: string[] = []; + for (const elm of this.elements) { + if (elm.optionReal.length) { + comb.push(...elm.optionReal); + } + } + console.log(comb, this); + return new Intl.ListFormat(I18n.lang).format(comb); + } +} +class ReportElement { + json: reportElements; + owner: ReportNode; + options: string[] = []; + optionReal: string[] = []; + constructor(json: reportElements, owner: ReportNode) { + this.json = json; + this.owner = owner; + } + makeHTML() { + const div = document.createElement("div"); + div.classList.add("reportOption", "flexttb"); + const json = this.json; + if (json.skip_if_unlocalized && !json.is_localized) { + return div; + } + const map = this.owner.owner.infoMap; + switch (json.type) { + case "message_preview": { + const m = this.owner.owner.infoMap.message; + if (m) { + div.append(m.buildhtml(undefined, true)); + } else { + //Apparently discord is dumb and will use in menus without messages + //div.append("You should never see this"); + } + break; + } + case "breadcrumbs": { + const title = document.createElement("span"); + title.textContent = I18n.report.summary(); + div.append(title); + const ul = document.createElement("ul"); + + for (let i = 0; i < this.owner.owner.options.length; i++) { + const str = this.owner.owner.options[i]; + if (str) { + const li = document.createElement("li"); + li.textContent = str; + ul.append(li); + } + const op = this.owner.owner.nodes[i]; + if (op) { + const str = op.getSelectStr(); + if (str) { + const li = document.createElement("li"); + li.textContent = str; + ul.append(li); + } + } + } + + div.append(ul); + break; + } + case "ignore_users": + case "share_with_parents": { + //TODO implement when spacebar implements these + break; + } + case "block_users": { + const user = map.message?.author || map.user; + if (!user) break; + const button = document.createElement("button"); + button.textContent = I18n.report.blockUser(); + div.append(button); + button.onclick = () => { + user.block(); + }; + break; + } + case "mute_users": { + const message = map.message; + if (!message) break; + if (!message.guild.member.hasPermission("MUTE_MEMBERS")) break; + (async () => { + const member = await Member.resolveMember(message.author, message.guild); + if (!member) return; + + const button = document.createElement("button"); + button.textContent = I18n.report.timeout(); + div.append(button); + button.onclick = () => { + member.timeout(); + }; + })(); + + break; + } + case "app_preview": { + //TODO figure out what this is supposed to be + break; + } + case "user_preview": { + const user = map.user; + if (!user) break; + div.append(user.createWidget(map.member?.guild)); + break; + } + case "external_link": + case "skip": { + break; + } + case "checkbox": { + div.classList.add("friendGroupSelect"); + for (const [subName, name, desc] of json.data) { + const elm = document.createElement("div"); + elm.classList.add("flexltr", "checkCard"); + const check = document.createElement("input"); + check.type = "checkbox"; + check.checked = this.options.includes(subName); + check.onclick = (e) => e.stopImmediatePropagation(); + elm.onclick = (e) => { + e.stopImmediatePropagation(); + check.click(); + }; + check.onchange = () => { + if (check.checked) { + this.options.push(subName); + } else { + this.options = this.options.filter((_) => _ !== subName); + } + this.optionReal = this.options.map( + (_) => json.data.find((e) => e[0] === _)?.[1] as string, + ); + }; + + const names = document.createElement("div"); + names.classList.add("flexttb"); + const nameElm = document.createElement("span"); + nameElm.textContent = name; + names.append(nameElm); + if (desc) { + const descElm = document.createElement("span"); + descElm.textContent = desc; + names.append(descElm); + } + elm.append(names, check); + div.append(elm); + } + break; + } + case "fail": { + if (!this.owner.owner.infoMap.failMessage) break; + const span = document.createElement("span"); + span.textContent = this.owner.owner.infoMap.failMessage; + div.append(span); + break; + } + case "text": { + const h4 = document.createElement("h4"); + h4.textContent = json.data.header; + const p = document.createElement("p"); + p.textContent = json.data.body; + div.append(h4, p); + break; + } + case "guild_preview": { + const guild = map.guild; + if (!guild) break; + const guildDiv = document.createElement("div"); + guildDiv.classList.add("flexltr"); + guildDiv.append(guild.generateGuildIcon(false)); + const title = document.createElement("h4"); + title.textContent = guild.properties.name; + guildDiv.append(title); + div.append(guildDiv); + break; + } + case "guild_discovery_preview": { + const dyn = map.dyn_preview; + if (!dyn) break; + div.append(dyn()); + break; + } + default: + console.log(json); + div.textContent = this.json.type; + } + + return div; + } + notFilled() { + switch (this.json.type) { + case "checkbox": + return !this.options.length; + default: + return false; + } + } +} diff --git a/src/webpage/reporting/types.ts b/src/webpage/reporting/types.ts new file mode 100644 index 00000000..38f681a6 --- /dev/null +++ b/src/webpage/reporting/types.ts @@ -0,0 +1,291 @@ +export type reportTypes = + | "message" + | "user" + | "guild_discovery" + | "guild" + | "guild_scheduled_event" + | "stage_channel" + | "first_dm" + | "user" + | "application" + | "widget" + | "guild_directory_entry"; +export type buttonTypes = + | "submit" // This button shows a warning about making false reports along with the submit and back button + | "done" //This button just shows a Done button without a back button + | "cancel" //This button may provide a back button, but does not provide a button to move forward + | "next"; //This button takes you to the next node along with a back button +interface reportElementBase { + name: string; + type: string; + /** + * Data used for more complex elements to allow for things like checkboxes or additional information + */ + data: unknown; + /** + * Set only if the elements information should be submitted, only ever true with the checkbox type + */ + should_submit_data: boolean; + /** + * This tag is used for if the element should not be rendered if its not been translated into the users language + */ + skip_if_unlocalized: boolean; + is_localized: boolean | null; +} +type singleElementTypes = + | "message_preview" //Shows the user a preview of the message + | "app_preview" // Shows you what you're reporting the user for + | "guild_preview" // Shows the user a preview of reported guild + | "guild_directory_entry_preview" // Shows the user a preview of the dirrectory the user is reporting + | "guild_scheduled_event_preview" //Shows the user a preview of the event the user is reporting + | "channel_preview" //shows the user a preview of the reported channel + | "widget_preview" //show the user a preview of the reported widget + | "breadcrumbs" // I think this is a defunct element that visually does not do anything, but was at one point a signal to include the breadcrumbs of how the user got there. + | "fail" // used in the failed menus + | "share_with_parents" // Offers to the user to shares report with parents (if applicable) + | "block_users" // Offers to the user to block them + | "mute_users" // Offers to the user to mute the user (if applicable) + | "success" // Used to notate a report that succeeded + | "ignore_users" // Offers to the user to ignore them + | "settings_upsells" // Additional settings actions that can be taken + | "delete_message" // Offers to delete the message (if applicable) + | "user_preview" // Shows a preview of the user + | "skip" // Exits the menu and does not allow for going back + | "leave_guild" // An option to let you leave the guild + | "deauthorize_app" //An option to deauthorize the app + | "guild_discovery_preview"; + +interface singleElementReport extends reportElementBase { + name: X; + type: X; + data: null; + /** + * If the element has no text to be localized, it is localized + */ + is_localized: true; + should_submit_data: false; +} + +interface externalLinkReport extends reportElementBase { + name: "external_link"; + type: "external_link"; + data: { + is_localized: null | boolean; + link_text: string; + url: string; + link_description: null | string; + /** + * If this is defined, it means to just not render it, not sure why this exists + */ + is_header_hidden?: true; + }; + should_submit_data: false; +} +interface selectReport extends reportElementBase { + name: string; + type: "checkbox"; + /** + * [actual_name, human_readable_name, ?description] + */ + data: ([string, string] | [string, string, string])[]; + should_submit_data: true; +} +interface dropdownReport extends reportElementBase { + name: string; + type: "dropdown"; + data: { + title: string; + options: { + /** + * Used for what to send to the APU + */ + value: string; + label: string; + }[]; + }; + should_submit_data: true; +} + +interface freeTextReport extends reportElementBase { + name: string; + type: "free_text"; + data: { + title: string; + options: { + title?: string; + subtitle?: string; + placeholder?: string; + /** + * Number of visible text rows + */ + rows: number; + character_limit: number; + /** + * It must match this regex pattern + */ + pattern: string; + }[]; + }; + should_submit_data: true; +} +interface selfHarmHelp extends reportElementBase { + name: string; + type: "text_line_resource"; + data: { + is_localized: boolean; + title: string; + body: string; + sms: string; + /** + * The information to send in the text message + */ + sms_body: string; + }; + should_submit_data: false; +} +interface reportText extends reportElementBase { + name: "text"; + type: "text"; + data: { + header: string; + body: string; + is_localized: boolean; + }; +} +export type reportElements = + | singleElementReport + | externalLinkReport + | selectReport + | selfHarmHelp + | reportText + | freeTextReport + | dropdownReport; +export interface report { + name: reportTypes; + variant: string; + version: string; + + /** + * this is an relative URL in respect to the API base URL in JS to implement correctly do `new URL(report.postback_url, api_url)` + */ + postback_url: string; + /** + * this is the node the reporting process should start on + */ + root_node_id: number; + success_node_id: number; + fail_node_id: number; + /** + * defaults to english + */ + language?: string; + nodes: { + [key: string]: reportNode; + }; +} +export interface reportNode { + id: number; + /** + * this key is likely only used for translation reasons, and is not used during the reporting process + */ + key: string; + header: string; + subheader: string | null; + /** + * information about a certain section displayed in a special box + */ + info: null | string; + /** + * the button at the bottom of the report box + */ + button: { + type: buttonTypes; + target: null | number; + } | null; + /** + * these are the elements that aren't the buttons to just continue along and may provide information to the user, or present the user with more options + */ + elements: reportElements[]; + /** + * this says what type of thing the user is reporting, though isn't actually used in the process of reporting + */ + report_type: null | string; + /** + * These are the options with the strings leading to the numbers which represent the nodes to go to + */ + children: [string, number][]; + /** + * This is true if the checkbox element is required to have at least one element selected to continue + */ + is_multi_select_required: boolean; + /** + * Used if this screen automatically submits the report without additional input + */ + is_auto_submit: boolean; +} +export interface reportPut { + version: string; + variant: string; + language: string; + /** + * This is the node path that the user took to get here, which includes the submit/auto_submit screens + */ + breadcrumbs: number[]; + /** + * This is for various elements, this will return all of the things selected with the key being the name + */ + elements: { + [key: string]: string[]; + }; + + name: reportTypes; +} + +export interface reportMessagePut extends reportPut { + channel_id: string; + message_id: string; + name: "message"; +} +export interface reportGuildDiscovery extends reportPut { + guild_id: string; + name: "guild_discovery"; +} +export interface reportFirstDMPut extends reportPut { + channel_id: string; + message_id: string; + name: "first_dm"; +} +export interface reportGuildDirPut extends reportPut { + channel_id: string; + guild_id: string; + name: "guild_directory_entry"; +} +export interface reportGuildPut extends reportPut { + guild_id: string; + name: "guild"; +} +export interface reportStagePut extends reportPut { + guild_id: string; + user_id: string; + stage_instance_id: string; + name: "stage_channel"; +} +export interface reportGuildEventPut extends reportPut { + guild_id: string; + guild_scheduled_event_id: string; + name: "guild_scheduled_event"; +} +export interface reportApplicationPut extends reportPut { + application_id: string; + name: "application"; +} +export interface reportUserPut extends reportPut { + guild_id: string; + user_id: string; + name: "user"; +} +export interface reportWidgetPut extends reportPut { + user_id: string; + widget_id: string; + name: "widget"; +} diff --git a/src/webpage/reset.html b/src/webpage/reset.html new file mode 100644 index 00000000..41836334 --- /dev/null +++ b/src/webpage/reset.html @@ -0,0 +1,27 @@ + + + + + + Fermi + + + + + + + + + + + + diff --git a/src/webpage/rights.ts b/src/webpage/rights.ts new file mode 100644 index 00000000..27bc22ba --- /dev/null +++ b/src/webpage/rights.ts @@ -0,0 +1,122 @@ +//@ts-expect-error +import {I18n} from "./i18n.js"; + +class Rights { + allow!: bigint; + constructor(allow: string | number) { + this.update(allow); + } + isSameAs(allow: string | number) { + return this.allow === BigInt(allow); + } + update(allow: string | number) { + try { + this.allow = BigInt(allow); + } catch { + this.allow = 875069521787904n; + console.error( + `Something really stupid happened with a permission with allow being ${allow}, execution will still happen, but something really stupid happened, please report if you know what caused this.`, + ); + } + } + getPermissionbit(b: number, big: bigint): boolean { + return Boolean((big >> BigInt(b)) & 1n); + } + setPermissionbit(b: number, state: boolean, big: bigint): bigint { + const bit = 1n << BigInt(b); + return (big & ~bit) | (BigInt(state) << BigInt(b)); //thanks to geotale for this code :3 + } + static *info(): Generator<{name: string; readableName: string; description: string}> { + throw new Error("not implemented yet"); + /* + for (const thing of this.permisions) { + yield { + name: thing, + readableName: I18n.rights.readableNames[thing](), + description: I18n.rights.descriptions[thing](), + }; + } + */ + } + static readonly permisions = [ + "OPERATOR", + "MANAGE_APPLICATIONS", + "MANAGE_GUILDS", + "MANAGE_MESSAGES", + "MANAGE_RATE_LIMITS", + "MANAGE_ROUTING", + "MANAGE_TICKETS", + "MANAGE_USERS", + "ADD_MEMBERS", + "BYPASS_RATE_LIMITS", + "CREATE_APPLICATIONS", + "CREATE_CHANNELS", + "CREATE_DMS", + "CREATE_DM_GROUPS", + "CREATE_GUILDS", + "CREATE_INVITES", + "CREATE_ROLES", + "CREATE_TEMPLATES", + "CREATE_WEBHOOKS", + "JOIN_GUILDS", + "PIN_MESSAGES", + "SELF_ADD_REACTIONS", + "SELF_DELETE_MESSAGES", + "SELF_EDIT_MESSAGES", + "SELF_EDIT_NAME", + "SEND_MESSAGES", + "USE_ACTIVITIES", + "USE_VIDEO", + "USE_VOICE", + "INVITE_USERS", + "SELF_DELETE_DISABLE", + "DEBTABLE", + "CREDITABLE", + "KICK_BAN_MEMBERS", + "SELF_LEAVE_GROUPS", + "PRESENCE", + "SELF_ADD_DISCOVERABLE", + "MANAGE_GUILD_DIRECTORY", + "POGGERS", + "USE_ACHIEVEMENTS", + "INITIATE_INTERACTIONS", + "RESPOND_TO_INTERACTIONS", + "SEND_BACKDATED_EVENTS", + "USE_MASS_INVITES", + "ACCEPT_INVITES", + "SELF_EDIT_FLAGS", + "EDIT_FLAGS", + "MANAGE_GROUPS", + "VIEW_SERVER_STATS", + "RESEND_VERIFICATION_EMAIL", + "CREATE_REGISTRATION_TOKENS", + ] as const; + getPermission(name: string): boolean { + if (undefined === Rights.permisions.indexOf(name as any)) { + console.error(name + " is not found in map", Rights.permisions); + } + return this.getPermissionbit(Rights.permisions.indexOf(name as any), this.allow); + } + hasPermission(name: string, adminOverride = true): boolean { + if (this.getPermissionbit(Rights.permisions.indexOf(name as any), this.allow)) return true; + if (name !== "OPERATOR" && adminOverride) return this.hasPermission("OPERATOR"); + return false; + } + setPermission(name: string, setto: number): void { + const bit = Rights.permisions.indexOf(name as any); + if (bit === undefined) { + return console.error( + "Tried to set permission to " + setto + " for " + name + " but it doesn't exist", + ); + } + + if (setto === 0) { + this.allow = this.setPermissionbit(bit, false, this.allow); + } else if (setto === 1) { + this.allow = this.setPermissionbit(bit, true, this.allow); + } else { + console.error("invalid number entered:" + setto); + } + } +} +export {Rights}; diff --git a/src/webpage/robots.txt b/src/webpage/robots.txt new file mode 100644 index 00000000..5f1df6c8 --- /dev/null +++ b/src/webpage/robots.txt @@ -0,0 +1,73 @@ +# Block all known AI crawlers and assistants +# from using content for training AI models. +# Source: https://robotstxt.com/ai +User-Agent: GPTBot +User-Agent: ClaudeBot +User-Agent: Claude-User +User-Agent: Claude-SearchBot +User-Agent: CCBot +User-Agent: Google-Extended +User-Agent: Applebot-Extended +User-Agent: Facebookbot +User-Agent: Meta-ExternalAgent +User-Agent: Meta-ExternalFetcher +User-Agent: diffbot +User-Agent: PerplexityBot +User-Agent: Perplexity‑User +User-Agent: Omgili +User-Agent: Omgilibot +User-Agent: webzio-extended +User-Agent: ImagesiftBot +User-Agent: Bytespider +User-Agent: TikTokSpider +User-Agent: Amazonbot +User-Agent: Youbot +User-Agent: SemrushBot-OCOB +User-Agent: Petalbot +User-Agent: VelenPublicWebCrawler +User-Agent: TurnitinBot +User-Agent: Timpibot +User-Agent: OAI-SearchBot +User-Agent: ICC-Crawler +User-Agent: AI2Bot +User-Agent: AI2Bot-Dolma +User-Agent: DataForSeoBot +User-Agent: AwarioBot +User-Agent: AwarioSmartBot +User-Agent: AwarioRssBot +User-Agent: Google-CloudVertexBot +User-Agent: PanguBot +User-Agent: Kangaroo Bot +User-Agent: Sentibot +User-Agent: img2dataset +User-Agent: Meltwater +User-Agent: Seekr +User-Agent: peer39_crawler +User-Agent: cohere-ai +User-Agent: cohere-training-data-crawler +User-Agent: DuckAssistBot +User-Agent: Scrapy +User-Agent: Cotoyogi +User-Agent: aiHitBot +User-Agent: Factset_spyderbot +User-Agent: FirecrawlAgent + +Disallow: / +DisallowAITraining: / + +# Block any non-specified AI crawlers (e.g., new +# or unknown bots) from using content for training +# AI models, while allowing the website to be +# indexed and accessed by bots. These directives +# are still experimental and may not be supported +# by all AI crawlers. +User-Agent: * +DisallowAITraining: / +Content-Usage: ai=n +Disallow: /channel/ +Disallow: /app +Allow: /invite/ +Allow: /oauth2/ +Allow: /login +Allow: /register +Allow: / diff --git a/src/webpage/role.ts b/src/webpage/role.ts index d31fb647..f9f65f45 100644 --- a/src/webpage/role.ts +++ b/src/webpage/role.ts @@ -1,10 +1,15 @@ -import{ Permissions }from"./permissions.js"; -import{ Localuser }from"./localuser.js"; -import{ Guild }from"./guild.js"; -import{ SnowFlake }from"./snowflake.js"; -import{ rolesjson }from"./jsontypes.js"; -import{ Search }from"./search.js"; -class Role extends SnowFlake{ +import {Permissions} from "./permissions.js"; +import {Localuser} from "./localuser.js"; +import {Guild} from "./guild.js"; +import {SnowFlake} from "./snowflake.js"; +import {rolesjson} from "./jsontypes.js"; +import {Search} from "./search.js"; +import {OptionsElement, Buttons, Dialog, ColorInput} from "./settings.js"; +import {Contextmenu} from "./contextmenu.js"; +import {Channel} from "./channel.js"; +import {I18n} from "./i18n.js"; + +class Role extends SnowFlake { permissions: Permissions; owner: Guild; color!: number; @@ -14,54 +19,130 @@ class Role extends SnowFlake{ icon!: string; mentionable!: boolean; unicode_emoji!: string; - position!:number; + position!: number; headers: Guild["headers"]; - constructor(json: rolesjson, owner: Guild){ + colors?: { + primary_color: number; + secondary_color?: number | null; + tertiary_color?: number | null; + }; + constructor(json: rolesjson, owner: Guild) { super(json.id); + this.headers = owner.headers; this.info = owner.info; - for(const thing of Object.keys(json)){ - if(thing === "id"){ + for (const thing of Object.keys(json)) { + if (thing === "id") { continue; } (this as any)[thing] = (json as any)[thing]; } this.permissions = new Permissions(json.permissions); this.owner = owner; + this.roleLoad(); } - newJson(json: rolesjson){ - for(const thing of Object.keys(json)){ - if(thing === "id"||thing==="permissions"){ + getIcon(): HTMLElement | void { + const hover = new Hover(this.name); + if (this.icon) { + const img = createImg( + this.info.cdn + + "/role-icons/" + + this.id + + "/" + + this.icon + + ".webp" + + new CDNParams({expectedSize: 32}), + ); + img.classList.add("roleIcon"); + hover.addEvent(img); + return img; + } + if (this.unicode_emoji) { + const span = document.createElement("span"); + span.textContent = this.unicode_emoji; + span.classList.add("roleIcon"); + hover.addEvent(span); + return span; + } + } + getColorStyle(short = false) { + const grad = this.localuser.perminfo?.user?.gradientColors as boolean; + const [len1, len2, len3] = short + ? (["", "", ""] as const) + : (["30px", "60px", "90px"] as const); + if (this.colors && !grad) { + const prim = this.getColor(); + if (this.colors.secondary_color) { + const second = Role.numberToColor(this.colors.secondary_color); + if (this.colors.tertiary_color) { + const third = Role.numberToColor(this.colors.tertiary_color); + return `repeating-linear-gradient(90deg, ${prim}, ${second} ${len1}, ${third} ${len2}, ${prim} ${len3})`; + } + return `repeating-linear-gradient(90deg, ${prim} , ${second} ${len1}, ${prim} ${len2})`; + } + } + return `linear-gradient(90deg, ${this.getColor()})`; + } + roleLoad() { + const style = this.getColorStyle(); + document.documentElement.style.setProperty(`--role-${this.id}`, style); + } + newJson(json: rolesjson) { + for (const thing of Object.keys(json)) { + if (thing === "id" || thing === "permissions") { continue; } (this as any)[thing] = (json as any)[thing]; } - this.permissions.allow=BigInt(json.permissions); + this.roleLoad(); + this.permissions.allow = BigInt(json.permissions); + } + compare(str: string) { + function similar(str2: string | null | undefined) { + if (!str2) return 0; + const strl = Math.max(str.length, 1); + if (str2.includes(str)) { + return strl / str2.length; + } else if (str2.toLowerCase().includes(str.toLowerCase())) { + return strl / str2.length / 1.2; + } + return 0; + } + return Math.max(similar(this.name), similar(this.id) / 1.5); } - get guild(): Guild{ + get guild(): Guild { return this.owner; } - get localuser(): Localuser{ + get localuser(): Localuser { return this.guild.localuser; } - getColor(): string | null{ - if(this.color === 0){ + static numberToColor(numb: number) { + return `#${numb.toString(16).padStart(6, "0")}`; + } + getColor(): string | null { + if (this.color === 0) { return null; } - return`#${this.color.toString(16)}`; + return Role.numberToColor(this.color); } - canManage(){ - if(this.guild.member.hasPermission("MANAGE_ROLES")){ - let max=-Infinity; - this.guild.member.roles.forEach(r=>max=Math.max(max,r.position)) - return this.position<=max||this.guild.properties.owner_id===this.guild.member.id; + canManage() { + if (this.guild.member.hasPermission("MANAGE_ROLES")) { + let max = -Infinity; + this.guild.member.roles.forEach((r) => (max = Math.max(max, r.position))); + return this.position <= max || this.guild.properties.owner_id === this.guild.member.id; } return false; } } -export{ Role }; -import{ Options }from"./settings.js"; -class PermissionToggle implements OptionsElement{ +export {Role}; +import {Options} from "./settings.js"; +import {createImg} from "./utils/utils.js"; +import {Hover} from "./hover.js"; +import {Emoji} from "./emoji.js"; +import {User} from "./user.js"; +import {Member} from "./member.js"; +import {CDNParams} from "./utils/cdnParams.js"; +class PermissionToggle implements OptionsElement { readonly rolejson: { name: string; readableName: string; @@ -70,17 +151,13 @@ class PermissionToggle implements OptionsElement{ permissions: Permissions; owner: Options; value!: number; - constructor( - roleJSON: PermissionToggle["rolejson"], - permissions: Permissions, - owner: Options - ){ + constructor(roleJSON: PermissionToggle["rolejson"], permissions: Permissions, owner: Options) { this.rolejson = roleJSON; this.permissions = permissions; this.owner = owner; } - watchForChange(){} - generateHTML(): HTMLElement{ + watchForChange() {} + generateHTML(): HTMLElement { const div = document.createElement("div"); div.classList.add("setting"); const name = document.createElement("span"); @@ -94,304 +171,512 @@ class PermissionToggle implements OptionsElement{ div.appendChild(p); return div; } - generateCheckbox(): HTMLElement{ + generateCheckbox(): HTMLElement { + const rand = Math.random() + ""; const div = document.createElement("div"); div.classList.add("tritoggle"); const state = this.permissions.getPermission(this.rolejson.name); const on = document.createElement("input"); on.type = "radio"; - on.name = this.rolejson.name; + on.name = this.rolejson.name + rand; div.append(on); - if(state === 1){ + if (state === 1) { on.checked = true; } - on.onclick = _=>{ + on.onclick = (_) => { this.permissions.setPermission(this.rolejson.name, 1); this.owner.changed(); }; const no = document.createElement("input"); no.type = "radio"; - no.name = this.rolejson.name; + no.name = this.rolejson.name + rand; div.append(no); - if(state === 0){ + if (state === 0) { no.checked = true; } - no.onclick = _=>{ + no.onclick = (_) => { this.permissions.setPermission(this.rolejson.name, 0); this.owner.changed(); }; - if(this.permissions.hasDeny){ + if (this.permissions.hasDeny) { const off = document.createElement("input"); off.type = "radio"; - off.name = this.rolejson.name; + off.name = this.rolejson.name + rand; div.append(off); - if(state === -1){ + if (state === -1) { off.checked = true; } - off.onclick = _=>{ + off.onclick = (_) => { this.permissions.setPermission(this.rolejson.name, -1); this.owner.changed(); }; } return div; } - submit(){} + submit() {} } -import{ OptionsElement, Buttons }from"./settings.js"; -import { Contextmenu } from "./contextmenu.js"; -import { Channel } from "./channel.js"; -import { I18n } from "./i18n.js"; -class RoleList extends Buttons{ - permissions: [Role, Permissions][]; + +class RoleList extends Buttons { + permissions: [Role | User, Permissions][]; permission: Permissions; readonly guild: Guild; - readonly channel: false|Channel; + readonly channel: false | Channel; declare buttons: [string, string][]; readonly options: Options; - onchange: Function; + onchange: (id: string, perms: Permissions) => void; curid?: string; - get info(){ + get info() { return this.guild.info; } - get headers(){ + get headers() { return this.guild.headers; } - constructor(permissions:[Role, Permissions][], guild:Guild, onchange:Function, channel:false|Channel){ + constructor( + permissions: [Role | User, Permissions][], + guild: Guild, + onchange: (id: string, perms: Permissions) => void, + channel: false | Channel, + ) { super(""); this.guild = guild; this.permissions = permissions; this.channel = channel; this.onchange = onchange; const options = new Options("", this); - if(channel){ + if (channel) { this.permission = new Permissions("0", "0"); - }else{ + } else { this.permission = new Permissions("0"); } - this.makeguildmenus(options); - for(const thing of Permissions.info()){ - options.options.push( - new PermissionToggle(thing, this.permission, options) - ); + //TODO maybe make channels work correctly with this (role permissions aren't currently saved) + if (!channel) { + this.makeguildmenus(options); + } + for (const thing of Permissions.info()) { + options.options.push(new PermissionToggle(thing, this.permission, options)); } - for(const i of permissions){ + if (!channel) { + options.addButtonInput("", I18n.role.delete(), () => { + const role = this.permissions.find((_) => _[0].id === this.curid)?.[0]; + if (role) this.deleteRole(role); + }); + } + for (const i of permissions) { this.buttons.push([i[0].name, i[0].id]); } this.options = options; - guild.roleUpdate=this.groleUpdate.bind(this); - if(channel){ - channel.croleUpdate=this.croleUpdate.bind(this); + guild.roleUpdate = this.groleUpdate.bind(this); + if (channel) { + channel.croleUpdate = this.croleUpdate.bind(this); } } - private groleUpdate(role:Role,added:1|0|-1){ - if(!this.channel){ - if(added===1){ - this.permissions.push([role,role.permissions]); + private groleUpdate(role: Role, added: 1 | 0 | -1) { + if (!this.channel) { + if (added === 1) { + this.permissions.push([role, role.permissions]); } } - if(added===-1){ - this.permissions=this.permissions.filter(r=>r[0]!==role); + if (added === -1) { + this.permissions = this.permissions.filter((r) => r[0] !== role); } this.redoButtons(); } - private croleUpdate(role:Role,perm:Permissions,added:boolean){ - if(added){ - this.permissions.push([role,perm]) - }else{ - this.permissions=this.permissions.filter(r=>r[0]!==role); + private croleUpdate(role: Role | User, perm: Permissions, added: boolean) { + if (added) { + this.permissions.push([role, perm]); + } else { + this.permissions = this.permissions.filter((r) => r[0] !== role); } this.redoButtons(); } - makeguildmenus(option:Options){ - option.addButtonInput("",I18n.getTranslation("role.displaySettings"),()=>{ - const role=this.guild.roleids.get(this.curid as string); - if(!role) return; - const form=option.addSubForm(I18n.getTranslation("role.displaySettings"),()=>{},{ - fetchURL:this.info.api+"/guilds/"+this.guild.id+"/roles/"+this.curid, - method:"PATCH", - headers:this.headers, - traditionalSubmit:true + makeguildmenus(option: Options) { + option.addButtonInput("", I18n.role.displaySettings(), () => { + const r = this.guild.roleids.get(this.curid as string); + if (!r) return; + const role = r; + const form = option.addSubForm( + I18n.role.displaySettings(), + (e) => { + if ("name" in e && typeof e.name === "string") { + option.name = e.name; + } + option.subOptions = undefined; + option.genTop(); + }, + { + fetchURL: this.info.api + "/guilds/" + this.guild.id + "/roles/" + this.curid, + method: "PATCH", + headers: this.headers, + traditionalSubmit: true, + }, + ); + form.addTextInput(I18n.role.name(), "name", { + initText: role.name, }); - form.addTextInput(I18n.getTranslation("role.name"),"name",{ - initText:role.name + form.addCheckboxInput(I18n.role.hoisted(), "hoist", { + initState: role.hoist, }); - form.addCheckboxInput(I18n.getTranslation("role.hoisted"),"hoist",{ - initState:role.hoist + form.addCheckboxInput(I18n.role.mentionable(), "mentionable", { + initState: role.mentionable, }); - form.addCheckboxInput(I18n.getTranslation("role.mentionable"),"mentionable",{ - initState:role.mentionable + let count: number; + if (role.colors) { + count = role.colors.secondary_color ? (role.colors.tertiary_color ? 3 : 2) : 1; + } else { + count = 1; + } + + form.options + .addSelect( + I18n.role.colors.name(), + () => {}, + (["one", "two", "three"] as const).map((_) => I18n.role.colors[_]()), + { + defaultIndex: count - 1, + }, + ) + .watchForChange((_) => { + count = _ + 1; + colorOptGen(); + }); + + const color = "#" + role.color.toString(16).padStart(6, "0"); + const colorI = form.addColorInput(I18n.role.color(), "color", { + initColor: color, }); - const color="#"+role.color.toString(16).padStart(6,"0"); - form.addColorInput(I18n.getTranslation("role.color"),"color",{ - initColor:color + + const opt = form.addOptions(""); + + const c2 = + role.colors?.secondary_color !== undefined && role.colors?.secondary_color !== null + ? Role.numberToColor(role.colors.secondary_color) + : color; + const c3 = + role.colors?.tertiary_color !== undefined && role.colors?.tertiary_color !== null + ? Role.numberToColor(role.colors.tertiary_color) + : color; + + const colors = [color, c2, c3]; + + let colorInputs: ColorInput[] = [colorI]; + function colorOptGen() { + colorInputs = [colorInputs[0]]; + opt.removeAll(); + + if (count >= 2) { + colorInputs[1] = opt.addColorInput(I18n.role.colors.secondColor(), () => {}, { + initColor: colors[1], + }); + colorInputs[1].watchForChange((_) => { + colors[1] = _; + }); + } + if (count === 3) { + colorInputs[2] = opt.addColorInput(I18n.role.colors.thirdColor(), () => {}, { + initColor: colors[2], + }); + + colorInputs[2].watchForChange((_) => { + colors[2] = _; + }); + } + } + colorOptGen(); + + form.addEmojiInput(I18n.role.roleEmoji(), "unicode_emoji", this.guild.localuser, { + initEmoji: role.unicode_emoji + ? new Emoji( + { + name: "Emoji", + emoji: role.unicode_emoji, + }, + undefined, + ) + : undefined, + required: false, + clear: true, + guild: false, + }); + form.addImageInput(I18n.role.roleFileIcon(), "icon", { + initImg: role.icon + ? role.info.cdn + + "/role-icons/" + + role.id + + "/" + + role.icon + + ".webp" + + new CDNParams({expectedSize: 32}) + : "", + clear: true, }); - form.addPreprocessor((obj:any)=>{ - obj.color=Number("0x"+obj.color.substring(1)); - console.log(obj.color); - }) - }) + form.addPreprocessor((obj: any) => { + obj.color = Number("0x" + colorI.colorContent.substring(1)); + obj.permissions = this.permission.allow.toString(); + obj.colors = { + primary_color: obj.color, + secondary_color: colorInputs[1] ? Number("0x" + colors[1].substring(1)) : undefined, + tertiary_color: colorInputs[2] ? Number("0x" + colors[2].substring(1)) : undefined, + }; + }); + }); } - static channelrolemenu=this.ChannelRoleMenu(); - static guildrolemenu=this.GuildRoleMenu(); - private static ChannelRoleMenu(){ - const menu=new Contextmenu("role settings"); - menu.addbutton(()=>I18n.getTranslation("role.remove"),function(role){ - if(!this.channel) return; - console.log(role); - fetch(this.info.api+"/channels/"+this.channel.id+"/permissions/"+role.id,{ - method:"DELETE", - headers:this.headers - }) - },null); + static channelrolemenu = this.ChannelRoleMenu(); + static guildrolemenu = this.GuildRoleMenu(); + private static ChannelRoleMenu() { + const menu = new Contextmenu("role settings"); + menu.addButton( + function (user) { + if (user instanceof User) { + return I18n.user.remove(); + } + return I18n.role.remove(); + }, + function (role) { + if (!this.channel) return; + console.log(role); + fetch(this.info.api + "/channels/" + this.channel.id + "/permissions/" + role.id, { + method: "DELETE", + headers: this.headers, + }); + }, + { + visible: function (role) { + //TODO, maybe this needs a check if the user is above/bellow the other user, hard to say + return role.id !== this.guild.id; + }, + }, + ); + menu.addButton( + function (user) { + if (user instanceof User) { + return I18n.user.copyId(); + } + return I18n.role.copyId(); + }, + function (role) { + navigator.clipboard.writeText(role.id); + }, + ); return menu; } - private static GuildRoleMenu(){ - const menu=new Contextmenu("role settings"); - menu.addbutton(()=>I18n.getTranslation("role.delete"),function(role){ - if(!confirm(I18n.getTranslation("role.confirmDelete"))) return; - console.log(role); - fetch(this.info.api+"/guilds/"+this.guild.id+"/roles/"+role.id,{ - method:"DELETE", - headers:this.headers - }) - },null); + + deleteRole(role: Role | User) { + const dio = new Dialog(I18n.role.confirmDelete(role.name)); + const opt = dio.options.addOptions("", {ltr: true}); + opt.addButtonInput("", I18n.yes(), async () => { + opt.removeAll(); + opt.addText(I18n.role.deleting()); + await fetch(role.info.api + "/guilds/" + this.guild.id + "/roles/" + role.id, { + method: "DELETE", + headers: this.guild.headers, + }); + if (this.curid === role.id) { + const id = this.permissions.filter((_) => _[0].id !== role.id)[0][0].id; + const elm = this.htmlarea.deref(); + if (elm) this.generateHTMLArea(id, elm); + } + dio.hide(); + }); + opt.addButtonInput("", I18n.no(), () => { + dio.hide(); + }); + dio.show(); + } + private static GuildRoleMenu() { + const menu = new Contextmenu("role settings"); + menu.addButton( + () => I18n.role.delete(), + function (role) { + this.deleteRole(role); + }, + { + visible: (role) => role.id !== role.guild.id, + }, + ); + menu.addButton( + function (user) { + if (user instanceof User) { + return I18n.user.copyId(); + } + return I18n.role.copyId(); + }, + function (role) { + navigator.clipboard.writeText(role.id); + }, + ); return menu; } - redoButtons(){ - this.buttons=[]; - this.permissions.sort(([a],[b])=>b.position-a.position); - for(const i of this.permissions){ + redoButtons() { + this.buttons = []; + this.permissions.sort(([a], [b]) => { + if (b instanceof User) return 1; + if (a instanceof User) return -1; + return b.position - a.position; + }); + for (const i of this.permissions) { this.buttons.push([i[0].name, i[0].id]); } - console.log("in here :P") - if(!this.buttonList)return; - console.log("in here :P"); - const elms=Array.from(this.buttonList.children); - const div=elms[0] as HTMLDivElement; - const div2=elms[1] as HTMLDivElement; + if (!this.buttonList) return; + const elms = Array.from(this.buttonList.children); + const div = elms[0] as HTMLDivElement; + const div2 = elms[1] as HTMLDivElement; console.log(div); - div.innerHTML=""; - div.append(this.buttonListGen(div2));//not actually sure why the html is needed + div.innerHTML = ""; + div.append(this.buttonListGen(div2)); //not actually sure why the html is needed } - buttonMap=new WeakMap(); - dragged?:HTMLButtonElement; - buttonDragEvents(button:HTMLButtonElement,role:Role){ - this.buttonMap.set(button,role); - button.addEventListener("dragstart", e=>{ + buttonRoleMap = new WeakMap(); + dragged?: HTMLButtonElement; + buttonDragEvents(button: HTMLButtonElement, role: Role) { + const height = 35; + this.buttonRoleMap.set(button, role); + button.addEventListener("dragstart", (e) => { this.dragged = button; e.stopImmediatePropagation(); }); - button.addEventListener("dragend", ()=>{ + button.addEventListener("dragend", () => { this.dragged = undefined; }); - button.addEventListener("dragenter", event=>{ - console.log("enter"); + button.addEventListener("dragenter", (event) => { event.preventDefault(); return true; }); - button.addEventListener("dragover", event=>{ + button.addEventListener("dragover", (event) => { event.preventDefault(); + if (event.offsetY / height < 0.5) { + button.classList.add("dragTopView"); + button.classList.remove("dragBottomView"); + } else { + button.classList.remove("dragTopView"); + button.classList.add("dragBottomView"); + } return true; }); + button.addEventListener("dragleave", () => { + button.classList.remove("dragTopView"); + button.classList.remove("dragBottomView"); + }); + + button.addEventListener("drop", (event) => { + const role2 = this.buttonRoleMap.get(this.dragged as HTMLButtonElement); + if (!role2 || this.dragged === button) return; + const index2 = this.guild.roles.indexOf(role2); + this.guild.roles.splice(index2, 1); + const index = this.guild.roles.indexOf(role); + if (event.offsetY / height < 0.5) { + this.guild.roles.splice(index, 0, role2); + } else { + this.guild.roles.splice(index + 1, 0, role2); + } - button.addEventListener("drop", _=>{ - const role2=this.buttonMap.get(this.dragged as HTMLButtonElement); - if(!role2) return; - const index2=this.guild.roles.indexOf(role2); - this.guild.roles.splice(index2,1); - const index=this.guild.roles.indexOf(role); - this.guild.roles.splice(index+1,0,role2); this.guild.recalcRoles(); console.log(role); }); } - buttonListGen(html:HTMLElement){ - const buttonTable=document.createElement("div"); + buttonListGen(html: HTMLElement) { + const buttonTable = document.createElement("div"); buttonTable.classList.add("flexttb"); - const roleRow=document.createElement("div"); - roleRow.classList.add("flexltr","rolesheader"); - roleRow.append("Roles"); - const add=document.createElement("span"); - add.classList.add("svg-plus","svgicon","addrole"); - add.onclick=async (e)=>{ - const box=add.getBoundingClientRect(); + const roleRow = document.createElement("div"); + roleRow.classList.add("flexltr", "rolesheader"); + roleRow.append(this.channel ? I18n.role.perms() : I18n.role.roles()); + const add = document.createElement("span"); + add.classList.add("svg-plus", "svgicon", "addrole"); + + add.onclick = async (e) => { + const box = add.getBoundingClientRect(); e.stopPropagation(); - if(this.channel){ - const roles:[Role,string[]][]=[]; - for(const role of this.guild.roles){ - if(this.permissions.find(r=>r[0]==role)){ + if (this.channel) { + const roles: [Role, string[]][] = []; + for (const role of this.guild.roles) { + if (this.permissions.find((r) => r[0] == role)) { continue; } - roles.push([role,[role.name]]); + roles.push([role, [role.name]]); } - const search=new Search(roles); - - const found=await search.find(box.left,box.top); + const search = new Search(roles, async (str) => { + const users = (await this.guild.searchMembers(3, str)) + .map((_) => _.user) + .map((_) => [_.name, _] as [string, User]); + console.log(users); + return users; + }); + const found = await search.find(box.left, box.top); - if(!found) return; + if (!found) return; console.log(found); - this.onchange(found.id,new Permissions("0","0")); - }else{ - const div=document.createElement("div"); - const bar=document.createElement("input"); - div.classList.add("fixedsearch","OptionList"); - bar.type="text"; - div.style.left=(box.left^0)+"px"; - div.style.top=(box.top^0)+"px"; - div.append(bar) + this.onchange(found.id, new Permissions("0", "0")); + } else { + const div = document.createElement("div"); + const bar = document.createElement("input"); + div.classList.add("fixedsearch", "OptionList"); + bar.type = "text"; + div.style.left = (box.left ^ 0) + "px"; + div.style.top = (box.top ^ 0) + "px"; + div.append(bar); document.body.append(div); - if(Contextmenu.currentmenu != ""){ - Contextmenu.currentmenu.remove(); - } - Contextmenu.currentmenu=div; + Contextmenu.declareMenu(div); Contextmenu.keepOnScreen(div); - bar.onchange=()=>{ + bar.onchange = () => { div.remove(); - console.log(bar.value) - if(bar.value==="") return; - fetch(this.info.api+`/guilds/${this.guild.id}/roles`,{ - method:"POST", - headers:this.headers, - body:JSON.stringify({ - color:0, - name:bar.value, - permissions:"" - }) - }) - } + console.log(bar.value); + if (bar.value === "") return; + fetch(this.info.api + `/guilds/${this.guild.id}/roles`, { + method: "POST", + headers: this.headers, + body: JSON.stringify({ + color: 0, + name: bar.value, + permissions: "", + }), + }); + }; } - } + }; roleRow.append(add); buttonTable.append(roleRow); - for(const thing of this.buttons){ + for (const thing of this.buttons) { const button = document.createElement("button"); + + this.buttonMap.set(thing[0], button); button.classList.add("SettingsButton"); - button.textContent = thing[0]; - const role=this.guild.roleids.get(thing[1]); - if(role){ - if(!this.channel){ - if(role.canManage()){ - this.buttonDragEvents(button,role); - button.draggable=true; - RoleList.guildrolemenu.bindContextmenu(button,this,role) + const span = document.createElement("span"); + span.textContent = thing[0]; + button.append(span); + span.classList.add("roleButtonStyle"); + const role = this.guild.roleids.get(thing[1]) || this.guild.localuser.userMap.get(thing[1]); + if (role) { + if (role instanceof Role) { + if (role.getColor()) button.style.setProperty("--user-bg", `var(--role-${role.id})`); + } else { + Member.resolveMember(role, this.guild).then((_) => { + if (!_) return; + const style = _.getColorStyle(); + if (style) button.style.setProperty("--user-bg", style); + }); + } + if (!this.channel) { + if (role instanceof Role && role.canManage()) { + this.buttonDragEvents(button, role); + button.draggable = true; + RoleList.guildrolemenu.bindContextmenu(button, this, role); } - }else{ - if(role.canManage()){ - RoleList.channelrolemenu.bindContextmenu(button,this,role) + } else { + if (role instanceof User || role.canManage()) { + RoleList.channelrolemenu.bindContextmenu(button, this, role); } } } - button.onclick = _=>{ + button.onclick = (_) => { + html.classList.remove("mobileHidden"); this.generateHTMLArea(thing[1], html); - if(this.warndiv){ + if (this.warndiv) { this.warndiv.remove(); } }; @@ -400,29 +685,31 @@ class RoleList extends Buttons{ return buttonTable; } - generateButtons(html:HTMLElement):HTMLDivElement{ + generateButtons(html: HTMLElement): HTMLDivElement { const div = document.createElement("div"); div.classList.add("settingbuttons"); div.append(this.buttonListGen(html)); return div; } - handleString(str: string): HTMLElement{ + handleString(str: string): HTMLElement { this.curid = str; - const arr = this.permissions.find(_=>_[0].id === str); - if(arr){ + const arr = this.permissions.find((_) => _[0].id === str); + if (arr) { const perm = arr[1]; this.permission.deny = perm.deny; this.permission.allow = perm.allow; - const role = this.permissions.find(e=>e[0].id === str); - if(role){ + const role = this.permissions.find((e) => e[0].id === str); + if (role) { this.options.name = role[0].name; this.options.haschanged = false; } } + this.options.subOptions = undefined; return this.options.generateHTML(); } - save(){ + save() { + if (this.options.subOptions || !this.curid) return; this.onchange(this.curid, this.permission); } } -export{ RoleList, PermissionToggle }; +export {RoleList, PermissionToggle}; diff --git a/src/webpage/search.ts b/src/webpage/search.ts index 00205c00..40ac6cfb 100644 --- a/src/webpage/search.ts +++ b/src/webpage/search.ts @@ -1,72 +1,99 @@ -import { Contextmenu } from "./contextmenu.js"; +import {Contextmenu} from "./contextmenu.js"; -class Search{ - options:Map; - readonly keys:string[]; - constructor(options:[E,string[]][]){ - const map=options.flatMap(e=>{ - const val=e[1].map(f=>[f,e[0]]); - return val as [string,E][]; - }) - this.options=new Map(map); - this.keys=[...this.options.keys()]; - } - generateList(str:string,max:number,res:(e:E)=>void){ - str=str.toLowerCase(); - const options=this.keys.filter(e=>{ - return e.toLowerCase().includes(str) - }); - const div=document.createElement("div"); - div.classList.add("OptionList","flexttb"); - for(const option of options.slice(0, max)){ - const hoption=document.createElement("span"); - hoption.textContent=option; - hoption.onclick=()=>{ - if(!this.options.has(option)) return; - res(this.options.get(option) as E) - } - div.append(hoption); - } - return div; - } - async find(x:number,y:number,max=4):Promise{ - return new Promise((res)=>{ +class Search { + options: Map; + readonly keys: string[]; + dynamic: (str: string) => Promise<[string, E][]> | [string, E][]; + constructor( + options: [E, string[]][], + dynamic: (str: string) => Promise<[string, E][]> | [string, E][] = async () => [], + ) { + const map = options.flatMap((e) => { + const val = e[1].map((f) => [f, e[0]]); + return val as [string, E][]; + }); + this.options = new Map(map); + this.keys = [...this.options.keys()]; + this.dynamic = dynamic; + } + generateList(str: string, max: number, res: (e: E) => void) { + str = str.toLowerCase(); + let options = this.keys.filter((e) => { + return e.toLowerCase().includes(str); + }); + const dyn = this.dynamic(str); - const container=document.createElement("div"); - container.classList.add("fixedsearch"); - console.log((x^0)+"",(y^0)+""); - container.style.left=(x^0)+"px"; - container.style.top=(y^0)+"px"; - const remove=container.remove; - container.remove=()=>{ - remove.call(container); - res(undefined); - } + const div = document.createElement("div"); + div.classList.add("OptionList", "flexttb"); - function resolve(e:E){ - res(e); - container.remove(); - } - const bar=document.createElement("input"); - const options=document.createElement("div"); - const keydown=()=>{ - const html=this.generateList(bar.value,max,resolve); - options.innerHTML=""; - options.append(html); - } - bar.oninput=keydown; - keydown(); - bar.type="text"; - container.append(bar); - container.append(options); - document.body.append(container); - if(Contextmenu.currentmenu != ""){ - Contextmenu.currentmenu.remove(); - } - Contextmenu.currentmenu=container; - Contextmenu.keepOnScreen(container); + const cmap = new Map(); + const genDiv = () => { + div.innerHTML = ""; + for (const option of options.slice(0, max)) { + const hoption = document.createElement("span"); + hoption.textContent = option; + hoption.onclick = () => { + if (cmap.has(option)) { + res(cmap.get(option) as E); + } + if (!this.options.has(option)) return; + res(this.options.get(option) as E); + }; + div.append(hoption); + } + }; + if (dyn instanceof Promise) { + genDiv(); + dyn.then((arr) => { + options = [...options, ...arr.map(([_]) => _)]; + for (const thing of arr) { + cmap.set(...thing); + } + genDiv(); + }); + } else { + const arr = dyn; + options = [...options, ...arr.map(([_]) => _)]; + for (const thing of arr) { + cmap.set(...thing); + } + genDiv(); + } + return div; + } + async find(x: number, y: number, max = 4): Promise { + return new Promise((res) => { + const container = document.createElement("div"); + container.classList.add("fixedsearch"); + console.log((x ^ 0) + "", (y ^ 0) + ""); + container.style.left = (x ^ 0) + "px"; + container.style.top = (y ^ 0) + "px"; + const remove = container.remove; + container.remove = () => { + remove.call(container); + res(undefined); + }; - }) - } + function resolve(e: E) { + res(e); + container.remove(); + } + const bar = document.createElement("input"); + const options = document.createElement("div"); + const keydown = () => { + const html = this.generateList(bar.value, max, resolve); + options.innerHTML = ""; + options.append(html); + }; + bar.oninput = keydown; + keydown(); + bar.type = "text"; + container.append(bar); + container.append(options); + document.body.append(container); + Contextmenu.declareMenu(container); + Contextmenu.keepOnScreen(container); + }); + } } export {Search}; diff --git a/src/webpage/service.ts b/src/webpage/service.ts index b2bf2881..11428c25 100644 --- a/src/webpage/service.ts +++ b/src/webpage/service.ts @@ -1,142 +1,408 @@ -function deleteoldcache(){ - caches.delete("cache"); - console.log("this ran :P"); +import {messageFrom, messageTo} from "./utils/serviceType"; + +async function deleteoldcache() { + await caches.delete("cache"); +} +type files = {[key: string]: string | files}; +async function getAllFiles() { + const files = await fetch("/files.json"); + const json: files = await files.json(); + return json; } +async function downloadAllFiles() { + async function cachePath(path: string, json: files) { + await Promise.all( + Object.entries(json).map(async ([name, thing]) => { + if (typeof thing === "string") { + const lpath = path + "/" + name; + if (lpath.endsWith(".map") && !dev) { + return; + } + const res = await fetch(lpath); + putInCache(new URL(path, self.location.origin), res); + } else { + await cachePath(path + "/" + name, thing); + } + }), + ); + } -async function putInCache(request: URL | RequestInfo, response: Response){ - console.log(request, response); - const cache = await caches.open("cache"); - console.log("Grabbed"); - try{ + const json = await getAllFiles(); + + cachePath("", json); +} +let swStorage = {} as Record; +async function swStorageGet() { + return JSON.parse( + (await (await (await caches.open("save")).match("/save"))?.text()) || "{}", + ) as Record; +} +swStorageGet().then(async (_) => { + function pathToTime(str: string): number { + return swStorage[str] ?? 0; + } + swStorage = _; + if (Object.keys(_).length === 0) { + caches.delete("cdn"); + } else { + const c = await caches.open("cdn"); + let stuff = (await cacheSize("cdn")).sort((a, b) => pathToTime(b[2]) - pathToTime(a[2])); + let size = stuff.reduce((c, l) => c + l[0], 0); + while (size > 100000000) { + const thing = stuff.pop()!; + c.delete(thing[2]); + size -= thing[0]; + } + } +}); +async function swStorageSave() { + const c = await caches.open("save"); + c.put("/save", new Response(JSON.stringify(swStorage))); +} +async function cacheSize(str: string) { + const c = await caches.open(str); + const keys = await c.keys(); + + const sum = ( + await Promise.all( + keys.map((req) => + c.match(req).then((res) => + res! + .clone() + .blob() + .then((b) => [b.size, res!, res!.url] as const), + ), + ), + ) + ).sort((a, b) => b[0] - a[0]); + return sum; +} +async function getFromCache(request: URL) { + request = new URL(request, self.location.href); + const port = rMap.get(request.host); + if (port) { + request.search = ""; + } + const cache = await caches.open(port ? "cdn" : "cache"); + if (port) { + swStorage[request + ""] = Date.now(); + swStorageSave(); + } + return cache.match(request); +} +async function putInCache(request: URL | string, response: Response) { + request = new URL(request, self.location.href); + const port = rMap.get(request.host); + if (port) { + request.search = ""; + } + const cache = await caches.open(port ? "cdn" : "cache"); + if (port) { + swStorage[request + ""] = Date.now(); + swStorageSave(); + } + try { console.log(await cache.put(request, response)); - }catch(error){ + } catch (error) { console.error(error); } } let lastcache: string; -self.addEventListener("activate", async ()=>{ +self.addEventListener("activate", async () => { console.log("Service Worker activated"); checkCache(); }); - -async function checkCache(){ - if(checkedrecently){ - return; +async function tryToClose() { + const portArr = [...ports]; + if (portArr.length) { + for (let i = 1; i < portArr.length; i++) { + portArr[i].postMessage({code: "closing"}); + } + portArr[0].postMessage({code: "close"}); + } else { + throw new Error("No Fermi clients connected?"); + } +} +function sendAll(message: messageFrom) { + for (const port of ports) { + port.postMessage(message); } - const promise = await caches.match("/getupdates"); - if(promise){ +} +async function checkCache() { + if (checkedrecently) { + return false; + } + const cache = await caches.open("cache"); + const promise = await cache.match("/getupdates"); + if (promise) { lastcache = await promise.text(); } console.log(lastcache); - fetch("/getupdates").then(async data=>{ - setTimeout((_: any)=>{ - checkedrecently = false; - }, 1000 * 60 * 30); - if(!data.ok) return; + return fetch("/getupdates").then(async (data) => { + setTimeout( + (_: any) => { + checkedrecently = false; + }, + 1000 * 60 * 30, + ); + if (!data.ok) return false; const text = await data.clone().text(); console.log(text, lastcache); - if(lastcache !== text){ + if (lastcache !== text) { deleteoldcache(); putInCache("/getupdates", data); + await downloadAllFiles(); + tryToClose(); + checkedrecently = true; + sendAll({ + code: "updates", + updates: true, + }); + return true; } checkedrecently = true; + return false; }); } var checkedrecently = false; -function samedomain(url: string | URL){ +function samedomain(url: string | URL) { return new URL(url).origin === self.origin; } -const htmlFiles=new Set(["/index","/login","/home","/register","/oauth2/auth"]); - - -function isHtml(url:string):string|void{ - const path=new URL(url).pathname; - if(htmlFiles.has(path)||htmlFiles.has(path+".html")){ - return path+path.endsWith(".html")?"":".html"; +let enabled = "false"; +let offline = false; +function toPathNoDefault(url: string) { + const Url = new URL(url); + let html: string | undefined = undefined; + const path = Url.pathname; + if (path.startsWith("/channels")) { + html = "./app"; + } else if (path.startsWith("/invite/") || path === "/invite") { + html = "./invite"; + } else if (path.startsWith("/template/") || path === "/template") { + html = "./template"; + } else if (path === "/") { + html = "./index"; } + return html; } -let enabled="false"; -let offline=false; - -function toPath(url:string):string{ - const Url= new URL(url); - let html=isHtml(url); - if(!html){ - const path=Url.pathname; - if(path.startsWith("/channels")){ - html="./index.html" - }else if(path.startsWith("/invite")){ - html="./invite.html" - } - } - return html||Url.pathname; +function toPath(url: string): string { + const Url = new URL(url); + return toPathNoDefault(url) || Url.pathname; } -let fails=0; -async function getfile(event: FetchEvent):Promise{ +let fails = 0; +async function getfile(req: Request): Promise { checkCache(); - if(!samedomain(event.request.url)||enabled==="false"||(enabled==="offlineOnly"&&!offline)){ - const responce=await fetch(event.request.clone()); - if(samedomain(event.request.url)){ - if(enabled==="offlineOnly"&&responce.ok){ - putInCache(toPath(event.request.url),responce.clone()); + if (!samedomain(req.url) || enabled === "false" || (enabled === "offlineOnly" && !offline)) { + const response = await fetch(req.clone()); + if (samedomain(req.url)) { + if (enabled === "offlineOnly" && response.ok) { + putInCache(toPath(req.url), response.clone()); } - if(!responce.ok){ + if (!response.ok) { fails++; - if(fails>5){ - offline=true; + if (fails > 5) { + offline = true; } } } - return responce; + return response; } - let path=toPath(event.request.url); - if(path === "/instances.json"){ + let path = toPath(req.url); + if (path === "/instances.json") { + //TODO the client shouldn't really even fetch this, it should just ask the SW for it return await fetch(path); } - console.log("Getting path: "+path); + console.log("Getting path: " + path); const responseFromCache = await caches.match(path); - if(responseFromCache){ + if (responseFromCache) { console.log("cache hit"); return responseFromCache; } - try{ + try { const responseFromNetwork = await fetch(path); - if(responseFromNetwork.ok){ + if (responseFromNetwork.ok) { await putInCache(path, responseFromNetwork.clone()); } return responseFromNetwork; - }catch(e){ + } catch (e) { console.error(e); return new Response(null); } } +const promURLMap = new Map void>(); +async function refreshUrl(url: URL, port: MessagePort): Promise { + port.postMessage({ + code: "refreshURL", + url: url.toString(), + }); + return new Promise((res) => promURLMap.set(url.toString(), res)); +} +self.addEventListener("fetch", async (e) => { + const event = e as FetchEvent; + const host = URL.canParse(event.request.url) && new URL(event.request.url).host; + let req = event.request; + + const port = rMap.get(host || ""); + if (port) { + const url = new URL(event.request.url); + const ignore = ["/api", "/_spacebar"].find((_) => url.pathname.startsWith(_)); + if (!ignore) { + const expired = + url.searchParams.get("ex") && + Number.parseInt(url.searchParams.get("ex") || "", 16) < Date.now() - 5000; + event.respondWith( + new Promise(async (res) => { + const cached = await getFromCache(url); + if (cached) { + res(cached); + return; + } + if (expired) { + const old = url; + const p = Date.now(); + req = await Promise.race([ + new Promise(async (res) => res(new Request(await refreshUrl(url, port), req))), + new Promise((res) => setTimeout(() => res(req), 5000)), + ]); + console.log(p - Date.now(), old === url); + } + const f = await fetch(req); + res(f); + putInCache(url, f.clone()); + }), + ); + return; + } + } + + if (apiHosts?.has(host || "")) { + try { + const response = await fetch(req.clone()); + try { + event.respondWith(response.clone()); + } catch {} + const json = await response.json(); + if (json._trace) { + sendAll({ + code: "trace", + trace: json._trace, + }); + } + } catch (e) { + console.error(e); + //Wasn't meant to be ig lol + } + return; + } -self.addEventListener("fetch", (e)=>{ - const event=e as FetchEvent; - try{ - event.respondWith(getfile(event)); - }catch(e){ + if (req.method === "POST") { + return; + } + if (new URL(req.url).pathname.startsWith("/api/")) { + return; + } + try { + event.respondWith(getfile(req)); + } catch (e) { console.error(e); } }); +const ports = new Set(); +let dev = false; +let apiHosts: Set | void; +const rMap = new Map(); +function listenToPort(port: MessagePort) { + function sendMessage(message: messageFrom) { + port.postMessage(message); + } + port.onmessage = async (e) => { + const data = e.data as messageTo; + switch (data.code) { + case "ping": { + sendMessage({ + code: "pong", + count: ports.size, + }); + break; + } + case "close": { + ports.delete(port); + break; + } + case "replace": { + //@ts-ignore-error Just the type or wrong or something + self.skipWaiting(); + break; + } + case "CheckUpdate": { + checkedrecently = false; + if (!(await checkCache())) { + sendMessage({ + code: "updates", + updates: false, + }); + } -self.addEventListener("message", (message)=>{ - const data=message.data; - switch(data.code){ + break; + } + case "isValid": { + sendMessage({code: "isValid", url: data.url, valid: !!toPathNoDefault(data.url)}); + break; + } + case "isDev": { + const refetch = !dev && data.dev; + dev = data.dev; + if (refetch) { + getAllFiles(); + } + break; + } + case "apiUrls": { + if (data.hosts) { + apiHosts = new Set(data.hosts); + } else { + apiHosts = undefined; + } + break; + } + case "canRefresh": { + rMap.set(data.host, port); + break; + } + case "refreshedUrl": { + const res = promURLMap.get(data.oldurl); + if (res) { + res(data.url); + promURLMap.delete(data.oldurl); + } + } + } + }; + port.addEventListener("close", () => { + ports.delete(port); + }); +} +self.addEventListener("message", (message) => { + const data = message.data; + switch (data.code) { case "setMode": - enabled=data.data; - break; - case "CheckUpdate": - checkedrecently=false; - checkCache(); + enabled = data.data; break; case "ForceClear": deleteoldcache(); break; + case "clearCdnCache": + caches.delete("cdn"); + break; + case "port": { + const port = data.port as MessagePort; + ports.add(port); + listenToPort(port); + } } -}) +}); diff --git a/src/webpage/settings.ts b/src/webpage/settings.ts index 9d306887..4c5c6a01 100644 --- a/src/webpage/settings.ts +++ b/src/webpage/settings.ts @@ -1,4 +1,15 @@ -import { I18n } from "./i18n.js"; +import { + checkInstance, + getInstances, + getStringURLMapPair, + instancefetch, + InstanceInfo, + removeAni, +} from "./utils/utils.js"; +import {Emoji} from "./emoji.js"; +import {I18n} from "./i18n.js"; +import {Localuser} from "./localuser.js"; +import {MarkDown} from "./markdown.js"; interface OptionsElement { // @@ -7,90 +18,119 @@ interface OptionsElement { readonly watchForChange: (func: (arg1: x) => void) => void; value: x; } - //future me stuff -class Buttons implements OptionsElement{ +//future me stuff +export class Buttons implements OptionsElement { readonly name: string; readonly buttons: [string, Options | string][]; + readonly buttonMap = new Map(); buttonList!: HTMLDivElement; warndiv!: HTMLElement; value: unknown; - top=false; - constructor(name: string,{top=false}={}){ - this.top=top; + top = false; + titles = true; + constructor(name: string, {top = false, titles = true} = {}) { + this.top = top; this.buttons = []; this.name = name; + this.titles = titles; } - add(name: string, thing?: Options | undefined){ - if(!thing){ - thing = new Options(name, this); + add(name: string, thing?: Options | undefined) { + if (!thing) { + thing = new Options(this.titles ? name : "", this); } - this.buttons.push([name, thing]); + const button = [name, thing] as [string, string | Options]; + this.buttons.push(button); + const htmlarea = this.htmlarea.deref(); + const buttonTable = this.buttonTable.deref(); + if (buttonTable && htmlarea) buttonTable.append(this.makeButtonHTML(button, htmlarea)); return thing; } - generateHTML(){ + htmlarea = new WeakRef(document.createElement("div")); + buttonTable = new WeakRef(document.createElement("div")); + generateHTML(hideButtons = false) { const buttonList = document.createElement("div"); buttonList.classList.add("Buttons"); - buttonList.classList.add(this.top?"flexttb":"flexltr"); + buttonList.classList.add(this.top ? "flexttb" : "flexltr"); this.buttonList = buttonList; const htmlarea = document.createElement("div"); - htmlarea.classList.add("flexgrow"); + htmlarea.classList.add("flexgrow", "settingsHTMLArea"); const buttonTable = this.generateButtons(htmlarea); - if(this.buttons[0]){ + if (this.buttons[0]) { this.generateHTMLArea(this.buttons[0][1], htmlarea); + if (!hideButtons) htmlarea.classList.add("mobileHidden"); } - buttonList.append(buttonTable); + if (!hideButtons) buttonList.append(buttonTable); buttonList.append(htmlarea); + this.htmlarea = new WeakRef(htmlarea); + this.buttonTable = new WeakRef(buttonTable); return buttonList; } - generateButtons(optionsArea:HTMLElement){ + makeButtonHTML(buttond: [string, string | Options], optionsArea: HTMLElement) { + const button = document.createElement("button"); + this.buttonMap.set(buttond[1], button); + button.classList.add("SettingsButton"); + button.textContent = buttond[0]; + button.onclick = (_) => { + this.generateHTMLArea(buttond[1], optionsArea); + optionsArea.classList.remove("mobileHidden"); + if (this.warndiv) { + this.warndiv.remove(); + } + }; + return button; + } + generateButtons(optionsArea: HTMLElement) { const buttonTable = document.createElement("div"); buttonTable.classList.add("settingbuttons"); - if(this.top){ + if (this.top) { buttonTable.classList.add("flexltr"); } - for(const thing of this.buttons){ - const button = document.createElement("button"); - button.classList.add("SettingsButton"); - button.textContent = thing[0]; - button.onclick = _=>{ - this.generateHTMLArea(thing[1], optionsArea); - if(this.warndiv){ - this.warndiv.remove(); - } - }; - buttonTable.append(button); + for (const thing of this.buttons) { + buttonTable.append(this.makeButtonHTML(thing, optionsArea)); } return buttonTable; } - handleString(str: string): HTMLElement{ + handleString(str: string): HTMLElement { const div = document.createElement("span"); div.textContent = str; return div; } - generateHTMLArea( - buttonInfo: Options | string, - htmlarea: HTMLElement - ){ + last?: Options | string; + generateHTMLArea(buttonInfo: Options | string, htmlarea: HTMLElement) { + if (this.last) { + const elm = this.buttonMap.get(this.last); + if (elm) { + elm.classList.remove("activeSetting"); + } + } + this.last = buttonInfo; + const elm = this.buttonMap.get(buttonInfo); + if (elm) { + elm.classList.add("activeSetting"); + } let html: HTMLElement; - if(buttonInfo instanceof Options){ + if (buttonInfo instanceof Options) { buttonInfo.subOptions = undefined; html = buttonInfo.generateHTML(); - }else{ + } else { html = this.handleString(buttonInfo); } + htmlarea.innerHTML = ""; htmlarea.append(html); + + return html; } - changed(html: HTMLElement){ + changed(html: HTMLElement) { this.warndiv = html; this.buttonList.append(html); } - watchForChange(){} - save(){} - submit(){} + watchForChange() {} + save() {} + submit() {} } -class TextInput implements OptionsElement{ +class TextInput implements OptionsElement { readonly label: string; readonly owner: Options; readonly onSubmit: (str: string) => void; @@ -101,15 +141,15 @@ class TextInput implements OptionsElement{ label: string, onSubmit: (str: string) => void, owner: Options, - { initText = "", password = false } = {} - ){ + {initText = "", password = false} = {}, + ) { this.label = label; this.value = initText; this.owner = owner; this.onSubmit = onSubmit; this.password = password; } - generateHTML(): HTMLDivElement{ + generateHTML(): HTMLDivElement { const div = document.createElement("div"); const span = document.createElement("span"); span.textContent = this.label; @@ -122,65 +162,108 @@ class TextInput implements OptionsElement{ div.append(input); return div; } - private onChange(){ + onChange() { this.owner.changed(); const input = this.input.deref(); - if(input){ + if (input) { const value = input.value as string; this.onchange(value); this.value = value; } } - onchange: (str: string) => void = _=>{}; - watchForChange(func: (str: string) => void){ + onchange: (str: string) => void = (_) => {}; + watchForChange(func: (str: string) => void) { this.onchange = func; } - submit(){ + submit() { this.onSubmit(this.value); } } +class DateInput extends TextInput { + generateHTML(): HTMLDivElement { + const div = document.createElement("div"); + const span = document.createElement("span"); + span.textContent = this.label; + div.append(span); + const input = document.createElement("input"); + input.value = this.value; + input.type = "date"; + input.oninput = this.onChange.bind(this); + this.input = new WeakRef(input); + div.append(input); + return div; + } +} +class SettingsMDText implements OptionsElement { + readonly onSubmit!: (str: string) => void; + value!: void; + text: MarkDown; + elm!: WeakRef; + constructor(text: MarkDown) { + this.text = text; + } + generateHTML(): HTMLSpanElement { + const span = document.createElement("span"); + this.elm = new WeakRef(span); + this.setText(this.text); + return span; + } + setText(text: MarkDown) { + this.text = text; + if (this.elm) { + const span = this.elm.deref(); + if (span) { + span.innerHTML = ""; -class SettingsText implements OptionsElement{ + span.append(text.makeHTML()); + } + } + } + watchForChange() {} + submit() {} +} + +class SettingsText implements OptionsElement { readonly onSubmit!: (str: string) => void; value!: void; readonly text: string; - elm!:WeakRef; - constructor(text: string){ + elm!: WeakRef; + constructor(text: string) { this.text = text; } - generateHTML(): HTMLSpanElement{ + generateHTML(): HTMLSpanElement { const span = document.createElement("span"); span.innerText = this.text; - this.elm=new WeakRef(span); + this.elm = new WeakRef(span); return span; } - setText(text:string){ - if(this.elm){ - const span=this.elm.deref(); - if(span){ - span.innerText=text; + setText(text: string) { + if (this.elm) { + const span = this.elm.deref(); + if (span) { + span.innerText = text; } } } - watchForChange(){} - submit(){} + watchForChange() {} + submit() {} } -class SettingsTitle implements OptionsElement{ +class SettingsTitle implements OptionsElement { readonly onSubmit!: (str: string) => void; value!: void; readonly text: string; - constructor(text: string){ + constructor(text: string) { this.text = text; } - generateHTML(): HTMLSpanElement{ + generateHTML(): HTMLSpanElement { const span = document.createElement("h2"); span.innerText = this.text; return span; } - watchForChange(){} - submit(){} + watchForChange() {} + submit() {} } -class CheckboxInput implements OptionsElement{ +class CheckboxInput implements OptionsElement { readonly label: string; readonly owner: Options; readonly onSubmit: (str: boolean) => void; @@ -190,14 +273,14 @@ class CheckboxInput implements OptionsElement{ label: string, onSubmit: (str: boolean) => void, owner: Options, - { initState = false } = {} - ){ + {initState = false} = {}, + ) { this.label = label; this.value = initState; this.owner = owner; this.onSubmit = onSubmit; } - generateHTML(): HTMLDivElement{ + generateHTML(): HTMLDivElement { const div = document.createElement("div"); const span = document.createElement("span"); span.textContent = this.label; @@ -210,54 +293,49 @@ class CheckboxInput implements OptionsElement{ div.append(input); return div; } - private onChange(){ + private onChange() { this.owner.changed(); const input = this.input.deref(); - if(input){ + if (input) { const value = input.checked as boolean; this.value = value; this.onchange(value); } } - setState(state:boolean){ - if(this.input){ - const checkbox=this.input.deref(); - if(checkbox){ - checkbox.checked=state; - this.value=state; + setState(state: boolean) { + if (this.input) { + const checkbox = this.input.deref(); + if (checkbox) { + checkbox.checked = state; + this.value = state; } } } - onchange: (str: boolean) => void = _=>{}; - watchForChange(func: (str: boolean) => void){ + onchange: (str: boolean) => void = (_) => {}; + watchForChange(func: (str: boolean) => void) { this.onchange = func; } - submit(){ + submit() { this.onSubmit(this.value); } } -class ButtonInput implements OptionsElement{ +class ButtonInput implements OptionsElement { readonly label: string; readonly owner: Options; readonly onClick: () => void; textContent: string; value!: void; - constructor( - label: string, - textContent: string, - onClick: () => void, - owner: Options, - {} = {} - ){ + constructor(label: string, textContent: string, onClick: () => void, owner: Options, {} = {}) { this.label = label; this.owner = owner; this.onClick = onClick; this.textContent = textContent; } - generateHTML(): HTMLDivElement{ + buttonHtml?: HTMLButtonElement; + generateHTML(): HTMLDivElement { const div = document.createElement("div"); - if(this.label){ + if (this.label) { const span = document.createElement("span"); span.classList.add("inlinelabel"); span.textContent = this.label; @@ -266,35 +344,37 @@ class ButtonInput implements OptionsElement{ const button = document.createElement("button"); button.textContent = this.textContent; button.onclick = this.onClickEvent.bind(this); + this.buttonHtml = button; div.append(button); return div; } - private onClickEvent(){ + private onClickEvent() { this.onClick(); } - watchForChange(){} - submit(){} + watchForChange() {} + submit() {} } -class ColorInput implements OptionsElement{ +export class ColorInput implements OptionsElement { readonly label: string; readonly owner: Options; readonly onSubmit: (str: string) => void; colorContent: string; input!: WeakRef; - value!: string; + value: string; constructor( label: string, onSubmit: (str: string) => void, owner: Options, - { initColor = "" } = {} - ){ + {initColor = "#000000"} = {}, + ) { this.label = label; this.colorContent = initColor; + this.value = initColor; this.owner = owner; this.onSubmit = onSubmit; } - generateHTML(): HTMLDivElement{ + generateHTML(): HTMLDivElement { const div = document.createElement("div"); const span = document.createElement("span"); span.textContent = this.label; @@ -307,64 +387,64 @@ class ColorInput implements OptionsElement{ div.append(input); return div; } - private onChange(){ + private onChange() { this.owner.changed(); const input = this.input.deref(); - if(input){ + if (input) { const value = input.value as string; this.value = value; this.onchange(value); this.colorContent = value; } } - onchange: (str: string) => void = _=>{}; - watchForChange(func: (str: string) => void){ + onchange: (str: string) => void = (_) => {}; + watchForChange(func: (str: string) => void) { this.onchange = func; } - submit(){ + submit() { this.onSubmit(this.colorContent); } } -class SelectInput implements OptionsElement{ +class SelectInput implements OptionsElement { readonly label: string; readonly owner: Options; readonly onSubmit: (str: number) => void; - options: string[]; + options: readonly string[]; index: number; select!: WeakRef; - radio:boolean; - get value(){ + radio: boolean; + get value() { return this.index; } constructor( label: string, onSubmit: (str: number) => void, - options: string[], + options: readonly string[], owner: Options, - { defaultIndex = 0,radio=false } = {} - ){ + {defaultIndex = 0, radio = false} = {}, + ) { this.label = label; this.index = defaultIndex; this.owner = owner; this.onSubmit = onSubmit; this.options = options; - this.radio=radio; + this.radio = radio; } - generateHTML(): HTMLDivElement{ - if(this.radio){ - const map=new WeakMap(); + generateHTML(): HTMLDivElement { + if (this.radio) { + const map = new WeakMap(); const div = document.createElement("div"); const fieldset = document.createElement("fieldset"); - fieldset.addEventListener("change", ()=>{ + fieldset.addEventListener("change", () => { let i = -1; - for(const thing of Array.from(fieldset.children)){ + for (const thing of Array.from(fieldset.children)) { i++; - if(i === 0){ + if (i === 0) { continue; } const checkbox = thing.children[0].children[0] as HTMLInputElement; - if(checkbox.checked){ + if (checkbox.checked) { this.onChange(map.get(checkbox)); } } @@ -373,15 +453,15 @@ class SelectInput implements OptionsElement{ legend.textContent = this.label; fieldset.appendChild(legend); let i = 0; - for(const thing of this.options){ + for (const thing of this.options) { const div = document.createElement("div"); const input = document.createElement("input"); input.classList.add("radio"); input.type = "radio"; input.name = this.label; input.value = thing; - map.set(input,i); - if(i === this.index){ + map.set(input, i); + if (i === this.index) { input.checked = true; } const label = document.createElement("label"); @@ -405,8 +485,8 @@ class SelectInput implements OptionsElement{ selectSpan.classList.add("selectspan"); const select = document.createElement("select"); - select.onchange = this.onChange.bind(this,-1); - for(const thing of this.options){ + select.onchange = this.onChange.bind(this, -1); + for (const thing of this.options) { const option = document.createElement("option"); option.textContent = thing; select.appendChild(option); @@ -415,34 +495,34 @@ class SelectInput implements OptionsElement{ select.selectedIndex = this.index; selectSpan.append(select); const selectArrow = document.createElement("span"); - selectArrow.classList.add("svgicon","svg-category","selectarrow"); + selectArrow.classList.add("svgicon", "svg-category", "selectarrow"); selectSpan.append(selectArrow); div.append(selectSpan); return div; } - private onChange(index=-1){ + private onChange(index = -1) { this.owner.changed(); - if(index!==-1){ - this.onchange(index); + if (index !== -1) { this.index = index; + this.onchange(index); return; } const select = this.select.deref(); - if(select){ + if (select) { const value = select.selectedIndex; this.onchange(value); this.index = value; } } - onchange: (str: number) => void = _=>{}; - watchForChange(func: (str: number) => void){ + onchange: (str: number) => void = (_) => {}; + watchForChange(func: (str: number) => void) { this.onchange = func; } - submit(){ + submit() { this.onSubmit(this.index); } } -class MDInput implements OptionsElement{ +class MDInput implements OptionsElement { readonly label: string; readonly owner: Options; readonly onSubmit: (str: string) => void; @@ -452,14 +532,14 @@ class MDInput implements OptionsElement{ label: string, onSubmit: (str: string) => void, owner: Options, - { initText = "" } = {} - ){ + {initText = ""} = {}, + ) { this.label = label; this.value = initText; this.owner = owner; this.onSubmit = onSubmit; } - generateHTML(): HTMLDivElement{ + generateHTML(): HTMLDivElement { const div = document.createElement("div"); const span = document.createElement("span"); span.textContent = this.label; @@ -472,58 +552,144 @@ class MDInput implements OptionsElement{ div.append(input); return div; } - onChange(){ + onChange() { this.owner.changed(); const input = this.input.deref(); - if(input){ + if (input) { const value = input.value as string; this.onchange(value); this.value = value; } } - onchange: (str: string) => void = _=>{}; - watchForChange(func: (str: string) => void){ + onchange: (str: string) => void = (_) => {}; + watchForChange(func: (str: string) => void) { + this.onchange = func; + } + submit() { + this.onSubmit(this.value); + } +} +class EmojiInput implements OptionsElement { + readonly label: string; + readonly owner: Options; + readonly onSubmit: (str: Emoji | undefined | null) => void; + input!: WeakRef; + value!: Emoji | undefined | null; + localuser: Localuser; + clear: boolean; + guild: boolean; + constructor( + label: string, + onSubmit: (str: Emoji | undefined | null) => void, + owner: Options, + localuser: Localuser, + { + initEmoji = undefined, + clear = false, + guild = true, + }: {initEmoji?: undefined | Emoji; clear?: boolean; guild?: boolean} = {}, + ) { + this.label = label; + this.owner = owner; + this.guild = guild; + this.onSubmit = onSubmit; + this.value = initEmoji; + this.localuser = localuser; + this.clear = !!clear; + } + generateHTML(): HTMLElement { + const outDiv = document.createElement("div"); + outDiv.classList.add("flexltr"); + const div = document.createElement("div"); + div.classList.add("flexltr", "emojiForm"); + const label = document.createElement("span"); + label.textContent = this.label; + + let emoji: HTMLElement; + if (this.value) { + emoji = this.value.getHTML(); + } else { + emoji = document.createElement("span"); + emoji.classList.add("emptyEmoji"); + } + div.onclick = (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + (async () => { + const emj = await this.localuser.emojiPicker(e.x, e.y, this.guild); + if (emj) { + this.value = emj; + emoji.remove(); + emoji = emj.getHTML(); + div.append(emoji); + this.onchange(emj); + this.owner.changed(); + } + })(); + }; + div.append(label, emoji); + outDiv.append(div); + if (this.clear) { + const button = document.createElement("button"); + button.textContent = I18n.settings.clear(); + button.onclick = () => { + this.value = null; + emoji.remove(); + this.onchange(null); + this.owner.changed(); + emoji = document.createElement("span"); + emoji.classList.add("emptyEmoji"); + div.append(emoji); + }; + outDiv.append(button); + } + + return outDiv; + } + onchange = (_: Emoji | undefined | null) => {}; + watchForChange(func: (arg1: Emoji | undefined | null) => void) { this.onchange = func; } - submit(){ + submit() { this.onSubmit(this.value); } } -class FileInput implements OptionsElement{ + +class FileInput implements OptionsElement { readonly label: string; readonly owner: Options; readonly onSubmit: (str: FileList | null) => void; input!: WeakRef; - value!: FileList | null; + value: FileList | null | undefined = undefined; clear: boolean; constructor( label: string, onSubmit: (str: FileList | null) => void, owner: Options, - { clear = false } = {} - ){ + {clear = false} = {}, + ) { this.label = label; this.owner = owner; this.onSubmit = onSubmit; this.clear = clear; } - generateHTML(): HTMLDivElement{ + generateHTML(): HTMLDivElement { const div = document.createElement("div"); const span = document.createElement("span"); span.textContent = this.label; div.append(span); const innerDiv = document.createElement("div"); - innerDiv.classList.add("flexltr","fileinputdiv"); + innerDiv.classList.add("flexltr", "fileinputdiv"); const input = document.createElement("input"); input.type = "file"; input.oninput = this.onChange.bind(this); this.input = new WeakRef(input); innerDiv.append(input); - if(this.clear){ + if (this.clear) { const button = document.createElement("button"); - button.textContent = "Clear"; - button.onclick = _=>{ - if(this.onchange){ + button.textContent = I18n.settings.clear(); + button.onclick = (_) => { + if (this.onchange) { this.onchange(null); } this.value = null; @@ -534,164 +700,407 @@ class FileInput implements OptionsElement{ div.append(innerDiv); return div; } - onChange(){ + onChange() { this.owner.changed(); const input = this.input.deref(); - if(input){ + if (input) { this.value = input.files; - if(this.onchange){ + if (this.onchange) { this.onchange(input.files); } } } - onchange: ((str: FileList | null) => void) | null = null; - watchForChange(func: (str: FileList | null) => void){ + onchange: ((str: FileList | null | undefined) => void) | null = null; + watchForChange(func: (str: FileList | null | undefined) => void) { this.onchange = func; } - submit(){ + submit() { const input = this.input.deref(); - if(input){ + if (input) { this.onSubmit(input.files); } } } +class ImageInput extends FileInput { + img: HTMLElement; + constructor( + label: string, + onSubmit: (str: FileList | null) => void, + owner: Options, + {clear = false, initImg = "", width = -1, objectFit = ""} = {}, + ) { + super(label, onSubmit, owner, {clear}); + + console.log(initImg); + let hasimg = "" !== initImg; + const input = document.createElement("input"); + input.type = "file"; + input.oninput = this.onChange.bind(this); + this.input = new WeakRef(input); + input.accept = "image/*"; + const img = document.createElement("img"); + img.src = initImg; + + const button = document.createElement("button"); + button.textContent = I18n.settings.clear(); + button.onclick = (_) => { + img.src = ""; + if (this.onchange) { + this.onchange(null); + } + this.value = null; + this.owner.changed(); + hasimg = false; + genImg(); + }; + this.clearbutton = button; + + input.addEventListener("change", () => { + if (!input.files) return; + const reader = new FileReader(); + reader.onload = (imgf) => { + const res = imgf.target?.result; + if (!res) return; + if (typeof res !== "string") return; + img.src = res; + hasimg = true; + genImg(); + }; + reader.readAsDataURL(input.files[0]); + }); + const div = document.createElement("div"); + const span = document.createElement("span"); + span.textContent = I18n.settings.img(); + const genImg = () => { + console.warn(hasimg); + if (hasimg) { + div.append(img); + span.remove(); + } else { + div.append(span); + img.remove(); + } + }; + if (width !== -1) { + div.style.width = width + "px"; + img.style.width = width + "px"; + } + if (objectFit) img.style.objectFit = objectFit; + console.warn(objectFit); + this.img = div; + div.onclick = () => { + input.click(); + }; + genImg(); + } + clearbutton: HTMLButtonElement; + generateHTML(): HTMLDivElement { + const div = document.createElement("div"); + const span = document.createElement("span"); + span.textContent = this.label; + div.append(span); + const innerDiv = document.createElement("div"); + innerDiv.classList.add("flexltr", "fileinputdiv"); + innerDiv.append(this.img); -class HtmlArea implements OptionsElement{ + if (this.clear) { + innerDiv.append(this.clearbutton); + } + div.append(innerDiv); + return div; + } +} + +class HtmlArea implements OptionsElement { submit: () => void; html: (() => HTMLElement) | HTMLElement; value!: void; - constructor(html: (() => HTMLElement) | HTMLElement, submit: () => void){ + constructor(html: (() => HTMLElement) | HTMLElement, submit: () => void) { this.submit = submit; this.html = html; } - generateHTML(): HTMLElement{ - if(this.html instanceof Function){ - return this.html(); - }else{ + generateHTML(): HTMLElement { + if (this.html instanceof Function) { + return (this.html = this.html()); + } else { return this.html; } } - watchForChange(){} + watchForChange() {} } /** -* This is a simple wrapper class for Options to make it happy so it can be used outside of Settings. -*/ -class Float{ - options:Options; + * This is a simple wrapper class for Options to make it happy so it can be used outside of Settings. + */ +class Float { + options: Options; + html: WeakRef = new WeakRef(document.createElement("div")); /** * This is a simple wrapper class for Options to make it happy so it can be used outside of Settings. */ - constructor(name:string, options={ ltr:false, noSubmit:true}){ - this.options=new Options(name,this,options) + constructor(name: string, options = {ltr: false, noSubmit: true}) { + this.options = new Options(name, this, options); + } + changed(d: HTMLElement) { + const html = this.html.deref(); + if (!html) return; + html.append(d); } - changed=()=>{}; - generateHTML(){ - return this.options.generateHTML(); + generateHTML() { + const html = this.options.generateHTML(); + this.html = new WeakRef(html); + return html; } } -class Dialog{ - float:Float; - get options(){ +class Dialog { + float: Float; + above = false; + get options() { return this.float.options; } - background=new WeakRef(document.createElement("div")); - constructor(name:string, { ltr=false, noSubmit=true}={}){ - this.float=new Float(name,{ltr,noSubmit}); + background = new WeakRef(document.createElement("div")); + constructor(name: string, {ltr = false, noSubmit = true, goAbove = false} = {}) { + this.float = new Float(name, {ltr, noSubmit}); + this.above = goAbove; } - show(){ + show(hideOnClick = true) { const background = document.createElement("div"); background.classList.add("background"); - const center=this.float.generateHTML(); - center.classList.add("centeritem","nonimagecenter"); + if (this.above) background.style.zIndex = "200"; + if (!hideOnClick) background.classList.add("solidBackground"); + const center = this.float.generateHTML(); + center.classList.add("centeritem", "nonimagecenter"); center.classList.remove("titlediv"); background.append(center); - center.onclick=e=>{ - e.stopImmediatePropagation(); - } document.body.append(background); - this.background=new WeakRef(background); - background.onclick = _=>{ - background.remove(); + this.background = new WeakRef(background); + background.onclick = (_) => { + if (hideOnClick && _.target === background) { + removeAni(background); + } + }; + background.tabIndex = 0; + background.focus(); + background.onkeydown = (e) => { + if (e.key === "Escape" && hideOnClick) { + removeAni(background); + } }; + return center; } - hide(){ - const background=this.background.deref(); - if(!background) return; - background.remove(); + hide() { + const background = this.background.deref(); + if (!background) return; + removeAni(background); } } -export{Dialog}; -class Options implements OptionsElement{ +class InstancePicker implements OptionsElement { + value: InstanceInfo | null = null; + owner: Options | Form; + verify = document.createElement("p"); + onchange = (_: InstanceInfo) => {}; + instance?: string; + watchForChange(func: (arg1: InstanceInfo) => void) { + this.onchange = func; + } + constructor( + owner: Options | Form, + onchange?: InstancePicker["onchange"], + button?: HTMLButtonElement, + instance?: string, + ) { + this.owner = owner; + this.instance = instance; + if (onchange) { + this.onchange = onchange; + } + this.button = button; + } + generateHTML(): HTMLElement { + const div = document.createElement("div"); + const span = document.createElement("span"); + span.textContent = I18n.htmlPages.instanceField(); + div.append(span); + + const verify = this.verify; + verify.classList.add("verify"); + div.append(verify); + + const input = this.input; + input.readOnly = !!new URLSearchParams(window.location.search).get("instance"); + console.log("read only", input.readOnly, window.location.search); + input.type = "search"; + input.setAttribute("list", "instances"); + div.append(input); + let cur = 0; + input.onkeyup = async () => { + const thiscur = ++cur; + await new Promise((res) => setTimeout(res, 500)); + if (thiscur !== cur) return; + const urls = await checkInstance(input.value, verify, this.button); + if (thiscur === cur && urls) { + this.onchange(urls); + } + }; + + InstancePicker.picker = this; + InstancePicker.genDataList(); + + return div; + } + button?: HTMLButtonElement; + input = document.createElement("input"); + giveButton(button: HTMLButtonElement | undefined) { + this.button = button; + } + static picker?: InstancePicker; + static genDataList() { + let datalist = document.getElementById("instances"); + if (!datalist) { + datalist = document.createElement("datalist"); + datalist.setAttribute("id", "instances"); + document.body.append(datalist); + } + + const json = getInstances(); + + const [stringURLMap, stringURLsMap] = getStringURLMapPair(); + if (!json) { + instancefetch.then(this.genDataList.bind(this)); + return; + } + + if (json.length !== 0) { + let name = + this.picker?.instance || new URLSearchParams(window.location.search).get("instance"); + if (!name) { + const l = localStorage.getItem("instanceinfo"); + if (l) { + const json = JSON.parse(l); + if (json.value) { + name = json.value; + } else { + name = json.wellknown; + } + } + } + if (!name) { + name = json[0].name; + } + if (this.picker) { + checkInstance( + name, + this.picker.verify, + this.picker.button || document.createElement("button"), + ).then((e) => { + if (e) this.picker?.onchange(e); + }); + this.picker.input.value = name; + } + } + + if (datalist.childElementCount !== 0) { + return; + } + + for (const instance of json) { + if (instance.display === false) { + continue; + } + const option = document.createElement("option"); + option.disabled = !instance.online; + option.value = instance.name; + if (instance.url) { + stringURLMap.set(option.value, instance.url); + if (instance.urls) { + stringURLsMap.set(instance.url, instance.urls); + } + } else if (instance.urls) { + stringURLsMap.set(option.value, instance.urls); + } else { + option.disabled = true; + } + if (instance.description) { + option.label = instance.description; + } else { + option.label = instance.name; + } + datalist.append(option); + } + } + submit() {} +} +setTimeout(InstancePicker.genDataList.bind(InstancePicker), 0); +export {Dialog}; +class Options implements OptionsElement { name: string; haschanged = false; - readonly options: OptionsElement[]; + options: OptionsElement[]; readonly owner: Buttons | Options | Form | Float; readonly ltr: boolean; value!: void; readonly html: WeakMap, WeakRef> = new WeakMap(); - readonly noSubmit:boolean=false; - container: WeakRef = new WeakRef( - document.createElement("div") - ); + readonly noSubmit: boolean = false; + container: WeakRef = new WeakRef(document.createElement("div")); + vsmaller = false; constructor( name: string, owner: Buttons | Options | Form | Float, - { ltr = false, noSubmit=false} = {} - ){ + {ltr = false, noSubmit = false, vsmaller = false} = {}, + ) { this.name = name; this.options = []; this.owner = owner; this.ltr = ltr; - this.noSubmit=noSubmit; + this.noSubmit = noSubmit; + this.vsmaller = vsmaller; } - removeAll(){ - while(this.options.length){ + removeAll() { + this.returnFromSub(); + while (this.options.length) { this.options.pop(); } const container = this.container.deref(); - if(container){ + if (container) { container.innerHTML = ""; } } - watchForChange(){} - addOptions(name: string, { ltr = false,noSubmit=false } = {}){ - const options = new Options(name, this, { ltr,noSubmit }); + watchForChange() {} + addOptions(name: string, {ltr = false, noSubmit = false} = {}) { + const options = new Options(name, this, {ltr, noSubmit}); this.options.push(options); this.generate(options); return options; } - addButtons(name: string, { top = false } = {}){ - const buttons = new Buttons(name, { top }); + addButtons(name: string, {top = false, titles = true} = {}) { + const buttons = new Buttons(name, {top, titles}); this.options.push(buttons); this.generate(buttons); return buttons; } subOptions: Options | Form | undefined; - genTop(){ + genTop() { const container = this.container.deref(); - if(container){ - if(this.isTop()){ - this.generateContainter(); - }else if(this.owner instanceof Options){ + if (container) { + if (this.isTop()) { + this.generateContainer(); + } else if (this.owner instanceof Options) { this.owner.genTop(); - }else{ + } else { (this.owner as Form).owner.genTop(); } - }else{ - throw new Error( - "Tried to make a sub menu when the options weren't rendered" - ); + } else { + throw new Error("Tried to make a sub menu when the options weren't rendered"); } } - addSubOptions(name: string, { ltr = false,noSubmit=false } = {}){ - const options = new Options(name, this, { ltr,noSubmit }); + addSubOptions(name: string, {ltr = false, noSubmit = false} = {}) { + const options = new Options(name, this, {ltr, noSubmit}); this.subOptions = options; this.genTop(); return options; } addSubForm( name: string, - onSubmit: (arg1: object,sent:object) => void, + onSubmit: (arg1: object, sent: object) => void, { ltr = false, submitText = "Submit", @@ -699,8 +1108,9 @@ class Options implements OptionsElement{ headers = {}, method = "POST", traditionalSubmit = false, - } = {} - ){ + tfaCheck = true, + } = {}, + ) { const options = new Form(name, this, onSubmit, { ltr, submitText, @@ -708,43 +1118,87 @@ class Options implements OptionsElement{ headers, method, traditionalSubmit, + tfaCheck, }); this.subOptions = options; this.genTop(); return options; } - returnFromSub(){ + addEmojiInput( + label: string, + onSubmit: (str: Emoji | null | undefined) => void, + localuser: Localuser, + {initEmoji = undefined, clear = false, guild = true} = {} as { + initEmoji?: Emoji; + clear?: boolean; + guild?: boolean; + }, + ) { + const emoji = new EmojiInput(label, onSubmit, this, localuser, { + initEmoji: initEmoji, + clear, + guild, + }); + this.options.push(emoji); + this.generate(emoji); + return emoji; + } + addInstancePicker( + onchange?: InstancePicker["onchange"], + {button, instance}: {button?: HTMLButtonElement; instance?: string} = {}, + ) { + const instancePicker = new InstancePicker(this, onchange, button, instance); + this.options.push(instancePicker); + this.generate(instancePicker); + return instancePicker; + } + returnFromSub() { this.subOptions = undefined; this.genTop(); } addSelect( label: string, onSubmit: (str: number) => void, - selections: string[], - { defaultIndex = 0,radio=false } = {} - ){ + selections: readonly string[], + {defaultIndex = 0, radio = false} = {}, + ) { const select = new SelectInput(label, onSubmit, selections, this, { - defaultIndex,radio + defaultIndex, + radio, }); this.options.push(select); this.generate(select); return select; } - addFileInput( + addImageInput( label: string, onSubmit: (files: FileList | null) => void, - { clear = false } = {} - ){ - const FI = new FileInput(label, onSubmit, this, { clear }); + {clear = false, initImg = "", width = -1, objectFit = ""} = {}, + ) { + const FI = new ImageInput(label, onSubmit, this, {clear, initImg, width, objectFit}); + this.options.push(FI); + this.generate(FI); + return FI; + } + addFileInput(label: string, onSubmit: (files: FileList | null) => void, {clear = false} = {}) { + const FI = new FileInput(label, onSubmit, this, {clear}); this.options.push(FI); this.generate(FI); return FI; } + addDateInput(label: string, onSubmit: (str: string) => void, {initText = ""} = {}) { + const textInput = new DateInput(label, onSubmit, this, { + initText, + }); + this.options.push(textInput); + this.generate(textInput); + return textInput; + } addTextInput( label: string, onSubmit: (str: string) => void, - { initText = "", password = false } = {} - ){ + {initText = "", password = false} = {}, + ) { const textInput = new TextInput(label, onSubmit, this, { initText, password, @@ -753,64 +1207,55 @@ class Options implements OptionsElement{ this.generate(textInput); return textInput; } - addColorInput( - label: string, - onSubmit: (str: string) => void, - { initColor = "" } = {} - ){ - const colorInput = new ColorInput(label, onSubmit, this, { initColor }); + addColorInput(label: string, onSubmit: (str: string) => void, {initColor = ""} = {}) { + const colorInput = new ColorInput(label, onSubmit, this, {initColor}); this.options.push(colorInput); this.generate(colorInput); return colorInput; } - addMDInput( - label: string, - onSubmit: (str: string) => void, - { initText = "" } = {} - ){ - const mdInput = new MDInput(label, onSubmit, this, { initText }); + addMDInput(label: string, onSubmit: (str: string) => void, {initText = ""} = {}) { + const mdInput = new MDInput(label, onSubmit, this, {initText}); this.options.push(mdInput); this.generate(mdInput); return mdInput; } - addHTMLArea( - html: (() => HTMLElement) | HTMLElement, - submit: () => void = ()=>{} - ){ + addHTMLArea(html: (() => HTMLElement) | HTMLElement, submit: () => void = () => {}) { const htmlarea = new HtmlArea(html, submit); this.options.push(htmlarea); this.generate(htmlarea); return htmlarea; } - addButtonInput(label: string, textContent: string, onSubmit: () => void){ + addButtonInput(label: string, textContent: string, onSubmit: () => void) { const button = new ButtonInput(label, textContent, onSubmit, this); this.options.push(button); this.generate(button); return button; } - addCheckboxInput( - label: string, - onSubmit: (str: boolean) => void, - { initState = false } = {} - ){ - const box = new CheckboxInput(label, onSubmit, this, { initState }); + addCheckboxInput(label: string, onSubmit: (str: boolean) => void, {initState = false} = {}) { + const box = new CheckboxInput(label, onSubmit, this, {initState}); this.options.push(box); this.generate(box); return box; } - addText(str: string){ + addText(str: string) { const text = new SettingsText(str); this.options.push(text); this.generate(text); return text; } - addHR(){ - const rule = new HorrizonalRule(); + addMDText(str: MarkDown) { + const text = new SettingsMDText(str); + this.options.push(text); + this.generate(text); + return text; + } + addHR() { + const rule = new HorizontalRule(); this.options.push(rule); this.generate(rule); return rule; } - addTitle(str: string){ + addTitle(str: string) { const text = new SettingsTitle(str); this.options.push(text); this.generate(text); @@ -818,16 +1263,17 @@ class Options implements OptionsElement{ } addForm( name: string, - onSubmit: (arg1: object,sent:object) => void, + onSubmit: (arg1: object, sent: object) => void, { ltr = false, - submitText = "Submit", + submitText = I18n.submit(), fetchURL = "", headers = {}, method = "POST", traditionalSubmit = false, - } = {} - ){ + vsmaller = false, + } = {}, + ) { const options = new Form(name, this, onSubmit, { ltr, submitText, @@ -835,16 +1281,17 @@ class Options implements OptionsElement{ headers, method, traditionalSubmit, + vsmaller, }); this.options.push(options); this.generate(options); return options; } - generate(elm: OptionsElement){ + generate(elm: OptionsElement) { const container = this.container.deref(); - if(container){ + if (container) { const div = document.createElement("div"); - if(!(elm instanceof Options)){ + if (!(elm instanceof Options)) { div.classList.add("optionElement"); } const html = elm.generateHTML(); @@ -853,112 +1300,138 @@ class Options implements OptionsElement{ container.append(div); } } + deleteElm(opt: OptionsElement) { + const html = this.html.get(opt)?.deref(); + this.options = this.options.filter((_) => _ !== opt); + if (!html) return; + html.remove(); + this.html.delete(opt); + } title: WeakRef = new WeakRef(document.createElement("h2")); - generateHTML(): HTMLElement{ + generateHTML(): HTMLElement { const div = document.createElement("div"); - div.classList.add("flexttb","titlediv"); - if(this.owner instanceof Options){ + div.classList.add("flexttb", "titlediv"); + if (this.vsmaller) div.classList.add("vsmaller"); + if (this.owner instanceof Options) { div.classList.add("optionElement"); } const title = document.createElement("h2"); title.textContent = this.name; div.append(title); - if(this.name !== "") title.classList.add("settingstitle"); + if (this.name !== "") title.classList.add("settingstitle"); this.title = new WeakRef(title); const container = document.createElement("div"); this.container = new WeakRef(container); container.classList.add(this.ltr ? "flexltr" : "flexttb", "flexspace"); - this.generateContainter(); + this.generateContainer(); div.append(container); return div; } - generateName():(HTMLElement|string)[]{ - const build:(HTMLElement|string)[]=[]; - if(this.subOptions){ - if(this.name!==""){ + + generateName(): (HTMLElement | string)[] { + const build: (HTMLElement | string)[] = []; + if (this.owner instanceof Buttons) { + const span = document.createElement("span"); + span.classList.add("svg-intoMenu", "svgicon", "mobileback"); + if (!(this.owner instanceof Settings) || !this.owner.hideButtons) build.push(span); + span.onclick = () => { + const container = this.container.deref(); + if (!container) return; + if (!container.parentElement) return; + if (!container.parentElement.parentElement) return; + container.parentElement.parentElement.classList.add("mobileHidden"); + }; + } + if (this.subOptions) { + if (this.name !== "") { const name = document.createElement("span"); name.innerText = this.name; name.classList.add("clickable"); - name.onclick = ()=>{ + name.onclick = () => { this.returnFromSub(); }; build.push(name); build.push(" > "); } - if(this.subOptions instanceof Options){ + if (this.subOptions instanceof Options) { build.push(...this.subOptions.generateName()); - }else{ + } else { build.push(...this.subOptions.options.generateName()); } - }else{ + } else { const name = document.createElement("span"); name.innerText = this.name; build.push(name); } return build; } - isTop(){ - return (this.owner instanceof Options&&this.owner.subOptions!==this)|| - (this.owner instanceof Form&&this.owner.owner.subOptions!==this.owner)|| - (this.owner instanceof Settings)|| - (this.owner instanceof Buttons); + isTop() { + return ( + (this.owner instanceof Options && this.owner.subOptions !== this) || + (this.owner instanceof Form && this.owner.owner.subOptions !== this.owner) || + this.owner instanceof Settings || + this.owner instanceof Buttons || + this.owner instanceof Float + ); } - generateContainter(){ + generateContainer() { const container = this.container.deref(); - if(container){ - const title = this.title.deref(); - if(title) title.innerHTML = ""; + if (container) { container.innerHTML = ""; - if(this.isTop()){ - if(title){ - const elms=this.generateName(); + const title = this.title.deref(); + if (title) title.innerHTML = ""; + + console.log(container.children); + if (this.isTop()) { + if (title) { + const elms = this.generateName(); title.append(...elms); } } - if(!this.subOptions){ - for(const thing of this.options){ + if (!this.subOptions) { + for (const thing of this.options) { this.generate(thing); } - }else{ + } else { container.append(this.subOptions.generateHTML()); } - if(title && title.innerText !== ""){ + if (title && title.innerText !== "") { title.classList.add("settingstitle"); - }else if(title){ + } else if (title) { title.classList.remove("settingstitle"); } - if(this.owner instanceof Form&&this.owner.button){ - const button=this.owner.button.deref(); - if(button){ - button.hidden=false; + if (this.owner instanceof Form && this.owner.button) { + const button = this.owner.button.deref(); + if (button) { + button.hidden = false; } } - }else{ + } else { console.warn("tried to generate container, but it did not exist"); } } - changed(){ - if(this.noSubmit){ + changed() { + if (this.noSubmit) { return; } - if(this.owner instanceof Options || this.owner instanceof Form ){ + if (this.owner instanceof Options || this.owner instanceof Form) { this.owner.changed(); return; } - if(!this.haschanged){ + if (!this.haschanged) { const div = document.createElement("div"); div.classList.add("flexltr", "savediv"); const span = document.createElement("span"); div.append(span); - span.textContent = I18n.getTranslation("settings.unsaved"); + span.textContent = I18n.settings.unsaved(); const button = document.createElement("button"); - button.textContent = I18n.getTranslation("settings.save"); + button.textContent = I18n.settings.save(); div.append(button); this.haschanged = true; this.owner.changed(div); - button.onclick = _=>{ - if(this.owner instanceof Buttons){ + button.onclick = (_) => { + if (this.owner instanceof Buttons) { this.owner.save(); } div.remove(); @@ -966,29 +1439,198 @@ class Options implements OptionsElement{ }; } } - submit(){ + afterSubmit = () => {}; + submit() { this.haschanged = false; - if(this.subOptions){ + if (this.subOptions) { this.subOptions.submit(); return; } - for(const thing of this.options){ + for (const thing of this.options) { thing.submit(); } + this.afterSubmit(); + } +} +class Captcha implements OptionsElement { + owner: Form; + value: string = ""; + constructor(owner: Form) { + this.owner = owner; + } + div?: HTMLElement; + generateHTML(): HTMLElement { + const div = document.createElement("div"); + this.div = div; + return div; + } + submit() {} + onchange = (_: string) => {}; + watchForChange(func: (arg1: string) => void) { + this.onchange = func; + } + static hcaptcha?: HTMLDivElement; + static async waitForCaptcha(ctype: "hcaptcha") { + switch (ctype) { + case "hcaptcha": + if (!this.hcaptcha) throw Error("no captcha found"); + const hcaptcha = this.hcaptcha; + console.log(hcaptcha); + //@ts-expect-error + while (!hcaptcha.children[1].children.length || !hcaptcha.children[1].children[1].value) { + await new Promise((res) => setTimeout(res, 100)); + } + //@ts-expect-error + return hcaptcha.children[1].children[1].value; + } + } + async makeCaptcha({ + captcha_sitekey, + captcha_service, + }: { + captcha_sitekey: string; + captcha_service: "hcaptcha"; + }): Promise { + if (!this.div) throw new Error("Div doesn't exist yet to give catpcha"); + switch (captcha_service) { + case "hcaptcha": + if (Captcha.hcaptcha) { + this.div.append(Captcha.hcaptcha); + Captcha.hcaptcha.setAttribute("data-sitekey", captcha_sitekey); + eval("hcaptcha.reset()"); + return Captcha.waitForCaptcha(captcha_service); + } else { + const capt = document.createElement("div"); + const capty = document.createElement("div"); + capty.classList.add("h-captcha"); + + capty.setAttribute("data-sitekey", captcha_sitekey); + const script = document.createElement("script"); + script.src = "https://js.hcaptcha.com/1/api.js"; + capt.append(script); + capt.append(capty); + Captcha.hcaptcha = capt; + this.div.append(capt); + return Captcha.waitForCaptcha(captcha_service); + } + } + } + static async makeCaptcha(json: { + captcha_sitekey: string; + captcha_service: "hcaptcha"; + }): Promise { + const float = new Dialog("", {noSubmit: true}); + float.options.addTitle(I18n.form.captcha()); + const cap = float.options.addForm("", () => {}, {traditionalSubmit: true}).addCaptcha(); + float.show().parentElement!.style.zIndex = "200"; + const ret = cap.makeCaptcha(json); + await ret; + float.hide(); + return ret; } } -class FormError extends Error{ +class FormError extends Error { elem: OptionsElement; message: string; - constructor(elem: OptionsElement, message: string){ + constructor(elem: OptionsElement, message: string) { super(message); this.message = message; this.elem = elem; } } -export{ FormError }; -class Form implements OptionsElement{ +async function handle2fa(json: any, api: string): Promise { + if (json.ticket) { + if (json.webauthn) { + const challenge = JSON.parse(json.webauthn) + .publicKey as PublicKeyCredentialRequestOptionsJSON; + challenge.challenge = challenge.challenge + .split("=")[0] + .replaceAll("+", "-") + .replaceAll("/", "_"); + challenge.allowCredentials?.forEach( + (_) => (_.id = _.id.split("=")[0].replaceAll("+", "-").replaceAll("/", "_")), + ); + console.log(challenge); + const options = PublicKeyCredential.parseRequestOptionsFromJSON(challenge); + const credential = (await navigator.credentials.get({publicKey: options})) as unknown as { + rawId: ArrayBuffer; + response: { + [key: string]: ArrayBuffer; + }; + }; + if (!credential) return false; + function toBase64(buf: ArrayBuffer) { + return btoa(String.fromCharCode(...new Uint8Array(buf))); + } + const keys = ["authenticatorData", "clientDataJSON", "signature"]; + const response = {} as any; + for (const key of keys) { + response[key] = toBase64(credential.response[key] as ArrayBuffer); + } + const res = { + rawId: toBase64(credential.rawId), + response, + }; + const resObj = await fetch(api + "/auth/mfa/webauthn", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({code: JSON.stringify(res), ticket: json.ticket}), + }); + if (!resObj.ok) return false; + const jsonRes = await resObj.json(); + return jsonRes; + } else { + return new Promise((resolution) => { + const better = new Dialog(""); + const form = better.options.addForm( + "", + (res: any) => { + if (res.message) { + throw new FormError(ti, res.message); + } else { + resolution(res); + better.hide(); + } + }, + { + fetchURL: api + "/auth/mfa/totp", + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }, + ); + form.addTitle(I18n["2faCode"]()); + form.addPreprocessor((e) => { + //@ts-ignore + e.ticket = json.ticket; + }); + const ti = form.addTextInput("", "code"); + better.show().parentElement!.style.zIndex = "200"; + }); + } + } else { + return false; + } +} +async function handleCaptcha(json: any, build: any, cap: Captcha | undefined) { + if (json.captcha_sitekey) { + let token: string; + if (cap) { + token = await cap.makeCaptcha(json); + } else { + token = await Captcha.makeCaptcha(json); + } + build.captcha_key = token; + return true; + } + return false; +} +export {FormError}; +class Form implements OptionsElement { name: string; readonly options: Options; readonly owner: Options; @@ -996,143 +1638,203 @@ class Form implements OptionsElement{ readonly names: Map> = new Map(); readonly required: WeakSet> = new WeakSet(); readonly submitText: string; - readonly fetchURL: string; + fetchURL: string; readonly headers = {}; readonly method: string; value!: object; traditionalSubmit: boolean; - values: { [key: string]: any } = {}; + values: {[key: string]: any} = {}; + tfaCheck: boolean; constructor( name: string, owner: Options, - onSubmit: (arg1: object,sent:object) => void, + onSubmit: (arg1: object, sent: object) => void, { ltr = false, - submitText = I18n.getTranslation("submit"), + submitText = I18n.submit(), fetchURL = "", headers = {}, method = "POST", traditionalSubmit = false, - } = {} - ){ + vsmaller = false, + tfaCheck = true, + } = {}, + ) { this.traditionalSubmit = traditionalSubmit; this.name = name; + this.tfaCheck = tfaCheck; this.method = method; this.submitText = submitText; - this.options = new Options(name, this, { ltr }); + this.options = new Options(name, this, {ltr, vsmaller}); this.owner = owner; this.fetchURL = fetchURL; this.headers = headers; this.ltr = ltr; this.onSubmit = onSubmit; } - setValue(key: string, value: any){ + setValue(key: string, value: any) { //the value can't really be anything, but I don't care enough to fix this this.values[key] = value; } - addSubOptions(name: string, { ltr = false,noSubmit=false } = {}){ - if(this.button&&this.button.deref()){ - (this.button.deref() as HTMLElement).hidden=true; + addSubOptions(name: string, {ltr = false, noSubmit = false} = {}) { + if (this.button && this.button.deref()) { + (this.button.deref() as HTMLElement).hidden = true; } - return this.options.addSubOptions(name,{ltr, noSubmit}); + return this.options.addSubOptions(name, {ltr, noSubmit}); + } + addHTMLArea(html: (() => HTMLElement) | HTMLElement, onSubmit = () => {}) { + return this.options.addHTMLArea(html, onSubmit); + } + private captcha?: Captcha; + addCaptcha() { + if (this.captcha) throw new Error("only one captcha is allowed per form"); + const cap = new Captcha(this); + this.options.options.push(cap); + this.options.generate(cap); + this.captcha = cap; + return cap; } addSubForm( name: string, - onSubmit: (arg1: object,sent:object) => void, + onSubmit: (arg1: object, sent: object) => void, { ltr = false, - submitText = I18n.getTranslation("submit"), + submitText = I18n.submit(), fetchURL = "", headers = {}, method = "POST", traditionalSubmit = false, - } = {} - ){ - if(this.button&&this.button.deref()){ + } = {}, + ) { + if (this.button && this.button.deref()) { console.warn("hidden"); - (this.button.deref() as HTMLElement).hidden=true; + (this.button.deref() as HTMLElement).hidden = true; } - return this.options.addSubForm(name,onSubmit,{ltr,submitText,fetchURL,headers,method,traditionalSubmit}); + return this.options.addSubForm(name, onSubmit, { + ltr, + submitText, + fetchURL, + headers, + method, + traditionalSubmit, + }); } - generateContainter(){ - this.options.generateContainter(); - if((this.options.isTop())&&this.button&&this.button.deref()){ - (this.button.deref() as HTMLElement).hidden=false; + generateContainer() { + this.options.generateContainer(); + if (this.options.isTop() && this.button && this.button.deref()) { + (this.button.deref() as HTMLElement).hidden = false; } } - selectMap=new WeakMap(); + selectMap = new WeakMap(); addSelect( label: string, formName: string, selections: string[], - { defaultIndex = 0, required = false,radio=false}={}, - correct:(string|number|null)[]=selections - ){ - const select = this.options.addSelect(label, _=>{}, selections, { - defaultIndex,radio + {defaultIndex = 0, required = false, radio = false} = {}, + correct: readonly (string | number | null | undefined)[] = selections, + ) { + const select = this.options.addSelect(label, (_) => {}, selections, { + defaultIndex, + radio, }); - this.selectMap.set(select,correct); + this.selectMap.set(select, correct); this.names.set(formName, select); - if(required){ + if (required) { this.required.add(select); } return select; } - readonly fileOptions: Map = new Map(); + readonly fileOptions: Map = new Map(); addFileInput( label: string, formName: string, - { required = false, files = "one", clear = false } = {} - ){ - const FI = this.options.addFileInput(label, _=>{}, { clear }); - if(files !== "one" && files !== "multi") - throw new Error("files should equal one or multi"); - this.fileOptions.set(FI, { files }); + {required = false, files = "one", clear = false} = {}, + ) { + const FI = this.options.addFileInput(label, (_) => {}, {clear}); + if (files !== "one" && files !== "multi") throw new Error("files should equal one or multi"); + this.fileOptions.set(FI, {files}); this.names.set(formName, FI); - if(required){ + if (required) { this.required.add(FI); } return FI; } - + addImageInput( + label: string, + formName: string, + {required = false, files = "one", clear = false, initImg = "", width = -1, objectFit = ""} = {}, + ) { + const FI = this.options.addImageInput(label, (_) => {}, {clear, initImg, width, objectFit}); + if (files !== "one" && files !== "multi") throw new Error("files should equal one or multi"); + this.fileOptions.set(FI, {files}); + this.names.set(formName, FI); + if (required) { + this.required.add(FI); + } + return FI; + } + addEmojiInput( + label: string, + formName: string, + localuser: Localuser, + {initEmoji = undefined, required = false, clear = false, guild = true} = {} as { + initEmoji?: Emoji; + required: boolean; + clear?: boolean; + guild?: boolean; + }, + ) { + const emoji = this.options.addEmojiInput(label, () => {}, localuser, { + initEmoji: initEmoji, + clear, + guild, + }); + if (required) { + this.required.add(emoji); + } + this.names.set(formName, emoji); + return emoji; + } + addDateInput(label: string, formName: string, {initText = "", required = false} = {}) { + const dateInput = this.options.addDateInput(label, (_) => {}, { + initText, + }); + this.names.set(formName, dateInput); + if (required) { + this.required.add(dateInput); + } + return dateInput; + } addTextInput( label: string, formName: string, - { initText = "", required = false, password = false } = {} - ){ - const textInput = this.options.addTextInput(label, _=>{}, { + {initText = "", required = false, password = false} = {}, + ) { + const textInput = this.options.addTextInput(label, (_) => {}, { initText, password, }); this.names.set(formName, textInput); - if(required){ + if (required) { this.required.add(textInput); } return textInput; } - addColorInput( - label: string, - formName: string, - { initColor = "", required = false } = {} - ){ - const colorInput = this.options.addColorInput(label, _=>{}, { + addColorInput(label: string, formName: string, {initColor = "", required = false} = {}) { + const colorInput = this.options.addColorInput(label, (_) => {}, { initColor, }); this.names.set(formName, colorInput); - if(required){ + if (required) { this.required.add(colorInput); } return colorInput; } - addMDInput( - label: string, - formName: string, - { initText = "", required = false } = {} - ){ - const mdInput = this.options.addMDInput(label, _=>{}, { initText }); + addMDInput(label: string, formName: string, {initText = "", required = false} = {}) { + const mdInput = this.options.addMDInput(label, (_) => {}, {initText}); this.names.set(formName, mdInput); - if(required){ + if (required) { this.required.add(mdInput); } return mdInput; @@ -1141,210 +1843,243 @@ class Form implements OptionsElement{ * This function does not integrate with the form, so be aware of that * */ - addButtonInput(label:string,textContent:string,onSubmit:()=>void){ - return this.options.addButtonInput(label,textContent,onSubmit); + addButtonInput(label: string, textContent: string, onSubmit: () => void) { + return this.options.addButtonInput(label, textContent, onSubmit); } /** * This function does not integrate with the form, so be aware of that * */ - addOptions(name: string, { ltr = false,noSubmit=false } = {}){ - return this.options.addOptions(name, {ltr,noSubmit}); + addOptions(name: string, {ltr = false, noSubmit = false} = {}) { + return this.options.addOptions(name, {ltr, noSubmit}); } - addCheckboxInput( - label: string, - formName: string, - { initState = false, required = false } = {} - ){ - const box = this.options.addCheckboxInput(label, _=>{}, { initState }); + addCheckboxInput(label: string, formName: string, {initState = false, required = false} = {}) { + const box = this.options.addCheckboxInput(label, (_) => {}, {initState}); this.names.set(formName, box); - if(required){ + if (required) { this.required.add(box); } return box; } - addText(str: string){ + addText(str: string) { return this.options.addText(str); } - addHR(){ + addMDText(str: MarkDown) { + return this.options.addMDText(str); + } + addHR() { return this.options.addHR(); } - addTitle(str: string){ + addTitle(str: string) { this.options.addTitle(str); } - button!:WeakRef; - generateHTML(): HTMLElement{ + handleError(error: FormError) { + this.onFormError(error); + const elm = this.options.html.get(error.elem); + if (elm) { + const html = elm.deref(); + if (html) { + this.makeError(html, error.message); + } + } + } + button!: WeakRef; + generateHTML(): HTMLElement { const div = document.createElement("div"); div.append(this.options.generateHTML()); div.classList.add("FormSettings"); - if(!this.traditionalSubmit){ + if (!this.traditionalSubmit && this.submitText) { const button = document.createElement("button"); - button.onclick = _=>{ + button.onclick = (_) => { this.submit(); }; button.textContent = this.submitText; div.append(button); - if(this.options.subOptions){ - button.hidden=true; + if (this.options.subOptions) { + button.hidden = true; } - this.button=new WeakRef(button); + this.button = new WeakRef(button); } return div; } - onSubmit: ((arg1: object,sent:object) => void )|((arg1: object,sent:object) => Promise ); - watchForChange(func: (arg1: object) => void){ + onSubmit: + | ((arg1: object, sent: object) => void) + | ((arg1: object, sent: object) => Promise); + watchForChange(func: (arg1: object) => void) { this.onSubmit = func; } - changed(){ - if(this.traditionalSubmit){ + changed() { + if (this.traditionalSubmit) { this.owner.changed(); } } - preprocessor:(obj:Object)=>void=()=>{}; - addPreprocessor(func:(obj:Object)=>void){ - this.preprocessor=func; + preprocessor: (obj: Object) => void = () => {}; + addPreprocessor(func: (obj: Object) => void) { + this.preprocessor = func; } - async submit(){ - if(this.options.subOptions){ + onFormError = (_: FormError) => {}; + async submit() { + if (this.options.subOptions) { this.options.subOptions.submit(); return; } console.log("start"); const build = {}; - for(const key of Object.keys(this.values)){ + for (const key of Object.keys(this.values)) { const thing = this.values[key]; - if(thing instanceof Function){ - try{ + if (thing instanceof Function) { + try { (build as any)[key] = thing(); - }catch(e: any){ - if(e instanceof FormError){ - const elm = this.options.html.get(e.elem); - if(elm){ - const html = elm.deref(); - if(html){ - this.makeError(html, e.message); - } - } + } catch (e: any) { + if (e instanceof FormError) { + this.handleError(e); } return; } - }else{ - (build as any)[thing] = thing; + } else { + (build as any)[key] = thing; } } console.log("middle"); const promises: Promise[] = []; - for(const thing of this.names.keys()){ - if(thing === "")continue; + for (const thing of this.names.keys()) { + if (thing === "") continue; const input = this.names.get(thing) as OptionsElement; - if(input instanceof SelectInput){ + if (input instanceof SelectInput) { (build as any)[thing] = (this.selectMap.get(input) as string[])[input.value]; continue; - }else if(input instanceof FileInput){ + } else if (input instanceof FileInput) { const options = this.fileOptions.get(input); - if(!options){ + if (!options) { throw new Error( - "FileInput without its options is in this form, this should never happen." + "FileInput without its options is in this form, this should never happen.", ); } - if(options.files === "one"){ + if (options.files === "one") { console.log(input.value); - if(input.value){ + if (input.value) { const reader = new FileReader(); - reader.readAsDataURL(input.value[0]); - const promise = new Promise(res=>{ - reader.onload = ()=>{ + const promise = new Promise((res) => { + reader.onload = () => { (build as any)[thing] = reader.result; res(); }; }); + reader.readAsDataURL(input.value[0]); promises.push(promise); + continue; } - }else{ + } else { console.error(options.files + " is not currently implemented"); } + } else if (input instanceof EmojiInput) { + if (!input.value) { + (build as any)[thing] = input.value; + } else if (input.value.id) { + (build as any)[thing] = input.value.id; + } else if (input.value.emoji) { + (build as any)[thing] = input.value.emoji; + } + continue; } (build as any)[thing] = input.value; } console.log("middle2"); await Promise.allSettled(promises); - try{ + try { this.preprocessor(build); - }catch(e){ - if(e instanceof FormError){ - const elm = this.options.html.get(e.elem); - if(elm){ - const html = elm.deref(); - if(html){ - this.makeError(html, e.message); - } - } + } catch (e) { + if (e instanceof FormError) { + this.handleError(e); } return; } - if(this.fetchURL !== ""){ - fetch(this.fetchURL, { - method: this.method, - body: JSON.stringify(build), - headers: this.headers, - }) - .then(_=>{ - return _.text() - }).then(_=>{ - if(_==="") return {}; - return JSON.parse(_) + if (this.fetchURL !== "") { + const onSubmit = async (json: any) => { + try { + await this.onSubmit(json, build); + } catch (e) { + console.error(e); + if (e instanceof FormError) { + this.handleError(e); + } + return; + } + }; + const doFetch = async () => { + fetch(this.fetchURL, { + method: this.method, + body: JSON.stringify(build), + headers: this.headers, }) - .then(async json=>{ - if(json.errors){ - if(this.errors(json)){ - return; + .then((_) => { + return _.text(); + }) + .then((_) => { + if (_ === "") return {}; + return JSON.parse(_); + }) + .then(async (json) => { + if (await handleCaptcha(json, build, this.captcha)) { + return await doFetch(); } - } - try{ - await this.onSubmit(json,build); - }catch(e){ - console.error(e); - if(e instanceof FormError){ - const elm = this.options.html.get(e.elem); - if(elm){ - const html = elm.deref(); - if(html){ - this.makeError(html, e.message); - } + const match = this.fetchURL.match(/https?:\/\/[^\/]*\/api/gm); + if (match && this.tfaCheck) { + const tried = await handle2fa(json, match[0]); + if (tried) { + return await onSubmit(tried); } } - return; - } - }); - }else{ - try{ - await this.onSubmit(build,build); - }catch(e){ - if(e instanceof FormError){ - const elm = this.options.html.get(e.elem); - if(elm){ - const html = elm.deref(); - if(html){ - this.makeError(html, e.message); + if (json.ticket) { } - } + if (json.errors) { + if (this.errors(json)) { + return; + } + } + if ( + Math.floor(json.code / 100) === 4 && + json.message && + typeof json.message === "string" + ) { + this.showPrimError(json.message); + return; + } + onSubmit(json); + }); + }; + doFetch(); + } else { + try { + await this.onSubmit(build, build); + } catch (e) { + if (e instanceof FormError) { + this.handleError(e); } return; } } console.warn("needs to be implemented"); } - errors(errors: {code: number; message: string; errors: { [key: string]: { _errors: { message: string; code: string }[] } }}){ - if(!(errors instanceof Object)){ + showPrimError(error: string) { + const pop = new PopUp(error, {goAbove: true, buttons: popUpButtonTypes.dismiss}); + pop.show(); + } + errors(errors: { + code: number; + message: string; + errors: {[key: string]: {_errors: {message: string; code: string}[]}}; + }) { + if (!(errors instanceof Object)) { return; } - for(const error of Object.keys(errors.errors)){ + for (const error of Object.keys(errors.errors)) { const elm = this.names.get(error); - if(elm){ + if (elm) { const ref = this.options.html.get(elm); - if(ref && ref.deref()){ + if (ref && ref.deref()) { const html = ref.deref() as HTMLDivElement; - const errorMessage=errors.errors[error]._errors[0].message; + const errorMessage = errors.errors[error]._errors[0].message; this.makeError(html, errorMessage); return true; } @@ -1352,85 +2087,127 @@ class Form implements OptionsElement{ } return false; } - error(formElm: string, errorMessage: string){ + error(formElm: string, errorMessage: string) { const elm = this.names.get(formElm); - if(elm){ + if (elm) { const htmlref = this.options.html.get(elm); - if(htmlref){ + if (htmlref) { const html = htmlref.deref(); - if(html){ + if (html) { this.makeError(html, errorMessage); } } - }else{ + } else { console.warn(formElm + " is not a valid form property"); } } - makeError(e: HTMLDivElement, message: string){ + makeError(e: HTMLDivElement, message: string) { let element = e.getElementsByClassName("suberror")[0] as HTMLElement; - if(!element){ + if (!element) { const div = document.createElement("div"); div.classList.add("suberror", "suberrora"); e.append(div); element = div; - }else{ + setTimeout((_) => { + element.scrollIntoView(false); + }, 100); + } else { element.classList.remove("suberror"); - setTimeout(_=>{ + setTimeout((_) => { element.classList.add("suberror"); + element.scrollIntoView(false); }, 100); } element.textContent = message; } } -class HorrizonalRule implements OptionsElement{ - constructor(){} +export const enum popUpButtonTypes { + ok = 1, + dismiss, +} +export class PopUp extends Dialog { + constructor(message: string, {buttons = popUpButtonTypes.ok, goAbove = false} = {}) { + super(message, {goAbove}); + switch (buttons) { + case popUpButtonTypes.ok: + this.options.addButtonInput("", I18n.ok(), () => { + this.hide(); + }); + break; + case popUpButtonTypes.dismiss: + this.options.addButtonInput("", I18n.dismiss(), () => { + this.hide(); + }); + break; + } + } +} +class HorizontalRule implements OptionsElement { + constructor() {} generateHTML(): HTMLElement { return document.createElement("hr"); } - watchForChange (_: (arg1: undefined) => void){ - throw new Error("don't do this") - }; - submit= () => {}; - value=undefined; + watchForChange(_: (arg1: undefined) => void) { + throw new Error("don't do this"); + } + submit = () => {}; + value = undefined; } -class Settings extends Buttons{ +class Settings extends Buttons { static readonly Buttons = Buttons; static readonly Options = Options; html!: HTMLElement | null; - constructor(name: string){ + hideButtons: boolean; + constructor(name: string, hideButtons = false) { super(name); + this.hideButtons = hideButtons; } - addButton(name: string, { ltr = false } = {}): Options{ - const options = new Options(name, this, { ltr }); + addButton(name: string, {ltr = false, optName = name, noSubmit = false} = {}): Options { + const options = new Options(optName, this, {ltr, noSubmit}); this.add(name, options); return options; } - show(){ + show() { const background = document.createElement("div"); - background.classList.add("flexttb","menu","background"); + background.classList.add("flexttb", "menu", "background"); const title = document.createElement("h2"); title.textContent = this.name; title.classList.add("settingstitle"); background.append(title); - background.append(this.generateHTML()); + background.append(this.generateHTML(this.hideButtons)); const exit = document.createElement("span"); - exit.classList.add("exitsettings","svgicon","svg-x"); + exit.classList.add("exitsettings", "svgicon", "svg-x"); background.append(exit); - exit.onclick = _=>{ + exit.onclick = (_) => { this.hide(); }; + background.addEventListener("keyup", (event) => { + if (event.key === "Escape") { + event.preventDefault(); + event.stopImmediatePropagation(); + // Cancel the default action, if needed + this.hide(); + } + }); document.body.append(background); + background.setAttribute("tabindex", "0"); + background.focus(); + this.html = background; } - hide(){ - if(this.html){ - this.html.remove(); + hide() { + if (this.html) { + const html = this.html; + html.classList.add("bgRemove"); + html.addEventListener("animationend", (e: AnimationEvent) => { + if (e.animationName === "bg-out") html.remove(); + }); this.html = null; } } } -export{ Settings, OptionsElement, Buttons, Options,Form,Float }; +export {Settings, OptionsElement, Options, Form, Float}; diff --git a/src/webpage/sitemap.xml b/src/webpage/sitemap.xml new file mode 100644 index 00000000..b9b1a935 --- /dev/null +++ b/src/webpage/sitemap.xml @@ -0,0 +1,18 @@ + + + + $$$ + weekly + 1 + + + $$$/login + weekly + .8 + + + $$$/register + weekly + .8 + + diff --git a/src/webpage/snowflake.ts b/src/webpage/snowflake.ts index e6a46035..2a2af43f 100644 --- a/src/webpage/snowflake.ts +++ b/src/webpage/snowflake.ts @@ -1,20 +1,17 @@ -abstract class SnowFlake{ +abstract class SnowFlake { public readonly id: string; - constructor(id: string){ + constructor(id: string) { this.id = id; } - getUnixTime(): number{ + getUnixTime(): number { return SnowFlake.stringToUnixTime(this.id); } - static stringToUnixTime(str: string){ - try{ + static stringToUnixTime(str: string) { + try { return Number((BigInt(str) >> 22n) + 1420070400000n); - }catch{ - console.error( - `The ID is corrupted, it's ${str} when it should be some number.` - ); - return 0; + } catch { + throw new Error(`The ID is corrupted, it's ${str} when it should be some number.`); } } } -export{ SnowFlake }; +export {SnowFlake}; diff --git a/src/webpage/sticker.ts b/src/webpage/sticker.ts new file mode 100644 index 00000000..560f39a8 --- /dev/null +++ b/src/webpage/sticker.ts @@ -0,0 +1,269 @@ +import {Contextmenu} from "./contextmenu.js"; +import {Guild} from "./guild.js"; +import {Hover} from "./hover.js"; +import {I18n} from "./i18n.js"; +import {stickerJson} from "./jsontypes.js"; +import {Localuser} from "./localuser.js"; +import {SnowFlake} from "./snowflake.js"; +import {CDNParams} from "./utils/cdnParams.js"; +import {createImg} from "./utils/utils.js"; + +class Sticker extends SnowFlake { + name: string; + type: number; + format_type: number; + owner: Guild | Localuser; + description: string; + tags: string; + get guild() { + return this.owner; + } + get localuser() { + if (this.owner instanceof Localuser) { + return this.owner; + } + return this.owner.localuser; + } + constructor(json: stickerJson, owner: Guild | Localuser) { + super(json.id); + this.name = json.name; + this.type = json.type; + this.format_type = json.format_type; + this.owner = owner; + this.tags = json.tags; + this.description = json.description || ""; + } + getHTML(): HTMLElement { + const img = createImg( + this.owner.info.cdn + "/stickers/" + this.id + ".webp" + new CDNParams({expectedSize: 160}), + ); + img.classList.add("sticker"); + const hover = new Hover(this.name); + hover.addEvent(img); + img.alt = this.description; + return img; + } + static searchStickers(search: string, localuser: Localuser, results = 50): [Sticker, number][] { + //NOTE this function is used for searching in the emoji picker for reactions, and the emoji auto-fill + const ranked: [Sticker, number][] = []; + function similar(json: Sticker) { + if (json.name.includes(search)) { + ranked.push([json, search.length / json.name.length]); + return true; + } else if (json.name.toLowerCase().includes(search.toLowerCase())) { + ranked.push([json, search.length / json.name.length / 1.4]); + return true; + } else { + return false; + } + } + const weakGuild = new WeakMap(); + for (const guild of localuser.guilds) { + if (guild.id !== "@me" && guild.stickers.length !== 0) { + for (const sticker of guild.stickers) { + if (similar(sticker)) { + weakGuild.set(sticker, guild); + } + } + } + } + ranked.sort((a, b) => b[1] - a[1]); + return ranked.splice(0, results).map((a) => { + return a; + }); + } + static getFromId(id: string, localuser: Localuser) { + for (const guild of localuser.guilds) { + const stick = guild.stickers.find((_) => _.id === id); + if (stick) { + return stick; + } + } + return undefined; + } + static emojiMap = new WeakMap>(); + static async lookupEmoji(id: string, localuser: Localuser): Promise { + const guild = localuser.guilds.find((guild) => guild.emojis.find((emoji) => emoji.id === id)); + if (guild) { + const sticker = guild.stickers.find((_) => _.id === id); + if (sticker) return sticker; + } + + const map = this.emojiMap.get(localuser) || new Map(); + this.emojiMap.set(localuser, map); + + if (map.has(id)) return map.get(id); + + const res = await fetch(localuser.info.api + `/stickers/${id}`, { + headers: localuser.headers, + }); + if (res.status === 403) { + map.set(id, undefined); + return undefined; + } + const json = (await res.json()) as stickerJson; + map.set(id, json); + return new Sticker(json, localuser); + } + static async stickerPicker(x: number, y: number, localuser: Localuser): Promise { + let res: (r: Sticker) => void; + this; + const promise: Promise = new Promise((r) => { + res = r; + }); + const menu = document.createElement("div"); + menu.classList.add("flexttb", "stickerPicker"); + if (y > 0) { + menu.style.top = y + "px"; + } else { + menu.style.bottom = y * -1 + "px"; + } + if (x > 0) { + menu.style.left = x + "px"; + } else { + menu.style.right = x * -1 + "px"; + } + + const topBar = document.createElement("div"); + topBar.classList.add("flexltr", "emojiHeading"); + const guilds = [ + localuser.lookingguild, + ...localuser.guilds.filter((guild) => guild !== localuser.lookingguild), + ] + .filter((guild) => guild !== undefined) + .filter((guild) => guild.id != "@me" && guild.stickers.length > 0); + if (guilds.length === 0) { + const title = document.createElement("h2"); + title.textContent = I18n.noStickers(); + topBar.append(title); + menu.append(topBar); + Contextmenu.declareMenu(menu); + document.body.append(menu); + Contextmenu.keepOnScreen(menu); + return new Promise(() => {}); + } + const title = document.createElement("h2"); + title.textContent = guilds[0].properties.name; + title.classList.add("emojiTitle"); + topBar.append(title); + + const search = document.createElement("input"); + search.type = "text"; + topBar.append(search); + + let html: HTMLElement | undefined = undefined; + let topSticker: undefined | Sticker = undefined; + const updateSearch = () => { + if (search.value === "") { + if (html) html.click(); + search.style.removeProperty("width"); + topSticker = undefined; + return; + } + + search.style.setProperty("width", "3in"); + title.innerText = ""; + body.innerHTML = ""; + const searchResults = Sticker.searchStickers(search.value, localuser, 200); + if (searchResults[0]) { + topSticker = searchResults[0][0]; + } + for (const [sticker] of searchResults) { + const emojiElem = document.createElement("div"); + emojiElem.classList.add("stickerSelect"); + + emojiElem.append(sticker.getHTML()); + body.append(emojiElem); + + emojiElem.addEventListener("click", () => { + res(sticker); + Contextmenu.declareMenu(); + }); + } + }; + search.addEventListener("input", () => { + updateSearch.call(this); + }); + search.addEventListener("keyup", (e) => { + if (e.key === "Enter" && topSticker) { + res(topSticker); + Contextmenu.declareMenu(); + } + }); + + menu.append(topBar); + + const selection = document.createElement("div"); + selection.classList.add("flexltr", "emojirow"); + const body = document.createElement("div"); + body.classList.add("stickerBody"); + + let isFirst = true; + let i = 0; + guilds.forEach((guild) => { + const select = document.createElement("div"); + if (i === 0) { + html = select; + i++; + } + select.classList.add("emojiSelect"); + + if (guild.properties.icon) { + const img = document.createElement("img"); + img.classList.add("pfp", "servericon", "emoji-server"); + img.crossOrigin = "anonymous"; + img.src = + localuser.info.cdn + + "/icons/" + + guild.properties.id + + "/" + + guild.properties.icon + + ".png" + + new CDNParams({expectedSize: 48}); + img.alt = "Server: " + guild.properties.name; + select.appendChild(img); + } else { + const div = document.createElement("span"); + div.textContent = guild.properties.name + .replace(/'s /g, " ") + .replace(/\w+/g, (word) => word[0]) + .replace(/\s/g, ""); + select.append(div); + } + + selection.append(select); + + const clickEvent = () => { + search.value = ""; + updateSearch.call(this); + title.textContent = guild.properties.name; + body.innerHTML = ""; + for (const sticker of guild.stickers) { + const stickerElem = document.createElement("div"); + stickerElem.classList.add("stickerSelect"); + stickerElem.append(sticker.getHTML()); + body.append(stickerElem); + stickerElem.addEventListener("click", () => { + res(sticker); + Contextmenu.declareMenu(); + }); + } + }; + + select.addEventListener("click", clickEvent); + if (isFirst) { + clickEvent(); + isFirst = false; + } + }); + + Contextmenu.declareMenu(menu); + document.body.append(menu); + Contextmenu.keepOnScreen(menu); + menu.append(selection); + menu.append(body); + search.focus(); + return promise; + } +} +export {Sticker}; diff --git a/src/webpage/style.css b/src/webpage/style.css index 6e94355b..bd3c48a5 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -3,7 +3,7 @@ body { height: 100%; margin: 0; background: var(--secondary-bg); - font-family: var(--font); + font-family: var(--font), emojiFont; color: var(--primary-text); overflow: hidden; /* avoid "bounce" */ -webkit-text-size-adjust: 100%; @@ -12,58 +12,516 @@ body { height: 100svh; background: var(--primary-bg); } +.emojiJoin { + background: var(--green); + margin-top: 6px; + &:hover { + background: color-mix(in srgb, var(--green), 20% white); + } +} +.mutFriends { + display: flex; + flex-direction: column; + align-items: stretch; + + .createdWebhook { + flex-grow: 1; + background: var(--primary-bg); + margin-bottom: 6px; + width: 100%; + box-sizing: border-box; + } +} +.createdWebhook { + display: flex; + align-items: center; + width: fit-content; + padding: 0.1in; + border-radius: 0.1in; + background: var(--secondary-bg); + user-select: none; + cursor: pointer; + + .pfpDiv { + width: 0.5in !important; + height: 0.5in !important; + margin-right: 0.15in !important; + } +} +.Mplayer { + padding: 4px; + border-radius: 3px; + background: var(--secondary-bg); + align-items: center; + flex-grow: 0; + flex-shrink: 0; + height: 52px; + position: relative; + + * { + margin: 2px; + accent-color: var(--primary-text-soft); + flex-shrink: 1; + } + .flexttb { + display: flex; + align-items: center; + } +} +.bandiv { + padding: 10px; + align-items: center; + margin-bottom: 8px; + background: var(--secondary-bg); + border-radius: 6px; + cursor: pointer; + div { + margin-left: 8px; + } +} +.templateMiniBox { + display: flex; + align-items: center; + padding: 6px; + border-radius: 4px; + width: fit-content; + background: var(--secondary-bg); + margin-bottom: 8px; + span { + margin-right: 4px; + } + button { + margin-left: 4px; + } +} +.adminList { + width: 95%; + background: var(--secondary-bg); + padding: 6px; + margin-bottom: 6px; + border-radius: 10px; + cursor: pointer; + .pfpDiv { + width: 48px; + height: 48px; + } + .flexttb { + margin-left: 10px; + display: flex; + justify-content: center; + + .flexltr { + span { + margin-right: 4px; + text-wrap: nowrap; + background: var(--primary-bg); + padding: 2px; + border-radius: 3px; + margin-bottom: 4px; + } + flex-wrap: wrap; + } + } +} +.buttonDanger { + --interColor: var(--red); +} +.buttonSuccess { + --interColor: var(--green); +} +.buttonLink { + --interColor: var(--button-bg); +} +.buttonPrimary { + --interColor: var(--primary-button-bg); +} +.buttonPremium { + --interColor: purple; +} +.buttonSecondary { + --interColor: var(--button-bg); +} +.interButton { + background: var(--interColor); + margin-right: 4px; +} +.interButton:hover { + background: color-mix(in srgb, var(--interColor), black 30%); +} + +.compAppStatus { + padding-left: 50px; + opacity: 0.6; + font-size: 12px; + &.failed { + color: var(--red); + } +} +.dmline { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 14px; +} +.commandinput { + display: inline-flex !important; + width: fit-content; + margin-left: 6px; + align-items: center; + background: var(--primary-bg); + padding-left: 4px; + border-radius: 4px; + input { + padding: 5px !important; + margin: 0px !important; + margin-left: 5px !important; + height: 100%; + field-sizing: content; + } +} .flexltr { min-height: 0; display: flex; } +.sessionDiv { + padding: 8px; + background: #00000040; + border-radius: 4px; + cursor: pointer; +} +.trustedDomain { + display: flex; + align-items: center; + background: #00000026; + padding: 8px; + border-radius: 4px; + justify-content: space-between; +} +.ephemeralDiv { + padding-left: 50px; + opacity: 0.6; + span { + padding-right: 6px; + } + font-size: 13px; +} +.traceBars { + height: 30px; +} +#recentBlog { + padding-top: 12px; + gap: 12px; +} +#chatArea { + display: flex; + min-width: 40%; +} .flexttb { min-width: 0; display: flex; flex-direction: column; } -.searchOptions{ - padding:.05in .15in; - border-radius: .1in; +.mediaDisp { + > * { + height: 150px; + margin-bottom: 4px; + > * { + height: 150px !important; + margin-right: 4px; + border-radius: 6px; + img { + height: 150px; + object-fit: cover; + } + } + } +} +.messageComps { + > .flexltr { + display: flex; + flex-wrap: wrap; + } +} +.reactionList { + div { + padding: 10px; + font-size: 18px; + margin: 3px 0px; + background: var(--secondary-bg); + border-radius: 6px; + cursor: pointer; + :first-child { + margin-right: 4px; + } + &.current { + background: color-mix(in srgb, var(--secondary-bg) 60%, var(--black)); + } + } + margin-right: 4px; +} +.reactionUserList { + .pfpDiv { + width: 32px; + height: 32px; + } + border-left: solid var(--secondary-bg) 2px; + padding-left: 6px; +} +.searchNavButtons { + height: 0.3in; + flex-shrink: 0; + background: var(--secondary-bg); + top: 0px; + padding: 0.1in; + display: flex; + align-items: center; + margin-top: auto; + + button { + flex-shrink: 0; + margin-left: 0.1in; + } +} +.sortBar { + height: 0.3in; + flex-shrink: 0; + background: var(--secondary-bg); + position: sticky; + top: 0px; + padding: 0.1in; + display: flex; + align-items: center; + z-index: 1; + + button { + flex-shrink: 0; + margin-left: 0.1in; + } +} +.selectedB { + background: color-mix(in srgb, black, var(--button-bg) 60%); +} +.pinnedMessages { + position: absolute; + background: color-mix(in srgb, var(--primary-bg) 20%, var(--secondary-bg)); + width: 3.5in; + padding: 8px; + border-radius: 6px; + box-shadow: 1px 2px 20px var(--shadow); + max-height: 60vh; + overflow-y: auto; + min-height: 1in; + z-index: 2; + /* border: solid 2px #00000061; */ + + .noPins { + height: 1in; + display: flex; + align-items: center; + justify-content: center; + } +} +.searchOptions { + padding: 0.05in 0.15in; + border-radius: 0.1in; background: var(--channels-bg); - position:absolute; - bottom:0; + position: absolute; + bottom: 0; width: calc(100% - 32px); box-sizing: border-box; + > span { - transition: background .1s; - margin-bottom:.025in; - margin-top:.025in; - padding:.075in .05in; - border-radius:.03in; - cursor:pointer; - > span, img{ - margin-right:.05in; + transition: background 0.1s; + margin-bottom: 0.025in; + margin-top: 0.025in; + padding: 0.075in 0.05in; + border-radius: 0.03in; + cursor: pointer; + > span, + img { + margin-right: 0.05in; } } - span.selected{ - background:var(--button-bg); + span.selected { + background: var(--button-bg); } - span:hover{ - background:var(--button-bg); + span:hover { + background: var(--button-bg); } + margin: 16px; - border: solid .025in var(--black); + border: solid 0.025in var(--black); + z-index: 1; } -.searchOptions:empty{ +.searchOptions:empty { padding: 0; border: 0; } -.messageEditContainer{ +#player { + flex-grow: 0; + + input { + width: 100px; + } +} +#player:empty { + height: 0px; +} +.messageEditContainer { + position: relative; + width: 100%; +} +#voiceArea:empty { + display: none; +} +#voiceArea { + background: var(--black); position: relative; - width:100%; + min-width: 60%; +} +.voiceUsers { + padding: 20px; + display: flex; + flex-direction: row; + flex-wrap: wrap; +} +.speaking { + outline: 3px solid var(--green); +} +.voiceUsers > :hover .leave { + bottom: 10px; + opacity: 1; +} +.leave { + position: absolute; + bottom: 0px; + right: 10px; + background: var(--red); + opacity: 0; + transition: + bottom 0.4s, + opacity 0.2s, + background 0.1s; +} +.leave:hover { + background: color-mix(in srgb, var(--red) 85%, white); +} + +.voiceUsers > * { + background: var(--accent_color, var(--primary-bg)); + border-radius: 8px; + position: relative; + box-sizing: border-box; + margin: 8px; + width: 340px; + aspect-ratio: 3/2; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + .pfpDiv { + width: 60px; + height: 60px; + cursor: unset; + } + video { + position: absolute; + flex-grow: 1; + flex-shrink: 1; + } +} +.voiceUsers:has(.bigBox) > * { + width: 220px; + flex-grow: 0; + flex-shrink: 0; +} +.bigBox { + width: min(80%, 600px) !important; + height: unset; + margin: 0 calc((100% - min(80%, 600px)) / 2); +} + +.buttonRow > * { + position: relative; + margin-right: 6px; + width: 54px; + height: 54px; + background: var(--secondary-hover); + border-radius: 100%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 1; + + * { + width: 32px !important; + height: 32px !important; + background: var(--primary-text); + } +} +.muteOptDiv { + background: var(--secondary-hover); + position: absolute; + display: flex; + top: 34px; + left: 34px; + width: 16px !important; + height: 16px !important; + border: solid 3px var(--black); + border-radius: 100%; + align-items: center; + justify-content: center; + span { + width: 12px !important; + height: 12px !important; + background: white; + display: block; + } +} +.buttonRow { + position: absolute; + bottom: 20px; + width: 100%; + display: flex; + justify-content: center; +} +.voiceUsername { + position: absolute; + bottom: 10px; + left: 14px; + background: var(--secondary-bg); + padding: 4px; + border-radius: 6px; + z-index: 1; } .flexgrow { flex-grow: 1; min-height: 0; } -p, h1, h2, h3, pre, form { + +.channelSTitle { + margin-top: 0.2in; + margin-bottom: 0; + margin-left: 10px; +} +p, +h1, +h2, +h3, +pre, +form { margin: 0; } +#inviteBanner { + img { + height: 30vh; + width: 80vw; + object-fit: cover; + margin: -24px; + margin-bottom: 0px; + } +} h2:empty { display: none; } @@ -71,20 +529,33 @@ h2:empty { font-size: 1.5rem; font-weight: bold; } +.cunread { + > button > .ellipsis { + font-weight: bold; + color: var(--primary-text); + } +} .ellipsis { display: -webkit-box; word-break: break-all; overflow: hidden; -webkit-box-orient: vertical; + line-clamp: 1; -webkit-line-clamp: 1; } -a, .clickable { +a, +.clickable { + samp { + color: var(--link); + background: color-mix(in srgb, var(--code-bg), black); + } color: var(--link); text-decoration: none; word-break: break-word; cursor: pointer; } -a:hover, .clickable:hover { +a:hover, +.clickable:hover { text-decoration: underline; } .clickable { @@ -95,20 +566,59 @@ hr { background: var(--divider); border: none; } -pre, samp { +pre, +samp { background: var(--code-bg); color: var(--code-text); text-wrap: wrap; word-break: break-word; + padding: 1px; + border-radius: 2px; } -video, iframe { - max-height: 50svh; +video { + max-height: 100%; + max-width: 100%; +} +.dateline { + hr { + width: 100%; + height: 1px; + margin: 0.05in; + } + span { + flex-shrink: 0; + } + align-items: center; +} +iframe { + /* max-height: 50svh; */ max-width: 100%; } audio::-webkit-media-controls-panel { background: var(--secondary-bg); } -button, input::file-selector-button, select { +.joinb { + background: var(--green); + border-radius: 200px; + transition: background 0.2s; +} +.joinb:hover { + background: color-mix(in srgb, var(--green) 80%, transparent); +} +.discoverButton { + width: calc(100% - 10px); + text-align: center; + margin: 5px; + box-sizing: border-box; + background: #00000029; + padding: 10px; + &.selected { + background: #00000050; + } +} +button, +input::file-selector-button, +select { padding: 6px 10px; background: var(--button-bg); font-size: 1rem; @@ -118,15 +628,17 @@ button, input::file-selector-button, select { border: none; border-radius: 4px; cursor: pointer; - transition: background .1s ease-in-out; + transition: background 0.1s ease-in-out; } input::file-selector-button { margin-right: 6px; } -button:hover, input::file-selector-button:hover { +button:hover, +input::file-selector-button:hover { background: var(--button-hover); } -button:active:enabled, input::file-selector-button:active { +button:active:enabled, +input::file-selector-button:active { border: none; } button:disabled { @@ -146,6 +658,7 @@ select { .fileinputdiv { width: 100%; justify-content: space-between; + margin-top: 4px; } input[type="file"] { flex: 1 0; @@ -169,6 +682,86 @@ textarea { height: 144px; resize: none; } +.fancySelect { + position: relative; + padding: 6px; + background: var(--text-input-bg); + border-radius: 6px; + flex-shrink: 1; + display: flex; + flex-direction: row; + flex-wrap: wrap; + width: fit-content; + flex-grow: 1; + + > input { + padding: 0px !important; + width: fit-content !important; + min-width: 96px; + flex-grow: 1; + } + .selected { + padding: 2px; + background: var(--blue); + border-radius: 4px; + cursor: default; + user-select: all; + display: flex; + align-items: center; + margin-right: 5px; + + span { + width: 16px; + height: 16px; + flex-shrink: 0; + display: block; + background: var(--primary-text); + margin: 0 3px; + cursor: pointer; + } + } +} +.fancyOptions { + position: absolute; + bottom: calc(100% + 5px); + background: var(--secondary-bg); + padding: 5px; + width: 100%; + box-sizing: border-box; + border-radius: 6px; + > div { + padding: 5px; + &:hover { + background-color: #00000061; + } + margin-top: 5px; + cursor: pointer; + transition: background-color 0.2s; + border-radius: 6px; + p { + font-size: 14px; + opacity: 0.6; + } + } + input { + margin-left: auto; + width: 18px; + height: 18px; + } + + z-index: 1; + animation-duration: 0.2s; + animation-name: fade-in-back; + animation-fill-mode: forwards; + animation-timing-function: ease-out; + max-height: 50vh; + overflow-y: auto; + left: 0; + + &.removeElm { + animation-name: fade-in-back-out; + } +} input[type="text"], input[type="password"], input[type="search"], @@ -190,6 +783,9 @@ textarea { ::-webkit-calendar-picker-indicator { opacity: 1; } +::placeholder { + color: color-mix(in srgb, var(--primary-text-soft), transparent); +} :focus-visible { outline: 2px solid var(--focus); outline-offset: 0; @@ -197,84 +793,550 @@ textarea { #typebox:focus-visible { outline: none; } - +.voiceTray { + margin-left: auto; + span { + width: 16px; + height: 16px; + margin: 1px; + margin-left: 4px; + background: var(--primary-text-soft); + } +} /* Icons */ .pfpDiv { position: relative; + background: none !important; + aspect-ratio: 1; + width: 32px; + height: 32px; } -.pfp, .replypfp { +.pfp, +.replypfp { display: block; - height: 32px; - width: 32px; + height: 100%; + width: 100%; border-radius: 50%; + object-fit: cover; cursor: pointer; user-select: none; } +.leftArrow { + z-index: 1; + width: 42px; + height: 42px; + display: block; + background: var(--primary-text-soft); + position: absolute; + top: calc(50% - 42px); + left: 4px; + cursor: pointer; +} +.rightArrow { + z-index: 1; + width: 42px; + height: 42px; + display: block; + background: var(--primary-text-soft); + position: absolute; + top: calc(50% - 42px); + right: 4px; + cursor: pointer; +} +.svg-mediaSettings { + width: 15px; + height: 30px; + background: var(--primary-text); + cursor: pointer; + display: block; + mask-size: contain !important; +} +.svg-mediaButton { + width: 30px; + height: 30px; + background: var(--primary-text); + cursor: pointer; + display: block; + mask-size: cover !important; +} +.svg-crown { + width: 16px; + height: 16px; + background: var(--yellow) !important; + mask: url(./icons/crown.svg); + mask-size: cover !important; + flex-shrink: 0; + cursor: pointer; + margin-left: -2px !important; +} +.svg-history { + width: 36px; + height: 36px; + background: var(--primary-text-soft); + mask: url(./icons/history.svg); + mask-size: cover !important; + flex-shrink: 0; + cursor: pointer; + + &:hover { + background: var(--black); + } +} +.svg-timeout { + mask: url(./icons/timeout.svg); + width: 16px; + height: 16px; + mask-size: cover !important; + background: var(--red); + margin-left: 6px; + margin-right: -2px; +} +.svg-folder { + mask: url(./icons/folder.svg); + background-color: var(--folder-color, var(--accent-color)); + mask-size: contain !important; + width: 36px; + height: 36px; +} +.messageimgdiv:hover { + .svg-gifstar { + top: 4px; + } +} +.svg-gifstar { + mask: url(./icons/gifstar.svg); + mask-size: contain !important; + width: 24px; + height: 24px; + position: absolute; + top: -30px; + right: 5px; + transition: top 0.2s; + transition-timing-function: ease-in; + background: white; + cursor: pointer; + &.favorited { + background: yellow; + } +} +.visually-hidden { + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: pre-wrap; + width: 1px; +} +.svg-stream { + mask: url(./icons/stream.svg); + mask-size: contain !important; + background: var(--primary-text); +} +.svg-select { + mask: url(./icons/select.svg); + mask-size: contain !important; + background: var(--primary-text); +} +.svg-noSelect { + mask: url(./icons/noSelect.svg); + mask-size: contain !important; + background: var(--primary-text); +} +.svg-stopstream { + mask: url(./icons/stopstream.svg); + mask-size: contain !important; + background-color: var(--red); +} +.svg-video { + mask: url(./icons/video.svg); + mask-size: contain !important; + background: var(--primary-text); +} +.svg-novideo { + mask: url(./icons/novideo.svg); + mask-size: contain !important; + background: var(--red); +} +.svg-call { + mask: url(./icons/call.svg); + mask-size: contain !important; +} +.svg-hangup { + mask: url(./icons/hangup.svg); + mask-size: contain !important; + background: var(--red); +} +.svg-plainx { + mask: url(./icons/plainx.svg); + mask-size: contain !important; +} +.svg-sticker { + mask: url(./icons/sticker.svg); + mask-size: contain !important; +} +.svg-search { + mask: url(./icons/search.svg); + mask-size: contain !important; +} +.svg-spoiler { + mask: url(./icons/spoiler.svg); + mask-size: contain !important; +} +.svg-pin { + mask: url(./icons/pin.svg); + mask-size: cover; +} +.svg-rules { + mask: url(./icons/rules.svg); + mask-size: cover; +} +.svg-unspoiler { + mask: url(./icons/unspoiler.svg); + mask-size: contain !important; +} +.svg-soundMore { + mask: url(./icons/soundMore.svg); + mask-size: contain !important; +} +.svg-play { + mask: url(./icons/play.svg); + mask-size: contain !important; +} +.svg-pause { + mask: url(./icons/pause.svg); + mask-size: contain !important; +} .svg-announce { - mask: url(/icons/announce.svg); + mask: url(./icons/announce.svg); + mask-size: contain !important; } .svg-emoji { - mask: url(/icons/emoji.svg); + mask: url(./icons/emoji.svg); + mask-size: contain !important; +} +.svg-gif { + mask: url(./icons/gif.svg); + mask-size: contain !important; } .svg-edit { - mask: url(/icons/edit.svg); + mask: url(./icons/edit.svg); + mask-size: contain !important; } .svg-reply { - mask: url(/icons/reply.svg); + mask: url(./icons/reply.svg); + mask-size: contain !important; } .svg-delete { - mask: url(/icons/delete.svg); + mask: url(./icons/delete.svg); + mask-size: contain !important; } .svg-category { - mask: url(/icons/category.svg); + mask: url(./icons/category.svg); + mask-size: contain !important; +} +.svg-intoMenu { + mask: url(./icons/intoMenu.svg); + mask-size: contain !important; +} +.svg-leftArrow { + mask: url(./icons/leftArrow.svg); + mask-size: contain !important; } .svg-channel { - mask: url(/icons/channel.svg); + mask: url(./icons/channel.svg); + mask-size: contain !important; +} +.svg-forum { + mask: url(./icons/forum.svg); + mask-size: contain !important; +} +.svg-forumnsfw { + mask: url(./icons/forumnsfw.svg); + mask-size: contain !important; } .svg-copy { - mask: url(/icons/copy.svg); + mask: url(./icons/copy.svg); + mask-size: contain !important; } .svg-explore { - mask: url(/icons/explore.svg); + mask: url(./icons/explore.svg); + mask-size: contain !important; } .svg-home { - mask: url(/icons/home.svg); + mask: url(./icons/home.svg); + mask-size: contain !important; } .svg-settings { - mask: url(/icons/settings.svg); + mask: url(./icons/settings.svg); + mask-size: contain !important; } .svg-voice { - mask: url(/icons/voice.svg); + mask: url(./icons/voice.svg); + mask-size: contain !important; } .svg-plus { - mask: url(/icons/plus.svg); + mask: url(./icons/plus.svg); + mask-size: contain !important; } .svg-upload { - mask: url(/icons/upload.svg); - width: .2in !important; + mask: url(./icons/upload.svg); + mask-size: contain !important; + width: 0.2in !important; cursor: pointer; } .svg-x { - mask: url(/icons/x.svg); + mask: url(./icons/x.svg); + mask-size: contain !important; +} +.svg-update { + mask: url(./icons/update.svg); + mask-size: cover !important; + width: 24px !important; + height: 24px !important; + margin-right: 0 !important; + background: var(--update) !important; + cursor: pointer; } -.svg-friends{ - mask: url(/icons/friends.svg); +.svg-friends { + mask: url(./icons/friends.svg); width: 24px !important; height: 24px !important; margin-right: 0 !important; + mask-size: contain !important; +} +.svg-frmessage { + mask: url(./icons/frmessage.svg); + mask-size: contain !important; +} +.svg-addfriend { + mask: url(./icons/addfriend.svg); + mask-size: contain !important; +} + +.svg-channelnsfw { + mask: url(./icons/channelnsfw.svg); + mask-size: contain !important; } -.svg-frmessage{ - mask: url(/icons/frmessage.svg); +.svg-announcensfw { + mask: url(./icons/announcensfw.svg); + mask-size: contain !important; } -.svg-addfriend{ - mask: url(/icons/addfriend.svg); +.svg-voicensfw { + mask: url(./icons/voicensfw.svg); + mask-size: contain !important; } + .svgicon { display: block; height: 100%; width: 100%; background: var(--primary-text-soft); mask-repeat: no-repeat; + aspect-ratio: 1/1; + flex-shrink: 0; + color: transparent; + &[hidden] { + display: none; + } +} +.forumIcon { + width: 28px; + height: 28px; +} +.forumHead { + width: calc(100% - 12px); + background: var(--dock-bg); + padding: 6px; + margin: 6px; + box-sizing: border-box; + border-radius: 4px; + flex: 0; + min-height: fit-content; + height: max-content; + flex-shrink: 0; + /* overflow: clip; */ + + > .flexltr { + width: 100%; + align-items: center; + } + .editMessage { + margin-top: 6px; + } + .searchOptions { + top: 32px; + bottom: unset; + } + .forumTagSelect { + margin-top: 6px; + > * { + margin-left: 0px; + margin-right: 6px; + } + } +} +.newPostForumButton { + width: 100px; + height: 34px; + padding: 6px; + transition: + width 0.15s, + padding 0.15s; + overflow: clip; + text-overflow: clip; + white-space: nowrap; + text-align: center; +} +.forumHead:has(input:placeholder-shown) { + .newPostForumButton { + width: 0px; + padding: 0px; + } +} +.forumBody { + height: 100%; +} +.forumList { + flex: 1; + overflow-y: scroll; +} +.allTagButton { + margin-left: auto; +} +.forumSortRow { + flex-shrink: 0; + padding: 4px 12px; + align-items: center; +} +.forumTagSelect { + display: flex; + flex-direction: row; + flex-wrap: wrap; + overflow-y: clip; + min-height: fit-content; + + > * { + margin-left: 6px; + } + .selected { + background: #ffffff5e; + } +} +.forumTag { + padding: 3px; + background: #ffffff21; + border-radius: 12px; + cursor: pointer; +} +.forumPostError { + color: var(--red); +} +.forumPostBody { + background: var(--secondary-bg); + margin: 4px 12px; + padding: 6px; + border-radius: 4px; + > * { + margin-bottom: 4px; + } + cursor: pointer; +} +.forumButtonRow { + flex-shrink: 0; + mix-height: fit-content; + padding: 12px; + button { + margin-right: 10px; + } +} +.forumMessageRow { + .svg-frmessage { + width: 16px; + margin-right: 4px; + } + display: flex; + align-items: center; +} +.msep { + margin: 6px; + background: #000000de; + width: 3px; + height: 3px; + border-radius: 100%; +} +.forumSearch { + margin-left: 3px; + background: transparent !important; +} +.svg-mic { + height: 22px; + width: 22px; + margin: 6px; + mask: url(./icons/mic.svg); + mask-size: contain !important; +} +.svg-micmute { + height: 22px; + width: 22px; + margin: 6px; + mask: url(./icons/micmute.svg); + background-color: var(--red); + mask-size: contain !important; +} +.mobileback { + visibility: hidden; + height: 0px; + width: 0px; +} +#searchX { + width: 0.16in; + height: 0.16in; + position: absolute; + right: 13px; + top: 6px; +} +#searchX.svg-plainx { + cursor: pointer; +} +#pinnedM { + width: 0.25in; + height: 0.25in; + cursor: pointer; +} +.unreadPin { + position: relative; +} +.unreadPin::after { + width: 0.1in; + height: 0.1in; + content: ""; + background: var(--red); + position: absolute; + right: 0px; + bottom: -5px; + border-radius: 1in; +} +#stickerTB { + width: 0.2in; + height: 0.2in; + cursor: pointer; + flex-shrink: 0; + margin-left: 6px; +} +#emojiTB { + width: 0.2in; + height: 0.2in; + cursor: pointer; + flex-shrink: 0; + margin-left: 6px; +} +#gifTB { + width: 0.2in; + height: 0.2in; + cursor: pointer; + flex-shrink: 0; + mask-size: 0.2in 0.2in; } .selectarrow { position: absolute; @@ -284,38 +1346,126 @@ textarea { width: 12px; pointer-events: none; } -.hoverthing{ - position:absolute; - background:var(--dock-bg); - padding:.04in; - border-radius:.05in; - transform: translate(-50%, 0); - transition: opacity .2s; - border: solid .03in var(--black); +.hoverthing { + position: absolute; + background: var(--dock-bg); + padding: 0.04in; + border-radius: 0.05in; + pointer-events: none; + animation-duration: 0.1s; + animation-name: fade-in-hover; + animation-timing-function: ease-out; + border: solid 0.03in var(--dock-bg); + z-index: 200; +} +@keyframes fade-in-hover { + 0% { + opacity: 0; + user-select: none; + } + 0.1% { + opacity: 0; + transform: scale(0.9) translateY(-10px); + user-select: none; + } + + 100% { + opacity: 1; + } +} +.errorHTML { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + height: 100vh; + justify-content: center; + align-content: center; + h3 { + margin-bottom: 15px; + } + > div { + background: var(--primary-bg); + padding: 15px; + border-radius: 10px; + height: 60%; + width: 60%; + box-shadow: 5px 5px 5px black; + display: flex; + flex-direction: column; + + > div { + padding: 10px; + box-shadow: 0px 0px 5px black; + border-radius: 10px; + margin-top: auto; + } + } + img { + background: var(--primary-bg); + padding: 10px; + border-radius: 10px 10px 0 0; + box-shadow: 5px 5px 5px black; + } } -.editMessage{ +.editMessage { background: var(--typebox-bg); - padding: .05in; - border-radius: .04in; + padding: 0.05in; + border-radius: 0.04in; + white-space: preserve-breaks; } -#gimmefile{ +#gimmefile { position: absolute; width: 100%; height: 100%; background: #00000070; - top:0px; + top: 0px; + display: flex; + justify-content: center; + align-items: center; + z-index: 99; + &[hidden] { + display: none; + } +} +.commandError { + position: absolute; + top: -36px; + left: 34px; + z-index: 1; + background: var(--red); + padding: 6px; + border-radius: 4px; + &.removeElm { + animation-duration: 4s; + animation-name: errorElm; + animation-fill-mode: forwards; + animation-timing-function: ease-in-out; + } +} +@keyframes errorElm { + 100%, + 0% { + opacity: 0; + } + 5%, + 95% { + opacity: 1; + } } /* Animations */ @keyframes fade { - 0%, 100% { - opacity: .2; + 0%, + 100% { + opacity: 0.2; } 50% { opacity: 1; } } @keyframes jumped { - 0%, 100% { + 0%, + 100% { background: transparent; } 50% { @@ -343,14 +1493,19 @@ textarea { ::-webkit-scrollbar-thumb:hover { background: var(--primary-text-soft); } -#sideDiv:empty{ - width:0px; - padding:0; -} -#servers::-webkit-scrollbar, #channels::-webkit-scrollbar, #sideDiv::-webkit-scrollbar { +#servers::-webkit-scrollbar, +#channels::-webkit-scrollbar, +#sideContainDiv::-webkit-scrollbar { display: none; } -#servers, #channels, #sideDiv { +#sideContainDiv:has(#sideDiv:empty):has(#player:empty) { + width: 0px; + margin: 0px; + padding: 0px; +} +#servers, +#channels, +#sideDiv { scrollbar-width: none; } @@ -358,7 +1513,7 @@ textarea { #titleDiv { padding: 8px; background: var(--primary-bg); - font-size: .8rem; + font-size: 0.8rem; display: flex; justify-content: center; align-items: center; @@ -415,11 +1570,11 @@ h1.pagehead { } .instance span { margin-bottom: 4px; - font-size: .9rem; + font-size: 0.9rem; color: var(--secondary-text-soft); } span.instanceStatus { - font-size: .75rem; + font-size: 0.75rem; } .instancetextbox h2 { margin-bottom: 4px; @@ -428,7 +1583,8 @@ span.instanceStatus { } /* Login/Invite */ -#logindiv, #invitebody { +#logindiv, +#invitebody { position: absolute; top: 50%; left: 50%; @@ -442,13 +1598,17 @@ span.instanceStatus { box-sizing: border-box; overflow-y: auto; } -#logindiv label, #TOSbox { +#logindiv label, +#TOSbox { display: inline-block; margin-top: 12px; } #verify { color: var(--primary-text-soft); } +.verify { + color: var(--primary-text-soft); +} #TOS { vertical-align: middle; margin-bottom: 4px; @@ -457,6 +1617,15 @@ span.instanceStatus { margin: 16px 0; text-align: center; } +.createAccount { + width: 94%; + padding: 8px; + margin-bottom: 16px; + font-size: 1.15rem; + font-weight: bold; + text-align: center; + box-sizing: border-box; +} #logindiv button { width: 100%; padding: 8px; @@ -471,7 +1640,13 @@ span.instanceStatus { flex-direction: column; align-items: center; gap: 8px; + box-shadow: 1px 1px 15px 0px black; + + &.moreShadow { + box-shadow: 1px 1px 20px 10px black; + } } + .inviteGuild { height: 64px; width: 64px; @@ -513,14 +1688,14 @@ span.instanceStatus { /* Loading */ #loading { - z-index: 100; + z-index: 110; position: absolute; height: 100%; width: 100%; background: var(--loading-bg, inherit) !important; color: var(--loading-text); text-align: center; - transition: transform .2s; + transition: transform 0.2s; overflow: hidden; /* keep size if window height is too small */ } #loading.doneloading { @@ -531,6 +1706,11 @@ span.instanceStatus { flex: 0 0 100%; /* push out scroller until loaded */ overflow-y: auto; flex-direction: column-reverse; + position: absolute; + background: var(--primary-bg); + z-index: 3; + width: 100%; + height: 100%; } #switchaccounts { color: var(--link); @@ -557,7 +1737,28 @@ span.instanceStatus { background: var(--typebox-bg); border-radius: 8px; } - +.folder-icon-div { + width: 44px; + height: 44px; + display: flex; + justify-content: center; + align-items: center; + background: var(--folder-bg); + border-radius: 16px; + margin: 2px; + margin-bottom: 8px; + cursor: pointer; +} +.folder-div { + background: color-mix(in srgb, var(--primary-text-soft), 70% transparent); + padding: 3px; + margin: -3px; + border-radius: 16px; + margin-bottom: 6px; +} +.guilds-div-folder { + transition: height 0.2s; +} /* Server List */ #servers { flex: 0 0 auto; @@ -567,19 +1768,21 @@ span.instanceStatus { overflow-y: auto; user-select: none; } -.servericon, #sentdms .pfp { +.servericon, +#sentdms .pfpDiv { height: 48px; width: 48px; - margin-bottom: 6px; + margin-bottom: 8px; border-radius: 50%; - overflow: hidden; display: flex; align-items: center; justify-content: center; cursor: pointer; - transition: border-radius .2s; + transition: border-radius 0.2s; } -.servericon:hover, .serveropen .servericon, #sentdms .pfp:hover { +.servericon:hover, +.serveropen .servericon, +#sentdms .pfp:hover { border-radius: 30%; } .home .svgicon { @@ -596,10 +1799,11 @@ span.instanceStatus { .lightbr { margin: 8px 4px; } -.blankserver, .home { +.blankserver, +.home { background: var(--blank-bg); } -.servernoti, .home { +.servernoti { position: relative; } .unread { @@ -609,15 +1813,25 @@ span.instanceStatus { width: 8px; background: var(--primary-text); border-radius: 4px; - transition: transform .2s, height .2s; + transition: + transform 0.2s, + height 0.2s; +} +.servernoti:hover .unread.pinged { + transform: translate(34px, 14px); + height: 20px; } .servernoti:hover .unread { - transform: translate(-12px, 12px); + transform: translate(-12px, 12px); height: 24px; } .serveropen .unread { transform: translate(-12px, 8px) !important; height: 32px !important; + width: 8px !important; +} +.serveropen .unread.pinged { + color: transparent; } .notiunread { transform: translate(-12px, 20px); @@ -625,18 +1839,20 @@ span.instanceStatus { #sentdms { position: relative; } -.unread.pinged, .servernoti:hover .unread.pinged { +.unread.pinged { height: 16px; width: 16px; - transform: translate(28px, 28px); + transform: translate(34px, 34px); background: var(--red); - font-size: .75rem; + font-size: 0.75rem; font-weight: bold; line-height: 15px; text-align: center; - border: 4px solid var(--servers-bg); - border-radius: 50%; + /* border: 4px solid var(--servers-bg); */ + /* border-radius: 50%; */ pointer-events: none; + z-index: 0; + top: 0px; } /* Channel Panel */ @@ -646,6 +1862,10 @@ span.instanceStatus { background: var(--channels-bg); user-select: none; } +.searchMeta { + position: relative; + display: flex; +} .header { flex: none; height: 48px; @@ -655,8 +1875,14 @@ span.instanceStatus { align-items: center; user-select: none; } +.Banner { + height: 100px; + align-items: start; + padding-top: 10px; + background-size: cover; +} #serverName { - font-size: 1.15rem; + font-size: 1rem; } #channels { flex: 1; @@ -664,9 +1890,36 @@ span.instanceStatus { color: var(--primary-text-soft); overflow-y: auto; } +.threads { + button { + position: relative; + } + button::before { + content: ""; + position: absolute; + top: 0px; + left: 0px; + height: calc(50% - 1px); + width: 3px; + border-bottom: 2px solid var(--reply-line); + border-left: 2px solid var(--reply-line); + border-bottom-left-radius: 8px; + } + button::after { + content: ""; + position: absolute; + bottom: 0px; + left: 0px; + height: calc(50% - 1px); + width: 3px; + border-top: 2px solid var(--reply-line); + border-left: 2px solid var(--reply-line); + border-top-left-radius: 8px; + } +} .channels { overflow-y: hidden; - transition: height .2s ease-in-out; + transition: height 0.2s ease-in-out; } #channels > div > div:first-child { margin-top: 6px; @@ -675,6 +1928,16 @@ span.instanceStatus { margin: 0 6px; display: flex; flex-direction: column; + margin-bottom: 2px; + position: relative; + &.muted { + span { + color: color-mix(in srgb, var(--primary-text-soft), transparent); + &.svgicon { + background: color-mix(in srgb, var(--primary-text-soft), transparent); + } + } + } } .channelbutton { height: 2em; @@ -683,7 +1946,7 @@ span.instanceStatus { color: var(--primary-text-soft); display: flex; align-items: center; - transition: font-weight .1s; + transition: font-weight 0.1s; } .channelbutton:hover { background: var(--channel-hover); @@ -692,31 +1955,35 @@ span.instanceStatus { .channels .channelbutton { margin-left: 8px; } -.viewChannel .channelbutton, .viewChannel .channelbutton:hover { +.viewChannel > .channelbutton, +.viewChannel > .channelbutton:hover { background: var(--channel-selected); font-weight: bold; color: var(--primary-text-prominent); } + .cunread { position: relative; color: var(--primary-text-prominent); } .cunread:after { - content: ''; + content: ""; position: absolute; - top: calc(50% - 4px); + top: 8px; left: -10px; - height: 8px; - width: 8px; + height: 16px; + width: 7px; background: var(--primary-text); border-radius: 50%; } +.cunread.mentioned:after { + background: var(--red); +} .space { flex: none; height: 1em; width: 1em; margin-right: 8px; - background: var(--primary-text-soft); } .capsflex { margin-top: 8px; @@ -737,11 +2004,27 @@ span.instanceStatus { height: 12px; width: 12px; margin-right: 6px; - transition: rotate .2s; + transition: rotate 0.2s; } .hiddencat { rotate: -90deg; } +.statBub { + position: absolute; + bottom: 10px; + right: 10px; + background: color-mix(in srgb, var(--secondary-bg) 75%, transparent); + border-radius: 50%; + z-index: 1; + + * { + background: var(--primary-text); + width: 16px; + height: 16px; + display: block; + margin: 5px; + } +} .addchannel { height: 10px; width: 20px; @@ -750,19 +2033,56 @@ span.instanceStatus { .addchannel:hover { background: var(--primary-text-prominent); } +.voiceuser:hover { + background: var(--channel-hover); +} .voiceuser { margin-left: 32px; - padding: 4px 0; - font-size: .9rem; + padding: 4px 4px; + border-radius: 4px; + margin-bottom: 2px; + font-size: 0.9rem; + align-items: center; + cursor: pointer; + .pfpDiv { + width: 20px; + height: 20px; + margin-right: 4px; + } +} +.unreadDateline { + color: var(--red); + hr { + background: var(--red); + } } - /* Member Info (DM/Member List) */ .liststyle { - padding: 4px 8px; + padding: 3px 8px; border-radius: 4px; align-items: center; gap: 8px; cursor: pointer; + flex-shrink: 0; + margin-bottom: 4px; + transition: background 0.2s; + &.viewChannel, + &.viewChannel:hover { + background: var(--channel-selected); + color: var(--primary-text-prominent); + font-weight: bold; + } +} +.memberListStyle { + span { + font-size: 0.9rem; + margin-left: 4px; + font-weight: 600; + color: transparent; + background-clip: text, border-box; + position: relative; + background-image: var(--userbg, linear-gradient(var(--primary-text))); + } } .liststyle .statusDiv { right: -1px; @@ -773,21 +2093,75 @@ span.instanceStatus { color: var(--primary-text-prominent); } #channels .liststyle { - margin: 0 8px; + margin-left: 8px; + margin-right: 8px; } /* Dock */ #VoiceBox { margin: 2px 0; background: var(--dock-bg); + div { + width: 100%; + } } #VoiceStatus { padding: 8px; font-weight: bold; + padding-bottom: 2px; } #VoiceStatus:empty { padding: 0; } +#VoiceButtons:empty { + padding: 0; + height: 0; +} +#VoiceButtons { + height: 46px; + display: flex; + align-items: stretch; + width: 100%; + + div { + flex-grow: 1; + display: flex; + align-items: center; + justify-content: center; + background: var(--primary-bg); + margin: 6px; + border-radius: 6px; + cursor: pointer; + span { + height: 24px; + width: 24px; + display: block; + } + transition: background 0.1s; + &:hover { + background: color-mix(in srgb, var(--primary-bg), 20% var(--primary-text)); + } + } +} + +#VoiceGuild { + padding-left: 8px; + font-weight: bold; + padding-bottom: 4px; + font-size: 14px; + cursor: pointer; + width: fit-content; + span { + white-space: nowrap; + } +} +#VoiceGuild:hover { + text-decoration: underline; +} +#VoiceGuild:empty { + padding: 0; +} + #userdock { padding: 4px 6px; background: var(--dock-bg); @@ -797,10 +2171,6 @@ span.instanceStatus { #userinfo { min-width: 50%; padding: 0 6px; - background: var(--user-info-bg); - color: var(--user-info-text); - border-radius: 8px; - box-sizing: border-box; gap: 6px; cursor: pointer; } @@ -809,18 +2179,23 @@ span.instanceStatus { } #userpfp { margin: 6px 0; + width: 32px; + height: 32px; } .userflex { justify-content: center; } #status { - font-size: .8em; + font-size: 0.8em; } -#user-actions { +#user-actions > * { border-radius: 50%; cursor: pointer; + background: var(--user-info-bg); + color: var(--user-info-text); + box-sizing: border-box; } -#user-actions:hover { +#user-actions > :hover { background: var(--dock-hover); } #settings { @@ -840,22 +2215,42 @@ span.instanceStatus { } #channelTopic { margin: auto 0 0 8px; - font-size: .9em; + font-size: 0.9em; color: var(--primary-text-soft); - button{ - margin-right:.05in; + cursor: pointer; + + button { + margin-right: 0.05in; } } #channelTopic[hidden] { display: none; } +#scrollWrap { + display: flex; + flex-direction: column; + justify-content: flex-end; + width: 100%; +} #channelw { flex: 1; + /* max-height: fit-content; */ + flex-basis: auto; + flex-grow: 1; + position: relative; } .scroller { flex: 1; padding-bottom: 22px; overflow-y: auto; + flex-grow: 1; + display: flex; + flex-direction: column; + flex-shrink: 1; + max-height: fit-content; + > * { + flex: 0; + } } #pasteimage { height: 30%; @@ -867,7 +2262,10 @@ span.instanceStatus { gap: 12px; overflow-y: auto; flex-wrap: wrap; + flex-shrink: 0; + margin-top: 8px; } + #pasteimage:empty { height: 0; padding: 0; @@ -879,9 +2277,13 @@ span.instanceStatus { background: var(--blank-bg); border-radius: 8px; overflow: hidden; + height: 192px; } .messageimgdiv { - height: 100%; + position: relative; + overflow: clip; + width: fit-content; + height: fit-content; } .messageimg { height: 100%; @@ -898,13 +2300,14 @@ span.instanceStatus { padding: 0 10px 0 16px; margin: 0 16px; background: var(--secondary-bg); - font-size: .9em; + font-size: 0.9em; color: var(--secondary-text); border-radius: 8px 8px 0 0; display: flex; align-items: center; justify-content: space-between; - transition: height .2s; + transition: height 0.2s; + flex-shrink: 0; } .cancelReply { height: 16px; @@ -917,32 +2320,87 @@ span.instanceStatus { height: 0; overflow: hidden; } +#replybox:not(:empty) ~ #typediv .outerTypeBox { + border-top-right-radius: 0; + border-top-left-radius: 0; +} #realbox { padding: 0 16px 28px 16px; + position: relative; } -#typebox{ - flex-grow:1; - width:100%; - margin-left: .06in; +.commandFront { + user-select: none !important; + padding: 4px; + background: var(--secondary-bg); + border-radius: 4px; +} +#typebox { + position: relative; + margin: -10px 0px; + flex-grow: 1; + width: 1px; + margin-left: 0.06in; + white-space: preserve-breaks; + flex-shrink: 1; + text-wrap: auto; + overflow-y: auto; + margin-right: 0.03in; + padding: 10px 0; +} +#typebox:not([contenteditable="false"]):empty:before, +#typebox:not([contenteditable="false"]):has(span:only-child:empty):before { + content: var(--channel-text); + opacity: 0.5; + position: absolute; + cursor: text; } .outerTypeBox { max-height: 50svh; padding: 10px 10px; background: var(--typebox-bg); border-radius: 4px; - overflow-y: auto; - display:flex; + display: flex; + flex-direction: row; + position: relative; +} + +.searchBox:empty { + width: 2in; +} +.searchBox:empty::after { + content: var(--hint-text); + color: var(--primary-text-soft); + position: absolute; + top: 4px; + cursor: text; +} +.searchBox { + white-space: nowrap; + height: 0.075in; + padding: 4px 10px 16px 10px; + background: var(--dock-bg); + border-radius: 4px; + /* overflow-y: auto; */ + display: flex; flex-direction: row; + width: 3in; + margin: 0 0.1in; + overflow: hidden; + flex-shrink: 0; + transition: width 0.2s; +} +.spaceElm { + margin: auto; } .outerTypeBox > span::before { content: "\feff"; } -#typebox[contenteditable=false] { - cursor:not-allowed; +#typebox[contenteditable="false"] { + cursor: not-allowed; } -#typebox[contenteditable=false]:before { +#typebox[contenteditable="false"]:before { content: "You can't send messages here"; - opacity: .5; + opacity: 0.5; } #typebox.typeboxreplying { border-radius: 0 0 4px 4px; @@ -954,7 +2412,7 @@ span.instanceStatus { position: absolute; bottom: 7px; margin-left: 24px; - font-size: .9em; + font-size: 0.9em; gap: 4px; } #typing.hidden { @@ -973,59 +2431,118 @@ span.instanceStatus { margin-right: 3px; } .dot:nth-child(2) { - animation-delay: .33s; + animation-delay: 0.33s; } .dot:nth-child(3) { - animation-delay: .66s; + animation-delay: 0.66s; +} +.loadingMessage { + span { + opacity: 0.5; + } +} +.erroredMessage { + span { + opacity: 1; + color: var(--red); + } } - /* Message */ -.messagediv, .titlespace { +.messagediv, +.titlespace { padding: 3px 36px 3px 16px; border-left: 2px solid transparent; } +.titlespace { + margin-bottom: 20px; +} .messagediv { position: relative; } +.messagediv:not(.topMessage):hover::after { + content: var(--time-text); + position: absolute; + color: #ffffffb8; + left: 2px; + top: 5px; + font-size: 12px; +} .messagediv:hover { background: var(--primary-hover); } -.messageButtons, .controls { +.fSpoil { + position: absolute; + top: 0; + width: 100%; + height: 100%; + backdrop-filter: blur(20px); + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + transition: backdrop-filter 0.4s; + span { + background: var(--spoiler-bg); + padding: 3px 6px; + border-radius: 6px; + } +} +.fSpoil:hover { + backdrop-filter: blur(15px); + span { + background: var(--spoiler-hover); + } +} +.messageButtons, +.controls { position: absolute; top: -16px; right: 16px; background: var(--secondary-bg); - box-shadow: 0 0 4px var(--shadow), 0 0 2px var(--shadow); + box-shadow: + 0 0 4px var(--shadow), + 0 0 2px var(--shadow); border-radius: 4px; overflow: hidden; } -.messageButtons button, .controls button { +.messageButtons button, +.controls button { padding: 8px; background: transparent; border-radius: 0; cursor: pointer; transition: none; } -.messageButtons button span, .controls button span { +.messageButtons button span, +.controls button span { display: block; height: 16px; width: 16px; background: var(--secondary-text-soft); } -.messageButtons button:hover, .controls button:hover { +.messageButtons button:hover, +.controls button:hover { background: var(--secondary-hover); } -.messageButtons button:hover span, .controls button:hover span { +.messageButtons button:hover span, +.controls button:hover span { background: var(--secondary-text); } .controls { top: 6px; right: 6px; box-shadow: 0 0 1.5px var(--primary-text-soft); + z-index: 1; } .message { padding-right: 28px; } +.displayComp { + padding: 12px; + background: var(--embed-bg); + border-radius: 4px; + border-left: solid var(--accent-color, black); +} .topMessage { margin-top: 16px; } @@ -1049,7 +2566,7 @@ span.instanceStatus { width: 40px; margin: 2px 12px 0 0; } -.pfprow .pfp { +.pfprow .pfpDiv { height: 40px; width: 40px; } @@ -1057,25 +2574,90 @@ span.instanceStatus { word-break: break-word; gap: 4px; width: 100%; + overflow: clip; + position: relative; +} + +.commentrow:hover::after { + background: var(--secondary-hover); +} +.fileinputdiv > div { + width: 64px; + height: 64px; + object-fit: contain; + background: var(--secondary-bg); + border-radius: 16px; + cursor: pointer; + img { + width: 64px; + height: 64px; + object-fit: contain; + border-radius: 16px; + } + &.pfp { + border-radius: 50%; + overflow: clip; + } + + display: flex; + justify-content: center; + align-items: center; + + span { + text-align: center; + } } .username { - margin-top: auto; font-weight: bold; word-break: break-all; cursor: pointer; + color: transparent; + background-clip: text, border-box; + position: relative; + background-image: var(--userbg, linear-gradient(var(--primary-text), var(--primary-text))); +} +.roleIcon { + display: inline-block; + padding: 1px; + margin-left: 3px; + cursor: default; + width: 20px; + height: 20px; + object-fit: contain; + align-self: center; +} +.userwrap { + display: flex; + align-items: baseline; +} +.pinText { + cursor: pointer; +} +.pinText:hover { + text-decoration: underline; } -.username:hover { +.username:hover:after { text-decoration: underline; + position: absolute; + content: ""; + width: 100%; + height: 1px; + background-image: inherit; + left: 0; + bottom: 1px; } .bot { padding: 2px 5px; margin-left: 4px; - vertical-align: 1px; - background: color-mix(in srgb, var(--accent-color) 75%, transparent); - font-size: .75em; + background: color-mix(in srgb, var(--accent-color) 75%, transparent) !important; + font-size: 0.75em !important; font-weight: bold; - color: var(--primary-text-prominent); + color: var(--primary-text-prominent) !important; border-radius: 4px; + flex: none; + width: fit-content; + background-clip: border-box !important; + align-self: center; } .membererror { display: inline-block; @@ -1097,18 +2679,46 @@ span.instanceStatus { margin-top: 4px; border-radius: 4px; } +.media-medium { + max-width: 1.2in !important; + max-height: 1in; + object-fit: contain; +} +.media-small { + max-width: 0.6in !important; + max-height: 0.4in; + object-fit: contain; +} .message img { max-width: 100%; } + +input[type="search"]:read-only { + cursor: not-allowed; + color: var(--secondary-text-soft); +} +.avDec { + position: absolute; + cursor: pointer; + width: 122%; + aspect-ratio: 1; + top: -11%; + left: -11%; + max-width: 150% !important; +} .timestamp { + flex: none; margin-left: 6px; - font-size: .75em; + font-size: 0.75em; color: var(--primary-text-soft); } .spoiler { background: var(--spoiler-bg); color: var(--spoiler-bg); cursor: pointer; + a { + color: var(--spoiler-bg); + } } .spoiler:hover { background: var(--spoiler-hover); @@ -1117,6 +2727,9 @@ span.instanceStatus { .unspoiled { color: var(--spoiler-text); } +a.unspoiled { + color: var(--link) !important; +} .quote { display: inline-block; position: relative; @@ -1135,7 +2748,7 @@ span .quote:last-of-type .quoteline { border-bottom-left-radius: 2px; border-bottom-right-radius: 2px; } -.quote span { +.quote > span { display: inline-block; margin-left: 8px; } @@ -1143,13 +2756,13 @@ span .quote:last-of-type .quoteline { position: relative; padding-left: 52px; margin-bottom: 4px; - font-size: .9em; + font-size: 0.9em; color: var(--reply-text); - align-items: center; + align-items: baseline; gap: 4px; } .replyflex::before { - content: ''; + content: ""; position: absolute; top: calc(50% - 1px); left: 19px; @@ -1168,13 +2781,13 @@ span .quote:last-of-type .quoteline { .replypfp { height: 20px; width: 20px; + align-self: center; } .replyflex .username { flex: none; max-width: calc(100% - 75px); } .replytext { - margin-top: auto; cursor: pointer; } .replytext a { @@ -1194,23 +2807,80 @@ span .quote:last-of-type .quoteline { .replying:hover { background: color-mix(in srgb, var(--reply-highlight) 10%, var(--primary-hover)); } -.mentioned { +.mentioned > button, +.mentioned.messagediv { background: color-mix(in srgb, var(--mention-highlight) 10%, transparent); border-color: var(--mention-highlight); } -.mentioned:hover { +.mentioned:hover > button { background: color-mix(in srgb, var(--mention-highlight) 10%, var(--primary-hover)); } +#slideCount { + position: absolute; + bottom: 0px; + display: flex; + flex-direction: row; + padding: 10px; + width: calc(100% - 30px); + justify-content: center; + box-sizing: border-box; + div { + padding: 3px; + margin: 2px; + cursor: pointer; + &.selected { + span { + opacity: 1; + } + } + } + span { + width: 10px; + height: 10px; + background: white; + display: block; + opacity: 0.5; + border-radius: 100%; + } +} +.screenshots { + width: calc(100% - 10px); + margin: 5px; + box-sizing: border-box; + border-radius: 8px; + position: absolute; + top: 0px; + left: 0px; + transition: opacity 0.4s; + &.hidden { + opacity: 0; + } +} +#ScreenshotSlides { + position: relative; + aspect-ratio: 1.9; +} .mentionMD { - background: color-mix(in srgb, var(--mention), transparent); + color: transparent; + background-clip: text; padding: 1px 2px; border-radius: 4px; cursor: pointer; + background-image: var(--userbg, linear-gradient(var(--primary-text), var(--primary-text))); + position: relative; + z-index: 1; } -.mentionMD::after{ - content:"​"; +.mentionMD::before { + background: color-mix(in srgb, var(--mention), transparent); + content: ""; + position: absolute; + width: 100%; + height: 100%; + left: 0px; + z-index: -1; + border-radius: 4px; } -.mentionMD:hover { +.mentionMD:hover::before { background: var(--mention); } .reactiondiv { @@ -1262,23 +2932,29 @@ span .quote:last-of-type .quoteline { max-width: 400px; padding: 12px; background: var(--embed-bg); - font-size: .88em; + font-size: 0.88em; color: var(--secondary-text); border-radius: 4px; + + > p { + line-height: 20px; + } } .linkembed { margin-top: 4px; } + .embed-color { - max-width: 400px; + max-width: fit-content; margin-top: 4px; border-radius: 4px; + width: 400px; } .embed-color .embed { margin-left: 4px; border-radius: 0 4px 4px 0; } -.embed a { +.embed .embedtitle { display: inline-block; padding-bottom: 4px; font-size: 1rem; @@ -1288,6 +2964,17 @@ span .quote:last-of-type .quoteline { padding-bottom: 4px; color: var(--secondary-text-soft); } +.rich-embed { + width: unset; + .embed { + max-width: unset; + } +} +.slowmodeTimer { + position: absolute; + right: 16px; + color: var(--primary-text-soft); +} img.embedicon { height: 16px; width: 16px; @@ -1298,8 +2985,12 @@ img.embedicon { img.embedimg { max-width: 96px; } +img.authorEmbedImg { + max-width: 20px; + margin-right: 4px; + border-radius: 1in; +} img.bigembedimg { - height: auto !important; margin-top: 8px; border-radius: 4px; cursor: pointer; @@ -1314,10 +3005,13 @@ img.bigembedimg { } .inviteEmbed .banner { height: 48px; + width: 400px; } .inviteEmbed .flexltr { padding: 12px; gap: 8px; + z-index: 2; + padding-top: 24px; } .inviteEmbed .unread { display: none; @@ -1349,6 +3043,7 @@ img.bigembedimg { padding: 4px; background: var(--secondary-bg); border-radius: 4px; + position: relative; } .acceptinvbutton { width: calc(100% - 24px); @@ -1359,33 +3054,67 @@ img.bigembedimg { font-weight: bold; text-align: center; } -.acceptinvbutton:hover, .acceptinvbutton:disabled { +.acceptinvbutton:hover, +.acceptinvbutton:disabled { background: color-mix(in hsl, var(--green) 80%, var(--black)); } /* Sidebar */ -#sideDiv { +#sideContainDiv { + padding: 16px 8px; display: none; flex: none; width: 240px; - padding: 16px 8px; background: var(--sidebar-bg); + flex-direction: column; + justify-content: space-between; + z-index: 4; +} +#sideDiv { overflow-y: auto; box-sizing: border-box; + position: relative; + overflow: auto; + flex-grow: 1; +} +#page:has(#memberlisttoggle:checked) #sideContainDiv { + display: flex; +} +#sideContainDiv.searchDiv { + padding: 0px; + display: flex; + width: max(30vw, 400px); + + .topMessage { + margin: 0px 10px; + margin-top: 2px; + margin-bottom: 10px; + cursor: pointer; + padding: 0.05in; + border-radius: 0.075in; + background: #00000020; + } + .topMessage:hover { + background: #00000050; + } } + .memberList { padding-bottom: 16px; color: var(--primary-text-soft); + .flexttb { + padding-left: 8px; + } } .memberList h3 { margin: 0 8px 4px 8px; - font-size: 1rem; + font-size: 0.9rem; } .memberList .liststyle:hover { background: var(--sidebar-hover); } .memberList.offline .liststyle { - opacity: .5; + opacity: 0.5; } #memberlisttoggle { display: none; @@ -1393,16 +3122,14 @@ img.bigembedimg { #memberlisttoggleicon { display: block; padding: 12px 0; - margin-left: auto; + margin-left: 0; cursor: pointer; + flex-grow: 0; } #memberlisttoggleicon span { height: 16px; width: 16px; } -#page:has(#memberlisttoggle:checked) #sideDiv { - display: block; -} #page:has(#memberlisttoggle:checked) #memberlisttoggleicon span { background: var(--primary-text-prominent); } @@ -1414,16 +3141,57 @@ img.bigembedimg { background: var(--secondary-bg); border-radius: 4px; box-shadow: 0 0 8px var(--shadow); + z-index: 300; + animation-duration: 0.2s; + animation-name: context-fade-in; + animation-timing-function: ease-out; + + hr { + width: 90%; + height: 1px; + } +} +.removeElm { + pointer-events: none; +} +.contextmenu.removeElm { + animation-name: context-fade-out; +} +@keyframes context-fade-out { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} +@keyframes context-fade-in { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } } .contextbutton { + position: relative; width: 180px; padding: 8px; background: transparent; - font-size: .9rem; + font-size: 0.9rem; line-height: 1em; color: var(--secondary-text); border: none; transition: none; + display: flex; + flex-direction: row; + .svgicon { + height: 0.2in; + width: 0.2in; + position: absolute; + right: 6px; + top: 6px; + } } .contextbutton:enabled:hover { background: var(--secondary-hover); @@ -1439,10 +3207,42 @@ img.bigembedimg { width: 300px; max-height: 100svh; background: var(--card-bg); - box-shadow: 0 0 8px var(--shadow), inset 0 132px 64px var(--accent_color); + box-shadow: + 0 0 8px var(--shadow), + inset 0 132px 64px var(--accent_color); border-radius: 8px; overflow: hidden; align-items: flex-start; + z-index: 3; + display: flex; + flex-direction: column; + &:not(.hypoprofile) { + animation-duration: 0.2s; + animation-name: fade-in; + animation-fill-mode: forwards; + animation-timing-function: cubic-bezier(0.28, 0.48, 0.24, 1.27); + } + input { + width: calc(100% - 20px); + margin: 0px 10px; + margin-bottom: 10px; + box-sizing: border-box; + padding: 10px; + background: var(--secondary-bg); + border: none; + border-radius: 5px; + font-size: 14px; + } +} +@keyframes fade-in { + 0% { + transform: translateY(30px); + opacity: 0; + } + 100% { + transform: translateY(0); + opacity: 1; + } } .hypoprofile { position: relative; @@ -1451,33 +3251,93 @@ img.bigembedimg { } .profile .pfpDiv { position: absolute; + z-index: 2; + top: 20px; } -.profile .pfp { - height: 48px; - width: 48px; +.profile .pfpDiv { + height: 64px; + width: 64px; margin: 8px 0 0 8px; - cursor: auto; + cursor: pointer; } .statusDiv { position: absolute; right: 2px; bottom: 2px; - height: 7px; - width: 7px; + height: 8px; + width: 8px; background: var(--black); border: 2.5px solid var(--black); - border-radius: 50%; + border-radius: 8px; + transition: width 0.5s; +} +.profile .statusDiv { + width: 14px; + height: 14px; } .onlinestatus { background: var(--green); } +.typingstatus { + width: 16px; + &::before { + content: " "; + background: white; + height: 4px; + width: 4px; + position: absolute; + top: 2px; + left: 1px; + border-radius: 100%; + animation: fade 1s infinite; + } + &::after { + content: " "; + background: white; + height: 4px; + width: 4px; + position: absolute; + top: 2px; + right: 1px; + border-radius: 100%; + animation: fade 1s infinite; + animation-delay: 0.33s; + } + div { + background: white; + height: 4px; + width: 4px; + position: absolute; + top: 2px; + right: 6px; + border-radius: 100%; + animation: fade 1s infinite; + animation-delay: 0.66s; + } +} +.dndstatus { + background: var(--red); +} .offlinestatus { - background:var(--primary-bg); + background: var(--primary-bg); } .banner { height: 100px; width: 100%; object-fit: cover; + position: absolute !important; + /* z-index: 2; */ +} +.badge { + display: inline-flex; + padding: 2px; + border: solid 2px var(--black); + border-radius: 8px; + img { + object-fit: contain; + width: 18px; + height: 18px; + } } .infosection { flex: 1; @@ -1490,23 +3350,24 @@ img.bigembedimg { border-radius: 8px; box-sizing: border-box; overflow-y: auto; + padding-top: 30px; + z-index: 1; } -.profile:has(.banner) .infosection { - margin-top: 8px; -} -.infosection h2, .infosection h3 { +.infosection h2, +.infosection h3 { word-break: break-word; + font-size: 20px; } .infosection hr { width: 100%; } .tag { margin: 4px 0; - font-size: .9em; + font-size: 0.9em !important; color: var(--secondary-text); } .pronouns { - font-size: .9em; + font-size: 0.9em; color: var(--secondary-text-soft); } .rolesbox { @@ -1520,13 +3381,17 @@ img.bigembedimg { padding: 4px 6px; background: var(--role-bg); color: var(--role-text); - font-size: .9em; + font-size: 0.9em; border: 1px solid var(--black); border-radius: 4px; display: flex; align-items: center; gap: 4px; } +ul { + margin-top: 2px; + margin-bottom: 2px; +} .colorrolediv { height: 12px; width: 12px; @@ -1541,12 +3406,75 @@ img.bigembedimg { width: 360px; max-height: 100svh; padding: 12px; - background: var(--card-bg); border-radius: 8px; box-shadow: 0 0 8px var(--shadow); gap: 8px; box-sizing: border-box; user-select: none; + background: var(--secondary-bg); + z-index: 104; + + input { + width: 1in; + position: absolute; + right: 8px; + top: 2px; + transition: width 0.2s; + background: var(--card-bg); + } +} +.emojiPicker, +.gifmenu, +.stickerPicker { + animation-duration: 0.2s; + animation-name: fade-in-back; + animation-fill-mode: forwards; + animation-timing-function: ease-out; + &.removeElm { + animation-name: fade-in-back-out; + } +} +.stickerSelect { + width: 1in; + height: 1in; + margin-right: 6px; + padding: 5px; + background: var(--secondary-bg); + border-radius: 6px; + cursor: pointer; + display: flex; + margin-bottom: 8px; +} +.stickerPicker { + position: absolute; + height: 440px; + width: 390px; + max-height: 100svh; + padding: 12px; + border-radius: 8px; + box-shadow: 0 0 8px var(--shadow); + gap: 8px; + box-sizing: border-box; + user-select: none; + background: var(--secondary-bg); + z-index: 4; + + input { + width: 1in; + position: absolute; + right: 8px; + top: 2px; + transition: width 0.2s; + background: var(--card-bg); + } + .sticker { + max-width: 1in; + max-height: 1in; + flex-grow: 1; + } +} +.emojiHeading { + height: 0.25in; } .emojiTitle { font-size: 1.2rem; @@ -1555,6 +3483,7 @@ img.bigembedimg { flex: none; align-items: center; overflow-x: auto; + flex-grow: 0; } .emojiSelect { flex: none; @@ -1571,7 +3500,8 @@ img.bigembedimg { .emojiSelect:hover { background: var(--secondary-hover); } -.emoji-server, .emojiBody .smallemoji { +.emoji-server, +.emojiBody .smallemoji { height: auto; width: auto; max-height: 28px; @@ -1581,50 +3511,172 @@ img.bigembedimg { .emojiBody { flex: 1; padding: 6px; - background: var(--secondary-bg); + background: var(--card-bg); border-radius: 8px; display: grid; grid-template-columns: repeat(auto-fill, minmax(40px, 1fr)); grid-auto-rows: min-content; overflow-y: auto; } - +.stickerBody { + flex: 1; + padding: 8px; + background: var(--card-bg); + border-radius: 8px; + display: flex; + overflow-y: auto; + flex-direction: row; + flex-wrap: wrap; +} /* Fullscreen and Modal (TEMP) */ .background { + contain-intrinsic-width: AUTO 100px; position: fixed; top: 0; height: 100%; width: 100%; background: color-mix(in srgb, var(--black) 75%, transparent); + backdrop-filter: blur(1px); + z-index: 102; + display: flex; + align-items: center; + justify-content: center; + + &:not(.menu) { + animation-name: bg-fade; + } + animation-duration: 0.2s; + &.removeElm { + animation-name: bg-fade-out; + > * { + &.centeritem { + animation-name: fade-in-back-out; + } + } + } + + > * { + animation-duration: 0.2s; + animation-name: fade-in-back-settings; + &.centeritem { + animation-name: fade-in-back; + } + animation-fill-mode: forwards; + animation-timing-function: ease-out; + } +} +@keyframes bg-fade-out { + 0% { + background: color-mix(in srgb, var(--black) 75%, transparent); + backdrop-filter: blur(1px); + } + 100% { + background: transparent; + backdrop-filter: blur(0px); + } +} +@keyframes bg-fade { + 0% { + background: transparent; + backdrop-filter: blur(0px); + } + 100% { + background: color-mix(in srgb, var(--black) 75%, transparent); + backdrop-filter: blur(1px); + } +} +@keyframes fade-in-back-settings { + 0% { + opacity: 0; + transform: scale(1.1); + } + 100% { + opacity: 1; + } +} +.bgRemove { + animation-duration: 0.2s; + animation-name: bg-out; + animation-fill-mode: forwards; + animation-timing-function: ease-out; +} +@keyframes bg-out { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + transform: scale(1.1); + } +} +@keyframes fade-in-back-out { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + backdrop-filter: blur(0px); + transform: scale(0.9); + } +} +@keyframes fade-in-back { + 0% { + transform: scale(1); + opacity: 0; + } + 1% { + opacity: 0; + transform: scale(0.9); + } + 100% { + opacity: 1; + } } .imgfit { max-height: 85svh; max-width: 85svw; + display: flex; + + img { + flex-shrink: 1; + height: auto; + max-width: inherit; + width: auto; + max-height: inherit; + } +} +.centeritem.profile { + width: 80%; + height: 80%; } -.centeritem, .accountSwitcher { +.accountSwitcher { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } -.nonimagecenter, .accountSwitcher { - max-height: 80svh; +.nonimagecenter, +.accountSwitcher { + max-height: 95svh; width: 500px; - padding: 12px; + padding: 6px; margin: 0; background: var(--secondary-bg); border: none; border-radius: 8px; - box-shadow: 0 0 24px var(--shadow), 0 0 1.5px var(--primary-text); + box-shadow: + 0 0 24px var(--shadow), + 0 0 1.5px var(--primary-text); box-sizing: border-box; - gap: 8px; overflow-y: auto; } -.nonimagecenter & .flexttb, .nonimagecenter & .flexltr { +.nonimagecenter & .flexttb, +.nonimagecenter & .flexltr { flex: 1; gap: 8px; } -.nonimagecenter > .flexttb, .nonimagecenter > .flexltr { +.nonimagecenter > .flexttb, +.nonimagecenter > .flexltr { padding: 0 0 16px 0 !important; background: var(--primary-bg); border-radius: 8px; @@ -1633,7 +3685,8 @@ img.bigembedimg { padding-bottom: 0; border-bottom: none; } -.nonimagecenter .flexspace, .nonimagecenter .FormSettings { +.nonimagecenter .flexspace, +.nonimagecenter .FormSettings { padding: 0; } .nonimagecenter button { @@ -1652,6 +3705,8 @@ fieldset input[type="radio"] { border-radius: 8px; cursor: pointer; align-items: center; + flex-shrink: 0; + margin-bottom: 10px; } .switchtable:hover { background: var(--primary-hover); @@ -1662,11 +3717,11 @@ fieldset input[type="radio"] { margin-right: 8px; } .serverURL { - font-size: .9em; + font-size: 0.9em; color: var(--primary-text-soft); } .accountSwitcher { - z-index: 102; + z-index: 111; } .accountSwitcher h1 { margin-bottom: 4px; @@ -1710,10 +3765,14 @@ fieldset input[type="radio"] { max-height: unset; width: auto; } +#typediv[style*="hidden"] { + height: 0px; +} .guildy { - height: 80svh; - width: 80svw; gap: 6px; + height: 100%; + padding: 10px; + box-sizing: border-box; } #discovery-guild-content { flex: 1; @@ -1728,6 +3787,10 @@ fieldset input[type="radio"] { background: var(--secondary-bg); overflow: hidden; cursor: pointer; + position: relative; + .banner { + position: relative !important; + } } .discovery-guild:hover { background: var(--secondary-hover); @@ -1748,7 +3811,7 @@ fieldset input[type="radio"] { } .discovery-guild p { margin: 0 12px; - font-size: .9em; + font-size: 0.9em; color: var(--secondary-text-soft); word-break: break-word; } @@ -1769,16 +3832,66 @@ fieldset input[type="radio"] { gap: 0 6px; align-items: center; user-select: none; + /* pointer-events: none; */ + width: 100%; + box-sizing: border-box; } .Buttons { flex: 1; + width: inherit; } -.settingbuttons.flexltr{ +.settingbuttons.flexltr { width: 100%; border-right: 0; - border-radius: .04in; - .SettingsButton{ - width:auto; + border-radius: 0.04in; + .SettingsButton { + width: auto; + border-radius: 3px 3px 1px 1px; + } +} +.reportMenu { + background: var(--primary-bg); + padding: 16px; + border-radius: 4px; + box-shadow: 1px 2px 5px 2px black; + width: 70%; + height: 70%; + overflow-y: auto; + > div { + > * { + margin-bottom: 4px; + } + min-height: 100%; + } +} +.removeElm { + > .reportMenu { + animation-duration: 0.2s; + animation-name: fade-in-back; + animation-fill-mode: forwards; + animation-timing-function: ease-out; + animation-name: fade-in-back-out; + } +} +.checkCard { + padding: 2px; + background: #00000036; + margin: 2px; + border-radius: 4px; + cursor: pointer; +} +.reportButtonDiv { + flex-shrink: 0; + button { + margin-right: 6px; + } +} +.reportChildren { + margin-top: auto; + height: fit-content; + + button { + margin-bottom: 6px; } } .settingbuttons { @@ -1791,18 +3904,38 @@ fieldset input[type="radio"] { box-sizing: border-box; overflow-y: auto; } +.roleButtonStyle { + color: transparent; + background-image: var( + --user-bg, + linear-gradient(var(--primary-text-soft), var(--primary-text-soft)) + ); + background-clip: text; +} .SettingsButton { width: 100%; padding: 8px 12px; background: transparent; color: var(--primary-text-soft); border: none; - transition: none; + transition: background 0.1s; + position: relative; +} +.infosection { + .activeSetting { + border-bottom: white solid 1px; + } +} +.activeSetting { + background: var(--secondary-hover); } .SettingsButton:hover { background: var(--settings-panel-hover); color: var(--primary-text-prominent); } +.activeSetting:hover { + background: color-mix(in srgb, var(--secondary-bg) 40%, transparent); +} .addrole { height: 10px; width: 10px; @@ -1810,30 +3943,152 @@ fieldset input[type="radio"] { align-self: center; cursor: pointer; } + +@keyframes fade-shrink { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + transform: scale(0.9); + } +} +@keyframes fade-grow-in { + 0% { + opacity: 0; + } + 1% { + opacity: 0; + transform: scale(0.9); + } + 100% { + opacity: 1; + } +} +.EmojiGuildMenu.removeElm { + animation-duration: 0.2s; + animation-name: fade-shrink; + animation-timing-function: ease-in; +} .titlediv { - height: 100%; + display: flex; + flex: 1; + + > flexspace { + flex: 1; + } } .flexspace { padding-bottom: 32px; overflow-y: auto; + position: relative; } .flexspace:has(.flexspace) { height: 100%; padding-bottom: 0; } -.FormSettings .flexspace { - padding-bottom: 0; +.FormSettings .flexspace { + padding-bottom: 0; +} +.FormSettings { + padding-bottom: 32px; +} +.webhookArea { + background: var(--secondary-bg); + padding: 0.2in; + display: flex; + align-items: center; + border-radius: 0.1in; + user-select: none; + cursor: pointer; + span { + color: var(--secondary-text-soft); + } + .svgicon { + width: 0.4in; + height: 0.4in; + margin-left: auto; + } +} +.stickersDiv { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} +.webhookpfppreview { + width: 0.8in; + height: 0.8in; + border-radius: 1in; + margin-right: 0.2in; +} +.stickerView { + max-width: 2.5in; + max-height: 2.5in; +} +.sticker { + max-width: 4in; + height: 2in; +} +.vsmaller { + .optionElement { + margin-top: 8px; + } +} +.traceHead { + cursor: pointer; + border-bottom: black solid 4px; +} +.traceDiv { + padding: 5px 10px; } -.FormSettings { - padding-bottom: 32px; +.traceBars { + padding: 10px 0px; +} +.mutGuildBox { + background: var(--primary-bg); + padding: 6px; + cursor: pointer; + margin-top: 6px; + + img { + width: 48px !important; + height: 48px !important; + padding: 4px !important; + margin: 0px !important; + } + .flexttb { + display: flex; + justify-content: space-evenly; + padding-left: 8px; + } + border-radius: 6px; } -.optionElement, .FormSettings > button { +.dropDownTrace { + background: var(--primary-hover); + padding: 20px 10px; + border-radius: 10px; + border: black solid 4px; +} +.optionElement, +.FormSettings > button { margin: 16px 16px 0 16px; word-break: break-word; overflow: hidden; flex-shrink: 0; } -.flexltr:has(>.optionElement) { +.permbox { + display: flex; + flex-wrap: wrap; +} +.permbox > span { + padding: 6px; + margin: 4px; + background: var(--role-bg); + white-space: nowrap; + word-break: keep-all; + border-radius: 3px; +} +.flexltr:has(> .optionElement) { margin: 16px 16px 0 16px; flex-wrap: wrap; gap: 6px; @@ -1843,6 +4098,7 @@ fieldset input[type="radio"] { } .optionElement:has(.optionElement) { margin: 0; + position: relative; } .optionElement:has(.Buttons) { height: 100%; @@ -1852,9 +4108,10 @@ fieldset input[type="radio"] { .optionElement input[type="text"], .optionElement textarea, .optionElement .fileinputdiv { - width: 500px; + max-width: 500px; } -#app-list-container div, #connection-container div { +#app-list-container div, +#connection-container div { max-width: 500px; padding: 8px; margin-top: 12px; @@ -1895,8 +4152,8 @@ fieldset input[type="radio"] { font-size: 1.15rem; } .setting p { - font-size: .9em; - color: var(--secondary-text-soft) + font-size: 0.9em; + color: var(--secondary-text-soft); } .tritoggle { display: inline; @@ -1904,27 +4161,34 @@ fieldset input[type="radio"] { } .tritoggle input { margin: 0 2px; + accent-color: var(--grey); } .tritoggle input:first-child { accent-color: var(--green); } -.tritoggle input:last-child { +.tritoggle input:nth-child(3) { accent-color: var(--red); } +.nonimagecenter > .flexltr.savediv { + padding: 16px !important; +} .savediv { position: fixed; bottom: 24px; right: 50%; transform: translateX(50%); - padding: 16px; - background: var(--secondary-bg); + padding: 16px !important; + background: var(--secondary-bg) !important; font-size: 1.2em; font-weight: bold; color: var(--secondary-text); - border-radius: 8px; + border-radius: 8px !important; align-items: center; - box-shadow: 0 0 24px var(--shadow), 0 0 1px var(--primary-text); + box-shadow: + 0 0 24px var(--shadow), + 0 0 1px var(--primary-text); box-sizing: border-box; + z-index: 2; } .savediv button { margin-left: 12px; @@ -1943,15 +4207,15 @@ fieldset input[type="radio"] { } /* Jank Mobile */ -#maintoggle, #maintoggleicon { +#maintoggle, +#maintoggleicon { display: none; } @media screen and (max-width: 1000px) { - #sideDiv { - position: absolute; - right: 0; - height: calc(100svh - 50px); + #sideContainDiv { + position: relative; + margin-left: -256px; } .flexspace:has(.hypoprofile) { flex-direction: column; @@ -1961,9 +4225,50 @@ fieldset input[type="radio"] { .optionElement .fileinputdiv { width: 100%; } + .searching { + #searchBox { + width: 2in; + margin-right: 36px; + padding: 7px 10px 13px 10px; + flex-shrink: 0; + } + } + #searchBox { + width: 0px; + padding: 0; + } + .searchBox:empty::after { + content: ""; + } + #searchX { + position: absolute; + width: 24px; + height: 24px; + margin: 0; + cursor: pointer; + top: 5px; + right: 2px; + transition: height 0.2s; + } + .searchMeta { + transition: width 0.2s; + min-width: 32px; + height: 32px; + } } -@media screen and (max-width: 500px) { +@media screen and (max-width: 600px) { + .searching { + position: absolute; + width: 100%; + background: var(--primary-bg); + z-index: 1; + #searchBox { + width: 100%; + padding: 7px 10px 13px 10px; + flex-shrink: 1; + } + } #homePage { max-width: unset; width: 94%; @@ -1972,7 +4277,9 @@ fieldset input[type="radio"] { display: block; position: fixed; } - #servers, .channelflex, #mainarea { + #servers, + .channelflex, + #mainarea { position: absolute; height: 100svh; } @@ -1983,22 +4290,28 @@ fieldset input[type="radio"] { left: 304px; width: 100svw; background: var(--primary-bg); - transition: left .3s; + transition: left 0.3s; } - #sideDiv { + #sideContainDiv, + #sideContainDiv.searchDiv { display: block; right: -100svw; + margin-left: -100svw; width: 100svw; - transition: right .3s; + transition: right 0.3s; + box-sizing: border-box; } .channelnamediv { padding: 0; } - #maintoggleicon, #memberlisttoggleicon { + #maintoggleicon { display: block; padding: 12px; cursor: pointer; } + #memberlisttoggleicon { + padding: 6px; + } #maintoggleicon span { height: 14px; width: 14px; @@ -2013,8 +4326,10 @@ fieldset input[type="radio"] { #page:has(#maintoggle:checked) #mainarea { left: 0; } - #page:has(#memberlisttoggle:checked) #sideDiv { + #page:has(#memberlisttoggle:checked) #sideContainDiv:not(.hideSearchDiv), + #sideContainDiv.searchDiv:not(.hideSearchDiv) { right: 0; + overflow: auto; } #page:has(#maintoggle:checked) #maintoggleicon { rotate: 180deg; @@ -2031,7 +4346,7 @@ fieldset input[type="radio"] { .pfprow { width: 32px; } - .pfprow .pfp { + .pfprow .pfpDiv { height: 32px; width: 32px; } @@ -2039,10 +4354,17 @@ fieldset input[type="radio"] { left: 15px; width: 24px; } - .replyflex, .reactiondiv { + .replyflex, + .reactiondiv { padding-left: 44px; } - #realbox, #logindiv, #pasteimg { + .sticker { + height: auto; + max-width: 100%; + } + #realbox, + #logindiv, + #pasteimg { padding-left: 12px; padding-right: 12px; } @@ -2050,26 +4372,52 @@ fieldset input[type="radio"] { margin-left: 12px; margin-right: 12px; } - .contextmenu, .profile, .emojiPicker { + .pinnedMessages { + box-sizing: border-box; + left: 8px; + width: calc(100% - 16px); + } + .contextmenu, + .profile, + .emojiPicker, + .stickerPicker, + .gifmenu { top: unset !important; - bottom: 0; - width: 100%; + bottom: 0 !important; + right: unset !important; + width: 100% !important; box-sizing: border-box; border-radius: 16px 16px 0 0; - box-shadow: 0 0 14px var(--shadow), 0 0 28px var(--shadow); + box-shadow: + 0 0 14px var(--shadow), + 0 0 28px var(--shadow); + } + .contextmenu hr { + width: calc(100% - 24px); } .contextbutton { width: 100%; padding: 12px; + .svgicon { + top: 10px; + right: 12px; + } } .profile { height: 65%; - box-shadow: 0 0 14px var(--shadow), 0 0 28px var(--shadow), inset 0 132px 64px var(--accent_color); + box-shadow: + 0 0 14px var(--shadow), + 0 0 28px var(--shadow), + inset 0 132px 64px var(--accent_color); } .hypoprofile { border-radius: 8px; } - #logindiv, #invitebody, .savediv, .nonimagecenter, .accountSwitcher { + #logindiv, + #invitebody, + .savediv, + .nonimagecenter, + .accountSwitcher { width: 94%; min-width: unset; } @@ -2080,8 +4428,16 @@ fieldset input[type="radio"] { font-size: 1.2rem; } .settingbuttons { - height: 100px; + height: 100%; width: 100%; + z-index: 1; + position: absolute; + } + .settingsHTMLArea { + z-index: 2; + height: 100%; + background: var(--primary-bg); + transition: transform 0.2s; } .nonimagecenter .settingbuttons { height: auto; @@ -2095,84 +4451,509 @@ fieldset input[type="radio"] { .rolesheader { margin: 6px 12px; } + .mobileback { + width: 20px; + height: 20px; + visibility: visible; + cursor: pointer; + } + .mobileHidden { + transform: translate(100%, 0px); + } + .outerTypeBox:not(.noConent) { + #mobileSend { + width: 25px; + margin-left: 4px; + } + #buttonCol { + width: 0px; + } + } +} + +#mobileSend { + cursor: pointer; + height: 25px; + margin: -3px 0px; + width: 0px; + transition: + width 0.2s, + margin-left 0.2s; + margin-left: 0px; +} +#buttonCol { + overflow: hidden; + width: 44px; + transition: width 0.2s; +} +.settingsHTMLArea { + position: relative; + display: flex; + flex-direction: row; } -.friendcontainer{ +.friendcontainer { display: flex; width: 100%; - padding: .2in; - >div{ - background:#00000030; - margin-bottom:.1in; - padding:.06in .1in; - border-radius:.1in; + padding: 0.2in; + overflow-y: auto; + height: 100%; + align-items: stretch; + box-sizing: border-box; + + > div { + background: #00000030; + margin-bottom: 0.1in; + padding: 0.06in 0.1in; + border-radius: 0.1in; border: solid 1px var(--black); } } -.fixedsearch{ +.fixedsearch { position: absolute; background: var(--primary-bg); - min-height: .2in; - padding:.05in; - border:solid .03in var(--black); - border-radius:.05in; - span{ - margin-top:.1in; - width:100%; - padding:.03in; - border:solid .03in var(--black); - box-sizing:border-box; - border-radius:.05in; - cursor:pointer; - } - -} -.suberror{ + min-height: 0.2in; + padding: 0.05in; + border: solid 1px var(--black); + border-radius: 0.05in; + z-index: 104; + span { + background: #00000042; + width: 100%; + margin-top: 4px; + padding: 6px; + box-sizing: border-box; + border-radius: 0.05in; + cursor: pointer; + } +} +.suberror { animation: goout 6s forwards; } -.suberrora{ - background:var(--channel-hover); - border-radius:.1in; - position:absolute; - border:solid var(--primary-text) .02in; - color:color-mix(in hsl,var(--yellow),var(--red)); - font-weight:bold; - opacity:0; - cursor:default; +.suberrora { + background: var(--channel-hover); + border-radius: 0.1in; + position: absolute; + border: solid var(--primary-text) 0.02in; + color: color-mix(in hsl, var(--yellow), var(--red)); + font-weight: bold; + opacity: 0; + cursor: default; /* height: .4in; */ display: flex; flex-direction: column; align-items: flex-start; justify-content: space-evenly; - padding: .075in; + padding: 0.075in; box-sizing: border-box; pointer-events: none; + z-index: 10000000; } @keyframes goout { - 0%,100%{ - opacity:0; + 0%, + 100% { + opacity: 0; } - 5%,90%{ - opacity:1; + 5%, + 90% { + opacity: 1; } } -.friendsbutton{ - transition: background-color .2s; - background-color: #00000050; - padding: .08in; +.friendsbutton { + transition: background-color 0.2s; + padding: 0.08in; } -.bigemoji{ - width:.6in; +.bigemoji { + width: 0.6in; + object-fit: contain; + height: 0.6in; +} +.bigemojiUni { + font-size: 47px; } -.friendlyButton{ - padding: .07in; +.friendlyButton { + padding: 0.07in; background: #00000045; - transition:background .2s; + transition: background 0.2s; border-radius: 1in; border: solid 1px var(--black); width: 24px; height: 24px; - margin: 0 .05in; + margin: 0 0.05in; +} +.friendlyButton:hover { + background: black; +} +.stickerOption { + border: solid 1px var(--black); + display: flex; + align-items: center; + padding: 0.075in; + margin-bottom: 0.2in; + border-radius: 0.1in; + background: var(--primary-hover); + position: relative; + margin-right: 15px; + cursor: pointer; + img { + height: 2in; + } +} +.emojiOption { + border: solid 1px var(--black); + display: flex; + align-items: center; + padding: 0.075in; + margin-bottom: 0.2in; + border-radius: 0.1in; + background: var(--primary-hover); + position: relative; + input { + width: 2in !important; + height: 0.3in; + } + .bigemoji { + padding-right: 0.5in; + } +} +.deleteEmoji { + width: 0.3in; + height: 0.3in; + position: absolute; + right: 0.2in; + cursor: pointer; +} + +.gifmenu { + position: absolute; + width: 4.5in; + height: 5in; + background: var(--secondary-bg); + border-radius: 8px; +} +.gifPreviewBox { + position: relative; + width: 2in; + margin-bottom: 10px; + border-radius: 7px; + overflow: hidden; + cursor: pointer; + + img { + width: 2in; + height: 1in; + object-fit: cover; + } + span { + top: 0px; + left: 0px; + position: absolute; + width: 100%; + height: 100%; + display: inline-flex; + align-items: center; + justify-content: center; + background: color-mix(in srgb, var(--card-bg) 60%, transparent); + font-weight: bold; + } +} +.gifbox { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-around; + overflow-y: auto; + margin: 0.1in; + align-items: center; + position: relative; + overflow-x: clip; +} +.searchGifBar { + background: var(--card-bg); +} +.vr-message { + border-right: 1px solid var(--black) !important; +} +.gifmenu .title { + display: flex; + align-items: center; +} +.searchGifBar, +.gifmenu .title { + height: 0.3in; + margin: 0.15in 0.15in 0 0.15in; + flex-shrink: 0; + + border: none; + border-radius: 4px; + font-size: 0.2in; + padding: 0 0.1in; + span { + width: 16px; + height: 16px; + background: var(--primary-text); + cursor: pointer; + } +} +img.error { + position: relative; +} +img.error::after { + background: var(--secondary-bg) url("./icons/sad.svg"); + background-size: contain; + content: ""; + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; +} +.dragBottomView { + position: relative; +} +.dragTopView { + position: relative; +} +.dragTopView::after { + content: ""; + width: 100%; + height: 4px; + border-radius: 4px; + background: var(--primary-text-soft); + top: -1px; + left: 0px; + position: absolute; +} +.dragBottomView::after { + content: ""; + width: 100%; + height: 4px; + border-radius: 4px; + background: var(--primary-text-soft); + bottom: -3px; + left: 0px; + position: absolute; +} +.dragFolderView::before { + content: ""; + width: 24px; + height: 24px; + background: var(--green); + top: 12px; + left: -6px; + border-radius: 100%; + position: absolute; + pointer-events: none; +} +.blogDiv { + background: var(--secondary-bg); + padding: 10px; + border-radius: 3px; + cursor: pointer; + div { + width: 100%; + h2 { + width: 100%; + border-bottom: solid var(--primary-text) 1px; + margin-bottom: 15px; + padding-bottom: 8px; + } + } +} +.gifBox { + img { + max-width: 196px; + border-radius: 4px; + } + cursor: pointer; + position: absolute; + overflow: hidden; +} +.emojiForm { + display: flex; + background: var(--secondary-bg); + padding: 6px; + width: fit-content; + border-radius: 4px; + align-items: center; + cursor: pointer; + :last-child { + margin-left: 6px; + max-width: 32px !important; + max-height: 32px !important; + flex-shrink: 0; + } +} +.emptyEmoji { + background: var(--primary-bg); + width: 32px; + height: 32px; + border-radius: 2in; +} +.stickerMArea { + padding-left: 48px; +} +.solidBackground { + background: var(--secondary-bg); + opacity: 1; +} +.EmojiGuildMenu { + position: absolute; + background: var(--dock-bg); + padding: 6px; + border-radius: 6px; + width: 350px; + animation-duration: 0.2s; + animation-name: fade-grow-in; + animation-timing-function: ease-out; + z-index: 1; +} +.GuildEmojiTop { + background: var(--primary-bg); + padding: 8px; + border-radius: 6px; + margin-bottom: 10px; + img { + margin-right: 10px; + } +} + +.guildEmojiRow { + img { + margin-right: 8px; + margin-bottom: 0px; + flex-shrink: 0; + } + margin: 4px; +} +.guildEmojiText { + display: flex; + justify-content: center; +} + +.optionElement:has(.friendGroupSelect) { + flex-shrink: 1; + overflow-y: scroll; +} +.friendGroupSelect { + > div { + padding: 6px; + display: flex; + align-items: center; + .pfpDiv { + margin-right: 12px; + width: 40px; + height: 40px; + } + input, + button { + margin-left: auto; + } + input { + height: 20px; + width: 20px; + } + } +} +.expandButtons { + .optionElement { + flex-grow: 1; + button { + width: 100%; + text-align: center; + } + } +} +.groupDmDiv { + position: relative; + width: 32px; + height: 32px; + img { + background: var(--channels-bg); + position: absolute; + width: 16px; + height: 16px; + &:nth-child(1) { + width: 32px; + height: 32px; + } + &:nth-child(2) { + right: 0px; + bottom: 0px; + } + &:nth-child(3) { + left: 0px; + bottom: 0px; + } + &:nth-child(4) { + right: 0px; + top: 0px; + } + &:nth-child(5) { + left: 0px; + top: 0px; + } + } +} + +#sentdms .groupDmDiv { + background: transparent; + width: 48px; + height: 48px; + img { + width: 24px; + height: 24px; + &:nth-child(1) { + width: 48px; + height: 48px; + } + margin-bottom: 0px; + } +} +.guildPreview { + .banner { + position: relative !important; + margin-left: 10px; + border-radius: 10px; + } + .icon { + flex-shrink: 0; + } +} +.inviteBG { + position: absolute; + width: 100%; + height: 100%; + object-fit: cover; + opacity: 0.8; +} +.threadBox { + padding: 6px; + background: #00000063; + border-radius: 4px; + margin: 6px; + min-width: 300px; + width: fit-content; + margin-left: 50px; + margin-top: 10px; + position: relative; + ::before { + content: ""; + position: absolute; + top: -8px; + left: -32px; + height: calc(100% - 8px); + width: 26px; + border-bottom: 2px solid var(--reply-line); + border-left: 2px solid var(--reply-line); + border-bottom-left-radius: 8px; + } + .clickable { + color: var(--link); + margin-left: auto; + } } -.friendlyButton:hover{ - background:black; +.embedImg { + max-width: 400px; + max-height: 400px; + border-radius: 3px; } diff --git a/src/webpage/tag.ts b/src/webpage/tag.ts new file mode 100644 index 00000000..feb4b715 --- /dev/null +++ b/src/webpage/tag.ts @@ -0,0 +1,36 @@ +import {Channel} from "./channel"; +import {tagjson} from "./jsontypes"; +import {SnowFlake} from "./snowflake"; + +export class Tag extends SnowFlake { + name: string; + moderated: boolean; + emoji_id?: string; + emoji_name?: string; + owner: Channel; + constructor(json: tagjson, owner: Channel) { + super(json.id); + this.name = json.name; + this.moderated = json.moderated; + this.update(json); + this.owner = owner; + } + update(json: tagjson) { + this.name = json.name; + this.moderated = json.moderated; + this.emoji_id = json.emoji_id; + this.emoji_name = json.emoji_name; + } + makeHTML() { + const tagDiv = document.createElement("div"); + tagDiv.classList.add("forumTag"); + + //TODO render emojis + + const name = document.createElement("span"); + name.textContent = this.name; + tagDiv.append(name); + + return tagDiv; + } +} diff --git a/src/webpage/template.html b/src/webpage/template.html new file mode 100644 index 00000000..efd0ce24 --- /dev/null +++ b/src/webpage/template.html @@ -0,0 +1,35 @@ + + + + + + Fermi + + + + + + + + + + +
+
+

Use Template Name

+

+ +
+
+ + + diff --git a/src/webpage/templatePage.ts b/src/webpage/templatePage.ts new file mode 100644 index 00000000..55c606dc --- /dev/null +++ b/src/webpage/templatePage.ts @@ -0,0 +1,110 @@ +import {I18n} from "./i18n.js"; +import {templateSkim} from "./jsontypes.js"; +import {getapiurls} from "./utils/utils.js"; +import {getBulkUsers, Specialuser} from "./utils/utils.js"; +if (window.location.pathname.startsWith("/template")) + (async () => { + const users = getBulkUsers(); + const well = new URLSearchParams(window.location.search).get("instance"); + const joinable: Specialuser[] = []; + + for (const key in users.users) { + if (Object.prototype.hasOwnProperty.call(users.users, key)) { + const user: Specialuser = users.users[key]; + if (well && user.serverurls.wellknown.includes(well)) { + joinable.push(user); + } + console.log(user); + } + } + + let urls: {api: string; cdn: string} | undefined; + + if (!joinable.length && well) { + const out = await getapiurls(well); + if (out) { + urls = out; + for (const key in users.users) { + if (Object.prototype.hasOwnProperty.call(users.users, key)) { + const user: Specialuser = users.users[key]; + if (user.serverurls.api.includes(out.api)) { + joinable.push(user); + } + console.log(user); + } + } + } else { + throw new Error("Someone needs to handle the case where the servers don't exist"); + } + } else { + urls = joinable[0].serverurls; + } + await I18n.done; + if (!joinable.length) { + document.getElementById("usetemplate")!.textContent = I18n.htmlPages.noAccount(); + } + + const code = window.location.pathname.split("/")[2]; + + fetch(`${urls!.api}/guilds/templates/${code}`, { + method: "GET", + headers: { + Authorization: joinable[0].token, + }, + }) + .then((response) => response.json()) + .then((json) => { + const template = json as templateSkim; + document.getElementById("templatename")!.textContent = I18n.useTemplate(template.name); + document.getElementById("templatedescription")!.textContent = template.description; + }); + + function showAccounts(): void { + const table = document.createElement("dialog"); + for (const user of joinable) { + console.log(user.pfpsrc); + + const userinfo = document.createElement("div"); + userinfo.classList.add("flexltr", "switchtable"); + + const pfp = document.createElement("img"); + pfp.src = user.pfpsrc; + pfp.classList.add("pfp"); + userinfo.append(pfp); + + const userDiv = document.createElement("div"); + userDiv.classList.add("userinfo"); + userDiv.textContent = user.username; + userDiv.append(document.createElement("br")); + + const span = document.createElement("span"); + span.textContent = user.serverurls.wellknown.replace("https://", "").replace("http://", ""); + span.classList.add("serverURL"); + userDiv.append(span); + + userinfo.append(userDiv); + table.append(userinfo); + + userinfo.addEventListener("click", () => { + const search = new URLSearchParams(); + search.set("templateID", code); + sessionStorage.setItem("currentuser", user.uid); + window.location.assign("/channels/@me?" + search); + }); + } + + if (!joinable.length) { + const l = new URLSearchParams("?"); + l.set("goback", window.location.href); + l.set("instance", well!); + window.location.href = "/login?" + l.toString(); + } + + table.classList.add("flexttb", "accountSwitcher"); + console.log(table); + document.body.append(table); + } + + document.getElementById("usetemplate")!.addEventListener("click", showAccounts); + document.getElementById("usetemplate")!.textContent = I18n.useTemplateButton(); + })(); diff --git a/src/webpage/themes.css b/src/webpage/themes.css index 7d77284e..8eaa2262 100644 --- a/src/webpage/themes.css +++ b/src/webpage/themes.css @@ -1,33 +1,59 @@ +@font-face { + font-family: "BetterFont"; + src: url("./Commissioner-Regular.woff2") format("woff2"); +} + :root { - --font: "acumin-pro", "Helvetica Neue", Helvetica, Arial, sans-serif; + --font: "BetterFont", "acumin-pro", "Helvetica Neue", Helvetica, Arial, sans-serif; --black: #000000; --red: #ff5555; --yellow: #ffc159; --green: #1c907b; + --blue: #779bff; + --grey: #b5b5b5; + --update: var(--green); } /* Themes. See themes.txt */ .Dark-theme { color-scheme: dark; - + --primary-button-bg: color-mix(in srgb, #777777 10%, var(--accent-color)); --primary-bg: #303339; --primary-hover: #272b31; --primary-text: #dfdfdf; --primary-text-soft: #adb8b9; - --secondary-bg: #16191b; --secondary-hover: #252b2c; - - --servers-bg: #141718; - --channels-bg: #25282b; + --servers-bg: #191c1d; + --channels-bg: #2a2d33; + --channel-selected: #3c4046; + --typebox-bg: #3a3e45; + --button-bg: #4e5457; + --button-hover: #6b7174; + --spoiler-bg: #000000; + --link: #5ca9ed; + --primary-text-prominent: #efefef; + --dock-bg: #1b1e20; + --card-bg: #000000; +} +/*TODO This is a copy of the dark theme, might want to look into other solutions*/ +.no-theme { + color-scheme: dark; + --primary-button-bg: color-mix(in srgb, #777777 10%, var(--accent-color)); + --primary-bg: #303339; + --primary-hover: #272b31; + --primary-text: #dfdfdf; + --primary-text-soft: #adb8b9; + --secondary-bg: #16191b; + --secondary-hover: #252b2c; + --servers-bg: #191c1d; + --channels-bg: #2a2d33; --channel-selected: #3c4046; --typebox-bg: #3a3e45; - --button-bg: #4e5457; --button-hover: #6b7174; --spoiler-bg: #000000; --link: #5ca9ed; - --primary-text-prominent: #efefef; --dock-bg: #1b1e20; --card-bg: #000000; @@ -36,6 +62,7 @@ .WHITE-theme { color-scheme: light; + --primary-button-bg: color-mix(in srgb, #000000 10%, var(--accent-color)); --primary-bg: #fefefe; --primary-hover: #f6f6f9; --primary-text: #4b4b59; @@ -65,7 +92,7 @@ .Light-theme { color-scheme: light; - + --primary-button-bg: white; --primary-bg: #aaafce; --primary-hover: #b1b6d4; --primary-text: #060415; @@ -73,7 +100,7 @@ --secondary-bg: #9397bd; --secondary-hover: #9ea5cc; - + --servers-bg: #7a7aaa; --channels-bg: #babdd2; --channel-selected: #9c9fbf; @@ -93,7 +120,7 @@ .Dark-Accent-theme { color-scheme: dark; - + --primary-button-bg: color-mix(in srgb, #777777 10%, var(--accent-color)); --primary-bg: color-mix(in srgb, #3f3f3f 65%, var(--accent-color)); --primary-hover: color-mix(in srgb, #373737 68%, var(--accent-color)); --primary-text: #ebebeb; @@ -101,6 +128,7 @@ --secondary-bg: color-mix(in srgb, #222222 72%, var(--accent-color)); --secondary-hover: color-mix(in srgb, #222222 65%, var(--accent-color)); + --folder-bg: color-mix(in srgb, var(--primary-text-soft) 70%, var(--accent-color)); --servers-bg: color-mix(in srgb, #0b0b0b 70%, var(--accent-color)); --channels-bg: color-mix(in srgb, #292929 68%, var(--accent-color)); @@ -112,12 +140,13 @@ --spoiler: color-mix(in srgb, #101010 72%, var(--accent-color)); --link: color-mix(in srgb, #99ccff 75%, var(--accent-color)); - + --black: color-mix(in srgb, #000000 90%, var(--accent-color)); --icon: color-mix(in srgb, #ffffff, var(--accent-color)); --dock-bg: color-mix(in srgb, #171717 68%, var(--accent-color)); --spoiler-hover: color-mix(in srgb, #111111 80%, var(--accent-color)); --card-bg: color-mix(in srgb, #0b0b0b 70%, var(--accent-color)); + --spoiler-bg: #000000; } /* Optional Variables */ @@ -134,6 +163,7 @@ body { --shadow: color-mix(in srgb, var(--black) 30%, transparent); --scrollbar: var(--primary-text-soft); --scrollbar-track: var(--primary-hover); + --folder-bg: var(--primary-text-soft); --blank-bg: var(--channels-bg); --divider: color-mix(in srgb, var(--primary-text), transparent); @@ -173,4 +203,5 @@ body { --settings-panel-hover: color-mix(in srgb, var(--settings-panel-selected), transparent); --loading-bg: var(--secondary-bg); --loading-text: var(--secondary-text); -} \ No newline at end of file + --embed-bg-side: color-mix(in srgb, var(--embed-bg), black); +} diff --git a/src/webpage/user.ts b/src/webpage/user.ts index 621793f9..128a184b 100644 --- a/src/webpage/user.ts +++ b/src/webpage/user.ts @@ -1,123 +1,289 @@ -import{ Member }from"./member.js"; -import{ MarkDown }from"./markdown.js"; -import{ Contextmenu }from"./contextmenu.js"; -import{ Localuser }from"./localuser.js"; -import{ Guild }from"./guild.js"; -import{ SnowFlake }from"./snowflake.js"; -import{ presencejson, userjson }from"./jsontypes.js"; -import { Role } from "./role.js"; -import { Search } from "./search.js"; -import { I18n } from "./i18n.js"; -import { Direct } from "./direct.js"; - -class User extends SnowFlake{ +import {Member} from "./member.js"; +import {MarkDown} from "./markdown.js"; +import {Contextmenu} from "./contextmenu.js"; +import {Localuser} from "./localuser.js"; +import {Guild} from "./guild.js"; +import {SnowFlake} from "./snowflake.js"; +import {highMemberJSON, presencejson, relationJson, userjson, webhookInfo} from "./jsontypes.js"; +import {Role} from "./role.js"; +import {Search} from "./search.js"; +import {I18n} from "./i18n.js"; +import {Hover} from "./hover.js"; +import {Dialog, Float, Options} from "./settings.js"; +import {createImg, removeAni, safeImg} from "./utils/utils.js"; +import {Direct} from "./direct.js"; +import {Permissions} from "./permissions.js"; +import {Channel} from "./channel.js"; +import {getDeveloperSettings} from "./utils/storage/devSettings"; +import {ReportMenu} from "./reporting/report.js"; +import {CDNParams} from "./utils/cdnParams.js"; +export const userVolMenu = new Contextmenu("user vol stacked", true); +userVolMenu.addSlider( + () => I18n.Voice.userVol(), + (local, id, e) => { + local.setUserAudio(id, e); + }, + undefined, + "default", + { + startVal: (local, id) => { + return local.getUserAudio(id); + }, + }, +); +class User extends SnowFlake { owner: Localuser; hypotheticalpfp!: boolean; avatar!: string | null; + uid: string; username!: string; nickname: string | null = null; relationshipType: 0 | 1 | 2 | 3 | 4 | 5 | 6 = 0; bio!: MarkDown; discriminator!: string; - pronouns!: string; + pronouns?: string; bot!: boolean; public_flags!: number; + webhook?: webhookInfo; accent_color!: number; banner: string | undefined; hypotheticalbanner!: boolean; premium_since!: string; premium_type!: number; - theme_colors!: string; + theme_colors: [number, number] | null = null; badge_ids!: string[]; - members: WeakMap> = - new WeakMap(); + members: WeakMap> = new WeakMap(); status!: string; + avatar_decoration_data?: { + asset: string; + sku_id: string; + } | null; + resolving: false | Promise = false; + get headers() { + return this.localuser.headers; + } - constructor(userjson: userjson, owner: Localuser, dontclone = false){ + constructor(userjson: userjson, owner: Localuser, dontclone: boolean = false) { super(userjson.id); this.owner = owner; - if(!owner){ + if (getDeveloperSettings().logBannedFields && owner.user && owner.user.id !== userjson.id) { + this.checkfortmi(userjson); + } + if (!owner) { console.error("missing localuser"); } - if(dontclone){ - for(const key of Object.keys(userjson)){ - if(key === "bio"){ - this.bio = new MarkDown(userjson[key], this.localuser); - continue; - } - if(key === "id"){ - continue; - } - (this as any)[key] = (userjson as any)[key]; - } + this.uid = userjson.id; + if (userjson.webhook) { + this.uid += ":::" + userjson.username; + console.log(this.uid); + } + userjson.uid = this.uid; + if (dontclone) { + this.userupdate(userjson); this.hypotheticalpfp = false; - }else{ + } else { return User.checkuser(userjson, owner); } } + static makeSelector( + opt: Options, + doneText: string, + options: User[], + {single = false, addText = I18n.add()} = {}, + ): Promise | void> { + return new Promise | void>((res) => { + const div = document.createElement("div"); + div.classList.add("flexttb", "friendGroupSelect"); - clone(): User{ - return new User( - { - username: this.username, - id: this.id + "#clone", - public_flags: this.public_flags, - discriminator: this.discriminator, - avatar: this.avatar, - accent_color: this.accent_color, - banner: this.banner, - bio: this.bio.rawString, - premium_since: this.premium_since, - premium_type: this.premium_type, - bot: this.bot, - theme_colors: this.theme_colors, - pronouns: this.pronouns, - badge_ids: this.badge_ids, - }, - this.owner + const invited = new Set(); + + const makeList = (search: string) => { + const list = options + .map((user) => [user, user.compare(search)] as const) + .filter((_) => _[1] !== 0) + .sort((a, b) => a[1] - b[1]) + .map((_) => _[0]); + div.innerHTML = ""; + div.append( + ...list.map((user) => { + const div = document.createElement("div"); + div.classList.add("flexltr"); + + //TODO implement status stuff here once spacebar really supports it + div.append(user.buildpfp(), user.name); + if (single) { + const button = document.createElement("button"); + button.textContent = addText; + div.append(button); + button.onclick = () => res(new Set([user])); + } else { + const check = document.createElement("input"); + check.type = "checkbox"; + check.checked = invited.has(user); + check.onchange = () => { + if (check.checked) { + invited.add(user); + } else { + invited.delete(user); + } + }; + div.append(check); + } + return div; + }), + ); + }; + opt.addTextInput("", () => {}).onchange = makeList; + opt.addHTMLArea(div); + const buttons = opt.addOptions("", {ltr: true}); + buttons.addButtonInput("", I18n.cancel(), () => { + res(); + }); + buttons.addButtonInput("", doneText, async () => { + res(invited); + }); + + makeList(""); + buttons.container.deref()?.classList.add("expandButtons"); + }); + } + compare(str: string) { + function similar(str2: string | null | undefined) { + if (!str2) return 0; + const strl = Math.max(str.length, 1); + if (str2.includes(str)) { + return strl / str2.length; + } else if (str2.toLowerCase().includes(str.toLowerCase())) { + return strl / str2.length / 1.2; + } + return 0; + } + return Math.max( + similar(this.name), + similar(this.nickname), + similar(this.username), + similar(this.id) / 1.5, ); } + /** + * function is meant to check if userjson contains too much information IE non-public stuff + * + * + */ + checkfortmi(json: any) { + if (json.data) { + console.error("Server sent *way* too much info, this is really bad, it sent data"); + } + const bad = new Set([ + "fingerprints", + "extended_settings", + "mfa_enabled", + "nsfw_allowed", + "premium_usage_flags", + "totp_last_ticket", + "totp_secret", + "webauthn_enabled", + ]); + if (!this.localuser.rights.getPermission("OPERATOR")) { + //Unless the user is an operator, we really shouldn't ever see this + bad.add("rights"); + } + for (const thing of bad) { + if (json.hasOwnProperty(thing)) { + console.error(thing + " should not be exposed to the client"); + } + } + } + tojson(): userjson { + return { + username: this.username, + id: this.id, + public_flags: this.public_flags, + discriminator: this.discriminator, + avatar: this.avatar, + accent_color: this.accent_color, + banner: this.banner, + bio: this.bio.rawString, + premium_since: this.premium_since, + premium_type: this.premium_type, + bot: this.bot, + theme_colors: this.theme_colors, + pronouns: this.pronouns, + badge_ids: this.badge_ids, + }; + } - public getPresence(presence: presencejson | undefined): void{ - if(presence){ + clone(): User { + const json = this.tojson(); + json.id += "#clone"; + return new User(json, this.owner); + } + + public getPresence(presence: presencejson | undefined): void { + if (presence) { this.setstatus(presence.status); - }else{ + } else { this.setstatus("offline"); } } - get online(){ - return (this.status)&&(this.status!="offline"); + get online() { + return this.status && this.status != "offline"; } - setstatus(status: string): void{ + setstatus(status: string): void { this.status = status; + const has = this.statusChange(); + if (has) this.localstatusUpdate(); } - getStatus(): string{ + getStatus(): string { return this.status || "offline"; } static contextmenu = new Contextmenu("User Menu"); - async opendm(){ - for(const dm of (this.localuser.guildids.get("@me") as Direct).channels){ - if(dm.user.id===this.id){ - this.localuser.goToChannel(dm.id); + async opendm(message?: string) { + for (const dm of (this.localuser.guildids.get("@me") as Direct).channels) { + if ((dm.type === 1 || dm.type === undefined) && dm.users[0].id === this.id) { + await this.localuser.goToChannel(dm.id); + if (message) { + await dm.sendMessage(message, { + attachments: [], + embeds: [], + replyingto: null, + sticker_ids: [], + }); + } + return; } } + await fetch(this.info.api + "/users/@me/channels", { method: "POST", - body: JSON.stringify({ recipients: [this.id] }), + body: JSON.stringify({recipients: [this.id]}), headers: this.localuser.headers, }) - .then(res=>res.json()) - .then(json=>{ - this.localuser.goToChannel(json.id); - }); + .then((res) => res.json()) + .then((json) => { + return this.localuser.goToChannel(json.id); + }); + if (message) { + for (const dm of (this.localuser.guildids.get("@me") as Direct).channels) { + if ((dm.type === 1 || dm.type === undefined) && dm.users[0].id === this.id) { + dm.sendMessage(message, { + attachments: [], + embeds: [], + replyingto: null, + sticker_ids: [], + }); + return; + } + } + } return; } - async changeRelationship(type:0|1|2|3|4|5){ - if(type!==0){ + async changeRelationship(type: 0 | 1 | 2 | 3 | 4 | 5) { + if (type !== 0) { await fetch(`${this.info.api}/users/@me/relationships/${this.id}`, { method: "PUT", headers: this.owner.headers, @@ -125,178 +291,453 @@ class User extends SnowFlake{ type, }), }); - }else{ + } else { await fetch(`${this.info.api}/users/@me/relationships/${this.id}`, { method: "DELETE", - headers: this.owner.headers + headers: this.owner.headers, }); } - this.relationshipType=type; + this.relationshipType = type; } - static setUpContextMenu(): void{ - this.contextmenu.addbutton(()=>I18n.getTranslation("user.copyId"), function(this: User){ - navigator.clipboard.writeText(this.id); - }); - this.contextmenu.addbutton(()=>I18n.getTranslation("user.message"), function(this: User){ - this.opendm(); - }); - this.contextmenu.addbutton( - ()=>I18n.getTranslation("user.block"), - function(this: User){ + static setUpContextMenu(): void { + this.contextmenu.addButton( + () => I18n.user.message(), + function (this: User) { + this.opendm(); + }, + { + icon: { + css: "svg-frmessage", + }, + }, + ); + + this.contextmenu.addSeperator(); + + this.contextmenu.addButton( + () => I18n.user.block(), + function (this: User) { this.block(); }, - null, - function(){ - return this.relationshipType !== 2; - } + { + visible: function () { + return this.relationshipType !== 2 && this.id !== this.localuser.user.id; + }, + }, ); - this.contextmenu.addbutton( - ()=>I18n.getTranslation("user.unblock"), - function(this: User){ + this.contextmenu.addButton( + () => I18n.user.unblock(), + function (this: User) { this.unblock(); }, - null, - function(){ - return this.relationshipType === 2; - } + { + visible: function () { + return this.relationshipType === 2 && this.id !== this.localuser.user.id; + }, + }, ); - this.contextmenu.addbutton(()=>I18n.getTranslation("user.friendReq"), function(this: User){ - this.changeRelationship(1); - }); - this.contextmenu.addbutton( - ()=>I18n.getTranslation("user.kick"), - function(this: User, member: Member | undefined){ - member?.kick(); + this.contextmenu.addButton( + () => I18n.user.friendReq(), + function (this: User) { + this.changeRelationship(1); }, - null, - member=>{ - if(!member)return false; - const us = member.guild.member; - if(member.id === us.id){ - return false; - } - if(member.id === member.guild.properties.owner_id){ - return false; + { + visible: function () { + return ( + (this.relationshipType === 0 || this.relationshipType === 3) && + this.id !== this.localuser.user.id && + !this.bot + ); + }, + icon: { + css: "svg-addfriend", + }, + }, + ); + this.contextmenu.addButton( + () => I18n.friends.removeFriend(), + function (this: User) { + this.changeRelationship(0); + }, + { + visible: function () { + return this.relationshipType === 1 && this.id !== this.localuser.user.id; + }, + }, + ); + this.contextmenu.addButton( + function () { + switch (this.relationshipType) { + case 1: + return I18n.user.nick.friend(); + case 2: + return I18n.user.nick.foe(); + case 3: + return I18n.user.nick.stalker(); + case 4: + return I18n.user.nick.stalking(); + default: + return "You shouldn't see this"; } - return us.hasPermission("KICK_MEMBERS") || false; - } + }, + function (this: User) { + this.setFriendNick(); + }, + { + visible: function () { + return new Set([1, 2, 3, 4]).has(this.relationshipType); + }, + }, + ); + + this.contextmenu.addSeperator(); + + this.contextmenu.addButton( + () => I18n.user.editServerProfile(), + function (this: User, member: Member | undefined) { + if (!member) return; + member.showEditProfile(); + }, + { + visible: function (member) { + return member?.id === this.localuser.user.id; + }, + }, + ); + + this.contextmenu.addButton( + () => I18n.user.editNick(), + function (this: User, member: Member | undefined) { + if (!member) return; + member.showEditNick(); + }, + { + visible: function (member) { + return ( + !!member && + member?.id !== this.localuser.user.id && + member.guild.member.hasPermission("MANAGE_NICKNAMES") + ); + }, + }, + ); + + this.contextmenu.addButton( + () => I18n.user.timeout(), + function (this: User, member: Member | undefined) { + member?.timeout(); + }, + { + visible: function (member) { + if (!member) return false; + if (member.hasPermission("MODERATE_MEMBERS")) return false; + + return ( + !member.commuicationDisabledLeft() && + member.guild.member.hasPermission("MODERATE_MEMBERS") + ); + }, + color: "red", + icon: { + css: "svg-timeout", + }, + }, ); - this.contextmenu.addbutton( - ()=>I18n.getTranslation("user.ban"), - function(this: User, member: Member | undefined){ + this.contextmenu.addButton( + () => I18n.user.unTimeout(), + function (memb) { + memb?.removeTimeout(); + }, + { + visible: function (member) { + if (!member) return false; + + return ( + !!member.commuicationDisabledLeft() && + member.guild.member.hasPermission("MODERATE_MEMBERS") + ); + }, + color: "red", + }, + ); + + //TODO kick icon + this.contextmenu.addButton( + () => I18n.user.kick(), + function (this: User, member: Member | undefined) { + member?.kick(); + }, + { + visible: function (member) { + if (!member) return false; + const us = member.guild.member; + if (member.id === us.id) { + return false; + } + if (member.id === member.guild.properties.owner_id) { + return false; + } + return us.hasPermission("KICK_MEMBERS") && this.id !== this.localuser.user.id; + }, + color: "red", + }, + ); + + //TODO ban icon + this.contextmenu.addButton( + () => I18n.user.ban(), + function (this: User, member: Member | undefined) { member?.ban(); }, - null, - member=>{ - if(!member)return false; - const us = member.guild.member; - if(member.id === us.id){ - return false; - } - if(member.id === member.guild.properties.owner_id){ - return false; - } - return us.hasPermission("BAN_MEMBERS") || false; - } + { + visible: function (member) { + if (!member) return false; + const us = member.guild.member; + if (member.id === us.id) { + return false; + } + if (member.id === member.guild.properties.owner_id) { + return false; + } + return us.hasPermission("BAN_MEMBERS") && this.id !== this.localuser.user.id; + }, + color: "red", + }, ); - this.contextmenu.addbutton( - ()=>I18n.getTranslation("user.addRole"), - async function(this: User, member: Member | undefined,e){ - if(member){ + + this.contextmenu.addSeperator(); + + this.contextmenu.addButton( + () => I18n.user.addRole(), + async function (this: User, member: Member | undefined, e) { + if (member) { e.stopPropagation(); - const roles:[Role,string[]][]=[]; - for(const role of member.guild.roles){ - if(!role.canManage()||member.roles.indexOf(role)!==-1){ + const roles: [Role, string[]][] = []; + for (const role of member.guild.roles) { + if (!role.canManage() || member.roles.indexOf(role) !== -1) { continue; } - roles.push([role,[role.name]]); + roles.push([role, [role.name]]); } - const search=new Search(roles); - const result=await search.find(e.x,e.y); - if(!result) return; + const search = new Search(roles); + const result = await search.find(e.x, e.y); + if (!result) return; member.addRole(result); } }, - null, - member=>{ - if(!member)return false; - const us = member.guild.member; - console.log(us.hasPermission("MANAGE_ROLES")) - return us.hasPermission("MANAGE_ROLES") || false; - } + { + visible: (member) => { + if (!member) return false; + const us = member.guild.member; + console.log(us.hasPermission("MANAGE_ROLES")); + return us.hasPermission("MANAGE_ROLES") || false; + }, + }, ); - this.contextmenu.addbutton( - ()=>I18n.getTranslation("user.removeRole"), - async function(this: User, member: Member | undefined,e){ - if(member){ + this.contextmenu.addButton( + () => I18n.user.removeRole(), + async function (this: User, member: Member | undefined, e) { + if (member) { e.stopPropagation(); - const roles:[Role,string[]][]=[]; - for(const role of member.roles){ - if(!role.canManage()){ + const roles: [Role, string[]][] = []; + for (const role of member.roles) { + if (!role.canManage()) { continue; } - roles.push([role,[role.name]]); + roles.push([role, [role.name]]); } - const search=new Search(roles); - const result=await search.find(e.x,e.y); - if(!result) return; + const search = new Search(roles); + const result = await search.find(e.x, e.y); + if (!result) return; member.removeRole(result); } }, - null, - member=>{ - if(!member)return false; - const us = member.guild.member; - console.log(us.hasPermission("MANAGE_ROLES")) - return us.hasPermission("MANAGE_ROLES") || false; - } + { + visible: (member) => { + if (!member) return false; + const us = member.guild.member; + console.log(us.hasPermission("MANAGE_ROLES")); + return us.hasPermission("MANAGE_ROLES") || false; + }, + }, + ); + + this.contextmenu.addSeperator(); + this.contextmenu.addButton( + () => I18n.user.copyId(), + function (this: User) { + navigator.clipboard.writeText(this.id); + }, ); + + this.contextmenu.addSeperator(); + + this.contextmenu.addButton( + () => I18n.user.report(), + async function (member) { + const menu = await ReportMenu.makeReport("user", this.localuser, {user: this, member}); + menu?.spawnMenu(); + }, + { + visible: function () { + const settings = getDeveloperSettings(); + return this.id !== this.localuser.user.id && settings.reportSystem; + }, + color: "red", + }, + ); + this.contextmenu.addButton( + () => I18n.user.reportApp(), + async function () { + const menu = await ReportMenu.makeReport("application", this.localuser, { + application_id: this.id, + }); + menu?.spawnMenu(); + }, + { + visible: function () { + const settings = getDeveloperSettings(); + return this.bot && settings.reportSystem; + }, + color: "red", + }, + ); + + this.contextmenu.addButton( + () => I18n.user.instanceBan(), + function (this: User) { + const params = { + reason: "", + persistInstanceBan: true, + }; + const menu = new Dialog(""); + const options = menu.float.options; + options.addTitle(I18n.user.confirmInstBan(this.name)); + options.addTextInput(I18n.member["reason:"](), () => {}, {}).onchange = (txt) => { + params.reason = txt; + }; + options.addCheckboxInput(I18n.member.persist(), () => {}, { + initState: false, + }).onchange = (checked) => { + params.persistInstanceBan = !checked; + }; + const opt = options.addOptions("", {ltr: true}); + opt.addButtonInput("", I18n.yes(), () => { + fetch(this.info.api + "/users/" + this.id + "/delete", { + headers: this.localuser.headers, + method: "POST", + body: JSON.stringify(params), + }); + menu.hide(); + }); + opt.addButtonInput("", I18n.no(), () => { + menu.hide(); + }); + menu.show(); + }, + { + visible: function () { + return this.localuser.rights.hasPermission("MANAGE_USERS"); + }, + color: "red", + }, + ); + console.warn("this ran"); + } + setFriendNick() { + const dio = new Dialog(""); + const form = dio.options.addForm( + "", + () => { + dio.hide(); + }, + { + fetchURL: this.info.api + `/users/@me/relationships/${this.id}`, + method: "PATCH", + headers: this.headers, + }, + ); + form.addTextInput(I18n.member["nick:"](), "nickname", { + initText: this.nickname || "", + }); + dio.show(); + } + getMembersSync() { + return this.localuser.guilds + .map((guild) => { + const m = this.members.get(guild); + return m instanceof Member ? m : undefined; + }) + .filter((m) => m !== undefined); + } + + elms = new Set>(); + subName(elm: HTMLElement) { + this.elms.add(new WeakRef(elm)); + } + nameChange() { + this.getMembersSync().forEach((memb) => { + memb.nameChange(); + }); + + for (const ref of this.elms) { + const elm = ref.deref(); + if (!elm || !document.contains(elm)) { + this.elms.delete(ref); + continue; + } + elm.textContent = this.name; + } } - static checkuser(user: User | userjson, owner: Localuser): User{ - if(owner.userMap.has(user.id)){ - return owner.userMap.get(user.id) as User; - }else{ + static checkuser(user: User | userjson, owner: Localuser): User { + const tempUser = owner.userMap.get(user.uid || user.id); + if (tempUser) { + if (!(user instanceof User)) { + tempUser.userupdate(user); + } + return tempUser; + } else { const tempuser = new User(user as userjson, owner, true); - owner.userMap.set(user.id, tempuser); + owner.userMap.set(user.uid || user.id, tempuser); return tempuser; } } - get info(){ + get info() { return this.owner.info; } - get localuser(){ + get localuser() { return this.owner; } - get name(){ - return this.username; + get name() { + return this.nickname || (this.relationshipType === 2 ? I18n.friends.bu() : this.username); } - async resolvemember(guild: Guild): Promise{ + async resolvemember(guild: Guild): Promise { return await Member.resolveMember(this, guild); } - async getUserProfile(): Promise{ + async getUserProfile(): Promise { return await fetch( `${this.info.api}/users/${this.id.replace( "#clone", - "" + "", )}/profile?with_mutual_guilds=true&with_mutual_friends=true`, { headers: this.localuser.headers, - } - ).then(res=>res.json()); + }, + ).then((res) => res.json()); } - async getBadge(id: string): Promise{ - if(this.localuser.badges.has(id)){ + async getBadge(id: string) { + if (this.localuser.badges.has(id)) { return this.localuser.badges.get(id); - }else{ - if(this.resolving){ + } else { + if (this.resolving) { await this.resolving; return this.localuser.badges.get(id); } @@ -305,199 +746,770 @@ class User extends SnowFlake{ this.resolving = prom; const badges = prom.badges; this.resolving = false; - for(const badge of badges){ + for (const badge of badges) { this.localuser.badges.set(badge.id, badge); } return this.localuser.badges.get(id); } } - buildpfp(): HTMLImageElement{ - const pfp = document.createElement("img"); + buildpfp(guild: Guild | void | Member | null, hoverElm: void | HTMLElement): HTMLDivElement { + const div = document.createElement("div"); + div.classList.add("pfpDiv"); + hoverElm ??= div; + const pfp = createImg(this.getpfpsrc(), undefined, hoverElm); pfp.loading = "lazy"; - pfp.src = this.getpfpsrc(); pfp.classList.add("pfp"); - pfp.classList.add("userid:" + this.id); - return pfp; + if (!this.webhook) pfp.classList.add("userid:" + this.id); + if (guild) { + (async () => { + if (guild instanceof Guild) { + const memb = await Member.resolveMember(this, guild); + if (!memb) return; + pfp.setSrcs(memb.getpfpsrc()); + } else { + pfp.setSrcs(guild.getpfpsrc()); + } + })(); + } + if (this.avatar_decoration_data && this.localuser.perminfo.user.decorations) { + const dec = createImg( + this.info.cdn + + `/avatar-decoration-presets/${this.avatar_decoration_data.asset}.png` + + new CDNParams({expectedSize: 96}), + void 0, + hoverElm, + ); + dec.classList.add("avDec"); + div.append(dec); + } + div.append(pfp); + return div; } - - async buildstatuspfp(): Promise{ + createWidget(guild?: Guild) { + guild = this.localuser.guildids.get("@me") as Guild; const div = document.createElement("div"); - div.classList.add("pfpDiv") - const pfp = this.buildpfp(); - div.append(pfp); + div.classList.add("flexltr", "createdWebhook"); + //TODO make sure this is something I can actually do here + const name = document.createElement("b"); + name.textContent = this.name; + + const nameBox = document.createElement("div"); + nameBox.classList.add("flexttb"); + nameBox.append(name); + const pfp = this.buildpfp(undefined, div); + div.append(pfp, nameBox); + Member.resolveMember(this, guild).then((_) => { + if (_) { + _.subName(name); + name.textContent = _.name; + (pfp.children[0] as HTMLImageElement).src = _.getpfpsrc(); + } else if (guild.id !== "@me") { + this.subName(name); + const notFound = document.createElement("span"); + notFound.textContent = I18n.webhooks.notFound(); + nameBox.append(notFound); + } + }); + this.bind(div, guild, undefined); + return div; + } + updateStatusSet = new Set>(); + contextMap = new WeakMap(); + localstatusUpdate = () => {}; + registerStatus(status: HTMLDivElement, thing: Guild | void | Member | null | Channel) { + if (thing) { + if (thing instanceof Member) { + this.contextMap.set(status, thing.guild); + } else { + this.contextMap.set(status, thing); + } + } + this.updateStatusSet.add(new WeakRef(status)); + this.updateStatus(status); + } + updateStatus(status: HTMLDivElement) { + status.classList.remove("offlinestatus", "dndstatus", "onlinestatus", "typingstatus"); + switch (this.getStatus()) { + case "offline": + case "invisible": + status.classList.add("offlinestatus"); + break; + case "dnd": + status.classList.add("dndstatus"); + break; + case "online": + default: + status.classList.add("onlinestatus"); + break; + } + const m = this.contextMap.get(status); + if (m) { + let guild: Guild; + let channel: Channel | void = undefined; + if ("guild" in m) { + channel = m; + guild = m.guild; + } else { + guild = m; + } + const memb = this.members.get(guild); + if (memb && !(memb instanceof Promise) && channel) { + const typing = channel.typingmap.get(memb); + + if (typing) { + status.classList.add("typingstatus"); + } + } + } + } + statusChange() { + let has = false; + for (const ref of this.updateStatusSet) { + const elm = ref.deref(); + if (!elm || !document.body.contains(elm)) { + this.updateStatusSet.delete(ref); + continue; + } + has = true; + this.updateStatus(elm); + } + return has; + } + buildstatuspfp(guild: Guild | void | Member | null | Channel): HTMLDivElement { + const isChannel = !!(guild && "guild" in guild); + const div = this.buildpfp(isChannel ? guild.guild : guild); + const status = document.createElement("div"); + this.registerStatus(status, guild); status.classList.add("statusDiv"); - switch(await this.getStatus()){ - case"offline": - status.classList.add("offlinestatus"); - break; - case"online": - default: - status.classList.add("onlinestatus"); - break; - } + status.append(document.createElement("div")); + div.append(status); return div; } - userupdate(json: userjson): void{ - if(json.avatar !== this.avatar){ - this.changepfp(json.avatar); + userupdate(json: userjson): void { + const up = json.username !== this.username; + if (json.avatar !== this.avatar) { + Array.from(document.getElementsByClassName("userid:" + this.id)).forEach((element) => { + const img = element as safeImg; + if ("setSrcs" in element) { + img.setSrcs(this.getpfpsrc()); + } else { + console.warn("element didn't have setSrcs property"); + } + }); + } + for (const key of Object.keys(json)) { + if (key === "bio") { + this.bio = new MarkDown(json[key], this.localuser); + continue; + } + if (key === "id") { + continue; + } + (this as any)[key] = (json as any)[key]; + } + if ("rights" in this) { + if ( + this === this.localuser.user && + (typeof this.rights == "string" || typeof this.rights == "number") + ) { + this.localuser.updateRights(this.rights); + } + } + if (up) { + this.nameChange(); } } - bind(html: HTMLElement, guild: Guild | null = null, error = true): void{ - if(guild && guild.id !== "@me"){ + bind( + html: HTMLElement, + guild: Guild | null = null, + error = true, + button: "right" | "left" | "none" = "right", + ): void { + if (guild && guild.id !== "@me") { Member.resolveMember(this, guild) - .then(member=>{ + .then((member) => { User.contextmenu.bindContextmenu(html, this, member); - if(member === undefined && error){ + if (member === undefined && error) { + if (this.webhook) return; const errorSpan = document.createElement("span"); errorSpan.textContent = "!"; errorSpan.classList.add("membererror"); html.after(errorSpan); return; } - if(member){ + if (member) { member.bind(html); + } else { + if (button !== "none") + User.contextmenu.bindContextmenu(html, this, undefined, undefined, undefined, button); } }) - .catch(err=>{ + .catch((err) => { console.log(err); }); + } else { + if (button !== "none") + User.contextmenu.bindContextmenu(html, this, undefined, undefined, undefined, button); } - if(guild){ - this.profileclick(html, guild); - }else{ - this.profileclick(html); - } + if (button !== "none") + if (guild) { + this.profileclick(html, guild); + } else { + this.profileclick(html); + } } - static async resolve(id: string, localuser: Localuser): Promise{ - const json = await fetch( - localuser.info.api.toString() + "/users/" + id + "/profile", - { headers: localuser.headers } - ).then(res=>res.json()); + static async resolve(id: string, localuser: Localuser): Promise { + const time = SnowFlake.stringToUnixTime(id); + + if (time < 1420070400000 + 100) + return new User( + { + id: "0", + public_flags: 0, + username: I18n.friends.notfound(), + avatar: null, + discriminator: "0000", + bio: "", + bot: false, + premium_type: 0, + premium_since: "", + accent_color: 0, + theme_colors: null, + badge_ids: [], + }, + localuser, + ); + let user: User | undefined; + if ((user = localuser.userMap.get(id))) return user; + const json = await fetch(localuser.info.api.toString() + "/users/" + id + "/profile", { + headers: localuser.headers, + }).then((res) => res.json()); + if (json.code === 404) { + return new User( + { + id: "0", + public_flags: 0, + username: I18n.friends.notfound(), + avatar: null, + discriminator: "0000", + bio: "", + bot: false, + premium_type: 0, + premium_since: "", + accent_color: 0, + theme_colors: null, + badge_ids: [], + }, + localuser, + ); + } return new User(json.user, localuser); } - changepfp(update: string | null): void{ + changepfp(update: string | null): void { this.avatar = update; this.hypotheticalpfp = false; - const src = this.getpfpsrc(); - Array.from(document.getElementsByClassName("userid:" + this.id)).forEach( - element=>{ - (element as HTMLImageElement).src = src; - } - ); } - async block(){ + async block() { await this.changeRelationship(2); const channel = this.localuser.channelfocus; - if(channel){ - for(const message of channel.messages){ + if (channel) { + for (const message of channel.messages) { message[1].generateMessage(); } } } - async unblock(){ + async unblock() { await this.changeRelationship(0); const channel = this.localuser.channelfocus; - if(channel){ - for(const message of channel.messages){ + if (channel) { + for (const message of channel.messages) { message[1].generateMessage(); } } } - - getpfpsrc(): string{ - if(this.hypotheticalpfp && this.avatar){ + /** + * @param guild this is an optional thing that'll get the src of the member if it exists, otherwise ignores it, this is meant to be fast, not accurate + */ + getpfpsrc(guild: Guild | void): string { + if (this.hypotheticalpfp && this.avatar) { return this.avatar; } - if(this.avatar !== null){ - return`${this.info.cdn}/avatars/${this.id.replace("#clone", "")}/${ - this.avatar - }.png`; - }else{ + if (guild) { + const member = this.members.get(guild); + if (member instanceof Member) { + return member.getpfpsrc(); + } + } + if (this.avatar !== null) { + return ( + `${this.info.cdn}/avatars/${this.id.replace("#clone", "")}/${this.avatar}.png` + + new CDNParams({expectedSize: 96}) + ); + } else { const int = Number((BigInt(this.id.replace("#clone", "")) >> 22n) % 6n); - return`${this.info.cdn}/embed/avatars/${int}.png`; + return `${this.info.cdn}/embed/avatars/${int}.png`; } } - - async buildprofile( - x: number, - y: number, - guild: Guild | null = null - ): Promise{ - if(Contextmenu.currentmenu != ""){ - Contextmenu.currentmenu.remove(); + async getBadges() { + let i = 0; + let flagbits = this.public_flags; + const ids = [ + "staff", + "partner", + "certified_moderator", + "hypesquad", + "hypesquad_house_1", + "hypesquad_house_2", + "hypesquad_house_3", + "bug_hunter_level_1", + "bug_hunter_level_2", + "active_developer", + "verified_developer", + "early_supporter", + "premium", + "guild_booster_lvl1", + "guild_booster_lvl2", + "guild_booster_lvl3", + "guild_booster_lvl4", + "guild_booster_lvl5", + "guild_booster_lvl6", + "guild_booster_lvl7", + "guild_booster_lvl8", + "guild_booster_lvl9", + "bot_commands", + "automod", + "application_guild_subscription", + "legacy_username", + "quest_completed", + ]; + let badgeids: string[] = []; + while (flagbits !== 0) { + if (flagbits & 1) { + badgeids.push(ids[i]); + } + flagbits >>= 1; + i++; } + if (this.badge_ids) { + badgeids = badgeids.concat(this.badge_ids); + } + + let badges: { + id: string; + description: string; + icon: string; + link?: string; + translate?: boolean; + }[] = []; + const b = (await Promise.all(badgeids.map((_) => this.getBadge(_)))).filter( + (_) => _ !== undefined, + ); + badges = b; + + return badges; + } + async highInfo() { + return (await ( + await fetch( + this.info.api + + "/users/" + + this.id + + "/profile?with_mutual_guilds=true&with_mutual_friends=true", + {headers: this.localuser.headers}, + ) + ).json()) as highMemberJSON; + } + handleRelationship(relation: relationJson) { + const nickChange = this.nickname !== relation.nickname; + this.nickname = relation.nickname; + this.relationshipType = relation.type; + this.localuser.inrelation.add(this); + if (nickChange) { + this.nameChange(); + } + } + removeRelation() { + const nickChange = this.nickname; + this.nickname = null; + this.relationshipType = 0; + this.localuser.inrelation.delete(this); + if (nickChange) { + this.nameChange(); + } + } + async fullProfile(guild: Guild | null | Member = null) { + console.log(guild); + const membres = (async () => { + if (!guild) return; + let member: Member | undefined; + if (guild instanceof Guild) { + member = await Member.resolveMember(this, guild); + } else { + member = guild; + } + return member; + })(); + const background = document.createElement("div"); + background.classList.add("background"); + background.onclick = () => { + removeAni(background); + }; const div = document.createElement("div"); + div.onclick = (e) => e.stopImmediatePropagation(); + div.classList.add("centeritem", "profile"); - if(this.accent_color){ + if (this.accent_color) { div.style.setProperty( "--accent_color", - `#${this.accent_color.toString(16).padStart(6, "0")}` + `#${this.accent_color.toString(16).padStart(6, "0")}`, ); - }else{ + } else { div.style.setProperty("--accent_color", "transparent"); } - if(this.banner){ - const banner = document.createElement("img"); - let src: string; - if(!this.hypotheticalbanner){ - src = `${this.info.cdn}/avatars/${this.id.replace("#clone", "")}/${ - this.banner - }.png`; - }else{ - src = this.banner; - } - banner.src = src; - banner.classList.add("banner"); - div.append(banner); + const banner = this.getBanner(guild); + div.append(banner); + membres.then((member) => { + if (!member) return; + if (member.accent_color && member.accent_color !== 0) { + div.style.setProperty( + "--accent_color", + `#${member.accent_color.toString(16).padStart(6, "0")}`, + ); + } + }); + + const badgediv = document.createElement("div"); + badgediv.classList.add("badges"); + (async () => { + const badges = await this.getBadges(); + for (const badgejson of badges) { + const badge = document.createElement(badgejson.link ? "a" : "div"); + badge.classList.add("badge"); + let src: string; + if (URL.canParse(badgejson.icon)) { + src = badgejson.icon; + } else { + src = + this.info.cdn + + "/badge-icons/" + + badgejson.icon + + ".png" + + new CDNParams({expectedSize: 32}); + } + const img = createImg(src, undefined, badgediv); + + badge.append(img); + let hovertxt: string; + if (badgejson.translate) { + //@ts-ignore + hovertxt = I18n.badge[badgejson.description](); + } else { + hovertxt = badgejson.description; + } + const hover = new Hover(hovertxt); + hover.addEvent(badge); + if (badgejson.link && badge instanceof HTMLAnchorElement) { + badge.href = badgejson.link; + } + badgediv.append(badge); + } + })(); + + const pfp = this.buildstatuspfp(guild); + div.appendChild(pfp); + const userbody = document.createElement("div"); + userbody.classList.add("flexttb", "infosection"); + div.appendChild(userbody); + + const usernamehtml = document.createElement("h2"); + usernamehtml.textContent = this.name; + + userbody.appendChild(usernamehtml); + + if (this.bot) { + const username = document.createElement("span"); + username.classList.add("bot"); + username.textContent = this.webhook ? I18n.webhook() : I18n.bot(); + usernamehtml.appendChild(username); } - if(x !== -1){ + + userbody.appendChild(badgediv); + const discrimatorhtml = document.createElement("h3"); + discrimatorhtml.classList.add("tag"); + discrimatorhtml.textContent = `${this.username}#${this.discriminator}`; + userbody.appendChild(discrimatorhtml); + + const pronounshtml = document.createElement("p"); + pronounshtml.textContent = this.pronouns || ""; + pronounshtml.classList.add("pronouns"); + userbody.appendChild(pronounshtml); + + membres.then((member) => { + if (!member) return; + if (member.pronouns && member.pronouns !== "") { + pronounshtml.textContent = member.pronouns; + } + }); + + const rule = document.createElement("hr"); + userbody.appendChild(rule); + const float = new Float(""); + const buttons = float.options.addButtons("", {top: true, titles: false}); + { + const info = buttons.add(I18n.profile.userInfo()); + const infoDiv = document.createElement("div"); + infoDiv.classList.add("flexttb"); + infoDiv.append(I18n.profile.bio(), document.createElement("hr")); + const biohtml = this.bio.makeHTML(); + infoDiv.appendChild(biohtml); + + membres.then((member) => { + if (!member) return; + if (member.bio && member.bio !== "") { + //TODO make markdown take Guild + infoDiv.insertBefore(new MarkDown(member.bio, this.localuser).makeHTML(), biohtml); + biohtml.remove(); + } + }); + info.addHTMLArea(infoDiv); + + const roles = document.createElement("div"); + const joined = document.createElement("div"); + joined.textContent = I18n.profile.joined(new Date(this.getUnixTime()).toLocaleString()); + infoDiv.append(roles, document.createElement("hr"), joined); + + if (guild) { + membres.then((member) => { + if (!member) { + this.subName(usernamehtml); + return; + } + const p = document.createElement("p"); + p.textContent = I18n.profile.joinedMember( + member.guild.properties.name, + new Date(member.joined_at).toLocaleString(), + ); + joined.append(p); + + usernamehtml.textContent = member.name; + member.subName(usernamehtml); + if (this.bot) { + const username = document.createElement("span"); + username.classList.add("bot"); + username.textContent = this.webhook ? I18n.webhook() : I18n.bot(); + usernamehtml.appendChild(username); + } + + roles.classList.add("flexltr", "rolesbox"); + for (const role of member.roles) { + if (role.id === member.guild.id) continue; + const roleDiv = document.createElement("div"); + roleDiv.classList.add("rolediv"); + const color = document.createElement("div"); + roleDiv.append(color); + + color.style.setProperty("--role-color", role.getColorStyle(true)); + color.classList.add("colorrolediv"); + const span = document.createElement("span"); + roleDiv.append(span); + span.textContent = role.name; + roles.append(roleDiv); + } + }); + } + } + + (async () => { + const memb = await membres; + if (!memb) return; + const perms = buttons.add(I18n.profile.permInfo()); + const permDiv = document.createElement("div"); + permDiv.classList.add("permbox"); + const permsL = Permissions.info() + .filter((_) => memb.hasPermission(_.name, false)) + .map((_) => _.readableName); + for (const perm of permsL) { + const span = document.createElement("span"); + span.textContent = perm; + permDiv.append(span); + } + perms.addHTMLArea(permDiv); + })(); + + const fhtml = float.generateHTML(); + fhtml.style.overflow = "auto"; + userbody.append(fhtml); + + document.body.append(background); + background.append(div); + console.log(background); + (async () => { + const high = await this.highInfo(); + const mut = buttons.add(I18n.profile.mut()); + const mutDiv = document.createElement("div"); + + mutDiv.append( + ...high.mutual_guilds + .map((_) => [this.localuser.guildids.get(_.id), _.nick] as const) + .map(([guild, nick]) => { + if (!guild) return; + const icon = guild.generateGuildIcon(false); + + const box = document.createElement("div"); + box.classList.add("mutGuildBox", "flexltr"); + + const info = document.createElement("div"); + info.classList.add("flexttb"); + const gname = document.createElement("span"); + gname.textContent = guild.properties.name; + info.append(gname); + box.append(icon, info); + if (nick) info.append(nick); + return box; + }) + .filter((_) => _ !== undefined), + ); + mut.addHTMLArea(mutDiv); + + if (high.mutual_friends) { + const friends = buttons.add(I18n.profile.mutFriends()); + const div = document.createElement("div"); + div.classList.add("mutFriends"); + div.append( + ...high.mutual_friends + .map((_) => new User(_, this.localuser)) + .map((user) => { + const html = user.createWidget(this.localuser.lookingguild); + html.onclick = (e) => { + e.stopImmediatePropagation(); + e.preventDefault(); + user.fullProfile(guild); + removeAni(background); + }; + return html; + }), + ); + friends.addHTMLArea(div); + } + })(); + return background; + } + + async buildprofile( + x: number, + y: number, + guild: Guild | null | Member = null, + zIndex = -1, + ): Promise { + const membres = (async () => { + if (!guild) return; + let member: Member | undefined; + if (guild instanceof Guild) { + member = await Member.resolveMember(this, guild); + } else { + member = guild; + } + return member; + })(); + const div = document.createElement("div"); + if (zIndex !== -1) { + div.style.zIndex = zIndex + ""; + } + if (this.accent_color) { + div.style.setProperty( + "--accent_color", + `#${this.accent_color.toString(16).padStart(6, "0")}`, + ); + } else { + div.style.setProperty("--accent_color", "transparent"); + } + const banner = this.getBanner(guild); + div.append(banner); + membres.then((member) => { + if (!member) return; + if (member.accent_color && member.accent_color !== 0) { + div.style.setProperty( + "--accent_color", + `#${member.accent_color.toString(16).padStart(6, "0")}`, + ); + } + }); + + if (x !== -1) { div.style.left = `${x}px`; div.style.top = `${y}px`; div.classList.add("profile", "flexttb"); - }else{ + } else { this.setstatus("online"); div.classList.add("hypoprofile", "profile", "flexttb"); } const badgediv = document.createElement("div"); badgediv.classList.add("badges"); - (async ()=>{ - if(!this.badge_ids)return; - for(const id of this.badge_ids){ - const badgejson = await this.getBadge(id); - if(badgejson){ - const badge = document.createElement(badgejson.link ? "a" : "div"); - badge.classList.add("badge"); - const img = document.createElement("img"); - img.src = badgejson.icon; - badge.append(img); - const span = document.createElement("span"); - span.textContent = badgejson.description; - badge.append(span); - if(badge instanceof HTMLAnchorElement){ - badge.href = badgejson.link; - } - badgediv.append(badge); + (async () => { + const badges = await this.getBadges(); + for (const badgejson of badges) { + const badge = document.createElement(badgejson.link ? "a" : "div"); + badge.classList.add("badge"); + let src: string; + if (URL.canParse(badgejson.icon)) { + src = badgejson.icon; + } else { + src = + this.info.cdn + + "/badge-icons/" + + badgejson.icon + + ".png" + + new CDNParams({expectedSize: 32}); + } + const img = createImg(src, undefined, badgediv); + + badge.append(img); + let hovertxt: string; + if (badgejson.translate) { + //@ts-ignore + hovertxt = I18n.badge[badgejson.description](); + } else { + hovertxt = badgejson.description; + } + const hover = new Hover(hovertxt); + hover.addEvent(badge); + if (badgejson.link && badge instanceof HTMLAnchorElement) { + badge.href = badgejson.link; } + badgediv.append(badge); } })(); - const pfp = await this.buildstatuspfp(); + const pfp = this.buildstatuspfp(guild); + pfp.onclick = (e) => { + this.fullProfile(guild); + div.remove(); + e.stopImmediatePropagation(); + e.preventDefault(); + }; div.appendChild(pfp); const userbody = document.createElement("div"); - userbody.classList.add("flexttb","infosection"); + userbody.classList.add("flexttb", "infosection"); div.appendChild(userbody); const usernamehtml = document.createElement("h2"); usernamehtml.textContent = this.username; + userbody.appendChild(usernamehtml); + if (this.bot) { + const username = document.createElement("span"); + username.classList.add("bot"); + username.textContent = this.webhook ? I18n.webhook() : I18n.bot(); + usernamehtml.appendChild(username); + } userbody.appendChild(badgediv); const discrimatorhtml = document.createElement("h3"); discrimatorhtml.classList.add("tag"); @@ -505,30 +1517,61 @@ class User extends SnowFlake{ userbody.appendChild(discrimatorhtml); const pronounshtml = document.createElement("p"); - pronounshtml.textContent = this.pronouns; + pronounshtml.textContent = this.pronouns || ""; pronounshtml.classList.add("pronouns"); userbody.appendChild(pronounshtml); + membres.then((member) => { + if (!member) return; + if (member.pronouns && member.pronouns !== "") { + pronounshtml.textContent = member.pronouns; + } + }); + const rule = document.createElement("hr"); userbody.appendChild(rule); const biohtml = this.bio.makeHTML(); userbody.appendChild(biohtml); - if(guild){ - Member.resolveMember(this, guild).then(member=>{ - if(!member)return; - usernamehtml.textContent=member.name; + + membres.then((member) => { + if (!member) return; + if (member.bio && member.bio !== "") { + //TODO make markdown take Guild + userbody.insertBefore(new MarkDown(member.bio, this.localuser).makeHTML(), biohtml); + biohtml.remove(); + } + }); + + const send = document.createElement("input"); + if (!this.id.includes("#clone")) div.append(send); + send.placeholder = I18n.user.sendMessage(this.name); + send.onkeyup = (e) => { + if (e.key === "Enter") { + this.opendm(send.value); + div.remove(); + } + }; + + if (guild) { + membres.then((member) => { + if (!member) return; + send.placeholder = I18n.user.sendMessage(member.name); + usernamehtml.textContent = member.name; + if (this.bot) { + const username = document.createElement("span"); + username.classList.add("bot"); + username.textContent = this.webhook ? I18n.webhook() : I18n.bot(); + usernamehtml.appendChild(username); + } const roles = document.createElement("div"); - roles.classList.add("flexltr","rolesbox"); - for(const role of member.roles){ - if(role.id===member.guild.id) continue; + roles.classList.add("flexltr", "rolesbox"); + for (const role of member.roles) { + if (role.id === member.guild.id) continue; const roleDiv = document.createElement("div"); roleDiv.classList.add("rolediv"); const color = document.createElement("div"); roleDiv.append(color); - color.style.setProperty( - "--role-color", - `#${role.color.toString(16).padStart(6, "0")}` - ); + color.style.setProperty("--role-color", role.getColorStyle(true)); color.classList.add("colorrolediv"); const span = document.createElement("span"); roleDiv.append(span); @@ -538,21 +1581,74 @@ class User extends SnowFlake{ userbody.append(roles); }); } - if(x !== -1){ - Contextmenu.currentmenu = div; + + if (x !== -1) { + Contextmenu.declareMenu(div); document.body.appendChild(div); Contextmenu.keepOnScreen(div); } return div; } + getBanner(guild: Guild | null | Member): HTMLImageElement { + const banner = createImg(undefined); - profileclick(obj: HTMLElement, guild?: Guild): void{ - obj.onclick = (e: MouseEvent)=>{ - this.buildprofile(e.clientX, e.clientY, guild); + const bsrc = this.getBannerUrl(); + if (bsrc) { + banner.setSrcs(bsrc); + banner.classList.add("banner"); + } + + if (guild) { + if (guild instanceof Member) { + const bsrc = guild.getBannerUrl(); + if (bsrc) { + banner.setSrcs(bsrc); + banner.classList.add("banner"); + } + } else { + Member.resolveMember(this, guild).then((memb) => { + if (!memb) return; + const bsrc = memb.getBannerUrl(); + if (bsrc) { + banner.setSrcs(bsrc); + banner.classList.add("banner"); + } + }); + } + } + return banner; + } + getBannerUrl(): string | undefined { + if (this.banner) { + if (!this.hypotheticalbanner) { + return ( + `${this.info.cdn}/banners/${this.id.replace("#clone", "")}/${this.banner}.png` + + new CDNParams({expectedSize: 160}) + ); + } else { + return this.banner; + } + } else { + return undefined; + } + } + profileclick(obj: HTMLElement, guild?: Guild): void { + const getIndex = (elm: HTMLElement) => { + const index = getComputedStyle(elm).zIndex; + if (index === "auto") { + if (elm.parentElement) { + return getIndex(elm.parentElement); + } + } + return +index; + }; + obj.onclick = (e: MouseEvent) => { + const index = 1 + getIndex(obj); + this.buildprofile(e.clientX, e.clientY, guild, index); e.stopPropagation(); }; } } User.setUpContextMenu(); -export{ User }; +export {User}; diff --git a/src/webpage/utils/binaryUtils.ts b/src/webpage/utils/binaryUtils.ts new file mode 100644 index 00000000..66c8d04c --- /dev/null +++ b/src/webpage/utils/binaryUtils.ts @@ -0,0 +1,102 @@ +class BinRead { + private i = 0; + private view: DataView; + constructor(buffer: ArrayBuffer) { + this.view = new DataView(buffer, 0); + } + read16() { + const int = this.view.getUint16(this.i); + this.i += 2; + return int; + } + read8() { + const int = this.view.getUint8(this.i); + this.i += 1; + return int; + } + readString8() { + return this.readStringNo(this.read8()); + } + readString16() { + return this.readStringNo(this.read16()); + } + readFloat32() { + const float = this.view.getFloat32(this.i); + this.i += 4; + return float; + } + readStringNo(length: number) { + const array = new Uint8Array(length); + for (let i = 0; i < length; i++) { + array[i] = this.read8(); + } + //console.log(array); + return new TextDecoder("utf8").decode(array.buffer as ArrayBuffer); + } + readAsciiString8() { + return this.readAsciiStringNo(this.read8()); + } + readAsciiString16() { + return this.readAsciiStringNo(this.read16()); + } + readAsciiStringNo(length: number) { + let build = ""; + for (let i = 0; i < length; i++) { + build += String.fromCharCode(this.read8()); + } + //console.log(array); + return build; + } +} + +class BinWrite { + private view: DataView; + private buffer: ArrayBuffer; + private i = 0; + constructor(maxSize: number = 2 ** 26) { + this.buffer = new ArrayBuffer(maxSize); + this.view = new DataView(this.buffer, 0); + } + write32Float(numb: number) { + this.view.setFloat32(this.i, numb); + this.i += 4; + } + write16(numb: number) { + this.view.setUint16(this.i, numb); + this.i += 2; + } + write8(numb: number) { + this.view.setUint8(this.i, numb); + this.i += 1; + } + writeString8(str: string) { + const encode = new TextEncoder().encode(str); + this.write8(encode.length); + for (const thing of encode) { + this.write8(thing); + } + } + writeString16(str: string) { + const encode = new TextEncoder().encode(str); + this.write16(encode.length); + for (const thing of encode) { + this.write8(thing); + } + } + writeStringNo(str: string) { + const encode = new TextEncoder().encode(str); + for (const thing of encode) { + this.write8(thing); + } + } + getBuffer() { + const buf = new ArrayBuffer(this.i); + const ar1 = new Uint8Array(buf); + const ar2 = new Uint8Array(this.buffer); + for (let i in ar1) { + ar1[+i] = ar2[+i]; + } + return buf; + } +} +export {BinRead, BinWrite}; diff --git a/src/webpage/utils/cdnParams.ts b/src/webpage/utils/cdnParams.ts new file mode 100644 index 00000000..10eb3d06 --- /dev/null +++ b/src/webpage/utils/cdnParams.ts @@ -0,0 +1,27 @@ +export class CDNParams { + expectedSize: number; + keep_aspect_ratio: boolean; + animated: boolean; + constructor({ + expectedSize, + keep_aspect_ratio, + animated, + }: {expectedSize?: number; keep_aspect_ratio?: boolean; animated?: boolean} = {}) { + this.expectedSize = expectedSize ?? 300; + this.keep_aspect_ratio = keep_aspect_ratio ?? false; + this.animated = animated ?? true; + } + getSize() { + return this.expectedSize; + } + toString() { + return ( + "?" + + new URLSearchParams([ + ["size", this.getSize() + ""], + ["keep_aspect_ratio", this.keep_aspect_ratio + ""], + ["animated", this.animated + ""], + ]) + ); + } +} diff --git a/src/webpage/utils/cssMagic.ts b/src/webpage/utils/cssMagic.ts new file mode 100644 index 00000000..512d5869 --- /dev/null +++ b/src/webpage/utils/cssMagic.ts @@ -0,0 +1,114 @@ +export function unnest(str: string, check = true) { + if (check && CSS.supports("selector(&)")) { + //no need to unnest if it's supported lol + return str; + } + let specificity: string[] = []; + let buildStr = str.split("\n"); + const rules: string[] = []; + let last = false; + let prop = false; + let conbuild = ""; + let ani = 0; + function append(str: string, line = rules.length - 1) { + let build = specificity[line] || ""; + build += str; + specificity[line] = build; + } + for (const uline of buildStr) { + const line = uline + .trim() + .split("//")[0] + .replace(/\/\*.*\*\//gm, "") + .trim(); + if (ani) { + conbuild += "\n" + uline; + if (line.includes("{")) { + ani++; + } + if (line.includes("}")) { + ani--; + } + if (!ani) { + append(conbuild, 1); + console.log(conbuild); + conbuild = ""; + } + + continue; + } + if (prop) { + conbuild += line + "\n"; + + if (line.endsWith(";")) { + append(conbuild); + prop = false; + conbuild = ""; + } + continue; + } + if (line.endsWith(",")) { + conbuild += line; + console.log(conbuild, line); + } else if (line.endsWith("{") && line.startsWith("@keyframes")) { + conbuild = uline; + ani++; + } else if (line.endsWith("{")) { + let rule = conbuild + line.slice(0, line.length - 1).trim(); + conbuild = ""; + if (rule.startsWith("&")) { + rule = rule.slice(1, line.length); + } else { + rule = " " + rule; + } + if (last) append("}\n"); + rules.push(rule); + + last = true; + append(rules.join("") + "{\n"); + } else if (line === "}") { + if (last) append("}\n"); + rules.pop(); + + if (rules.length) { + append(rules.join("") + "{\n"); + } else { + last = false; + } + } else if (line.includes(":")) { + if (line.endsWith(";")) { + append(line); + } else { + console.warn(line); + prop = true; + conbuild = ""; + conbuild += line + "\n"; + } + } + } + if (last) alert("huh"); + console.log(specificity); + return specificity.join("\n"); +} +export function fix() { + if (!CSS.supports("selector(&)")) { + fetch("/style.css") + .then((_) => _.text()) + .then((txt) => { + const link = Array.from(document.getElementsByTagName("link")).find((_) => + _.href.endsWith("/style.css"), + ); + if (!link) { + alert("link not found"); + return; + } + const css = unnest(txt); + console.log(css); + + const style = document.createElement("style"); + style.textContent = css; + link.after(style); + link.remove(); + }); + } +} diff --git a/src/webpage/utils/dirrWorker.ts b/src/webpage/utils/dirrWorker.ts new file mode 100644 index 00000000..83d31ae4 --- /dev/null +++ b/src/webpage/utils/dirrWorker.ts @@ -0,0 +1,13 @@ +//This is *only* for webkit, and it really sucks +//If webkit starts supporting the more sain way, let me know so I can remove this after a year or two of them supporting it +onmessage = async (e) => { + const [file, content, rand] = e.data as [FileSystemFileHandle, ArrayBuffer, number]; + try { + const handle = await file.createSyncAccessHandle(); + handle.write(content); + handle.close(); + postMessage([rand, true]); + } catch { + postMessage([rand, false]); + } +}; diff --git a/src/webpage/utils/fancySelect.ts b/src/webpage/utils/fancySelect.ts new file mode 100644 index 00000000..b234e86e --- /dev/null +++ b/src/webpage/utils/fancySelect.ts @@ -0,0 +1,137 @@ +import {removeAni} from "./utils"; + +interface option { + value: string; + label: string; + description?: string; + icon?: HTMLElement | (() => HTMLElement); + default: boolean; +} +export class FancySelect { + options: option[]; + min: number; + max: number; + constructor(options: option[], {min, max} = {min: 1, max: 1}) { + this.options = options; + this.min = min; + this.max = max; + } + getHTML() { + const div = document.createElement("div"); + div.classList.add("fancySelect"); + const input = document.createElement("input"); + input.type = "text"; + div.append(input); + + const options = document.createElement("div"); + options.classList.add("fancyOptions"); + const genArgs = () => { + Array.from(div.getElementsByClassName("selected")).forEach((_) => _.remove()); + for (const option of this.options.toReversed()) { + if (!option.default) continue; + const span = document.createElement("span"); + span.classList.add("selected"); + span.textContent = option.label; + + const x = document.createElement("span"); + x.classList.add("svg-x"); + span.prepend(x); + x.onmousedown = (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + input.focus(); + span.remove(); + option.default = false; + genList(input.value); + }; + + div.prepend(span); + } + }; + const genList = (filter: string) => { + options.innerHTML = ""; + options.classList.remove("removeElm"); + for (const option of this.options) { + if ( + !option.label.includes(filter) && + !option.description?.includes(filter) && + !option.value.includes(filter) + ) { + continue; + } + const div = document.createElement("div"); + div.classList.add("flexltr"); + + const check = document.createElement("input"); + check.type = "checkbox"; + check.checked = option.default; + check.onclick = (e) => e.preventDefault(); + + const label = document.createElement("div"); + label.classList.add("flexttb"); + label.append(option.label); + if (option.description) { + const p = document.createElement("p"); + p.textContent = option.description; + label.append(p); + } + if (option.icon) { + label.append(option instanceof Function ? option() : option); + } + div.append(label); + if (this.max !== 1) div.append(check); + + div.onmousedown = (e) => { + if (this.max === 1) { + input.value = option.label; + removeAni(options); + } else { + e.preventDefault(); + e.stopImmediatePropagation(); + if (!check.checked) { + let sum = 0; + for (const thing of this.options) { + sum += +thing.default; + } + if (sum === this.max) return; + } + check.checked = !check.checked; + option.default = check.checked; + genArgs(); + } + + input.focus(); + }; + + options.append(div); + } + }; + genArgs(); + genList(input.value); + input.oninput = () => { + genList(input.value); + }; + input.onfocus = () => { + options.classList.remove("removeElm"); + div.append(options); + }; + input.onblur = async () => { + removeAni(options); + }; + input.onkeypress = (e) => { + if (e.key === "Enter") { + this.figureSubmit(); + } + }; + + return div; + } + onSubmit: (values: string[]) => unknown = () => {}; + private figureSubmit() { + const stuff = this.options.filter((_) => _.default); + if (stuff.length < this.min || stuff.length > this.max) { + return; //TODO error or something? + } + this.onSubmit(stuff.map((_) => _.value)); + } +} diff --git a/src/webpage/utils/netUtils.ts b/src/webpage/utils/netUtils.ts new file mode 100644 index 00000000..c6b05da8 --- /dev/null +++ b/src/webpage/utils/netUtils.ts @@ -0,0 +1,8 @@ +export function trimTrailingSlashes(uri: string) { + if (!uri) return uri; + return uri.replace(/\/+$/, ""); +} + +export function isLoopback(str: string) { + return str.includes("localhost") || str.includes("127.0.0.1"); +} diff --git a/src/webpage/utils/pollyfills.ts b/src/webpage/utils/pollyfills.ts new file mode 100644 index 00000000..38434ceb --- /dev/null +++ b/src/webpage/utils/pollyfills.ts @@ -0,0 +1,65 @@ +//So... there are polyfills for letting some older browsers to run Fermi, but it's not going to be anywhere near perfect with the CSS +Set.prototype.intersection ??= function (set: Set) { + const newSet = new Set(); + for (const elm of this) { + if (set.has(elm)) { + newSet.add(elm); + } + } + return newSet; +}; +Set.prototype.difference ??= function (set: Set) { + const newSet = new Set(); + for (const elm of this) { + if (!set.has(elm)) { + newSet.add(elm); + } + } + return newSet; +}; +Set.prototype.symmetricDifference ??= function (set: Set) { + const newSet = new Set(); + for (const elm of this) { + if (!set.has(elm)) { + newSet.add(elm); + } + } + for (const elm of set) { + if (!this.has(elm)) { + newSet.add(elm); + } + } + return newSet; +}; +Set.prototype.isDisjointFrom ??= function (set: Set) { + return this.intersection(set).size === 0; +}; +Set.prototype.union ??= function (set: Set) { + return new Set([...this, ...set]); +}; + +function defineItter(itter: typeof Iterator) { + itter.prototype.map ??= function* map( + callbackfn: (value: unknown, index: number) => U, + ): IteratorObject { + let i = 0; + for (const thing of this) { + yield callbackfn(thing, i); + i++; + } + }; +} +if ("Iterator" in globalThis) { + defineItter(globalThis.Iterator); +} else { + defineItter("".matchAll(/6/g).constructor as typeof Iterator); +} + +ReadableStream.prototype[Symbol.asyncIterator] ??= async function* () { + const reader = this.getReader(); + while (true) { + const {value, done} = await reader.read(); + yield value; + if (done) return undefined; + } +}; diff --git a/src/webpage/utils/progessiveLoad.ts b/src/webpage/utils/progessiveLoad.ts new file mode 100644 index 00000000..134769bd --- /dev/null +++ b/src/webpage/utils/progessiveLoad.ts @@ -0,0 +1,326 @@ +export class ProgressiveArray { + read?: ReadableStreamDefaultReader; + controller: AbortController; + cbuff?: Uint8Array = new Uint8Array(0); + index = 0; + sizeLeft = 0; + ready: Promise; + constructor(url: string, req: RequestInit = {}) { + this.controller = new AbortController(); + this.ready = fetch(url, { + ...req, + signal: this.controller.signal, + }).then(async (f) => { + if (!f.ok || !f.body) { + throw new Error("request not ok"); + } + const read = f.body.getReader(); + this.cbuff = (await read.read()).value; + this.read = read; + }); + } + async next() { + return (await this.get8BitArray(1))[0]; + } + async get8BitArray(size: number) { + if (!this.read) throw new Error("not ready to read"); + this.sizeLeft -= size; + const arr = new Uint8Array(size); + let arri = 0; + while (size > 0) { + if (!this.cbuff) throw Error("ran out of file to read"); + let itter = Math.min(size, this.cbuff.length - this.index); + size -= itter; + for (let i = 0; i < itter; i++, arri++, this.index++) { + arr[arri] = this.cbuff[this.index]; + } + + if (size !== 0) { + this.cbuff = (await this.read.read()).value; + this.index = 0; + } + } + return arr; + } + decoder = new TextDecoder(); + backChar?: string; + chars = ""; + curchar = 0; + async getChar() { + if (this.backChar) { + const temp = this.backChar; + delete this.backChar; + return temp; + } + let char: string; + if ((char = this.chars[this.curchar])) { + this.curchar++; + return char; + } + let chars = ""; + while (!chars) { + const buflen = (this.cbuff?.length || 0) - this.index; + chars = this.decoder.decode((await this.get8BitArray(buflen <= 0 ? 1 : buflen)).buffer, { + stream: true, + }); + } + this.chars = chars; + this.curchar = 1; + return chars[0]; + } + putBackChar(char: string) { + this.backChar = char; + } + close() { + try { + this.controller.abort(); + } catch { + //Ignore error when trying to close after it was closed + } + } +} + +async function getNextNonWhiteSpace(prog: ProgressiveArray) { + let char = " "; + const whiteSpace = new Set("\n\t \r"); + while (whiteSpace.has(char)) { + char = await prog.getChar(); + } + return char; +} +async function identifyType(prog: ProgressiveArray) { + let char = await getNextNonWhiteSpace(prog); + switch (char) { + case "-": + case "0": + case "1": + case "2": + case "3": + case "4": + case "5": + case "6": + case "7": + case "8": + case "9": { + const validNumber = new Set("0123456789eE.+-"); + let build = ""; + do { + build += char; + char = await prog.getChar(); + } while (validNumber.has(char)); + prog.putBackChar(char); + return Number(build); + } + case '"': + let build = ""; + do { + build += char; + if (char == "\\") { + char = await prog.getChar(); + build += char; + } + char = await prog.getChar(); + } while (char !== '"'); + build += char; + return JSON.parse(build) as string; + case "t": + case "f": + case "n": { + let build = char; + while (build.match(/(^tr?u?$)|(^fa?l?s?$)|(^nu?l?$)/)) { + char = await prog.getChar(); + build += char; + } + return JSON.parse(build) as boolean | null; + } + case "[": + return await ArrayProgressive.make(prog); + case "{": + return await ObjectProgressive.make(prog); + default: + throw new Error("bad JSON"); + } +} +class ArrayProgressive> { + ondone = async () => {}; + prog: ProgressiveArray; + done = false; + private constructor(prog: ProgressiveArray) { + this.prog = prog; + } + static async make(prog: ProgressiveArray) { + const o = new ArrayProgressive(prog); + await o.check(); + return o; + } + async check() { + const lastChar = await getNextNonWhiteSpace(this.prog); + if (lastChar === "]") { + this.done = true; + await this.ondone(); + return; + } else { + this.prog.putBackChar(lastChar); + } + } + awaiting = new Promise((_) => _()); + + async doChecks(): Promise<() => void> { + let res1: () => void; + let cur = new Promise((res) => { + res1 = res; + }); + [cur, this.awaiting] = [this.awaiting, cur]; + await cur; + + return () => res1(); + } + async getNext(): Promise> { + const checks = await this.doChecks(); + if (this.done) throw new Error("no more array"); + const ret = (await identifyType(this.prog)) as Progressive; + const check = async () => { + const lastChar = await getNextNonWhiteSpace(this.prog); + if (lastChar === "]") { + this.done = true; + await this.ondone(); + } else if (lastChar !== ",") throw Error("Bad JSON Object:" + lastChar); + checks(); + }; + if ((ret instanceof ArrayProgressive || ret instanceof ObjectProgressive) && !ret.done) { + ret.ondone = check; + } else { + await check(); + } + + return ret; + } + async *[Symbol.asyncIterator]() { + while (!this.done) { + yield await this.getNext(); + } + } + /** + * this only gets what's left, not everything + */ + async getWhole(): Promise { + const arr: T[] = []; + while (!this.done) { + let t = await this.getNext(); + if (t instanceof ArrayProgressive) { + t = await t.getWhole(); + } + if (t instanceof ObjectProgressive) { + t = await t.getWhole(); + } + arr.push(t as T); + } + + return arr as X; + } +} +class ObjectProgressive { + ondone = async () => {}; + prog: ProgressiveArray; + done = false; + private constructor(prog: ProgressiveArray) { + this.prog = prog; + } + static async make(prog: ProgressiveArray) { + const o = new ObjectProgressive(prog); + await o.check(); + return o; + } + async check() { + const lastChar = await getNextNonWhiteSpace(this.prog); + if (lastChar === "}") { + this.done = true; + await this.ondone(); + return; + } else { + this.prog.putBackChar(lastChar); + } + } + awaiting = new Promise((_) => _()); + + async doChecks(): Promise<() => void> { + let res1: () => void; + let cur = new Promise((res) => { + res1 = res; + }); + [cur, this.awaiting] = [this.awaiting, cur]; + await cur; + return () => res1(); + } + async getNextPair(): Promise<{[K in keyof X]: {key: K; value: Progressive}}[keyof X]> { + const checks = await this.doChecks(); + if (this.done) throw new Error("no more object"); + const key = (await identifyType(this.prog)) as unknown; + if (typeof key !== "string") { + throw Error("Bad key:" + key); + } + const nextChar = await getNextNonWhiteSpace(this.prog); + if (nextChar !== ":") throw Error("Bad JSON"); + const value = (await identifyType(this.prog)) as unknown; + const check = async () => { + const lastChar = await getNextNonWhiteSpace(this.prog); + if (lastChar === "}") { + this.done = true; + await this.ondone(); + } else if (lastChar !== ",") throw Error("Bad JSON Object:" + lastChar); + checks(); + }; + if ((value instanceof ArrayProgressive || value instanceof ObjectProgressive) && !value.done) { + value.ondone = check; + } else { + await check(); + } + return {key, value} as any; + } + /** + * this only gets what's left, not everything + */ + async getWhole(): Promise { + const obj: Partial = {}; + while (!this.done) { + let {key, value} = await this.getNextPair(); + if (value instanceof ArrayProgressive) { + value = await value.getWhole(); + } + if (value instanceof ObjectProgressive) { + value = await value.getWhole(); + } + obj[key] = value as any; + } + return obj as X; + } +} +Object.entries; +type Progressive = + T extends Array + ? ArrayProgressive ? X : never, T> + : T extends string | boolean | null | number + ? T + : T extends Object + ? ObjectProgressive + : T; +/* + * this will progressively load a JSON object, you must read everything you get to get the next thing in line. + */ +export async function ProgessiveDecodeJSON( + url: string, + req: RequestInit = {}, +): Promise> { + const prog = new ProgressiveArray(url, req); + await prog.ready; + return identifyType(prog) as Promise>; +} +/* +const test = [1, 2, 3, 4, 5, 6]; +const blob = new Blob([JSON.stringify(test)]); +ProgessiveDecodeJSON("https://api.github.com/repos/spacebarchat/server/git/refs") + .then(async (obj) => { + console.log(await obj.getWhole()); //returns the ping object + }) + .then(console.warn); +//*/ diff --git a/src/webpage/utils/promiseLock.ts b/src/webpage/utils/promiseLock.ts new file mode 100644 index 00000000..1cf6e750 --- /dev/null +++ b/src/webpage/utils/promiseLock.ts @@ -0,0 +1,10 @@ +export class PromiseLock { + lastLock = Promise.resolve(); + async acquireLock() { + const {promise, resolve: res} = Promise.withResolvers(); + const last = this.lastLock; + this.lastLock = promise; + await last; + return res; + } +} diff --git a/src/webpage/utils/serviceType.ts b/src/webpage/utils/serviceType.ts new file mode 100644 index 00000000..e49480bf --- /dev/null +++ b/src/webpage/utils/serviceType.ts @@ -0,0 +1,65 @@ +export type messageTo = + | { + code: "ping"; + } + | { + code: "close"; + } + | { + code: "replace"; + } + | { + code: "CheckUpdate"; + } + | { + code: "clearCdnCache"; + } + | { + code: "isValid"; + url: string; + } + | { + code: "isDev"; + dev: boolean; + } + | { + code: "apiUrls"; + hosts?: string[]; + } + | { + code: "canRefresh"; + host: string; + } + | { + code: "refreshedUrl"; + oldurl: string; + url: string; + }; +export type messageFrom = + | { + code: "pong"; + count: number; + } + | { + code: "close"; + } + | { + code: "closing"; + } + | { + code: "updates"; + updates: boolean; + } + | { + code: "isValid"; + url: string; + valid: boolean; + } + | { + code: "trace"; + trace: string[]; + } + | { + code: "refreshURL"; + url: string; + }; diff --git a/src/webpage/utils/storage/devSettings.ts b/src/webpage/utils/storage/devSettings.ts new file mode 100644 index 00000000..8a0c62f6 --- /dev/null +++ b/src/webpage/utils/storage/devSettings.ts @@ -0,0 +1,75 @@ +export class DeveloperSettings { + gatewayLogging: boolean = false; + gatewayCompression: boolean = true; + showTraces: boolean = false; + interceptApiTraces: boolean = false; + cacheSourceMaps: boolean = false; + logBannedFields: boolean = false; + reportSystem = false; + + constructor(data: Partial = {}) { + Object.assign(this, data); + } +} + +export function getDeveloperSettings(): DeveloperSettings { + return new DeveloperSettings(JSON.parse(localStorage.getItem("developerSettings") || "{}")); +} + +export function setDeveloperSettings(settings: DeveloperSettings): void { + localStorage.setItem("developerSettings", JSON.stringify(settings)); +} + +//region Migration from untyped storage +async function migrateOldDeveloperSettings(): Promise { + const devSettings = getDeveloperSettings(); + let mod = false; + + const oldGatewayLogging = localStorage.getItem("logGateway"); + if (oldGatewayLogging !== null) { + devSettings.gatewayLogging = oldGatewayLogging === "true"; + localStorage.removeItem("logGateway"); + mod = true; + } + + const oldGatewayCompression = localStorage.getItem("gateWayComp"); + if (oldGatewayCompression !== null) { + devSettings.gatewayCompression = oldGatewayCompression === "true"; + localStorage.removeItem("gateWayComp"); + mod = true; + } + + const oldShowTraces = localStorage.getItem("traces"); + if (oldShowTraces !== null) { + devSettings.showTraces = oldShowTraces === "true"; + localStorage.removeItem("traces"); + mod = true; + } + + const oldInterceptApiTraces = localStorage.getItem("capTrace"); + if (oldInterceptApiTraces !== null) { + devSettings.interceptApiTraces = oldInterceptApiTraces === "true"; + localStorage.removeItem("capTrace"); + mod = true; + } + + const oldCacheSourceMaps = localStorage.getItem("isDev"); + if (oldCacheSourceMaps !== null) { + devSettings.cacheSourceMaps = oldCacheSourceMaps === "true"; + localStorage.removeItem("isDev"); + mod = true; + } + + const oldLogBannedFields = localStorage.getItem("logbad"); + if (oldLogBannedFields !== null) { + devSettings.logBannedFields = oldLogBannedFields === "true"; + localStorage.removeItem("logbad"); + mod = true; + } + + if (mod) { + setDeveloperSettings(devSettings); + } +} +await migrateOldDeveloperSettings(); +//endregion diff --git a/src/webpage/utils/storage/localSettings.ts b/src/webpage/utils/storage/localSettings.ts new file mode 100644 index 00000000..b2bd6b6c --- /dev/null +++ b/src/webpage/utils/storage/localSettings.ts @@ -0,0 +1,52 @@ +export const enum ServiceWorkerMode { + // Skips registering the service worker completely + Unregistered = "unregistered", + // Registers the service worker but does not activate it + Disabled = "disabled", + // Ensures client files are cached and used when offline + OfflineOnly = "offlineOnly", + // Cache everything and use cached files when possible + Enabled = "enabled", +} + +export const ServiceWorkerModeValues = [ + ServiceWorkerMode.Unregistered, + ServiceWorkerMode.Disabled, + ServiceWorkerMode.OfflineOnly, + ServiceWorkerMode.Enabled, +]; + +export class LocalSettings { + serviceWorkerMode: ServiceWorkerMode = ServiceWorkerMode.Unregistered; + constructor(init?: Partial) { + Object.assign(this, init); + } +} + +export function getLocalSettings(): LocalSettings { + return new LocalSettings(JSON.parse(localStorage.getItem("localSettings") || "{}")); +} + +export function setLocalSettings(settings: LocalSettings): void { + localStorage.setItem("localSettings", JSON.stringify(settings)); +} + +//region Migration from untyped storage +function migrateOldSettings() { + const settings = getLocalSettings(); + let mod = false; + + const oldSWMode = localStorage.getItem("SWMode"); + if (oldSWMode !== null) { + settings.serviceWorkerMode = oldSWMode as ServiceWorkerMode; + localStorage.removeItem("SWMode"); + mod = true; + } + + if (mod) { + localStorage.setItem("localSettings", JSON.stringify(settings)); + } +} + +migrateOldSettings(); +//endregion diff --git a/src/webpage/utils/storage/sessionStore.ts b/src/webpage/utils/storage/sessionStore.ts new file mode 100644 index 00000000..954605b8 --- /dev/null +++ b/src/webpage/utils/storage/sessionStore.ts @@ -0,0 +1,24 @@ +export type UserAuth = { + serverName: string; + token: string; + id: string; + cachedUsername: string; + cachedAvatarUrl: string; + // @deprecated + cachedEmail?: string; + sessionId: string; +}; +/* +export class SessionStore { + currentSession: string; + sessions: Map = {}; + + constructor(init?: Partial) { + Object.assign(this, init); + } + + getCurrentSession(): UserAuth | null { + return this.sessions.get(this.currentSession) || null; + } +} +*/ diff --git a/src/webpage/utils/storage/userPreferences.ts b/src/webpage/utils/storage/userPreferences.ts new file mode 100644 index 00000000..d017300b --- /dev/null +++ b/src/webpage/utils/storage/userPreferences.ts @@ -0,0 +1,105 @@ +// Async in order to account for maybe some day Spacebar supporting account data... +export const enum AnimateTristateValue { + Always = "always", + OnlyOnHover = "hover", + Never = "never", +} +export const AnimateTristateValues = [ + AnimateTristateValue.Always, + AnimateTristateValue.OnlyOnHover, + AnimateTristateValue.Never, +]; + +export const enum ThemeOption { + Dark = "Dark", + White = "WHITE", + Light = "Light", + DarkAccent = "Dark-Accent", +} +export const ThemeOptionValues = [ + ThemeOption.Dark, + ThemeOption.White, + ThemeOption.Light, + ThemeOption.DarkAccent, +]; + +export class UserPreferences { + showBlogUpdates?: boolean; + locale: string = navigator.language || "en"; + + // render settings + animateIcons: AnimateTristateValue = AnimateTristateValue.OnlyOnHover; + animateGifs: AnimateTristateValue = AnimateTristateValue.OnlyOnHover; + renderJoinAvatars: boolean = true; + theme: ThemeOption = ThemeOption.Dark; + accentColor: string = "#5865F2"; + emojiFont?: string; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} + +export async function getPreferences(): Promise { + return new UserPreferences(JSON.parse(localStorage.getItem("userPreferences") || "{}")); +} + +export async function setPreferences(prefs: UserPreferences): Promise { + localStorage.setItem("userPreferences", JSON.stringify(prefs)); +} + +//region Migration from untyped storage +async function migrateOldPreferences(): Promise { + const prefs = await getPreferences(); + const oldBlogUpdates = localStorage.getItem("blogUpdates"); + let mod = false; + + if (oldBlogUpdates !== null) { + prefs.showBlogUpdates = oldBlogUpdates === "Yes"; + localStorage.removeItem("blogUpdates"); + mod = true; + } + + const oldAnimateGifs = localStorage.getItem("gifSetting"); + if (oldAnimateGifs !== null) { + prefs.animateGifs = oldAnimateGifs as AnimateTristateValue; + localStorage.removeItem("gifSetting"); + mod = true; + } + + const oldAnimateIcons = localStorage.getItem("iconSetting"); + if (oldAnimateIcons !== null) { + prefs.animateIcons = oldAnimateIcons as AnimateTristateValue; + localStorage.removeItem("iconSetting"); + mod = true; + } + + const oldTheme = localStorage.getItem("theme"); + if (oldTheme !== null) { + prefs.theme = oldTheme as ThemeOption; + localStorage.removeItem("theme"); + mod = true; + } + + const oldLocale = localStorage.getItem("locale"); + if (oldLocale !== null) { + prefs.locale = oldLocale; + localStorage.removeItem("locale"); + mod = true; + } + + const oldEmojiFont = localStorage.getItem("emoji-font"); + if (oldEmojiFont !== null) { + prefs.emojiFont = oldEmojiFont; + localStorage.removeItem("emoji-font"); + mod = true; + } + + if (mod) { + // TODO: proper saving and versioning and crap... + localStorage.setItem("userPreferences", JSON.stringify(prefs)); + } +} + +await migrateOldPreferences(); +//endregion diff --git a/src/webpage/utils/switcher.ts b/src/webpage/utils/switcher.ts new file mode 100644 index 00000000..679c7dac --- /dev/null +++ b/src/webpage/utils/switcher.ts @@ -0,0 +1,119 @@ +import {Contextmenu} from "../contextmenu"; +import {I18n} from "../i18n.js"; +import {Localuser} from "../localuser.js"; +import {createImg, Specialuser} from "./utils.js"; +await I18n.done; +const menu = new Contextmenu(""); +menu.addButton( + "Logout of account", + async function () { + await this.logout(); + }, + {color: "red"}, +); +export class AccountSwitcher { + filter: (spec: Specialuser) => boolean; + canCreate: boolean; + canLogOut: boolean; + canHide: boolean; + createOpt: boolean; + loginText: () => string; + loginurl: string; + registerurl: string; + constructor( + filter: AccountSwitcher["filter"] = () => true, + { + canCreate = true, + createOpt = false, + canLogOut = true, + canHide = true, + loginurl = "/login", + registerurl = "/register", + loginText = () => I18n.switchAccounts(), + } = {}, + ) { + this.filter = filter; + this.canCreate = canCreate; + this.canLogOut = canLogOut; + this.canHide = canHide; + this.loginText = loginText; + this.createOpt = createOpt; + this.loginurl = loginurl; + this.registerurl = registerurl; + } + async show(): Promise { + const table = document.createElement("div"); + table.classList.add("flexttb", "accountSwitcher"); + return new Promise((res) => { + for (const user of Object.values(Localuser.users.users)) { + const specialUser = user as Specialuser; + if (!this.filter(specialUser)) continue; + const userInfo = document.createElement("div"); + + userInfo.classList.add("flexltr", "switchtable"); + + const pfp = createImg(specialUser.pfpsrc); + pfp.classList.add("pfp"); + userInfo.append(pfp); + + const userDiv = document.createElement("div"); + userDiv.classList.add("userinfo"); + userDiv.textContent = specialUser.username; + userDiv.append(document.createElement("br")); + + const span = document.createElement("span"); + span.textContent = specialUser.serverurls.wellknown + .replace("https://", "") + .replace("http://", ""); + span.classList.add("serverURL"); + userDiv.append(span); + + userInfo.append(userDiv); + table.append(userInfo); + userInfo.addEventListener("contextmenu", (e) => { + if (this.canLogOut) { + e.stopImmediatePropagation(); + e.preventDefault(); + console.log(e, e.clientX, e.clientY); + menu.makemenu(e.clientX, e.clientY, specialUser, undefined, table); + } + return; + }); + userInfo.addEventListener("click", (e) => { + if (e.button === 2) { + if (this.canLogOut) { + e.stopImmediatePropagation(); + e.preventDefault(); + menu.makemenu(e.clientX, e.clientY, specialUser, undefined, table); + } + return; + } + res(specialUser); + userInfo.remove(); + }); + } + if (this.canCreate) { + const switchAccountDiv = document.createElement("div"); + switchAccountDiv.classList.add("switchtable"); + switchAccountDiv.textContent = this.loginText(); + switchAccountDiv.addEventListener("click", () => { + window.location.href = this.loginurl; + }); + table.append(switchAccountDiv); + } + if (this.createOpt) { + const switchAccountDiv = document.createElement("div"); + switchAccountDiv.classList.add("switchtable"); + switchAccountDiv.textContent = I18n.createAccount(); + switchAccountDiv.addEventListener("click", () => { + window.location.href = this.registerurl; + }); + table.append(switchAccountDiv); + } + if (this.canHide) { + Contextmenu.declareMenu(table); + } + document.body.append(table); + }); + } +} diff --git a/src/webpage/utils/utils.ts b/src/webpage/utils/utils.ts new file mode 100644 index 00000000..84d62e34 --- /dev/null +++ b/src/webpage/utils/utils.ts @@ -0,0 +1,1081 @@ +import {I18n} from "../i18n.js"; +import {MarkDown} from "../markdown.js"; +import {Dialog} from "../settings.js"; +import {fix} from "./cssMagic.js"; +import {messageFrom, messageTo} from "./serviceType.js"; +import {isLoopback, trimTrailingSlashes} from "./netUtils"; +import {getLocalSettings, ServiceWorkerMode, setLocalSettings} from "./storage/localSettings"; +import {getPreferences} from "./storage/userPreferences"; +import {getDeveloperSettings} from "./storage/devSettings"; + +fix(); +const apiDoms = new Set(); +let instances: + | { + name: string; + description?: string; + descriptionLong?: string; + image?: string; + url?: string; + display?: boolean; + online?: boolean; + uptime: {alltime: number; daytime: number; weektime: number}; + urls: { + wellknown: string; + api: string; + cdn: string; + gateway: string; + login?: string; + }; + }[] + | null = null; +await setTheme(); +export async function setTheme(theme?: string) { + const prefs = await getPreferences(); + document.body.className = (theme || prefs.theme) + "-theme"; + console.log(theme); +} +export function getBulkUsers() { + const json = getBulkInfo(); + apiDoms.clear(); + for (const thing in json.users) { + try { + const user = (json.users[thing] = new Specialuser(json.users[thing])); + apiDoms.add(new URL(user.serverurls.api).host); + } catch { + delete json.users[thing]; + } + } + if (getDeveloperSettings().interceptApiTraces) { + SW.postMessage({ + code: "apiUrls", + hosts: [...apiDoms], + }); + } else { + SW.postMessage({ + code: "apiUrls", + hosts: undefined, + }); + } + return json; +} +export function getBulkInfo() { + return JSON.parse(localStorage.getItem("userinfos") as string); +} +export function setDefaults() { + let userinfos = getBulkInfo(); + if (!userinfos) { + localStorage.setItem( + "userinfos", + JSON.stringify({ + currentuser: null, + users: {}, + preferences: { + theme: "Dark", + notifications: false, + notisound: "three", + }, + }), + ); + userinfos = getBulkInfo(); + } + if (userinfos.users === undefined) { + userinfos.users = {}; + } + if (userinfos.accent_color === undefined) { + userinfos.accent_color = "#3096f7"; + } + + getPreferences().then((perfs) => + document.documentElement.style.setProperty("--accent-color", perfs.accentColor), + ); + if (userinfos.preferences === undefined) { + userinfos.preferences = { + theme: "Dark", + notifications: false, + notisound: "three", + }; + } + if (userinfos.preferences && userinfos.preferences.notisound === undefined) { + console.warn("uhoh"); + userinfos.preferences.notisound = "three"; + } + localStorage.setItem("userinfos", JSON.stringify(userinfos)); +} +setDefaults(); +export class Specialuser { + serverurls: InstanceUrls; + email: string; + token: string; + loggedin; + json; + constructor(json: any) { + if (json instanceof Specialuser) { + console.error("specialuser can't construct from another specialuser"); + } + if (!json.serverurls) throw new Error("nope"); + this.serverurls = json.serverurls; + let apistring = new URL(json.serverurls.api).toString(); + apistring = apistring.replace(/\/(v\d+\/?)?$/, "") + "/v9"; + this.serverurls.api = apistring; + this.serverurls.cdn = new URL(json.serverurls.cdn).toString().replace(/\/$/, ""); + this.serverurls.gateway = new URL(json.serverurls.gateway).toString().replace(/\/$/, ""); + this.serverurls.wellknown = new URL(json.serverurls.wellknown).toString().replace(/\/$/, ""); + this.email = json.email; + this.token = json.token; + this.loggedin = json.loggedin; + this.json = json; + this.json.localuserStore ??= {}; + if (!this.serverurls || !this.email || !this.token) { + console.error("There are fundamentally missing pieces of info missing from this user"); + } + } + async logout() { + for (let i = 0; i < 3; i++) { + try { + const ok = ( + await fetch(this.serverurls.api + "/auth/logout", { + method: "POST", + headers: {Authorization: this.token}, + }) + ).ok; + if (ok) break; + } catch {} + if (i == 2) { + const d = new Dialog(""); + if ( + await new Promise((res) => { + const buttons = d.options.addOptions(I18n.logout.error.title(), { + ltr: true, + }); + buttons.addText(I18n.logout.error.desc()); + buttons.addButtonInput("", I18n.logout.error.cont(), () => { + res(false); + d.hide(); + }); + buttons.addButtonInput("", I18n.logout.error.cancel(), () => { + res(true); + d.hide(); + }); + d.show(); + }) + ) + return false; + } + } + this.remove(); + return true; + } + remove() { + const info = getBulkInfo(); + delete info.users[this.uid]; + if (info.currentuser === this.uid) { + const user = info.users[0]; + if (user) { + info.currentuser = new Specialuser(user).uid; + } else { + info.currentuser = null; + } + } + if (sessionStorage.getItem("currentuser") === this.uid) { + sessionStorage.removeItem("currentuser"); + } + localStorage.setItem("userinfos", JSON.stringify(info)); + } + set pfpsrc(e) { + this.json.pfpsrc = e; + this.updateLocal(); + } + get pfpsrc() { + return this.json.pfpsrc; + } + set username(e) { + this.json.username = e; + this.updateLocal(); + } + get username() { + return this.json.username; + } + set localuserStore(e) { + this.json.localuserStore = e; + this.updateLocal(); + } + proxySave(e: Object) { + return new Proxy(e, { + set: (target, p, newValue, receiver) => { + const bool = Reflect.set(target, p, newValue, receiver); + try { + this.updateLocal(); + } catch (_) { + Reflect.deleteProperty(target, p); + throw _; + } + return bool; + }, + get: (target, p, receiver) => { + const value = Reflect.get(target, p, receiver) as unknown; + if (value instanceof Object) { + return this.proxySave(value); + } + return value; + }, + }); + } + get localuserStore() { + type jsonParse = { + [key: string | number]: any; + }; + return this.proxySave(this.json.localuserStore) as { + [key: string | number]: jsonParse; + }; + } + set id(e) { + this.json.id = e; + this.updateLocal(); + } + get id() { + return this.json.id; + } + get uid() { + return this.email + this.serverurls.wellknown; + } + toJSON() { + return this.json; + } + updateLocal() { + const info = getBulkInfo(); + info.users[this.uid] = this.toJSON(); + localStorage.setItem("userinfos", JSON.stringify(info)); + } +} +export function trimswitcher() { + const json = getBulkInfo(); + const map = new Map(); + for (const thing in json.users) { + const user = json.users[thing]; + let wellknown = user.serverurls.wellknown; + if (wellknown.at(-1) !== "/") { + wellknown += "/"; + } + wellknown = (user.id || user.email) + "@" + wellknown; + if (map.has(wellknown)) { + const otheruser = map.get(wellknown); + if (otheruser[1].serverurls.wellknown.at(-1) === "/") { + delete json.users[otheruser[0]]; + map.set(wellknown, [thing, user]); + } else { + delete json.users[thing]; + } + } else { + map.set(wellknown, [thing, user]); + } + } + for (const thing in json.users) { + if (thing.at(-1) === "/") { + const user = json.users[thing]; + delete json.users[thing]; + json.users[thing.slice(0, -1)] = user; + } + } + localStorage.setItem("userinfos", JSON.stringify(json)); + console.log(json); +} +export function adduser(user: typeof Specialuser.prototype.json): Specialuser { + const suser = new Specialuser(user); + const info = getBulkInfo(); + info.users[suser.uid] = suser; + info.currentuser = suser.uid; + sessionStorage.setItem("currentuser", suser.uid); + localStorage.setItem("userinfos", JSON.stringify(info)); + return suser; +} +class Directory { + static home = this.createHome(); + handle: FileSystemDirectoryHandle; + writeWorker?: Worker; + private constructor(handle: FileSystemDirectoryHandle) { + this.handle = handle; + } + static async createHome(): Promise { + navigator.storage.persist(); + const home = new Directory(await navigator.storage.getDirectory()); + return home; + } + async *getAllInDir() { + for await (const [name, handle] of this.handle.entries()) { + if (handle instanceof FileSystemDirectoryHandle) { + yield [name, new Directory(handle)] as [string, Directory]; + } else if (handle instanceof FileSystemFileHandle) { + yield [name, await handle.getFile()] as [string, File]; + } else { + console.log(handle, "oops :3"); + } + } + console.log("done"); + } + async getRawFileHandler(name: string) { + return await this.handle.getFileHandle(name); + } + async getRawFile(name: string) { + try { + return await (await this.handle.getFileHandle(name)).getFile(); + } catch { + return undefined; + } + } + async getString(name: string): Promise { + try { + return await (await this.getRawFile(name))!.text(); + } catch { + return undefined; + } + } + initWorker() { + if (this.writeWorker) return this.writeWorker; + this.writeWorker = new Worker("/utils/dirrWorker.js"); + this.writeWorker.onmessage = (event) => { + const res = this.wMap.get(event.data[0]); + this.wMap.delete(event.data[0]); + if (!res) throw new Error("Res is not defined here somehow"); + res(event.data[1]); + }; + return this.writeWorker; + } + wMap = new Map void>(); + async setStringWorker(name: FileSystemFileHandle, value: ArrayBuffer) { + const worker = this.initWorker(); + const random = Math.random(); + worker.postMessage([name, value, random]); + return new Promise((res) => { + this.wMap.set(random, res); + }); + } + async setString(name: string, value: string): Promise { + const file = await this.handle.getFileHandle(name, {create: true}); + const contents = new TextEncoder().encode(value); + + if (file.createWritable as unknown) { + const stream = await file.createWritable({keepExistingData: false}); + await stream.write(contents); + await stream.close(); + return true; + } else { + //Curse you webkit! + return await this.setStringWorker(file, contents.buffer as ArrayBuffer); + } + } + async getDir(name: string) { + return new Directory(await this.handle.getDirectoryHandle(name, {create: true})); + } +} + +export {Directory}; + +const mobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) || (window.matchMedia && window.matchMedia("(pointer: coarse)").matches); +const iOS = /iPhone|iPad|iPod/i.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); +export {mobile, iOS}; + +const datalist = document.getElementById("instances"); +console.warn(datalist); +export const instancefetch = fetch("/instances.json") + .then((res) => res.json()) + .then( + async ( + json: { + name: string; + description?: string; + descriptionLong?: string; + image?: string; + url?: string; + display?: boolean; + online?: boolean; + uptime: {alltime: number; daytime: number; weektime: number}; + urls: { + wellknown: string; + api: string; + cdn: string; + gateway: string; + login?: string; + }; + }[], + ) => { + await I18n.done; + instances = json; + }, + ); +const stringURLMap = new Map(); + +const stringURLsMap = new Map< + string, + { + wellknown: string; + api: string; + cdn: string; + gateway: string; + login?: string; + } +>(); + +export interface InstanceUrls { + admin?: string; + api: string; + cdn: string; + gateway: string; + wellknown: string; +} + +export interface InstanceInfo extends InstanceUrls { + value: string; +} + +export async function getapiurls(str: string): Promise { + str = str.trim(); + if (!str) return null; + + console.info("Attempting to fetch .well-known's for", str); + + // Override first: + let urls: InstanceUrls | null = await getInstanceInfo(str); + if (urls) return urls; + + // Otherwise, fall back to looking it up... + try { + urls = await getApiUrlsV2(str); + if (!urls) throw new Error("meow"); + return urls; + } catch { + return await getApiUrlsV1(str); + } +} + +//region Instance list +export async function getInstanceInfo(str: string): Promise { + // wait for it to be loaded...? Where is this even comming from? + if (stringURLMap.size == 0) { + await new Promise((res, _) => { + let intervalId = setInterval(() => { + if (stringURLMap.size !== 0) { + clearInterval(intervalId); + res(); + } + }, 10); + }); + } + + console.info("Checking if we already know", str, "in our instance lists:", { + stringURLMap, + stringURLsMap, + }); + + if (stringURLMap.has(str)) { + console.error("OOH WE GOT STRING->URL MAP ENTRY FOR", str, "!!!!", stringURLMap.get(str)); + return (await getapiurls(stringURLMap.get(str)!)) as InstanceInfo; + } + + if (stringURLsMap.has(str)) { + console.error( + "WE GOT URL->INSTANCE MAP ENTRY FOR ", + str, + "!!!!!!!!!!11", + stringURLsMap.get(str), + ); + return stringURLsMap.get(str) as InstanceInfo; + } + + return null; +} +//endregion + +//region Well-Known v2 +export async function getApiUrlsV2(str: string): Promise { + if (!URL.canParse(str)) { + console.log("getApiUrlsV2:", str, "is not a parseable url"); + return null; + } + try { + const info = await fetch(str + "/.well-known/spacebar/client").then((r) => r.json()); + let urls: InstanceUrls = { + admin: info.admin?.baseUrl, + api: info.api.baseUrl + "/api/v9", //We don't support anything other than v9 + gateway: info.gateway.baseUrl, + cdn: info.cdn.baseUrl, + wellknown: str, + }; + const check = await checkURLs(str, urls); + if (check) { + urls = check; + } + return urls; + } catch (e) { + console.log("No .well-known v2 for", str, (e as Error).message); + return null; + } +} +//endregion + +//region Well-Known v1 +/** + * this function checks if a string is an instance, it'll either return the API urls or null + */ +async function checkURLs(wellknown: string, urls: InstanceUrls) { + if (isLoopback(urls.api) !== isLoopback(wellknown)) { + return new Promise((res) => { + const menu = new Dialog(""); + const options = menu.float.options; + options.addMDText(new MarkDown(I18n.incorrectURLS(), undefined)); + const opt = options.addOptions("", {ltr: true}); + let clicked = false; + opt.addButtonInput("", I18n.yes(), async () => { + if (clicked) return; + clicked = true; + if (urls == null) throw new Error("Unexpected undefined, exiting"); + const temp = new URL(wellknown); + temp.port = ""; + const newOrigin = temp.host; + const protocol = temp.protocol; + const tempurls = { + api: new URL(urls.api), + cdn: new URL(urls.cdn), + gateway: new URL(urls.gateway), + wellknown: new URL(urls.wellknown), + }; + tempurls.api.host = newOrigin; + tempurls.api.protocol = protocol; + + tempurls.cdn.host = newOrigin; + tempurls.api.protocol = protocol; + + tempurls.gateway.host = newOrigin; + tempurls.gateway.protocol = temp.protocol === "http:" ? "ws:" : "wss:"; + + tempurls.wellknown.host = newOrigin; + tempurls.wellknown.protocol = protocol; + + try { + if ( + !( + await fetch( + tempurls.api + (tempurls.api.toString().endsWith("/") ? "" : "/") + "ping", + ) + ).ok + ) { + res(null); + menu.hide(); + return; + } + } catch { + res(null); + menu.hide(); + return; + } + res({ + api: tempurls.api.toString(), + cdn: tempurls.cdn.toString(), + gateway: tempurls.gateway.toString(), + wellknown: tempurls.wellknown.toString(), + }); + menu.hide(); + }); + const no = opt.addButtonInput("", I18n.no(), async () => { + if (clicked) return; + clicked = true; + if (urls == null) throw new Error("URLs is undefined"); + try { + //TODO make this a promise race for when the server just never responds + //TODO maybe try to strip ports as another way to fix it + if (!(await fetch(urls.api + "ping")).ok) { + res(null); + menu.hide(); + return; + } + } catch { + res(null); + return; + } + res(urls); + menu.hide(); + }); + const span = document.createElement("span"); + options.addHTMLArea(span); + const t = setInterval(() => { + if (!document.contains(span)) { + clearInterval(t); + no.onClick(); + } + }, 100); + menu.show(); + }); + } + return undefined; +} +export async function getApiUrlsV1(str: string): Promise { + function appendApi(str: string) { + return str.includes("api") ? str : str.endsWith("/") ? str + "api" : str + "/api"; + } + if (!URL.canParse(str)) { + if (stringURLMap.size === 0) { + await new Promise((res) => { + let intervalID = setInterval(() => { + if (stringURLMap.size !== 0) { + clearInterval(intervalID); + res(); + } + }, 100); + }); + } + const val = stringURLMap.get(str); + if (val) { + str = val; + } else { + const val = stringURLsMap.get(str); + if (val) { + const response = await fetch(val.api + (val.api.endsWith("/") ? "" : "/") + "ping"); + if (response.ok) { + if (val.login) { + return val as InstanceUrls; + } else { + val.login = val.api; + return val as InstanceUrls; + } + } + } else if (!str.match(/^https?:\/\//gm)) { + str = "https://" + str; + } + } + } + str = trimTrailingSlashes(str); + let api: string; + try { + const info = await fetch(`${str}/.well-known/spacebar`).then((x) => x.json()); + api = trimTrailingSlashes(info.api); + } catch { + api = str; + } + if (!URL.canParse(api)) { + return null; + } + const url = new URL(api); + let urls: undefined | InstanceUrls; + function fixApi() { + if (!urls) return; + urls = { + wellknown: trimTrailingSlashes(urls.wellknown), + api: trimTrailingSlashes(urls.api), + cdn: trimTrailingSlashes(urls.cdn), + gateway: trimTrailingSlashes(urls.gateway), + }; + } + + try { + const info = await fetch( + `${api}${url.pathname.includes("api") ? "" : "api"}/policies/instance/domains`, + ).then((x) => x.json()); + const apiurl = new URL(info.apiEndpoint); + urls = { + api: apiurl.origin + appendApi(apiurl.pathname), + gateway: info.gateway, + cdn: info.cdn, + wellknown: str, + }; + fixApi(); + } catch { + const val = stringURLsMap.get(str); + if (val) { + const response = await fetch(trimTrailingSlashes(val.api) + "/ping"); + if (response.ok) { + if (val.login) { + urls = val as InstanceUrls; + fixApi(); + } else { + val.login = val.api; + urls = val as InstanceUrls; + fixApi(); + } + } + } + } + if (urls) { + const check = await checkURLs(str, urls); + if (check) { + urls = check; + } + //*/ + try { + if (!(await fetch(urls.api + "/ping")).ok) { + return null; + } + } catch { + return null; + } + return urls; + } + return null; +} +//endregion + +async function isAnimated(src: string) { + try { + src = new URL(src).pathname; + } catch {} + return ( + src.endsWith(".apng") || + src.endsWith(".gif") || + src.split("/").at(-1)?.startsWith("a_") || + src.includes("avatar-decoration-presets") + ); +} +const staticImgMap = new Map>(); +export async function removeAni(elm: HTMLElement, time = 500) { + elm.classList.add("removeElm"); + const ani = elm.getAnimations(); + await Promise.race([ + Promise.all(ani.map((_) => _.finished)), + new Promise((res) => setTimeout(res, time)), + ]); + if (document.contains(elm)) elm.remove(); +} +export type safeImg = HTMLImageElement & { + setSrcs: (nsrc: string, nstaticsrc: string | void) => void; + isAnimated: () => Promise; +}; +export function createImg( + src: string | undefined, + staticsrc: string | void, + elm: HTMLElement | void, + type: "gif" | "icon" = "gif", +): safeImg { + const aniOpt = getPreferences().then((prefs) => { + return ( + (type === "gif" ? prefs.animateGifs : prefs.animateIcons) || + ("hover" as "hover") || + "always" || + "never" + ); + }); + const img = document.createElement("img"); + img.loading = "lazy"; + img.decoding = "async"; + img.addEventListener("error", () => { + img.classList.add("error"); + }); + elm ||= img; + if (src) { + isAnimated(src).then(async (animated) => { + if (animated) { + img.crossOrigin = "anonymous"; + } + img.src = (await aniOpt) !== "always" ? staticsrc || src || "" : src || ""; + }); + } + img.onload = async () => { + if ((await aniOpt) === "always") return; + if (!src) return; + if ((await isAnimated(src)) && !staticsrc) { + let s = staticImgMap.get(src); + if (s) { + staticsrc = await s; + } else { + staticImgMap.set( + src, + new Promise(async (res) => { + const c = new OffscreenCanvas(img.naturalWidth, img.naturalHeight); + const ctx = c.getContext("2d"); + if (!ctx) return; + ctx.drawImage(img, 0, 0); + const blob = await c.convertToBlob(); + res(URL.createObjectURL(blob)); + }), + ); + staticsrc = (await staticImgMap.get(src)) as string; + } + img.src = staticsrc; + } + }; + elm.addEventListener("mouseover", async () => { + if ((await aniOpt) === "never") return; + if (img.src !== src && src) { + img.src = src; + } + }); + elm.addEventListener("mouseleave", async () => { + if (staticsrc && (await aniOpt) !== "always") { + img.src = staticsrc; + } + }); + + return Object.assign(img, { + setSrcs: (nsrc: string, nstaticsrc: string | void) => { + src = nsrc; + staticsrc = nstaticsrc; + if (src) { + isAnimated(src).then(async (animated) => { + if (animated) { + img.crossOrigin = "anonymous"; + } + img.src = (await aniOpt) !== "always" ? staticsrc || src || "" : src || ""; + }); + } + }, + isAnimated: async () => { + return !!(src && (await isAnimated(src))); + }, + }); +} + +/** + * + * This function takes in a string and checks if the string is a valid instance + * the string may be a URL or the name of the instance + * the alt property is something you may fire on success. + */ +const checkInstance = Object.assign( + async function ( + instance: string, + verify = document.getElementById("verify"), + loginButton = (document.getElementById("loginButton") || + document.getElementById("createAccount") || + document.createElement("button")) as HTMLButtonElement, + ) { + await instancefetch; + try { + loginButton.disabled = true; + verify!.textContent = I18n.login.checking(); + const instanceValue = instance; + const instanceinfo = (await getapiurls(instanceValue)) as InstanceInfo; + if (instanceinfo) { + instanceinfo.value = instanceValue; + localStorage.setItem("instanceinfo", JSON.stringify(instanceinfo)); + verify!.textContent = I18n.login.allGood(); + loginButton.disabled = false; + if (checkInstance.alt) { + checkInstance.alt(instanceinfo); + } + setTimeout((_: any) => { + console.log(verify!.textContent); + verify!.textContent = ""; + }, 3000); + return instanceinfo; + } else { + verify!.textContent = I18n.login.invalid(); + loginButton.disabled = true; + return; + } + } catch { + verify!.textContent = I18n.login.invalid(); + loginButton.disabled = true; + return; + } + }, + {} as { + alt?: (e: InstanceInfo) => void; + }, +); +{ + //TODO look at this and see if this can be made less hacky :P + const originalFetch = window.fetch; + window.fetch = function (input: RequestInfo | URL, init?: RequestInit) { + const url = new URL(input instanceof Request ? input.url : input, window.location.href); + if (apiDoms.has(url.host)) { + init = init || {}; + init.credentials ??= "include"; + } + + return originalFetch(input, init); + }; +} +export {checkInstance}; + +export class SW { + static worker: undefined | ServiceWorker; + static registration: ServiceWorkerRegistration; + static port?: MessagePort; + static init() { + SW.setMode(getLocalSettings().serviceWorkerMode); + const port = new MessageChannel(); + SW.worker?.postMessage( + { + code: "port", + port: port.port2, + }, + [port.port2], + ); + this.port = port.port1; + this.port.onmessage = (e) => { + this.handleMessage(e.data); + }; + window.addEventListener("beforeunload", () => { + this.postMessage({code: "close"}); + port.port1.close(); + }); + this.postMessage({code: "ping"}); + this.postMessage({code: "isDev", dev: getDeveloperSettings().cacheSourceMaps}); + this.captureEvent("updates", (update, stop) => { + this.needsUpdate ||= update.updates; + if (update) { + stop(); + const updateIcon = document.getElementById("updateIcon"); + if (updateIcon) { + updateIcon.hidden = false; + } + } + }); + } + static traceInit() { + getBulkUsers(); + } + static needsUpdate = false; + static async postMessage(message: messageTo) { + if (!("serviceWorker" in navigator)) return; + while (!this.port) { + await new Promise((res) => setTimeout(res, 100)); + } + this.port.postMessage(message); + } + static eventListeners = new Map< + messageFrom["code"], + Set<(y: messageFrom, remove: () => void) => void> + >(); + static captureEvent( + name: X, + fucny: (y: Extract, remove: () => void) => void, + ) { + let sett = this.eventListeners.get(name); + if (!sett) { + sett = new Set(); + this.eventListeners.set(name, sett); + } + sett.add(fucny as () => void); + } + static uncaptureEvent( + name: X, + fucny: (y: Extract, remove: () => void) => void, + ) { + let sett = this.eventListeners.get(name); + if (!sett) return; + sett.delete(fucny as () => void); + } + static async handleMessage(message: messageFrom) { + const sett = this.eventListeners.get(message.code); + if (sett) { + for (const thing of sett) { + thing(message, () => { + this.uncaptureEvent(message.code, thing); + }); + } + } + switch (message.code) { + case "pong": { + console.log(message); + break; + } + case "close": { + for (const thing of await navigator.serviceWorker.getRegistrations()) { + await thing.unregister(); + } + await this.start(); + this.postMessage({code: "replace"}); + break; + } + case "closing": { + await this.start(); + break; + } + case "updates": { + break; + } + case "isValid": { + } + } + } + + static async isValid(url: string): Promise { + return new Promise((res) => { + this.captureEvent("isValid", (e, stop) => { + if (e.url === url) { + res(e.valid); + stop(); + } + }); + this.postMessage({code: "isValid", url}); + }); + } + static async checkUpdates(): Promise { + if (this.needsUpdate) return true; + return new Promise((res) => { + this.captureEvent("updates", (update, remove) => { + remove; + res(update.updates); + }); + this.postMessage({code: "CheckUpdate"}); + }); + } + static async start() { + if (!("serviceWorker" in navigator)) return; + + // If it's registered, it handles CDN caching regardless of settings. + if (getLocalSettings().serviceWorkerMode == ServiceWorkerMode.Unregistered) return; + return new Promise((res) => { + navigator.serviceWorker + .register("/service.js", { + scope: "/", + }) + .then((registration) => { + let serviceWorker: ServiceWorker | undefined; + if (registration.installing) { + serviceWorker = registration.installing; + console.log("Service worker: installing"); + } else if (registration.waiting) { + serviceWorker = registration.waiting; + console.log("Service worker: waiting"); + } else if (registration.active) { + serviceWorker = registration.active; + console.log("Service worker: active"); + } + SW.worker = serviceWorker; + SW.registration = registration; + SW.init(); + + if (serviceWorker) { + console.log("Service worker state changed:", serviceWorker.state); + serviceWorker.addEventListener("statechange", (_) => { + console.log("Service worker state changed:", serviceWorker.state); + }); + res(); + } + }); + }); + } + static setMode(mode: ServiceWorkerMode) { + const localSettings = getLocalSettings(); + localSettings.serviceWorkerMode = mode; + setLocalSettings(localSettings); + if (this.worker) { + this.worker.postMessage({data: mode, code: "setMode"}); + } + + if (mode === ServiceWorkerMode.Unregistered) + this.registration.unregister().then((r) => console.log("Service worker unregistered:", r)); + } + + static forceClear() { + if (this.worker) { + this.worker.postMessage({code: "ForceClear"}); + } + } +} +SW.start(); +let installPrompt: Event | undefined = undefined; +window.addEventListener("beforeinstallprompt", (event) => { + event.preventDefault(); + installPrompt = event; +}); +export function installPGet() { + return installPrompt; +} + +export function getInstances() { + return instances; +} +export function getStringURLMapPair() { + return [stringURLMap, stringURLsMap] as const; +} diff --git a/src/webpage/voice.ts b/src/webpage/voice.ts index ada524f9..584b5386 100644 --- a/src/webpage/voice.ts +++ b/src/webpage/voice.ts @@ -1,137 +1,388 @@ -import { memberjson, sdpback, voiceserverupdate, voiceupdate, webRTCSocket } from "./jsontypes.js"; - -class VoiceFactory{ - settings:{id:string}; - constructor(usersettings:VoiceFactory["settings"]){ - this.settings=usersettings; - } - voices=new Map>(); - voiceChannels=new Map(); - currentVoice?:Voice; - guildUrlMap=new Map,gotUrl:()=>void}>(); - makeVoice(guildid:string,channelId:string,settings:Voice["settings"]){ - let guild=this.voices.get(guildid); - if(!guild){ +import { + memberjson, + sdpback, + streamCreate, + streamServerUpdate, + voiceserverupdate, + voiceStatus, + webRTCSocket, +} from "./jsontypes.js"; +function forceVideo(video: HTMLVideoElement) { + video.controls = false; + //TODO loading? + video.classList.add("media-engine-video"); + video.addEventListener("pause", () => { + video.play(); + }); +} +class VoiceFactory { + settings: {id: string}; + handleGateway: (obj: Object) => void; + secure: boolean; + constructor( + usersettings: VoiceFactory["settings"], + handleGateway: VoiceFactory["handleGateway"], + secure: boolean, + ) { + this.secure = secure; + this.settings = usersettings; + this.handleGateway = handleGateway; + } + voices = new Map>(); + voiceChannels = new Map(); + currentVoice?: Voice; + guildUrlMap = new Map< + string, + {url?: string; token?: string; geturl: Promise; gotUrl: () => void} + >(); + makeVoice(guildid: string, channelId: string, settings: Voice["settings"]) { + let guild = this.voices.get(guildid); + if (!guild) { this.setUpGuild(guildid); - guild=new Map(); - this.voices.set(guildid,guild); - } - const urlobj=this.guildUrlMap.get(guildid); - if(!urlobj) throw new Error("url Object doesn't exist (InternalError)"); - const voice=new Voice(this.settings.id,settings,urlobj); - this.voiceChannels.set(channelId,voice); - guild.set(channelId,voice); + guild = new Map(); + this.voices.set(guildid, guild); + } + const urlobj = this.guildUrlMap.get(guildid); + if (!urlobj) throw new Error("url Object doesn't exist (InternalError)"); + const voice = new Voice(this.settings.id, settings, urlobj, this); + this.voiceChannels.set(channelId, voice); + guild.set(channelId, voice); return voice; } - onJoin=(_voice:Voice)=>{}; - onLeave=(_voice:Voice)=>{}; - joinVoice(channelId:string,guildId:string){ - if(this.currentVoice){ + onJoin = (_voice: Voice) => {}; + onLeave = (_voice: Voice) => {}; + private imute = false; + video = false; + stream = false; + get mute() { + return this.imute; + } + set mute(s) { + const changed = this.imute !== s; + this.imute = s; + this.sendVoiceStateIfNeeded(); + if (this.currentVoice && changed) { + this.currentVoice.updateMute(); + } + } + disconect() { + if (!this.curChan) return; + this.curChan = null; + this.curGuild = null; + this.sendVoiceState(); + } + + updateSelf() { + if (this.currentVoice && this.currentVoice.open) { + this.sendVoiceState(); + } + } + curGuild: string | null = null; + curChan: string | null = null; + lastSentState = ""; + sendVoiceStateIfNeeded() { + const state = this.calcState; + if (JSON.stringify(state) !== this.lastSentState) this.sendVoiceState(); + } + get calcState() { + return { + op: 4, + d: { + guild_id: this.curGuild, + channel_id: this.curChan, + self_mute: this.imute, + self_deaf: false, + self_video: (this.curGuild ?? false) && this.video, + flags: 3, + }, + } as const; + } + sendVoiceState() { + const send = this.calcState; + this.lastSentState = JSON.stringify(send); + this.handleGateway(send); + } + joinVoice(channelId: string, guildId: string, self_mute = false) { + const voice = this.voiceChannels.get(channelId); + this.mute = self_mute; + if (this.currentVoice && this.currentVoice.ws) { this.currentVoice.leave(); } - const voice=this.voiceChannels.get(channelId); - if(!voice) throw new Error(`Voice ${channelId} does not exist`); + this.curChan = channelId; + this.curGuild = guildId; + if (!voice) throw new Error(`Voice ${channelId} does not exist`); voice.join(); - this.currentVoice=voice; + this.currentVoice = voice; this.onJoin(voice); - return { - d:{ - guild_id: guildId, - channel_id: channelId, - self_mute: true,//todo - self_deaf: false,//todo - self_video: false,//What is this? I have some guesses - flags: 2//????? + this.sendVoiceState(); + return voice; + } + leaveLive() { + const userid = this.settings.id; + const stream_key = `${this.curGuild === "@me" ? "call" : `guild:${this.curGuild}`}:${this.curChan}:${userid}`; + this.handleGateway({ + op: 19, + d: { + stream_key, }, - op:4 - } + }); + } + live = new Map void>(); + steamTokens = new Map>(); + steamTokensRes = new Map void>(); + async joinLive(userid: string) { + const stream_key = `${this.curGuild === "@me" ? "call" : `guild:${this.curGuild}`}:${this.curChan}:${userid}`; + this.handleGateway({ + op: 20, + d: { + stream_key, + }, + }); + return new Promise(async (res) => { + this.live.set(stream_key, res); + this.steamTokens.set( + stream_key, + new Promise<[string, string]>((res) => { + this.steamTokensRes.set(stream_key, res); + }), + ); + }); + } + islive = false; + liveStream?: MediaStream; + async createLive(stream: MediaStream) { + const userid = this.settings.id; + this.islive = true; + this.liveStream = stream; + const stream_key = `${this.curGuild === "@me" ? "call" : `guild:${this.curGuild}`}:${this.curChan}:${userid}`; + this.handleGateway({ + op: 18, + d: { + type: this.curGuild === "@me" ? "call" : "guild", + guild_id: this.curGuild === "@me" ? null : this.curGuild, + channel_id: this.curChan, + preferred_region: null, + }, + }); + this.handleGateway({ + op: 22, + d: { + paused: false, + stream_key, + }, + }); + + const voice = await new Promise(async (res) => { + this.live.set(stream_key, res); + this.steamTokens.set( + stream_key, + new Promise<[string, string]>((res) => { + this.steamTokensRes.set(stream_key, res); + }), + ); + }); + stream.getTracks().forEach((track) => + track.addEventListener("ended", () => { + this.leaveLive(); + }), + ); + return voice; } - userMap=new Map(); - voiceStateUpdate(update:voiceupdate){ + async streamCreate(create: streamCreate) { + const prom1 = this.steamTokens.get(create.d.stream_key); + if (!prom1) throw new Error("oops"); + const [token, endpoint] = await prom1; + if (create.d.stream_key.startsWith("guild")) { + const [_, _guild, chan, user] = create.d.stream_key.split(":"); + const voice2 = this.voiceChannels.get(chan); - const prev=this.userMap.get(update.d.user_id); - console.log(prev,this.userMap); - if(prev){ - prev.disconnect(update.d.user_id); + if (!voice2 || !voice2.session_id) throw new Error("oops"); + if (voice2.voiceMap.has(user)) { + voice2.makeOp12(); + return; + } + let stream: undefined | MediaStream = undefined; + console.error(user, this.settings.id); + if (user === this.settings.id) { + stream = this.liveStream; + } + const voice = new Voice( + this.settings.id, + { + bitrate: 10000, + stream: true, + }, + { + url: endpoint, + token, + }, + this, + ); + voice.join(); + voice.startWS(voice2.session_id, create.d.rtc_server_id); + let video = false; + voice.onSatusChange = (e) => { + console.warn(e); + if (e === "done" && stream && !video) { + console.error("starting to stream"); + voice.startVideo(stream); + video = true; + } + }; + + voice2.gotStream(voice, user); + console.warn(voice2); + const res = this.live.get(create.d.stream_key); + if (res) res(voice); + } + } + streamServerUpdate(update: streamServerUpdate) { + const res = this.steamTokensRes.get(update.d.stream_key); + if (res) res([update.d.token, update.d.endpoint]); + } + userMap = new Map(); + voiceStateUpdate(update: voiceStatus) { + const prev = this.userMap.get(update.user_id); + console.log(prev, this.userMap); + if (update.user_id === this.settings.id && this.liveStream && !update.self_stream) { + const stream_key = `${this.curGuild === "@me" ? "call" : `guild:${this.curGuild}`}:${this.curChan}:${this.settings.id}`; + this.handleGateway({ + op: 22, + d: { + paused: false, + stream_key, + }, + }); + } + if (prev && prev !== this.voiceChannels.get(update.channel_id)) { + prev.disconnect(update.user_id); this.onLeave(prev); } - const voice=this.voiceChannels.get(update.d.channel_id); - if(voice){ - this.userMap.set(update.d.user_id,voice); + const voice = this.voiceChannels.get(update.channel_id); + if (voice) { + this.userMap.set(update.user_id, voice); voice.voiceupdate(update); } } - private setUpGuild(id:string){ - const obj:{url?:string,geturl?:Promise,gotUrl?:()=>void}={}; - obj.geturl=new Promise(res=>{obj.gotUrl=res}); - this.guildUrlMap.set(id,obj as {geturl:Promise,gotUrl:()=>void}); + private setUpGuild(id: string) { + const obj: {url?: string; geturl?: Promise; gotUrl?: () => void} = {}; + obj.geturl = new Promise((res) => { + obj.gotUrl = res; + }); + this.guildUrlMap.set(id, obj as {geturl: Promise; gotUrl: () => void}); } - voiceServerUpdate(update:voiceserverupdate){ - const obj=this.guildUrlMap.get(update.d.guild_id); - if(!obj) return; - obj.url=update.d.endpoint; + voiceServerUpdate(update: voiceserverupdate) { + const obj = this.guildUrlMap.get(update.d.guild_id); + if (!obj) return; + obj.url = update.d.endpoint; + obj.token = update.d.token; obj.gotUrl(); } } - -class Voice{ - private pstatus:string="not connected"; - public onSatusChange:(e:string)=>unknown=()=>{}; - set status(e:string){ - this.pstatus=e; +export type voiceStatusStr = + | "done" + | "notconnected" + | "sendingStreams" + | "conectionFailed" + | "makingOffer" + | "startingRTC" + | "noSDP" + | "waitingMainWS" + | "waitingURL" + | "badWS" + | "wsOpen" + | "wsAuth" + | "left"; +class UserVolume { + gain!: GainNode; + id: string; + set volume(vol: number) { + this.gain.gain.value = vol; + } + constructor(media: MediaStream, id: string) { + this.startAudio(media); + this.id = id; + } + async startAudio(media: MediaStream) { + const context = new AudioContext(); + this.gain = context.createGain(); + console.log(context); + await context.resume(); + const ss = context.createMediaStreamSource(media); + console.log(media, ss); + new Audio().srcObject = media; //weird I know, but it's for chromium/webkit bug + ss.connect(this.gain); + this.gain.connect(context.destination); + } +} +class Voice { + private pstatus: voiceStatusStr = "notconnected"; + public onSatusChange: (e: voiceStatusStr) => unknown = () => {}; + set status(e: voiceStatusStr) { + console.log("state changed: " + e); + this.pstatus = e; this.onSatusChange(e); } - get status(){ + get status() { return this.pstatus; } - readonly userid:string; - settings:{bitrate:number}; - urlobj:{url?:string,geturl:Promise,gotUrl:()=>void}; - constructor(userid:string,settings:Voice["settings"],urlobj:Voice["urlobj"]){ - this.userid=userid; - this.settings=settings; - this.urlobj=urlobj; - } - pc?:RTCPeerConnection; - ws?:WebSocket; - timeout:number=30000; - interval:NodeJS.Timeout=0 as unknown as NodeJS.Timeout; - time:number=0; - seq:number=0; - sendAlive(){ - if(this.ws){ - this.ws.send(JSON.stringify({ op: 3,d:10})); - } - } - readonly users= new Map(); - readonly speakingMap= new Map(); - onSpeakingChange=(_userid:string,_speaking:number)=>{}; - disconnect(userid:string){ + readonly userid: string; + settings: {bitrate: number; stream?: boolean; live?: MediaStream}; + urlobj: {url?: string; token?: string; geturl?: Promise; gotUrl?: () => void}; + owner: VoiceFactory; + constructor( + userid: string, + settings: Voice["settings"], + urlobj: Voice["urlobj"], + owner: VoiceFactory, + ) { + this.userid = userid; + this.settings = settings; + this.urlobj = urlobj; + this.owner = owner; + } + pc?: RTCPeerConnection; + ws?: WebSocket; + timeout: number = 30000; + interval: NodeJS.Timeout = 0 as unknown as NodeJS.Timeout; + time: number = 0; + seq: number = 0; + sendAlive() { + if (this.ws) { + this.ws.send(JSON.stringify({op: 3, d: 10})); + } + } + users = new Map(); + vidusers = new Map(); + readonly speakingMap = new Map(); + onSpeakingChange = (_userid: string, _speaking: number) => {}; + disconnect(userid: string) { console.warn(userid); - if(userid===this.userid){ + if (userid === this.userid) { this.leave(); } - const ssrc=this.speakingMap.get(userid); + const ssrc = this.speakingMap.get(userid); - if(ssrc){ - this.users.delete(ssrc); - for(const thing of this.ssrcMap){ - if(thing[1]===ssrc){ + if (ssrc) { + this.users.set(ssrc, ""); + for (const thing of this.ssrcMap) { + if (thing[1] === ssrc) { this.ssrcMap.delete(thing[0]); } } } this.speakingMap.delete(userid); this.userids.delete(userid); - console.log(this.userids,userid); + console.log(this.userids, userid); //there's more for sure, but this is "good enough" for now - this.onMemberChange(userid,false); + this.onMemberChange(userid, false); } - packet(message:MessageEvent){ - const data=message.data - if(typeof data === "string"){ - const json:webRTCSocket = JSON.parse(data); - switch(json.op){ + + async packet(message: MessageEvent) { + const data = message.data; + if (typeof data === "string") { + const json: webRTCSocket = JSON.parse(data); + switch (json.op) { case 2: this.startWebRTC(); break; @@ -139,435 +390,954 @@ class Voice{ this.continueWebRTC(json); break; case 5: - this.speakingMap.set(json.d.user_id,json.d.speaking); - this.onSpeakingChange(json.d.user_id,json.d.speaking); + this.speakingMap.set(json.d.user_id, json.d.speaking); + this.onSpeakingChange(json.d.user_id, json.d.speaking); break; case 6: - this.time=json.d.t; + this.time = json.d.t; setTimeout(this.sendAlive.bind(this), this.timeout); break; case 8: - this.timeout=json.d.heartbeat_interval; + this.timeout = json.d.heartbeat_interval; setTimeout(this.sendAlive.bind(this), 1000); break; case 12: - this.figureRecivers(); - if(!this.users.has(json.d.audio_ssrc)){ + await this.figureRecivers(); + if ( + (!this.users.has(json.d.audio_ssrc) && json.d.audio_ssrc !== 0) || + (!this.vidusers.has(json.d.video_ssrc) && json.d.video_ssrc !== 0) + ) { console.log("redo 12!"); this.makeOp12(); } - this.users.set(json.d.audio_ssrc,json.d.user_id); + if (this.pc && json.d.audio_ssrc) { + this.pc.addTransceiver("audio", { + direction: "recvonly", + sendEncodings: [{active: true}], + }); + this.getAudioTrans(this.users.size + 1).direction = "recvonly"; + this.users.set(json.d.audio_ssrc, json.d.user_id); + } + if (this.pc && json.d.video_ssrc) { + this.pc.addTransceiver("video", { + direction: "recvonly", + sendEncodings: [{active: true}], + }); + this.getVideoTrans(this.vidusers.size + 1).direction = "recvonly"; + this.vidusers.set(json.d.video_ssrc, json.d.user_id); + } + break; } } } - offer?:string; - cleanServerSDP(sdp:string):string{ - const pc=this.pc; - if(!pc) throw new Error("pc isn't defined") - const ld=pc.localDescription; - if(!ld) throw new Error("localDescription isn't defined"); + getVideoTrans(id: number) { + if (!this.pc) throw new Error("no pc"); + let i = 0; + for (const thing of this.pc.getTransceivers()) { + if (thing.receiver.track.kind === "video") { + if (id === i) { + return thing; + } + i++; + } + } + throw new Error("none by that id"); + } + getAudioTrans(id: number) { + if (!this.pc) throw new Error("no pc"); + let i = 0; + for (const thing of this.pc.getTransceivers()) { + if (thing.receiver.track.kind === "audio") { + if (id === i) { + return thing; + } + i++; + } + } + throw new Error("none by that id"); + } + hoffer?: string; + get offer() { + return this.hoffer; + } + set offer(e: string | undefined) { + this.hoffer = e; + } + fingerprint?: string; + async cleanServerSDP(sdp: string): Promise { + const out = await this.getCamInfo(); + if (out.rtx_ssrc) { + this.vidusers.set(out.rtx_ssrc, this.userid); + console.log(out); + } else { + const i = [...this.vidusers].findIndex((_) => _[1] === this.userid); + this.vidusers.delete(i); + } + const pc = this.pc; + if (!pc) throw new Error("pc isn't defined"); + const ld = pc.localDescription; + if (!ld) throw new Error("localDescription isn't defined"); const parsed = Voice.parsesdp(ld.sdp); - const group=parsed.atr.get("group"); - if(!group) throw new Error("group isn't in sdp"); - const [_,...bundles]=(group.entries().next().value as [string, string])[0].split(" "); - bundles[bundles.length-1]=bundles[bundles.length-1].replace("\r",""); + const group = parsed.atr.get("group"); + console.warn(parsed); + if (!group) throw new Error("group isn't in sdp"); + const [_, ...bundles] = (group.entries().next().value as [string, string])[0].split(" "); + bundles[bundles.length - 1] = bundles[bundles.length - 1].replace("\r", ""); console.log(bundles); - if(!this.offer) throw new Error("Offer is missing :P"); - let cline=sdp.split("\n").find(line=>line.startsWith("c=")); - if(!cline) throw new Error("c line wasn't found"); - const parsed1=Voice.parsesdp(sdp).medias[0]; + if (!this.offer) throw new Error("Offer is missing :P"); + let cline = sdp.split("\n").find((line) => line.startsWith("c=")); + if (!cline) throw new Error("c line wasn't found"); + const parsed1 = Voice.parsesdp(sdp).medias[0]; //const parsed2=Voice.parsesdp(this.offer); - const rtcport=(parsed1.atr.get("rtcp") as Set).values().next().value as string; - const ICE_UFRAG=(parsed1.atr.get("ice-ufrag") as Set).values().next().value as string; - const ICE_PWD=(parsed1.atr.get("ice-pwd") as Set).values().next().value as string; - const FINGERPRINT=(parsed1.atr.get("fingerprint") as Set).values().next().value as string; - const candidate=(parsed1.atr.get("candidate") as Set).values().next().value as string; - let build=`v=0\r -o=- 1420070400000 0 IN IP4 127.0.0.1\r + const rtcport = (parsed1.atr.get("rtcp") as Set).values().next().value as string; + const ICE_UFRAG = (parsed1.atr.get("ice-ufrag") as Set).values().next().value as string; + const ICE_PWD = (parsed1.atr.get("ice-pwd") as Set).values().next().value as string; + const FINGERPRINT = + this.fingerprint || + ((parsed1.atr.get("fingerprint") as Set).values().next().value as string); + this.fingerprint = FINGERPRINT; + const candidate = (parsed1.atr.get("candidate") as Set).values().next().value as string; + + const audioUsers = [...this.users]; + const videoUsers = [...this.vidusers]; + console.warn(audioUsers); + + let build = `v=0\r +o=- 1420070400000 0 IN IP4 ${this.urlobj.url}\r s=-\r t=0 0\r a=msid-semantic: WMS *\r -a=group:BUNDLE ${bundles.join(" ")}\r` - let i=0; - for(const grouping of parsed.medias){ - let mode="recvonly"; - for(const _ of this.senders){ - if(i<2){ - mode="sendrecv"; - } - } - if(grouping.media==="audio"){ - build+=` -m=audio ${parsed1.port} UDP/TLS/RTP/SAVPF 111\r +a=group:BUNDLE ${bundles.join(" ")}\r`; + let ai = -1; + let vi = -1; + let i = 0; + for (const grouping of parsed.medias) { + const cur = + ([...grouping.atr] + .map((_) => _[0].trim()) + .find((_) => + new Set(["inactive", "recvonly", "sendonly", "sendrecv"]).has(_), + ) as "inactive") || + "recvonly" || + "sendonly" || + "sendrecv"; + const mode = { + inactive: "inactive", + recvonly: "sendonly", + sendonly: "recvonly", + sendrecv: "sendrecv", + }[cur]; + if (grouping.media === "audio") { + const port = [...grouping.ports][0]; + build += ` +m=audio ${parsed1.port} UDP/TLS/RTP/SAVPF ${port}\r ${cline}\r -a=rtpmap:111 opus/48000/2\r -a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1\r +a=rtpmap:${port} opus/48000/2\r +a=fmtp:${port} minptime=10;useinbandfec=1;usedtx=1\r a=rtcp:${rtcport}\r -a=rtcp-fb:111 transport-cc\r -a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r -a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01/r/n +a=rtcp-fb:${port} transport-cc\r a=setup:passive\r -a=mid:${bundles[i]}\r +a=mid:${bundles[i]}${audioUsers[ai] && audioUsers[ai][1] ? `\r\na=msid:${audioUsers[ai][1]}-${audioUsers[ai][0]} a${audioUsers[ai][1]}-${audioUsers[ai][0]}\r` : "\r"} a=maxptime:60\r -a=${mode}\r +a=${audioUsers[ai] && audioUsers[ai][1] ? "sendonly" : mode}\r a=ice-ufrag:${ICE_UFRAG}\r a=ice-pwd:${ICE_PWD}\r a=fingerprint:${FINGERPRINT}\r -a=candidate:${candidate}\r -a=rtcp-mux\r` - }else{ - build+=` -m=video ${rtcport} UDP/TLS/RTP/SAVPF 102 103\r +a=candidate:${candidate}${audioUsers[ai] && audioUsers[ai][1] ? `\r\na=ssrc:${audioUsers[ai][0]} cname:${audioUsers[ai][1]}-${audioUsers[ai][0]}\r` : "\r"} +a=rtcp-mux\r`; + console.log(audioUsers[ai], "audio user"); + ai++; + } else { + const set = grouping.atr.get("rtpmap") || new Set(); + let port1 = ""; + let port2 = ""; + for (const thing of set) { + if (thing.includes("H264/90000") && !port1) { + port1 = thing.split(" ")[0]; + } else if (thing.includes("rtx/90000") && !port2) { + port2 = thing.split(" ")[0]; + } + } + + build += ` +m=video ${parsed1.port} UDP/TLS/RTP/SAVPF ${port1} ${port2}\r ${cline}\r -a=rtpmap:102 H264/90000\r -a=rtpmap:103 rtx/90000\r -a=fmtp:102 x-google-max-bitrate=2500;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r -a=fmtp:103 apt=102\r +a=rtpmap:${port1} H264/90000\r +a=rtpmap:${port2} rtx/90000\r +a=fmtp:${port1} level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r +a=fmtp:${port2} apt=${port1}\r a=rtcp:${rtcport}\r -a=rtcp-fb:102 ccm fir\r -a=rtcp-fb:102 nack\r -a=rtcp-fb:102 nack pli\r -a=rtcp-fb:102 goog-remb\r -a=rtcp-fb:102 transport-cc\r -a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time/r/n -a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01/r/n -a=extmap:14 urn:ietf:params:rtp-hdrext:toffset\r -a=extmap:13 urn:3gpp:video-orientation\r -a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay/r/na=setup:passive/r/n -a=mid:${bundles[i]}\r -a=${mode}\r +a=rtcp-fb:${port1} ccm fir\r +a=rtcp-fb:${port1} nack\r +a=rtcp-fb:${port1} nack pli\r +a=rtcp-fb:${port1} goog-remb\r +a=rtcp-fb:${port1} transport-cc\r +a=setup:passive\r +a=mid:${bundles[i]}${videoUsers[vi] && videoUsers[vi][1] ? `\r\na=msid:${videoUsers[vi][1]}-${videoUsers[vi][0]} v${videoUsers[vi][1]}-${videoUsers[vi][0]}\r` : "\r"} +a=${videoUsers[vi] && videoUsers[vi][1] ? "sendonly" : mode}\r a=ice-ufrag:${ICE_UFRAG}\r a=ice-pwd:${ICE_PWD}\r a=fingerprint:${FINGERPRINT}\r -a=candidate:${candidate}\r +a=candidate:${candidate}${videoUsers[vi] && videoUsers[vi][1] ? `\r\na=ssrc:${videoUsers[vi][0]} cname:${videoUsers[vi][1]}-${videoUsers[vi][0]}\r` : "\r"} a=rtcp-mux\r`; + vi++; + console.log(mode, "fine me :3"); } - i++ + i++; } - build+="\n"; + build += "\n"; + console.log(ld.sdp, "fime :3", build, this.pc?.remoteDescription?.sdp); return build; } - counter?:string; - negotationneeded(){ - if(this.pc&&this.offer){ - const pc=this.pc; - pc.addEventListener("negotiationneeded", async ()=>{ - this.offer=(await pc.createOffer({ - offerToReceiveAudio: true, - offerToReceiveVideo: true - })).sdp; - await pc.setLocalDescription({sdp:this.offer}); - - if(!this.counter) throw new Error("counter isn't defined"); - const counter=this.counter; - const remote:{sdp:string,type:RTCSdpType}={sdp:this.cleanServerSDP(counter),type:"answer"}; - console.log(remote); - await pc.setRemoteDescription(remote); - const senders=this.senders.difference(this.ssrcMap); - for(const sender of senders){ - for(const thing of (await sender.getStats() as Map)){ - if(thing[1].ssrc){ - this.ssrcMap.set(sender,thing[1].ssrc); + counter?: string; + forceNext: boolean = false; + async updateRemote() { + const counter = this.counter; + if (!counter || !this.pc) return; + const remote: {sdp: string; type: RTCSdpType} = { + sdp: await this.cleanServerSDP(counter), + type: "answer", + }; + console.log([remote.sdp, this.pc.localDescription?.sdp]); + await this.pc.setRemoteDescription(remote); + } + negotationneeded() { + if (this.pc) { + const pc = this.pc; + let setting = false; + const setLocal = async (forced: boolean = this.forceNext) => { + if (setting) return; + const val = (Math.random() * 1000) ^ 0; + + setting = true; + const offer = await pc.createOffer(); + if (offer.sdp === pc.localDescription?.sdp || forced) { + if (forced) console.log("foced :3"); + logState("update", "will Sent offer " + val); + await pc.setLocalDescription(); + logState("update", "Sent offer " + val); + } + setting = false; + this.forceNext = false; + }; + const sendOffer = async (forced = this.forceNext) => { + if (!setting) { + setLocal(forced); + console.log("set local"); + } + + const senders = this.senders.difference(this.ssrcMap); + console.log(senders, this.ssrcMap); + let made12 = false; + this.pc + ?.getStats() + .then((_) => _.forEach((_) => _.type === "local-candidate" || console.error(_))); + console.log(pc.localDescription?.sdp); + for (const sender of senders) { + const d = await sender.getStats(); + let found = false; + d.forEach((thing) => { + if (thing.ssrc) { + made12 = true; + found = true; + this.ssrcMap.set(sender, thing.ssrc); + this.makeOp12(sender); + console.warn("ssrc"); + } + }); + //TODO Firefox made me do this, if I can figure out how to not do this, that'd be great + if (!found && pc.localDescription?.sdp) { + const sdp = Voice.parsesdp(pc.localDescription.sdp); + + const index = pc.getTransceivers().findIndex((_) => _.sender === sender); + const temp = sdp.medias[index].atr.get("ssrc"); + if (temp) { + const ssrc = +[...temp][0].split(" ")[0]; + this.ssrcMap.set(sender, ssrc); this.makeOp12(sender); + console.warn("ssrc"); + made12 = true; } } } + if (!made12) { + console.warn("this was ran :3"); + this.makeOp12(); + } + }; + const detectDone = () => { + if ( + pc.signalingState === "stable" && + pc.iceConnectionState === "connected" && + pc.connectionState === "connected" + ) { + this.status = "done"; + } + console.log(pc.signalingState, pc.iceConnectionState, pc.connectionState); + }; + function logState(thing: string, message = "") { + console.log("log state: " + thing + (message ? ":" + message : "")); + } + pc.addEventListener("negotiationneeded", async () => { + logState("negotiationneeded"); + await sendOffer(true); console.log(this.ssrcMap); }); + pc.onicecandidate = (e) => { + console.warn(e.candidate); + }; + + pc.addEventListener("signalingstatechange", async () => { + logState("signalingstatechange", pc.signalingState); + detectDone(); + while (!this.counter) await new Promise((res) => setTimeout(res, 100)); + if (this.pc && this.counter) { + console.warn("in here :3"); + if (pc.signalingState === "have-local-offer") { + const val = (Math.random() * 1000) ^ 0; + logState("update", "start sent remote " + val); + await this.updateRemote(); + logState("update", "end sent remote " + val); + } + } else { + console.warn("uh oh!"); + } + }); + pc.addEventListener("connectionstatechange", async () => { + logState("connectionstatechange", pc.connectionState); + detectDone(); + if (pc.connectionState === "connecting") { + //logState("update2", "start Set local desc"); + //await pc.setLocalDescription(); + //logState("update2", "Set local desc"); + } + }); + pc.addEventListener("icegatheringstatechange", async () => { + logState("icegatheringstatechange", pc.iceGatheringState); + detectDone(); + console.log(this.counter, this.pc); + if (pc.iceGatheringState === "complete") { + if (setting) return; + if (this.pc && this.counter) { + setLocal(); + } + } + }); + pc.addEventListener("iceconnectionstatechange", async () => { + logState("iceconnectionstatechange", pc.iceConnectionState); + + detectDone(); + if (pc.iceConnectionState === "checking") { + await sendOffer(); + } + }); } } - async makeOp12(sender:RTCRtpSender|undefined|[RTCRtpSender,number]=(this.ssrcMap.entries().next().value)){ - if(!sender) throw new Error("sender doesn't exist"); - if(sender instanceof Array){ - sender=sender[0]; - } - if(this.ws){ - this.ws.send(JSON.stringify({ - op: 12, - d: { - audio_ssrc: this.ssrcMap.get(sender), - video_ssrc: 0, - rtx_ssrc: 0, - streams: [ - { - type: "video", - rid: "100", - ssrc: 0,//TODO - active: false, - quality: 100, - rtx_ssrc: 0,//TODO - max_bitrate: 2500000,//TODO - max_framerate: 0,//TODO - max_resolution: { - type: "fixed", - width: 0,//TODO - height: 0//TODO - } - } - ] + async getCamInfo() { + let video_ssrc = 0; + let rtx_ssrc = 0; + const cammera = this.cammera; + const cam = this.cam; + let attemps = 0; + if (cam && cammera) { + do { + if (attemps > 10) { + return {video_ssrc, rtx_ssrc}; } - })); - this.status="Sending audio streams"; + const stats = (await cam.sender.getStats()) as Map; + Array.from(stats).forEach((_) => { + if (_[1].ssrc) { + video_ssrc = _[1].ssrc; + console.warn(_); + } + if (_[1].rtxSsrc) { + rtx_ssrc = _[1].rtxSsrc; + } + }); + const settings = cammera.getSettings(); + console.error(settings); + attemps++; + await new Promise((res) => setTimeout(res, 100)); + } while (!video_ssrc || !rtx_ssrc); } + return {video_ssrc, rtx_ssrc}; } - senders:Set=new Set(); - recivers=new Set(); - ssrcMap:Map=new Map(); - speaking=false; - async setupMic(audioStream:MediaStream){ - const audioContext = new AudioContext(); - const analyser = audioContext.createAnalyser(); - const microphone = audioContext.createMediaStreamSource(audioStream); + async makeOp12(sender: RTCRtpSender | undefined | [RTCRtpSender, number] = this.mic?.sender) { + //debugger; + //await new Promise((res) => setTimeout(res, 500)); + this.owner.sendVoiceStateIfNeeded(); + console.warn("making 12?"); + if (!this.ws) return; + if (sender instanceof Array) { + sender = sender[0]; + } + //console.trace(sender === this.mic?.sender); + let max_framerate = 20; + let width = 1280; + let height = 720; + const {rtx_ssrc, video_ssrc} = await this.getCamInfo(); + if (this.cam && this.cammera) { + } else if (!sender) { + return; + } + + console.log(this.ssrcMap); + try { + console.error("start 12"); + if (!video_ssrc) + this.ws.send( + JSON.stringify({ + op: 12, + d: { + audio_ssrc: 0, + video_ssrc: 0, + rtx_ssrc: 0, + streams: [ + { + type: "video", + rid: "100", + ssrc: video_ssrc, + active: !!video_ssrc, + quality: 100, + rtx_ssrc: rtx_ssrc, + max_bitrate: 2500000, //TODO + max_framerate, //TODO + max_resolution: {type: "fixed", width, height}, + }, + ], + }, + }), + ); + this.ws.send( + JSON.stringify({ + op: 12, + d: { + audio_ssrc: + sender?.track?.kind === "audio" + ? this.owner.mute + ? 0 + : this.ssrcMap.get(sender as RTCRtpSender) + : 0, + video_ssrc, + rtx_ssrc, + streams: [ + { + type: "video", + rid: "100", + ssrc: video_ssrc, + active: !!video_ssrc, + quality: 100, + rtx_ssrc: rtx_ssrc, + max_bitrate: 2500000, //TODO + max_framerate, //TODO + max_resolution: {type: "fixed", width, height}, + }, + ], + }, + }), + ); + this.status = "sendingStreams"; + console.error("made 12"); + } catch (e) { + console.error(e); + } + } + micContext = new AudioContext(); + streamDest?: MediaStreamAudioDestinationNode; + micNode = this.micContext.createGain(); + makeMicOuts() { + //if (this.streamDest) this.streamDest.disconnect(this.micNode); + this.streamDest = this.micContext.createMediaStreamDestination(); + this.micNode.gain.setValueAtTime(1, this.micContext.currentTime); + + if (!this.owner.mute) this.micNode.connect(this.streamDest); + + return this.streamDest.stream; + } + async giveMicTrack(stream: MediaStream) { + /* + const audioStream = await ; + + */ + if (this.micTrack) this.micTrack.stop(); + this.micContext.resume(); + const microphone = this.micContext.createMediaStreamSource(stream); + microphone.connect(this.micNode); + const [track] = stream.getAudioTracks(); + this.micTrack = track; + track.enabled = true; + } + senders: Set = new Set(); + recivers = new Set(); + ssrcMap: Map = new Map(); + speaking = false; + async setupMic() { + const analyser = this.micContext.createAnalyser(); analyser.smoothingTimeConstant = 0; analyser.fftSize = 32; - microphone.connect(analyser); - const array=new Float32Array(1); - const interval=setInterval(()=>{ - if(!this.ws){ + this.micNode.connect(analyser); + const array = new Float32Array(1); + const interval = setInterval(() => { + if (!this.ws) { clearInterval(interval); } - analyser.getFloatFrequencyData(array); - const value=array[0]+65; - if(value<0){ - if(this.speaking){ - this.speaking=false; + if (!this.owner.mute) analyser.getFloatFrequencyData(array); + const value = this.owner.mute ? -Infinity : array[0] + 65; + if (value < 0) { + if (this.speaking) { + this.speaking = false; this.sendSpeaking(); - console.log("not speaking") + console.log("not speaking"); } - }else if(!this.speaking){ + } else if (!this.speaking) { console.log("speaking"); - this.speaking=true; + this.speaking = true; this.sendSpeaking(); } - },500); - } - async sendSpeaking(){ - if(!this.ws) return; - const pair=this.ssrcMap.entries().next().value; - if(!pair) return - this.ws.send(JSON.stringify({ - op:5, - d:{ - speaking:+this.speaking, - delay:5,//not sure - ssrc:pair[1] - } - })) - } - async continueWebRTC(data:sdpback){ - if(this.pc&&this.offer){ - const pc=this.pc; - this.negotationneeded(); - this.status="Starting Audio streams"; - const audioStream = await navigator.mediaDevices.getUserMedia({video: false, audio: true} ); - for (const track of audioStream.getAudioTracks()){ - //Add track - - this.setupMic(audioStream); - const sender = pc.addTrack(track); - this.senders.add(sender); - console.log(sender) - } - for(let i=0;i<10;i++){ - pc.addTransceiver("audio",{ - direction:"recvonly", - streams:[], - sendEncodings:[{active:true,maxBitrate:this.settings.bitrate}] - }); - } - for(let i=0;i<10;i++){ - pc.addTransceiver("video",{ - direction:"recvonly", - streams:[], - sendEncodings:[{active:true,maxBitrate:this.settings.bitrate}] - }); - } - this.counter=data.d.sdp; - pc.ontrack = async (e) => { - this.status="Done"; - if(e.track.kind==="video"){ - return; + }, 500); + } + async sendSpeaking() { + if (!this.ws) return; + const pair = this.ssrcMap.entries().next().value; + if (!pair) return; + this.onSpeakingChange(this.userid, +this.speaking); + this.ws.send( + JSON.stringify({ + op: 5, + d: { + speaking: this.speaking, + delay: 5, //not sure + ssrc: pair[1], + }, + }), + ); + } + async continueWebRTC(data: sdpback) { + if (this.pc && this.offer) { + this.counter = data.d.sdp; + } else { + this.status = "conectionFailed"; + } + } + reciverMap = new Map(); + off?: Promise; + async makeOffer() { + if (this.off) { + if (this.pc?.localDescription?.sdp) return {sdp: this.pc?.localDescription?.sdp}; + return this.off; + } + return (this.off = new Promise(async (res) => { + if (!this.pc) throw new Error("stupid"); + console.error("stupid!"); + const offer = await this.pc.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true, + }); + res(offer); + })); + } + async figureRecivers() { + await new Promise((res) => setTimeout(res, 500)); + for (const reciver of this.recivers) { + const stats = (await reciver.getStats()) as Map; + for (const thing of stats) { + if (thing[1].ssrc) { + this.reciverMap.set(thing[1].ssrc, reciver); } + } + } + console.log(this.reciverMap); + } + updateMute() { + if (this.owner.mute) { + if (this.streamDest) this.micNode.disconnect(this.streamDest); + } else { + if (this.streamDest) this.micNode.connect(this.streamDest); + } + //this.pc?.setLocalDescription(); + this.makeOp12(); + } + mic?: RTCRtpTransceiver; + micTrack?: MediaStreamTrack; + onVideo = (_video: HTMLVideoElement, _id: string) => {}; + videos = new Map(); + cam?: RTCRtpTransceiver; + cammera?: MediaStreamTrack; + async stopVideo() { + if (!this.cam) return; + this.owner.video = false; + if (!this.cammera || !this.pc) return; + this.cammera.stop(); + this.cammera = undefined; - const media=e.streams[0]; - console.log("got audio:",e); - for(const track of media.getTracks()){ - console.log(track); - } + this.cam.sender.replaceTrack(null); + this.cam.direction = "inactive"; - const context= new AudioContext(); - await context.resume(); - const ss=context.createMediaStreamSource(media); - console.log(media); - ss.connect(context.destination); - new Audio().srcObject = media;//weird I know, but it's for chromium/webkit bug - this.recivers.add(e.receiver) - }; + this.pc.setLocalDescription(await this.pc.createOffer()); + + this.owner.updateSelf(); - }else{ - this.status="Connection failed"; + this.videos.delete(this.userid); + this.onUserChange(this.userid, { + deaf: false, + muted: this.owner.mute, + video: false, + live: this.owner.stream, + }); + } + liveMap = new Map(); + voiceMap = new Map(); + isLive() { + return !!this.voiceMap.get(this.userid); + } + getLive(id: string) { + return this.liveMap.get(id); + } + joinLive(id: string) { + return this.owner.joinLive(id); + } + createLive(stream: MediaStream) { + return this.owner.createLive(stream); + } + leaveLive(id: string) { + const v = this.voiceMap.get(id); + if (!v) return; + v.leave(); + this.voiceMap.delete(id); + this.liveMap.delete(id); + this.onLeaveStream(id); + } + stopStream() { + this.leaveLive(this.userid); + this.owner.leaveLive(); + } + onLeaveStream = (_user: string) => {}; + onGotStream = (_v: HTMLVideoElement, _user: string) => {}; + gotStream(voice: Voice, user: string) { + voice.onVideo = (video) => { + this.liveMap.set(user, video); + this.onGotStream(video, user); + }; + this.voiceMap.set(user, voice); + } + videoStarted = false; + async startVideo(caml: MediaStream) { + while (!this.cam) { + await new Promise((res) => setTimeout(res, 100)); + } + console.warn("test test test test video sent!"); + const tracks = caml.getVideoTracks(); + const [cam] = tracks; + + if (!this.settings.stream) this.owner.video = true; + + this.cammera = cam; + + const video = document.createElement("video"); + forceVideo(video); + this.onVideo(video, this.userid); + this.videos.set(this.userid, video); + video.srcObject = caml; + video.autoplay = true; + this.cam.direction = "sendonly"; + const sender = this.cam.sender; + this.senders.add(sender); + + await sender.replaceTrack(cam); + sender.setStreams(caml); + + this.forceNext = true; + + console.warn("replaced track", cam); + this.pc?.setLocalDescription((await this.pc?.createOffer()) || {}); + this.owner.updateSelf(); + if (this.settings.stream) { + this.makeOp12(); } } - reciverMap=new Map() - async figureRecivers(){ - await new Promise(res=>setTimeout(res,500)); - for(const reciver of this.recivers){ - const stats=await reciver.getStats() as Map; - for(const thing of (stats)){ - if(thing[1].ssrc){ - this.reciverMap.set(thing[1].ssrc,reciver) + onconnect = () => {}; + streams = new Set(); + uVolMap = new Map(); + onUserVol: (uv: UserVolume) => unknown = () => {}; + async startWebRTC() { + this.status = "makingOffer"; + const pc = new RTCPeerConnection({ + bundlePolicy: "max-bundle", + }); + pc.ontrack = async (e) => { + this.status = "done"; + this.onconnect(); + const media = e.streams[0]; + if (!media) { + console.log(e); + return; + } + const userId = media.id.split("-")[0]; + if (e.track.kind === "video") { + //TODO I don't know why but without this firefox bugs out on streams + if (media.id.match("{")) return; + if (this.owner.currentVoice?.voiceMap.get(this.userid) === this) { + return; } + + this.streams.add(e.track); + const video = document.createElement("video"); + forceVideo(video); + this.onVideo(video, userId); + this.videos.set(userId, video); + video.srcObject = media; + console.log(video); + + video.autoplay = true; + const settings = e.track.getConstraints(); + + console.log("gotVideo?", media, settings); + settings.height = 300; + settings.width = 300; + e.track.applyConstraints(settings); + //e.track + + //if ("resizeMode" in settings) + return; + } + + console.log("got audio:", e); + for (const track of media.getTracks()) { + console.log(track); } + const a = new UserVolume(media, userId); + this.uVolMap.set(userId, a); + this.onUserVol(a); + + this.recivers.add(e.receiver); + console.log(this.recivers); + }; + if (!this.settings.stream) { + const outStream = this.makeMicOuts(); + /* + + */ + const sender = pc.addTransceiver(outStream.getTracks()[0]); + this.mic = sender; + this.senders.add(sender.sender); + console.log(sender); + } else { + pc.addTransceiver("audio", { + direction: "inactive", + streams: [], + sendEncodings: [{active: true, maxBitrate: this.settings.bitrate}], + }); } - console.log(this.reciverMap); - } - async startWebRTC(){ - this.status="Making offer"; - const pc = new RTCPeerConnection(); - this.pc=pc; - const offer = await pc.createOffer({ - offerToReceiveAudio: true, - offerToReceiveVideo: true + this.cam = pc.addTransceiver("video", { + direction: "sendonly", + streams: [], + sendEncodings: [ + {active: true, maxBitrate: 2500000, scaleResolutionDownBy: 1, maxFramerate: 20}, + ], }); - this.status="Starting RTC connection"; - const sdp=offer.sdp; - this.offer=sdp; + const count = this.settings.stream ? 1 : 10; + for (let i = 0; i < count; i++) { + pc.addTransceiver("audio", { + direction: "inactive", + streams: [], + sendEncodings: [{active: true, maxBitrate: this.settings.bitrate}], + }); + } + if (this.settings.live) { + this.cam = pc.addTransceiver("video", { + direction: "sendonly", + streams: [], + sendEncodings: [ + {active: true, maxBitrate: 2500000, scaleResolutionDownBy: 1, maxFramerate: 20}, + ], + }); + await this.startVideo(this.settings.live); + } else { + for (let i = 0; i < count; i++) { + pc.addTransceiver("video", { + direction: "inactive", + streams: [], + sendEncodings: [{active: true, maxBitrate: this.settings.bitrate}], + }); + } + } + //this.makeOp12(); - if(!sdp){ - this.status="No SDP"; + this.pc = pc; + await this.makeOp12(this.mic?.sender); + this.negotationneeded(); + await new Promise((res) => setTimeout(res, 100)); + let sdp = this.offer; + if (!sdp) { + const offer = await this.makeOffer(); + this.status = "startingRTC"; + sdp = offer.sdp; + this.offer = sdp; + } + + await pc.setLocalDescription(); + if (!sdp) { + this.status = "noSDP"; this.ws?.close(); return; } - const parsed=Voice.parsesdp(sdp); - const video=new Map(); - const audio=new Map(); - let cur:[number,number]|undefined; - let i=0; - for(const thing of parsed.medias){ - try{ - if(thing.media==="video"){ - const rtpmap=thing.atr.get("rtpmap"); - if(!rtpmap) continue; - for(const codecpair of rtpmap){ - - const [port, codec]=codecpair.split(" "); - if(cur&&codec.split("/")[0]==="rtx"){ - cur[1]=Number(port); - cur=undefined; - continue + const parsed = Voice.parsesdp(sdp); + const video = new Map(); + const audio = new Map(); + let cur: [number, number] | undefined; + let i = 0; + for (const thing of parsed.medias) { + try { + if (thing.media === "video") { + const rtpmap = thing.atr.get("rtpmap"); + if (!rtpmap) continue; + for (const codecpair of rtpmap) { + const [port, codec] = codecpair.split(" "); + if (cur && codec.split("/")[0] === "rtx") { + cur[1] = Number(port); + cur = undefined; + continue; } - if(video.has(codec.split("/")[0])) continue; - cur=[Number(port),-1]; - video.set(codec.split("/")[0],cur); + if (video.has(codec.split("/")[0])) continue; + cur = [Number(port), -1]; + video.set(codec.split("/")[0], cur); } - }else if(thing.media==="audio"){ - const rtpmap=thing.atr.get("rtpmap"); - if(!rtpmap) continue; - for(const codecpair of rtpmap){ - const [port, codec]=codecpair.split(" "); - if(audio.has(codec.split("/")[0])) { continue}; - audio.set(codec.split("/")[0],Number(port)); + } else if (thing.media === "audio") { + const rtpmap = thing.atr.get("rtpmap"); + if (!rtpmap) continue; + for (const codecpair of rtpmap) { + const [port, codec] = codecpair.split(" "); + if (audio.has(codec.split("/")[0])) { + continue; + } + audio.set(codec.split("/")[0], Number(port)); } } - }finally{ + } finally { i++; } } - const codecs:{ - name: string, - type: "video"|"audio", - priority: number, - payload_type: number, - rtx_payload_type: number|null - }[]=[]; - const include=new Set(); - const audioAlloweds=new Map([["opus",{priority:1000,}]]); - for(const thing of audio){ - if(audioAlloweds.has(thing[0])){ + const codecs: { + name: string; + type: "video" | "audio"; + priority: number; + payload_type: number; + rtx_payload_type: number | null; + }[] = []; + const include = new Set(); + const audioAlloweds = new Map([["opus", {priority: 1000}]]); + for (const thing of audio) { + if (audioAlloweds.has(thing[0])) { include.add(thing[0]); codecs.push({ - name:thing[0], - type:"audio", - priority:audioAlloweds.get(thing[0])?.priority as number, - payload_type:thing[1], - rtx_payload_type:null + name: thing[0], + type: "audio", + priority: audioAlloweds.get(thing[0])?.priority as number, + payload_type: thing[1], + rtx_payload_type: null, }); } } - const videoAlloweds=new Map([["H264",{priority:1000}],["VP8",{priority:2000}],["VP9",{priority:3000}]]); - for(const thing of video){ - if(videoAlloweds.has(thing[0])){ + const videoAlloweds = new Map([ + ["H264", {priority: 1000}], + ["VP8", {priority: 2000}], + ["VP9", {priority: 3000}], + ]); + for (const thing of video) { + if (videoAlloweds.has(thing[0])) { include.add(thing[0]); codecs.push({ - name:thing[0], - type:"video", - priority:videoAlloweds.get(thing[0])?.priority as number, - payload_type:thing[1][0], - rtx_payload_type:thing[1][1] + name: thing[0], + type: "video", + priority: videoAlloweds.get(thing[0])?.priority as number, + payload_type: thing[1][0], + rtx_payload_type: thing[1][1], }); } } - let sendsdp="a=extmap-allow-mixed"; - let first=true; - for(const media of parsed.medias){ - - for(const thing of first?["ice-ufrag","ice-pwd","ice-options","fingerprint","extmap","rtpmap"]:["extmap","rtpmap"]){ - const thing2=media.atr.get(thing); - if(!thing2) continue; - for(const thing3 of thing2){ - if(thing === "rtpmap"){ - const name=thing3.split(" ")[1].split("/")[0]; - if(include.has(name)){ + let sendsdp = "a=extmap-allow-mixed"; + let first = true; + + for (const media of parsed.medias) { + for (const thing of first + ? (["ice-ufrag", "ice-pwd", "ice-options", "fingerprint", "extmap", "rtpmap"] as const) + : (["extmap", "rtpmap"] as const)) { + let thing2 = media.atr.get(thing); + if (!thing2) { + thing2 = parsed.atr.get(thing); + if (!thing2) { + console.error("couldn't find " + thing); + continue; + } + } + for (const thing3 of thing2) { + if (thing === "rtpmap") { + const name = thing3.split(" ")[1].split("/")[0]; + if (include.has(name)) { include.delete(name); - }else{ + } else { continue; } } - sendsdp+=`\na=${thing}:${thing3}`; + sendsdp += `\na=${thing}:${thing3}`; } } - first=false; - } - if(this.ws){ - this.ws.send(JSON.stringify({ - d:{ - codecs, - protocol:"webrtc", - data:sendsdp, - sdp:sendsdp - }, - op:1 - })); + first = false; + } + console.log(sendsdp); + if (this.ws) { + this.ws.send( + JSON.stringify({ + d: { + codecs, + protocol: "webrtc", + data: sendsdp, + sdp: sendsdp, + }, + op: 1, + }), + ); } + console.warn("done with this!"); } - static parsesdp(sdp:string){ - let currentA=new Map>(); - const out:{version?:number,medias:{media:string,port:number,proto:string,ports:number[],atr:Map>}[],atr:Map>}={medias:[],atr:currentA}; - for(const line of sdp.split("\n")){ - const [code,setinfo]=line.split("="); - switch(code){ + static parsesdp(sdp: string) { + let currentA = new Map>(); + const out: { + version?: number; + medias: { + media: string; + port: number; + proto: string; + ports: number[]; + atr: Map>; + }[]; + atr: Map>; + } = {medias: [], atr: currentA}; + for (const line of sdp.split("\n")) { + const [code, setinfo] = line.split("="); + switch (code) { case "v": - out.version=Number(setinfo); + out.version = Number(setinfo); break; case "o": case "s": case "t": break; case "m": - currentA=new Map(); - const [media,port,proto,...ports]=setinfo.split(" "); - const portnums=ports.map(Number); - out.medias.push({media,port:Number(port),proto,ports:portnums,atr:currentA}); + currentA = new Map(); + const [media, port, proto, ...ports] = setinfo.split(" "); + const portnums = ports.map(Number); + out.medias.push({media, port: Number(port), proto, ports: portnums, atr: currentA}); break; case "a": const [key, ...value] = setinfo.split(":"); - if(!currentA.has(key)){ - currentA.set(key,new Set()); + if (!currentA.has(key)) { + currentA.set(key, new Set()); } currentA.get(key)?.add(value.join(":")); break; @@ -575,78 +1345,139 @@ a=rtcp-mux\r`; } return out; } - open=false; - async join(){ + open = false; + async join() { console.warn("Joining"); - this.open=true - this.status="waiting for main WS"; + this.open = true; + this.status = "waitingMainWS"; } - onMemberChange=(_member:memberjson|string,_joined:boolean)=>{}; - userids=new Map(); - async voiceupdate(update:voiceupdate){ + onMemberChange = (_member: memberjson | string, _joined: boolean) => {}; + userids = new Map(); + onUserChange = ( + _user: string, + _change: {deaf: boolean; muted: boolean; video: boolean; live: boolean}, + ) => {}; + async voiceupdate(update: voiceStatus) { console.log("Update!"); - this.userids.set(update.d.member.id,{deaf:update.d.deaf,muted:update.d.mute}); - this.onMemberChange(update.d.member,true); - if(update.d.member.id===this.userid&&this.open){ - if(!update) { - this.status="bad responce from WS"; + if (!this.userids.has(update.user_id)) { + this.onMemberChange(update?.member || update.user_id, true); + } + const vals = { + deaf: update.deaf, + muted: update.mute || update.self_mute, + video: update.self_video, + live: update.self_stream, + }; + this.onUserChange(update.user_id, vals); + this.userids.set(update.user_id, vals); + if (update.user_id === this.userid && this.videoStarted !== update.self_video) { + this.makeOp12(); + this.videoStarted = update.self_video; + } + if (update.user_id === this.userid && this.open && !this.ws) { + if (!update) { + this.status = "badWS"; return; - }; - if(!this.urlobj.url){ - this.status="waiting for Voice URL"; - await this.urlobj.geturl; - if(!this.open){this.leave();return} } - - const ws=new WebSocket("ws://"+this.urlobj.url as string); - this.ws=ws; - ws.onclose=()=>{ - this.leave(); - } - this.status="waiting for WS to open"; - ws.addEventListener("message",(m)=>{ - this.packet(m); - }) - await new Promise(res=>{ - ws.addEventListener("open",()=>{ - res() - }) - }); - if(!this.ws){ + this.session_id = update.session_id; + await this.startWS(update.session_id, update.guild_id); + } + } + session_id?: string; + async startWS(session_id: string, server_id: string) { + if (!this.urlobj.url) { + this.status = "waitingURL"; + await this.urlobj.geturl; + if (!this.open) { this.leave(); return; } - this.status="waiting for WS to authorize"; - ws.send(JSON.stringify({ - "op": 0, - "d": { - server_id: update.d.guild_id, - user_id: update.d.user_id, - session_id: update.d.session_id, - token: update.d.token, - video: false, - "streams": [ + } + + const ws = new WebSocket( + ((this.owner.secure ? "wss://" : "ws://") + this.urlobj.url) as string, + ); + this.ws = ws; + this.setupMic(); + ws.onclose = () => { + this.leave(); + }; + this.status = "wsOpen"; + ws.addEventListener("message", (m) => { + this.packet(m); + }); + await new Promise((res) => { + ws.addEventListener("open", () => { + res(); + }); + }); + if (!this.ws) { + this.leave(); + return; + } + this.status = "wsAuth"; + ws.send( + JSON.stringify({ + op: 0, + d: { + server_id, + user_id: this.userid, + session_id, + token: this.urlobj.token, + max_secure_frames_version: 0, + video: !!this.settings.live, + streams: [ { - type: "video", + type: this.settings.live ? "screen" : "video", rid: "100", - quality: 100 - } - ] - } - })); - } + quality: 100, + }, + ], + }, + }), + ); } - async leave(){ - this.open=false; - this.status="Left voice chat"; - if(this.ws){ + onLeave = () => {}; + async leave() { + console.warn("leave"); + this.open = false; + this.status = "left"; + if (!this.settings.stream) this.owner.video = false; + this.onLeave(); + + for (const thing of this.liveMap) { + this.leaveLive(thing[0]); + } + if (!this.settings.stream) { + this.onMemberChange(this.userid, false); + } + this.userids.delete(this.userid); + if (this.ws) { this.ws.close(); - this.ws=undefined; + this.ws = undefined; } - if(this.pc){ + if (this.pc) { this.pc.close(); - this.pc=undefined; + this.pc = undefined; } + this.micTrack?.stop(); + this.micTrack = undefined; + this.mic = undefined; + this.off = undefined; + this.counter = undefined; + this.offer = undefined; + this.senders = new Set(); + this.recivers = new Set(); + this.ssrcMap = new Map(); + this.fingerprint = undefined; + this.users = new Map(); + if (!this.settings.stream) this.owner.disconect(); + this.vidusers = new Map(); + this.videos = new Map(); + if (this.cammera) this.cammera.stop(); + this.cammera = undefined; + this.cam = undefined; + console.log(this); } } -export {Voice,VoiceFactory}; +export {Voice, VoiceFactory}; diff --git a/src/webpage/webhooks.ts b/src/webpage/webhooks.ts new file mode 100644 index 00000000..f4fc70e0 --- /dev/null +++ b/src/webpage/webhooks.ts @@ -0,0 +1,182 @@ +import {Guild} from "./guild.js"; +import {I18n} from "./i18n.js"; +import {webhookType} from "./jsontypes.js"; +import {MarkDown} from "./markdown.js"; +import {Dialog, Options} from "./settings.js"; +import {SnowFlake} from "./snowflake.js"; +import {User} from "./user.js"; +import {CDNParams} from "./utils/cdnParams.js"; + +async function webhookMenu( + guild: Guild, + hookURL: string, + webhooks: Options, + channelId: false | string = false, +) { + const moveChannels = guild.channels.filter( + (_) => _.hasPermission("MANAGE_WEBHOOKS") && _.type !== 4, + ); + async function regenArea() { + webhooks.removeAll(); + + webhooks.addButtonInput("", I18n.webhooks.newWebHook(), () => { + const nameBox = new Dialog(I18n.webhooks.EnterWebhookName()); + const options = nameBox.float.options; + const defualts = I18n.webhooks.sillyDefaults().split("\n"); + let channel = channelId || moveChannels[0].id; + options.addTextInput( + I18n.webhooks.name(), + async (name) => { + const json = await ( + await fetch(`${guild.info.api}/channels/${channel}/webhooks/`, { + method: "POST", + headers: guild.headers, + body: JSON.stringify({name}), + }) + ).json(); + makeHook(json); + }, + { + initText: defualts[Math.floor(Math.random() * defualts.length)], + }, + ); + if (!channelId) { + const select = options.addSelect( + I18n.webhooks.channel(), + () => {}, + moveChannels.map((_) => _.name), + { + defaultIndex: 0, + }, + ); + select.watchForChange((i: number) => { + channel = moveChannels[i].id; + }); + } + options.addButtonInput("", I18n.submit(), () => { + options.submit(); + nameBox.hide(); + }); + nameBox.show(); + }); + const hooks = (await (await fetch(hookURL, {headers: guild.headers})).json()) as webhookType[]; + for (const hook of hooks) { + makeHook(hook); + } + } + + const makeHook = (hook: webhookType) => { + //TODO remove once the server fixes this bug + hook.url ||= `${guild.info.api}/webhooks/${guild.id}/${hook.token}`; + + const div = document.createElement("div"); + div.classList.add("flexltr", "webhookArea"); + const pfp = document.createElement("img"); + if (hook.avatar) { + pfp.src = + `${guild.info.cdn}/avatars/${hook.id}/${hook.avatar}` + new CDNParams({expectedSize: 96}); + } else { + const int = Number((BigInt(hook.id) >> 22n) % 6n); + pfp.src = `${guild.info.cdn}/embed/avatars/${int}.png`; + } + pfp.classList.add("webhookpfppreview"); + + const namePlate = document.createElement("div"); + namePlate.classList.add("flexttb"); + + const name = document.createElement("b"); + name.textContent = hook.name; + + const createdAt = document.createElement("span"); + createdAt.textContent = I18n.webhooks.createdAt( + new Intl.DateTimeFormat(I18n.lang).format(SnowFlake.stringToUnixTime(hook.id)), + ); + + const wtype = document.createElement("span"); + let typeText: string; + switch (hook.type) { + case 1: + typeText = I18n.webhooks.type1(); + break; + case 2: + typeText = I18n.webhooks.type2(); + break; + case 3: + typeText = I18n.webhooks.type3(); + break; + } + wtype.textContent = I18n.webhooks.type(typeText); + + namePlate.append(name, createdAt, wtype); + + const icon = document.createElement("span"); + icon.classList.add("svg-intoMenu", "svgicon"); + + div.append(pfp, namePlate, icon); + + div.onclick = () => { + const form = webhooks.addSubForm( + hook.name, + (e) => { + regenArea(); + console.log(e); + }, + { + traditionalSubmit: true, + method: "PATCH", + fetchURL: guild.info.api + "/webhooks/" + hook.id, + headers: guild.headers, + }, + ); + form.addTextInput(I18n.webhooks.name(), "name", {initText: hook.name}); + form.addFileInput(I18n.webhooks.avatar(), "avatar", {clear: true}); + + form.addSelect( + I18n.webhooks.channel(), + "channel_id", + moveChannels.map((_) => _.name), + { + defaultIndex: moveChannels.findIndex((_) => _.id === hook.channel_id), + }, + moveChannels.map((_) => _.id), + ); + + form.addMDText(new MarkDown(I18n.webhooks.token(hook.token), undefined)); + form.addMDText(new MarkDown(I18n.webhooks.url(hook.url), undefined)); + form.addText(I18n.webhooks.type(typeText)); + form.addButtonInput("", I18n.webhooks.copyURL(), () => { + navigator.clipboard.writeText(hook.url); + }); + + form.addText(I18n.webhooks.createdBy()); + + try { + const user = new User(hook.user, guild.localuser); + const div = user.createWidget(guild); + form.addHTMLArea(div); + } catch {} + form.addButtonInput("", I18n.webhooks.deleteWebhook(), () => { + const d = new Dialog("areYouSureDelete"); + const opt = d.options; + opt.addTitle(I18n.webhooks.areYouSureDelete(hook.name)); + const opt2 = opt.addOptions("", {ltr: true}); + opt2.addButtonInput("", I18n.yes(), () => { + fetch(guild.info.api + "/webhooks/" + hook.id, { + method: "DELETE", + headers: guild.headers, + }).then(() => { + d.hide(); + regenArea(); + }); + }); + opt2.addButtonInput("", I18n.no(), () => { + d.hide(); + }); + d.show(); + }); + }; + webhooks.addHTMLArea(div); + }; + regenArea(); +} +export {webhookMenu}; diff --git a/translations.md b/translations.md index 340ff870..b4886a4a 100644 --- a/translations.md +++ b/translations.md @@ -1,5 +1,5 @@ # Translations -the translations are stored in `/src/webpage/translations` in this format. +The translations are stored in `/src/webpage/translations` in this format below. ```json { "@metadata": { @@ -13,7 +13,7 @@ the translations are stored in `/src/webpage/translations` in this format. ``` ## I want to help translate this -Please go to [https://translatewiki.net/wiki/Translating:JankClient](https://translatewiki.net/wiki/Translating:JankClient) to help translate this project +Please go to [https://translatewiki.net/wiki/Translating:Fermi](https://translatewiki.net/wiki/Translating:Fermi) to help translate this project. ## What is the format? It's the same format found [here](https://github.com/wikimedia/jquery.i18n#message-file-format), though we are not using jquery, and you might notice some of the strings use markdown, but most do not. diff --git a/translations/de.json b/translations/de.json index 80aa4c34..3514997f 100644 --- a/translations/de.json +++ b/translations/de.json @@ -2,123 +2,969 @@ "@metadata": { "authors": [ "Booky", - "Brettchenweber" + "Brettchenweber", + "Byte", + "Exil S", + "LeoGe25", + "McDutchie", + "Mrmaffen", + "Mukeber", + "Plunoto", + "Reinmedia", + "Russianbabe1978", + "SergeCroise", + "Simon04", + "SomeRandomDeveloper", + "YeBoy371", + "ZoeyG" ] }, - "readableName": "Deutsch", - "reply": "Antworten", - "copyrawtext": "Rohen Text kopieren", - "copymessageid": "Nachrichten-Id kopieren", - "permissions": { - "descriptions": { - "CREATE_INSTANT_INVITE": "Erlaubt dem Nutzer, Server-Einladungen zu erstellen", - "KICK_MEMBERS": "Erlaubt dem Nutzer, Mitglieder vom Server zu werfen", - "BAN_MEMBERS": "Erlaubt dem Nutzer, Mitglieder vom Server zu bannen", - "ADMINISTRATOR": "Erlaubt dem Nutzer alle Berechtigungen und umgeht das Überschreiben von Kanalberechtigungen. Dies ist eine gefährliche Berechtigung!", - "MANAGE_CHANNELS": "Erlaubt dem Nutzer, Kanäle zu verwalten und zu editieren", - "MANAGE_GUILD": "Erlaubt dem Nutzer, den Server zu verwalten und zu editieren", - "ADD_REACTIONS": "Erlaubt dem Nutzer, auf Nachrichten zu reagieren", - "VIEW_AUDIT_LOG": "Erlaubt dem Nutzer die Einsicht des Audit-Logs", - "PRIORITY_SPEAKER": "Erlaubt dem Nutzer, ein Prioritätssprecher in Sprachkanälen zu sein", - "STREAM": "Erlaubt dem Nutzer zu streamen", - "VIEW_CHANNEL": "Erlaubt dem Nutzer, den Kanal zu sehen", - "SEND_MESSAGES": "Erlaubt dem Nutzer, Nachrichten zu senden", - "SEND_TTS_MESSAGES": "Erlaubt dem Nutzer das Senden von Text-zu-Sprache Nachrichten", - "MANAGE_MESSAGES": "Erlaubt dem Nutzer das Löschen von Nachrichten anderer Nutzer", - "EMBED_LINKS": "Erlaubt dem Nutzer die automatische Einbettung von Links", - "ATTACH_FILES": "Erlaubt dem Nutzer den Anhang von Dateien", - "READ_MESSAGE_HISTORY": "Erlaubt dem Nutzer, den Nachrichtenverlauf einzusehen", - "MENTION_EVERYONE": "Erlaubt dem Nutzer die Erwähnung von @everyone", - "USE_EXTERNAL_EMOJIS": "Erlaubt dem Nutzer die Benutzung von externen Emojis", - "VIEW_GUILD_INSIGHTS": "Erlaubt dem Nutzer die Einsicht von Server Insights", - "CONNECT": "Erlaubt dem Nutzer die Verbindung zu Sprachkanälen", - "SPEAK": "Erlaubt dem Nutzer das Sprechen in Sprachkanälen", - "MUTE_MEMBERS": "Erlaubt dem Nutzer, andere Nutzer serverweit stummzuschalten", - "MOVE_MEMBERS": "Erlaubt dem Nutzer, andere Nutzer zwischen Sprachkanälen zu verschieben", - "USE_VAD": "Erlaubt dem Nutzer, in Sprachkanälen ohne Push-to-Talk zu sprechen", - "CHANGE_NICKNAME": "Erlaubt dem Nutzer, seinen eigenen Nicknamen zu ändern", - "MANAGE_NICKNAMES": "Erlaubt dem Nutzer, die Nicknamen von anderen Nutzern zu verwalten", - "MANAGE_ROLES": "Erlaubt dem Nutzer, die Serverrollen zu verwalten und zu editieren", - "MANAGE_WEBHOOKS": "Erlaubt dem Nutzer, Webhooks zu verwalten und zu editieren", - "MANAGE_GUILD_EXPRESSIONS": "Erlaubt dem Nutzer, Emojis, Sticker und Soundboard Sounds zu verwalten", - "USE_APPLICATION_COMMANDS": "Erlaubt dem Nutzer die Verwendung von Befehlen bestimmter Anwendungen", - "REQUEST_TO_SPEAK": "Erlaubt dem Nutzer, in einem Bühnenkanal das Rederecht zu erbitten", - "MANAGE_EVENTS": "Erlaubt dem Nutzer das Verwalten und Editieren von Events" + "2faCode": "2FA-Code:", + "404": { + "404": "Fehler 404 – Seite nicht gefunden", + "app": "Zur App", + "blog": "Der Fermi-Blog", + "home": "Startseite", + "listtitle": "Vielleicht wolltest du zu einem dieser Orte gehen", + "login": "Anmelden", + "report": "Falls Du diese Seite im Client gefunden hast, melde sie bitte:", + "reset": "Passwort-Reset-Seite", + "title": "Du scheinst dich verirrt zu haben", + "whatelse": "Was sollte deiner Meinung nach sonst noch passieren?", + "whereever": "Wo auch immer das sein mag" + }, + "onboarding": { + "name": "Einführung", + "disable": "Einführung deaktivieren", + "addChannel": "Kanal hinzufügen", + "channel": "Kanal:", + "desc": "Beschreibung:", + "deleteChannel": "Kanal aus Einführung löschen", + "enable": "Einführung aktivieren", + "title": "Willkommen zu $1!" + }, + "DMs": { + "add": "Füge jemanden zu dieser Direktnachricht hinzu", + "close": "Direktnachricht schließen", + "copyId": "Direktnachricht-ID kopieren", + "markRead": "Als gelesen markieren", + "name": "Direktnachrichten" + }, + "ok": "Okay", + "dismiss": "Schließen", + "UrlGen": "Bot-Einladungsgenerator", + "Voice": { + "status": { + "badWS": "Schlechte Reaktion vom Web Socket", + "conectionFailed": "Verbindung fehlgeschlagen", + "done": "Verbunden", + "left": "Sprachchat verlassen", + "makingOffer": "Angebot", + "noSDP": "Kein SDP", + "notconnected": "Nicht verbunden", + "sendingStreams": "Senden von Audio-Streams", + "startingRTC": "Start der RTC-Verbindung", + "waitingMainWS": "Warten auf Haupt-Websocket", + "waitingURL": "Warten auf Sprach-URL", + "wsAuth": "Wartend auf die Genehmigung von Websocket", + "wsOpen": "Warten auf die Öffnung von Websocket" } }, - "hideBlockedMessages": "Dieser Benutzer ist blockiert, klicke, um die Nachrichten zu verbergen.", - "showBlockedMessages": "Dieser Benutzer ist blockiert, klicke, um die {{PLURAL:$1| blockierte Nachricht|$1 blockierten Nachrichten}} anzuzeigen.", - "deleteConfirm": "Bist du dir sicher, dass das gelöscht werden soll?", - "yes": "Ja", - "no": "Nein", - "todayAt": "Heute um $1", - "yesterdayAt": "Gestern um $1", - "otherAt": "$1, $2", - "botSettings": "Boteinstellungen", - "uploadPfp": "Profilbild hochladen:", - "uploadBanner": "Banner hochladen:", - "pronouns": "Pronomen:", + "accessibility": { + "gifSettings": { + "always": "Immer", + "hover": "Beim Darüberfahren mit der Maus", + "never": "Niemals" + }, + "name": "Barrierefreiheit", + "playGif": "GIFs abspielen:", + "playIcon": "Animierte Symbole abspielen:", + "roleColors": "Rollenfarben deaktivieren:", + "gradientColors": "Farbverläufe deaktivieren:", + "decorations": "Avatardekorationen aktivieren:" + }, + "accountNotStart": "Benutzerkonto kann nicht gestartet werden", + "add": "Hinzufügen", + "applications": { + "delete": "Anwendung löschen", + "sure": "Sicher, dass $1 gelöscht werden soll?" + }, + "badge": { + "active_developer": "Aktiver Entwickler", + "application_guild_subscription": "Hat Premium", + "automod": "Verwendet AutoMod", + "bot_commands": "Unterstützt Befehle", + "bug_hunter_level_1": "Bug-Hunter Level 1", + "bug_hunter_level_2": "Bug-Hunter Level 2", + "certified_moderator": "Moderator", + "early_supporter": "Früher Unterstützer", + "guild_booster_lvl1": "Server Boost", + "guild_booster_lvl2": "Server Boost", + "guild_booster_lvl3": "Server Boost", + "guild_booster_lvl4": "Server Boost", + "guild_booster_lvl5": "Server Boost", + "guild_booster_lvl6": "Server Boost", + "guild_booster_lvl7": "Server Boost", + "guild_booster_lvl8": "Server Boost", + "guild_booster_lvl9": "Unterstützt den Server seit einer ganzen Weile", + "hypesquad": "Vibesquad [PLATZHALTER]", + "hypesquad_house_1": "Kühnheit", + "hypesquad_house_2": "Exzellenz", + "hypesquad_house_3": "Gleichgewicht", + "legacy_username": "hat einen alten Benutzernamen", + "partner": "Instanzpartner", + "premium": "Premium", + "quest_completed": "hat eine Quest abgeschlossen", + "staff": "Instanz-Staff", + "verified_developer": "Verifizierter Entwickler" + }, "bio": "Bio:", - "profileColor": "Profilfarbe", - "leaveGuild": "Server verlassen", - "noMessages": "Es scheinen keinen Nachrichten vorhanden zu sein, sei der Erste, der etwas sagt!", "blankMessage": "Leere Nachricht", + "blog": { + "blog": "Blog", + "blogUpdates": "Blog-Aktualisierungen:", + "desc": "Lies oder abonniere den Blog, um über Fermi auf dem Laufenden zu bleiben!", + "fermi": "Fermi Blog", + "gotoPost": "Zum Beitrag gehen", + "wantUpdates": "Möchtest du wöchentliche Blog-Updates zum aktuellen Stand von Fermi erhalten?" + }, + "bot": "BOT", + "botGuilds": "Server, denen der Bot angehört:", + "botSettings": "Boteinstellungen", + "cancel": "Abbrechen", "channel": { + "SlowmodeCool": "Abklingzeit im Slowmode: $1", + "TimeOutCool": "Im Timeout bis: $1", + "allowIcons": "Erlaubt benutzerdefinierte Kanal-Icons", + "announcement": "Ankündigungen", "copyId": "Kanal-ID kopieren", - "markRead": "Als gelesen markieren", - "settings": "Einstellungen", + "copyIdCat": "Kategorie-ID kopieren", + "createCatagory": "Kategorie erstellen", + "createChannel": "Kanal erstellen", + "creating": "Kanal wird erstellt", "delete": "Kanal löschen", + "deleteCat": "Kategorie löschen", + "icon": "Symbol:", "makeInvite": "Einladung erstellen", + "markRead": "Als gelesen markieren", + "mute": "Kanal stummschalten", + "name": "Kanal", + "name:": "Kanalname:", + "nsfw:": "NSFW/Warnung vor nicht jugendfreien Inhalten:", + "permissions": "Berechtigungen", + "selectCatName": "Name der Kategorie", + "selectName": "Name des Kanals", + "selectType": "Kanaltyp auswählen", + "settings": "Einstellungen", "settingsFor": "Einstellungen für $1", - "voice": "Sprache", + "slowmode": "Langsamer Modus:", "text": "Text", - "announcement": "Ankündigungen" + "timedOutUntil": "Im Timeout bis: $1", + "topic:": "Thema:", + "typebox": "Nachricht in $1", + "unmute": "Stummschaltung des Kanals aufheben", + "voice": "Sprache", + "deleteThread": "Thema löschen", + "hideThreads": "Nach Inaktivität verstecken", + "forum": "Forum" + }, + "threads": { + "leave": "Thread verlassen", + "join": "Thread beitreten", + "name": "Themenname:", + "editTags": "Tags bearbeiten" + }, + "forum": { + "creorsear": "Erstelle oder suche Posts", + "next": "Weiter", + "back": "Zurück", + "newPost": "Neuen Beitrag erstellen", + "post": "Post", + "sortOptions": { + "sortby": { + "title": "Sortieren nach", + "recent": "Kürzlich aktiv", + "posted": "Veröffentlichungsdatum" + }, + "sortOrder": { + "title": "Sortierungsreihenfolge", + "recent": "Letzte zuerst", + "old": "Ältere zuerst" + }, + "tagMatch": { + "title": "Tag-Übereinstimmungen", + "some": "Einige Tags abgleichen", + "all": "Alle Tags abgleichen" + }, + "name": "Sortierungsoptionen" + }, + "errors": { + "tagsReq": "Tags sind erforderlich", + "requireText": "Die Nachricht darf nicht leer sein" + }, + "allTags": "Alle Tags", + "settings": { + "editTags": "Tags bearbeiten:", + "editTag": "Tag bearbeiten", + "tagName": "Tag-Name:", + "moderated": "Nur Thread-Moderatoren erlauben, Tags anzuwenden:", + "addTag": "Tag hinzufügen", + "delTag": "Tag löschen" + } + }, + "channelLink": "# $1", + "clientDesc": "Client-Version: $1 \n\n[Tritt der offiziellen Fermi-Server bei]($2/invite/USgYJo?instance=https%3A%2F%2Fspacebar.chat)\n\n[Hilf, Fermi zu übersetzen](https://translatewiki.net/wiki/Translating:JankClient#sortable:3=desc) \n\n[Hilf, Fermi zu entwickeln](https://github.com/MathMan05/Fermi)\n\n[Hilf, den Server zu pflegen, auf den Fermi angewiesen ist](https://github.com/spacebarchat/server)\n\nBerechnete Rechte: $3", + "commands": { + "errorNotValid": "$1 ist keine gültige Option für $2", + "required": "$1 ist ein notwendiger Teil dieses Befehls" + }, + "confirmGuildLeave": "Bist du sicher, dass du $1 verlassen willst?", + "copyLink": "Nachrichten-Id kopieren", + "copyRegLink": "Link kopieren", + "copymessageid": "Nachrichten-ID kopieren", + "copyrawtext": "Text kopieren", + "createAccount": "Konto erstellen", + "delete": "Löschen", + "deleteConfirm": "Bist du dir sicher, dass du das löschen willst?", + "devSettings": { + "badUser": "Protokollierung fehlerhafter Benutzerobjekte aktivieren, die zu viele Informationen senden:", + "cache": "Service-Worker-Caching für Kartendateien aktivieren", + "cacheDesc": "Die Kartendateien werden trotzdem geladen, so dass sie nur sichergestellt werden, dass sie im Cache sind, wenn ein neues Update veröffentlicht wird.", + "captureTrace": "Diese Einstellung weist Fermi an, _trace-Eigenschaften vom Server zu erfassen. Durch die Aktivierung dieser Option kann es vorkommen, dass die progressive JSON-Dekodierung nicht funktioniert (möglicherweise ist ein Neuladen erforderlich).", + "clearWellKnowns": ".well-known Einträge erneut abrufen:", + "description": "Diese Hinweise richten sich an die Entwickler von Spacebar/Fermi. Wenn Sie nicht genau wissen, was Sie tun, verändern Sie diese Einstellungen bitte nicht.", + "gatewayComp": "Gateway-Komprimierung deaktivieren:", + "reportSystem": "Experimentelles Meldesystem aktivieren", + "logGateway": "Log-Empfang von Gateway-Ereignisse (Log-Level-Informationen):", + "name": "Entwicklereinstellungen", + "traces": "Spuren aufdecken:" + }, + "deviceManage": { + "city": "Stadt: $1", + "continent": "Kontinent: $1", + "country": "Land: $1", + "curSes": "Das ist die aktuelle Sitzung, Sie müssen sich über das andere Menü abmelden.", + "estimateWarn": "Warnung: Alle diese Informationen sind lediglich Schätzungen und könnten fehlerhaft sein.", + "ip": "Letzte bekannte IP: $1", + "last": "Ungefähr zuletzt verwendet: $1", + "latitude": "Breitengrad: $1", + "logout": "Abmelden", + "longitude": "Längengrad: $1", + "manageDev": "Gerät verwalten", + "postal": "Porto: $1", + "region": "Region: $1", + "title": "Sitzungen verwalten" + }, + "discovery": "Entdecken", + "dms": "Direktnachrichten", + "edit": "Bearbeiten", + "emoji": { + "confirmDel": "Sicher, dass dieser Emoji gelöscht werden soll?", + "found": { + "not": "Dieses Emoji ist nicht von einem deiner Server, du musst diesem Server erst beitreten, um dieses Emoji verwenden zu können.", + "other": "Dieses Emoji ist von einem deiner anderen Server.", + "private": "Der Server für dieses Emoji konnte nicht gefunden werden", + "this": "Dieses Emoji ist von diesem Server." + }, + "from": "Dieses Emoji ist von", + "image:": "Bild:", + "join": "Server beitreten", + "name:": "Name:", + "privateGuild": "Dieser Server ist privat.", + "title": "Emojis", + "upload": "Emojis hochladen" + }, + "emojiSelect": "Emoji:", + "errorReconnect": "Verbindung zum Server konnte nicht hergestellt werden. Neuer Versuch in **$1** Sekunden …", + "favoriteGifs": "Favorisierte GIFs", + "folder": { + "color": "Ordnerfarbe:", + "create": "Neuen Ordner erstellen", + "edit": "Ordner bearbeiten", + "name": "Ordnername:" + }, + "form": { + "captcha": "Warte, bist du ein Mensch?" + }, + "friends": { + "addfriend": "Freund hinzufügen", + "addfriendpromt": "Freunde per Benutzernamen hinzufügen:", + "all": "Alle", + "all:": "Alle Freunde:", + "blocked": "Geblockt", + "blockedusers": "Geblockte Benutzer:", + "bu": "Blockierter Benutzer", + "discnotfound": "Diskriminator nicht gefunden", + "friendlist": "Freundesliste", + "friends": "Freunde", + "notfound": "Benutzer nicht gefunden", + "online": "Online", + "online:": "Freunde online:", + "pending": "Ausstehend", + "pending:": "Ausstehende Freundschaftsanfragen:", + "removeFriend": "Freund entfernen" + }, + "goThere": "Ja, los geht’s", + "goThereTrust": "Ja und vertraue dieser Domain von nun an", + "goingToURL": "Dieser Hyperlink führt dich zu $1. Möchtest du wirklich dorthin gehen?", + "group": { + "createdm": "Direktnachricht schreiben!", + "edit": "Gruppenchat bearbeiten", + "select": "Freunde auswählen" }, - "switchAccounts": "Konto wechseln ⇌", + "guild": { + "COMMUNITY": "Beitritt beantragen", + "disableInvites": "Einladungen deaktivieren:", + "DISCOVERABLE": "Entdecken", + "INVITES_DISABLED": "Nur auf Einladung", + "adminMenu": { + "changePerms": "Ändern Sie die Berechtigungen zum Suchen", + "finding": "Administratoren finden", + "ownName": "Besitzer", + "owner": "Finde den Besitzer", + "permission": "Berechtigungen:" + }, + "report": "Server melden", + "admins": "Administratoren finden", + "all": "Alle", + "banReason": "Begründung des Banns: $1", + "bannedBy": "Gebannt durch:", + "banner:": "Banner:", + "bans": "Banns", + "boostMessage?": "Sende eine Nachricht, wenn jemand Ihre Gilde boostet!", + "community": "Community", + "confirmDelete": "Sicher, dass $1 gelöscht werden soll?", + "confirmLeave": "Bist du dir sicher, dass du den Server verlassen möchtest?", + "copyId": "Server-ID kopieren", + "create": "Server von Grund auf erstellen", + "createFromTemplate": "Server aus Vorlage erstellen", + "createNewTemplate": "Servervorlage erstellen", + "creating": "Erstelle Server", + "default": "Standard ($1)", + "defaultNoti": "Standard-Benachrichtigung des Server einrichten.", + "delete": "Server löschen", + "description:": "Beschreibung:", + "disoveryTitle": "Server entdecken ($1 {{PLURAL:$1|entry|entries}})", + "editingTemplate": "Bearbeite $1", + "emptytext": "Wie seltsam, diese Gilde hat keine Kanäle!?", + "emptytitle": "Seltsamer Ort", + "guilds": "Server", + "helpTips?": "Sende hilfreiche Tipps zu deinem Server.", + "howJoin": "Wie können Leute deinem Server beitreten?", + "icon:": "Symbol:", + "invites": "Einladungen", + "joinConfirm": "Beitreten $1?", + "leave": "Server verlassen", + "loadingDiscovery": "Lade...", + "makeInvite": "Einladung erstellen", + "markRead": "Als gelesen markieren", + "memberCount": "$1 Mitglied{{PLURAL:$1||er}}", + "mute": "Server stummschalten", + "name:": "Name:", + "nameNoMatch": "Die Namen stimmen nicht überein", + "noDelete": "Macht nichts", + "noLeave": "Macht nichts", + "none": "Keine", + "notifications": "Benachrichtigungen", + "onlyMentions": "Nur @Erwähnungen", + "overview": "Übersicht", + "region:": "Region:", + "roles": "Rollen", + "ruleId": "Regelkanal:", + "selectnoti": "Benachrichtigungstyp auswählen:", + "sendrandomwelcome?": "Sende eine zufällige Nachricht, wenn jemand diesem Server beitritt:", + "serverName": "Name des Server:", + "settings": "Einstellungen", + "settingsFor": "Einstellungen für $1", + "splash": "Entdeckungs-Tour:", + "stickWelcomeReact?": "Mitglieder ihres Server auffordern, mit einem Sticker zu reagieren, wenn jemand beitritt!", + "systemSelect:": "Kanal für Systemnachrichten:", + "tempCreatedBy": "Vorlage erstellt von:", + "tempUseCount": "Die Vorlage wurde $1-mal{{PLURAL:$1}} verwendet", + "template": "Link/Code zum Erstellen eines Servers aus einer Vorlage:", + "templateDesc": "Beschreibung der Vorlage:", + "templateName": "Name der Vorlage:", + "templateNameShort": "Der Name der Vorlage muss mindestens 2 Zeichen lang sein", + "templateURL": "Vorlagenlink: $1", + "templates": "Vorlagen", + "templcateMetaDesc": "Eine Vorlage ermöglicht es anderen, diesen Server als Basis für ihre eigenen Server zu verwenden: Sie kopiert die Kanäle, Rollen und Einstellungen dieses Server, aber nicht die Nachrichten innerhalb des Server, die Bots oder das Server-Icon.", + "topic:": "Thema:", + "unknownRole": "@unknown-role", + "viewTemplate": "Vorlage ansehen", + "yesDelete": "Ja, ich bin mir sicher", + "yesLeave": "Ja, ich bin mir sicher" + }, + "hideBlockedMessages": "Dieser Benutzer ist blockiert, klicke, um die Nachrichten zu verbergen.", "home": { + "icon": "$1 Instanzsymbol", "uptimeStats": "Uptime: \n Insgesamt: $1%\nDiese Woche: $2%\nHeute: $3%", "warnOffiline": "Instanz ist offline, Verbindung kann nicht hergestellt werden" }, "htmlPages": { - "loadingText": "Jank Client wird geladen", + "addBot": "Zum Server hinzufügen", + "alreadyHave": "Hast du bereits ein Benutzerkonto?", + "box1Items": "Direktnachrichten|Reaktionen auf Nachrichten|Einladungen|Benutzerkontowechsel|Benutzereinstellungen|Entwicklerportal|Bot-Einladungen|Übersetzungen", + "box1title": "Fermi ist ein Spacebar-kompatibler Client, welcher versucht, so gut wie möglich zu sein, durch viele Funktionen wie:", + "box3description": "Wir freuen uns immer über Hilfe, sei es in Form von Fehlerberichten, Programmierung oder einfach nur das Hinweisen auf Tippfehler.", + "box3title": "Unterstützung bei der Entwicklung von Fermi", + "compatableInstances": "Einige Spacebar-Instanzen:", + "createAccount": "Benutzerkonto erstellen", + "dobField": "Geburtsdatum:", + "emailField": "E-Mail-Adresse:", + "idpermissions": "Dies berechtigt den Bot zu:", + "instanceField": "Instanz:", "loaddesc": "Das sollte nicht lange dauern", + "loadingText": "Fermi lädt", + "loginButton": "Anmelden", + "noAccount": "Du hast noch kein Benutzerkonto?", "openClient": "Client öffnen", - "welcomeJank": "Willkommen bei Jank Client", - "box1title": "Jank Client ist ein Spacebar-kompatibler Client, welcher versucht so gut wie möglich sein durch einige Funktionen wie:", - "box1Items": "Direktnachrichten|Reaktionen auf Nachrichten|Einladungen|Kontowechsel|Benutzereinstellungen|Entwicklerportal|Bot-Einladungen|Übersetzungen", - "compatableInstances": "Einige Spacebar-Instanzen:", - "box3title": "Unterstützung in der Entwicklung von Jank Client", - "box3description": "Wir freuen uns immer über Hilfe, sei es in Form von Fehlerberichten, Programmierung oder einfach nur das Hinweisen auf Tippfehler." + "pw2Field": "Passwort erneut eingeben:", + "pwField": "Passwort:", + "switchaccounts": "Benutzerkonto wechseln", + "trans": "Übersetzen", + "transDesc": "Hilf Fermi in deine eigene Sprache zu übersetzen!", + "transTitle": "Übersetze Fermi", + "userField": "Benutzername:", + "welcomeJank": "Willkommen bei Fermi" }, - "register": { - "passwordError:": "Passwort: $1", - "usernameError": "Benutzername: $1", - "emailError": "E-Mail-Adresse: $1", - "DOBError": "Geburtsdatum: $1", - "agreeTOS": "Ich stimme den [Nutzungsbedingungen]($1) zu:", - "noTOS": "Diese Instanz hat keine Nutzungsbedingungen, akzeptiere die ToS trotzdem:" + "incorrectURLS": "## Diese Instanz hat wahrscheinlich die falschen Links gesendet.\n### Wenn Du der Instanzbesitzer sind, lies [hier](https://docs.spacebar.chat/setup/server/) unter *Verbindung von Remotecomputern herstellen* nach, um das Problem zu beheben.\nSoll Fermi automatisch versuchen, diesen Fehler zu beheben, damit Du eine Verbindung zur Instanz herstellen kannst?", + "instInfo": "Instanzinformation", + "instanceInfo": { + "contact": "E-Mail-Instanz-Admins", + "frontPage": "[Front Page]($1)", + "name": "Instanzinformationen", + "publicUrl": "[Öffentlicher Link]($1)", + "tosPage": "[TOS Page]($1)" + }, + "instanceStats": { + "members": "Mitglieder: $1", + "messages": "Nachrichten: $1", + "name": "Instanzstatistik: $1", + "servers": "Server: $1", + "users": "Registrierte Benutzer: $1" + }, + "interactions": { + "ephemeralDismiss": "Nachricht ablehnen", + "failed": "Interaktion fehlgeschlagen", + "nonsence": "Der Server hat eine Komponente des Typs $1 gesendet, die nicht existiert", + "notImpl": "Fermi hat den Komponententyp $1 noch nicht implementiert.", + "onlyYou": "Nur du kannst diese Nachricht sehen.", + "replyline": "Interaktion gestartet", + "started": "Interaktion wird verarbeitet...", + "worked": "Die Interaktion war erfolgreich." + }, + "invite": { + "accept": "Akzeptieren", + "alreadyJoined": "Bereits beigetreten", + "channel:": "Kanal:", + "createInvite": "Einladung erstellen", + "createdAt": "Erstellt am $1", + "expireAfter": "Gültig bis:", + "expires": "Läuft ab: $1", + "forChannel": "Für Kanal: $1", + "inviteLinkCode": "Einladungslink/-code", + "inviteMaker": "Einladungsgenerator", + "invitedBy": "Du wurdest von $1 eingeladen", + "joinUsing": "Tritt einem Server über eine Einladung bei", + "loginOrCreateAccount": "Anmelden oder Benutzerkonto erstellen ⇌", + "longInvitedBy": "$1 hat Dich eingeladen, $2 beizutreten", + "maxUses": "Max. Verwendungen: $1", + "never": "Niemals", + "noAccount": "Erstelle ein Benutzerkonto, um die Einladung anzunehmen", + "notFound": "Einladung nicht gefunden", + "subtext": "zu $1 in $2", + "used": "Einmal{{PLURAL:$1||$1-mal}} verwendet:" + }, + "inviteOptions": { + "12h": "12 Stunden", + "1d": "1 Tag", + "1h": "1 Stunde", + "30d": "30 Tage", + "30m": "30 Minuten", + "6h": "6 Stunden", + "7d": "7 Tage", + "limit": "$1 Verwendung{{PLURAL:$1||Verwendungen}}", + "never": "Niemals", + "noLimit": "Unbegrenzt", + "title": "Personen einladen" }, + "jankInfo": "Client-Information", + "leaveGuild": "Server verlassen", "leaving": "Du verlässt Spacebar", - "goingToURL": "Du wirst zu $1 gehen. Bist du sicher, dass du dorthin gehen willst?", - "goThere": "Ja", - "goThereTrust": "Ja, vertraue auch in der Zukunft", + "loaded": "Geladen", + "localuser": { + "2faCode:": "Code:", + "2faCodeGive": "Ihr TOTP-Geheimnis lautet: $1. Damit wird ein 6-stelliger 2FA-Token mit einer Gültigkeitsdauer von 30 Sekunden generiert.", + "2faDisable": "2FA deaktivieren", + "2faEnable": "2FA aktivieren", + "CheckUpdate": "Nach Updates suchen", + "PasswordsNoMatch": "Passwörter stimmen nicht überein", + "TOSURL": "Link zu den Nutzungsbedingungen:", + "VoiceWarning": "Diese Funktion wirklich aktivieren? Sie ist sehr experimentell und verursacht wahrscheinlich Probleme. (Diese Funktion ist für Entwickler. Bitte nicht aktivieren, wenn Du nicht weißt, was Du tust.)", + "accentColor": "Akzentfarbe des Designs:", + "accountSettings": "Kontoeinstellungen", + "addBot": "Bot hinzufügen", + "addStatus": "Status hinzufügen", + "advancedBot": "Erweiterte Bot-Einstellungen", + "appName": "Anwendungsname:", + "areYouSureDelete": "Das Benutzerkonto wirklich löschen? Wenn ja, dann bitte den Satz $1 eingeben.", + "badCode": "Ungültiger Code", + "badPassword": "Falsches Passwort", + "botAvatar": "Bot-Avatar:", + "botInviteCreate": "Bot-Einladungsgenerator", + "botUsername": "Bot-Benutzername:", + "changeDiscriminator": "Diskriminator ändern", + "changeEmail": "E-Mail-Adresse ändern", + "changePassword": "Passwort ändern", + "changeUsername": "Benutzernamen ändern", + "clearCache": "Cache leeren", + "confirmAddBot": "Dieser Anwendung wirklich einen Bot hinzufügen? Dies ist nicht umkehrbar.", + "confirmReset": "Den Bot-Token wirklich zurücksetzen? Der Bot wird dann nicht mehr funktionieren, bis er aktualisiert wird.", + "confuseNoBot": "Aus irgendeinem Grund verfügt diese Anwendung (noch) nicht über einen Bot.", + "connections": "Verbindungen", + "createApp": "Anwendung erstellen", + "customSound": "Benutzerdefinierter Sound", + "customStatusWarn": "Spacebar unterstützt derzeit nicht die Anzeige von einem benutzerdefinierten Status. Der Status wird zwar akzeptiert, aber nicht weiter verarbeitet.", + "deleteAccount": "Löschung des Kontos", + "deleteAccountButton": "Benutzerkonto löschen", + "description": "Beschreibung:", + "devPortal": "Entwicklerportal", + "disableConnection": "Diese Verbindung wurde serverseitig deaktiviert", + "editingBot": "Bearbeiten des Bots $1", + "install": "Installieren", + "installDesc": "Die Installation ermöglicht, Fermi in einem eigenen Fenster zu öffnen und es wie eine eigene App zu nutzen! Fermi kann aber auch einfach wie bisher im Webbrowser verwendet werden, es funktioniert dort genauso.", + "installJank": "Fermi installieren", + "language": "Sprache:", + "manageBot": "Bot verwalten", + "manageInstance": "Instanz verwalten", + "mustTypePhrase": "Um das Benutzerkonto zu löschen, bitte den Satz eingeben", + "newDiscriminator": "Neuer Diskriminator:", + "newEmail:": "Neue E-Mail-Adresse", + "newPassword:": "Neues Passwort:", + "newUsername": "Neuer Benutzername:", + "noToken": "Kenne das Token nicht, kann es also leider nicht lokal speichern", + "noUpdates": "Keine Aktualisierungen gefunden", + "notisound": "Benachrichtigungston:", + "oldPassword:": "Altes Passwort:", + "password:": "Passwort", + "privacyPolcyURL": "Datenschutzerklärung:", + "appID": "Anwendungs-ID: $1", + "showSecret": "Client-Geheimnis anzeigen", + "clientSecret": "Client-Geheimnis: $1", + "secret": "Secret", + "publicAvaliable": "Bot öffentlich einladbar machen?", + "refreshPage": "Aktualisieren, um die Anwendung zu übernehmen", + "requireCode": "Ist eine Code-Bewilligung erforderlich, um den Bot einzuladen?", + "resetToken": "Token zurücksetzen", + "saveToken": "Token im lokalen Speicher speichern", + "setUp2fa": "2FA-Einrichtung", + "setUp2faInstruction": "Kopiere dieses Geheimnis in deine TOTP-App (zeitbasiertes Einmalpasswort), um deine 2FA-Anmeldecodes zu generieren", + "settings": "Einstellungen", + "sillyDeleteConfirmPhrase": "Shrek ist Liebe, Shrek ist Leben", + "soundTooLarge": "Die hochgeladene Sound-Datei war zu groß. Versuche es erneut.", + "status": "Status", + "team:": "Team:", + "theme:": "Farbschema", + "themesAndSounds": "Farbschema & Sounds", + "tokenDisplay": "Token: $1", + "trace": "Spuren", + "trusted": "Vertrauenswürdige Domains", + "trustedDesc": "Wenn du auf Links klickest, die dich zu dieser Domain führen, wirst du ***nicht*** wie bei anderen Links zur Bestätigung aufgefordert. Erlaube dies nur für Domains, denen du vertraust, wie z. B. „https://fermi.chat“.", + "updateSettings": "Einstellungen aktualisieren", + "updatesYay": "Aktualisierungen gefunden!", + "userSettings": "Benutzereinstellungen" + }, + "login": { + "allGood": "Alles gut", + "checking": "Überprüfe Instanz", + "enterPAgain": "Passwort erneut eingeben:", + "invalid": "Ungültige Instanz, bitte versuche es erneut", + "login": "Anmeldung", + "newPassword": "Neues Passwort:", + "pasteInfo": "Füge den Wiederherstellungslink hier ein:", + "recover": "Passwort vergessen?", + "recovery": "Passwort vergessen", + "waiting": "Warte auf die Überprüfung der Instanz" + }, + "logout": { + "error": { + "cancel": "Abbrechen", + "cont": "Trotzdem fortfahren", + "desc": "Fermi konnte Sie nicht abmelden\n Möchten Sie das Konto trotzdem entfernen?", + "title": "Es gab einen Fehler bei der Abmeldung." + }, + "logout": "Abmelden" + }, + "manageInstance": { + "AreYouSureStop": "Sicher, dass diese Instanz gestoppt werden soll?", + "TokenFormats": { + "JSON": "JSON-formatiert", + "URLs": "Einladungslink", + "plain": "Einfach" + }, + "clientURL": "Client-URL:", + "copy": "Kopieren", + "count": "Anzahl:", + "create": "Erstellen", + "createTokens": "Registrierungstoken erstellen", + "format": "Format:", + "genericType": "Generisch", + "length": "Länge:", + "regType": "Typ des Tokenlinks registrieren", + "stop": "Instanz stoppen" + }, + "media": { + "artist": "Künstler: $1", + "composer": "Komponist: $1", + "download": "Medien herunterladen", + "length": "Länge: $1 Minuten und $2 Sekunden", + "loading": "Lädt", + "moreInfo": "Weitere Infos", + "notFound": "Medien konnten nicht gefunden werden" + }, + "member": { + "ban": "$1 aus $2 bannen", + "kick": "$1 aus $2 rauswerfen", + "nick:": "Nickname:", + "persist": "Nur Benutzer löschen:", + "reason:": "Grund:", + "timeout": "Timeout $1", + "timeoutTime": "Dauer der Auszeit:" + }, + "message": { + "messages": "$1 Nachricht{{PLURAL:$1||Nachrichten}} >", + "andMore": "$1, und mehr!", + "attached": "hat einen Anhang gesendet", + "delete": "Nachricht löschen", + "report": "Beitrag melden", + "deleted": "Gelöschte Nachricht", + "edit": "Nachricht bearbeiten", + "edited": "(bearbeitet)", + "fullMessage": "Vollständige Nachricht:", + "pin": "$1 hat eine Nachricht angeheftet", + "thread": "$1 hat einen neuen Thread erstellt: $2", + "reactedWith": "Reagiert mit $1", + "reactionAdd": "Reaktion hinzufügen", + "createThread": "Thread erstellen", + "threadOptions": "Thread-Optionen", + "reactions": "Reaktionen ansehen", + "reactionsTitle": "Reaktionen", + "retry": "Fehlerhafte Nachricht erneut senden", + "viewrest": "Rest anzeigen" + }, + "report": { + "back": "Zurück", + "next": "nächster", + "cancel": "Abbrechen", + "submit": "Speichern", + "blockUser": "Benutzer sperren", + "timeout": "Benutzer timeouten", + "summary": "Zusammenfassung der Meldung:" + }, + "messageLink": "Nachricht in $1", + "muteDuration": "Stummschaltung aufheben in:", "nevermind": "Nein", - "guild": { - "confirmLeave": "Bist du dir sicher, dass du den Server verlassen möchtest?", - "yesLeave": "Ja, ich bin mir sicher", - "noLeave": "Doch nicht", - "serverName": "Name des Servers:", - "yesDelete": "Ja, ich bin mir sicher", - "noDelete": "Doch nicht", - "loadingDiscovery": "Lade..." + "no": "Nein", + "noEmojiFont": "System-Emoji verwenden", + "noMessages": "Keine Nachrichten vorhanden, sei der Erste, der etwas sagt!", + "noPins": "Hier gibt es nichts für dich... noch nicht.", + "noStickers": "Das ist schade, es gibt keine Sticker, die du nutzen kannst!", + "notiClump": "Sie haben $1 Benachrichtigungen von $2 erhalten.", + "notiVolume": "Benachrichtigungslautstärke:", + "otherAt": "$1, $2", + "permissions": { + "descriptions": { + "ADMINISTRATOR": "Gewährt Rollenmitgliedern ALLE unten aufgeführten Berechtigungen und umgeht ALLE Berechtigungsüberschreibungen. Nur DU und Mitglieder/Bots, denen du absolut vertraust, sollten diese GEFÄHRLICHE Berechtigung haben!!", + "MANAGE_GUILD": "Ermöglicht Rollenmitgliedern, den Server zu verwalten und zu bearbeiten.", + "VIEW_AUDIT_LOG": "Erlaubt Rollenmitgliedern die Einsicht des Audit-Logs", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Ermöglicht Rollenmitgliedern das Anzeigen von Einblicken in Rollenabonnements", + "VIEW_GUILD_INSIGHTS": "Erlaubt Rollenmitgliedern die Einsicht von Servereinblicken", + "BYPASS_SLOWMODE": "Der Slowmode hat keine Auswirkungen mehr auf Rollenmitglieder.", + "MENTION_EVERYONE": "Erlaubt Rollenmitgliedern die Erwähnung von @everyone", + "MANAGE_NICKNAMES": "Erlaubt Rollenmitgliedern, die Nicknamen von anderen Nutzern zu verwalten.", + "PIN_MESSAGES": "Ermöglicht es Rollenmitgliedern, Nachrichten anzuheften.", + "SEND_POLLS": "Erlaubt Rollenmitgliedern, Umfragen zu senden.", + "MANAGE_MESSAGES": "Erlaubt Rollenmitgliedern das Löschen von Nachrichten anderer Nutzer.", + "MANAGE_THREADS": "Erlaubt es Rollenmitgliedern, Threads zu löschen, zu archivieren sowie alle privaten Threads anzuzeigen.", + "MANAGE_CHANNELS": "Erlaubt Rollenmitgliedern, Kanäle zu verwalten und zu editieren.", + "MANAGE_ROLES": "Erlaubt Rollenmitgliedern, die Serverrollen zu verwalten und zu editieren.", + "MANAGE_WEBHOOKS": "Erlaubt Rollenmitgliedern, Webhooks zu verwalten und zu editieren.", + "CREATE_EVENTS": "Erlaubt Rollenmitgliedern die Erstellung von geplanten Ereignissen.", + "MANAGE_EVENTS": "Ermöglicht Rollenmitgliedern das Bearbeiten/Löschen von Ereignissen (bestehende und zukünftige).", + "CREATE_GUILD_EXPRESSIONS": "Ermöglicht Rollenmitgliedern das Erstellen von Ausdrücken (Emojis, Sticker und Soundboard-Sounds).", + "MANAGE_GUILD_EXPRESSIONS": "Ermöglicht Rollenmitgliedern das Bearbeiten/Löschen von Ausdrücken (Emojis, Sticker und Soundboard-Sounds).", + "MODERATE_MEMBERS": "Ermöglicht es Rollenmitgliedern, Gildenmitglieder zu timeouten (Das hindert sie daran, Nachrichten im Chat/in Threads zu senden, darauf zu reagieren und in Sprach-/Bühnenkanälen zu sprechen).", + "KICK_MEMBERS": "Erlaubt Rollenmitgliedern, Mitglieder vom Server zu werfen.", + "BAN_MEMBERS": "Erlaubt Rollenmitgliedern, Mitglieder vom Server zu bannen.", + "MOVE_MEMBERS": "Erlaubt Rollenmitgliedern, andere Nutzer zwischen Sprachkanälen zu verschieben", + "MUTE_MEMBERS": "Erlaubt Rollenmitgliedern, andere Nutzer in Sprachkanälen stummzuschalten.", + "DEAFEN_MEMBERS": "Ermöglicht es Rollenmitgliedern, den Sound für andere Mitglieder in Sprachkanälen zu deaktivieren (Deaktivieren des Sounds eines Mitglieds schaltet dieses automatisch auch stumm).", + "CHANGE_NICKNAME": "Erlaubt Rollenmitgliedern, ihrer eigenen Nicknamen zu ändern.", + "VIEW_CHANNEL": "Ermöglicht Rollenmitgliedern das Anzeigen von Textkanälen.", + "READ_MESSAGE_HISTORY": "Ermöglicht Rollenmitgliedern das Lesen des Nachrichtenverlaufs von Textkanälen.", + "ADD_REACTIONS": "Ermöglicht Rollenmitgliedern, Reaktionen zu Nachrichten in Textkanälen hinzuzufügen.", + "SEND_MESSAGES": "Ermöglicht Rollenmitgliedern das Senden von Nachrichten in Textkanälen.", + "SEND_MESSAGES_IN_THREADS": "Ermöglicht Rollenmitgliedern das Senden von Nachrichten in Threads.", + "SEND_TTS_MESSAGES": "Erlaubt Rollenmitgliedern das Senden von Text-zu-Sprache Nachrichten in Textkanälen.", + "USE_EXTERNAL_EMOJIS": "Erlaubt Rollenmitgliedern die Benutzung von externen Emojis in Textkanälen.", + "USE_EXTERNAL_STICKERS": "Ermöglicht Rollenmitgliedern, externe Sticker in Textkanälen zu verwenden.", + "EMBED_LINKS": "Erlauben, dass von Rollenmitgliedern gesendete Links automatisch in Textkanäle eingebettet werden.", + "ATTACH_FILES": "Erlaubt Rollenmitgliedern den Anhang von Dateien in Textkanälen.", + "SEND_VOICE_MESSAGES": "Ermöglicht das Senden von Sprachnachrichten in Textkanälen.", + "CREATE_INSTANT_INVITE": "Erlaubt Rollenmitgliedern, Einladungen für den Server zu erstellen.", + "CREATE_PUBLIC_THREADS": "Erlaubt Rollenmitgliedern, öffentliche Threads zu erstellen.", + "CREATE_PRIVATE_THREADS": "Erlaubt Rollenmitgliedern, private Threads zu erstellen.", + "CONNECT": "Erlaubt Rollenmitgliedern die Verbindung zu Sprachkanälen.", + "SPEAK": "Erlaubt Rollenmitgliedern das Sprechen in Sprachkanälen.", + "USE_VAD": "Ermöglicht es Rollenmitgliedern, in Sprachkanälen zu sprechen, indem sie einfach sprechen (wenn diese Funktion deaktiviert ist, können sie nur über die Push-to-Talk-Taste sprechen).", + "STREAM": "Erlaubt Rollenmitgliedern, in Sprachkanälen zu streamen.", + "USE_SOUNDBOARD": "Erlaubt Rollenmitgliedern die Nutzung eines Soundboards in einem Sprachkanal.", + "USE_EXTERNAL_SOUNDS": "Ermöglicht Rollenmitgliedern die Verwendung benutzerdefinierter Soundboard-Sounds von anderen Servern.", + "PRIORITY_SPEAKER": "Erlaubt Rollenmitgliedern, ein Prioritätssprecher in Sprachkanälen zu sein.", + "REQUEST_TO_SPEAK": "Erlaubt Rollenmitgliedern, in einem Bühnenkanal das Rederecht zu erbitten.", + "USE_EMBEDDED_ACTIVITIES": "Erlaubt Rollenmitgliedern, eingebettete Aktivitäten zu nutzen.", + "USE_APPLICATION_COMMANDS": "Ermöglicht Rollenmitgliedern die Verwendung von Anwendungsbefehlen.", + "USE_EXTERNAL_APPS": "Ermöglicht es Rollenmitgliedern, Anwendungsantworten öffentlich im Kanal anzuzeigen (wenn diese Option deaktiviert ist, können Benutzer ihre Apps weiterhin verwenden, aber die Antworten sind nur für sie selbst sichtbar. Dies gilt nur für Apps, die nicht auch auf dem Server installiert sind)." + }, + "readableNames": { + "ADD_REACTIONS": "Reaktionen hinzufügen", + "ADMINISTRATOR": "Administrator", + "ATTACH_FILES": "Dateien anhängen", + "BAN_MEMBERS": "Mitglieder bannen", + "BYPASS_SLOWMODE": "Langsammodus umgehen", + "CHANGE_NICKNAME": "Nickname ändern", + "CONNECT": "Verbunden", + "CREATE_EVENTS": "Events erstellen", + "CREATE_GUILD_EXPRESSIONS": "Erstellen von Ausdrücken", + "CREATE_INSTANT_INVITE": "Einladung erstellen", + "CREATE_PRIVATE_THREADS": "Private Threads erstellen", + "CREATE_PUBLIC_THREADS": "Öffentliche Threads erstellen", + "DEAFEN_MEMBERS": "Mitglieder stumm schalten", + "EMBED_LINKS": "Links einbetten", + "KICK_MEMBERS": "Mitglieder rauswerfen", + "MANAGE_CHANNELS": "Vorlagen verwalten", + "MANAGE_EVENTS": "Events verwalten", + "MANAGE_GUILD": "Server verwalten", + "MANAGE_GUILD_EXPRESSIONS": "Ausdrücke verwalten", + "MANAGE_MESSAGES": "Nachrichten verwalten", + "MANAGE_NICKNAMES": "Nicknames verwalten", + "MANAGE_ROLES": "Rollen verwalten", + "MANAGE_THREADS": "Threads verwalten", + "MANAGE_WEBHOOKS": "Webhooks verwalten", + "MENTION_EVERYONE": "Erwähne @everyone, @here und alle Rollen", + "MODERATE_MEMBERS": "Timeout für Mitglieder", + "MOVE_MEMBERS": "Mitglieder verschieben", + "MUTE_MEMBERS": "Mitglieder stummschalten", + "PIN_MESSAGES": "Nachrichten anpinnen", + "PRIORITY_SPEAKER": "Prioritätsredner", + "READ_MESSAGE_HISTORY": "Nachrichtenverlauf lesen", + "REQUEST_TO_SPEAK": "Anfrage, sprechen zu dürfen", + "SEND_MESSAGES": "Nachricht senden", + "SEND_MESSAGES_IN_THREADS": "Senden von Nachrichten in Threads", + "SEND_POLLS": "Umfragen erstellen", + "SEND_TTS_MESSAGES": "Text-zu-Sprache-Nachrichten senden", + "SEND_VOICE_MESSAGES": "Sprachnachrichten senden", + "SPEAK": "Bergspitze", + "STREAM": "Video", + "USE_APPLICATION_COMMANDS": "Anwendungsbefehle verwenden", + "USE_EMBEDDED_ACTIVITIES": "Aktivitäten verwenden", + "USE_EXTERNAL_APPS": "Externe Apps verwenden", + "USE_EXTERNAL_EMOJIS": "Externe Emojis verwenden", + "USE_EXTERNAL_SOUNDS": "Externe Sounds verwenden", + "USE_EXTERNAL_STICKERS": "Externe Sticker verwenden", + "USE_SOUNDBOARD": "Soundboard verwenden", + "USE_VAD": "Sprachaktivitätserkennung verwenden", + "VIEW_AUDIT_LOG": "Audit-Protokoll anzeigen", + "VIEW_CHANNEL": "Änderungen zeigen", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Monetarisierungsanalytics für Ersteller anzeigen", + "VIEW_GUILD_INSIGHTS": "Server-Statistiken" + } + }, + "pinMessage": "Nachricht anheften", + "profile": { + "bio": "Über mich:", + "joined": "Konto erstellt: $1", + "joinedMember": "Beitritt $1 : $2", + "mut": "Gemeinsame Server", + "mutFriends": "Gemeinsame Freunde", + "permInfo": "Berechtigungen", + "userInfo": "Benutzerinformationen" }, + "profileColor": "Profilfarbe", + "pronouns": "Pronomen:", + "readableName": "Deutsch", + "recentEmoji": "Letzte Emojis", + "register": { + "DOBError": "Geburtsdatum: $1", + "agreeTOS": "Ich stimme den [Nutzungsbedingungen]($1) zu:", + "emailError": "E-Mail-Adresse: $1", + "noTOS": "Diese Instanz hat keine Nutzungsbedingungen, akzeptiere die ToS trotzdem:", + "passwordError:": "Passwort: $1", + "register": "Registrieren", + "tos": "Du musst den Nutzungsbedingungen zustimmen", + "usernameError": "Benutzername: $1" + }, + "remove": "Entfernen", + "renderJoinAvatars": "Anzeigen von Avataren für neue Mitglieder:", + "reply": "Antworten", + "replyingTo": "Antworte auf $1", + "retrying": "Erneuter Versuch...", "role": { - "color": "Farbe", + "color": "Erste Farbe", + "colors": { + "name": "Farbschema:", + "one": "Einfarbig", + "two": "Zweifarbig", + "three": "Dreifarbig", + "secondColor": "Zweite Farbe:", + "thirdColor": "Dritte Farbe:" + }, + "confirmDelete": "Sicher, dass $1 gelöscht werden soll?", + "copyId": "Rollen-ID kopieren", + "delete": "Rolle löschen", + "deleting": "Rolle wird gelöscht...", + "displaySettings": "Anzeigeeinstellungen", + "hoisted": "Zeige Rollenmitglieder getrennt von Online-Mitgliedern an (sie werden anhand der höchsten Rollengruppe, zu der sie gehören, angezeigt):", + "mentionable": "Erlaube jedem, diese Rolle anzupingen:", + "name": "Rollenname:", + "perms": "Berechtigungen", "remove": "Rolle entfernen", - "delete": "Rolle löschen" + "roleEmoji": "Rollen-Emoji:", + "roleFileIcon": "Rollensymbol:", + "roles": "Rollen" }, - "localuser": { - "privacyPolcyURL": "Datenschutzerklärung:", - "TOSURL": "Nutzungsbedingungen:" - } + "search": { + "back": "Zurück", + "new": "Neu", + "next": "Weiter", + "nofind": "Es scheint keine Nachrichten zu geben, welche der Suche entsprechen. Versuche vielleicht, die Suche zu erweitern, um das Gewünschte zu finden.", + "old": "Alt", + "page": "Seite $1", + "search": "Suche" + }, + "searchGifs": "Suche $1", + "settings": { + "clear": "Reinigen", + "img": "Bild hochladen", + "save": "Änderungen speichern", + "unsaved": "Vorsicht, du hast nicht gespeicherte Änderungen", + "updates": { + "serviceWorkerMode": { + "disabled": "Aus", + "enabled": "An", + "offlineOnly": "Nur offline", + "title": "Service-Worker-Einstellung", + "unregistered": "Aus (einschließlich Mediencache)" + } + } + }, + "showBlockedMessages": "Dieser Benutzer ist blockiert, klicke, um die {{PLURAL:$1| blockierte Nachricht|$1 blockierten Nachrichten}} anzuzeigen.", + "spoiler": "Spoiler", + "sticker": { + "confirmDel": "Sicher, dass dieser Sticker gelöscht werden soll?", + "del": "Sticker löschen", + "desc": "Beschreibung", + "errEmjMust": "Du musst ein Emoji mit deinem Sticker kombinieren", + "errFileMust": "Muss ein Bild für den Sticker enthalten", + "image": "Bild:", + "name": "Name:", + "tags": "Zugehöriges Emoji:", + "title": "Sticker", + "upload": "Sticker hochladen" + }, + "submit": "Senden", + "switchAccounts": "Benutzerkonto wechseln ⇌", + "todayAt": "Heute um $1", + "trace": { + "totalTime": "$2: $1ms", + "traces": "$1 ($2ms) $3" + }, + "typing": "$2 {{PLURAL:$1|tippt|tippen}}", + "unableToConnect": "Es konnte keine Verbindung zum Spacebar-Server hergestellt werden. Bitte versuche Dich ab- und wieder neu anzumelden.", + "unableToPin": "Nachricht kann nicht angeheftet werden", + "unpinMessage": "Nachricht lösen", + "updateAv": "Aktualisierungen verfügbar", + "uploadBanner": "Banner hochladen:", + "uploadFilesText": "Lade hier deine Dateien hoch!", + "uploadPfp": "Profilbild hochladen:", + "useTemplate": "Verwende $1 als Vorlage", + "useTemplateButton": "Vorlage verwenden", + "usedFermi": "gesendet über Fermi", + "user": { + "report": "Nutzer melden", + "deleted": "Gelöschter Benutzer", + "reportApp": "Anwendung melden", + "addRole": "Rollen hinzufügen", + "ban": "Mitglied bannen", + "block": "Benutzer bannen", + "confirmInstBan": "Sicher, dass $1 auf dieser Instanz gebannt werden soll?", + "copyId": "Benutzer-ID kopieren", + "dnd": "Bitte nicht stören", + "editNick": "Spitznamen bearbeiten", + "editServerProfile": "Serverprofil bearbeiten", + "friendReq": "Freundschaftsanfrage", + "idle": "Abwesend", + "instanceBan": "Instanzbann", + "invisible": "Unsichtbar", + "kick": "Mitglied rauswerfen", + "message": "Nachricht an den Benutzer", + "nick": { + "foe": "Spitzname verwenden", + "friend": "Spitznamen eines Freundes festlegen", + "stalker": "Spitzname des Anfragenden vergeben", + "stalking": "Spitzname des Angefragten vergeben" + }, + "offline": "Offline", + "online": "Online", + "remove": "Benutzer entfernen", + "removeRole": "Rollen entfernen", + "sendMessage": "Nachricht $1", + "timeout": "Timeout-Mitglieder", + "unTimeout": "Timeout für Mitglied entfernen", + "unban": "$1 entbannen", + "unblock": "Benutzer entsperren" + }, + "userping": { + "resolving": "Benutzer auflösen", + "unknown": "@unbekannter-Benutzer" + }, + "vc": { + "joinForStream": "Treten Sie dem Sprachkanal bei, um zuzusehen", + "joiningStream": "Trete Stream bei...", + "joinstream": "Stream ansehen", + "leavestream": "Stream verlassen", + "stopstream": "Stream stoppen" + }, + "webauth": { + "addKey": "Schlüssel hinzufügen", + "keyname": "Schlüsselname:", + "manage": "Sicherheitsschlüssel verwalten" + }, + "webhook": "WEBHOOK", + "webhooks": { + "EnterWebhookName": "Webhook-Namen eingeben", + "areYouSureDelete": "Sicher, dass $1 gelöscht werden soll?", + "avatar": "Avatar", + "base": "Webhooks", + "channel": "Kanal für Webhook-Ausgaben:", + "copyURL": "Webhook-Link kopieren", + "createdAt": "Erstellt am $1", + "createdBy": "Erstellt von:", + "deleteWebhook": "Webhook löschen", + "name": "Name des Webhooks:", + "newWebHook": "Neuer Webhook", + "notFound": "Der Benutzer ist nicht mehr auf dem Server", + "sillyDefaults": "Captain Hook\nBillie Hooks\nSpidey-Bot", + "token": "Webhook-Token: `$1`", + "type": "Typ: $1", + "type1": "Eingehend", + "type2": "Kanalabonnent", + "type3": "Anwendung", + "url": "Webhook-Link: `$1`" + }, + "welcomeMessages": "Willkommen, $1! Schön, dich zu sehen!\nHallo, schön, dich zu treffen, $1!\nWillkommen, ich hoffe, du hast Pizza mitgebracht, $1!", + "widget": "Server-Widget", + "widgetEnabled": "Widget aktiviert", + "yes": "Ja", + "yesterdayAt": "Gestern um $1" } diff --git a/translations/en.json b/translations/en.json index 3bf4cde3..440c7f16 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1,414 +1,958 @@ { + "2faCode": "2FA code:", + "404": { + "404": "error:404 - Page Not Found", + "app": "To the app", + "blog": "The Fermi blog", + "home": "Homepage", + "listtitle": "Maybe you meant to go to one of these places", + "login": "Login", + "report": "If you found this page within the client please report it:", + "reset": "Password reset page", + "title": "It seems you're lost", + "whatelse": "What else do you think should happen?", + "whereever": "Wherever this is" + }, "@metadata": { - "authors": [ - "MathMan05" - ], - "last-updated": "2024/15/24", - "locale": "en", - "comment":"Don't know how often I'll update this top part lol" - }, - "readableName":"English", - - "reply": "Reply", - "copyrawtext":"Copy raw text", - "copymessageid":"Copy message id", - "permissions":{ - "descriptions":{ - "CREATE_INSTANT_INVITE": "Allows the user to create invites for the guild", - "KICK_MEMBERS": "Allows the user to kick members from the guild", - "BAN_MEMBERS": "Allows the user to ban members from the guild", - "ADMINISTRATOR": "Allows all permissions and bypasses channel permission overwrites. This is a dangerous permission!", - "MANAGE_CHANNELS": "Allows the user to manage and edit channels", - "MANAGE_GUILD": "Allows management and editing of the guild", - "ADD_REACTIONS": "Allows user to add reactions to messages", - "VIEW_AUDIT_LOG": "Allows the user to view the audit log", - "PRIORITY_SPEAKER": "Allows for using priority speaker in a voice channel", - "STREAM": "Allows the user to stream", - "VIEW_CHANNEL": "Allows the user to view the channel", - "SEND_MESSAGES": "Allows user to send messages", - "SEND_TTS_MESSAGES": "Allows the user to send text-to-speech messages", - "MANAGE_MESSAGES": "Allows the user to delete messages that aren't their own", - "EMBED_LINKS": "Allow links sent by this user to auto-embed", - "ATTACH_FILES": "Allows the user to attach files", - "READ_MESSAGE_HISTORY": "Allows user to read the message history", - "MENTION_EVERYONE": "Allows the user to mention everyone", - "USE_EXTERNAL_EMOJIS": "Allows the user to use external emojis", - "VIEW_GUILD_INSIGHTS": "Allows the user to see guild insights", - "CONNECT": "Allows the user to connect to a voice channel", - "SPEAK": "Allows the user to speak in a voice channel", - "MUTE_MEMBERS": "Allows user to mute other members", - "DEAFEN_MEMBERS": "Allows user to deafen other members", - "MOVE_MEMBERS": "Allows the user to move members between voice channels", - "USE_VAD": "Allows users to speak in a voice channel by simply talking", - "CHANGE_NICKNAME": "Allows the user to change their own nickname", - "MANAGE_NICKNAMES": "Allows user to change nicknames of other members", - "MANAGE_ROLES": "Allows user to edit and manage roles", - "MANAGE_WEBHOOKS": "Allows management and editing of webhooks", - "MANAGE_GUILD_EXPRESSIONS": "Allows for managing emoji, stickers, and soundboards", - "USE_APPLICATION_COMMANDS": "Allows the user to use application commands", - "REQUEST_TO_SPEAK": "Allows user to request to speak in stage channel", - "MANAGE_EVENTS": "Allows user to edit and manage events", - "MANAGE_THREADS": "Allows the user to delete and archive threads and view all private threads", - "CREATE_PUBLIC_THREADS": "Allows the user to create public threads", - "CREATE_PRIVATE_THREADS": "Allows the user to create private threads", - "USE_EXTERNAL_STICKERS": "Allows user to use external stickers", - "SEND_MESSAGES_IN_THREADS": "Allows the user to send messages in threads", - "USE_EMBEDDED_ACTIVITIES": "Allows the user to use embedded activities", - "MODERATE_MEMBERS": "Allows the user to time out other users to prevent them from sending or reacting to messages in chat and threads, and from speaking in voice and stage channels", - "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Allows for viewing role subscription insights", - "USE_SOUNDBOARD": "Allows for using soundboard in a voice channel", - "CREATE_GUILD_EXPRESSIONS": "Allows for creating emojis, stickers, and soundboard sounds, and editing and deleting those created by the current user.", - "CREATE_EVENTS": "Allows for creating scheduled events, and editing and deleting those created by the current user.", - "USE_EXTERNAL_SOUNDS": "Allows the usage of custom soundboard sounds from other servers", - "SEND_VOICE_MESSAGES": "Allows sending voice messages", - "SEND_POLLS": "Allows sending polls", - "USE_EXTERNAL_APPS": "Allows user-installed apps to send public responses. When disabled, users will still be allowed to use their apps but the responses will be ephemeral. This only applies to apps not also installed to the server." + "authors": ["MathMan05, TheGeekn°72"], + "comment": "Don't know how often I'll update this top part lol // Aye, I'll take care of it", + "last-updated": "2026/02/13", + "locale": "en" + }, + "onboarding":{ + "name":"Onboarding", + "disable":"Disable onboarding", + "addChannel":"Add channel", + "channel":"Channel:", + "desc":"Description:", + "deleteChannel":"Delete channel from onboarding", + "enable":"Enable onboarding", + "title":"Welcome to $1!" + }, + "DMs": { + "add": "Add someone to this DM", + "close": "Close DM", + "copyId": "Copy DM id", + "markRead": "Mark as read", + "name": "Direct Messages" + }, + "ok":"Ok", + "dismiss":"Dismiss", + "UrlGen": "Bot invite generator", + "Voice": { + "userVol":"User volume:", + "status": { + "badWS": "Bad response from WS", + "conectionFailed": "Connection Failed", + "done": "Connected", + "left": "Left voice chat", + "makingOffer": "Making offer", + "noSDP": "No SDP", + "notconnected": "Not connected", + "sendingStreams": "Sending audio streams", + "startingRTC": "Starting RTC connection", + "waitingMainWS": "Waiting for main WS", + "waitingURL": "Waiting for voice URL", + "wsAuth": "waiting for WS to authorize", + "wsOpen": "Waiting for WS to open" + } + }, + "accessibility": { + "gifSettings": { + "always": "Always", + "hover": "On hover", + "never": "Never" }, - "readableNames":{ + "name": "Accessibility", + "playGif": "Play GIFs:", + "playIcon": "Play animated icons:", + "roleColors": "Disable role colors:", + "gradientColors":"Disable gradient coloring:", + "decorations":"Enable avatar decorations:" + }, + "accountNotStart": "Account unable to start", + "add": "Add", + "applications": { + "delete": "Delete Application", + "sure": "Are you sure you want to delete $1?" + }, + "badge": { + "active_developer": "Active Developer", + "application_guild_subscription": "has Premium", + "automod": "uses AutoMod", + "bot_commands": "supports commands", + "bug_hunter_level_1": "Bug hunter level 1", + "bug_hunter_level_2": "Bug hunter level 2", + "certified_moderator": "Moderator", + "early_supporter": "Early supporter", + "guild_booster_lvl1": "Boosted guild", + "guild_booster_lvl2": "Boosted guild", + "guild_booster_lvl3": "Boosted guild", + "guild_booster_lvl4": "Boosted guild", + "guild_booster_lvl5": "Boosted guild", + "guild_booster_lvl6": "Boosted guild", + "guild_booster_lvl7": "Boosted guild", + "guild_booster_lvl8": "Boosted guild", + "guild_booster_lvl9": "Boosted guild for quite a while", + "hypesquad": "Vibesquad [PLACEHOLDER]", + "hypesquad_house_1": "Boldness", + "hypesquad_house_2": "Excellence", + "hypesquad_house_3": "Equilibrium", + "legacy_username": "has legacy username", + "partner": "Instance Partner", + "premium": "Premium", + "quest_completed": "finished a quest", + "staff": "Instance staff", + "verified_developer": "Verified Developer" + }, + "bio": "Bio:", + "blankMessage": "Blank message", + "blog": { + "blog": "Blog", + "blogUpdates": "Blog updates:", + "desc": "Read or subscribe to the blog for updates on Fermi!", + "fermi": "Fermi Blog", + "gotoPost": "Go to post", + "wantUpdates": "Would you like to get weekly blog updates on the state of Fermi?" + }, + "bot": "BOT", + "botGuilds": "Guilds bot is in:", + "botSettings": "Bot Settings", + "cancel": "Cancel", + "channel": { + "SlowmodeCool": "Slowmode cooldown: $1", + "TimeOutCool": "Timed out until: $1", + "allowIcons": "Allow custom channel icons", + "announcement": "Announcements", + "copyId": "Copy channel id", + "copyIdCat": "Copy category id", + "createCatagory": "Create category", + "createChannel": "Create channel", + "creating": "Creating channel", + "delete": "Delete channel", + "deleteCat": "Delete category", + "icon": "Icon:", + "makeInvite": "Make invite", + "markRead": "Mark as read", + "mute": "Mute channel", + "name": "Channel", + "name:": "Channel name:", + "nsfw:": "NSFW/Mature content warning:", + "permissions": "Permissions", + "selectCatName": "Name of category", + "selectName": "Name of channel", + "selectType": "Select channel type", + "settings": "Settings", + "settingsFor": "Settings for $1", + "slowmode": "Slowmode:", + "text": "Text", + "timedOutUntil": "Timed out until: $1", + "topic:": "Topic:", + "typebox": "Message in $1", + "unmute": "Unmute channel", + "voice": "Voice", + "deleteThread":"Delete thread", + "hideThreads":"Hide after inactivity", + "forum":"Forum" + }, + "threads":{ + "leave":"Leave thread", + "join":"Join thread", + "name":"Thread name:", + "editTags":"Edit tags" + }, + "forum":{ + "creorsear":"Create or search for post", + "next":"Next", + "back":"Back", + "newPost":"Make a new post", + "post":"Post", + "sortOptions":{ + "sortby":{ + "title":"Sort by", + "recent":"Recently active", + "posted":"Posted date" + }, + "sortOrder":{ + "title":"Sort order", + "recent":"Recent first", + "old":"Old first" + }, + "tagMatch":{ + "title":"Tag matching", + "some":"Match some tags", + "all":"Match all tags" + }, + "name":"Sorting options" + }, + "errors":{ + "tagsReq":"Tags are required", + "requireText":"Message cannot be empty" + }, + "allTags":"All tags", + "settings":{ + "editTags":"Edit tags:", + "editTag":"Edit tag", + "tagName":"Tag name:", + "moderated":"Only allow thread moderators to apply tag:", + "addTag":"Add a tag", + "delTag":"Delete tag" + } + }, + "channelLink": "# $1", + "clientDesc": "Client version: $1\n\n[Join the official Fermi guild]($2/invite/USgYJo?instance=https%3A%2F%2Fspacebar.chat)\n\n[Help translate Fermi](https://translatewiki.net/wiki/Translating:JankClient#sortable:3=desc) \n\n[Help create Fermi](https://github.com/MathMan05/Fermi)\n\n[Help maintain the server Fermi relies on](https://github.com/spacebarchat/server)\n\n[Read the blog](https://blog.fermi.chat/)\n\nCalculated rights: $3", + "commands": { + "errorNotValid": "$1 is not a valid choice for $2", + "required": "$1 is a required part of this command" + }, + "confirmGuildLeave": "Are you sure you want to leave $1", + "copyLink": "Copy message link", + "copyRegLink":"Copy link", + "copymessageid": "Copy message id", + "copyrawtext": "Copy raw text", + "createAccount": "Create Account", + "delete": "Delete", + "deleteConfirm": "Are you sure you want to delete this?", + "devSettings": { + "badUser": "Enable logging of bad user objects that send too much information:", + "cache": "Enable Service Worker Caching map files:", + "cacheDesc": "map files will still load either way, this'll just make sure they're in cache when a new update rolls out.", + "captureTrace": "This setting tells Fermi to capture _trace properties from the server, enabling this may cause progressive JSON decoding to not work (might require a reload)", + "clearWellKnowns": "Re-fetch .well-known records:", + "description": "These are for developers of Spacebar/Fermi, if you don't know what you're doing, please don't mess with these settings.", + "gatewayComp": "Disable Gateway compression:", + "reportSystem":"Enable experimental reporting system:", + "logGateway": "Log received gateway events (log level info):", + "name": "Developer Settings", + "traces": "Expose traces:" + }, + "deviceManage": { + "city": "City: $1", + "continent": "Continent: $1", + "country": "Country: $1", + "curSes": "This is the current session, you have to logout via the other menu", + "estimateWarn": "Warning: All of this information is just a best guess, this could be incorrect.", + "ip": "Last known IP: $1", + "last": "Approximately last used: $1", + "latitude": "Latitude: $1", + "logout": "Logout", + "longitude": "Longitude: $1", + "manageDev": "Manage device", + "postal": "Postal: $1", + "region": "Region: $1", + "title": "Manage sessions" + }, + "discovery": "Discovery", + "dms": "Direct messages", + "edit": "Edit", + "emoji": { + "confirmDel": "Are you sure you want to delete this emoji?", + "found": { + "not": "This emoji is not from your guilds, join the guild hosting it to use this emoji", + "other": "This emoji is from one of your other guilds.", + "private": "The guild for this emoji can't be found", + "this": "This emoji is from this guild." + }, + "from": "This emoji is from", + "image:": "Image:", + "join": "Join guild", + "name:": "Name:", + "privateGuild": "This guild is private", + "title": "Emojis", + "upload": "Upload emojis" + }, + "emojiSelect": "Emoji:", + "errorReconnect": "Unable to connect to the server, retrying in **$1** seconds...", + "favoriteGifs": "Favorited GIFs", + "folder": { + "color": "Folder color:", + "create": "Create new folder", + "edit": "Edit folder", + "name": "Folder name:" + }, + "form": { + "captcha": "Wait, are you a human?" + }, + "friends": { + "addfriend": "Add friend", + "addfriendpromt": "Add friends by username:", + "requestsent": "Request sent!", + "all": "All", + "all:": "All friends:", + "blocked": "Blocked", + "blockedusers": "Blocked users:", + "bu": "Blocked user", + "discnotfound": "Discriminator not found", + "friendlist": "Friend list", + "friends": "Friends", + "notfound": "User not found", + "online": "Online", + "online:": "Online friends:", + "pending": "Pending", + "pending:": "Pending friend requests:", + "removeFriend": "Remove friend" + }, + "goThere": "Yes, let's go", + "goThereTrust": "Yes & trust this domain from now on", + "goingToURL": "This hyperlink will send you to $1. Are you sure you want to go there?", + "group": { + "createdm": "Create DM!", + "edit": "Edit group chat", + "select": "Select friends" + }, + "guild": { + "COMMUNITY": "Apply to join", + "disableInvites":"Disable invites:", + "DISCOVERABLE": "Discovery", + "INVITES_DISABLED": "Invite only", + "adminMenu": { + "changePerms": "Change permissions to find", + "finding": "Finding admins", + "ownName": "Owner", + "owner": "Find the owner", + "permission": "Permissions:" + }, + "report":"Report guild", + "admins": "Find admins", + "all": "All", + "banReason": "Ban reason: $1", + "bannedBy": "Banned by:", + "banner:": "Banner:", + "bans": "Bans", + "boostMessage?": "Send a message when someone boosts your guild!", + "community": "Community", + "confirmDelete": "Are you sure you want to delete $1?", + "confirmLeave": "Are you sure you want to leave?", + "copyId": "Copy guild id", + "create": "Create guild from scratch", + "createFromTemplate": "Create guild from template", + "createNewTemplate": "Create new template", + "creating": "Creating guild", + "default": "Default ($1)", + "defaultNoti": "Set the default notification settings of your guild!", + "delete": "Delete guild", + "description:": "Description:", + "disoveryTitle": "Guild discovery ($1 {{PLURAL:$1|entry|entries}})", + "editingTemplate": "Editing $1", + "emptytext": "How curious, this guild has no channels!?", + "emptytitle": "Weird spot", + "guilds": "Guilds", + "helpTips?": "Send helpful tips for your guild!", + "howJoin": "How can people join your guild?", + "icon:": "Icon:", + "invites": "Invites", + "joinConfirm": "Join $1?", + "leave": "Leave guild", + "loadingDiscovery": "Loading...", + "makeInvite": "Make invite", + "markRead": "Mark as read", + "memberCount": "$1 member{{PLURAL:$1||s}}", + "mute": "Mute guild", + "name:": "Name:", + "nameNoMatch": "Names don't match", + "noDelete": "Nevermind", + "noLeave": "Nevermind", + "none": "None", + "notifications": "Notifications", + "onlyMentions": "Only @mentions", + "overview": "Overview", + "region:": "Region:", + "roles": "Roles", + "ruleId": "Rules channel:", + "selectnoti": "Select notifications type:", + "sendrandomwelcome?": "Send a random message when someone joins this guild:", + "serverName": "Name of guild:", + "settings": "Settings", + "settingsFor": "Settings for $1", + "splash": "Discovery splash:", + "stickWelcomeReact?": "Prompt members of your guild to react with a sticker when someone joins!", + "systemSelect:": "Systems messages channel:", + "tempCreatedBy": "Template created by:", + "tempUseCount": "Template has been used $1 time{{PLURAL:$1||s}}", + "template": "Template link/code to build guild from:", + "templateDesc": "Template description:", + "templateName": "Template name:", + "templateNameShort": "Template name must at least be 2 characters long", + "templateURL": "Template link: $1", + "templates": "Templates", + "templcateMetaDesc": "A template allows others to use this guild as a base for their own guilds: it'll copy the channels, roles and settings of this guild but not the messages from within the guild, the bots nor the guilds icon.", + "topic:": "Topic:", + "unknownRole": "@unknown-role", + "viewTemplate": "View template", + "yesDelete": "Yes, I'm sure", + "yesLeave": "Yes, I'm sure" + }, + "hideBlockedMessages": "You have this user blocked, click to hide these messages.", + "home": { + "icon": "$1's instance icon", + "uptimeStats": "Uptime: \n All time: $1%\nThis week: $2%\nToday: $3%", + "warnOffiline": "Instance is offline, can't connect" + }, + "htmlPages": { + "addBot": "Add to guild", + "alreadyHave": "Already have an account?", + "box1Items": "Direct Messaging|Reactions support|Invites|Account switching|User settings|Developer portal|Bot invites|Translation support", + "box1title": "Fermi is a Spacebar-compatible client seeking to be as good as it can be with many features including:", + "box3description": "We always appreciate some help, whether that be in the form of bug reports, or code, or even just pointing out some typos.", + "box3title": "Contribute to Fermi", + "compatableInstances": "Spacebar-compatible instances:", + "createAccount": "Create account", + "dobField": "Date of birth:", + "emailField": "Email:", + "idpermissions": "This will allow the bot to:", + "instanceField": "Instance:", + "loaddesc": "This shouldn't take long", + "loadingText": "Fermi is loading", + "loginButton": "Login", + "noAccount": "Don't have an account?", + "openClient": "Open client", + "pw2Field": "Enter password again:", + "pwField": "Password:", + "switchaccounts": "Switch accounts", + "trans": "Translate", + "transDesc": "You can help translate Fermi into your own language!", + "transTitle": "Translate Fermi", + "userField": "Username:", + "welcomeJank": "Welcome to Fermi" + }, + "incorrectURLS": "## This instance has likely sent the incorrect links.\n### If you're the instance owner please see [here](https://docs.spacebar.chat/setup/server/) under *Connecting from remote machines* to correct the issue.\n Would you like Fermi to automatically try to fix this error to let you connect to the instance?", + "instInfo": "Instance information", + "instanceInfo": { + "contact": "Email instance admins", + "frontPage": "[Front page]($1)", + "name": "Instance info", + "publicUrl": "[Public link]($1)", + "tosPage": "[TOS page]($1)" + }, + "instanceStats": { + "members": "Members: $1", + "messages": "Messages: $1", + "name": "Instance stats: $1", + "servers": "Guilds: $1", + "users": "Registered users: $1" + }, + "interactions": { + "ephemeralDismiss": "Dismiss message", + "failed": "Interaction failed", + "nonsence": "The server sent a component of type $1 which does not exist", + "notImpl": "Fermi has not implemented component type $1 yet", + "onlyYou": "Only you can see this message", + "replyline": "Started interaction", + "started": "Processing interaction...", + "worked": "Interaction succeeded" + }, + "invite": { + "accept": "Accept", + "alreadyJoined": "Already joined", + "channel:": "Channel:", + "createInvite": "Create invite", + "createdAt": "Created at $1", + "expireAfter": "Expire after:", + "expires": "Expires: $1", + "forChannel": "For channel: $1", + "inviteLinkCode": "Invite link/code", + "inviteMaker": "Invite maker", + "invitedBy": "You've been invited by $1", + "joinUsing": "Join a guild using an invite", + "loginOrCreateAccount": "Login or create an account ⇌", + "longInvitedBy": "$1 invited you to join $2", + "maxUses": "Max uses: $1", + "never": "Never", + "noAccount": "Create an account to accept the invite", + "notFound": "Invite could not be found", + "subtext": "to $1 in $2", + "used": "Used $1 time{{PLURAL:$1||s}}:" + }, + "inviteOptions": { + "12h": "12 Hours", + "1d": "1 Day", + "1h": "1 Hour", + "30d": "30 Days", + "30m": "30 Minutes", + "6h": "6 Hours", + "7d": "7 Days", + "limit": "$1 use{{PLURAL:$1||s}}", + "never": "Never", + "noLimit": "No limit", + "title": "Invite people" + }, + "jankInfo": "Client information", + "leaveGuild": "Leave guild", + "leaving": "You're leaving Spacebar", + "loaded": "Loaded", + "localuser": { + "2faCode:": "Code:", + "2faCodeGive": "Your TOTP secret is: $1. This will generate a 6 digits 2FA token with a 30 second expiration period.", + "2faDisable": "Disable 2FA", + "2faEnable": "Enable 2FA", + "CheckUpdate": "Check for updates", + "PasswordsNoMatch": "Passwords don't match", + "TOSURL": "Terms of Service link:", + "VoiceWarning": "Are you sure you want to enable this? It's very experimental and is likely to cause issues. (this feature is for devs, please don't enable if you don't know what you're doing)", + "accentColor": "Theme accent color:", + "accountSettings": "Account settings", + "addBot": "Add bot", + "addStatus": "Add status", + "advancedBot": "Advanced bot settings", + "appName": "Application name:", + "areYouSureDelete": "Are you sure you want to delete your account? If so enter the phrase $1", + "badCode": "Invalid code", + "badPassword": "Incorrect password", + "botAvatar": "Bot avatar:", + "botInviteCreate": "Bot invite creator", + "botUsername": "Bot username:", + "changeDiscriminator": "Change discriminator", + "changeEmail": "Change email", + "changePassword": "Change password", + "changeUsername": "Change username", + "clearCache": "Clear cache", + "confirmAddBot": "Are you sure you want to add a bot to this application? There's no going back.", + "confirmReset": "Are you sure you want to reset the bot token? Your bot will stop working until you update it.", + "confuseNoBot": "For some reason, this application doesn't have a bot (yet).", + "connections": "Connections", + "createApp": "Create application", + "customSound": "Custom sound", + "customStatusWarn": "Spacebar does not support custom status being displayed at this time so while it'll accept the status, it will not do anything with it", + "deleteAccount": "Account deletion", + "deleteAccountButton": "Delete account", + "description": "Description:", + "devPortal": "Developer portal", + "disableConnection": "This connection has been disabled server-side", + "editingBot": "Editing bot $1", + "install": "Install", + "installDesc": "Installing Fermi will allow you to open it in its own window and act like its own app! You can also just continue to use Fermi in the web browser like you have been and it'll work the same.", + "installJank": "Install Fermi", + "language": "Language:", + "manageBot": "Manage bot", + "manageInstance": "Manage instance", + "mustTypePhrase": "To delete your account you must type the phrase", + "newDiscriminator": "New discriminator:", + "newEmail:": "New email", + "newPassword:": "New password:", + "newUsername": "New username:", + "noToken": "Don't know token so can't save it to localStorage, sorry", + "noUpdates": "No updates found", + "notisound": "Notification sound:", + "oldPassword:": "Old password:", + "password:": "Password", + "privacyPolcyURL": "Privacy policy link:", + "appID":"Application ID: $1", + "showSecret":"Show client secret", + "clientSecret":"Client secret: $1", + "secret":"Secret", + "publicAvaliable": "Make bot publicly inviteable?", + "refreshPage": "Refresh to apply", + "requireCode": "Require code grant to invite the bot?", + "resetToken": "Reset token", + "saveToken": "Save token to local storage", + "setUp2fa": "2FA Setup", + "setUp2faInstruction": "Copy this secret into your TOTP (time-based one time password) app to generate your 2FA login codes", + "settings": "Settings", + "sillyDeleteConfirmPhrase": "Shrek is love, Shrek is life", + "soundTooLarge": "The sound you tried to upload was too large, try again", + "status": "Status", + "team:": "Team:", + "theme:": "Theme", + "themesAndSounds": "Themes & Sounds", + "tokenDisplay": "Token: $1", + "trace": "Traces", + "trusted": "Trusted Domains", + "trustedDesc": "When you click on links sending you to these domain, you will ***not*** be prompted for permission to open like other links, only give this to domains you trust, such as 'https://fermi.chat'", + "updateSettings": "Update settings", + "updatesYay": "Updates found!", + "userSettings": "Your public profile" + }, + "login": { + "allGood": "All good", + "checking": "Checking instance", + "enterPAgain": "Enter new password again:", + "invalid": "Invalid instance, try again", + "login": "Login", + "newPassword": "New password:", + "pasteInfo": "Paste the recovery link here:", + "recover": "Forgot password?", + "recovery": "Forgotten password", + "waiting": "Waiting to check instance" + }, + "logout": { + "error": { + "cancel": "Cancel", + "cont": "Continue anyways", + "desc": "Fermi was unable to log you out,\n do you wish to remove the account anyways?", + "title": "An error occurred trying to log you out" + }, + "logout": "Logout" + }, + "manageInstance": { + "AreYouSureStop": "Are you sure you want to stop this instance?", + "TokenFormats": { + "JSON": "JSON formatted", + "URLs": "Invite links", + "plain": "Plain" + }, + "clientURL": "Client link:", + "copy": "Copy", + "count": "Count:", + "create": "Create", + "createTokens": "Create registration tokens", + "format": "Format:", + "genericType": "Generic", + "length": "Length:", + "regType": "Register token link type", + "stop": "Stop instance" + }, + "media": { + "artist": "Artist: $1", + "composer": "Composer: $1", + "download": "Download media", + "length": "Length: $1 minutes and $2 seconds", + "loading": "Loading", + "moreInfo": "More info", + "notFound": "Media could not be found" + }, + "member": { + "ban": "Ban $1 from $2", + "kick": "Kick $1 from $2", + "nick:": "Nickname:", + "persist": "Only delete user:", + "reason:": "Reason:", + "timeout": "Timeout $1", + "timeoutTime": "Timeout duration:" + }, + "message": { + "messages":"$1 message{{PLURAL:$1||s}} >", + "andMore": "$1, and more!", + "attached": "sent an attachment", + "delete": "Delete message", + "report":"Report message", + "deleted": "Deleted message", + "edit": "Edit message", + "edited": "(edited)", + "fullMessage": "Full message:", + "pin": "$1 has pinned a message", + "thread": "$1 has created a thread: $2", + "reactedWith": "Reacted with $1", + "reactionAdd": "Add reaction", + "createThread":"Create thread", + "threadOptions":"Thread options", + "reactions": "View reactions", + "reactionsTitle": "Reactions", + "retry": "Resend errored message", + "viewrest": "View rest" + }, + "report":{ + "back":"Back", + "next":"Next", + "cancel":"Cancel", + "submit":"Submit", + "blockUser":"Block user", + "timeout":"Timeout member", + "summary":"Report summary:" + }, + "messageLink": "Message in # $1", + "muteDuration": "Unmute in:", + "nevermind": "Nevermind", + "no": "No", + "noEmojiFont": "Use system emojis", + "noMessages": "No messages here yet, be the first to say something!", + "noPins": "There's nothing for you here... yet", + "noStickers": "That's unfortunate, there are no stickers for you to use!", + "notiClump": "You've gotten $1 notifications from $2", + "notiVolume": "Notification volume:", + "otherAt": "$1 at $2", + "permissions": { + "descriptions": { + "ADMINISTRATOR": "Gives ALL permissions below to role members, bypasses ALL permission overwrites. Only YOU and members/bots you absolutely trust should have this DANGEROUS permission!!", + "MANAGE_GUILD": "Allows role members to manage and edit the guild.", + "VIEW_AUDIT_LOG": "Allows role members to view the audit log.", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Allows role members to view role subscription insights.", + "VIEW_GUILD_INSIGHTS": "Allows role members to see guild insights.", + "BYPASS_SLOWMODE": "Slowmode no longer effects role members.", + "MENTION_EVERYONE": "Allows role members to mention @everyone.", + "MANAGE_NICKNAMES": "Allows role members to change nicknames of other members.", + "PIN_MESSAGES": "Allows role members to pin messages.", + "SEND_POLLS": "Allows role members to send polls.", + "MANAGE_MESSAGES": "Allows role members to delete messages that aren't their own.", + "MANAGE_THREADS": "Allows role members to delete and archive threads and view all private threads.", + "MANAGE_CHANNELS": "Allows role members to edit/delete channels.", + "MANAGE_ROLES": "Allows role members to edit/delete roles.", + "MANAGE_WEBHOOKS": "Allows role members to edit/delete webhooks.", + "CREATE_EVENTS": "Allows role members to create scheduled events.", + "MANAGE_EVENTS": "Allows role members to edit/delete events (existing and future).", + "CREATE_GUILD_EXPRESSIONS": "Allows role members to create expressions (emojis, stickers and soundboard sounds).", + "MANAGE_GUILD_EXPRESSIONS": "Allows role members to edit/delete expressions (emojis, stickers and soundboard sounds).", + "MODERATE_MEMBERS": "Allows role members to timeout guild members (prevent them from sending or reacting to messages in chat/threads and from speaking in voice/stage channels).", + "KICK_MEMBERS": "Allows role members to kick members from the guild.", + "BAN_MEMBERS": "Allows role members to ban members from the guild.", + "MOVE_MEMBERS": "Allows role members to move members between voice channels.", + "MUTE_MEMBERS": "Allows role members to mute other members in voice channels.", + "DEAFEN_MEMBERS": "Allows role members to deafen other members in voice channels (deafening a member will automatically mute them as well).", + "CHANGE_NICKNAME": "Allows role members to change their own nickname.", + "VIEW_CHANNEL": "Allows role members to view text channels.", + "READ_MESSAGE_HISTORY": "Allows role members to read the message history of text channels.", + "ADD_REACTIONS": "Allows role members to add reactions to messages in text channels.", + "SEND_MESSAGES": "Allows role members to send messages in text channels.", + "SEND_MESSAGES_IN_THREADS": "Allows role members to send messages in threads.", + "SEND_TTS_MESSAGES": "Allows role members to send text-to-speech messages in text channels.", + "USE_EXTERNAL_EMOJIS": "Allows role members to use external emojis in text channels.", + "USE_EXTERNAL_STICKERS": "Allows role members to use external stickers in text channels.", + "EMBED_LINKS": "Allow links sent by role members to auto-embed in text channels.", + "ATTACH_FILES": "Allows role members to attach files in text channels.", + "SEND_VOICE_MESSAGES": "Allows sending voice messages in text channels.", + "CREATE_INSTANT_INVITE": "Allows role members to create invites for the guild.", + "CREATE_PUBLIC_THREADS": "Allows role members to create public threads.", + "CREATE_PRIVATE_THREADS": "Allows role members to create private threads.", + "CONNECT": "Allows role members to connect to voice channels.", + "SPEAK": "Allows role members to speak in voice channels.", + "USE_VAD": "Allows role members to speak in voice channels by simply talking (When disabled, they can only speak using their Push-To-Talk key input).", + "STREAM": "Allows role members to stream in voice channels.", + "USE_SOUNDBOARD": "Allows role members to use a soundboard in voice channels.", + "USE_EXTERNAL_SOUNDS": "Allows role members to use soundboard sounds from other guilds in voice channels.", + "PRIORITY_SPEAKER": "Allows role members to be priority speaker in voice channels.", + "REQUEST_TO_SPEAK": "Allows role members to request to speak in stage channels.", + "USE_EMBEDDED_ACTIVITIES": "Allows role members to use embedded activities.", + "USE_APPLICATION_COMMANDS": "Allows role members to use application commands.", + "USE_EXTERNAL_APPS": "Allows role members to have application responses to show publicly in channel (When disabled, users will still be allowed to use their apps but responses will be visible only to themselves. This only applies to apps not also installed to the guild)." + }, + "readableNames": { + "ADD_REACTIONS": "Add reactions", + "ADMINISTRATOR": "Administrator", + "ATTACH_FILES": "Attach files", + "BAN_MEMBERS": "Ban members", + "BYPASS_SLOWMODE": "Bypass slowmode", + "CHANGE_NICKNAME": "Change nickname", + "CONNECT": "Connect", + "CREATE_EVENTS": "Create events", + "CREATE_GUILD_EXPRESSIONS": "Create expressions", "CREATE_INSTANT_INVITE": "Create invite", + "CREATE_PRIVATE_THREADS": "Create private threads", + "CREATE_PUBLIC_THREADS": "Create public threads", + "DEAFEN_MEMBERS": "Deafen members", + "EMBED_LINKS": "Embed links", "KICK_MEMBERS": "Kick members", - "BAN_MEMBERS": "Ban members", - "ADMINISTRATOR": "Administrator", "MANAGE_CHANNELS": "Manage channels", + "MANAGE_EVENTS": "Manage events", "MANAGE_GUILD": "Manage guild", - "ADD_REACTIONS": "Add reactions", - "VIEW_AUDIT_LOG": "View audit log", - "PRIORITY_SPEAKER": "Priority speaker", - "STREAM": "Video", - "VIEW_CHANNEL": "View channels", - "SEND_MESSAGES": "Send messages", - "SEND_TTS_MESSAGES": "Send text-to-speech messages", + "MANAGE_GUILD_EXPRESSIONS": "Manage expressions", "MANAGE_MESSAGES": "Manage messages", - "EMBED_LINKS": "Embed links", - "ATTACH_FILES": "Attach files", - "READ_MESSAGE_HISTORY": "Read message history", - "MENTION_EVERYONE": "Mention @everyone, @here and all roles", - "USE_EXTERNAL_EMOJIS": "Use external emojis", - "VIEW_GUILD_INSIGHTS": "View guild insights", - "CONNECT": "Connect", - "SPEAK": "Speak", - "MUTE_MEMBERS": "Mute members", - "DEAFEN_MEMBERS": "Deafen members", - "MOVE_MEMBERS": "Move members", - "USE_VAD": "Use voice activity detection", - "CHANGE_NICKNAME": "Change nickname", "MANAGE_NICKNAMES": "Manage nicknames", "MANAGE_ROLES": "Manage roles", + "MANAGE_THREADS": "Manage threads", "MANAGE_WEBHOOKS": "Manage webhooks", - "MANAGE_GUILD_EXPRESSIONS": "Manage expressions", - "USE_APPLICATION_COMMANDS": "Use application commands", + "MENTION_EVERYONE": "Mention @everyone, @here and all roles", + "MODERATE_MEMBERS": "Timeout members", + "MOVE_MEMBERS": "Move members", + "MUTE_MEMBERS": "Mute members", + "PIN_MESSAGES": "Pin messages", + "PRIORITY_SPEAKER": "Priority speaker", + "READ_MESSAGE_HISTORY": "Read message history", "REQUEST_TO_SPEAK": "Request to speak", - "MANAGE_EVENTS": "Manage events", - "MANAGE_THREADS": "Manage threads", - "CREATE_PUBLIC_THREADS": "Create public threads", - "CREATE_PRIVATE_THREADS": "Create private threads", - "USE_EXTERNAL_STICKERS": "Use external stickers", + "SEND_MESSAGES": "Send messages", "SEND_MESSAGES_IN_THREADS": "Send messages in threads", + "SEND_POLLS": "Create polls", + "SEND_TTS_MESSAGES": "Send Text-To-Speech messages", + "SEND_VOICE_MESSAGES": "Send voice messages", + "SPEAK": "Speak", + "STREAM": "Video", + "USE_APPLICATION_COMMANDS": "Use application commands", "USE_EMBEDDED_ACTIVITIES": "Use activities", - "MODERATE_MEMBERS": "Timeout members", - "VIEW_CREATOR_MONETIZATION_ANALYTICS": "View creator monetization analytics", - "USE_SOUNDBOARD": "Use soundboard", - "CREATE_GUILD_EXPRESSIONS": "Create expressions", - "CREATE_EVENTS": "Create events", + "USE_EXTERNAL_APPS": "Use external apps", + "USE_EXTERNAL_EMOJIS": "Use external emojis", "USE_EXTERNAL_SOUNDS": "Use external sounds", - "SEND_VOICE_MESSAGES": "Send voice messages", - "SEND_POLLS": "Create polls", - "USE_EXTERNAL_APPS": "Use external apps" + "USE_EXTERNAL_STICKERS": "Use external stickers", + "USE_SOUNDBOARD": "Use soundboard", + "USE_VAD": "Use voice activity detection", + "VIEW_AUDIT_LOG": "View audit log", + "VIEW_CHANNEL": "View channels", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "View creator monetization analytics", + "VIEW_GUILD_INSIGHTS": "View guild insights" } }, - "hideBlockedMessages":"You have this user blocked, click to hide these messages.", - "showBlockedMessages":"You have this user blocked, click to see the $1 blocked {{PLURAL:$1|message|messages}}.", - "deleteConfirm":"Are you sure you want to delete this?", - "yes":"Yes", - "no":"No", - "todayAt":"Today at $1", - "yesterdayAt":"Yesterday at $1", - "otherAt":"$1 at $2", - "botSettings":"Bot Settings", - "uploadPfp":"Upload pfp:", - "uploadBanner":"Upload banner:", - "pronouns":"Pronouns:", - "bio":"Bio:", - "profileColor":"Profile color", - "botGuilds":"Guilds bot is in:", - "leaveGuild":"Leave Guild", - "confirmGuildLeave":"Are you sure you want to leave $1", - "UrlGen":"URL generator", - "typing":"$2 {{PLURAL:$1|is|are}} typing", - "noMessages":"No messages appear to be here, be the first to say something!", - "blankMessage":"Blank Message", - "channel":{ - "copyId":"Copy channel id", - "markRead":"Mark as read", - "settings":"Settings", - "delete":"Delete channel", - "makeInvite":"Make invite", - "settingsFor":"Settings for $1", - "voice":"Voice", - "text":"Text", - "announcement":"Announcements", - "name:":"Name:", - "topic:":"Topic:", - "nsfw:":"NSFW:", - "selectType":"Select channel type", - "selectName":"Name of channel", - "selectCatName":"Name of category", - "createChannel":"Create channel", - "createCatagory":"Create category" - }, - "switchAccounts":"Switch accounts ⇌", - "accountNotStart":"Account unable to start", - "home":{ - "uptimeStats":"Uptime: \n All time: $1%\nThis week: $2%\nToday: $3%", - "warnOffiline":"Instance is offline, can't connect" - }, - "htmlPages":{ - "idpermissions":"This will allow the bot to:", - "addBot":"Add to server", - "loadingText":"Jank Client is loading", - "loaddesc":"This shouldn't take long", - "switchaccounts":"Switch Accounts", - "instanceField":"Instance:", - "emailField":"Email:", - "pwField":"Password:", - "loginButton":"Login", - "noAccount":"Don't have an account?", - "userField":"Username:", - "pw2Field":"Enter password again:", - "dobField":"Date of birth:", - "createAccount":"Create account", - "alreadyHave":"Already have an account?", - "openClient":"Open Client", - "welcomeJank":"Welcome to Jank Client", - "box1title":"Jank Client is a Spacebar-compatible client seeking to be as good as it can be with many features including:", - "box1Items":"Direct Messaging|Reactions support|Invites|Account switching|User settings|Developer portal|Bot invites|Translation support", - "compatableInstances":"Spacebar-Compatible Instances:", - "box3title":"Contribute to Jank Client", - "box3description":"We always appreciate some help, whether that be in the form of bug reports, or code, or even just pointing out some typos." - }, - "register":{ - "passwordError:":"Password: $1", - "usernameError":"Username: $1", - "emailError":"Email: $1", - "DOBError":"Date of Birth: $1", - "agreeTOS":"I agree to the [Terms of Service]($1):", - "noTOS":"This instance has no Terms of Service, accept ToS anyways:" - }, - "leaving":"You're leaving Spacebar", - "goingToURL":"You're going to $1. Are you sure you want to go there?", - "goThere":"Go there", - "goThereTrust":"Go there and trust in the future", - "nevermind":"Nevermind", - "submit":"submit", - "guild":{ - "copyId":"Copy guild id", - "markRead":"Mark as read", - "notifications":"Notifications", - "leave":"Leave guild", - "settings":"Settings", - "delete":"Delete guild", - "makeInvite":"Make invite", - "settingsFor":"Settings for $1", - "name:":"Name:", - "topic:":"Topic:", - "icon:":"Icon:", - "overview":"Overview", - "banner:":"Banner:", - "region:":"Region:", - "roles":"Roles", - "selectnoti":"Select notifications type", - "all":"all", - "onlyMentions":"only mentions", - "none":"none", - "confirmLeave":"Are you sure you want to leave?", - "yesLeave":"Yes, I'm sure", - "noLeave":"Nevermind", - "confirmDelete":"Are you sure you want to delete $1?", - "serverName":"Name of server:", - "yesDelete":"Yes, I'm sure", - "noDelete":"Nevermind", - "create":"Create guild", - "loadingDiscovery":"Loading...", - "disoveryTitle":"Guild discovery ($1) {{PLURAL:$1|entry|entries}}", - "emptytitle":"Weird spot", - "emptytext":"You're in a weird spot, this guild has no channels", - "default":"Default ($1)", - "description:":"Description:", - "systemSelect:":"Systems messages channel:", - "sendrandomwelcome?":"Send a random message when someone joins this guild", - "stickWelcomeReact?":"Prompt members of your guild to react with a sticker when someone joins!", - "boostMessage?":"Send a message when someone boosts your guild!", - "helpTips?":"Send helpful tips for your guild!", - "defaultNoti":"Set the default notification settings of your guild!" - }, - "role":{ - "displaySettings":"Display settings", - "name":"Role name:", - "hoisted":"Hoisted:", - "mentionable":"Allow anyone to ping this role:", - "color":"Color", - "remove":"Remove role", - "delete":"Delete Role", - "confirmDelete":"Are you sure you want to delete $1?" - }, - "settings":{ - "unsaved":"Careful, you have unsaved changes", - "save":"Save changes" - }, - "localuser":{ - "settings":"Settings", - "userSettings":"User Settings", - "themesAndSounds":"Themes & Sounds", - "theme:":"Theme", - "notisound":"Notification sound:", - "accentColor":"Accent color:", - "enableEVoice":"Enable experimental Voice support", - "VoiceWarning":"Are you sure you want to enable this, this is very experimental and is likely to cause issues. (this feature is for devs, please don't enable if you don't know what you're doing)", - "updateSettings":"Update Settings", - "swSettings":"Service Worker setting", - "SWOff":"Off", - "SWOffline":"Offline only", - "SWOn":"On", - "clearCache":"Clear cache", - "CheckUpdate":"Check for updates", - "accountSettings":"Account Settings", - "2faDisable":"Disable 2FA", - "badCode":"Invalid code", - "2faEnable":"Enable 2FA", - "2faCode:":"Code:", - "setUp2fa":"2FA Setup", - "badPassword":"Incorrect password", - "setUp2faInstruction":"Copy this secret into your totp(time-based one time password) app", - "2faCodeGive":"Your secret is: $1 and it's 6 digits, with a 30 second token period", - "changeDiscriminator":"Change discriminator", - "newDiscriminator":"New discriminator:", - "changeEmail":"Change email", - "password:":"Password", - "newEmail:":"New email", - "changeUsername":"Change username", - "newUsername":"New username:", - "changePassword":"Change password", - "oldPassword:":"Old password:", - "newPassword:":"New password:", - "PasswordsNoMatch":"Password don't match", - "disableConnection":"This connection has been disabled server-side", - "devPortal":"Developer Portal", - "createApp":"Create application", - "team:":"Team:", - "appName":"Application name:", - "description":"Description:", - "privacyPolcyURL":"Privacy policy URL:", - "TOSURL":"Terms of Service URL:", - "publicAvaliable":"Make bot publicly inviteable?", - "requireCode":"Require code grant to invite the bot?", - "manageBot":"Manage bot", - "addBot":"Add bot", - "confirmAddBot":"Are you sure you want to add a bot to this application? There's no going back.", - "confuseNoBot":"For some reason, this application doesn't have a bot (yet).", - "editingBot":"Editing bot $1", - "botUsername":"Bot username:", - "botAvatar":"Bot avatar:", - "resetToken":"Reset Token", - "confirmReset":"Are you sure you want to reset the bot token? Your bot will stop working until you update it.", - "tokenDisplay":"Token: $1", - "saveToken":"Save token to localStorage", - "noToken":"Don't know token so can't save it to localStorage, sorry", - "advancedBot":"Advanced Bot Settings", - "botInviteCreate":"Bot Invite Creator", - "language":"Language:", - "connections":"Connections" - }, - "message":{ - "reactionAdd":"Add reaction", - "delete":"Delete message", - "edit":"Edit message", - "edited":"(edited)" - }, - "instanceStats":{ - "name":"Instance stats: $1", - "users":"Registered users: $1", - "servers":"Servers: $1", - "messages":"Messages: $1", - "members":"Members: $1" - }, - "inviteOptions":{ - "title":"Invite People", - "30m":"30 Minutes", - "1h":"1 Hour", - "6h":"6 Hours", - "12h":"12 Hours", - "1d":"1 Day", - "7d":"7 Days", - "30d":"30 Days", - "never":"Never", - "limit":"$1 {{PLURAL:$1|use|uses}}", - "noLimit":"No limit" - }, - "2faCode":"2FA code:", - "invite":{ - "invitedBy":"You've been invited by $1", - "alreadyJoined":"Already joined", - "accept":"Accept", - "noAccount":"Create an account to accept the invite", - "longInvitedBy":"$1 invited you to join $2", - "loginOrCreateAccount":"Login or create an account ⇌", - "joinUsing":"Join using invite", - "inviteLinkCode":"Invite Link/Code", - "subtext":"to $1 in $2", - "expireAfter":"Expire after:", - "channel:":"Channel:", - "inviteMaker":"Invite Maker", - "createInvite":"Create invite" - }, - "friends":{ - "blocked":"Blocked", - "blockedusers":"Blocked Users:", - "addfriend":"Add Friend", - "addfriendpromt":"Add friends by username:", - "notfound":"User not found", - "discnotfound":"Discriminator not found", - "pending":"Pending", - "pending:":"Pending friend requests:", - "all":"All", - "all:":"All friends:", - "online":"Online", - "online:":"Online friends:", - "friendlist":"Friend List", - "friends":"Friends" - }, - "replyingTo":"Replying to $1", - "DMs":{ - "copyId":"Copy DM id", - "markRead":"Mark as read", - "close":"Close DM", - "name":"Direct Messages" - }, - "user":{ - "copyId":"Copy user ID", - "online":"Online", - "offline":"Offline", - "message":"Message user", - "block":"Block user", - "unblock":"unblock user", - "friendReq":"Friend request", - "kick":"Kick member", - "ban":"Ban member", - "addRole":"Add roles", - "removeRole":"Remove roles" - }, - "login":{ - "checking":"Checking Instance", - "allGood":"All good", - "invalid":"Invalid Instance, try again", - "waiting":"Waiting to check Instance" - }, - "member":{ - "kick":"Kick $1 from $2", - "reason:":"Reason:", - "ban":"Ban $1 from $2" - }, - "uploadFilesText":"Upload your files here!", - "errorReconnect":"Unable to connect to the server, retrying in **$1** seconds...", - "retrying":"Retrying...", - "unableToConnect":"Unable to connect to the Spacebar server. Please try logging out and back in." + "pinMessage": "Pin message", + "profile": { + "bio": "About me:", + "joined": "Account made: $1", + "joinedMember": "Joined $1: $2", + "mut": "Mutual guilds", + "mutFriends": "Mutual friends", + "permInfo": "Permissions", + "userInfo": "User info" + }, + "profileColor": "Profile color", + "pronouns": "Pronouns:", + "readableName": "English", + "recentEmoji": "Recent emojis", + "register": { + "DOBError": "Date of birth: $1", + "agreeTOS": "I agree to the [Terms of Service]($1):", + "emailError": "Email: $1", + "noTOS": "This instance has no Terms of Service, accept ToS anyways:", + "passwordError:": "Password: $1", + "register": "Register", + "tos": "You must agree to the TOS", + "usernameError": "Username: $1" + }, + "remove": "Remove", + "renderJoinAvatars": "Display avatars for new members: ", + "reply": "Reply", + "replyingTo": "Replying to $1", + "retrying": "Retrying...", + "role": { + "color": "First color", + "colors": { + "name": "Color scheme:", + "one": "Monochrome", + "two": "Bicolor", + "three": "Tricolor", + "secondColor": "Second color:", + "thirdColor": "Third color:" + }, + "confirmDelete": "Are you sure you want to delete $1?", + "copyId": "Copy role ID", + "delete": "Delete Role", + "deleting": "Deleting role...", + "displaySettings": "Display settings", + "hoisted": "Display role members separately from online members (they will be hoisted using the highest role group they're a part of):", + "mentionable": "Allow anyone to ping this role:", + "name": "Role name:", + "perms": "Permissions", + "remove": "Remove role", + "roleEmoji": "Role emoji:", + "roleFileIcon": "Role icon:", + "roles": "Roles" + }, + "search": { + "back": "Back", + "new": "New", + "next": "Next", + "nofind": "There seems to be no messages that match your search, maybe trying broadening your search to try and find what you want", + "old": "Old", + "page": "Page $1", + "search": "Search" + }, + "searchGifs": "Search $1", + "settings": { + "clear": "Clear", + "img": "Upload image", + "save": "Save changes", + "unsaved": "Careful, you have unsaved changes", + "updates": { + "serviceWorkerMode": { + "disabled": "Off", + "enabled": "On", + "offlineOnly": "Offline only", + "title": "Service Worker setting", + "unregistered": "Off (including media cache)" + } + } + }, + "showBlockedMessages": "You have this user blocked, click to see the $1 blocked {{PLURAL:$1|message|messages}}.", + "spoiler": "Spoiler", + "sticker": { + "confirmDel": "Are you sure you want to delete this sticker?", + "del": "Delete sticker", + "desc": "Description", + "errEmjMust": "Must pair an emoji with your sticker", + "errFileMust": "Must include an image for your sticker", + "image": "Image:", + "name": "Name:", + "tags": "Associated emoji: ", + "title": "Stickers", + "upload": "Upload stickers" + }, + "submit": "Submit", + "switchAccounts": "Switch accounts ⇌", + "todayAt": "Today at $1", + "trace": { + "totalTime": "$2: $1ms", + "traces": "$1 ($2ms) $3" + }, + "typing": "$2 {{PLURAL:$1|is|are}} typing", + "unableToConnect": "Unable to connect to the Spacebar server. Please try logging out and back in.", + "unableToPin": "Unable to pin message", + "unpinMessage": "Unpin message", + "updateAv": "Updates available", + "uploadBanner": "Upload banner:", + "uploadFilesText": "Upload your files here!", + "uploadPfp": "Upload avatar:", + "useTemplate": "Use $1 as a template", + "useTemplateButton": "Use template", + "usedFermi": "sent via Fermi", + "user": { + "report":"Report user", + "deleted":"Deleted user", + "reportApp":"Report application", + "addRole": "Add roles", + "ban": "Ban member", + "block": "Block user", + "confirmInstBan": "Are you sure you want to instance ban $1?", + "copyId": "Copy user ID", + "dnd": "Do Not Disturb", + "editNick": "Edit nickname", + "editServerProfile": "Edit guild profile", + "friendReq": "Friend request", + "idle": "Idle", + "instanceBan": "Instance ban", + "invisible": "Invisible", + "kick": "Kick member", + "message": "Message user", + "nick": { + "foe": "Set foe nickname", + "friend": "Set friend nickname", + "stalker": "Set nickname of requester", + "stalking": "Set nickname of requested" + }, + "offline": "Offline", + "online": "Online", + "remove": "Remove user", + "removeRole": "Remove roles", + "sendMessage": "Message @$1", + "timeout": "Timeout member", + "unTimeout": "Remove timeout on member", + "unban": "Unban $1", + "unblock": "Unblock user" + }, + "userping": { + "resolving": "resolving user", + "unknown": "@unknown-user" + }, + "vc": { + "joinForStream": "Join the voice channel to watch", + "joiningStream": "Joining stream...", + "joinstream": "Watch stream", + "leavestream": "Leave stream", + "stopstream": "Stop stream" + }, + "webauth": { + "addKey": "Add key", + "keyname": "Key name:", + "manage": "Manage security keys" + }, + "webhook": "WEBHOOK", + "webhooks": { + "EnterWebhookName": "Enter webhook name", + "areYouSureDelete": "Are you sure you want to delete $1?", + "avatar": "Avatar", + "base": "Webhooks", + "channel": "Webhook outputs to channel:", + "copyURL": "Copy webhook link", + "createdAt": "Created at $1", + "createdBy": "Created by:", + "deleteWebhook": "Delete webhook", + "name": "Webhook name:", + "newWebHook": "New webhook", + "notFound": "User no longer is in the guild", + "sillyDefaults": "Captain Hook\nBillie Hooks\nSpidey bot", + "token": "Webhook token: `$1`", + "type": "Type: $1", + "type1": "Incoming", + "type2": "Channel follower", + "type3": "Application", + "url": "Webhook link: `$1`" + }, + "welcomeMessages": "Welcome $1! Nice to see ya!\nHello, nice to meet you $1!\nWelcome, hope you brought pizza $1!", + "widget": "Guild widget", + "widgetEnabled": "Widget enabled", + "yes": "Yes", + "yesterdayAt": "Yesterday at $1" } diff --git a/translations/fr.json b/translations/fr.json new file mode 100644 index 00000000..beb540d1 --- /dev/null +++ b/translations/fr.json @@ -0,0 +1,977 @@ +{ + "@metadata": { + "authors": [ + "Asteralee", + "Bananax47", + "Codeurluce", + "Derugon", + "EmDashUser002", + "Gomoko", + "Goombiis", + "Lenchanteur", + "Mahabarata", + "McDutchie", + "Momo50WM", + "Od1n", + "Oh64", + "Pyschobbens", + "Rgoplay", + "Rwobweats", + "Sylvie", + "TheGeekn°72", + "Urhixidur", + "WillyM", + "Wladek92" + ] + }, + "2faCode": "Code A2F :", + "404": { + "404": "erreur:404 - Page introuvable", + "app": "Vers l'application", + "blog": "Le blog de Fermi", + "home": "Page d'accueil", + "listtitle": "Tu voulais peut-être aller dans un de ces endroits", + "login": "Se connecter", + "report": "Si vous avez trouvé cette page dans le client, veuillez le signaler :", + "reset": "Page de réinitialisation du mot de passe", + "title": "Il semble que tu sois perdu", + "whatelse": "Que pensez-vous qu'il devrait se passer d'autre ?", + "whereever": "Où que ce soit" + }, + "onboarding": { + "name": "Introduction", + "disable": "Désactiver l'introduction", + "addChannel": "Ajouter un canal", + "channel": "Canal :", + "desc": "Description :", + "deleteChannel": "Supprimer le canal de l'introduction", + "enable": "Activer l'introduction", + "title": "Bienvenue sur $1 !" + }, + "DMs": { + "add": "Ajouter une personne à ce MP", + "close": "Fermer le MP", + "copyId": "Copier l'identifiant du MP", + "markRead": "Marquer comme lu", + "name": "Messages privés" + }, + "ok": "OK", + "dismiss": "Ignorer", + "UrlGen": "Générateur d'invitations de bot", + "Voice": { + "userVol": "Volume d'utilisateurs :", + "status": { + "badWS": "Réponse incorrecte du WS", + "conectionFailed": "La connexion a échoué", + "done": "Connecté", + "left": "A quitté le chat vocal", + "makingOffer": "Appel en cours de configuration", + "noSDP": "SDP introuvable", + "notconnected": "Non connecté", + "sendingStreams": "Envoi des flux audio", + "startingRTC": "Démarrage de la connexion RTC", + "waitingMainWS": "En attente du WS principal", + "waitingURL": "En attente du lien du vocal", + "wsAuth": "En attente d'autorisation du WS", + "wsOpen": "En attente de l'ouverture du WS" + } + }, + "accessibility": { + "gifSettings": { + "always": "Toujours", + "hover": "Au survol", + "never": "Jamais" + }, + "name": "Accessibilité", + "playGif": "Jouer les GIFs :", + "playIcon": "Jouer les icônes animées :", + "roleColors": "Désactiver les couleurs des rôles :", + "gradientColors": "Désactiver la coloration en dégradé :", + "decorations": "Activer les décorations d'avatar :" + }, + "accountNotStart": "Impossible de démarrer le compte", + "add": "Ajouter", + "applications": { + "delete": "Supprimer l'application", + "sure": "Voulez-vous vraiment supprimer $1 ?" + }, + "badge": { + "active_developer": "Développeur actif", + "application_guild_subscription": "a Premium", + "automod": "utilise l'automod", + "bot_commands": "prend en charge les commandes", + "bug_hunter_level_1": "Chasseur de bugs niveau 1", + "bug_hunter_level_2": "Chasseur de bugs niveau 2", + "certified_moderator": "Modérateur", + "early_supporter": "Supporteur de la première heure", + "guild_booster_lvl1": "Guilde boostée", + "guild_booster_lvl2": "Guilde boostée", + "guild_booster_lvl3": "Guilde boostée", + "guild_booster_lvl4": "Guilde boostée", + "guild_booster_lvl5": "Guilde boostée", + "guild_booster_lvl6": "Guilde boostée", + "guild_booster_lvl7": "Guilde boostée", + "guild_booster_lvl8": "Guilde boostée", + "guild_booster_lvl9": "Guilde boostée depuis un bon moment", + "hypesquad": "Vibesquad [TEMP]", + "hypesquad_house_1": "Audace", + "hypesquad_house_2": "Excellence", + "hypesquad_house_3": "Équilibre", + "legacy_username": "a un nom d'utilisateur d'origine", + "partner": "Partenaire de l'instance", + "premium": "Premium", + "quest_completed": "a fait une quête", + "staff": "Personnel de l'instance", + "verified_developer": "Développeur vérifié" + }, + "bio": "Bio :", + "blankMessage": "Message vide", + "blog": { + "blog": "Blog", + "blogUpdates": "Actualités du blog :", + "desc": "Lisez ou abonnez-vous au blog pour rester informé des actualités de Fermi !", + "fermi": "Le blog sur Fermi", + "gotoPost": "Aller à la publication", + "wantUpdates": "Souhaiteriez-vous recevoir des actualités hebdomadaires du blog à propos de l'état de Fermi ?" + }, + "bot": "BOT", + "botGuilds": "Guildes où le bot est présent :", + "botSettings": "Paramètres du bot", + "cancel": "Annuler", + "channel": { + "SlowmodeCool": "Temporisation du mode lent : $1", + "TimeOutCool": "Exclu jusqu'à : $1", + "allowIcons": "Autoriser les icônes de canal personnalisées", + "announcement": "Annonces", + "copyId": "Copier l'id du canal", + "copyIdCat": "Copier l'id de la catégorie", + "createCatagory": "Créer une catégorie", + "createChannel": "Créer un canal", + "creating": "Création du canal", + "delete": "Supprimer le canal", + "deleteCat": "Supprimer la catégorie", + "icon": "Icône :", + "makeInvite": "Créer une invitation", + "markRead": "Marquer comme lu", + "mute": "Rendre le canal muet", + "name": "Canal", + "name:": "Nom du canal :", + "nsfw:": "Avertissement contenu mature :", + "permissions": "Permissions", + "selectCatName": "Nom de la catégorie", + "selectName": "Nom du canal", + "selectType": "Sélectionnez le type de canal", + "settings": "Paramètres", + "settingsFor": "Paramètres de $1", + "slowmode": "Mode ralenti :", + "text": "Texte", + "timedOutUntil": "Exclu jusqu'à : $1", + "topic:": "Sujet :", + "typebox": "Chatter dans $1", + "unmute": "Rétablir les notifications du canal", + "voice": "Voix", + "deleteThread": "Supprimer le fil", + "hideThreads": "Masquer si inactif plus de", + "forum": "Forum" + }, + "threads": { + "leave": "Quitter le fil", + "join": "Rejoindre le fil", + "name": "Nom du fil :", + "editTags": "Modifier les tags" + }, + "forum": { + "creorsear": "Créer ou rechercher une publication", + "next": "Suivant", + "back": "Retour", + "newPost": "Faire une nouvelle publication", + "post": "Publier", + "sortOptions": { + "sortby": { + "title": "Trier par", + "recent": "Récemment actif", + "posted": "Date de publication" + }, + "sortOrder": { + "title": "Ordre de tri", + "recent": "Plus récent d'abord", + "old": "Plus ancien d'abord" + }, + "tagMatch": { + "title": "Correspondance des tags", + "some": "Correspond à un tag ou plus", + "all": "Correspond à tous les tags" + }, + "name": "Options de tri" + }, + "errors": { + "tagsReq": "Tags requis", + "requireText": "Le message ne peut pas être vide" + }, + "allTags": "Tous les tags", + "settings": { + "editTags": "Modifier les tags :", + "editTag": "Modifier le tag", + "tagName": "Nom du tag :", + "moderated": "Ne permettre qu'aux modérateurs de fil d'appliquer ce tag", + "addTag": "Ajouter un tag", + "delTag": "Supprimer un tag" + } + }, + "channelLink": "# $1", + "clientDesc": "Version du client : $1\n\n[Rejoignez la guilde officielle du client Fermi]($2/invite/USgYJo?instance=https%3A%2F%2Fspacebar.chat)\n\n[Contribuez à la traduction du client Fermi](https://translatewiki.net/wiki/Translating:JankClient#sortable:3=desc) \n\n[Contribuez au client Fermi](https://github.com/MathMan05/Fermi)\n\n[Contribuez au serveur Spacebar (l'infrastructure derrière Fermi)](https://github.com/spacebarchat/server)\n\n[Lisez le blog](https://blog.fermi.chat/)\n\nDroits calculés : $3", + "commands": { + "errorNotValid": "$1 n'est pas un choix valide pour $2", + "required": "$1 est une partie requise de cette commande" + }, + "confirmGuildLeave": "Êtes-vous sûr·e de vouloir quitter $1", + "copyLink": "Copier le lien du message", + "copyRegLink": "Copier le lien", + "copymessageid": "Copier l'id du message", + "copyrawtext": "Copier le texte brut", + "createAccount": "Créer un compte", + "delete": "Supprimer", + "deleteConfirm": "Êtes-vous sûr·e de vouloir supprimer ceci ?", + "devSettings": { + "badUser": "Activer la journalisation des objets utilisateur incorrects qui envoient trop d’informations :", + "cache": "Activer les fichiers de mapping du cache du Service Worker", + "cacheDesc": "les fichiers de mapping seront toujours chargés, cela s'assurera juste qu'ils sont en cache quand une nouvelle mise à jour est disponible.", + "captureTrace": "Cette option indique à Fermi de capturer les propriétés _trace depuis le serveur. Son activation peut empêcher le décodage progressif du JSON de fonctionner (un rechargement peut être nécessaire).", + "clearWellKnowns": "Re-charger les enregistrements .well-known", + "description": "Ces paramètres sont destinés aux développeurs de Spacebar/Fermi. Si vous ne savez pas ce que vous faites, ne les modifiez pas.", + "gatewayComp": "Désactiver la compression de la passerelle", + "reportSystem": "Activer le système de rapport expérimental", + "logGateway": "Journaliser les événements reçus par la passerelle (niveau de log : info)", + "name": "Paramètres développeur", + "traces": "Affichage des traces :" + }, + "deviceManage": { + "city": "Ville : $1", + "continent": "Continent : $1", + "country": "Pays : $1", + "curSes": "Ceci est la session en cours, vous devez vous déconnecter via l'autre menu", + "estimateWarn": "Avertissement : Toutes ces informations sont approximatives, elles peuvent donc être inexactes.", + "ip": "Dernière adresse IP connue : $1", + "last": "Dernière utilisation approximative : $1", + "latitude": "Latitude : $1", + "logout": "Déconnecter cet appareil", + "longitude": "Longitude : $1", + "manageDev": "Gérer l'appareil", + "postal": "Code postal : $1", + "region": "Région : $1", + "title": "Gérer les sessions" + }, + "discovery": "Découverte", + "dms": "Messages privés", + "edit": "Modifier", + "emoji": { + "confirmDel": "Êtes-vous sûr de vouloir supprimer cet emoji ?", + "found": { + "not": "Cet émoji ne provient pas de vos guildes, rejoignez la guilde qui l'héberge pour pouvoir l'utiliser", + "other": "Cet emoji provient d'une de vos autres guildes.", + "private": "La guilde associée à cet émoji est introuvable", + "this": "Cet émoji provient de cette guilde." + }, + "from": "Cet émoji provient de", + "image:": "Image :", + "join": "Rejoindre cette guilde", + "name:": "Nom :", + "privateGuild": "Cette guilde est privée", + "title": "Émojis", + "upload": "Importer des émojis" + }, + "emojiSelect": "Émoji :", + "errorReconnect": "Impossible de se connecter au serveur, nouvelle tentative dans **$1** secondes...", + "favoriteGifs": "GIFs favoris", + "folder": { + "color": "Couleur du dossier :", + "create": "Créer un nouveau dossier", + "edit": "Modifier le dossier", + "name": "Nom du dossier :" + }, + "form": { + "captcha": "Attendez, êtes-vous un humain ?" + }, + "friends": { + "addfriend": "Ajouter un ami", + "addfriendpromt": "Ajouter des amis par nom d'utilisateur :", + "requestsent": "Demande envoyée!", + "all": "Tous", + "all:": "Tous les amis :", + "blocked": "Bloqué", + "blockedusers": "Utilisateurs bloqués :", + "bu": "Utilisateur bloqué", + "discnotfound": "Discriminateur introuvable", + "friendlist": "Liste d'amis", + "friends": "Amis", + "notfound": "Utilisateur non trouvé", + "online": "En ligne", + "online:": "Amis en ligne :", + "pending": "En attente", + "pending:": "Demandes d'amis en attente :", + "removeFriend": "Supprimer cet ami" + }, + "goThere": "Oui, allons-y", + "goThereTrust": "Oui & faire confiance à ce domaine désormais.", + "goingToURL": "Cet hyperlien vous envoie vers $1. Êtes-vous sûr·e de vouloir y aller ?", + "group": { + "createdm": "Créer un MP!", + "edit": "Modifier le chat de groupe", + "select": "Sélectionnez des amis" + }, + "guild": { + "COMMUNITY": "Demander à rejoindre", + "disableInvites": "Désactiver les invitations :", + "DISCOVERABLE": "Découverte", + "INVITES_DISABLED": "Sur invitation uniquement", + "adminMenu": { + "changePerms": "Modifier les permissions pour rechercher", + "finding": "Trouver des administrateurs", + "ownName": "Propriétaire", + "owner": "Trouver le propriétaire", + "permission": "Permissions :" + }, + "report": "Signaler la guilde", + "admins": "Trouver des administrateurs", + "all": "Toutes", + "banReason": "Motif du bannissement : $1", + "bannedBy": "Banni par :", + "banner:": "Bannière :", + "bans": "Bans", + "boostMessage?": "Envoyer un message lorsque quelqu'un booste votre guilde !", + "community": "Communauté", + "confirmDelete": "Voulez-vous vraiment supprimer $1 ?", + "confirmLeave": "Êtes-vous sûr·e de vouloir partir ?", + "copyId": "Copier l'identifiant de la guilde", + "create": "Créer une guilde à partir de zéro", + "createFromTemplate": "Créer une guilde à partir d'un modèle", + "createNewTemplate": "Créer un nouveau modèle", + "creating": "Création de la guilde", + "default": "Par défaut ($1)", + "defaultNoti": "Définissez les paramètres de notification par défaut de votre guilde!", + "delete": "Supprimer la guilde", + "description:": "Description :", + "disoveryTitle": "Découverte des guildes ($1 {{PLURAL:$1|élément|éléments}})", + "editingTemplate": "Modification de $1", + "emptytext": "Curieux, cette guilde n'a pas de canaux !?", + "emptytitle": "Endroit étrange", + "guilds": "Guildes", + "helpTips?": "Envoyez des conseils utiles à votre guilde !", + "howJoin": "Comment les gens peuvent rejoindre votre guilde ?", + "icon:": "Icône :", + "invites": "Invitations", + "joinConfirm": "Rejoindre $1 ?", + "leave": "Quitter la guilde", + "loadingDiscovery": "Chargement...", + "makeInvite": "Créer une invitation", + "markRead": "Marquer comme lu", + "memberCount": "$1 membre{{PLURAL:$1||s}}", + "mute": "Rendre la guilde muette", + "name:": "Nom :", + "nameNoMatch": "Les noms ne correspondent pas", + "noDelete": "Annuler", + "noLeave": "Annuler", + "none": "Aucune", + "notifications": "Notifications", + "onlyMentions": "Seulement les mentions", + "overview": "Aperçu", + "region:": "Région :", + "roles": "Rôles", + "ruleId": "Canal des règles :", + "selectnoti": "Sélectionner le type de notification", + "sendrandomwelcome?": "Envoyer un message aléatoire lorsque quelqu'un rejoint cette guilde :", + "serverName": "Nom de la guilde :", + "settings": "Paramètres", + "settingsFor": "Paramètres de $1", + "splash": "Bannière Découverte :", + "stickWelcomeReact?": "Invitez les membres de votre guilde à réagir avec un autocollant lorsque quelqu'un la rejoint!", + "systemSelect:": "Canal de messages systèmes :", + "tempCreatedBy": "Modèle créé par :", + "tempUseCount": "Le modèle a été utilisé $1 fois{{PLURAL:$1}}", + "template": "Link/code du modèle de guilde à créer :", + "templateDesc": "Description du modèle :", + "templateName": "Nom du modèle :", + "templateNameShort": "Le nom du modèle doit comporter au moins 2 caractères", + "templateURL": "Lien du modèle : $1", + "templates": "Modèles", + "templcateMetaDesc": "Un modèle permet aux autres d’utiliser cette guilde comme base pour leurs propres guildes : cela va copier les canaux, les rôles et les paramètres, mais pas les messages, les bots ou l’icône de la guilde.", + "topic:": "Sujet :", + "unknownRole": "@rôle-inconnu", + "viewTemplate": "Afficher le modèle", + "yesDelete": "Oui, je suis sûr·e", + "yesLeave": "Oui, je suis sûr·e" + }, + "hideBlockedMessages": "Vous avez bloqué cet utilisateur, cliquez pour masquer ces messages.", + "home": { + "icon": "Icône de l’instance de $1", + "uptimeStats": "Temps de disponibilité : \n au total : $1% \n cette semaine : $2% \n aujourd'hui : $3%", + "warnOffiline": "L'instance est hors ligne, impossible de se connecter" + }, + "htmlPages": { + "addBot": "Ajouter à la guilde", + "alreadyHave": "Vous avez déjà un compte ?", + "box1Items": "Messagerie en direct|Prise en charge des réactions|Invitations|Changement de compte|Paramètres utilisateur|Portail des développeurs|Invitations de bots|Prise en charge des traductions", + "box1title": "Le client Fermi est un client compatible avec Spacebar qui cherche à être aussi bon que possible avec de nombreuses fonctionnalités, notamment :", + "box3description": "Nous apprécierons toujours votre aide, que ce soit sous forme de rapports de bugs, de code ou même simplement en signalant des fautes de frappe.", + "box3title": "Contribuer au client Fermi", + "compatableInstances": "Instances compatibles avec Spacebar :", + "createAccount": "Créer un compte", + "dobField": "Date de naissance :", + "emailField": "E-mail :", + "idpermissions": "Cela permettra au bot de :", + "instanceField": "Instance :", + "loaddesc": "Cela ne devrait pas prendre longtemps", + "loadingText": "Le client Fermi est en cours de chargement", + "loginButton": "Se connecter", + "noAccount": "Vous n'avez pas de compte ?", + "openClient": "Ouvrir le client", + "pw2Field": "Entrez à nouveau le mot de passe :", + "pwField": "Mot de passe :", + "switchaccounts": "Changer de compte", + "trans": "Traduire", + "transDesc": "Vous pouvez aider à traduire le client Fermi dans votre propre langue !", + "transTitle": "Traduire le client Fermi", + "userField": "Nom d’utilisateur :", + "welcomeJank": "Bienvenue dans le client Fermi" + }, + "incorrectURLS": "## Cette instance a probablement envoyé des liens incorrects.\n### Si vous êtes le propriétaire de l’instance, veuillez consulter la section *Connecting from remote machines* de [cette page](https://docs.spacebar.chat/setup/server/) pour corriger le problème.\n Souhaitez-vous que le client Fermi tente automatiquement de corriger cette erreur pour vous permettre de vous connecter à l’instance ?", + "instInfo": "Informations d’instance", + "instanceInfo": { + "contact": "Envoyer un e-mail aux administrateurs de l’instance", + "frontPage": "[Page d'accueil de l'instance]($1)", + "name": "Informations de l'instance", + "publicUrl": "[Url publique]($1)", + "tosPage": "[Conditions Générales d’Utilisation]($1)" + }, + "instanceStats": { + "members": "Membres : $1", + "messages": "Messages : $1", + "name": "Stats d'instance : $1", + "servers": "Guildes : $1", + "users": "Utilisateurs enregistrés : $1" + }, + "interactions": { + "ephemeralDismiss": "Ignorer le message", + "failed": "L'interaction a échoué", + "nonsence": "Le serveur a envoyé un composant de type $1 qui n'existe pas", + "notImpl": "Fermi n'a pas encore implémenté le composant de type $1", + "onlyYou": "Vous seul pouvez voir ce message", + "replyline": "Interaction commencée", + "started": "Traitement de l'interaction...", + "worked": "L'interaction a réussi" + }, + "invite": { + "accept": "Accepter", + "alreadyJoined": "Déjà rejoint", + "channel:": "Canal :", + "createInvite": "Créer une invitation", + "createdAt": "Créée le : $1", + "expireAfter": "Expire après :", + "expires": "Expire le : $1", + "forChannel": "Pour le canal : $1", + "inviteLinkCode": "Lien/code d'invitation", + "inviteMaker": "Créateur d'invitations", + "invitedBy": "Vous avez été invité par $1", + "joinUsing": "Rejoindre une guilde en utilisant une invitation", + "loginOrCreateAccount": "Connectez-vous ou créez un compte ⇌", + "longInvitedBy": "$1 vous a invité à rejoindre $2", + "maxUses": "Utilisations maximales : $1", + "never": "Jamais", + "noAccount": "Créez un compte pour accepter l'invitation", + "notFound": "Invitation introuvable", + "subtext": "à $1 sur $2", + "used": "Utilisée $1 fois{{PLURAL:$1|}} :" + }, + "inviteOptions": { + "12h": "12 heures", + "1d": "1 jour", + "1h": "1 heure", + "30d": "30 jours", + "30m": "30 minutes", + "6h": "6 heures", + "7d": "7 jours", + "limit": "$1 utilisation{{PLURAL:$1||s}}", + "never": "Jamais", + "noLimit": "Sans limite", + "title": "Inviter des gens" + }, + "jankInfo": "Informations du client", + "leaveGuild": "Quitter la guilde", + "leaving": "Vous quittez Spacebar", + "loaded": "Chargé", + "localuser": { + "2faCode:": "Code :", + "2faCodeGive": "Votre secret TOTP est : $1. Cela va génèrer un jeton A2F à 6 chiffres avec période d'expiration de 30 secondes.", + "2faDisable": "Désactiver l'authentification à deux facteurs", + "2faEnable": "Activer l'authentification à deux facteurs", + "CheckUpdate": "Vérifier les mises à jour", + "PasswordsNoMatch": "Les mots de passe ne correspondent pas", + "TOSURL": "Lien vers les Conditions Générales d'Utilisation :", + "VoiceWarning": "Êtes-vous sûr·e de vouloir activer cette option ? C'est très expérimental et est susceptible de causer des problèmes (cette fonctionnalité est destinée aux développeurs, veuillez ne pas l'activer si vous ne savez pas ce que vous faites)", + "accentColor": "Couleur d'accentuation du thème :", + "accountSettings": "Paramètres du compte", + "addBot": "Ajouter un bot", + "addStatus": "Ajouter un statut", + "advancedBot": "Paramètres avancés du bot", + "appName": "Nom de l’application :", + "areYouSureDelete": "Êtes-vous sûr·e de vouloir supprimer votre compte ? Si oui, saisissez la phrase $1", + "badCode": "Code invalide", + "badPassword": "Mot de passe incorrect", + "botAvatar": "Avatar du bot :", + "botInviteCreate": "Création d'invites de bot", + "botUsername": "Nom d'utilisateur du bot :", + "changeDiscriminator": "Changer le discriminateur", + "changeEmail": "Changer l’adresse e-mail", + "changePassword": "Changer de mot de passe", + "changeUsername": "Changer le nom d'utilisateur", + "clearCache": "Vider le cache", + "confirmAddBot": "Êtes-vous sûr·e de vouloir ajouter un bot à cette application ? Cette action est irréversible.", + "confirmReset": "Êtes-vous sûr·e de vouloir réinitialiser le jeton du bot ? Votre bot cessera de fonctionner jusqu'à ce que vous le mettiez à jour.", + "confuseNoBot": "Pour une raison quelconque, cette application n'a pas (encore) de bot.", + "connections": "Connexions", + "createApp": "Créer une application", + "customSound": "Son personnalisé", + "customStatusWarn": "Spacebar ne prend pas en charge l’affichage d’un statut personnalisé pour le moment. Même si le statut est accepté, le système ne l'affichera pas", + "deleteAccount": "Suppression du compte", + "deleteAccountButton": "Supprimer le compte", + "description": "Description :", + "devPortal": "Portail des développeurs", + "disableConnection": "Cette connexion a été désactivée côté serveur", + "editingBot": "Modifier le bot $1", + "install": "Installer", + "installDesc": "Installer le client Fermi vous permettra de l’ouvrir dans sa propre fenêtre et de le faire fonctionner comme une application séparée ! Vous pouvez également continuer à utiliser le client Fermi dans votre navigateur web comme auparavant, et le fonctionnement restera le même.", + "installJank": "Installer le client Fermi", + "language": "Langue :", + "manageBot": "Gérer le bot", + "manageInstance": "Gérer l’instance", + "mustTypePhrase": "Pour supprimer votre compte, vous devez saisir la phrase", + "newDiscriminator": "Nouveau discriminateur :", + "newEmail:": "Nouvel e-mail", + "newPassword:": "Nouveau mot de passe :", + "newUsername": "Nouveau nom d’utilisateur :", + "noToken": "Le jeton n'est pas connu par Fermi, impossible de le sauvegarder dans le stockage local.", + "noUpdates": "Aucune mise à jour trouvée", + "notisound": "Son de notification :", + "oldPassword:": "Ancien mot de passe :", + "password:": "Mot de passe", + "privacyPolcyURL": "Lien vers la politique de confidentialité :", + "appID": "ID d'application : $1", + "showSecret": "Afficher le secret du client", + "clientSecret": "Secret du client : $1", + "secret": "Secret", + "publicAvaliable": "Rendre le bot invitable publiquement ?", + "refreshPage": "Actualisez pour appliquer", + "requireCode": "Nécessite un code pour inviter le bot ?", + "resetToken": "Réinitialiser le jeton", + "saveToken": "Enregistrer le jeton dans le stockage local", + "setUp2fa": "Configurer l'authentification à deux facteurs", + "setUp2faInstruction": "Copiez ce secret dans votre application TOTP (\"jeton à usage unique basé sur le temps\") pour générer vos codes d'accès A2F", + "settings": "Paramètres", + "sillyDeleteConfirmPhrase": "Shrek est l'amour, Shrek est la vie", + "soundTooLarge": "Le son que vous avez essayé d'importer était trop volumineux, réessayez", + "status": "Statut", + "team:": "Équipe :", + "theme:": "Thème", + "themesAndSounds": "Thèmes et sons", + "tokenDisplay": "Jeton : $1", + "trace": "Traces", + "trusted": "Domaines de confiance", + "trustedDesc": "Lorsque vous cliquez sur un lien provenant de ces domaines, ils ne vous demanderont ***pas*** l'autorisation d'ouvrir le lien, contrairement aux autres liens. N'accordez cette autorisation qu'aux sites de confiance.", + "updateSettings": "Paramètres de mise à jour", + "updatesYay": "Mises à jour disponibles !", + "userSettings": "Votre profil public" + }, + "login": { + "allGood": "OK", + "checking": "Vérification de l'instance", + "enterPAgain": "Entrez à nouveau le mot de passe :", + "invalid": "Instance invalide, réessayez", + "login": "Se connecter", + "newPassword": "Nouveau mot de passe :", + "pasteInfo": "Collez le lien de récupération ici :", + "recover": "Mot de passe oublié ?", + "recovery": "Mot de passe oublié", + "waiting": "En attente de vérification de l'instance" + }, + "logout": { + "error": { + "cancel": "Annuler", + "cont": "Continuer quand même", + "desc": "Fermi n’a pas pu vous déconnecter,\n souhaitez-vous tout de même retirer le compte ?", + "title": "Une erreur s’est produite en essayant de vous déconnecter" + }, + "logout": "Se déconnecter" + }, + "manageInstance": { + "AreYouSureStop": "Êtes-vous sûr·e de vouloir arrêter cette instance ?", + "TokenFormats": { + "JSON": "JSON formaté", + "URLs": "Liens d’invitation", + "plain": "Texte brut" + }, + "clientURL": "Lien du client :", + "copy": "Copier", + "count": "Nombre :", + "create": "Créer", + "createTokens": "Créer des jetons d’enregistrement", + "format": "Format :", + "genericType": "Générique", + "length": "Longueur :", + "regType": "Enregistrer le type de lien du jeton", + "stop": "Arrêter l’instance" + }, + "media": { + "artist": "Artiste : $1", + "composer": "Compositeur : $1", + "download": "Télécharger le média", + "length": "Durée : $1 minute(s) et $2 seconde(s)", + "loading": "Chargement", + "moreInfo": "Plus d’infos", + "notFound": "Le média n’a pas pu être trouvé" + }, + "member": { + "ban": "Bannir $1 de $2", + "kick": "Expulser $1 de $2", + "nick:": "Surnom :", + "persist": "Supprimer uniquement l'utilisateur :", + "reason:": "Motif :", + "timeout": "Temps d'exclusion $1", + "timeoutTime": "Durée de l'exclusion :" + }, + "message": { + "messages": "$1 message{{PLURAL:$1||s}} >", + "andMore": "$1 et plus !", + "attached": "à envoyé une pièce jointe", + "delete": "Supprimer le message", + "report": "Signaler le message", + "deleted": "Message supprimé", + "edit": "Modifier le message", + "edited": "(modifié)", + "fullMessage": "Message complet :", + "pin": "$1 a épinglé un message", + "thread": "$1 a créé un fil: $2", + "reactedWith": "à réagi avec $1", + "reactionAdd": "Ajouter une réaction", + "createThread": "Créer un fil", + "threadOptions": "Options du fil", + "reactions": "Voir les réactions", + "reactionsTitle": "Réactions", + "retry": "Renvoyer le message", + "viewrest": "Afficher le reste" + }, + "report": { + "back": "Retour", + "next": "Suivant", + "cancel": "Annuler", + "submit": "Soumettre", + "blockUser": "Bloquer", + "timeout": "Exclure temporairement le membre", + "summary": "Résumé du signalement :" + }, + "messageLink": "Envoyer un message dans # $1", + "muteDuration": "Durée :", + "nevermind": "Annuler", + "no": "Non", + "noEmojiFont": "Utiliser les émojis système", + "noMessages": "Il n'y a pas encore de messages ici, soyez le premier à dire quelque chose !", + "noPins": "Il n'y a rien pour toi ici... pour l'instant", + "noStickers": "C'est dommage, il n'y a pas d'autocollants que vous puissiez utiliser !", + "notiClump": "Vous avez reçu $1 notifications de $2", + "notiVolume": "Volume des notifications :", + "otherAt": "$1 à $2", + "permissions": { + "descriptions": { + "ADMINISTRATOR": "Accorde TOUTES les permissions ci-dessous aux membres du rôle, contourne TOUTES les permissions individuelles des canaux. Seuls vous-même et les membres/bots de confiance absolue devraient avoir cette permission DANGEREUSE !!", + "MANAGE_GUILD": "Permet aux membres du rôle de gérer et éditer la guilde.", + "VIEW_AUDIT_LOG": "Permet aux membres du rôle de visualiser le journal d'audit.", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Permet aux membres du rôle de visualiser les informations sur les abonnements aux rôles.", + "VIEW_GUILD_INSIGHTS": "Permet aux membres du rôle de voir les informations de la guilde.", + "BYPASS_SLOWMODE": "Le mode lent n'a plus d'effet sur les membres du rôle.", + "MENTION_EVERYONE": "Permet aux membres du rôle de mentionner @everyone.", + "MANAGE_NICKNAMES": "Permet aux membres du rôle de modifier les surnoms d'autres membres.", + "PIN_MESSAGES": "Permet aux membres du rôle d'épingler des messages.", + "SEND_POLLS": "Permet aux membres du rôle d'envoyer des sondages.", + "MANAGE_MESSAGES": "Permet aux membres du rôle de supprimer les messages qui ne sont pas les leurs.", + "MANAGE_THREADS": "Permet aux membres du rôle de supprimer et d'archiver les fils de discussion et de voir tous les fils privés.", + "MANAGE_CHANNELS": "Permet aux membres du rôle d'éditer/modifier les canaux.", + "MANAGE_ROLES": "Permet aux membres du rôle d'éditer/modifier les rôles.", + "MANAGE_WEBHOOKS": "Permet aux membres du rôle d'éditer/modifier les Webhooks.", + "CREATE_EVENTS": "Permet aux membres du rôle de créer des événements planifiés.", + "MANAGE_EVENTS": "Permet aux membres du rôle d'éditer/supprimer les événements (présents & futurs).", + "CREATE_GUILD_EXPRESSIONS": "Permet de créer des expressions (émojis, autocollants et sons de table de bruitage).", + "MANAGE_GUILD_EXPRESSIONS": "Permet aux membres du rôle d'éditer/modifier les expressions (émojis, autocollants et sons de table de bruitage).", + "MODERATE_MEMBERS": "Permet aux membres du rôle d'exclure temporairement des membres de la guilde (les empêche d'envoyer/réagir aux messages dans les canaux/fils de discussion et de parler dans les canaux vocaux et les scènes).", + "KICK_MEMBERS": "Permet aux membres du rôle d'expulser des membres de la guilde.", + "BAN_MEMBERS": "Permet aux membres du rôle de bannir des membres de la guilde.", + "MOVE_MEMBERS": "Permet aux membres du rôle de déplacer les membres entre les canaux vocaux.", + "MUTE_MEMBERS": "Permet aux membres du rôle de couper le son des autres membres.", + "DEAFEN_MEMBERS": "Permet aux membres du rôle d’empêcher d'autres membres d'écouter dans un canal vocal (assourdir un membre l'empêche automatiquement de parler).", + "CHANGE_NICKNAME": "Permet aux membres du rôle de modifier leur propre surnom.", + "VIEW_CHANNEL": "Permet aux membres du rôle de voir le canal.", + "READ_MESSAGE_HISTORY": "Permet aux membres du rôle de lire l'historique des messages des canaux textuels.", + "ADD_REACTIONS": "Permet aux membres du rôle d'ajouter des réactions aux messages dans les canaux textuels.", + "SEND_MESSAGES": "Permet aux membres du rôle d'envoyer des messages dans les canaux textuels.", + "SEND_MESSAGES_IN_THREADS": "Permet aux membres du rôle d'envoyer des messages dans les fils de discussion.", + "SEND_TTS_MESSAGES": "Permet aux membres du rôle d'envoyer des messages en synthèse vocale (Text-To-Speech: texte-vers-parole) dans les canaux textuels.", + "USE_EXTERNAL_EMOJIS": "Permet aux membres du rôle d'utiliser des émojis externes dans les canaux textuels.", + "USE_EXTERNAL_STICKERS": "Permet aux membres du rôle d'utiliser des autocollants externes dans les canaux textuels.", + "EMBED_LINKS": "Permet aux liens envoyés par les membres du rôle de s'intégrer automatiquement dans les canaux textuels.", + "ATTACH_FILES": "Permet aux membres du rôle de joindre des fichiers dans les canaux textuels.", + "SEND_VOICE_MESSAGES": "Permet aux membres du rôle d'envoyer des messages vocaux dans les canaux textuels.", + "CREATE_INSTANT_INVITE": "Permet aux membres du rôle de créer des invitations pour la guilde.", + "CREATE_PUBLIC_THREADS": "Permet aux membres du rôle de créer des fils de discussion publics.", + "CREATE_PRIVATE_THREADS": "Permet aux membres du rôle de créer des fils de discussion privés.", + "CONNECT": "Permet aux membres du rôle de se connecter à un canal vocal.", + "SPEAK": "Permet aux membres du rôle de parler dans un canal vocal.", + "USE_VAD": "Permet aux membres du rôle de parler dans un canal vocal en parlant simplement (Si désactivé, les membres devront utiliser la touche de Push-To-Talk, presser-pour-parler).", + "STREAM": "Permet aux membres du rôle de lancer une diffusion dans les canaux vocaux.", + "USE_SOUNDBOARD": "Permet aux membres du rôle d'utiliser la table de bruitages dans les canaux vocaux.", + "USE_EXTERNAL_SOUNDS": "Permet aux membres du rôle d'utiliser des tables de bruitages provenant d'autres guildes dans les canaux vocaux.", + "PRIORITY_SPEAKER": "Permet aux membres du rôle d'utiliser la parole prioritaire dans les canaux vocaux.", + "REQUEST_TO_SPEAK": "Permet aux membres du rôle de demander à parler dans le canal de scène.", + "USE_EMBEDDED_ACTIVITIES": "Permet aux membres du rôle d'utiliser les activités intégrées.", + "USE_APPLICATION_COMMANDS": "Permet aux membres du rôle d'utiliser les commandes d'application.", + "USE_EXTERNAL_APPS": "Permet aux applications des membres du rôle d'envoyer des réponses publiques. (Si désactivé, les membres du rôle seront toujours autorisés à utiliser leurs applications mais les réponses seront visible que d'eux-mêmes. Cela s'applique uniquement aux applications qui ne sont pas également installées dans la guilde)." + }, + "readableNames": { + "ADD_REACTIONS": "Ajouter des réactions", + "ADMINISTRATOR": "Administrateur", + "ATTACH_FILES": "Joindre des fichiers", + "BAN_MEMBERS": "Bannir des membres", + "BYPASS_SLOWMODE": "Ignorer le mode lent", + "CHANGE_NICKNAME": "Changer de surnom", + "CONNECT": "Se connecter", + "CREATE_EVENTS": "Créer des événements", + "CREATE_GUILD_EXPRESSIONS": "Créer des expressions", + "CREATE_INSTANT_INVITE": "Créer une invitation", + "CREATE_PRIVATE_THREADS": "Créer des fils de discussion privés", + "CREATE_PUBLIC_THREADS": "Créer des fils de discussion publics", + "DEAFEN_MEMBERS": "Rendre les membres sourds (et muets)", + "EMBED_LINKS": "Intégrer des liens", + "KICK_MEMBERS": "Expulser des membres", + "MANAGE_CHANNELS": "Gérer les canaux", + "MANAGE_EVENTS": "Gérer les événements", + "MANAGE_GUILD": "Gérer la guilde", + "MANAGE_GUILD_EXPRESSIONS": "Gérer les expressions", + "MANAGE_MESSAGES": "Gérer les messages", + "MANAGE_NICKNAMES": "Gérer les surnoms", + "MANAGE_ROLES": "Gérer les rôles", + "MANAGE_THREADS": "Gérer les fils de discussion", + "MANAGE_WEBHOOKS": "Gérer les webhooks", + "MENTION_EVERYONE": "Mention @everyone, @here et tous les rôles", + "MODERATE_MEMBERS": "Exclure temporairement des membres", + "MOVE_MEMBERS": "Déplacer des membres", + "MUTE_MEMBERS": "Rendre les membres muets", + "PIN_MESSAGES": "Épingler des messages", + "PRIORITY_SPEAKER": "Orateur prioritaire", + "READ_MESSAGE_HISTORY": "Lire l'historique des messages", + "REQUEST_TO_SPEAK": "Demander à prendre la parole", + "SEND_MESSAGES": "Envoyer des messages", + "SEND_MESSAGES_IN_THREADS": "Envoyer des messages dans des fils de discussion", + "SEND_POLLS": "Créer des sondages", + "SEND_TTS_MESSAGES": "Envoyer des messages TTS (synthèse vocale)", + "SEND_VOICE_MESSAGES": "Envoyer des messages vocaux", + "SPEAK": "Parler", + "STREAM": "Vidéo", + "USE_APPLICATION_COMMANDS": "Utiliser les commandes d'application", + "USE_EMBEDDED_ACTIVITIES": "Utiliser des activités", + "USE_EXTERNAL_APPS": "Utiliser des applications externes", + "USE_EXTERNAL_EMOJIS": "Utiliser des émojis externes", + "USE_EXTERNAL_SOUNDS": "Utiliser des sons externes", + "USE_EXTERNAL_STICKERS": "Utiliser des autocollants externes", + "USE_SOUNDBOARD": "Utiliser la table de bruitages", + "USE_VAD": "Utiliser la détection d’activité vocale", + "VIEW_AUDIT_LOG": "Afficher le journal d'audit", + "VIEW_CHANNEL": "Voir les canaux", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Consulter les analyses de monétisation des créateurs", + "VIEW_GUILD_INSIGHTS": "Voir les informations sur la guilde" + } + }, + "pinMessage": "Épingler le message", + "profile": { + "bio": "À propos de moi :", + "joined": "Compte créé : $1", + "joinedMember": "A rejoint $1 : $2", + "mut": "Guildes en commun", + "mutFriends": "Amis en commun", + "permInfo": "Permissions", + "userInfo": "Informations sur l’utilisateur" + }, + "profileColor": "Couleur du profil", + "pronouns": "Pronoms :", + "readableName": "Français", + "recentEmoji": "Emojis récents", + "register": { + "DOBError": "Date de naissance : $1", + "agreeTOS": "J'accepte les [Conditions d'Utilisation]($1) :", + "emailError": "E-mail : $1", + "noTOS": "Cette instance n'a pas de conditions d'utilisation, acceptez quand même les conditions d'utilisation :", + "passwordError:": "Mot de passe : $1", + "register": "S’inscrire", + "tos": "Vous devez accepter les conditions d’utilisation", + "usernameError": "Nom d’utilisateur : $1" + }, + "remove": "Supprimer", + "renderJoinAvatars": "Afficher l'avatar des nouveaux membres :", + "reply": "Répondre", + "replyingTo": "En réponse à $1", + "retrying": "Nouvelle tentative...", + "role": { + "color": "Couleur primaire", + "colors": { + "name": "Schéma de couleurs :", + "one": "Monochrome", + "two": "Bicolore", + "three": "Tricolore", + "secondColor": "Couleur secondaire :", + "thirdColor": "Couleur tertiaire :" + }, + "confirmDelete": "Êtes-vous sûr·e de vouloir supprimer $1 ?", + "copyId": "Copier l'ID du rôle", + "delete": "Supprimer le rôle", + "deleting": "Suppression du rôle...", + "displaySettings": "Paramètres d'affichage", + "hoisted": "Affiche les membres du rôle séparément des membres en ligne (ils seront hissés au plus haut rôle duquel ils font partie) :", + "mentionable": "Permettre à n'importe qui de mentionner ce rôle :", + "name": "Nom du rôle :", + "perms": "Permissions", + "remove": "Retirer le rôle", + "roleEmoji": "Emoji de rôle :", + "roleFileIcon": "Icône de rôle :", + "roles": "Rôles" + }, + "search": { + "back": "Retour", + "new": "Nouveau", + "next": "Suivant", + "nofind": "Il semble qu'il n'y ait aucun message correspondant à votre recherche, essayez peut-être d'élargir votre recherche pour trouver ce que vous recherchez", + "old": "Ancien", + "page": "Page $1", + "search": "Rechercher" + }, + "searchGifs": "Chercher sur $1", + "settings": { + "clear": "Retirer", + "img": "Importer une image", + "save": "Enregistrer les modifications", + "unsaved": "Attention, vous avez des modifications non enregistrées", + "updates": { + "serviceWorkerMode": { + "disabled": "Désactivé", + "enabled": "Activé", + "offlineOnly": "Hors ligne uniquement", + "title": "Paramètre Service Worker", + "unregistered": "Désactivé (cache média inclu)" + } + } + }, + "showBlockedMessages": "Vous avez bloqué cet utilisateur, cliquez pour voir $1 {{PLURAL:$1|message bloqué|messages bloqués}}.", + "spoiler": "Spoiler", + "sticker": { + "confirmDel": "Êtes-vous sûr·e de vouloir supprimer cet autocollant?", + "del": "Supprimer l'autocollant", + "desc": "Description", + "errEmjMust": "Vous devez associer un emoji avec votre autocollant", + "errFileMust": "Vous devez inclure une image pour votre autocollant", + "image": "Image :", + "name": "Nom :", + "tags": "Emoji associé :", + "title": "Autocollants", + "upload": "Importer des autocollants" + }, + "submit": "Soumettre", + "switchAccounts": "Changer de compte ⇌", + "todayAt": "Aujourd'hui à $1", + "trace": { + "totalTime": "$2: $1ms", + "traces": "$1 ($2ms) $3" + }, + "typing": "$2 {{PLURAL:$1|est|sont}} en train d'écrire", + "unableToConnect": "Impossible de se connecter au serveur Spacebar. Veuillez essayer de vous déconnecter et de vous reconnecter.", + "unableToPin": "Impossible d'épingler le message", + "unpinMessage": "Désépingler le message", + "updateAv": "Mises à jour disponibles", + "uploadBanner": "Importer une bannière :", + "uploadFilesText": "Importez vos fichiers ici !", + "uploadPfp": "Importer un avatar :", + "useTemplate": "Utiliser $1 comme modèle", + "useTemplateButton": "Utiliser le modèle", + "usedFermi": "envoyé via Fermi", + "user": { + "report": "Signaler l’utilisateur", + "deleted": "Utilisateur supprimé", + "reportApp": "Signaler l'application", + "addRole": "Ajouter des rôles", + "ban": "Bannir un membre", + "block": "Bloquer", + "confirmInstBan": "Êtes-vous sûr·e de vouloir bannir $1 de l’instance ?", + "copyId": "Copier l'ID de l'utilisateur", + "dnd": "Ne pas déranger", + "editNick": "Modifier le surnom", + "editServerProfile": "Modifier le profil de guilde", + "friendReq": "Demande d'ami", + "idle": "Inactif", + "instanceBan": "Banissement d’instance", + "invisible": "Invisible", + "kick": "Expulser un membre", + "message": "Envoyer un MP", + "nick": { + "foe": "Définir le surnom de l'utilisateur", + "friend": "Définir un surnom d'ami", + "stalker": "Définir le surnom du demandeur", + "stalking": "Définir le surnom de l’utilisateur concerné" + }, + "offline": "Hors ligne", + "online": "En ligne", + "remove": "Supprimer l’utilisateur", + "removeRole": "Retirer des rôles", + "sendMessage": "Envoyer un message à @$1", + "timeout": "Exclure temporairement le membre", + "unTimeout": "Révoquer l'exclusion du membre", + "unban": "Lever le bannissement de $1", + "unblock": "Débloquer" + }, + "userping": { + "resolving": "résolution de l'utilisateur", + "unknown": "@utilisateur-inconnu" + }, + "vc": { + "joinForStream": "Rejoignez le canal vocal pour regarder", + "joiningStream": "Connexion à la diffusion en cours…", + "joinstream": "Regarder la diffusion", + "leavestream": "Quitter la diffusion", + "stopstream": "Arrêter la diffusion" + }, + "webauth": { + "addKey": "Ajouter une clé", + "keyname": "Nom de la clé :", + "manage": "Gérer les clés de sécurité" + }, + "webhook": "WEBHOOK", + "webhooks": { + "EnterWebhookName": "Entrez le nom du Webhook", + "areYouSureDelete": "Êtes-vous sûr·e de vouloir supprimer $1 ?", + "avatar": "Avatar", + "base": "Webhooks", + "channel": "Canal de sortie du Webhook :", + "copyURL": "Copier le lien du Webhook", + "createdAt": "Créé le $1", + "createdBy": "Créé par :", + "deleteWebhook": "Supprimer le Webhook", + "name": "Nom du Webhook :", + "newWebHook": "Nouveau Webhook", + "notFound": "L’utilisateur n’est plus dans la guilde", + "sillyDefaults": "Capitaine Crochet\nBillie Crochets\nSainte Ernette", + "token": "Jeton Webhook : `$1`", + "type": "Type : $1", + "type1": "Entrant", + "type2": "Abonné du canal", + "type3": "Application", + "url": "Lien du Webhook : `$1`" + }, + "welcomeMessages": "Bienvenue $1 ! Ravi de vous voir !\nBonjour $1, enchanté de vous rencontrer !\nBienvenue, on espère que vous avez apporté de la pizza $1 !", + "widget": "Widget de la guilde", + "widgetEnabled": "Widget activé", + "yes": "Oui", + "yesterdayAt": "Hier à $1" +} diff --git a/translations/ga.json b/translations/ga.json new file mode 100644 index 00000000..3a165bd8 --- /dev/null +++ b/translations/ga.json @@ -0,0 +1,957 @@ +{ + "@metadata": { + "authors": [ + "Aindriu80" + ] + }, + "2faCode": "Cód 2FA:", + "404": { + "404": "earráid:404 - Leathanach Gan Aimsiú", + "app": "Chuig an aip", + "blog": "Blag Fermi", + "home": "Leathanach baile", + "listtitle": "B’fhéidir gur mian leat dul chuig ceann de na háiteanna seo", + "login": "Logáil Isteach", + "report": "Má fuair tú an leathanach seo laistigh den chliant, déan é a thuairisciú le do thoil:", + "reset": "Leathanach athshocraithe pasfhocail", + "title": "Is cosúil go bhfuil tú caillte", + "whatelse": "Cad eile a cheapann tú ba chóir a tharlú?", + "whereever": "Cibé áit a bhfuil sé seo" + }, + "onboarding": { + "name": "Ionduchtú", + "disable": "Díchumasaigh an clárú", + "addChannel": "Cuir cainéal leis", + "channel": "Cainéal:", + "desc": "Cur síos:", + "deleteChannel": "Scrios an cainéal ón gclárú", + "enable": "Cumasaigh ionduchtú", + "title": "Fáilte go $1!" + }, + "DMs": { + "add": "Cuir duine éigin leis an DM seo", + "close": "Dún DM", + "copyId": "Cóipeáil aitheantas DM", + "markRead": "Marcáil mar léite", + "name": "Teachtaireachtaí Díreacha" + }, + "ok": "Ceart go leor", + "dismiss": "Díbhe", + "UrlGen": "Gineadóir cuireadh bota", + "Voice": { + "userVol": "Toirt úsáideora:", + "status": { + "badWS": "Freagra dona ó WS", + "conectionFailed": "Theip ar an gCeangal", + "done": "Ceangailte", + "left": "Comhrá gutha ar chlé", + "makingOffer": "Tairiscint á déanamh", + "noSDP": "Gan SDP", + "notconnected": "Gan cheangal", + "sendingStreams": "Sruthanna fuaime á seoladh", + "startingRTC": "Ag tosú nasc RTC", + "waitingMainWS": "Ag fanacht leis an bpríomh-WS", + "waitingURL": "Ag fanacht le URL gutha", + "wsAuth": "ag fanacht le húdarú WS", + "wsOpen": "Ag fanacht le hoscailt WS" + } + }, + "accessibility": { + "gifSettings": { + "always": "I gcónaí", + "hover": "Ar an luchóg", + "never": "Choíche" + }, + "name": "Inrochtaineacht", + "playGif": "Seinn GIFanna:", + "playIcon": "Seinn deilbhíní beoite:", + "roleColors": "Díchumasaigh dathanna róil:", + "gradientColors": "Díchumasaigh dathú grádáin:", + "decorations": "Cumasaigh maisiúcháin abhatár:" + }, + "accountNotStart": "Ní féidir an cuntas a thosú", + "add": "Cuir leis", + "applications": { + "delete": "Scrios Feidhmchlár", + "sure": "An bhfuil tú cinnte gur mian leat $1 a scriosadh?" + }, + "badge": { + "active_developer": "Forbróir Gníomhach", + "application_guild_subscription": "tá Préimh aige", + "automod": "úsáideann AutoMod", + "bot_commands": "tacaíonn le horduithe", + "bug_hunter_level_1": "Sealgaire feithidí leibhéal 1", + "bug_hunter_level_2": "Sealgaire feithidí leibhéal 2", + "certified_moderator": "Modhnóir", + "early_supporter": "Tacaíocht luath", + "guild_booster_lvl1": "Guild feabhsaithe", + "guild_booster_lvl2": "Guild feabhsaithe", + "guild_booster_lvl3": "Guild feabhsaithe", + "guild_booster_lvl4": "Guild feabhsaithe", + "guild_booster_lvl5": "Guild feabhsaithe", + "guild_booster_lvl6": "Guild feabhsaithe", + "guild_booster_lvl7": "Guild feabhsaithe", + "guild_booster_lvl8": "Guild feabhsaithe", + "guild_booster_lvl9": "Feabhsaíodh an guild ar feadh tamaill mhaith", + "hypesquad": "Vibesquad [ÁITSEALBHÓIR]", + "hypesquad_house_1": "Dána", + "hypesquad_house_2": "Sármhaitheas", + "hypesquad_house_3": "Cothromaíocht", + "legacy_username": "tá seanainm úsáideora aige", + "partner": "Comhpháirtí Cás", + "premium": "Préimh", + "quest_completed": "rompu críochnaithe", + "staff": "Foireann an cháis", + "verified_developer": "Forbróir Fíoraithe" + }, + "bio": "Beathaisnéis:", + "blankMessage": "Teachtaireacht bhán", + "blog": { + "blog": "Blag", + "blogUpdates": "Nuashonruithe blag:", + "desc": "Léigh nó liostáil leis an mblag le haghaidh nuashonruithe ar Fermi!", + "fermi": "Blag Fermi", + "gotoPost": "Téigh go dtí an post", + "wantUpdates": "Ar mhaith leat nuashonruithe blag seachtainiúla a fháil ar staid Fermi?" + }, + "bot": "BOTA", + "botGuilds": "Tá bota na Guilds i:", + "botSettings": "Socruithe Bota", + "cancel": "Cealaigh", + "channel": { + "SlowmodeCool": "Fuarú mallmhód: $1", + "TimeOutCool": "Am caite go dtí: $1", + "allowIcons": "Ceadaigh deilbhíní saincheaptha cainéil", + "announcement": "Fógraí", + "copyId": "Cóipeáil aitheantas an chainéil", + "copyIdCat": "Cóipeáil aitheantas na catagóire", + "createCatagory": "Cruthaigh catagóir", + "createChannel": "Cruthaigh cainéal", + "creating": "Ag cruthú cainéal", + "delete": "Scrios cainéal", + "deleteCat": "Scrios catagóir", + "icon": "Deilbhín:", + "makeInvite": "Cuir cuireadh", + "markRead": "Marcáil mar léite", + "mute": "Cainéal balbh", + "name": "Cainéal", + "name:": "Ainm an chainéil:", + "nsfw:": "Rabhadh maidir le hábhar NSFW/aibí:", + "permissions": "Ceadanna", + "selectCatName": "Ainm na catagóire", + "selectName": "Ainm an chainéil", + "selectType": "Roghnaigh cineál cainéil", + "settings": "Socruithe", + "settingsFor": "Socruithe le haghaidh $1", + "slowmode": "Mód Mall:", + "text": "Téacs", + "timedOutUntil": "Am caite go dtí: $1", + "topic:": "Topaic:", + "typebox": "Teachtaireacht i $1", + "unmute": "Díbholg an cainéal", + "voice": "Guth", + "deleteThread": "Scrios an snáithe", + "hideThreads": "Folaigh tar éis neamhghníomhaíochta", + "forum": "Fóram" + }, + "threads": { + "leave": "Fág an snáithe", + "join": "Glac páirt sa snáithe", + "name": "Ainm an snáithe:", + "editTags": "Cuir clibeanna in eagar" + }, + "forum": { + "creorsear": "Cruthaigh nó déan cuardach ar phost", + "next": "Ar aghaidh", + "back": "Ar ais", + "newPost": "Déan post nua", + "post": "Post", + "sortOptions": { + "sortby": { + "title": "Sórtáil de réir", + "recent": "Gníomhach le déanaí", + "posted": "Dáta postála" + }, + "sortOrder": { + "title": "Ord sórtála", + "recent": "Le déanaí ar dtús", + "old": "Sean ar dtús" + }, + "tagMatch": { + "title": "Meaitseáil clibeanna", + "some": "Meaitseáil roinnt clibeanna", + "all": "Meaitseáil na clibeanna go léir" + }, + "name": "Roghanna sórtála" + }, + "errors": { + "tagsReq": "Tá clibeanna riachtanach", + "requireText": "Ní féidir an teachtaireacht a fhágáil folamh" + }, + "allTags": "Gach clibeanna", + "settings": { + "editTags": "Cuir clibeanna in eagar:", + "editTag": "Cuir an clib in eagar", + "tagName": "Ainm an chlib:", + "moderated": "Ní cheadaítear ach do mhodhnóirí snáithe clib a chur i bhfeidhm:", + "addTag": "Cuir clib leis", + "delTag": "Scrios an chlib" + } + }, + "channelLink": "# $1", + "clientDesc": "Leagan cliant: $1\n\n[Bí páirteach i ngiúld oifigiúil Fermi]($2/invite/USgYJo?instance=https%3A%2F%2Fspacebar.chat)\n\n[Cabhraigh le Fermi a aistriú](https://translatewiki.net/wiki/Translating:JankClient#sortable:3=desc)\n\n[Cabhraigh le Fermi a chruthú](https://github.com/MathMan05/Fermi)\n\n[Cabhraigh leis an bhfreastalaí a bhfuil Fermi ag brath air a chothabháil](https://github.com/spacebarchat/server)\n\n[Léigh an blag](https://blog.fermi.chat/)\n\nCearta ríofa: $3", + "commands": { + "errorNotValid": "Ní rogha bhailí é $1 le haghaidh $2", + "required": "Is cuid riachtanach den ordú seo é $1" + }, + "confirmGuildLeave": "An bhfuil tú cinnte gur mhaith leat $1 a fhágáil", + "copyLink": "Cóipeáil nasc an teachtaireachta", + "copyRegLink": "Cóipeáil nasc", + "copymessageid": "Cóipeáil aitheantas an teachtaireachta", + "copyrawtext": "Cóipeáil téacs amh", + "createAccount": "Cruthaigh Cuntas", + "delete": "Scrios", + "deleteConfirm": "An bhfuil tú cinnte gur mian leat é seo a scriosadh?", + "devSettings": { + "badUser": "Cumasaigh logáil réada úsáideora lochtacha a sheolann an iomarca faisnéise:", + "cache": "Cumasaigh comhaid léarscáile Taisceála Oibrí Seirbhíse:", + "cacheDesc": "Lódálfaidh comhaid léarscáile fós ar aon nós, cinnteoidh sé seo go mbeidh siad sa taisce nuair a chuirtear nuashonrú nua amach.", + "captureTrace": "Insíonn an socrú seo do Fermi airíonna _trace a ghabháil ón bhfreastalaí, agus má chuireann tú seo ar siúl d’fhéadfadh sé nach n-oibreoidh díchódú forásach JSON (d’fhéadfadh athlódáil a bheith ag teastáil).", + "clearWellKnowns": "Ath-aisghabh taifid .well-known:", + "description": "Tá siad seo do fhorbróirí Spacebar/Fermi, mura bhfuil a fhios agat cad atá á dhéanamh agat, ná cuir isteach ar na socruithe seo le do thoil.", + "gatewayComp": "Díchumasaigh comhbhrú Geata:", + "reportSystem": "Cumasaigh córas tuairiscithe turgnamhach:", + "logGateway": "Logáil imeachtaí geata a fuarthas (eolas ar leibhéal logála):", + "name": "Socruithe Forbróra", + "traces": "Nochtadh rianta:" + }, + "deviceManage": { + "city": "Cathair: $1", + "continent": "Mór-Roinn: $1", + "country": "Tír: $1", + "curSes": "Seo an seisiún reatha, ní mór duit logáil amach tríd an roghchlár eile.", + "estimateWarn": "Rabhadh: Is buille faoi thuairim is fearr amháin atá san fhaisnéis seo go léir, d'fhéadfadh sé a bheith mícheart.", + "ip": "IP deireanach ar a bhfuil aithne air: $1", + "last": "Úsáideadh go deireanach thart ar: $1", + "latitude": "Domhanleithead: $1", + "logout": "Logáil Amach", + "longitude": "Domhanfhad: $1", + "manageDev": "Bainistigh an gléas", + "postal": "Poist: $1", + "region": "Réigiún: $1", + "title": "Bainistigh seisiúin" + }, + "discovery": "Fionnachtain", + "dms": "Teachtaireachtaí díreacha", + "edit": "Cuir in Eagar", + "emoji": { + "confirmDel": "An bhfuil tú cinnte gur mhaith leat an emoji seo a scriosadh?", + "found": { + "not": "Ní ó do cheardchumainn an emoji seo, bí páirteach sa cheardchumann atá á óstáil chun an emoji seo a úsáid", + "other": "Is ó cheann de do gildeanna eile an emoji seo.", + "private": "Ní féidir an guild don emoji seo a aimsiú", + "this": "Is ón gceardchumann seo an emoji seo." + }, + "from": "Is as an emoji seo", + "image:": "Íomhá:", + "join": "Bí páirteach i ngiúld", + "name:": "Ainm:", + "privateGuild": "Tá an guild seo príobháideach", + "title": "Emojis", + "upload": "Uaslódáil emojis" + }, + "emojiSelect": "Emoji:", + "errorReconnect": "Ní féidir ceangal leis an bhfreastalaí, déan iarracht eile i gceann **$1** soicind...", + "favoriteGifs": "GIFanna is fearr leat", + "folder": { + "color": "Dath an fhillteáin:", + "create": "Cruthaigh fillteán nua", + "edit": "Cuir fillteán in eagar", + "name": "Ainm an fhillteáin:" + }, + "form": { + "captcha": "Fan, an duine thú?" + }, + "friends": { + "addfriend": "Cuir cara leis", + "addfriendpromt": "Cuir cairde leis de réir ainm úsáideora:", + "requestsent": "Iarratas seolta!", + "all": "Gach", + "all:": "Gach cara:", + "blocked": "Blocáilte", + "blockedusers": "Úsáideoirí blocáilte:", + "bu": "Úsáideoir blocáilte", + "discnotfound": "Níor aimsíodh idirdhealaitheoir", + "friendlist": "Liosta cairde", + "friends": "Cairde", + "notfound": "Níor aimsíodh an t-úsáideoir", + "online": "Ar líne", + "online:": "Cairde ar líne:", + "pending": "Ar feitheamh", + "pending:": "Iarratais chairde atá ar feitheamh:", + "removeFriend": "Bain cara" + }, + "goThere": "Sea, déanaimis dul", + "goThereTrust": "Sea & muinín a bheith agat as an bhfearann ​​seo as seo amach", + "goingToURL": "Seolfaidh an hipearnasc seo chuig $1 thú. An bhfuil tú cinnte gur mhaith leat dul ann?", + "group": { + "createdm": "Cruthaigh DM!", + "edit": "Cuir comhrá grúpa in eagar", + "select": "Roghnaigh cairde" + }, + "guild": { + "COMMUNITY": "Déan iarratas chun páirt a ghlacadh", + "disableInvites": "Díchumasaigh cuirí:", + "DISCOVERABLE": "Fionnachtain", + "INVITES_DISABLED": "Cuireadh amháin", + "adminMenu": { + "changePerms": "Athraigh ceadanna chun aimsigh", + "finding": "Ag aimsiú riarthóirí", + "ownName": "Úinéir", + "owner": "Aimsigh an t-úinéir", + "permission": "Ceadanna:" + }, + "report": "Tuairiscigh an cumann", + "admins": "Aimsigh riarthóirí", + "all": "Gach", + "banReason": "Cúis toirmisc: $1", + "bannedBy": "Toirmiscthe ag:", + "banner:": "Meirge:", + "bans": "Toirmisc", + "boostMessage?": "Seol teachtaireacht nuair a chuireann duine borradh faoi do ghrúpa!", + "community": "Pobal", + "confirmDelete": "An bhfuil tú cinnte gur mian leat $1 a scriosadh?", + "confirmLeave": "An bhfuil tú cinnte gur mian leat imeacht?", + "copyId": "Cóipeáil aitheantas an cheardchumainn", + "create": "Cruthaigh guild ón tús", + "createFromTemplate": "Cruthaigh guild ó theimpléad", + "createNewTemplate": "Cruthaigh teimpléad nua", + "creating": "Ag cruthú an gilde", + "default": "Réamhshocrú ($1)", + "defaultNoti": "Socraigh socruithe fógra réamhshocraithe do ghild!", + "delete": "Scrios an guild", + "description:": "Cur síos:", + "disoveryTitle": "Fionnachtain na Guild ($1 {{PLURAL:$1|iontráil|two=iontrálacha|few=iontrálacha|many=iontrálacha|other=iontrálacha}})", + "editingTemplate": "Ag eagarthóireacht $1", + "emptytext": "Nach aisteach é, níl aon chainéil ag an ngiúld seo!?", + "emptytitle": "Spota aisteach", + "guilds": "Gildeanna", + "helpTips?": "Seol leideanna cabhracha chuig do gild!", + "howJoin": "Conas is féidir le daoine dul isteach i do gild?", + "icon:": "Deilbhín:", + "invites": "Cuirí", + "joinConfirm": "Bí páirteach i $1?", + "leave": "Fág an gild", + "loadingDiscovery": "Ag lódáil...", + "makeInvite": "Cuir cuireadh", + "markRead": "Marcáil mar léite", + "memberCount": "$1 {{PLURAL:$1|ball|baill}}", + "mute": "Gild balbh", + "name:": "Ainm:", + "nameNoMatch": "Ní hionann na hainmneacha", + "noDelete": "Is cuma liom", + "noLeave": "Is cuma", + "none": "Dada", + "notifications": "Fógraí", + "onlyMentions": "@tráchtanna amháin", + "overview": "Forbhreathnú", + "region:": "Réigiún:", + "roles": "Róil", + "ruleId": "Cainéal rialacha:", + "selectnoti": "Roghnaigh cineál fógraí:", + "sendrandomwelcome?": "Seol teachtaireacht randamach nuair a théann duine isteach sa ghrúpa seo:", + "serverName": "Ainm an gild:", + "settings": "Socruithe", + "settingsFor": "Socruithe le haghaidh $1", + "splash": "Splancscáileán fionnachtana:", + "stickWelcomeReact?": "Spreag baill de do cheardchumann chun freagairt le greamán nuair a théann duine isteach!", + "systemSelect:": "Cainéal teachtaireachtaí córais:", + "tempCreatedBy": "Teimpléad cruthaithe ag:", + "tempUseCount": "Úsáideadh an teimpléad $1 {{PLURAL:$1|uair}}", + "template": "Nasc/cód teimpléid chun an guild a thógáil uaidh:", + "templateDesc": "Cur síos ar an teimpléad:", + "templateName": "Ainm an teimpléid:", + "templateNameShort": "Ní mór ainm an teimpléid a bheith dhá charachtar ar a laghad", + "templateURL": "Nasc teimpléid: $1", + "templates": "Teimpléid", + "templcateMetaDesc": "Ligeann teimpléad do dhaoine eile an gild seo a úsáid mar bhunús dá gildeanna féin: cóipeálfaidh sé bealaí, róil agus socruithe na gildeanna seo ach ní dhéanfaidh sé na teachtaireachtaí ón taobh istigh den cheardchumann, na botanna ná deilbhín na gildeanna a chóipeáil.", + "topic:": "Topaic:", + "unknownRole": "@ról-anaithnid", + "viewTemplate": "Féach ar an teimpléad", + "yesDelete": "Sea, táim cinnte", + "yesLeave": "Sea, táim cinnte" + }, + "hideBlockedMessages": "Tá an t-úsáideoir seo blocáilte agat, cliceáil chun na teachtaireachtaí seo a cheilt.", + "home": { + "icon": "Deilbhín sampla $1", + "uptimeStats": "Am ar Fáil: \nAm ar fad: $1%\nAn tseachtain seo: $2%\nInniu: $3%", + "warnOffiline": "Tá an sampla as líne, ní féidir ceangal" + }, + "htmlPages": { + "addBot": "Cuir leis an gild", + "alreadyHave": "An bhfuil cuntas agat cheana féin?", + "box1Items": "Teachtaireachtaí Díreacha|Tacaíocht imoibrithe|Cuirí|Athrú cuntais|Socruithe úsáideora|Tairseach forbróra|Cuirí bota|Tacaíocht aistriúcháin", + "box1title": "Is cliant atá comhoiriúnach leis an Spacebar é Fermi atá ag iarraidh a bheith chomh maith agus is féidir le go leor gnéithe lena n-áirítear:", + "box3description": "Cuirimid fáilte i gcónaí roimh chabhair, bíodh sé sin i bhfoirm tuairiscí fabht, nó cód, nó fiú trí roinnt clóscríobh a thabhairt ar aird.", + "box3title": "Cuir le Fermi", + "compatableInstances": "Cásanna atá comhoiriúnach leis an spásbharra:", + "createAccount": "Cruthaigh cuntas", + "dobField": "Dáta breithe:", + "emailField": "Ríomhphost:", + "idpermissions": "Cuirfidh sé seo ar chumas an bhota:", + "instanceField": "Mar shampla:", + "loaddesc": "Níor cheart go dtógfadh sé seo i bhfad", + "loadingText": "Tá Fermi ag luchtú", + "loginButton": "Logáil Isteach", + "noAccount": "Nach bhfuil cuntas agat?", + "openClient": "Oscail cliant", + "pw2Field": "Cuir isteach an focal faire arís:", + "pwField": "Pasfhocal:", + "switchaccounts": "Athraigh cuntais", + "trans": "Aistrigh", + "transDesc": "Is féidir leat cabhrú le Fermi a aistriú go do theanga féin!", + "transTitle": "Aistrigh Fermi", + "userField": "Ainm úsáideora:", + "welcomeJank": "Fáilte go Fermi" + }, + "incorrectURLS": "## Is dócha gur sheol an cás seo na naisc mhíchearta.\n### Más tusa úinéir an chás, féach [anseo](https://docs.spacebar.chat/setup/server/) faoi *Ag ceangal ó mheaisíní iargúlta* chun an fhadhb a cheartú.\nAr mhaith leat go ndéanfadh Fermi iarracht an earráid seo a shocrú go huathoibríoch chun ligean duit ceangal leis an gcás?", + "instInfo": "Faisnéis faoi chás", + "instanceInfo": { + "contact": "Riarthóirí sampla ríomhphoist", + "frontPage": "[An chéad leathanach]($1)", + "name": "Eolas faoin sampla", + "publicUrl": "[Nasc poiblí]($1)", + "tosPage": "[Leathanach Téarmaí Seirbhíse]($1)" + }, + "instanceStats": { + "members": "Baill: $1", + "messages": "Teachtaireachtaí: $1", + "name": "Staitisticí sampla: $1", + "servers": "Gildeanna: $1", + "users": "Úsáideoirí cláraithe: $1" + }, + "interactions": { + "ephemeralDismiss": "Ruaig an teachtaireacht", + "failed": "Theip ar an idirghníomhaíocht", + "nonsence": "Sheol an freastalaí comhpháirt de chineál $1 nach bhfuil ann", + "notImpl": "Níl cineál comhpháirte $1 curtha i bhfeidhm ag Fermi go fóill", + "onlyYou": "Ní féidir ach leatsa an teachtaireacht seo a fheiceáil", + "replyline": "Idirghníomhaíocht tosaithe", + "started": "Ag próiseáil idirghníomhaíochta...", + "worked": "D'éirigh leis an idirghníomhaíocht" + }, + "invite": { + "accept": "Glac leis", + "alreadyJoined": "Tá ballraíocht agat cheana féin", + "channel:": "Cainéal:", + "createInvite": "Cruthaigh cuireadh", + "createdAt": "Cruthaithe ag $1", + "expireAfter": "Rachaidh in éag tar éis:", + "expires": "Dáta éaga: $1", + "forChannel": "Don chainéal: $1", + "inviteLinkCode": "Nasc/cód cuireadh", + "inviteMaker": "Déantóir cuireadh", + "invitedBy": "Tá cuireadh tugtha duit ag $1", + "joinUsing": "Bí i do bhall de ghrúpa ag baint úsáide as cuireadh", + "loginOrCreateAccount": "Logáil isteach nó cruthaigh cuntas ⇌", + "longInvitedBy": "Thug $1 cuireadh duit páirt a ghlacadh i $2", + "maxUses": "Uasmhéid úsáide: $1", + "never": "Choíche", + "noAccount": "Cruthaigh cuntas chun glacadh leis an gcuireadh", + "notFound": "Níorbh fhéidir an cuireadh a aimsiú", + "subtext": "go $1 i $2", + "used": "Úsáidte $1{{PLURAL:$1|uair}}:" + }, + "inviteOptions": { + "12h": "12 Uair an Chloig", + "1d": "1 Lá", + "1h": "1 Uair an Chloig", + "30d": "30 Lá", + "30m": "30 Nóiméad", + "6h": "6 Uair an Chloig", + "7d": "7 Lá", + "limit": "$1 uair {{PLURAL:$1|úsáid}}", + "never": "Choíche", + "noLimit": "Gan teorainn", + "title": "Tabhair cuireadh do dhaoine" + }, + "jankInfo": "Faisnéis chliaint", + "leaveGuild": "Fág an gild", + "leaving": "Tá tú ag fágáil an Spásbharra", + "loaded": "Luchtaithe", + "localuser": { + "2faCode:": "Cód:", + "2faCodeGive": "Is é seo do rún TOTP: $1. Ginfidh sé seo comhartha 2FA 6 dhigit le tréimhse éaga 30 soicind.", + "2faDisable": "Díchumasaigh 2FA", + "2faEnable": "Cumasaigh 2FA", + "CheckUpdate": "Seiceáil le haghaidh nuashonruithe", + "PasswordsNoMatch": "Ní hionann na pasfhocail", + "TOSURL": "Nasc na dTéarmaí Seirbhíse:", + "VoiceWarning": "An bhfuil tú cinnte gur mhaith leat é seo a chumasú? Tá sé an-turgnamhach agus is dócha go mbeidh fadhbanna ann. (is do fhorbróirí an ghné seo, ná cumasaigh é mura bhfuil a fhios agat cad atá á dhéanamh agat)", + "accentColor": "Dath béime téama:", + "accountSettings": "Socruithe cuntais", + "addBot": "Cuir bota leis", + "addStatus": "Cuir stádas leis", + "advancedBot": "Socruithe bota ardleibhéil", + "appName": "Ainm an iarratais:", + "areYouSureDelete": "An bhfuil tú cinnte gur mian leat do chuntas a scriosadh? Más ea, cuir isteach an frása $1", + "badCode": "Cód neamhbhailí", + "badPassword": "Pasfhocal mícheart", + "botAvatar": "Abhatár bota:", + "botInviteCreate": "Cruthaitheoir cuireadh bota", + "botUsername": "Ainm úsáideora an bhota:", + "changeDiscriminator": "Athraigh idirdhealú", + "changeEmail": "Athraigh ríomhphost", + "changePassword": "Athraigh an focal faire", + "changeUsername": "Athraigh ainm úsáideora", + "clearCache": "Glan an taisce", + "confirmAddBot": "An bhfuil tú cinnte gur mhaith leat bot a chur leis an aip seo? Níl aon dul siar ann.", + "confirmReset": "An bhfuil tú cinnte gur mian leat an comhartha bota a athshocrú? Stopfaidh do bota ag obair go dtí go ndéanfaidh tú é a nuashonrú.", + "confuseNoBot": "Ar chúis éigin, níl bota ag an aip seo (go fóill).", + "connections": "Naisc", + "createApp": "Cruthaigh feidhmchlár", + "customSound": "Fuaim saincheaptha", + "customStatusWarn": "Ní thacaíonn an spásbharra le taispeáint stádais saincheaptha faoi láthair, mar sin cé go nglacfaidh sé leis an stádas, ní dhéanfaidh sé aon rud leis.", + "deleteAccount": "Scriosadh cuntais", + "deleteAccountButton": "Scrios cuntas", + "description": "Cur síos:", + "devPortal": "Tairseach forbróra", + "disableConnection": "Tá an nasc seo díchumasaithe ar thaobh an fhreastalaí", + "editingBot": "Bota eagarthóireachta $1", + "install": "Suiteáil", + "installDesc": "Trí Fermi a shuiteáil, beidh tú in ann é a oscailt ina fhuinneog féin agus gníomhú mar aip féin! Is féidir leat leanúint ar aghaidh ag úsáid Fermi sa bhrabhsálaí gréasáin mar a bhí tú agus oibreoidh sé mar an gcéanna.", + "installJank": "Suiteáil Fermi", + "language": "Teanga:", + "manageBot": "Bainistigh an bota", + "manageInstance": "Bainistigh an cás", + "mustTypePhrase": "Chun do chuntas a scriosadh ní mór duit an frása a chlóscríobh", + "newDiscriminator": "Idirdhealaitheoir nua:", + "newEmail:": "Ríomhphost nua", + "newPassword:": "Pasfhocal nua:", + "newUsername": "Ainm úsáideora nua:", + "noToken": "Níl a fhios agam an comhartha agus mar sin ní féidir é a shábháil chuig an stóras áitiúil, gabh mo leithscéal.", + "noUpdates": "Níor aimsíodh aon nuashonruithe", + "notisound": "Fuaim fógra:", + "oldPassword:": "Seanfhocal faire:", + "password:": "Pasfhocal", + "privacyPolcyURL": "Nasc polasaí príobháideachais:", + "appID": "ID an iarratais: $1", + "showSecret": "Taispeáin rún an chliaint", + "clientSecret": "Rún cliant: $1", + "secret": "Rúnda", + "publicAvaliable": "An féidir cuireadh poiblí a thabhairt don bhota?", + "refreshPage": "Athnuaigh le cur i bhfeidhm", + "requireCode": "An bhfuil gá le deonú cód chun cuireadh a thabhairt don bota?", + "resetToken": "Athshocraigh an comhartha", + "saveToken": "Sábháil an comhartha chuig an stóras áitiúil", + "setUp2fa": "Socrú 2FA", + "setUp2faInstruction": "Cóipeáil an rún seo isteach i d’aip TOTP (pasfhocal aonuaire bunaithe ar am) chun do chóid logála isteach 2FA a ghiniúint.", + "settings": "Socruithe", + "sillyDeleteConfirmPhrase": "Is grá é Shrek, is saol é Shrek", + "soundTooLarge": "Bhí an fhuaim a rinne tú iarracht a uaslódáil ró-mhór, déan iarracht arís", + "status": "Stádas", + "team:": "Foireann:", + "theme:": "Téama", + "themesAndSounds": "Téamaí & Fuaimeanna", + "tokenDisplay": "Comhartha: $1", + "trace": "Rianta", + "trusted": "Fearainn Iontaofa", + "trustedDesc": "Nuair a chliceálann tú ar naisc a sheolann chuig na fearainn seo thú, ní iarrfar ort cead a thabhairt chun naisc a oscailt cosúil le naisc eile, tabhair cead seo ach do fhearainn a bhfuil muinín agat astu, amhail 'https://fermi.chat'", + "updateSettings": "Nuashonraigh socruithe", + "updatesYay": "Nuashonruithe aimsithe!", + "userSettings": "Do phróifíl phoiblí" + }, + "login": { + "allGood": "Gach rud go maith", + "checking": "Ag seiceáil sampla", + "enterPAgain": "Cuir isteach an focal faire nua arís:", + "invalid": "Cás neamhbhailí, déan iarracht arís", + "login": "Logáil Isteach", + "newPassword": "Pasfhocal nua:", + "pasteInfo": "Greamaigh an nasc téarnaimh anseo:", + "recover": "An ndearna tú dearmad ar do phasfhocal?", + "recovery": "Pasfhocal dearmadta", + "waiting": "Ag fanacht le seiceáil an tsamhail" + }, + "logout": { + "error": { + "cancel": "Cealaigh", + "cont": "Lean ar aghaidh ar aon nós", + "desc": "Ní raibh Fermi in ann tú a logáil amach,\nar mhaith leat an cuntas a bhaint ar aon nós?", + "title": "Tharla earráid agus iarracht á déanamh tú a logáil amach" + }, + "logout": "Logáil Amach" + }, + "manageInstance": { + "AreYouSureStop": "An bhfuil tú cinnte gur mian leat an cás seo a stopadh?", + "TokenFormats": { + "JSON": "JSON formáidithe", + "URLs": "Naisc cuireadh", + "plain": "Simplí" + }, + "clientURL": "Nasc cliant:", + "copy": "Cóipeáil", + "count": "Líon:", + "create": "Cruthaigh", + "createTokens": "Cruthaigh comharthaí clárúcháin", + "format": "Formáid:", + "genericType": "Cineálach", + "length": "Fad:", + "regType": "Cineál nasc comhartha clárúcháin", + "stop": "Stop an sampla" + }, + "media": { + "artist": "Ealaíontóir: $1", + "composer": "Cumadóir: $1", + "download": "Íoslódáil meáin", + "length": "Fad: $1 nóiméad agus $2 soicind", + "loading": "Ag luchtú", + "moreInfo": "Tuilleadh eolais", + "notFound": "Níorbh fhéidir na meáin a aimsiú" + }, + "member": { + "ban": "Toirmeasc ar $1 ó $2", + "kick": "Ciceáil $1 ó $2", + "nick:": "Leasainm:", + "persist": "Scrios úsáideoir amháin:", + "reason:": "Cúis:", + "timeout": "Sos ama $1", + "timeoutTime": "Fad ama scoir:" + }, + "message": { + "messages": "$1 teachtaireacht{{PLURAL:$1|teachtaireachtaí}} >", + "andMore": "$1, agus níos mó!", + "attached": "cuireadh ceangaltán", + "delete": "Scrios an teachtaireacht", + "report": "Tuairiscigh teachtaireacht", + "deleted": "Teachtaireacht scriosta", + "edit": "Cuir teachtaireacht in eagar", + "edited": "(atá curtha in eagar)", + "fullMessage": "Teachtaireacht iomlán:", + "pin": "Tá teachtaireacht greamaithe ag $1", + "thread": "Chruthaigh $1 snáithe: $2", + "reactedWith": "D’fhreagair sé le $1", + "reactionAdd": "Cuir imoibriú leis", + "createThread": "Cruthaigh snáithe", + "threadOptions": "Roghanna snáithe", + "reactions": "Féach ar imoibrithe", + "reactionsTitle": "Imoibrithe", + "retry": "Athsheol teachtaireacht earráideach", + "viewrest": "Féach ar an gcuid eile" + }, + "report": { + "back": "Ar ais", + "next": "Ar aghaidh", + "cancel": "Cealaigh", + "submit": "Cuir isteach", + "blockUser": "Úsáideoir blocáilte", + "timeout": "Ball ama scoir", + "summary": "Achoimre ar an tuarascáil:" + }, + "messageLink": "Teachtaireacht i # $1", + "muteDuration": "Díbholadh i:", + "nevermind": "Is cuma", + "no": "Níl", + "noEmojiFont": "Úsáid emojis córais", + "noMessages": "Níl aon teachtaireachtaí anseo go fóill, bí ar an gcéad duine a déarfaidh rud éigin!", + "noPins": "Níl aon rud anseo duit... go fóill", + "noStickers": "Is trua é sin, níl aon ghreamáin ann le húsáid agat!", + "notiClump": "Fuair ​​tú fógraí $1 ó $2", + "notiVolume": "Toirt fógraí:", + "otherAt": "$1 ag $2", + "permissions": { + "descriptions": { + "ADMINISTRATOR": "Tugann sé seo na ceadanna GO LÉIR thíos do bhaill róil, seachnaíonn sé na ceadanna GO LÉIR a scríobhtar thar ceal. Níor cheart go mbeadh an cead CONTÚIRTEACH seo agat ach TUSA agus baill/robots a bhfuil muinín iomlán agat astu!!", + "MANAGE_GUILD": "Ceadaíonn sé do bhaill róil an guild a bhainistiú agus a chur in eagar.", + "VIEW_AUDIT_LOG": "Ceadaíonn sé seo do bhaill róil an log iniúchta a fheiceáil.", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Ceadaíonn sé seo do bhaill róil léargais ar shíntiús róil a fheiceáil.", + "VIEW_GUILD_INSIGHTS": "Ligeann sé do bhaill róil léargais an cheardchumainn a fheiceáil.", + "BYPASS_SLOWMODE": "Ní bhíonn tionchar ag Modh Mall ar bhaill róil a thuilleadh.", + "MENTION_EVERYONE": "Ceadaíonn sé seo do bhaill róil @gach duine a lua.", + "MANAGE_NICKNAMES": "Ceadaíonn sé seo do bhaill róil leasainmneacha ball eile a athrú.", + "PIN_MESSAGES": "Ceadaíonn sé seo do bhaill róil teachtaireachtaí a phionáil.", + "SEND_POLLS": "Ceadaíonn sé do bhaill róil pobalbhreitheanna a sheoladh.", + "MANAGE_MESSAGES": "Ceadaíonn sé seo do bhaill róil teachtaireachtaí nach leo féin iad a scriosadh.", + "MANAGE_THREADS": "Ceadaíonn sé seo do bhaill róil snáitheanna a scriosadh agus a chartlannú agus na snáitheanna príobháideacha go léir a fheiceáil.", + "MANAGE_CHANNELS": "Ceadaíonn sé seo do bhaill róil cainéil a chur in eagar/a scriosadh.", + "MANAGE_ROLES": "Ceadaíonn sé seo do bhaill róil róil a chur in eagar/a scriosadh.", + "MANAGE_WEBHOOKS": "Ceadaíonn sé seo do bhaill róil crúcaí gréasáin a chur in eagar/a scriosadh.", + "CREATE_EVENTS": "Ceadaíonn sé do bhaill róil imeachtaí sceidealaithe a chruthú.", + "MANAGE_EVENTS": "Ceadaíonn sé seo do bhaill róil imeachtaí (atá ann cheana féin agus imeachtaí amach anseo) a chur in eagar/a scriosadh.", + "CREATE_GUILD_EXPRESSIONS": "Ligeann sé do bhaill róil nathanna a chruthú (emojis, greamáin agus fuaimeanna cláir fuaime).", + "MANAGE_GUILD_EXPRESSIONS": "Ceadaíonn sé seo do bhaill róil nathanna a chur in eagar/a scriosadh (emojis, greamáin agus fuaimeanna cláir fuaime).", + "MODERATE_MEMBERS": "Ceadaíonn sé do bhaill róil sos a chur ar bhaill an cheardchumainn (cosc a chur orthu teachtaireachtaí a sheoladh nó freagairt dóibh i gcomhrá/snáitheanna agus labhairt i gcainéil gutha/ardáin).", + "KICK_MEMBERS": "Ceadaíonn sé do bhaill róil baill a dhíbirt ón gceardchumann.", + "BAN_MEMBERS": "Ceadaíonn sé do bhaill róil baill a thoirmeasc ón gildeanna.", + "MOVE_MEMBERS": "Ceadaíonn sé seo do bhaill róil baill a bhogadh idir cainéil gutha.", + "MUTE_MEMBERS": "Ceadaíonn sé seo do bhaill róil baill eile a mhúchadh i gcainéil gutha.", + "DEAFEN_MEMBERS": "Ceadaíonn sé seo do bhaill róil baill eile a chur ar neamhní i gcainéil gutha (má chuireann tú neamhní ar bhall, cuirfear an fuaim sin ar neamhní go huathoibríoch freisin).", + "CHANGE_NICKNAME": "Ceadaíonn sé seo do bhaill róil a leasainm féin a athrú.", + "VIEW_CHANNEL": "Ceadaíonn sé seo do bhaill róil féachaint ar chainéil téacs.", + "READ_MESSAGE_HISTORY": "Ceadaíonn sé seo do bhaill róil stair teachtaireachtaí na gcainéal téacs a léamh.", + "ADD_REACTIONS": "Ceadaíonn sé seo do bhaill róil imoibrithe a chur le teachtaireachtaí i gcainéil téacs.", + "SEND_MESSAGES": "Ceadaíonn sé seo do bhaill róil teachtaireachtaí a sheoladh i gcainéil téacs.", + "SEND_MESSAGES_IN_THREADS": "Ceadaíonn sé seo do bhaill róil teachtaireachtaí a sheoladh i snáitheanna.", + "SEND_TTS_MESSAGES": "Ceadaíonn sé do bhaill róil teachtaireachtaí téacs go hurlabhra a sheoladh i gcainéil téacs.", + "USE_EXTERNAL_EMOJIS": "Ceadaíonn sé seo do bhaill róil emojis seachtracha a úsáid i gcainéil téacs.", + "USE_EXTERNAL_STICKERS": "Ceadaíonn sé seo do bhaill róil greamáin sheachtracha a úsáid i gcainéil téacs.", + "EMBED_LINKS": "Ceadaigh naisc a sheolann baill róil a leabú go huathoibríoch i gcainéil téacs.", + "ATTACH_FILES": "Ceadaíonn sé seo do bhaill róil comhaid a cheangal i gcainéil téacs.", + "SEND_VOICE_MESSAGES": "Ceadaíonn sé teachtaireachtaí gutha a sheoladh i gcainéil téacs.", + "CREATE_INSTANT_INVITE": "Ceadaíonn sé seo do bhaill róil cuirí a chruthú don ghild.", + "CREATE_PUBLIC_THREADS": "Ceadaíonn sé seo do bhaill róil snáitheanna poiblí a chruthú.", + "CREATE_PRIVATE_THREADS": "Ceadaíonn sé do bhaill róil snáitheanna príobháideacha a chruthú.", + "CONNECT": "Ceadaíonn sé do bhaill róil ceangal le bealaí gutha.", + "SPEAK": "Ceadaíonn sé do bhaill róil labhairt i gcainéil gutha.", + "USE_VAD": "Ceadaíonn sé seo do bhaill róil labhairt i gcainéil gutha trí chaint amháin (Nuair atá sé díchumasaithe, ní féidir leo labhairt ach trí úsáid a bhaint as a n-ionchur eochrach Brúigh-Chun-Labhairt).", + "STREAM": "Ceadaíonn sé do bhaill róil sruthú i gcainéil gutha.", + "USE_SOUNDBOARD": "Ceadaíonn sé do bhaill róil bord fuaime a úsáid i gcainéil gutha.", + "USE_EXTERNAL_SOUNDS": "Ceadaíonn sé do bhaill róil fuaimeanna cláir fuaime ó cheardchumainn eile a úsáid i gcainéil gutha.", + "PRIORITY_SPEAKER": "Ceadaíonn sé seo do bhaill róil a bheith ina gcainteoirí tosaíochta i gcainéil gutha.", + "REQUEST_TO_SPEAK": "Ceadaíonn sé seo do bhaill róil iarratas a dhéanamh chun labhairt i gcainéil stáitse.", + "USE_EMBEDDED_ACTIVITIES": "Ceadaíonn sé do bhaill róil gníomhaíochtaí leabaithe a úsáid.", + "USE_APPLICATION_COMMANDS": "Ceadaíonn sé seo do bhaill róil orduithe feidhmchláir a úsáid.", + "USE_EXTERNAL_APPS": "Ceadaíonn sé seo do bhaill róil freagraí feidhmchlár a thaispeáint go poiblí sa chainéal (Nuair a bheidh sé díchumasaithe, beidh cead ag úsáideoirí a n-aipeanna a úsáid fós ach ní bheidh na freagraí le feiceáil ach acu féin. Ní bhaineann sé seo ach le haipeanna nach bhfuil suiteáilte sa cheardchumann freisin)." + }, + "readableNames": { + "ADD_REACTIONS": "Cuir imoibrithe leis", + "ADMINISTRATOR": "Riarthóir", + "ATTACH_FILES": "Ceangail comhaid", + "BAN_MEMBERS": "Toirmeasc ar bhaill", + "BYPASS_SLOWMODE": "Seachain an modh mall", + "CHANGE_NICKNAME": "Athraigh leasainm", + "CONNECT": "Ceangail", + "CREATE_EVENTS": "Cruthaigh imeachtaí", + "CREATE_GUILD_EXPRESSIONS": "Cruthaigh léirithe", + "CREATE_INSTANT_INVITE": "Cruthaigh cuireadh", + "CREATE_PRIVATE_THREADS": "Cruthaigh snáitheanna príobháideacha", + "CREATE_PUBLIC_THREADS": "Cruthaigh snáitheanna poiblí", + "DEAFEN_MEMBERS": "Baill bodhra", + "EMBED_LINKS": "Naisc a leabú", + "KICK_MEMBERS": "Ciceáil baill", + "MANAGE_CHANNELS": "Bainistigh bealaí", + "MANAGE_EVENTS": "Bainistigh imeachtaí", + "MANAGE_GUILD": "Bainistigh an gild", + "MANAGE_GUILD_EXPRESSIONS": "Bainistigh nathanna", + "MANAGE_MESSAGES": "Bainistigh teachtaireachtaí", + "MANAGE_NICKNAMES": "Bainistigh leasainmneacha", + "MANAGE_ROLES": "Bainistigh róil", + "MANAGE_THREADS": "Bainistigh snáitheanna", + "MANAGE_WEBHOOKS": "Bainistigh crúcaí gréasáin", + "MENTION_EVERYONE": "Luaigh @gach duine, @anseo agus gach ról", + "MODERATE_MEMBERS": "Baill sos ama", + "MOVE_MEMBERS": "Bog baill", + "MUTE_MEMBERS": "Baill a mhúchadh", + "PIN_MESSAGES": "Teachtaireachtaí bioráin", + "PRIORITY_SPEAKER": "Cainteoir tosaíochta", + "READ_MESSAGE_HISTORY": "Léigh stair teachtaireachtaí", + "REQUEST_TO_SPEAK": "Iarratas chun labhairt", + "SEND_MESSAGES": "Seol teachtaireachtaí", + "SEND_MESSAGES_IN_THREADS": "Seol teachtaireachtaí i snáitheanna", + "SEND_POLLS": "Cruthaigh pobalbhreitheanna", + "SEND_TTS_MESSAGES": "Seol teachtaireachtaí Téacs-go-Caint", + "SEND_VOICE_MESSAGES": "Seol teachtaireachtaí gutha", + "SPEAK": "Labhair", + "STREAM": "Físeán", + "USE_APPLICATION_COMMANDS": "Úsáid orduithe feidhmchláir", + "USE_EMBEDDED_ACTIVITIES": "Úsáid gníomhaíochtaí", + "USE_EXTERNAL_APPS": "Úsáid aipeanna seachtracha", + "USE_EXTERNAL_EMOJIS": "Úsáid emojis seachtracha", + "USE_EXTERNAL_SOUNDS": "Úsáid fuaimeanna seachtracha", + "USE_EXTERNAL_STICKERS": "Úsáid greamáin sheachtracha", + "USE_SOUNDBOARD": "Úsáid clár fuaime", + "USE_VAD": "Úsáid braiteadh gníomhaíochta gutha", + "VIEW_AUDIT_LOG": "Féach ar log iniúchta", + "VIEW_CHANNEL": "Féach ar chainéil", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Féach ar anailísíocht monetization cruthaitheoirí", + "VIEW_GUILD_INSIGHTS": "Féach ar léargais na gildeanna" + } + }, + "pinMessage": "Teachtaireacht bioráin", + "profile": { + "bio": "Fúmsa:", + "joined": "Cuntas cruthaithe: $1", + "joinedMember": "Chuaigh isteach $1: $2", + "mut": "Gildeanna frithpháirteacha", + "mutFriends": "Cairde frithpháirteacha", + "permInfo": "Ceadanna", + "userInfo": "Eolas úsáideora" + }, + "profileColor": "Dath próifíle", + "pronouns": "Forainmneacha:", + "readableName": "Gaeilge", + "recentEmoji": "Emojis le déanaí", + "register": { + "DOBError": "Dáta breithe: $1", + "agreeTOS": "Aontaím leis na [Téarmaí Seirbhíse]($1):", + "emailError": "Ríomhphost: $1", + "noTOS": "Níl aon Théarmaí Seirbhíse ag an gcás seo, glac leis na Téarmaí Seirbhíse ar aon nós:", + "passwordError:": "Pasfhocal: $1", + "register": "Cláraigh", + "tos": "Ní mór duit aontú leis na Téarmaí Seirbhíse", + "usernameError": "Ainm úsáideora: $1" + }, + "remove": "Bain", + "renderJoinAvatars": "Taispeáin abhatár do bhaill nua:", + "reply": "Freagra", + "replyingTo": "Ag freagairt do $1", + "retrying": "Ag iarraidh arís...", + "role": { + "color": "An chéad dath", + "colors": { + "name": "Scéim dathanna:", + "one": "Monacrómach", + "two": "Dédhathach", + "three": "Trídhathach", + "secondColor": "Dara dath:", + "thirdColor": "An tríú dath:" + }, + "confirmDelete": "An bhfuil tú cinnte gur mian leat $1 a scriosadh?", + "copyId": "Cóipeáil ID róil", + "delete": "Scrios Ról", + "deleting": "Ag scriosadh ról...", + "displaySettings": "Socruithe taispeána", + "hoisted": "Taispeáin baill róil ar leithligh ó bhaill ar líne (ardófar iad ag baint úsáide as an ngrúpa róil is airde a bhfuil siad mar chuid de):", + "mentionable": "Ceadaigh d'aon duine an ról seo a phingeadh:", + "name": "Ainm an róil:", + "perms": "Ceadanna", + "remove": "Bain an ról", + "roleEmoji": "Emoji róil:", + "roleFileIcon": "Deilbhín róil:", + "roles": "Róil" + }, + "search": { + "back": "Ar ais", + "new": "Nua", + "next": "Ar aghaidh", + "nofind": "Is cosúil nach bhfuil aon teachtaireachtaí ann a mheaitseálann do chuardach, b'fhéidir go bhfuil tú ag iarraidh do chuardach a leathnú chun iarracht a dhéanamh an rud atá uait a aimsiú.", + "old": "Sean", + "page": "Leathanach $1", + "search": "Cuardaigh" + }, + "searchGifs": "Cuardaigh i $1", + "settings": { + "clear": "Glan", + "img": "Uaslódáil íomhá", + "save": "Sábháil athruithe", + "unsaved": "Bí cúramach, tá athruithe neamhshábháilte agat", + "updates": { + "serviceWorkerMode": { + "disabled": "As", + "enabled": "Ar", + "offlineOnly": "As líne amháin", + "title": "Socrú Oibrí Seirbhíse", + "unregistered": "As (lena n-áirítear taisce meán)" + } + } + }, + "showBlockedMessages": "Tá an t-úsáideoir seo blocáilte agat, cliceáil chun an $1 {{PLURAL:$1|teachtaireacht|teachtaireachtaí}} blocáilte a fheiceáil.", + "spoiler": "Milleadh scéil", + "sticker": { + "confirmDel": "An bhfuil tú cinnte gur mian leat an greamán seo a scriosadh?", + "del": "Scrios an greamán", + "desc": "Cur síos", + "errEmjMust": "Ní mór emoji a phéireáil le do ghreamán", + "errFileMust": "Ní mór íomhá a bheith san áireamh le haghaidh do ghreamáin", + "image": "Íomhá:", + "name": "Ainm:", + "tags": "Emoji gaolmhar:", + "title": "Greamáin", + "upload": "Uaslódáil greamáin" + }, + "submit": "Cuir isteach", + "switchAccounts": "Athraigh cuntais ⇌", + "todayAt": "Inniu ag $1", + "trace": { + "totalTime": "$2: $1ms", + "traces": "$1 ($2ms) $3" + }, + "typing": "Tá $2 {{PLURAL:$1|}} ag clóscríobh", + "unableToConnect": "Ní féidir ceangal leis an bhfreastalaí Spásbharra. Déan iarracht logáil amach agus isteach arís.", + "unableToPin": "Ní féidir an teachtaireacht a phionáil", + "unpinMessage": "Díphionáil teachtaireacht", + "updateAv": "Nuashonruithe ar fáil", + "uploadBanner": "Uaslódáil meirge:", + "uploadFilesText": "Uaslódáil do chuid comhad anseo!", + "uploadPfp": "Uaslódáil abhatár:", + "useTemplate": "Úsáid $1 mar theimpléad", + "useTemplateButton": "Úsáid teimpléad", + "usedFermi": "seolta trí Fermi", + "user": { + "report": "Tuairiscigh úsáideoir", + "deleted": "Úsáideoir scriosta", + "reportApp": "Déan gearán faoi fheidhmchlár", + "addRole": "Cuir róil leis", + "ban": "Toirmeasc ar bhall", + "block": "Úsáideoir blocáilte", + "confirmInstBan": "An bhfuil tú cinnte gur mian leat $1 a thoirmeasc ar chás?", + "copyId": "Cóipeáil ID úsáideora", + "dnd": "Ná Cuir Isteach", + "editNick": "Cuir leasainm in eagar", + "editServerProfile": "Cuir próifíl an gild in eagar", + "friendReq": "Iarratas cairdeas", + "idle": "Díomhaoin", + "instanceBan": "Toirmeasc ar chás", + "invisible": "Dofheicthe", + "kick": "Ciceáil ball", + "message": "Teachtaireacht a thabhairt don úsáideoir", + "nick": { + "foe": "Socraigh leasainm namhaid", + "friend": "Socraigh leasainm cara", + "stalker": "Socraigh leasainm an iarratóra", + "stalking": "Socraigh leasainm an iarrtha" + }, + "offline": "As líne", + "online": "Ar Líne", + "remove": "Bain úsáideoir", + "removeRole": "Bain róil", + "sendMessage": "Teachtaireacht @$1", + "timeout": "Ball ama scoir", + "unTimeout": "Bain an t-am scoir ar bhall", + "unban": "Díchosc $1", + "unblock": "Díchoisc úsáideoir" + }, + "userping": { + "resolving": "úsáideoir a réiteach", + "unknown": "@úsáideoir-anaithnid" + }, + "vc": { + "joinForStream": "Bígí páirteach sa chainéal gutha le féachaint", + "joiningStream": "Ag dul isteach sa sruth...", + "joinstream": "Féach ar an sruth", + "leavestream": "Fág an sruth", + "stopstream": "Stop an sruth" + }, + "webauth": { + "addKey": "Cuir eochair leis", + "keyname": "Ainm eochrach:", + "manage": "Bainistigh eochracha slándála" + }, + "webhook": "CRÚCA GRÉASÁIN", + "webhooks": { + "EnterWebhookName": "Cuir isteach ainm an chrúca gréasáin", + "areYouSureDelete": "An bhfuil tú cinnte gur mian leat $1 a scriosadh?", + "avatar": "Abhatár", + "base": "Crúcaí Gréasáin", + "channel": "Aschuir Crúca Gréasáin chuig an gcainéal:", + "copyURL": "Cóipeáil nasc an chrúca gréasáin", + "createdAt": "Cruthaithe ag $1", + "createdBy": "Cruthaithe ag:", + "deleteWebhook": "Scrios an crúca gréasáin", + "name": "Ainm an chrúca gréasáin:", + "newWebHook": "Crúca gréasáin nua", + "notFound": "Níl an t-úsáideoir sa gild a thuilleadh", + "sillyDefaults": "Captaen Crúca\nBillie Crúcaí\nBota Spidey", + "token": "Comhartha crúca gréasáin: `$1`", + "type": "Cineál: $1", + "type1": "Ag teacht isteach", + "type2": "Leantóir cainéil", + "type3": "Feidhmchlár", + "url": "Nasc crúca gréasáin: `$1`" + }, + "welcomeMessages": "Fáilte $1! Is breá liom tú a fheiceáil!\nHaigh, is breá liom bualadh leat $1!\nFáilte, tá súil agam gur thug tú píotsa $1 leat!", + "widget": "Giuirléid an Chumainn", + "widgetEnabled": "Giuirléid cumasaithe", + "yes": "Tá", + "yesterdayAt": "Inné ag $1" +} diff --git a/translations/hi.json b/translations/hi.json new file mode 100644 index 00000000..6e57c443 --- /dev/null +++ b/translations/hi.json @@ -0,0 +1,253 @@ +{ + "@metadata": { + "authors": [ + "Abijeet Patro", + "Saurmandal" + ] + }, + "2faCode": "2FA कोड:", + "404": { + "404": "404 पृष्ठ नहीं मिला", + "app": "ऐप पर", + "blog": "Fermi ब्लॉग", + "home": "मुखपृष्ठ", + "listtitle": "हो सकता है कि आप इनमें से किसी एक जगह पर जाना चाहते हों", + "login": "लॉग-इन करें", + "report": "अगर आपको यह पृष्ठ क्लाइंट पर मिला है तो कृपया इसे रिपोर्ट करें:", + "reset": "पासवर्ड रीसेट पृष्ठ", + "title": "लगता है आप रास्ता भटक गए हैं", + "whatelse": "आपके अनुसार और क्या होना चाहिए?", + "whereever": "यह जहाँ कहीं भी हो" + }, + "DMs": { + "add": "इस DM में किसी को जोड़ें", + "close": "DM बंद करें", + "copyId": "DM ID कॉपी करें", + "markRead": "पठित चिह्नित करें", + "name": "प्रत्यक्ष संदेश" + }, + "UrlGen": "बॉट आमंत्रण जनरेटर", + "Voice": { + "status": { + "badWS": "WS की ओर से खराब उत्तर", + "conectionFailed": "कनेक्शन विफल", + "done": "कनेक्ट हो गया", + "left": "वॉइस चैट छोड़ दिया", + "makingOffer": "प्रस्ताव दिया जा रहा है", + "noSDP": "कोई SDP नहीं", + "notconnected": "कनेक्ट नहीं किए हुए", + "sendingStreams": "ऑडियो स्ट्रीम्स भेजे जा रहे हैं", + "startingRTC": "RTC कनेक्शन शुरू की जा रही है", + "waitingMainWS": "मुख्य WS की प्रतीक्षा में", + "waitingURL": "वॉइस URL की प्रतीक्षा में", + "wsAuth": "WS द्वारा अधिकृत किए जाने की प्रतीक्षा में", + "wsOpen": "WS के खुलने की प्रतीक्षा में" + } + }, + "accessibility": { + "gifSettings": { + "always": "हमेशा", + "hover": "होवर पर", + "never": "कभी नहीं" + }, + "name": "सुलभता", + "playGif": "GIF चित्र चलाएँ:", + "playIcon": "ऐनिमेटेड आईकॉन्स चलाएँ:", + "roleColors": "भूमिकाओं के रंग अक्षम करें" + }, + "accountNotStart": "खाता शुरू नहीं किया जा सका", + "add": "जोड़ें", + "applications": { + "delete": "एप्लिकेशन हटाएँ", + "sure": "क्या आप वाकई में $1 को हटाना चाहते हैं?" + }, + "badge": { + "active_developer": "सक्रिय विकासक", + "application_guild_subscription": "प्रीमियम है", + "automod": "ऑटोमॉड उपयोगकर्ता", + "bot_commands": "कमांड्स का समर्थन", + "bug_hunter_level_1": "बग हंटर लेवल 1", + "bug_hunter_level_2": "बग हंटर लेवल 2", + "certified_moderator": "मॉडरेटर", + "early_supporter": "प्रारंभिक समर्थक", + "guild_booster_lvl1": "गिल्ड को बूस्ट किया है", + "guild_booster_lvl2": "गिल्ड को बूस्ट किया है", + "guild_booster_lvl3": "गिल्ड को बूस्ट किया है", + "guild_booster_lvl4": "गिल्ड को बूस्ट किया है", + "guild_booster_lvl5": "गिल्ड को बूस्ट किया है", + "guild_booster_lvl6": "गिल्ड को बूस्ट किया है", + "guild_booster_lvl7": "गिल्ड को बूस्ट किया है", + "guild_booster_lvl8": "गिल्ड को बूस्ट किया है", + "guild_booster_lvl9": "काफी समय तक गिल्ड को बूस्ट किया है", + "hypesquad": "कॉपीराइट की हुई चीज़", + "hypesquad_house_1": "साहस", + "hypesquad_house_2": "उत्कृष्टता", + "hypesquad_house_3": "साम्यावस्था", + "legacy_username": "पुराना उपयोगकर्ता नाम है", + "partner": "इंस्टैंस सहभागी", + "premium": "प्रीमियम", + "quest_completed": "एक खोज पूरी की", + "staff": "इंस्टैंस स्टाफ़", + "verified_developer": "सत्यापित विकासक" + }, + "bio": "बायो:", + "blankMessage": "खाली संदेश", + "blog": { + "blog": "ब्लॉग", + "blogUpdates": "ब्लॉग अपडेट्स:" + }, + "bot": "बॉट", + "botGuilds": "बॉट जिन गिल्ड्स में मौजूद है:", + "botSettings": "बॉट सेटिंग्स", + "cancel": "रद्द करें", + "channel": { + "SlowmodeCool": "धीमे मोड का कूलडाउन: $1", + "TimeOutCool": "$1 तक टाइम-आउट किए हुए", + "allowIcons": "अनुकूलित चैनल आईकॉन्स की अनुमति दें", + "announcement": "घोषणाएँ", + "copyId": "चैनल ID कॉपी करें", + "copyIdCat": "श्रेणी ID कॉपी करें", + "createCatagory": "श्रेणी बनाएँ", + "createChannel": "चैनल बनाएँ", + "creating": "चैनल बनाया जा रहा है", + "delete": "चैनल हटाएँ", + "deleteCat": "श्रेणी हटाएँ", + "icon": "आईकॉन:", + "makeInvite": "आमंत्रण बनाएँ", + "markRead": "पठित चिह्नित करें", + "mute": "चैनल को म्यूट करें", + "name": "चैनल", + "name:": "नाम:", + "nsfw:": "NSFW (अश्लील):", + "permissions": "अनुमतियाँ", + "selectCatName": "श्रेणी का नाम", + "selectName": "चैनल का नाम", + "selectType": "चैनल का प्रकार चुनें", + "settings": "सेटिंग्स", + "settingsFor": "$1 के लिए सेटिंग्स", + "slowmode": "धीमा मोड:", + "text": "टेक्स्ट", + "timedOutUntil": "$1 तक टाइम-आउट किए हुए", + "topic:": "विषय:", + "typebox": "$1 में संदेश", + "unmute": "चैनल को अनम्यूट करें", + "voice": "वॉइस" + }, + "threads": { + "name": "नाम:" + }, + "clientDesc": "क्लाइंट संस्करण: $1\n\n[आधिकारिक Fermi गिल्ड में शामिल हों]($2/invite/USgYJo?instance=https%3A%2F%2Fspacebar.chat)\n\n[Fermi का अनुवाद करने में सहायता करें](https://translatewiki.net/wiki/Translating:JankClient#sortable:3=desc) \n\n[Fermi के निर्माण में सहायता करें](https://github.com/MathMan05/Fermi)\n\n[Fermi जिस सर्वर पर निर्भर है उसके रखरखाव में सहायता करें](https://github.com/spacebarchat/server)\n\n[ब्लॉग पढ़ें](https://blog.fermi.chat/)\n\nगणना किए गए अधिकार: $3", + "commands": { + "errorNotValid": "$1 $2 के लिए कोई मान्य विकल्प नहीं", + "required": "$1 इस कमांड का एक अनिवार्य हिस्सा है" + }, + "confirmGuildLeave": "क्या आप वाकई में $1 को छोड़ना चाहते हैं?", + "copyLink": "संदेश की कड़ी कॉपी करें", + "copymessageid": "संदेश ID कॉपी करें", + "copyrawtext": "रॉ टेक्स्ट कॉपी करें", + "createAccount": "खाता बनाएँ", + "delete": "हटाएँ", + "deleteConfirm": "क्या आप वाकई में इसे हटाना चाहते हैं?", + "deviceManage": { + "continent": "महाद्वीप: $1", + "country": "देश: $1", + "curSes": "यह वर्तमान सत्र है; आपको दूसरे मेन्यू से लॉग-आउट करने होगा", + "estimateWarn": "चेतावनी: यह सारी जानकारी केवल एक अनुमान है, और यह गलत भी हो सकती है।", + "ip": "अंतिम ज्ञात IP: $1", + "last": "लगभग अंतिम बार उपयोग किया गया: $1", + "latitude": "अक्षांश: $1", + "logout": "लॉग-आउट करें", + "longitude": "रेखांश: $1", + "manageDev": "डिवाइस प्रबंधित करें", + "postal": "डाक कोड: $1", + "region": "क्षेत्र: $1", + "title": "सत्र प्रबंधित करें" + }, + "discovery": "अन्वेषण", + "dms": "प्रत्यक्ष संदेश", + "edit": "सम्पादित करें", + "emoji": { + "confirmDel": "क्या आप सच में इस इमोजी को हटाना चाहते हैं?", + "found": { + "not": "यह इमोजी आपके किसी भी गिल्ड से नहीं है। इस इमोजी का प्रयोग करने के लिए आपको इसमें शामिल होना होगा", + "other": "यह इमोजी आपके एक दूसरे गिल्ड से है।", + "private": "इस इमोजी का गिल्ड नहीं मिला", + "this": "यह इमोजी इस गिल्ड से है।" + }, + "from": "यह इमोजी यहाँ से है:", + "image:": "चित्र:", + "join": "गिल्ड में शामिल हों", + "name:": "नाम:", + "privateGuild": "यह गिल्ड गोपनीय है", + "title": "इमोजी", + "upload": "इमोजी अपलोड करें" + }, + "emojiSelect": "इमोजी:", + "favoriteGifs": "पसंदीदा GIF", + "folder": { + "color": "फ़ोल्डर का रंग:", + "create": "नया फ़ोल्डर बनाएँ", + "edit": "फ़ोल्डर सम्पादित करें", + "name": "फ़ोल्डर का नाम:" + }, + "form": { + "captcha": "ठहरिए, क्या आप एक इंसान हैं?" + }, + "friends": { + "addfriend": "दोस्त जोड़ें", + "addfriendpromt": "उपयोगकर्ता नाम से दोस्त जोड़ें:", + "all": "सभी", + "all:": "सभी दोस्त:", + "blocked": "अवरोधित", + "blockedusers": "अवरोधित उपयोगकर्ताएँ:", + "bu": "अवरोधित उपयोगकर्ता", + "discnotfound": "भेदकर्ता नहीं मिला", + "friendlist": "दोस्तों की सूची", + "friends": "दोस्त", + "notfound": "उपयोगकर्ता नहीं मिला", + "online": "ऑनलाइन", + "online:": "ऑनलाइन दोस्त:", + "pending": "लंबित", + "pending:": "लंबित दोस्ती के अनुरोध:", + "removeFriend": "दोस्त हटाएँ" + }, + "goThere": "हाँ, चलो चलें", + "guild": { + "all": "सभी", + "name:": "नाम:", + "topic:": "विषय:" + }, + "htmlPages": { + "dobField": "जन्म तिथि:", + "emailField": "ईमेल:", + "loginButton": "लॉग-इन करें", + "pwField": "पासवर्ड:", + "userField": "उपयोगकर्ता नाम:" + }, + "invite": { + "never": "कभी नहीं" + }, + "no": "नहीं", + "permissions": { + "readableNames": { + "SEND_MESSAGES": "संदेश भेजें", + "STREAM": "वीडियो" + } + }, + "readableName": "हिन्दी", + "register": { + "emailError": "ईमेल: $1", + "passwordError:": "पासवर्ड: $1", + "usernameError": "उपयोगकर्ता नाम: $1" + }, + "reply": "उत्तर दें", + "settings": { + "clear": "खाली करें" + }, + "todayAt": "आज $1 पर", + "webhooks": { + "name": "नाम:" + }, + "yes": "हाँ", + "yesterdayAt": "कल $1 पर" +} diff --git a/translations/it.json b/translations/it.json new file mode 100644 index 00000000..7f03c35b --- /dev/null +++ b/translations/it.json @@ -0,0 +1,680 @@ +{ + "@metadata": { + "authors": [ + "Amedeo463", + "Asteralee", + "Beta16", + "Dream Indigo", + "FL2226", + "Nonloso10", + "Slow", + "Wheelygay" + ] + }, + "2faCode": "Codice 2FA:", + "404": { + "404": "Errore: 404 - Pagina non trovata", + "app": "Vai all'app", + "blog": "Il blog Fermi", + "home": "Pagina principale", + "listtitle": "Forse volevi andare in uno di questi", + "login": "Accedi", + "report": "Se hai trovato questa pagina all'interno del client, per favore segnalacelo:", + "reset": "Pagina di reset della password", + "title": "Sembra che tu ti sia perso", + "whatelse": "Cos'altro pensi che dovrebbe succedere?", + "whereever": "Ovunque questo sia" + }, + "onboarding": { + "name": "Inserimento", + "addChannel": "Aggiungi canale", + "channel": "Canale:", + "desc": "Descrizione:", + "title": "Benvenuto a $1!" + }, + "DMs": { + "add": "Aggiungi qualcuno a questo DM", + "close": "Chiudi DM", + "copyId": "Copia l'ID del DM", + "markRead": "Segna come già letto", + "name": "Messaggi diretti" + }, + "ok": "Ok", + "dismiss": "Nascondi", + "UrlGen": "Generatore di inviti per bot", + "Voice": { + "status": { + "badWS": "Risposta sbagliata dal WS", + "conectionFailed": "Connessione fallita", + "done": "Connesso", + "left": "Ha lasciato la chat vocale", + "makingOffer": "Sta facendo un'offerta", + "noSDP": "Nessun SDP", + "notconnected": "Non connesso", + "sendingStreams": "Invio di flussi audio", + "startingRTC": "Avvio della connessione RTC", + "waitingMainWS": "In attesa del WS principale", + "waitingURL": "In attesa dell'URL vocale", + "wsAuth": "in attesa dell'autorizzazione da parte di WS", + "wsOpen": "In attesa dell'apertura di WS" + } + }, + "accessibility": { + "gifSettings": { + "always": "Sempre", + "hover": "Al passaggio del mouse", + "never": "Mai" + }, + "name": "Accessibilità", + "playGif": "Riproduci GIF:", + "playIcon": "Riproduci icone animate:", + "roleColors": "Disabilita i colori dei ruoli:", + "gradientColors": "Disattiva i colori sfumati:", + "decorations": "Abilita le decorazioni dell'avatar:" + }, + "accountNotStart": "Impossibile avviare l'account", + "add": "Aggiungi", + "applications": { + "delete": "Cancella applicazione", + "sure": "Sei sicuro di voler eliminare $1?" + }, + "badge": { + "active_developer": "Sviluppatore attivo", + "application_guild_subscription": "è abbonato a Premium", + "automod": "utilizza l'AutoMod", + "bot_commands": "supporta i comandi", + "bug_hunter_level_1": "Cacciatore di insetti livello 1", + "bug_hunter_level_2": "Cacciatore di insetti livello 2", + "certified_moderator": "Moderatore", + "early_supporter": "Veterano", + "guild_booster_lvl1": "Guild rafforzata", + "guild_booster_lvl2": "Gilda potenziata", + "guild_booster_lvl3": "Guild rafforzata", + "guild_booster_lvl4": "Gilda potenziata", + "guild_booster_lvl5": "Guild rafforzata", + "guild_booster_lvl6": "Gilda potenziata", + "guild_booster_lvl7": "Gilda potenziata", + "guild_booster_lvl8": "Gilda potenziata", + "guild_booster_lvl9": "Gilda potenziata per un bel po'", + "hypesquad_house_1": "Audacia", + "hypesquad_house_2": "Eccellenza", + "hypesquad_house_3": "Equilibrio", + "legacy_username": "ha un nome utente legacy", + "partner": "Partner d'istanza", + "premium": "Premium", + "quest_completed": "ha completato una missione", + "staff": "Staff dell'istanza", + "verified_developer": "Sviluppatore verificato" + }, + "bio": "Biografia:", + "blankMessage": "Messaggio vuoto", + "blog": { + "blog": "Blog", + "blogUpdates": "Aggiornamenti del blog:", + "desc": "Leggi o iscriviti al blog per ricevere aggiornamenti su Fermi!", + "fermi": "Blog di Fermi", + "gotoPost": "Vai a post", + "wantUpdates": "Vorresti ricevere aggiornamenti settimanali del blog riguardo lo stato di Fermi?" + }, + "bot": "BOT", + "botGuilds": "I guild a cui il bot partecipa:", + "botSettings": "Impostazioni del bot", + "cancel": "Annulla", + "channel": { + "SlowmodeCool": "Tempo di attesa slowmode: $1", + "TimeOutCool": "In time-out fino: $1", + "allowIcons": "Consenti di mettere icone del canale personalizzati", + "announcement": "Annunci", + "copyId": "Copia l'ID del canale", + "copyIdCat": "Copia l'ID della categoria", + "createCatagory": "Crea categoria", + "createChannel": "Crea canale", + "creating": "Creazione del canale", + "delete": "Elimina canale", + "deleteCat": "Elimina categoria", + "icon": "Icona:", + "makeInvite": "Crea invito", + "markRead": "Segna come già letto", + "mute": "Disattiva canale", + "name": "Canale", + "name:": "Nome del canale:", + "nsfw:": "Avviso sui contenuti NSFW/per adulti:", + "permissions": "Permessi", + "selectCatName": "Nome della categoria", + "selectName": "Nome del canale", + "selectType": "Seleziona il tipo di canale", + "settings": "Impostazioni", + "settingsFor": "Impostazioni per $1", + "slowmode": "Modalità lenta:", + "text": "Testo", + "timedOutUntil": "In time-out fino: $1", + "topic:": "Argomento:", + "typebox": "Messaggio in $1", + "unmute": "Riattiva canale", + "voice": "Voce", + "deleteThread": "Elimina thread", + "hideThreads": "Nascondi dopo inattività", + "forum": "Forum" + }, + "threads": { + "leave": "Esci dal thread", + "join": "Unisciti alla discussione", + "name": "Nome del thread:", + "editTags": "Modifica etichette" + }, + "forum": { + "creorsear": "Crea o cerca un post", + "next": "Successivo", + "back": "Indietro", + "newPost": "Crea un nuovo post", + "post": "Messaggio", + "sortOptions": { + "sortby": { + "title": "Ordina per", + "recent": "Attivo di recente", + "posted": "Data di pubblicazione" + }, + "sortOrder": { + "recent": "Più recenti", + "old": "Meno recenti" + } + }, + "errors": { + "requireText": "Il messaggio non può essere vuoto" + }, + "settings": { + "editTag": "Modifica etichetta", + "tagName": "Nome etichetta:", + "addTag": "Aggiungi un'etichetta", + "delTag": "Cancella etichetta" + } + }, + "channelLink": "# $1", + "clientDesc": "Versione client: $1 \n\n[Unisciti alla gilda ufficiale di Fermi]($2/invite/USgYJo?instance=https%3A% 2F%2Fspacebar.chat)\n\n[Aiutaci a tradurre Fermi](https://translatewiki.net/wiki/Translating:JankClient#sortable:3=desc) \n\n[Aiuta a creare Fermi](https://github.com/MathMan05/Fermi)\n\n[Aiuta a gestire il server su cui si basa Fermi](https://github.com/spacebarchat/server)\n\n[Leggi il blog](https://blog.fermi.chat/)\n\nDiritti calcolati: $3", + "commands": { + "errorNotValid": "$1 non è una scelta valida per $2", + "required": "$1 è una parte obbligatoria di questo comando" + }, + "confirmGuildLeave": "Sei sicuro di voler uscire da $1?", + "copyLink": "Copia il collegamento al messaggio", + "copyRegLink": "Copia link", + "copymessageid": "Copia l'ID del messaggio", + "copyrawtext": "Copia testo grezzo", + "createAccount": "Registrati", + "delete": "Cancella", + "deleteConfirm": "Sei sicuro di voler cancellare questo?", + "devSettings": { + "description": "Queste impostazioni sono riservate agli sviluppatori di Spacebar/Fermi, se non sapete cosa state facendo, vi preghiamo di non modificarle.", + "reportSystem": "Attiva sistema di segnalazione sperimentale:", + "name": "Impostazioni sviluppatore" + }, + "deviceManage": { + "city": "Città: $1", + "continent": "Continente: $1", + "country": "Paese: $1", + "curSes": "Questa è la sessione corrente, devi disconnetterti tramite l'altro menu", + "estimateWarn": "Attenzione: tutte queste informazioni sono solo una supposizione e potrebbero essere errate.", + "ip": "Ultimo IP conosciuto: $1", + "last": "Approssimativamente usato per l'ultima volta: $1", + "latitude": "Latitudine: $1", + "logout": "Esci", + "longitude": "Longitudine: $1", + "manageDev": "Gestisci dispositivo", + "region": "Regione: $1", + "title": "Gestisci sessioni" + }, + "discovery": "Scopri", + "dms": "Messaggi diretti", + "edit": "Modifica", + "emoji": { + "confirmDel": "Sei sicuro di voler cancellare questo emoji?", + "found": { + "not": "Questa emoji non appartiene alla tua gilda, unisciti alla gilda che la ospita per utilizzare questa emoji", + "other": "Questa emoji appartiene a una delle tue altre gilde.", + "private": "La gilda per questa emoji non può essere trovata", + "this": "Questa emoji appartiene a questa gilda." + }, + "from": "Questa emoji è da", + "image:": "Immagine:", + "join": "Unisciti alla gilda", + "name:": "Nome:", + "privateGuild": "Questa gilda è privata", + "title": "Emoji", + "upload": "Carica emoji" + }, + "emojiSelect": "Emoji:", + "errorReconnect": "Impossibile connettersi al server, nuovo tentativo tra **$1** secondi...", + "favoriteGifs": "GIF preferite", + "folder": { + "color": "Colore cartella:", + "create": "Crea una nuova cartella", + "edit": "Modifica cartella", + "name": "Nome cartella:" + }, + "form": { + "captcha": "Aspetta, sei un essere umano?" + }, + "friends": { + "addfriend": "Aggiungi amico", + "addfriendpromt": "Aggiungi amici tramite nome utente:", + "requestsent": "Richiesta inviata!", + "all": "Tutto", + "all:": "Tutti gli amici:", + "blocked": "Bloccato", + "blockedusers": "Utenti bloccati:", + "bu": "Utenza bloccata", + "friendlist": "Lista degli amici", + "friends": "Amici", + "notfound": "Utente non trovato", + "online": "In linea", + "online:": "Amici online:", + "pending": "In attesa", + "pending:": "Richieste di amicizia in sospeso:", + "removeFriend": "Rimuovi amico" + }, + "goThere": "Sì, andiamo", + "goThereTrust": "Sì e fidati di questo dominio da ora in poi", + "goingToURL": "Questo collegamento ipertestuale ti indirizzerà a $1. Sei sicuro di voler continuare?", + "group": { + "createdm": "Crea DM!", + "edit": "Modifica chat di gruppo", + "select": "Seleziona amici" + }, + "guild": { + "COMMUNITY": "Fai domanda per unirti", + "disableInvites": "Disattiva gli inviti:", + "DISCOVERABLE": "Scopri", + "INVITES_DISABLED": "Solo su invito", + "adminMenu": { + "changePerms": "Cambia i permessi per trovare", + "ownName": "Proprietario", + "permission": "Permessi:" + }, + "report": "Segnala gilda", + "all": "Tutto", + "banReason": "Motivo del ban: $1", + "bannedBy": "Bannato da:", + "banner:": "Banner:", + "bans": "Ban", + "boostMessage?": "Invia un messaggio quando qualcuno potenzia la tua gilda!", + "community": "Comunità", + "confirmDelete": "Sei sicuro di voler cancellare $1?", + "confirmLeave": "Sei sicuro di voler abbandonare?", + "copyId": "Copia l'ID della gilda", + "create": "Crea una gilda da zero", + "createFromTemplate": "Crea gilda da un modello", + "createNewTemplate": "Crea un nuovo template", + "creating": "Creazione della gilda", + "default": "Predefinito ($1)", + "defaultNoti": "Imposta le impostazioni di notifica predefinite della tua gilda!", + "delete": "Cancella gilda", + "description:": "Descrizione:", + "editingTemplate": "Modificando $1", + "emptytext": "E' strano, questo guild non ha canali?!", + "guilds": "Gilda", + "helpTips?": "Invia suggerimenti utili per la tua gilda!", + "howJoin": "Come possono unirsi alla tua gilda?", + "icon:": "Icona:", + "invites": "Inviti", + "joinConfirm": "Unirsi a $1?", + "leave": "Lascia la gilda", + "loadingDiscovery": "Caricamento...", + "makeInvite": "Crea invito", + "markRead": "Segna come letto", + "mute": "Disattiva guild", + "name:": "Nome:", + "nameNoMatch": "I nomi non corrispondono", + "noDelete": "Non importa", + "noLeave": "Non importa", + "none": "Nessuno", + "notifications": "Notifiche", + "onlyMentions": "Solo @menzioni", + "overview": "Panoramica", + "region:": "Regione:", + "roles": "Ruoli", + "ruleId": "Canale delle regole:", + "selectnoti": "Seleziona il tipo di notifica:", + "sendrandomwelcome?": "Invia un messaggio casuale quando qualcuno si unisce a questa gilda:", + "serverName": "Nome della gilda:", + "settings": "Impostazioni", + "settingsFor": "Impostazioni per $1", + "stickWelcomeReact?": "Chiedi ai membri della tua gilda di reagire con un adesivo quando qualcuno si unisce!", + "systemSelect:": "Canale dei messaggi di sistema:", + "tempCreatedBy": "Template creato da:", + "templateDesc": "Descrizione template:", + "templateName": "Nome template:", + "templateNameShort": "Il nome del template deve essere lungo almeno 2 caratteri", + "templateURL": "Collegamento al template: $1", + "templates": "Template", + "templcateMetaDesc": "Un template consente ad altri di utilizzare questa guild come base per le proprie guild: copierà i canali, i ruoli e le impostazioni di questa guild, ma non i messaggi al suo interno, i bot o l'icona.", + "topic:": "Argomento:", + "unknownRole": "@ruolo-sconosciuto", + "viewTemplate": "Vedi template", + "yesDelete": "Si, ne sono sicuro", + "yesLeave": "Si, ne sono sicuro" + }, + "hideBlockedMessages": "Hai bloccato quest'utente, clicca per nascondere questi messaggi.", + "home": { + "icon": "Icona dell'istanza $1", + "uptimeStats": "Tempo di attività: \nSempre: $1 %\nQuesta settimana: $2 %\nOggi: $3 %", + "warnOffiline": "L'istanza è offline, impossibile connettersi" + }, + "htmlPages": { + "addBot": "Aggiungi alla gilda", + "alreadyHave": "Hai già un'utenza?", + "box1title": "Fermi è un client compatibile con Spacebar che punta a essere il migliore possibile, con numerose funzionalità, tra cui:", + "box3description": "Apprezziamo sempre un po' di aiuto, che si tratti di segnalazioni di bug, di codice o anche solo di segnalazioni di errori di battitura.", + "box3title": "Contribuisci a Fermi", + "compatableInstances": "Istanze compatibili con Spacebar:", + "createAccount": "Registrati", + "dobField": "Data di nascita:", + "emailField": "Email:", + "idpermissions": "Questo consentirà al bot di:", + "instanceField": "Instanza:", + "loaddesc": "Non dovrebbe volerci molto tempo", + "loadingText": "Fermi sta caricando", + "loginButton": "Accedi", + "noAccount": "Non hai un'utenza?", + "openClient": "Apri client", + "pw2Field": "Inserisci nuovamente la password:", + "pwField": "Password:", + "switchaccounts": "Cambia utenza", + "trans": "Traduci", + "transDesc": "Puoi aiutarci a tradurre Fermi nella tua lingua!", + "transTitle": "Traduci Fermi", + "userField": "Nome utente:", + "welcomeJank": "Benvenuto a Fermi" + }, + "instInfo": "Informazioni sull'istanza", + "instanceInfo": { + "frontPage": "[Prima pagina]($1)", + "name": "Informazioni sull'istanza", + "publicUrl": "[Link pubblico]($1)" + }, + "instanceStats": { + "members": "Membri: $1", + "messages": "Messaggi: $1", + "servers": "Gilda: $1", + "users": "Utenti registrati: $1" + }, + "interactions": { + "ephemeralDismiss": "Ignora messaggio", + "failed": "Interazione fallita", + "notImpl": "Fermi non ha ancora implementato il tipo di componente $1", + "onlyYou": "Solo tu puoi vedere questo messaggio" + }, + "invite": { + "accept": "Accetta", + "alreadyJoined": "Già iscritto", + "channel:": "Canale:", + "createInvite": "Crea invito", + "createdAt": "Creato il $1", + "expireAfter": "Scadenza dopo:", + "expires": "Scade: $1", + "forChannel": "Per il canale: $1", + "inviteLinkCode": "Link/codice di invito", + "inviteMaker": "Creatore di inviti", + "invitedBy": "Sei stato invitato da $1", + "joinUsing": "Unisciti a un guild tramite invito", + "loginOrCreateAccount": "Accedi o registrati ⇌", + "longInvitedBy": "$1 ti ha invitato a unirti a $2", + "maxUses": "Utilizzi massimi: $1", + "never": "Mai", + "noAccount": "Crea un'utenza per accettare l'invito", + "notFound": "Impossibile trovare l'invito", + "used": "Usato $1 {{PLURAL:$1|volta|volte}}:" + }, + "inviteOptions": { + "12h": "12 ore", + "1d": "1 giorno", + "1h": "1 ora", + "30d": "30 giorni", + "30m": "30 minuti", + "6h": "6 ore", + "7d": "7 giorni", + "never": "Mai", + "noLimit": "Nessun limite", + "title": "Invita persone" + }, + "jankInfo": "Informazioni sul client", + "leaveGuild": "Lascia il guild", + "leaving": "Stai lasciando Spacebar", + "loaded": "Caricato", + "localuser": { + "2faCode:": "Codice:", + "2faDisable": "Disattiva 2FA", + "2faEnable": "Attiva 2FA", + "CheckUpdate": "Controlla aggiornamenti", + "PasswordsNoMatch": "Le password non corrispondono", + "TOSURL": "Link ai Termini di servizio:", + "accountSettings": "Impostazioni utenza", + "addBot": "Aggiungi bot", + "appName": "Nome applicazione:", + "areYouSureDelete": "Sei sicuro di voler cancellare la tua utenza? Se sì, inserisci la frase $1", + "badCode": "Codice non valido", + "badPassword": "Password incorretta", + "botAvatar": "Avatar bot:", + "botUsername": "Nome utente del bot:", + "changeEmail": "Cambia email", + "changePassword": "Cambia password", + "changeUsername": "Cambia nome utente", + "clearCache": "Cancella cache", + "connections": "Connessioni", + "customSound": "Suono personalizzato", + "deleteAccount": "Cancellazione utenza", + "deleteAccountButton": "Cancella utenza", + "install": "Installa", + "installJank": "Scarica Fermi", + "language": "Lingua:", + "manageBot": "Gestisci bot", + "manageInstance": "Gestisci istanza", + "mustTypePhrase": "Per cancellare la tua utenza devi digitare la frase", + "newEmail:": "Nuova email", + "newPassword:": "Nuova password:", + "newUsername": "Nuovo username:", + "noUpdates": "Nessun aggiornamento trovato", + "oldPassword:": "Vecchia password:", + "password:": "Password", + "privacyPolcyURL": "Link all'informativa sulla privacy:", + "appID": "ID applicazione: $1", + "showSecret": "Mostra segreto client", + "clientSecret": "Segreto del client: $1", + "secret": "Segreto", + "publicAvaliable": "Vuoi rendere il bot pubblicamente invitabile?", + "refreshPage": "Aggiorna per applicare", + "resetToken": "Reimposta token", + "saveToken": "Salva il token nella memoria locale", + "setUp2fa": "Configurazione 2FA", + "setUp2faInstruction": "Copia questo segreto nella tua app TOTP (password monouso a tempo) per generare i tuoi codici di accesso 2FA", + "settings": "Impostazioni", + "sillyDeleteConfirmPhrase": "Shrek è amore, Shrek è vita", + "soundTooLarge": "Il suono che hai provato a caricare era troppo grande, riprova", + "status": "Stato", + "team:": "Squadra:", + "theme:": "Tema", + "themesAndSounds": "Temi e suoni", + "tokenDisplay": "Token: $1", + "trace": "Tracciati", + "trusted": "Domini attendibili", + "trustedDesc": "Quando clicchi sui link che ti indirizzano a questi domini, ***NON*** ti verrà chiesto il permesso di aprire come per altri link, concedilo solo ai domini di cui ti fidi, come 'https://fermi.chat'", + "updateSettings": "Aggiorna impostazioni", + "updatesYay": "Aggiornamenti trovati!", + "userSettings": "Il tuo profilo pubblico" + }, + "login": { + "allGood": "Tutto bene", + "checking": "Controllando l'istanza", + "enterPAgain": "Inserisci nuovamente la nuova password:", + "invalid": "Istanza non valida, riprova", + "login": "Accedi", + "newPassword": "Nuova password:", + "pasteInfo": "Incolla qui il link di recupero:", + "recover": "Hai dimenticato la password?", + "recovery": "Password dimenticata", + "waiting": "In attesa di controllare l'istanza" + }, + "logout": { + "error": { + "cancel": "Annulla", + "cont": "Continua comunque", + "desc": "Fermi non è riuscito a disconnetterti, vuoi comunque rimuovere l'utenza?", + "title": "Si è verificato un errore durante il tentativo di disconnetterti" + }, + "logout": "Esci" + }, + "manageInstance": { + "AreYouSureStop": "Sei sicuro di voler interrompere questa istanza?", + "TokenFormats": { + "JSON": "JSON formattato", + "URLs": "Link di invito", + "plain": "Normale" + }, + "clientURL": "Link del client:", + "copy": "Copia", + "count": "Conteggio:", + "create": "Crea", + "createTokens": "Crea token di registrazione", + "format": "Formato:", + "genericType": "Generico", + "length": "Lunghezza:", + "regType": "Registra il tipo di link del token", + "stop": "Interrompi istanza" + }, + "media": { + "artist": "Artista: $1", + "composer": "Compositore: $1", + "download": "Scarica media", + "length": "Durata: $1 minuti e $2 secondi", + "loading": "Caricamento", + "moreInfo": "Ulteriori informazioni", + "notFound": "Non sono trovati i media" + }, + "member": { + "ban": "Banna $1 da $2", + "kick": "Espelli $1 da $2", + "nick:": "Soprannome:", + "persist": "Cancella solo l'utente:", + "reason:": "Motivo:", + "timeout": "Metti $1 in time-out", + "timeoutTime": "Durata del time-out:" + }, + "message": { + "messages": "$1 {{PLURAL:$1|messaggio|messaggi}} >", + "andMore": "$1 e altro!", + "attached": "ha inviato un allegato", + "delete": "Cancella messaggio", + "report": "Segnala messaggio", + "deleted": "Messaggio cancellato", + "edit": "Modifica messaggio", + "edited": "(modificato)", + "fullMessage": "Messaggio completo:", + "pin": "$1 ha fissato un messaggio", + "thread": "$1 ha creato un thread: $2", + "reactedWith": "Ha reagito con $1", + "reactionAdd": "Aggiungi reazione", + "createThread": "Crea thread", + "threadOptions": "Opzioni thread", + "reactions": "Visualizza reazioni", + "reactionsTitle": "Reazioni" + }, + "report": { + "back": "Indietro", + "next": "Successivo", + "cancel": "Annulla", + "submit": "Invia", + "blockUser": "Blocca utente" + }, + "messageLink": "Messaggio in # $1", + "no": "No", + "noPins": "Non c'è niente per te qui...per ora", + "otherAt": "$1 alle $2", + "permissions": { + "descriptions": { + "BYPASS_SLOWMODE": "La modalità lenta non ha più effetto sui membri del ruolo.", + "PIN_MESSAGES": "Consente ai membri del ruolo di fissare messaggi.", + "MANAGE_EVENTS": "Consente ai membri del ruolo di modificare/cancellare eventi (esistenti e futuri).", + "SEND_VOICE_MESSAGES": "Consente l'invio di messaggi vocali nei canali di testo.", + "CREATE_INSTANT_INVITE": "Consente ai membri del ruolo di creare inviti per la gilda." + }, + "readableNames": { + "BYPASS_SLOWMODE": "Bypassa la slowmode", + "CONNECT": "Connetti", + "PIN_MESSAGES": "Fissa messaggi", + "SEND_MESSAGES": "Invia messaggi", + "STREAM": "Video" + } + }, + "profile": { + "userInfo": "Informazioni utente" + }, + "profileColor": "Colore del profilo", + "pronouns": "Pronomi:", + "readableName": "Italiano", + "register": { + "DOBError": "Data di nascita: $1", + "register": "Registrati", + "usernameError": "Nome utente: $1" + }, + "remove": "Rimuovi", + "reply": "Rispondi", + "role": { + "color": "Primo colore", + "colors": { + "name": "Schema di colori:", + "two": "Bicolore", + "secondColor": "Secondo colore:" + }, + "copyId": "Copia ID ruolo", + "displaySettings": "Impostazioni di visualizzazione", + "perms": "Autorizzazioni", + "roles": "Ruoli" + }, + "search": { + "back": "Indietro", + "new": "Nuovo", + "next": "Successivo", + "old": "Vecchio", + "page": "Pagina $1" + }, + "searchGifs": "Ricerca $1", + "settings": { + "clear": "Pulisci", + "img": "Carica immagine" + }, + "spoiler": "Spoiler", + "sticker": { + "confirmDel": "Sei sicuro di voler cancellare questo sticker?", + "del": "Elimina sticker", + "desc": "Descrizione", + "image": "Immagine:", + "name": "Nome:" + }, + "todayAt": "Oggi alle $1", + "updateAv": "Aggiornamenti disponibili", + "uploadFilesText": "Carica i tuoi file qui!", + "user": { + "report": "Segnala utente", + "deleted": "Utenza cancellata", + "reportApp": "Segnala applicazione", + "editServerProfile": "Modifica il profilo della gilda", + "remove": "Rimuovi utente", + "unblock": "Sblocca utente" + }, + "userping": { + "unknown": "@utente-sconosciuto" + }, + "webauth": { + "addKey": "Aggiungi chiave", + "keyname": "Nome chiave:", + "manage": "Gestisci chiavi di sicurezza" + }, + "webhooks": { + "channel": "Output del webhook sul canale:", + "createdBy": "Creato da:", + "name": "Nome del webhook:", + "type": "Tipo: $1", + "type3": "Applicazione" + }, + "yes": "Si", + "yesterdayAt": "Ieri alle $1" +} diff --git a/translations/ja.json b/translations/ja.json new file mode 100644 index 00000000..3bd3fb86 --- /dev/null +++ b/translations/ja.json @@ -0,0 +1,508 @@ +{ + "@metadata": { + "authors": [ + "Miyakkon", + "Omotecho", + "RhAnish", + "もなー(偽物)" + ] + }, + "2faCode": "2段階認証コード:", + "404": { + "404": "404 ページが見つかりません", + "app": "アプリへ", + "blog": "Fermi ブログ", + "home": "ホームページ", + "listtitle": "お探しのページはこちらでしたか?", + "login": "ログイン", + "report": "クライアント内でこのページを見た場合は、報告してください:", + "reset": "パスワードリセットページ", + "title": "道に迷われているようです", + "whatelse": "他に何が起こるべきだと思いますか?", + "whereever": "どこでも" + }, + "onboarding": { + "name": "オンボーディング", + "disable": "オンボーディングを無効化", + "addChannel": "チャンネルを追加", + "channel": "チャンネル:", + "desc": "説明:", + "deleteChannel": "オンボーディングからチャンネルを削除", + "enable": "オンボーディングを有効化", + "title": "$1へようこそ!" + }, + "DMs": { + "add": "このDMに誰かを追加する", + "close": "DMを閉じる", + "copyId": "DMのIDをコピー", + "markRead": "既読にする", + "name": "ダイレクトメッセージ" + }, + "ok": "完了", + "dismiss": "完了", + "UrlGen": "URLジェネレータ", + "Voice": { + "status": { + "badWS": "WSからの不正なレスポンス", + "conectionFailed": "接続に失敗しました", + "done": "接続済み", + "left": "ボイスチャットから退出", + "makingOffer": "オファーを出す", + "noSDP": "SDPなし", + "notconnected": "未接続", + "sendingStreams": "オーディオストリームの送信", + "startingRTC": "RTC接続を開始しています", + "waitingMainWS": "メインWSを待機しています", + "waitingURL": "音声のURLを待機しています", + "wsAuth": "WSの承認を待機しています", + "wsOpen": "WSが開くのを待機しています" + } + }, + "accessibility": { + "gifSettings": { + "always": "常時", + "hover": "ホバーした時", + "never": "常にしない" + }, + "name": "アクセシビリティ", + "playGif": "GIFを再生します:", + "playIcon": "アニメ絵アイコンを再生する:", + "roleColors": "ロールの色を無効にする:", + "gradientColors": "色のグラデーションを無効にする:", + "decorations": "アバターの装飾を有効にする:" + }, + "accountNotStart": "アカウントを開始できません", + "add": "追加", + "applications": { + "delete": "アプリケーションを削除", + "sure": "$1を本当に削除しますか?" + }, + "badge": { + "active_developer": "アクティブな開発者", + "application_guild_subscription": "プレミアムあり", + "automod": "automod を使用", + "bot_commands": "サポートコマンド", + "bug_hunter_level_1": "バグハンター レベル1", + "bug_hunter_level_2": "バグハンター レベル2", + "certified_moderator": "モデレータ", + "early_supporter": "初期からのサポーター", + "guild_booster_lvl1": "ギルドをブーストした", + "guild_booster_lvl2": "ギルドをブーストした", + "guild_booster_lvl3": "ギルドをブーストした", + "guild_booster_lvl4": "ギルドをブーストした", + "guild_booster_lvl5": "ギルドをブーストした", + "guild_booster_lvl6": "ギルドをブーストした", + "guild_booster_lvl7": "ギルドをブーストした", + "guild_booster_lvl8": "ギルドをブーストした", + "guild_booster_lvl9": "長期間ギルドをブーストした", + "hypesquad": "Vibesquad [プレスホルダー]", + "hypesquad_house_1": "勇気", + "hypesquad_house_2": "優秀", + "hypesquad_house_3": "安定", + "legacy_username": "昔の利用者名を持つ", + "partner": "インスタンスのパートナー", + "premium": "プレミアム", + "quest_completed": "クエストをクリア", + "staff": "インスタンスのスタッフ", + "verified_developer": "認証済みの開発者" + }, + "bio": "自己紹介:", + "blankMessage": "空白のメッセージ", + "blog": { + "blog": "ブログ", + "blogUpdates": "ブログの更新:", + "desc": "Fermiの最新情報についてはブログを読むか、登録してください!", + "fermi": "Fermi ブログ", + "gotoPost": "投稿へ移動", + "wantUpdates": "毎週更新されるFermiの状態に関する情報を受け取りませんか?" + }, + "bot": "ボット", + "botGuilds": "ギルドのボットは次の場所にあります:", + "botSettings": "ボットの設定", + "cancel": "キャンセル", + "channel": { + "SlowmodeCool": "低速モードのクールダウン: $1", + "TimeOutCool": "次の時間までタイムアウト: $1", + "allowIcons": "独自のチャンネルアイコンを許可する", + "announcement": "アナウンス", + "copyId": "チャンネルIDをコピー", + "copyIdCat": "カテゴリIDをコピー", + "createCatagory": "カテゴリを作成", + "createChannel": "チャンネルを作成", + "creating": "チャンネルを作成しています", + "delete": "チャンネルを削除", + "deleteCat": "カテゴリを削除", + "icon": "アイコン:", + "makeInvite": "招待を作成", + "markRead": "既読にする", + "mute": "チャンネルをミュート", + "name": "チャンネル", + "name:": "チャンネル名:", + "nsfw:": "NSFW(仕事場で見るには適さない)/成人向けコンテンツの警告:", + "permissions": "権限", + "selectCatName": "カテゴリ名", + "selectName": "チャンネル名", + "selectType": "チャンネルの種別を選択", + "settings": "設定", + "settingsFor": "$1の設定", + "slowmode": "低速モード:", + "text": "テキスト", + "timedOutUntil": "次の時間までタイムアウト: $1", + "topic:": "トピック:", + "typebox": "$1 にメッセージ", + "unmute": "チャンネルのミュートを解除", + "voice": "ボイス", + "deleteThread": "スレッドを削除", + "hideThreads": "非アクティブになれば隠す", + "forum": "フォーラム" + }, + "threads": { + "leave": "スレッドから退出", + "join": "スレッドに参加", + "name": "スレッド名:", + "editTags": "タグを編集" + }, + "forum": { + "creorsear": "投稿を作成または検索", + "next": "次へ", + "back": "戻る", + "newPost": "新しい投稿", + "post": "投稿", + "sortOptions": { + "sortby": { + "title": "並び順", + "recent": "直近でアクティブ", + "posted": "作成日時" + }, + "sortOrder": { + "title": "並び替え順", + "recent": "新しい順で", + "old": "古い順で" + }, + "tagMatch": { + "title": "タグの一致", + "some": "いくつかのタグと一致させる", + "all": "全てのタグと一致させる" + }, + "name": "並び替え" + }, + "errors": { + "tagsReq": "投稿にタグを必須にする", + "requireText": "メッセージは空にできません" + }, + "allTags": "全てのタグ", + "settings": { + "editTags": "タッグ編集", + "editTag": "タッグ編集", + "tagName": "タッグ名前", + "moderated": "タグ適用をモデレーターにのみ許可す\nる", + "addTag": "タグを追加", + "delTag": "タグを削除" + } + }, + "channelLink": "# $1", + "clientDesc": "クライアント バージョン: $1 \n\n[Fermiの公式ギルドに参加する]( $2 /invite/USgYJo?instance=https %3A% 2F%2Fspacebar.chat)\n\n[Fermiの翻訳に協力する](https://translatewiki.net/wiki/Translating:JankClient#sortable:3=desc) \n\n[Fermiの開発に協力する](https://github.com/MathMan05/Fermi)\n\n[Fermiが依存するサーバーのメンテナンスに協力する](https://github.com/spacebarchat/server)\n\n[ブログを読む](https://blog.fermi.chat/)\n\n推定された権限: $3", + "commands": { + "errorNotValid": "$1は$2に対する有効な選択肢ではありません", + "required": "$1はこのコマンドに必須の箇所です" + }, + "confirmGuildLeave": "$1から本当に退出しますか?", + "copyLink": "メッセージリンクをコピー", + "copyRegLink": "リンクをコピー", + "copymessageid": "メッセージIDをコピー", + "copyrawtext": "原文をコピー", + "createAccount": "アカウントを作成", + "delete": "削除", + "deleteConfirm": "メッセージを削除します。よろしいですか?", + "devSettings": { + "badUser": "過剰な情報を送信する不正な利用者オブジェクトのログ記録を有効にする:", + "cache": "Service Worker のキャッシュ マップ ファイルを有効にする:", + "cacheDesc": "どちらの方法でもマップ ファイルは読み込まれますが、これにより、新しい更新がロールアウトされたときにマップ ファイルが確実にキャッシュ内に存在するようになります。", + "captureTrace": "この設定は、Fermi にサーバーから _trace プロパティをキャプチャするように指示します。これを有効にすると、プログレッシブな JSON デコードが機能しなくなる可能性があります (再読み込みが必要になる場合があります)。", + "clearWellKnowns": ".well-known レコードを再取得する:", + "description": "これらは、SpacebarやFermiの開発者向け設定です。何をしているのか分からない場合は、これらの設定を変更しないでください。", + "gatewayComp": "ゲートウェイ圧縮を無効にする:", + "reportSystem": "試験的な通報機能を有効にする:", + "logGateway": "受信したゲートウェイ イベントを記録します (ログレベル情報):", + "name": "開発者設定", + "traces": "トレースを公開:" + }, + "deviceManage": { + "city": "都市: $1", + "continent": "大陸: $1", + "country": "国: $1", + "curSes": "これは現在使用しているセッションです。他のメニューからログアウトする必要があります。", + "estimateWarn": "警告:この情報はすべて推測に過ぎず、間違っている可能性があります。", + "ip": "最後に確認されたIPアドレス:$1", + "last": "最後に使用したおおよその時期:$1", + "latitude": "緯度: $1", + "logout": "ログアウト", + "longitude": "経度: $1", + "manageDev": "デバイスを管理する", + "postal": "郵便番号: $1", + "region": "地域: $1", + "title": "セッションの管理" + }, + "discovery": "ディスコバリー", + "dms": "メッセージ", + "edit": "編集", + "emoji": { + "confirmDel": "絵文字を削除します。よろしいですか?", + "found": { + "not": "この絵文字はあなたのギルドのものではありません。この絵文字を使用するには、それをホストしているギルドに参加してください。", + "other": "この絵文字は他のギルドのものです。", + "private": "この絵文字を持つギルドが見つかりません", + "this": "この絵文字はこのギルドのものです。" + }, + "from": "この絵文字は", + "image:": "画像:", + "join": "ギルドに参加する", + "name:": "名前:", + "privateGuild": "プライベートギルド", + "title": "絵文字", + "upload": "絵文字をアップロードする" + }, + "emojiSelect": "絵文字:", + "errorReconnect": "サーバーに接続できません。** $1 ** 秒後に再試行します...", + "favoriteGifs": "お気に入りのGIF", + "folder": { + "color": "フォルダの色:", + "create": "新しいフォルダを作成", + "edit": "フォルダ編集", + "name": "フォルダ名前:" + }, + "form": { + "captcha": "待って、あなたは人間ですか?" + }, + "friends": { + "addfriend": "友達を追加", + "addfriendpromt": "ユーザー名で友達を追加:", + "all": "全て", + "all:": "友達全員:", + "blocked": "ブロック中", + "blockedusers": "ブロックされたユーザー:", + "bu": "ブロックされたユーザー", + "discnotfound": "識別子が見つかりません", + "friendlist": "友達リスト", + "friends": "友達", + "notfound": "ユーザーが見つかりません", + "online": "オンライン", + "online:": "オンライン友達:", + "pending": "保留中", + "pending:": "保留中のフレンド申請:", + "removeFriend": "友達を削除" + }, + "goThere": "はい、行きましょう", + "goThereTrust": "今後、このドメインを頼する", + "goingToURL": "このリンクをクリックすると、次のウェブサイトに移動します: $1", + "group": { + "createdm": "DMを作って!", + "edit": "グループチャットを編集", + "select": "友達を選択" + }, + "guild": { + "COMMUNITY": "参加を申し込む", + "disableInvites": "招待を無効にする:", + "DISCOVERABLE": "ディスコバリー", + "INVITES_DISABLED": "招待のみ", + "adminMenu": { + "changePerms": "検索に関する権限を変更する", + "finding": "管理者を探す", + "ownName": "オーナー", + "owner": "オーナーを見つける", + "permission": "権限" + }, + "report": "ギルドを報告する", + "admins": "管理者を見つける", + "all": "全て", + "banReason": "禁止理由: $1", + "bannedBy": "禁止者:", + "banner:": "バナー:", + "bans": "禁止したユーザー", + "boostMessage?": "このギルドをブーストする人がいたら、メッセージを送信します!", + "community": "コミュニティ", + "confirmDelete": "$1を削除します。よろしいですか?", + "confirmLeave": "退出します。よろしいですか?", + "copyId": "ギルドIDをコピー", + "create": "ゼロからギルドを作成", + "createFromTemplate": "テンプレートからギルドを作成", + "createNewTemplate": "新しいテンプレートを作成", + "creating": "ギルドの作成", + "default": "デフォルト ( $1 )", + "defaultNoti": "ギルドの標準の通知設定", + "delete": "ギルドを削除", + "description:": "説明:", + "disoveryTitle": "ギルドを見つける ($1{{PLURAL:$1|エントリ}})", + "editingTemplate": "$1 を編集", + "emptytext": "なんと不思議なことでしょう、このギルドにはチャンネルがありません!?", + "emptytitle": "奇妙な場所", + "guilds": "ギルド", + "helpTips?": "ギルドに役立つヒントを送ってください!", + "howJoin": "どうすればギルドに参加できますか?", + "icon:": "アイコン:", + "invites": "招待", + "joinConfirm": "$1に参加しますか?", + "leave": "ギルドを脱退", + "loadingDiscovery": "読み込み中...", + "makeInvite": "招待する", + "markRead": "既読にする", + "memberCount": "$1{{PLURAL:$1|人}}のメンバー", + "mute": "ミュートギルド", + "name:": "名前:", + "nameNoMatch": "名前が一致しません", + "noDelete": "いいえ", + "noLeave": "いいえ", + "none": "なし", + "notifications": "通知", + "onlyMentions": "@メンションのみ", + "overview": "概要", + "region:": "地域:", + "roles": "ロール", + "ruleId": "ルールチャンネル:", + "selectnoti": "このギルドでの通知設定", + "sendrandomwelcome?": "このギルドに入る人がいたら、ウェルカムメッセージをランダムに送信します。", + "serverName": "ギルド名:", + "settings": "設定", + "settingsFor": "$1の設定", + "splash": "ディスカバリースプラッシュ:", + "stickWelcomeReact?": "ウェルカムメッセージにスタンプで返信するようメンバーに促します。", + "systemSelect:": "チャンネル", + "tempCreatedBy": "テンプレート作成者:", + "tempUseCount": "テンプレートは$1{{PLURAL:$1|回}}使用されています", + "template": "ギルド構築用テンプレートのリンク/コード:", + "templateDesc": "テンプレートの説明:", + "templateName": "テンプレート名:", + "templateNameShort": "テンプレート名は2文字以上である必要があります", + "templateURL": "テンプレートへのリンク: $1", + "templates": "テンプレート", + "yesDelete": "はい", + "yesLeave": "はい" + }, + "hideBlockedMessages": "ブロックしたメッセージ", + "htmlPages": { + "createAccount": "アカウントを作成", + "dobField": "生年月日:", + "emailField": "メールアドレス:", + "idpermissions": "このことでボットは:", + "loadingText": "Fermiが読み込まれています", + "loginButton": "ログイン", + "noAccount": "アカウントをお持ちではありませんか?", + "pw2Field": "パスワードをもう一度入力してください:", + "pwField": "パスワード:", + "switchaccounts": "アカウントを切り替える", + "trans": "翻訳する", + "transTitle": "Fermiを翻訳する", + "userField": "ユーザー名:", + "welcomeJank": "Fermiへようこそ" + }, + "instanceStats": { + "servers": "ギルド: $1", + "users": "登録ユーザー: $1" + }, + "interactions": { + "ephemeralDismiss": "メッセージを閉じる", + "onlyYou": "これはあなただけに表示されています" + }, + "invite": { + "channel:": "チャンネル:", + "createInvite": "招待を作成", + "createdAt": "$1で作成", + "expireAfter": "期限", + "expires": "期限: $1", + "forChannel": "チャンネル: $1", + "inviteLinkCode": "招待リンク/コード", + "inviteMaker": "招待メーカー", + "never": "期限なし", + "notFound": "招待状が見つかりません" + }, + "inviteOptions": { + "12h": "12時間", + "1d": "1日", + "1h": "1時間", + "30d": "30日", + "30m": "30分", + "6h": "6時間", + "7d": "7日", + "never": "期限なし", + "noLimit": "∞", + "title": "フレンドを招待する" + }, + "leaveGuild": "ギルドを脱退", + "loaded": "ロード済み", + "localuser": { + "2faCode:": "コード:", + "2faDisable": "2FAを無効にする", + "2faEnable": "2FAを有効にする", + "PasswordsNoMatch": "パスワードが一致しません", + "TOSURL": "利用規約リンク:", + "accentColor": "テーマごとの色指定:", + "accountSettings": "アカウント設定", + "deleteAccount": "アカウント削除" + }, + "logout": { + "error": { + "cancel": "キャンセル", + "cont": "続行", + "desc": "Fermi からログアウトすることができませんでした。強制的にアカウントを除去しますか?", + "title": "ログアウト中にエラーが発生しました" + }, + "logout": "ログアウト" + }, + "media": { + "artist": "アーティスト: $1", + "composer": "作曲家: $1", + "download": "メディアをダウンロード", + "length": "長さ: $1分$2秒", + "loading": "読み込み中", + "moreInfo": "詳細情報", + "notFound": "メディアが見つかりません" + }, + "noStickers": "変な場所に来てしまったようです。使えるステッカーはありません!", + "permissions": { + "descriptions": { + "ADMINISTRATOR": "すべての権限を許可し、チャンネル権限の上書きを回避します。これは危険な権限です。", + "MANAGE_GUILD": "ギルドの管理と編集ができるようにする", + "VIEW_AUDIT_LOG": "利用者が監査ログを閲覧できるようにする", + "VIEW_GUILD_INSIGHTS": "利用者がギルドの分析情報を閲覧できるようにする", + "MENTION_EVERYONE": "所属するユーザーが全員にメンションできるようにする", + "MANAGE_NICKNAMES": "利用者が他の参加者のニックネームを変更できるようにする", + "MANAGE_MESSAGES": "利用者が自分のものではないメッセージを削除できるようにする", + "MANAGE_CHANNELS": "利用者がチャンネルを管理・編集できるようにする", + "KICK_MEMBERS": "利用者がギルドからメンバーをキックできるようにする", + "BAN_MEMBERS": "利用者がギルドからメンバーをBANできるようにする", + "MOVE_MEMBERS": "利用者が他の参加者を別のボイスチャンネルに移動できるようにする", + "MUTE_MEMBERS": "利用者が他の参加者をミュートできるようにする", + "DEAFEN_MEMBERS": "利用者が自分をミュートし、かつ他の利用者の音声を自らに聴こえなくすること(deaf)ができるようにする", + "CHANGE_NICKNAME": "利用者が自分のニックネームを変更できるようにする", + "VIEW_CHANNEL": "利用者がチャンネルを閲覧できるようにする", + "READ_MESSAGE_HISTORY": "利用者がメッセージ履歴を閲覧できるようにする", + "ADD_REACTIONS": "利用者がメッセージにリアクションを追加できるようにする", + "SEND_MESSAGES": "利用者がメッセージを送信できるようにする", + "SEND_TTS_MESSAGES": "利用者がテキスト読み上げメッセージを送信できるようにする", + "USE_EXTERNAL_EMOJIS": "利用者が外部の絵文字を使用できるようにする", + "EMBED_LINKS": "この利用者が送信したリンクの自動埋め込みを許可する", + "ATTACH_FILES": "利用者がファイルを添付できるようにする", + "CREATE_INSTANT_INVITE": "利用者がギルドへの招待を作成できるようにします", + "CONNECT": "利用者がボイスチャンネルに接続できるようにする", + "SPEAK": "利用者がボイスチャンネルで発言できるようにする", + "USE_VAD": "利用者が声を出すだけでボイスチャンネルで話し始められるようにする", + "STREAM": "利用者が配信できるようにする", + "PRIORITY_SPEAKER": "ボイスチャネルで優先スピーカーを使用できるようにする" + } + }, + "pinMessage": "メッセージをピン留め", + "profile": { + "userInfo": "利用者情報" + }, + "readableName": "英語", + "recentEmoji": "最近の絵文字", + "reply": "返信", + "unableToPin": "メッセージをピン留めできません", + "vc": { + "joinForStream": "ボイスチャットに参加して視聴する", + "joiningStream": "配信に参加しています…", + "joinstream": "配信を見る", + "leavestream": "配信から退出", + "stopstream": "配信を停止" + } +} diff --git a/translations/ko.json b/translations/ko.json index 45116978..d01ca6a5 100644 --- a/translations/ko.json +++ b/translations/ko.json @@ -1,251 +1,452 @@ { "@metadata": { "authors": [ + "Bisang", + "McDutchie", + "Suleiman the Magnificent Television", + "YeBoy371", "Ykhwong" ] }, - "readableName": "영어", - "reply": "답변", - "copymessageid": "메시지 ID 복사", - "permissions": { - "descriptions": { - "SEND_MESSAGES": "사용자가 메시지를 보낼 수 있도록 허용합니다", - "MENTION_EVERYONE": "사용자가 모든 사람을 언급할 수 있도록 허용합니다", - "MANAGE_ROLES": "사용자가 역할을 편집하고 관리할 수 있도록 허용합니다", - "MANAGE_EVENTS": "사용자가 이벤트를 편집하고 관리할 수 있도록 허용합니다" - }, - "readableNames": { - "CREATE_INSTANT_INVITE": "초대장 만들기", - "ADMINISTRATOR": "관리자", - "MANAGE_CHANNELS": "채널 관리", - "ADD_REACTIONS": "반응 추가", - "VIEW_AUDIT_LOG": "감사 로그 보기", - "STREAM": "비디오", - "VIEW_CHANNEL": "채널 보기", - "SEND_MESSAGES": "메시지 보내기", - "MANAGE_MESSAGES": "메시지 관리", - "EMBED_LINKS": "링크 삽입", - "ATTACH_FILES": "파일 첨부", - "READ_MESSAGE_HISTORY": "메시지 역사 읽기", - "USE_EXTERNAL_EMOJIS": "외부 이모티콘 사용", - "CONNECT": "연결", - "MUTE_MEMBERS": "회원 알림을 받지 않음", - "CHANGE_NICKNAME": "별명 바꾸기", - "MANAGE_NICKNAMES": "별명 관리", - "MANAGE_ROLES": "역할 관리", - "MANAGE_WEBHOOKS": "웹훅 관리", - "MANAGE_EVENTS": "이벤트 관리", - "MANAGE_THREADS": "스레드 관리", - "CREATE_PUBLIC_THREADS": "공개 스레드 만들기", - "CREATE_PRIVATE_THREADS": "비공개 스레드 만들기", - "USE_EXTERNAL_STICKERS": "외부 스티커 사용", - "USE_SOUNDBOARD": "사운드보드 사용", - "CREATE_EVENTS": "이벤트 만들기", - "USE_EXTERNAL_SOUNDS": "외부 사운드 사용", - "SEND_VOICE_MESSAGES": "음성 메시지 보내기", - "USE_EXTERNAL_APPS": "외부 앱 사용" + "2faCode": "2FA 코드:", + "404": { + "404": "오류:404 - 페이지를 찾을 수 없음", + "app": "앱으로", + "blog": "Fermi 블로그", + "home": "홈페이지", + "login": "로그인" + }, + "onboarding": { + "addChannel": "채널 추가", + "channel": "채널:", + "desc": "설명:", + "title": "$1에 오신 것을 환영합니다!" + }, + "DMs": { + "close": "DM 닫기", + "copyId": "DM ID 복사", + "markRead": "읽은 것으로 표시", + "name": "직신" + }, + "ok": "확인", + "UrlGen": "봇 초대 생성기", + "Voice": { + "status": { + "waitingMainWS": "주요 WS를 기다리는 중", + "waitingURL": "음성 URL을 기다리는 중", + "wsAuth": "WS의 승인을 기다리는 중", + "wsOpen": "WS가 열리기를 기다리는 중" } }, - "hideBlockedMessages": "이 사용자를 차단하였습니다. 이 메시지를 숨기려면 클릭하세요", - "deleteConfirm": "이것을 삭제하시겠습니까?", - "yes": "예", - "no": "아니요", - "todayAt": "오늘 $1", - "yesterdayAt": "어제 $1", - "botSettings": "봇 설정", - "confirmGuildLeave": "$1 장소를 떠나시겠습니까", - "UrlGen": "URL 생성기", - "typing": "$2님이 타이핑 중{{PLURAL:$1|입니다}}", + "accessibility": { + "name": "접근성" + }, + "badge": { + "certified_moderator": "중재자" + }, "blankMessage": "빈 메시지", + "bot": "봇", + "botSettings": "봇 설정", "channel": { + "announcement": "알림", "copyId": "채널 ID 복사", - "markRead": "읽은 것으로 표시", - "settings": "설정", + "createCatagory": "분류 만들기", + "createChannel": "채널 만들기", "delete": "채널 삭제", "makeInvite": "초대하기", + "markRead": "읽은 것으로 표시", + "name": "채널", + "name:": "채널 이름:", + "nsfw:": "후방주의/성인용 콘텐츠 경고:", + "permissions": "권한", + "selectCatName": "분류 이름", + "selectName": "채널 이름", + "selectType": "채널 유형 선택", + "settings": "설정", "settingsFor": "$1 설정", - "voice": "음성", "text": "텍스트", - "announcement": "알림", - "name:": "이름:", "topic:": "주제:", - "nsfw:": "NSFW:", - "selectType": "채널 유형 선택", - "selectName": "채널 이름", - "selectCatName": "채널 이름", - "createChannel": "채널 만들기", - "createCatagory": "분류 만들기" + "voice": "음성", + "deleteThread": "스레드 삭제", + "forum": "포럼" }, - "switchAccounts": "계정 전환 ⇌", - "htmlPages": { - "idpermissions": "이를 통해 봇은 다음 작업을 수행할 수 있습니다.", - "addBot": "서버에 추가", - "loaddesc": "오래 걸리지 않습니다", - "switchaccounts": "계정 전환", - "instanceField": "인스턴스:", - "emailField": "이메일:", - "pwField": "비밀번호:", - "loginButton": "로그인", - "noAccount": "계정이 없나요?", - "userField": "사용자 이름:", - "dobField": "출생일:", - "createAccount": "계정 만들기", - "alreadyHave": "계정이 이미 있습니까?", - "openClient": "클라이언트 열기" + "threads": { + "name": "스레드 이름:" }, - "register": { - "passwordError:": "비밀번호: $1", - "usernameError": "사용자 이름: $1", - "emailError": "이메일: $1", - "DOBError": "출생일: $1" + "forum": { + "next": "다음", + "back": "뒤로", + "newPost": "새 글 작성하기", + "sortOptions": { + "sortby": { + "title": "정렬 기준" + }, + "sortOrder": { + "title": "정렬 순서" + } + }, + "allTags": "모든 태그", + "settings": { + "editTags": "태그 편집:", + "editTag": "태그 편집", + "tagName": "태그 이름:", + "addTag": "태그 추가", + "delTag": "태그 삭제" + } + }, + "channelLink": "# $1", + "confirmGuildLeave": "$1 장소를 떠나시겠습니까", + "copyRegLink": "링크 복사", + "copymessageid": "메시지 ID 복사", + "delete": "삭제", + "deleteConfirm": "이것을 삭제하시겠습니까?", + "emoji": { + "confirmDel": "이 이모티콘을 삭제하시겠습니까?", + "image:": "이미지:", + "name:": "이름:", + "title": "이모티콘", + "upload": "이모티콘 업로드" + }, + "friends": { + "addfriend": "친구 추가", + "addfriendpromt": "사용자 이름으로 친구 추가:", + "all": "모두", + "all:": "모든 친구:", + "blocked": "차단됨", + "blockedusers": "차단된 사용자:", + "friendlist": "친구 목록", + "friends": "친구", + "notfound": "사용자를 찾을 수 없습니다", + "online": "온라인", + "online:": "온라인 친구:", + "pending": "보류 중", + "pending:": "대기 중인 친구 요청:", + "removeFriend": "친구 제거" }, - "submit": "제출", "guild": { - "markRead": "읽은 것으로 표시", - "notifications": "알림", - "settings": "설정", + "INVITES_DISABLED": "초대 전용", + "adminMenu": { + "changePerms": "찾기 위해 권한 변경", + "finding": "관리자 찾기", + "ownName": "소유자", + "owner": "소유자 찾기", + "permission": "권한:" + }, + "admins": "관리자 찾기", + "all": "모두", + "banner:": "배너:", + "confirmLeave": "떠나시겠습니까?", + "createFromTemplate": "틀에서 길드 만들기", + "createNewTemplate": "새 틀 만들기", + "default": "기본값 ($1)", + "description:": "설명:", + "icon:": "아이콘:", + "loadingDiscovery": "불러오는 중...", "makeInvite": "초대하기", - "settingsFor": "$1 설정", + "markRead": "읽은 것으로 표시", + "memberCount": "$1명의 {{PLURAL:$1|회원}}", "name:": "이름:", - "topic:": "주제:", - "icon:": "아이콘:", + "none": "없음", + "notifications": "알림", "overview": "개요", - "banner:": "배너:", "region:": "지역:", "roles": "역할", - "all": "모두", - "none": "노드", - "confirmLeave": "떠나시겠습니까?", - "yesLeave": "네, 확실합니다", - "serverName": "서버 이름:", + "serverName": "길드 이름:", + "settings": "설정", + "settingsFor": "$1 설정", + "systemSelect:": "시스템 메시지 채널:", + "tempCreatedBy": "틀 만든이:", + "templateName": "틀 이름:", + "templateURL": "틀 링크: $1", + "templates": "틀", + "topic:": "주제:", + "viewTemplate": "틀 보기", "yesDelete": "네, 확실합니다", - "loadingDiscovery": "불러오는 중..." + "yesLeave": "네, 확실합니다" }, - "role": { - "displaySettings": "표시 설정", - "name": "역할 이름:", - "color": "색", - "remove": "역할 제거", - "delete": "역할 삭제", - "confirmDelete": "'$1' 항목을 삭제하시겠습니까?" + "hideBlockedMessages": "이 사용자를 차단하였습니다. 이 메시지를 숨기려면 클릭하세요", + "htmlPages": { + "addBot": "길드에 추가", + "alreadyHave": "계정이 이미 있습니까?", + "createAccount": "계정 만들기", + "dobField": "출생일:", + "emailField": "이메일:", + "idpermissions": "이를 통해 봇은 다음 작업을 수행할 수 있습니다.", + "instanceField": "인스턴스:", + "loaddesc": "오래 걸리지 않습니다", + "loginButton": "로그인", + "noAccount": "계정이 없나요?", + "openClient": "클라이언트 열기", + "pwField": "비밀번호:", + "switchaccounts": "계정 전환", + "trans": "번역", + "userField": "사용자 이름:" }, - "settings": { - "save": "변경사항 저장" + "instanceStats": { + "messages": "메시지: $1", + "name": "인스턴스 통계: $1", + "servers": "길드: $1", + "users": "등록된 사용자: $1" + }, + "invite": { + "accept": "수락", + "alreadyJoined": "이미 참여했습니다", + "channel:": "채널:", + "createInvite": "초대장 만들기", + "inviteLinkCode": "초대 링크/코드", + "invitedBy": "$1님이 당신을 초대했습니다", + "longInvitedBy": "$1님이 당신을 $2에 초대했습니다", + "notFound": "초대장을 찾을 수 없습니다" + }, + "inviteOptions": { + "12h": "12시간", + "1d": "1일", + "1h": "1시간", + "30d": "30일", + "30m": "30분", + "6h": "6시간", + "7d": "7일", + "never": "없음", + "noLimit": "제한 없음", + "title": "인원 초대" }, "localuser": { - "settings": "설정", - "userSettings": "사용자 설정", - "themesAndSounds": "테마 및 사운드", - "theme:": "테마", - "notisound": "알림음:", - "VoiceWarning": "이 기능을 활성화하시겠습니까? 이 기능은 매우 실험적이며 문제가 발생할 가능성이 높습니다. (이 기능은 개발자를 위한 것이므로 무엇을 하는지 모르는 경우 활성화하지 마십시오)", - "updateSettings": "설정 업데이트", - "swSettings": "서비스 워커 설정", - "SWOffline": "오프라인 전용", - "clearCache": "캐시 지우기", + "2faCode:": "코드:", + "2faDisable": "2FA 비활성화", + "2faEnable": "2FA 활성화", "CheckUpdate": "업데이트 확인", + "PasswordsNoMatch": "비밀번호가 일치하지 않습니다", + "TOSURL": "이용 약관 URL:", + "VoiceWarning": "이 기능을 활성화하시겠습니까? 이 기능은 매우 실험적이며 문제가 발생할 가능성이 높습니다. (이 기능은 개발자를 위한 것이므로 무엇을 하는지 모르는 경우 활성화하지 마십시오)", "accountSettings": "계정 설정", - "2faDisable": "2FA 비활성화", + "addBot": "봇 추가", + "addStatus": "상태 추가", + "advancedBot": "고급 봇 설정", + "appName": "애플리케이션 이름:", "badCode": "유효하지 않은 코드입니다", - "2faEnable": "2FA 활성화", - "2faCode:": "코드:", - "setUp2fa": "2FA 구성", "badPassword": "비밀번호가 잘못되었습니다", + "botAvatar": "봇 아바타:", + "botUsername": "봇 사용자 이름:", "changeEmail": "이메일 주소 바꾸기", - "password:": "비밀번호", - "newEmail:": "새 이메일", - "changeUsername": "사용자 이름 변경", - "newUsername": "새 사용자 이름:", "changePassword": "비밀번호 바꾸기", - "oldPassword:": "이전 비밀번호:", - "newPassword:": "새 비밀번호:", - "PasswordsNoMatch": "비밀번호가 일치하지 않습니다", - "devPortal": "개발자 포털", + "changeUsername": "사용자 이름 변경", + "clearCache": "캐시 지우기", + "connections": "연결", "createApp": "애플리케이션 만들기", - "team:": "팀:", - "appName": "애플리케이션 이름:", + "deleteAccount": "계정 삭제", + "deleteAccountButton": "계정 삭제", "description": "설명:", - "privacyPolcyURL": "개인정보보호정책 URL:", - "TOSURL": "이용 약관 URL:", - "publicAvaliable": "봇을 공개적으로 초대할 수 있게 할까요?", + "devPortal": "개발자 포털", + "install": "설치", + "language": "언어:", "manageBot": "봇 관리", - "addBot": "봇 추가", - "botUsername": "봇 사용자 이름:", - "botAvatar": "봇 아바타:", + "manageInstance": "인스턴스 관리", + "newEmail:": "새 이메일", + "newPassword:": "새 비밀번호:", + "newUsername": "새 사용자 이름:", + "notisound": "알림음:", + "oldPassword:": "이전 비밀번호:", + "password:": "비밀번호", + "privacyPolcyURL": "개인정보처리방침 링크:", + "publicAvaliable": "봇을 공개적으로 초대할 수 있게 할까요?", "resetToken": "토큰 초기화", + "setUp2fa": "2FA 구성", + "settings": "설정", + "status": "상태", + "team:": "팀:", + "theme:": "테마", + "themesAndSounds": "테마 및 사운드", "tokenDisplay": "토큰: $1", - "advancedBot": "고급 봇 설정", - "language": "언어:", - "connections": "연결" + "updateSettings": "설정 업데이트", + "updatesYay": "업데이트가 있습니다!", + "userSettings": "사용자 설정" + }, + "login": { + "checking": "인스턴스 확인 중", + "enterPAgain": "새 비밀번호를 다시 입력하세요:", + "invalid": "유효하지 않은 인스턴스입니다. 다시 시도하세요", + "login": "로그인", + "newPassword": "새 비밀번호:", + "recover": "비밀번호를 잊으셨나요?", + "recovery": "비밀번호를 잊으셨나요?" + }, + "logout": { + "error": { + "cont": "그래도 계속하기" + } + }, + "manageInstance": { + "AreYouSureStop": "이 인스턴스를 중지하시겠습니까?", + "TokenFormats": { + "URLs": "초대 링크" + }, + "clientURL": "클라이언트 링크:", + "copy": "복사", + "create": "만들기", + "format": "포맷:", + "length": "길이:", + "regType": "토큰 링크 유형 등록", + "stop": "인스턴스 중지" + }, + "media": { + "download": "미디어 다운로드", + "loading": "불러오는 중", + "moreInfo": "자세한 정보", + "notFound": "미디어를 찾을 수 없습니다" + }, + "member": { + "nick:": "별명:", + "reason:": "이유:" }, "message": { - "reactionAdd": "반응 추가", "delete": "메시지 삭제", + "deleted": "삭제된 메시지", "edit": "메시지 편집", - "edited": "(편집됨)" + "edited": "(편집됨)", + "reactionAdd": "반응 추가", + "threadOptions": "스레드 옵션" }, - "instanceStats": { - "name": "인스턴스 통계: $1", - "users": "등록된 사용자: $1", - "servers": "서버: $1", - "messages": "메시지: $1" + "no": "아니요", + "permissions": { + "descriptions": { + "VIEW_AUDIT_LOG": "역할 회원이 감사 로그를 볼 수 있도록 허용합니다.", + "MENTION_EVERYONE": "역할 회원이 모든 사람을 언급할 수 있도록 허용합니다.", + "MANAGE_NICKNAMES": "역할 회원이 다른 회원의 닉네임을 변경할 수 있도록 허용합니다.", + "MANAGE_MESSAGES": "역할 회원이 자신의 메시지가 아닌 것을 삭제할 수 있도록 허용합니다.", + "MANAGE_THREADS": "역할 회원이 스레드를 삭제 및 보관하고 모든 개인 스레드를 볼 수 있도록 허용합니다.", + "MANAGE_CHANNELS": "역할 회원이 채널을 관리하고 편집할 수 있도록 허용합니다.", + "MANAGE_ROLES": "역할 회원이 역할을 편집하고 관리할 수 있도록 허용합니다.", + "MANAGE_EVENTS": "역할 회원이 이벤트를 편집하고 관리할 수 있도록 허용합니다.", + "KICK_MEMBERS": "역할 회원이 길드에서 회원을 내보낼 수 있도록 허용합니다.", + "BAN_MEMBERS": "역할 회원이 길드에서 회원을 금지할 수 있도록 허용합니다.", + "MOVE_MEMBERS": "역할 회원이 음성 채널 간에 회원을 이동할 수 있도록 허용합니다.", + "MUTE_MEMBERS": "역할 회원이 다른 회원을 조용히 하게 할 수 있도록 허용합니다.", + "DEAFEN_MEMBERS": "역할 회원이 다른 회원의 소리를 듣지 못하게 할 수 있도록 허용합니다. (회원의 소리를 듣지 못하게 하면 자동으로 음소거됩니다.)", + "CHANGE_NICKNAME": "역할 회원이 자신의 닉네임을 변경할 수 있도록 허용합니다.", + "VIEW_CHANNEL": "역할 회원이 채널을 볼 수 있도록 허용합니다.", + "READ_MESSAGE_HISTORY": "역할 회원이 메시지 기록을 읽을 수 있도록 허용합니다.", + "ADD_REACTIONS": "역할 회원이 메시지에 반응을 추가할 수 있도록 허용합니다.", + "SEND_MESSAGES": "역할 회원이 메시지를 보낼 수 있도록 허용합니다.", + "SEND_MESSAGES_IN_THREADS": "역할 회원이 스레드에서 메시지를 보낼 수 있도록 허용합니다.", + "USE_EXTERNAL_EMOJIS": "역할 회원이 외부 이모티콘을 사용할 수 있도록 허용합니다.", + "USE_EXTERNAL_STICKERS": "역할 회원이 외부 스티커를 사용할 수 있도록 허용합니다.", + "ATTACH_FILES": "역할 회원이 파일을 첨부할 수 있도록 허용합니다.", + "CREATE_INSTANT_INVITE": "역할 회원이 길드 초대장을 만들 수 있도록 허용합니다.", + "CREATE_PUBLIC_THREADS": "역할 회원이 공개 스레드를 만들 수 있도록 허용합니다.", + "CREATE_PRIVATE_THREADS": "역할 회원이 개인 스레드를 만들 수 있도록 허용합니다.", + "CONNECT": "역할 회원이 음성 채널에 연결할 수 있도록 허용합니다.", + "SPEAK": "역할 회원이 음성 채널에서 말할 수 있도록 허용합니다.", + "USE_VAD": "역할 회원이 간단히 말하는 것만으로 음성 채널에서 대화할 수 있도록 허용합니다. (비활성된 경우, Push-To-Talk 키를 눌러야 말할 수 있습니다.)", + "STREAM": "역할 회원이 음성 채널에 스트리밍할 수 있도록 허용합니다.", + "REQUEST_TO_SPEAK": "역할 회원이 스테이지 채널에서 말할 수 있도록 허용합니다.", + "USE_EMBEDDED_ACTIVITIES": "역할 회원이 임베디드 활동을 사용할 수 있도록 허용합니다.", + "USE_APPLICATION_COMMANDS": "역할 회원이 애플리케이션 명령을 사용할 수 있도록 허용합니다." + }, + "readableNames": { + "ADD_REACTIONS": "반응 추가", + "ADMINISTRATOR": "관리자", + "ATTACH_FILES": "파일 첨부", + "CHANGE_NICKNAME": "별명 바꾸기", + "CONNECT": "연결", + "CREATE_EVENTS": "이벤트 만들기", + "CREATE_INSTANT_INVITE": "초대장 만들기", + "CREATE_PRIVATE_THREADS": "비공개 스레드 만들기", + "CREATE_PUBLIC_THREADS": "공개 스레드 만들기", + "EMBED_LINKS": "링크 삽입", + "MANAGE_CHANNELS": "채널 관리", + "MANAGE_EVENTS": "이벤트 관리", + "MANAGE_MESSAGES": "메시지 관리", + "MANAGE_NICKNAMES": "별명 관리", + "MANAGE_ROLES": "역할 관리", + "MANAGE_THREADS": "스레드 관리", + "MANAGE_WEBHOOKS": "웹훅 관리", + "MUTE_MEMBERS": "회원 알림을 받지 않음", + "READ_MESSAGE_HISTORY": "메시지 역사 읽기", + "SEND_MESSAGES": "메시지 보내기", + "SEND_VOICE_MESSAGES": "음성 메시지 보내기", + "STREAM": "비디오", + "USE_EXTERNAL_APPS": "외부 앱 사용", + "USE_EXTERNAL_EMOJIS": "외부 이모티콘 사용", + "USE_EXTERNAL_SOUNDS": "외부 사운드 사용", + "USE_EXTERNAL_STICKERS": "외부 스티커 사용", + "USE_SOUNDBOARD": "사운드보드 사용", + "VIEW_AUDIT_LOG": "감사 로그 보기", + "VIEW_CHANNEL": "채널 보기" + } }, - "inviteOptions": { - "title": "인원 초대", - "30m": "30분", - "1h": "1시간", - "6h": "6시간", - "12h": "12시간", - "1d": "1일", - "7d": "7일", - "30d": "30일", - "never": "없음", - "noLimit": "제한 없음" + "profile": { + "userInfo": "사용자 정보" }, - "2faCode": "2FA 코드:", - "invite": { - "invitedBy": "$1님이 당신을 초대했습니다", - "alreadyJoined": "이미 참여했습니다", - "accept": "수락", - "longInvitedBy": "$1님이 당신을 $2에 초대했습니다", - "inviteLinkCode": "초대 링크/코드" + "readableName": "한국어", + "register": { + "DOBError": "출생일: $1", + "emailError": "이메일: $1", + "passwordError:": "비밀번호: $1", + "tos": "TOS에 동의해야 합니다", + "usernameError": "사용자 이름: $1" }, - "friends": { - "blocked": "차단됨", - "blockedusers": "차단된 사용자:", - "addfriend": "친구 추가", - "notfound": "사용자를 찾을 수 없습니다", - "pending": "보류 중", - "all": "모두", - "all:": "모든 친구:", - "online": "온라인", - "online:": "온라인 친구:", - "friendlist": "친구 목록", - "friends": "친구" + "reply": "답변", + "retrying": "다시 시도하는 중...", + "role": { + "color": "첫 번째 색", + "colors": { + "name": "컬러 스킴:" + }, + "confirmDelete": "'$1' 항목을 삭제하시겠습니까?", + "delete": "역할 삭제", + "displaySettings": "표시 설정", + "name": "역할 이름:", + "remove": "역할 제거", + "roleFileIcon": "역할 아이콘:" }, - "DMs": { - "copyId": "DM ID 복사", - "markRead": "읽은 것으로 표시", - "close": "DM 닫기" + "settings": { + "clear": "지우기", + "img": "이미지 업로드", + "save": "변경사항 저장", + "updates": { + "serviceWorkerMode": { + "offlineOnly": "오프라인 전용", + "title": "서비스 워커 설정" + } + } + }, + "sticker": { + "upload": "스티커 업로드" }, + "submit": "제출", + "switchAccounts": "계정 전환 ⇌", + "todayAt": "오늘 $1", + "typing": "$2님이 타이핑 중{{PLURAL:$1|입니다}}", + "updateAv": "업데이트가 있습니다", + "uploadFilesText": "여기에 파일을 올리세요!", "user": { - "copyId": "사용자 ID 복사", - "online": "온라인", - "offline": "오프라인", - "message": "사용자에게 메시지 보내기", + "addRole": "역할 추가", "block": "사용자 차단", - "unblock": "사용자 차단 해제", + "copyId": "사용자 ID 복사", + "editServerProfile": "길드 프로필 편집", "friendReq": "친구 요청", - "addRole": "역할 추가", - "removeRole": "역할 제거" - }, - "login": { - "checking": "인스턴스 확인 중", - "invalid": "유효하지 않은 인스턴스입니다. 다시 시도하세요" + "message": "사용자에게 메시지 보내기", + "offline": "오프라인", + "online": "온라인", + "removeRole": "역할 제거", + "unblock": "사용자 차단 해제" }, - "member": { - "reason:": "이유:" + "webhook": "웹훅", + "webhooks": { + "EnterWebhookName": "웹훅 이름 입력", + "areYouSureDelete": "$1 항목을 삭제하시겠습니까?", + "avatar": "아바타", + "base": "웹훅", + "channel": "Webhook을 채널로 출력:", + "copyURL": "웹훅 링크 복사", + "createdAt": "만든 날짜: $1", + "createdBy": "만든이:", + "name": "Webhook 이름:", + "newWebHook": "새 웹훅", + "token": "웹혹 토큰: `$1`", + "type2": "채널 팔로워" }, - "uploadFilesText": "여기에 파일을 올리세요!", - "retrying": "다시 시도하는 중..." + "widget": "길드 위젯", + "yes": "예", + "yesterdayAt": "어제 $1" } diff --git a/translations/lb.json b/translations/lb.json new file mode 100644 index 00000000..e659e754 --- /dev/null +++ b/translations/lb.json @@ -0,0 +1,403 @@ +{ + "@metadata": { + "authors": [ + "Volvox" + ] + }, + "2faCode": "2FA-Code:", + "404": { + "404": "Feeler: 404 – Säit net fonnt", + "blog": "De Fermi-Blog", + "home": "Haaptsäit", + "login": "Aloggen" + }, + "onboarding": { + "addChannel": "Kanal derbäisetzen", + "channel": "Kanal:", + "desc": "Beschreiwung:", + "title": "Wëllkomm op $1!" + }, + "DMs": { + "markRead": "Als gelies markéieren" + }, + "ok": "OK", + "Voice": { + "status": { + "conectionFailed": "Verbindung feelgeschloen", + "done": "Verbonnen", + "noSDP": "Keen SDP", + "notconnected": "Net verbonnen" + } + }, + "accessibility": { + "gifSettings": { + "always": "Ëmmer", + "never": "Ni" + } + }, + "add": "Derbäisetzen", + "applications": { + "delete": "Applikatioun läschen", + "sure": "Sidd Dir sécher, datt Dir $1 läsche wëllt?" + }, + "badge": { + "certified_moderator": "Moderateur" + }, + "blankMessage": "Eidele Message", + "blog": { + "blog": "Blog", + "fermi": "Fermi-Blog" + }, + "bot": "BOT", + "botSettings": "Bot-Astellungen", + "cancel": "Ofbriechen", + "channel": { + "announcement": "Ukënnegungen", + "copyId": "Kanal-ID kopéieren", + "createCatagory": "Kategorie uleeën", + "createChannel": "Kanal uleeën", + "creating": "Kanal uleeën", + "delete": "Kanal läschen", + "deleteCat": "Kategorie läschen", + "icon": "Symbol:", + "makeInvite": "Invitatioun maachen", + "markRead": "Als gelies markéieren", + "name": "Kanal", + "name:": "Numm vum Kanal:", + "nsfw:": "NSFW:", + "selectCatName": "Numm vun der Kategorie", + "selectName": "Numm vum Kanal", + "selectType": "Kanaltyp auswielen", + "settings": "Astellungen", + "settingsFor": "Astellunge fir $1", + "text": "Text", + "topic:": "Theema:", + "hideThreads": "No Inaktivitéit verstoppen", + "forum": "Forum" + }, + "threads": { + "name": "Numm vum Thread:" + }, + "forum": { + "sortOptions": { + "sortby": { + "title": "Sortéieren no" + }, + "name": "Sortéieroptiounen" + }, + "settings": { + "delTag": "Markéierung läschen" + } + }, + "commands": { + "errorNotValid": "$1 ass kee gültege Choix fir $2" + }, + "confirmGuildLeave": "Sidd Dir sécher, datt Dir aus $1 erausgoe wëllt", + "copyRegLink": "Link kopéieren", + "createAccount": "Kont uleeën", + "delete": "Läschen", + "deleteConfirm": "Sidd Dir sécher, datt Dir dat läsche wëllt?", + "devSettings": { + "name": "Entwécklerastellungen" + }, + "deviceManage": { + "city": "Stad: $1", + "continent": "Kontinent: $1", + "country": "Land: $1", + "region": "Regioun: $1" + }, + "discovery": "Entdeckung", + "edit": "Änneren", + "emoji": { + "confirmDel": "Sidd Dir sécher, datt Dir dësen Emoji läsche wëllt?", + "image:": "Bild:", + "name:": "Numm:", + "title": "Emojien", + "upload": "Emojien eroplueden" + }, + "emojiSelect": "Emoji:", + "folder": { + "color": "Faarf vum Dossier:", + "create": "Neien Dossier uleeën", + "edit": "Dossier änneren", + "name": "Numm vum Dossier:" + }, + "friends": { + "requestsent": "Ufro ofgeschéckt!", + "blocked": "Gespaart", + "blockedusers": "Gespaarte Benotzer:", + "bu": "Gespaarte Benotzer", + "friends": "Frënn", + "notfound": "Benotzer net fonnt", + "online": "Online" + }, + "group": { + "edit": "Gruppenchat änneren" + }, + "guild": { + "adminMenu": { + "finding": "Administrateure fannen", + "ownName": "Besëtzer", + "owner": "De Besëtzer fannen" + }, + "admins": "Administrateure fannen", + "all": "All", + "community": "Communautéit", + "createNewTemplate": "Nei Schabloun uleeën", + "default": "Standard ($1)", + "description:": "Beschreiwung:", + "loadingDiscovery": "Lueden…", + "markRead": "Als gelies markéieren", + "memberCount": "$1 {{PLURAL:$1|Member|Memberen}}", + "name:": "Numm:", + "none": "Keng", + "notifications": "Notifikatiounen", + "region:": "Regioun:", + "roles": "Rollen", + "serverName": "Servernumm:", + "settings": "Astellungen", + "settingsFor": "Astellunge fir $1", + "tempCreatedBy": "Schabloun ugeluecht vum:", + "template": "Schabloun:", + "templateDesc": "Beschreiwung vun der Schabloun:", + "templateName": "Numm vun der Schabloun:", + "templateURL": "Schablounelink: $1", + "templates": "Schablounen", + "viewTemplate": "Schabloun weisen", + "yesDelete": "Jo, ech si mer sécher", + "yesLeave": "Jo, ech si mer sécher" + }, + "htmlPages": { + "createAccount": "Kont uleeën", + "dobField": "Gebuertsdatum:", + "emailField": "E-Mail:", + "instanceField": "Instanz:", + "loginButton": "Aloggen", + "noAccount": "Hutt Dir kee Benotzerkont?", + "pw2Field": "Passwuert nach eng Kéier aginn:", + "pwField": "Passwuert:", + "trans": "Iwwersetzen", + "transDesc": "Dir kënnt dobäi hëllefe fir Fermi an Är eege Sprooch z'iwwersetzen!", + "transTitle": "Fermi iwwersetzen", + "userField": "Benotzernumm:" + }, + "instanceStats": { + "members": "Memberen: $1", + "messages": "Messagen: $1", + "servers": "Serveren: $1", + "users": "Registréiert Benotzer: $1" + }, + "invite": { + "accept": "Akzeptéieren", + "alreadyJoined": "Scho bäigetrueden", + "channel:": "Kanal:", + "createInvite": "Invitatioun maachen", + "invitedBy": "Dir gouft vum $1 invitéiert", + "never": "Ni" + }, + "inviteOptions": { + "12h": "12 Stonnen", + "1d": "1 Dag", + "1h": "1 Stonn", + "30d": "30 Deeg", + "30m": "30 Minutten", + "6h": "6 Stonnen", + "7d": "7 Deeg", + "never": "Ni", + "noLimit": "Keng Limitt", + "title": "Leit invitéieren" + }, + "loaded": "Gelueden", + "localuser": { + "2faCode:": "Code:", + "2faDisable": "2FA desaktivéieren", + "2faEnable": "2FA aktivéieren", + "CheckUpdate": "No Aktualiséierunge sichen", + "PasswordsNoMatch": "D'Passwierder sinn net d'selwecht", + "addBot": "Bot derbäisetzen", + "advancedBot": "Erweidert Bot-Astellungen", + "badCode": "Ongültege Code", + "badPassword": "Falscht Passwuert", + "botUsername": "Botbenotzernumm:", + "changeEmail": "E-Mail-Adress änneren", + "changePassword": "Passwuert änneren", + "changeUsername": "Benotzernumm änneren", + "clearCache": "Cache eidelmaachen", + "connections": "Verbindungen", + "deleteAccountButton": "Kont läschen", + "description": "Beschreiwung:", + "install": "Installéieren", + "installJank": "Fermi installéieren", + "language": "Sprooch:", + "manageBot": "Bot geréieren", + "newEmail:": "Nei E-Mail", + "newPassword:": "Neit Passwuert:", + "newUsername": "Neie Benotzernumm:", + "notisound": "Notifikatiounstoun:", + "oldPassword:": "Aalt Passwuert:", + "password:": "Passwuert", + "settings": "Astellungen", + "status": "Status", + "trace": "Spueren", + "updateSettings": "Astellungen aktualiséieren", + "userSettings": "Äert ëffentlecht Profil" + }, + "login": { + "login": "Aloggen", + "newPassword": "Neit Passwuert:", + "recover": "Passwuert vergiess?" + }, + "logout": { + "error": { + "cancel": "Ofbriechen", + "cont": "Trotzdeem virufueren", + "title": "Beim Versuch Iech auszeloggen ass e Feeler geschitt" + }, + "logout": "Ausloggen" + }, + "manageInstance": { + "copy": "Kopéieren", + "genericType": "Geneeresch", + "length": "Längt:" + }, + "media": { + "artist": "Kënschtler: $1", + "composer": "Komponist: $1", + "length": "Längt: $1 Minutten a(n) $2 Sekonnen", + "loading": "Lueden", + "moreInfo": "Méi Informatiounen" + }, + "member": { + "nick:": "Spëtznumm:", + "reason:": "Grond:" + }, + "message": { + "messages": "$1 {{PLURAL:$1|Message|Messagen}} >", + "delete": "Message läschen", + "deleted": "Geläschte Message", + "edit": "Message änneren", + "edited": "(geännert)", + "fullMessage": "Vollstännege Message:", + "reactionAdd": "Reaktioun derbäisetzen", + "reactions": "Reaktioune weisen", + "reactionsTitle": "Reaktiounen" + }, + "report": { + "cancel": "Ofbriechen" + }, + "no": "Nee", + "permissions": { + "readableNames": { + "ADMINISTRATOR": "Administrateur", + "ATTACH_FILES": "Fichieren uspéngelen", + "CHANGE_NICKNAME": "Spëtznumm änneren", + "CONNECT": "Verbannen", + "CREATE_EVENTS": "Evenementer uleeën", + "MANAGE_GUILD_EXPRESSIONS": "Ausdréck geréieren", + "MANAGE_NICKNAMES": "Spëtznimm geréieren", + "MANAGE_ROLES": "Rolle geréieren", + "MUTE_MEMBERS": "Membere stommschalten", + "SEND_POLLS": "Ëmfroen uleeën", + "SPEAK": "Schwätzen", + "STREAM": "Video", + "USE_EXTERNAL_APPS": "Extern Appe benotzen", + "USE_EXTERNAL_EMOJIS": "Extern Emojie benotzen" + } + }, + "profile": { + "bio": "Iwwer mech:", + "userInfo": "Benotzerinformatiounen" + }, + "profileColor": "Profilfaarf", + "pronouns": "Pronomen:", + "readableName": "Lëtzebuergesch", + "recentEmoji": "Rezent Emojien", + "register": { + "DOBError": "Gebuertsdatum: $1", + "emailError": "E-Mail-Adress: $1", + "passwordError:": "Passwuert: $1", + "usernameError": "Benotzernumm: $1" + }, + "remove": "Ewechhuelen", + "reply": "Äntweren", + "role": { + "color": "Éischt Faarf", + "colors": { + "name": "Faarfscheema:", + "one": "Monochrom", + "two": "Zweefaarweg", + "three": "Dräifaarweg", + "secondColor": "Zweet Faarf:", + "thirdColor": "Drëtt Faarf:" + }, + "delete": "Roll läschen", + "deleting": "Roll gëtt geläscht …", + "displaySettings": "Affichageastellungen", + "perms": "Berechtegungen", + "roles": "Rollen" + }, + "search": { + "back": "Zeréck", + "new": "Nei", + "next": "Nächst", + "old": "Al", + "page": "Säit $1", + "search": "Sichen" + }, + "settings": { + "img": "Bild eroplueden", + "save": "Ännerunge späicheren", + "updates": { + "serviceWorkerMode": { + "disabled": "Aus", + "enabled": "Un", + "offlineOnly": "Nëmmen offline" + } + } + }, + "sticker": { + "confirmDel": "Sidd Dir sécher, datt Dir dëse Sticker läsche wëllt?", + "del": "Sticker läschen", + "desc": "Beschreiwung", + "image": "Bild:", + "name": "Numm:", + "tags": "Associéierten Emoji:", + "title": "Stickeren", + "upload": "Stickeren eroplueden" + }, + "switchAccounts": "Konto wiesselen ⇌", + "todayAt": "Haut um $1", + "trace": { + "totalTime": "$2: $1 ms", + "traces": "$1 ($2: ms) $3" + }, + "typing": "$2 {{PLURAL:$1|ass|sinn}} am schreiwen", + "uploadFilesText": "Lued Är Fichieren hei erop!", + "useTemplate": "$1 als Schabloun benotzen", + "useTemplateButton": "Schabloun benotzen", + "user": { + "deleted": "Geläschte Benotzer", + "block": "Benotzer spären", + "editServerProfile": "Serverprofil änneren", + "invisible": "Onsiichtbar", + "offline": "Offline", + "online": "Online", + "remove": "Benotzer ewechhuelen", + "unblock": "Benotzer entspären" + }, + "vc": { + "joinstream": "Stream kucken" + }, + "webauth": { + "addKey": "Schlëssel derbäisetzen", + "manage": "Sécherheetsschlëssele geréieren" + }, + "webhooks": { + "areYouSureDelete": "Sidd Dir sécher, datt Dir $1 läsche wëllt?", + "channel": "Kanal fir Webhook-Output", + "name": "Webhook-Numm:", + "type": "Typ: $1" + }, + "yes": "Jo", + "yesterdayAt": "Gëschter um $1" +} diff --git a/translations/lt.json b/translations/lt.json index 2556df8b..411f6e15 100644 --- a/translations/lt.json +++ b/translations/lt.json @@ -1,350 +1,354 @@ { "@metadata": { "authors": [ + "McDutchie", "Nokeoo" ] }, - "readableName": "Anglų", - "reply": "Atsakyti", - "copyrawtext": "Kopijuoti neapdorotą tekstą", - "copymessageid": "Kopijuoti žinutės id", - "permissions": { - "descriptions": { - "CREATE_INSTANT_INVITE": "Leidžia naudotojui kurti gildijos kvietimus", - "KICK_MEMBERS": "Leidžia naudotojui išmesti narius iš gildijos", - "BAN_MEMBERS": "Leidžia naudotojui blokuoti narius gildijoje", - "ADMINISTRATOR": "Leidžia visus leidimus ir apeina kanalo leidimų perrašymus. Tai pavojingas leidimas!", - "MANAGE_CHANNELS": "Leidžia naudotojui valdyti ir redaguoti kanalus", - "MANAGE_GUILD": "Leidžia valdyti ir redaguoti gildiją", - "ADD_REACTIONS": "Leidžia naudotojui pridėti reakcijas į žinutes", - "VIEW_AUDIT_LOG": "Leidžia naudotojui peržiūrėti audito žurnalą", - "PRIORITY_SPEAKER": "Leidžia naudoti prioritetinį garsiakalbį balso kanale", - "STREAM": "Leidžia naudotojui transliuoti", - "VIEW_CHANNEL": "Leidžia naudotojui peržiūrėti kanalą", - "SEND_MESSAGES": "Leidžia naudotojui siųsti pranešimus", - "SEND_TTS_MESSAGES": "Leidžia naudotojui siųsti teksto į kalbą pranešimus", - "MANAGE_MESSAGES": "Leidžia naudotojui ištrinti ne savo žinutes", - "EMBED_LINKS": "Leisti šio naudotojo atsiųstas nuorodas automatiškai įterpti", - "ATTACH_FILES": "Leidžia naudotojui pridėti failus", - "READ_MESSAGE_HISTORY": "Leidžia naudotojui skaityti žinučių istoriją", - "MENTION_EVERYONE": "Leidžia naudotojui paminėti visus", - "USE_EXTERNAL_EMOJIS": "Leidžia naudotojui naudoti išorinius jaustukus", - "VIEW_GUILD_INSIGHTS": "Leidžia naudotojui matyti gildijos įžvalgas", - "CONNECT": "Leidžia naudotojui prisijungti prie balso kanalo", - "SPEAK": "Leidžia naudotojui kalbėti balso kanalu", - "MUTE_MEMBERS": "Leidžia naudotojui nutildyti kitus narius", - "DEAFEN_MEMBERS": "Leidžia naudotojui apkurtinti kitus narius", - "MOVE_MEMBERS": "Leidžia naudotojui perkelti narius iš vieno balso kanalo į kitą", - "USE_VAD": "Leidžia naudotojams kalbėti balso kanale tiesiog kalbant", - "CHANGE_NICKNAME": "Leidžia naudotojui pakeisti savo slapyvardį", - "MANAGE_NICKNAMES": "Leidžia naudotojui keisti kitų narių slapyvardžius", - "MANAGE_ROLES": "Leidžia naudotojui redaguoti ir valdyti vaidmenis", - "MANAGE_GUILD_EXPRESSIONS": "Leidžia tvarkyti jaustukus, lipdukus ir garso lentas", - "USE_APPLICATION_COMMANDS": "Leidžia naudotojui naudoti programų komandas", - "REQUEST_TO_SPEAK": "Leidžia naudotojui pateikti užklausą kalbėti sceniniame kanale", - "MANAGE_EVENTS": "Leidžia naudotojui redaguoti ir valdyti renginius", - "MANAGE_THREADS": "Leidžia naudotojui ištrinti ir archyvuoti gijas bei peržiūrėti visas privačias gijas", - "CREATE_PUBLIC_THREADS": "Leidžia naudotojui kurti viešąsias gijas", - "CREATE_PRIVATE_THREADS": "Leidžia naudotojui kurti privačias gijas", - "USE_EXTERNAL_STICKERS": "Leidžia naudotojui naudoti išorinius lipdukus", - "SEND_MESSAGES_IN_THREADS": "Leidžia naudotojui siųsti pranešimus gijose", - "USE_EMBEDDED_ACTIVITIES": "Leidžia naudotojui naudoti įterptą veiklą", - "MODERATE_MEMBERS": "Leidžia naudotojui pristabdyti kitus naudotojus, kad jie negalėtų siųsti žinučių pokalbiuose ir gijuose, taip pat kalbėti balso ir scenos kanaluose", - "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Leidžia peržiūrėti vaidmenų prenumeratos įžvalgas", - "USE_SOUNDBOARD": "Leidžia naudoti garso pultą balso kanale", - "CREATE_GUILD_EXPRESSIONS": "Leidžia kurti jaustukus, lipdukus ir garso pulto garsus bei redaguoti ir ištrinti esamo naudotojo sukurtus.", - "CREATE_EVENTS": "Leidžia kurti suplanuotus įvykius ir redaguoti bei ištrinti dabartinio naudotojo sukurtus įvykius.", - "USE_EXTERNAL_SOUNDS": "Leidžia naudoti pasirinktinius garso pulto garsus iš kitų serverių", - "SEND_VOICE_MESSAGES": "Leidžia siųsti balso pranešimus", - "SEND_POLLS": "Leidžia siųsti apklausas", - "USE_EXTERNAL_APPS": "Leidžia naudotojo įdiegtoms programoms siųsti viešus atsakymus. Kai išjungta, naudotojams vis tiek bus leista naudoti savo programas, tačiau atsakymai bus trumpalaikiai. Tai taikoma tik programoms, kurios taip pat neįdiegtos serveryje." - }, - "readableNames": { - "CREATE_INSTANT_INVITE": "Sukurti kvietimą", - "KICK_MEMBERS": "Išmesti narius", - "BAN_MEMBERS": "Blokuoti narius", - "ADMINISTRATOR": "Administratorius", - "MANAGE_CHANNELS": "Tvarkyti kanalus", - "MANAGE_GUILD": "Tvarkyti gildiją", - "ADD_REACTIONS": "Pridėkite reakcijas", - "VIEW_AUDIT_LOG": "Žiūrėti audito žurnalą", - "PRIORITY_SPEAKER": "Pirmenybinis garsiakalbis", - "STREAM": "Vaizdo įrašas", - "VIEW_CHANNEL": "Žiūrėti kanalus", - "SEND_MESSAGES": "Siųsti žinutes", - "SEND_TTS_MESSAGES": "Siųsti teksto į kalbą žinutes", - "MANAGE_MESSAGES": "Tvarkyti žinutes", - "EMBED_LINKS": "Įterpti nuorodas", - "ATTACH_FILES": "Prisegti failus", - "READ_MESSAGE_HISTORY": "Skaityti žinučių istoriją", - "USE_EXTERNAL_EMOJIS": "Naudoti išorinius jaustukus", - "VIEW_GUILD_INSIGHTS": "Žiūrėti gildijos įžvalgas", - "CONNECT": "Prisijungti", - "SPEAK": "Kalbėti", - "MUTE_MEMBERS": "Nutildyti narius", - "DEAFEN_MEMBERS": "Kurtinti narius", - "MOVE_MEMBERS": "Perkelti narius", - "USE_VAD": "Naudoti balso aptikimą", - "CHANGE_NICKNAME": "Keisti slapyvardį", - "MANAGE_NICKNAMES": "Tvarkyti slapyvardžius", - "MANAGE_ROLES": "Valdykite vaidmenis", - "MANAGE_GUILD_EXPRESSIONS": "Valdyti išraiškas", - "USE_APPLICATION_COMMANDS": "Naudoti programų komandas", - "REQUEST_TO_SPEAK": "Prašymas kalbėti", - "MANAGE_EVENTS": "Tvarkyti įvykius", - "MANAGE_THREADS": "Tvarkyti gijas", - "CREATE_PUBLIC_THREADS": "Kurti viešas gijas", - "CREATE_PRIVATE_THREADS": "Kurti privačias gijas", - "USE_EXTERNAL_STICKERS": "Naudoti išorinius lipdukus", - "SEND_MESSAGES_IN_THREADS": "Siųsti žinutes gijose", - "USE_EMBEDDED_ACTIVITIES": "Naudoti veiklas", - "MODERATE_MEMBERS": "Pristabdyti narius", - "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Žiūrėti kūrėjų pajamų gavimo analizę", - "CREATE_GUILD_EXPRESSIONS": "Kurti išraiškas", - "CREATE_EVENTS": "Kurti įvykius", - "USE_EXTERNAL_SOUNDS": "Naudoti išorinius garsus", - "SEND_VOICE_MESSAGES": "Siųsti balso žinutes", - "SEND_POLLS": "Kurti apklausas", - "USE_EXTERNAL_APPS": "Naudoti išorines programas" - } + "2faCode": "2FA kodas:", + "DMs": { + "close": "Uždaryti DM", + "copyId": "Kopijuoti DM id", + "markRead": "Žymėti skaitytu" }, - "hideBlockedMessages": "Užblokavote šį naudotoją. Spustelėkite, kad paslėptumėte šias žinutes.", - "showBlockedMessages": "Užblokavote šį naudotoją. Spustelėkite, kad pamatytumėte užblokuotą $1 {{PLURAL:$1|žinutę|žinutes}}.", - "deleteConfirm": "Ar tikrai norite tai ištrinti?", - "yes": "Taip", - "no": "Ne", - "todayAt": "Šiandien $1", - "yesterdayAt": "Vakar $1", - "otherAt": "$1 $2", - "botSettings": "Roboto nustatymai", - "uploadPfp": "Įkelti pfp:", - "uploadBanner": "Įkelti banerį:", - "pronouns": "Įvardžiai:", - "bio": "Biografija:", - "profileColor": "Profilio spalva", - "botGuilds": "Gildijų botas yra:", - "leaveGuild": "Palikti gildiją", - "confirmGuildLeave": "Ar tikrai norite palikti $1", "UrlGen": "URL generatorius", - "noMessages": "Atrodo, kad čia nėra pranešimų, būkite pirmas, kuris ką nors pasakykite!", + "accountNotStart": "Nepavyko paleisti paskyros", + "bio": "Biografija:", "blankMessage": "Tuščia žinutė", + "botGuilds": "Gildijų botas yra:", + "botSettings": "Roboto nustatymai", "channel": { + "announcement": "Skelbimai", "copyId": "Kopijuoti kanalo id", - "markRead": "Žymėti skaitytu", - "settings": "Nustatymai", + "createCatagory": "Sukurti kategoriją", + "createChannel": "Sukurti kanalą", "delete": "Ištrinti kanalą", "makeInvite": "Padaryti kvietimą", - "settingsFor": "$1 nustatymai", - "voice": "Balsas", - "text": "Tekstas", - "announcement": "Skelbimai", + "markRead": "Žymėti skaitytu", "name:": "Pavadinimas:", - "topic:": "Tema:", "nsfw:": "NSFW:", - "selectType": "Pasirinkite kanalo tipą", - "selectName": "Kanalo pavadinimas", "selectCatName": "Kanalo pavadinimas", - "createChannel": "Sukurti kanalą", - "createCatagory": "Sukurti kategoriją" - }, - "switchAccounts": "Perjungti paskyras ⇌", - "accountNotStart": "Nepavyko paleisti paskyros", - "home": { - "uptimeStats": "Veikimo laikas: \n Visas laikas: $1%\nŠią savaitę: $2%\nŠiandien: $3%", - "warnOffiline": "Egzempliorius neprisijungęs, negali prisijungti" - }, - "htmlPages": { - "idpermissions": "Tai leis robotui:", - "addBot": "Pridėti prie serverio", - "loaddesc": "Tai neturėtų trukti ilgai", - "switchaccounts": "Perjungti paskyras", - "instanceField": "Egzempliorius:", - "emailField": "El. paštas:", - "pwField": "Slaptažodis:", - "loginButton": "Prisijungti", - "noAccount": "Neturite paskyros?", - "userField": "Naudotojo vardas:", - "pw2Field": "Įveskite slaptažodį dar kartą:", - "dobField": "Gimimo data:", - "createAccount": "Sukurti paskyrą", - "alreadyHave": "Jau turite paskyrą?", - "openClient": "Atidaryti klientą", - "box3description": "Visada vertiname pagalbą, nesvarbu, ar tai būtų pranešimai apie klaidas, kodas, ar tiesiog rašybos klaidų nurodymas." - }, - "register": { - "passwordError:": "Slaptažodis: $1", - "usernameError": "Naudotojo vardas: $1", - "emailError": "El. paštas: $1", - "DOBError": "Gimimo data: $1" + "selectName": "Kanalo pavadinimas", + "selectType": "Pasirinkite kanalo tipą", + "settings": "Nustatymai", + "settingsFor": "$1 nustatymai", + "text": "Tekstas", + "topic:": "Tema:", + "voice": "Balsas" }, - "goingToURL": "Einate į $1. Tikrai norite ten eiti?", + "confirmGuildLeave": "Ar tikrai norite palikti $1", + "copymessageid": "Kopijuoti žinutės id", + "copyrawtext": "Kopijuoti neapdorotą tekstą", + "deleteConfirm": "Ar tikrai norite tai ištrinti?", + "errorReconnect": "Nepavyko prisijungti prie serverio, bandoma iš naujo po **$1** sekundžių...", "goThere": "Eiti ten", "goThereTrust": "Eiti ten ir ateityje pasitikėti", - "nevermind": "Nesvarbu", - "submit": "siųsti", + "goingToURL": "Einate į $1. Tikrai norite ten eiti?", "guild": { + "all": "visi", + "banner:": "Baneris:", + "confirmDelete": "Ar tikrai norite ištrinti $1?", + "confirmLeave": "Ar tikrai norite išeiti?", "copyId": "Kopijuoti gildijos ID", - "markRead": "Žymėti skaitytu", - "notifications": "Pranešimai", - "leave": "Palikti gildiją", - "settings": "Nustatymai", + "create": "Kurti gildiją", "delete": "Ištrinti gildiją", + "disoveryTitle": "Gildijos atradimas ($1) {{PLURAL:$1|įrašas|įrašai}}", + "icon:": "Ikona:", + "leave": "Palikti gildiją", + "loadingDiscovery": "Įkeliama…", "makeInvite": "Kurti kvietimą", - "settingsFor": "$1 nustatymai", + "markRead": "Žymėti skaitytu", "name:": "Pavadinimas:", - "topic:": "Tema:", - "icon:": "Ikona:", + "noDelete": "Nesvarbu", + "noLeave": "Nesvarbu", + "none": "taškas", + "notifications": "Pranešimai", + "onlyMentions": "tik paminėjimai", "overview": "Apžvalga", - "banner:": "Baneris:", "region:": "Regionas:", "roles": "Vaidmenys", "selectnoti": "Pasirinkite pranešimų tipą", - "all": "visi", - "onlyMentions": "tik paminėjimai", - "none": "taškas", - "confirmLeave": "Ar tikrai norite išeiti?", - "yesLeave": "Taip, esu tikras", - "noLeave": "Nesvarbu", - "confirmDelete": "Ar tikrai norite ištrinti $1?", "serverName": "Serverio pavadinimas:", + "settings": "Nustatymai", + "settingsFor": "$1 nustatymai", + "topic:": "Tema:", "yesDelete": "Taip, esu tikras", - "noDelete": "Nesvarbu", - "create": "Kurti gildiją", - "loadingDiscovery": "Įkeliama…", - "disoveryTitle": "Gildijos atradimas ($1) {{PLURAL:$1|įrašas|įrašai}}" + "yesLeave": "Taip, esu tikras" }, - "role": { - "displaySettings": "Rodinio nustatymai", - "name": "Vaidmens pavadinimas:", - "color": "Spalva", - "remove": "Pašalinti vaidmenį", - "delete": "Ištrinti vaidmenį", - "confirmDelete": "Ar tikrai norite ištrinti $1?" + "hideBlockedMessages": "Užblokavote šį naudotoją. Spustelėkite, kad paslėptumėte šias žinutes.", + "home": { + "uptimeStats": "Veikimo laikas: \n Visas laikas: $1%\nŠią savaitę: $2%\nŠiandien: $3%", + "warnOffiline": "Egzempliorius neprisijungęs, negali prisijungti" }, - "settings": { - "unsaved": "Atsargiai, turite neišsaugotų pakeitimų", - "save": "Išsaugoti pakeitimus" + "htmlPages": { + "addBot": "Pridėti prie serverio", + "alreadyHave": "Jau turite paskyrą?", + "box3description": "Visada vertiname pagalbą, nesvarbu, ar tai būtų pranešimai apie klaidas, kodas, ar tiesiog rašybos klaidų nurodymas.", + "createAccount": "Sukurti paskyrą", + "dobField": "Gimimo data:", + "emailField": "El. paštas:", + "idpermissions": "Tai leis robotui:", + "instanceField": "Egzempliorius:", + "loaddesc": "Tai neturėtų trukti ilgai", + "loginButton": "Prisijungti", + "noAccount": "Neturite paskyros?", + "openClient": "Atidaryti klientą", + "pw2Field": "Įveskite slaptažodį dar kartą:", + "pwField": "Slaptažodis:", + "switchaccounts": "Perjungti paskyras", + "userField": "Naudotojo vardas:" + }, + "instanceStats": { + "members": "Nariai: $1", + "messages": "Žinutės: $1", + "name": "Egzemplioriaus statistika: $1", + "servers": "Serveriai: $1", + "users": "Registruoti naudotojai: $1" + }, + "invite": { + "accept": "Priimti", + "alreadyJoined": "Jau prisijungta", + "inviteLinkCode": "Pakvietimo nuoroda/kodas", + "invitedBy": "Jus pakvietė $1", + "joinUsing": "Prisijunkite naudodami kvietimą", + "loginOrCreateAccount": "Prisijunkite arba susikurkite paskyrą ⇌", + "longInvitedBy": "$1 pakvietė jus prisijungti prie $2", + "noAccount": "Sukurkite paskyrą, kad priimtumėte kvietimą" }, + "inviteOptions": { + "12h": "12 valandų", + "1d": "1 diena", + "1h": "1 valanda", + "30d": "30 dienų", + "30m": "30 minučių", + "6h": "6 valandos", + "7d": "7 dienos", + "limit": "$1 {{PLURAL:$1|panaudojimas|panaudojimai}}", + "never": "Niekada", + "noLimit": "Nėra limito", + "title": "Pakvieskite žmones" + }, + "leaveGuild": "Palikti gildiją", "localuser": { - "settings": "Nustatymai", - "userSettings": "Naudotojo nustatymai", - "themesAndSounds": "Temos ir garsai", - "theme:": "Tema", - "notisound": "Pranešimo garsas:", - "accentColor": "Akcento spalva:", - "enableEVoice": "Įgalinti eksperimentinį balso palaikymą", - "VoiceWarning": "Ar tikrai norite tai įjungti, tai eksperimentinė versija ir gali sukelti problemų. (ši funkcija skirta kūrėjams, neįjunkite, jei nežinote, ką darote)", - "updateSettings": "Atnaujinti nustatymus", - "swSettings": "Paslaugos darbuotojo nustatymas", - "SWOff": "Išjungta", - "SWOffline": "Tik neprisijungus", - "SWOn": "Įjungta", - "clearCache": "Išvalyti talpyklą", + "2faCode:": "Kodas:", + "2faDisable": "Išjungti 2FA", + "2faEnable": "Įjungti 2FA", "CheckUpdate": "Tikrinti, ar yra naujinių", + "PasswordsNoMatch": "Slaptažodžiai nesutampa", + "TOSURL": "Paslaugų teikimo sąlygų URL:", + "VoiceWarning": "Ar tikrai norite tai įjungti, tai eksperimentinė versija ir gali sukelti problemų. (ši funkcija skirta kūrėjams, neįjunkite, jei nežinote, ką darote)", + "accentColor": "Akcento spalva:", "accountSettings": "Paskyros nustatymai", - "2faDisable": "Išjungti 2FA", + "addBot": "Pridėti robotą", + "advancedBot": "Išplėstiniai roboto nustatymai", + "appName": "Programos pavadinimas:", "badCode": "Netinkamas kodas", - "2faEnable": "Įjungti 2FA", - "2faCode:": "Kodas:", - "setUp2fa": "2FA sąranka", "badPassword": "Neteisingas slaptažodis", + "botAvatar": "Roboto avataras:", + "botInviteCreate": "Roboto kvietimo kūrėjas", + "botUsername": "Roboto naudotojo vardas:", "changeEmail": "Keisti el. pašto adresą", - "password:": "Slaptažodis", - "newEmail:": "Naujas el. paštas", - "changeUsername": "Keisti naudotojo vardą", - "newUsername": "Naujas naudotojo vardas:", "changePassword": "Keisti slaptažodį", - "oldPassword:": "Senas slaptažodis:", - "newPassword:": "Naujas slaptažodis:", - "PasswordsNoMatch": "Slaptažodžiai nesutampa", - "disableConnection": "Šis ryšys išjungtas serverio pusėje", - "devPortal": "Kūrėjo portalas", + "changeUsername": "Keisti naudotojo vardą", + "clearCache": "Išvalyti talpyklą", + "confirmAddBot": "Ar tikrai norite pridėti robotą prie šios programos? Nėra kelio atgal.", + "confuseNoBot": "Dėl kažkokių priežasčių ši programa neturi robotų (kol kas).", + "connections": "Jungtys", "createApp": "Sukurti programą", - "team:": "Komanda:", - "appName": "Programos pavadinimas:", "description": "Aprašymas:", + "devPortal": "Kūrėjo portalas", + "disableConnection": "Šis ryšys išjungtas serverio pusėje", + "editingBot": "Redaguojamas robotas $1", + "language": "Kalba:", + "manageBot": "Valdyti robotą", + "newEmail:": "Naujas el. paštas", + "newPassword:": "Naujas slaptažodis:", + "newUsername": "Naujas naudotojo vardas:", + "notisound": "Pranešimo garsas:", + "oldPassword:": "Senas slaptažodis:", + "password:": "Slaptažodis", "privacyPolcyURL": "Privatumo politikos URL:", - "TOSURL": "Paslaugų teikimo sąlygų URL:", "publicAvaliable": "Padaryti robotą viešai kviečiamu?", "requireCode": "Reikalauti suteikti kodą norint pakviesti robotą?", - "manageBot": "Valdyti robotą", - "addBot": "Pridėti robotą", - "confirmAddBot": "Ar tikrai norite pridėti robotą prie šios programos? Nėra kelio atgal.", - "confuseNoBot": "Dėl kažkokių priežasčių ši programa neturi robotų (kol kas).", - "editingBot": "Redaguojamas robotas $1", - "botUsername": "Roboto naudotojo vardas:", - "botAvatar": "Roboto avataras:", - "advancedBot": "Išplėstiniai roboto nustatymai", - "botInviteCreate": "Roboto kvietimo kūrėjas", - "language": "Kalba:", - "connections": "Jungtys" + "setUp2fa": "2FA sąranka", + "settings": "Nustatymai", + "team:": "Komanda:", + "theme:": "Tema", + "themesAndSounds": "Temos ir garsai", + "updateSettings": "Atnaujinti nustatymus", + "userSettings": "Naudotojo nustatymai" + }, + "login": { + "allGood": "Viskas gerai", + "checking": "Tikrinamas egzempliorius", + "invalid": "Netinkamas egzempliorius, bandykite dar kartą", + "waiting": "Laukiama egzemplioriaus patikrinimo" + }, + "member": { + "ban": "Blokuoti $1 iš $2", + "kick": "Išmesti $1 iš $2", + "reason:": "Priežastis:" }, "message": { - "reactionAdd": "Pridėti reakciją", "delete": "Ištrinti žinutę", - "edit": "Redaguoti žinutę" - }, - "instanceStats": { - "name": "Egzemplioriaus statistika: $1", - "users": "Registruoti naudotojai: $1", - "servers": "Serveriai: $1", - "messages": "Žinutės: $1", - "members": "Nariai: $1" + "edit": "Redaguoti žinutę", + "reactionAdd": "Pridėti reakciją" }, - "inviteOptions": { - "title": "Pakvieskite žmones", - "30m": "30 minučių", - "1h": "1 valanda", - "6h": "6 valandos", - "12h": "12 valandų", - "1d": "1 diena", - "7d": "7 dienos", - "30d": "30 dienų", - "never": "Niekada", - "limit": "$1 {{PLURAL:$1|panaudojimas|panaudojimai}}", - "noLimit": "Nėra limito" + "nevermind": "Nesvarbu", + "no": "Ne", + "noMessages": "Atrodo, kad čia nėra pranešimų, būkite pirmas, kuris ką nors pasakykite!", + "otherAt": "$1 $2", + "permissions": { + "descriptions": { + "ADMINISTRATOR": "Leidžia visus leidimus ir apeina kanalo leidimų perrašymus. Tai pavojingas leidimas!", + "MANAGE_GUILD": "Leidžia valdyti ir redaguoti gildiją", + "VIEW_AUDIT_LOG": "Leidžia naudotojui peržiūrėti audito žurnalą", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Leidžia peržiūrėti vaidmenų prenumeratos įžvalgas", + "VIEW_GUILD_INSIGHTS": "Leidžia naudotojui matyti gildijos įžvalgas", + "MENTION_EVERYONE": "Leidžia naudotojui paminėti visus", + "MANAGE_NICKNAMES": "Leidžia naudotojui keisti kitų narių slapyvardžius", + "SEND_POLLS": "Leidžia siųsti apklausas", + "MANAGE_MESSAGES": "Leidžia naudotojui ištrinti ne savo žinutes", + "MANAGE_THREADS": "Leidžia naudotojui ištrinti ir archyvuoti gijas bei peržiūrėti visas privačias gijas", + "MANAGE_CHANNELS": "Leidžia naudotojui valdyti ir redaguoti kanalus", + "MANAGE_ROLES": "Leidžia naudotojui redaguoti ir valdyti vaidmenis", + "CREATE_EVENTS": "Leidžia kurti suplanuotus įvykius ir redaguoti bei ištrinti dabartinio naudotojo sukurtus įvykius.", + "MANAGE_EVENTS": "Leidžia naudotojui redaguoti ir valdyti renginius", + "CREATE_GUILD_EXPRESSIONS": "Leidžia kurti jaustukus, lipdukus ir garso pulto garsus bei redaguoti ir ištrinti esamo naudotojo sukurtus.", + "MANAGE_GUILD_EXPRESSIONS": "Leidžia tvarkyti jaustukus, lipdukus ir garso lentas", + "MODERATE_MEMBERS": "Leidžia naudotojui pristabdyti kitus naudotojus, kad jie negalėtų siųsti žinučių pokalbiuose ir gijuose, taip pat kalbėti balso ir scenos kanaluose", + "KICK_MEMBERS": "Leidžia naudotojui išmesti narius iš gildijos", + "BAN_MEMBERS": "Leidžia naudotojui blokuoti narius gildijoje", + "MOVE_MEMBERS": "Leidžia naudotojui perkelti narius iš vieno balso kanalo į kitą", + "MUTE_MEMBERS": "Leidžia naudotojui nutildyti kitus narius", + "DEAFEN_MEMBERS": "Leidžia naudotojui apkurtinti kitus narius", + "CHANGE_NICKNAME": "Leidžia naudotojui pakeisti savo slapyvardį", + "VIEW_CHANNEL": "Leidžia naudotojui peržiūrėti kanalą", + "READ_MESSAGE_HISTORY": "Leidžia naudotojui skaityti žinučių istoriją", + "ADD_REACTIONS": "Leidžia naudotojui pridėti reakcijas į žinutes", + "SEND_MESSAGES": "Leidžia naudotojui siųsti pranešimus", + "SEND_MESSAGES_IN_THREADS": "Leidžia naudotojui siųsti pranešimus gijose", + "SEND_TTS_MESSAGES": "Leidžia naudotojui siųsti teksto į kalbą pranešimus", + "USE_EXTERNAL_EMOJIS": "Leidžia naudotojui naudoti išorinius jaustukus", + "USE_EXTERNAL_STICKERS": "Leidžia naudotojui naudoti išorinius lipdukus", + "EMBED_LINKS": "Leisti šio naudotojo atsiųstas nuorodas automatiškai įterpti", + "ATTACH_FILES": "Leidžia naudotojui pridėti failus", + "SEND_VOICE_MESSAGES": "Leidžia siųsti balso pranešimus", + "CREATE_INSTANT_INVITE": "Leidžia naudotojui kurti gildijos kvietimus", + "CREATE_PUBLIC_THREADS": "Leidžia naudotojui kurti viešąsias gijas", + "CREATE_PRIVATE_THREADS": "Leidžia naudotojui kurti privačias gijas", + "CONNECT": "Leidžia naudotojui prisijungti prie balso kanalo", + "SPEAK": "Leidžia naudotojui kalbėti balso kanalu", + "USE_VAD": "Leidžia naudotojams kalbėti balso kanale tiesiog kalbant", + "STREAM": "Leidžia naudotojui transliuoti", + "USE_SOUNDBOARD": "Leidžia naudoti garso pultą balso kanale", + "USE_EXTERNAL_SOUNDS": "Leidžia naudoti pasirinktinius garso pulto garsus iš kitų serverių", + "PRIORITY_SPEAKER": "Leidžia naudoti prioritetinį garsiakalbį balso kanale", + "REQUEST_TO_SPEAK": "Leidžia naudotojui pateikti užklausą kalbėti sceniniame kanale", + "USE_EMBEDDED_ACTIVITIES": "Leidžia naudotojui naudoti įterptą veiklą", + "USE_APPLICATION_COMMANDS": "Leidžia naudotojui naudoti programų komandas", + "USE_EXTERNAL_APPS": "Leidžia naudotojo įdiegtoms programoms siųsti viešus atsakymus. Kai išjungta, naudotojams vis tiek bus leista naudoti savo programas, tačiau atsakymai bus trumpalaikiai. Tai taikoma tik programoms, kurios taip pat neįdiegtos serveryje." + }, + "readableNames": { + "ADD_REACTIONS": "Pridėkite reakcijas", + "ADMINISTRATOR": "Administratorius", + "ATTACH_FILES": "Prisegti failus", + "BAN_MEMBERS": "Blokuoti narius", + "CHANGE_NICKNAME": "Keisti slapyvardį", + "CONNECT": "Prisijungti", + "CREATE_EVENTS": "Kurti įvykius", + "CREATE_GUILD_EXPRESSIONS": "Kurti išraiškas", + "CREATE_INSTANT_INVITE": "Sukurti kvietimą", + "CREATE_PRIVATE_THREADS": "Kurti privačias gijas", + "CREATE_PUBLIC_THREADS": "Kurti viešas gijas", + "DEAFEN_MEMBERS": "Kurtinti narius", + "EMBED_LINKS": "Įterpti nuorodas", + "KICK_MEMBERS": "Išmesti narius", + "MANAGE_CHANNELS": "Tvarkyti kanalus", + "MANAGE_EVENTS": "Tvarkyti įvykius", + "MANAGE_GUILD": "Tvarkyti gildiją", + "MANAGE_GUILD_EXPRESSIONS": "Valdyti išraiškas", + "MANAGE_MESSAGES": "Tvarkyti žinutes", + "MANAGE_NICKNAMES": "Tvarkyti slapyvardžius", + "MANAGE_ROLES": "Valdykite vaidmenis", + "MANAGE_THREADS": "Tvarkyti gijas", + "MODERATE_MEMBERS": "Pristabdyti narius", + "MOVE_MEMBERS": "Perkelti narius", + "MUTE_MEMBERS": "Nutildyti narius", + "PRIORITY_SPEAKER": "Pirmenybinis garsiakalbis", + "READ_MESSAGE_HISTORY": "Skaityti žinučių istoriją", + "REQUEST_TO_SPEAK": "Prašymas kalbėti", + "SEND_MESSAGES": "Siųsti žinutes", + "SEND_MESSAGES_IN_THREADS": "Siųsti žinutes gijose", + "SEND_POLLS": "Kurti apklausas", + "SEND_TTS_MESSAGES": "Siųsti teksto į kalbą žinutes", + "SEND_VOICE_MESSAGES": "Siųsti balso žinutes", + "SPEAK": "Kalbėti", + "STREAM": "Vaizdo įrašas", + "USE_APPLICATION_COMMANDS": "Naudoti programų komandas", + "USE_EMBEDDED_ACTIVITIES": "Naudoti veiklas", + "USE_EXTERNAL_APPS": "Naudoti išorines programas", + "USE_EXTERNAL_EMOJIS": "Naudoti išorinius jaustukus", + "USE_EXTERNAL_SOUNDS": "Naudoti išorinius garsus", + "USE_EXTERNAL_STICKERS": "Naudoti išorinius lipdukus", + "USE_VAD": "Naudoti balso aptikimą", + "VIEW_AUDIT_LOG": "Žiūrėti audito žurnalą", + "VIEW_CHANNEL": "Žiūrėti kanalus", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Žiūrėti kūrėjų pajamų gavimo analizę", + "VIEW_GUILD_INSIGHTS": "Žiūrėti gildijos įžvalgas" + } }, - "2faCode": "2FA kodas:", - "invite": { - "invitedBy": "Jus pakvietė $1", - "alreadyJoined": "Jau prisijungta", - "accept": "Priimti", - "noAccount": "Sukurkite paskyrą, kad priimtumėte kvietimą", - "longInvitedBy": "$1 pakvietė jus prisijungti prie $2", - "loginOrCreateAccount": "Prisijunkite arba susikurkite paskyrą ⇌", - "joinUsing": "Prisijunkite naudodami kvietimą", - "inviteLinkCode": "Pakvietimo nuoroda/kodas" + "profileColor": "Profilio spalva", + "pronouns": "Įvardžiai:", + "readableName": "Lietuvių", + "register": { + "DOBError": "Gimimo data: $1", + "emailError": "El. paštas: $1", + "passwordError:": "Slaptažodis: $1", + "usernameError": "Naudotojo vardas: $1" }, + "reply": "Atsakyti", "replyingTo": "Atsakoma $1", - "DMs": { - "copyId": "Kopijuoti DM id", - "markRead": "Žymėti skaitytu", - "close": "Uždaryti DM" + "retrying": "Bandoma dar kartą...", + "role": { + "color": "Spalva", + "confirmDelete": "Ar tikrai norite ištrinti $1?", + "delete": "Ištrinti vaidmenį", + "displaySettings": "Rodinio nustatymai", + "name": "Vaidmens pavadinimas:", + "remove": "Pašalinti vaidmenį" }, + "settings": { + "save": "Išsaugoti pakeitimus", + "unsaved": "Atsargiai, turite neišsaugotų pakeitimų", + "updates": { + "serviceWorkerMode": { + "disabled": "Išjungta", + "enabled": "Įjungta", + "offlineOnly": "Tik neprisijungus", + "title": "Paslaugos darbuotojo nustatymas" + } + } + }, + "showBlockedMessages": "Užblokavote šį naudotoją. Spustelėkite, kad pamatytumėte užblokuotą $1 {{PLURAL:$1|žinutę|žinutes}}.", + "submit": "siųsti", + "switchAccounts": "Perjungti paskyras ⇌", + "todayAt": "Šiandien $1", + "uploadBanner": "Įkelti banerį:", + "uploadPfp": "Įkelti pfp:", "user": { - "copyId": "Kopijuoti naudotojo ID", - "online": "Prisijungę", - "offline": "Neprisijungę", - "message": "Parašyti naudotojui", + "addRole": "Pridėti vaidmenis", + "ban": "Blokuoti narį", "block": "Blokuoti naudotoją", - "unblock": "atblokuoti naudotoją", + "copyId": "Kopijuoti naudotojo ID", "friendReq": "Draugo prašymas", "kick": "Išmesti narį", - "ban": "Blokuoti narį", - "addRole": "Pridėti vaidmenis", - "removeRole": "Pašalinti vaidmenis" - }, - "login": { - "checking": "Tikrinamas egzempliorius", - "allGood": "Viskas gerai", - "invalid": "Netinkamas egzempliorius, bandykite dar kartą", - "waiting": "Laukiama egzemplioriaus patikrinimo" - }, - "member": { - "kick": "Išmesti $1 iš $2", - "reason:": "Priežastis:", - "ban": "Blokuoti $1 iš $2" + "message": "Parašyti naudotojui", + "offline": "Neprisijungę", + "online": "Prisijungę", + "removeRole": "Pašalinti vaidmenis", + "unblock": "atblokuoti naudotoją" }, - "errorReconnect": "Nepavyko prisijungti prie serverio, bandoma iš naujo po **$1** sekundžių...", - "retrying": "Bandoma dar kartą..." + "yes": "Taip", + "yesterdayAt": "Vakar $1" } diff --git a/translations/mk.json b/translations/mk.json new file mode 100644 index 00000000..f2f72c7a --- /dev/null +++ b/translations/mk.json @@ -0,0 +1,938 @@ +{ + "@metadata": { + "authors": [ + "Bjankuloski06" + ] + }, + "2faCode": "2ЧЗ-код:", + "404": { + "404": "грешка 404 — Страницата не е пронајдена", + "app": "На прилогот", + "blog": "Блогот на Ферми", + "home": "Почетна страница", + "listtitle": "Можеби сакавте да појдете на едно од овие места", + "login": "Најава", + "report": "Ако ја најдовте страницава во рамките на клиентот, пријавете ја:", + "reset": "Страница за менување лозинка", + "title": "Се чини дека сте изгубени", + "whatelse": "Што друго сметате дека треба да се случи?", + "whereever": "Каде и да е ова" + }, + "DMs": { + "add": "Додајте некого во оваа лична порака", + "close": "Затвори ЛП", + "copyId": "Копирај назнака на ЛП", + "markRead": "Означи како прочитано", + "name": "Лични пораки" + }, + "ok": "ОК", + "dismiss": "Отфрли", + "UrlGen": "Создавач на ботовски покани", + "Voice": { + "status": { + "badWS": "Лош одговор од WS", + "conectionFailed": "Поврзувањето не успеа", + "done": "Поврзано", + "left": "Напушти гласовен разговор", + "makingOffer": "Дава понуда", + "noSDP": "Нема SDP", + "notconnected": "Неповрзано", + "sendingStreams": "Испраќа аудиопренос", + "startingRTC": "Покрева RTC-поврзување", + "waitingMainWS": "Чека на главниот WS", + "waitingURL": "Чека на URL за глас", + "wsAuth": "чека овластување од WS", + "wsOpen": "Чека да се отвори WS" + } + }, + "accessibility": { + "gifSettings": { + "always": "Секогаш", + "hover": "При лебдење", + "never": "Никогаш" + }, + "name": "Пристапност", + "playGif": "Пушти GIF-ови:", + "playIcon": "Пушти анимирани икони:", + "roleColors": "Оневозможи бои на улоги", + "gradientColors": "Оневозможи градиентни бои" + }, + "accountNotStart": "Сметката не може да се покрене", + "add": "Додај", + "applications": { + "delete": "Избриши прилог", + "sure": "Дали сигурно сакате да го избришете $1?" + }, + "badge": { + "active_developer": "Активен разработувач", + "application_guild_subscription": "има Премиум", + "automod": "користи AutoMod", + "bot_commands": "поддржува наредби", + "bug_hunter_level_1": "Ловец на грешки I степен", + "bug_hunter_level_2": "Ловец на грешки II степен", + "certified_moderator": "Модератор", + "early_supporter": "Ран поддржувач", + "guild_booster_lvl1": "Истакнат еснаф", + "guild_booster_lvl2": "Истакнат еснаф", + "guild_booster_lvl3": "Истакнат еснаф", + "guild_booster_lvl4": "Истакнат еснаф", + "guild_booster_lvl5": "Истакнат еснаф", + "guild_booster_lvl6": "Истакнат еснаф", + "guild_booster_lvl7": "Истакнат еснаф", + "guild_booster_lvl8": "Истакнат еснаф", + "guild_booster_lvl9": "Истакнат еснаф прилично долго", + "hypesquad": "Vibesquad [PLACEHOLDER]", + "hypesquad_house_1": "Смелост", + "hypesquad_house_2": "Извонредност", + "hypesquad_house_3": "Рамнотежа", + "legacy_username": "има старо корисничко име", + "partner": "Партнер на примерокот", + "premium": "Премиум", + "quest_completed": "исполнил задача", + "staff": "Персонал на примерокот", + "verified_developer": "Потврден разработувач" + }, + "bio": "Био:", + "blankMessage": "Празна порака", + "blog": { + "blog": "Блог", + "blogUpdates": "Блоговски новости:", + "desc": "Прочитајте или претплатете се за новости за Ферми!", + "fermi": "Блог на Ферми", + "gotoPost": "Оди на објавата", + "wantUpdates": "Дали би сакале да добивате неделни блоговски новости за состојбата на Ферми?" + }, + "bot": "БОТ", + "botGuilds": "Ботот на еснафите е во:", + "botSettings": "Нагодувања за бот", + "cancel": "Откажи", + "channel": { + "SlowmodeCool": "Мирување на бавниот режим: $1", + "TimeOutCool": "Времено исклучен до: $1", + "allowIcons": "Овозможи прилагодливи икони за канали", + "announcement": "Соопштенија", + "copyId": "Ископирај назнака на каналот", + "copyIdCat": "Прекопирај категориска назнака", + "createCatagory": "Создај категорија", + "createChannel": "Создај канал", + "creating": "Создавање на канал", + "delete": "Избриши канал", + "deleteCat": "Избриши категорија", + "icon": "Икона:", + "makeInvite": "Направи покана", + "markRead": "Означи како прочитано", + "mute": "Занеми канал", + "name": "Канал", + "name:": "Име:", + "nsfw:": "Старосно ограничување:", + "permissions": "Дозволи", + "selectCatName": "Име на категоријата", + "selectName": "Име на каналот", + "selectType": "Изберете вид на канал", + "settings": "Нагодувања", + "settingsFor": "Нагодувања за $1", + "slowmode": "Бавен режим:", + "text": "Текст", + "timedOutUntil": "Времено исклучен до: $1", + "topic:": "Тема:", + "typebox": "Порака во $1", + "unmute": "Вклучи звук во каналот", + "voice": "Глас", + "deleteThread": "Избриши нишка", + "hideThreads": "Скриј по неактивност", + "forum": "Форум" + }, + "threads": { + "leave": "Напушти нишка", + "join": "Стапи во нишка", + "name": "Име:", + "editTags": "Уреди ознаки" + }, + "forum": { + "creorsear": "Создајте или пребарајте објава", + "next": "Следно", + "back": "Назад", + "newPost": "Направете нова објава", + "post": "Објава", + "sortOptions": { + "sortby": { + "title": "Подреди по", + "recent": "Неодамна активно", + "posted": "Датум на објавување" + }, + "sortOrder": { + "title": "Редослед на подредување", + "recent": "Прво скорешно", + "old": "Прво старо" + }, + "tagMatch": { + "title": "Совпаѓање на ознаки", + "some": "Совпадни некои ознаки", + "all": "Совпадни ги сите ознаки" + }, + "name": "Подредувања" + }, + "errors": { + "tagsReq": "Се бараат ознаки", + "requireText": "Пораката не може да стои празна." + }, + "allTags": "Сите ознаки", + "settings": { + "editTags": "Уреди ознаки:", + "editTag": "Уреди ознака", + "tagName": "Име на ознаката:", + "moderated": "Допуштај ознака да ставаат само модератори на нишката:", + "addTag": "Додај ознака", + "delTag": "Избриши ознака" + } + }, + "channelLink": "бр. $1", + "clientDesc": "Верзија на клиентот: $1\n\n[Придружете се на официјалниот еснаф на Ферми]($2/invite/USgYJo?instance=https%3A%2F%2Fspacebar.chat)\n\n[Да го преведеме Ферми](https://translatewiki.net/wiki/Translating:JankClient#sortable:3=desc) \n\n[Помагајте во создавањето на Ферми](https://github.com/MathMan05/Fermi)\n\n[Помагајте во одржувањето на опслужувачот на кој работи Ферми](https://github.com/spacebarchat/server)\n\n[Читајте го блогот](https://blog.fermi.chat/)\n\nПресметани права: $3", + "commands": { + "errorNotValid": "$1 не е важечки избор за $2", + "required": "$1 е задолжителен дел од оваа наредба" + }, + "confirmGuildLeave": "Дали сигурно сакате да го напуштите $1", + "copyLink": "Копирај врска за пораката", + "copymessageid": "Копирај назнака на пораката", + "copyrawtext": "Копирај сиров текст", + "createAccount": "Создај сметка", + "delete": "Избриши", + "deleteConfirm": "Дали сигурно сакате да го избришете ова?", + "devSettings": { + "badUser": "Овозможи заведување на неисправни кориснички објекти кои испраќаат премногу информации:", + "cache": "Овозможи го меѓускладирање на картографски податотеки од услужен работник:", + "cacheDesc": "картографските податотеки ќе се вчитаат како и да е, а ова само ќе обезбеди дека ќе бидат во меѓускладот кога ќе дојде новата надградба.", + "captureTrace": "Оваа поставка му кажува на Ферми да ги зафати својствата _trace од опслужувачот. Ова може да направи прогресивното JSON-декодирање да престане да работи (може да бара превчитување)", + "clearWellKnowns": "Повторно дај ги записите .well-known:", + "description": "Ова е наменето за разработувачи на Spacebar/Fermi. Ако не знаете што правите, не ги чепкајте овие поставки.", + "gatewayComp": "Оневозможи збивање на портата:", + "reportSystem": "Овозможи експериментален систем на пријавување:", + "logGateway": "Заведувај примени настани на портата (инфо за дневнички степен):", + "name": "Поставки за разработувачи", + "traces": "Изложи траги:" + }, + "deviceManage": { + "city": "Град: $1", + "continent": "Континент: $1", + "country": "Земја: $1", + "curSes": "Ова е тековната седница. Ќе треба да се одјавите преку другото мени", + "estimateWarn": "Предупредување: Сите овие информации се само најдобра претпоставка. Може да бидат неточни.", + "ip": "Последна позната IP-адреса: $1", + "last": "Последно користено (приближно): $1", + "latitude": "Гео. ширина: $1", + "logout": "Одјава", + "longitude": "Гео. должина: $1", + "manageDev": "Управување со уредот", + "postal": "Поштенски: $1", + "region": "Регион: $1", + "title": "Управување со седници" + }, + "discovery": "Откривање", + "dms": "Лични пораки", + "edit": "Уреди", + "emoji": { + "confirmDel": "Дали сигурно сакате да го избришете емотиконов?", + "found": { + "not": "Овој емотикон не е од вашите еснафи. Ќе мора да се придружите на еснаф за да го користите", + "other": "Овој емотикон е од еден од другите ваши еснафи.", + "private": "Не можев да го најдам еснафот за овој емотикон", + "this": "Овој емотикон е од овој еснаф." + }, + "from": "Овој емотикон е од", + "image:": "Слика:", + "join": "Придружи се на еснафот", + "name:": "Име:", + "privateGuild": "Овој еснаф е приватен", + "title": "Емотикони", + "upload": "Подигни емотикони" + }, + "emojiSelect": "Емотикон:", + "errorReconnect": "Не можам да се поврзам со опслужувачот. Пробувам повторно за **$1** секунди...", + "favoriteGifs": "Омилени GIF-ови", + "folder": { + "color": "Боја на папката:", + "create": "Создај нова папка", + "edit": "Уреди папка", + "name": "Име на папката:" + }, + "form": { + "captcha": "Чекајте, дали сте човек?" + }, + "friends": { + "addfriend": "Додај пријател", + "addfriendpromt": "Додај пријатели по корисничко име:", + "all": "Сите", + "all:": "Сите пријатели:", + "blocked": "Блокиран", + "blockedusers": "Блокирани корисници:", + "bu": "Блокиран корисник", + "discnotfound": "Дискриминаторот не е пронајден", + "friendlist": "Список на пријатели", + "friends": "Пријатели", + "notfound": "Корисникот не е пронајден", + "online": "На линија", + "online:": "Пријатели на линија:", + "pending": "Во исчекување", + "pending:": "Барања за пријателство во исчекување:", + "removeFriend": "Отстрани пријател" + }, + "goThere": "Да, ајде", + "goThereTrust": "Да, верувај му на овој домен во иднина", + "goingToURL": "Оваа хиперврска ве испрати на $1. Сигурно сакате да одите таму?", + "group": { + "createdm": "Создај ЛП!", + "edit": "Уреди групен разговор", + "select": "Избери пријатели" + }, + "guild": { + "COMMUNITY": "Поднеси барање за придружување", + "disableInvites": "Оневозможи покани:", + "DISCOVERABLE": "Откривање", + "INVITES_DISABLED": "Само со покана", + "adminMenu": { + "changePerms": "Смениете ги дозволите за да наоѓате", + "finding": "Наоѓање на администратори", + "ownName": "Сопственик", + "owner": "Најди го сопственикот", + "permission": "Дозволи:" + }, + "report": "Приајви еснаф", + "admins": "Најди администратори", + "all": "Сите", + "banReason": "Причина за забраната: $1", + "bannedBy": "Забранил:", + "banner:": "Транспарент:", + "bans": "Забрани", + "boostMessage?": "Испратете порака кога некој ќе го истакне вашиот еснаф!", + "community": "Заедница", + "confirmDelete": "Дали сигурно сакате да го избришете $1?", + "confirmLeave": "Дали сигурно сакате да напуштите?", + "copyId": "Копирај назнака на еснаф", + "create": "Создај еснаф", + "createFromTemplate": "Еснаф од предлошка", + "createNewTemplate": "Создај нова предлошка", + "creating": "Создавање на еснаф", + "default": "По основно ($1)", + "defaultNoti": "Задајте ги стандардните нагодувања за известувањата во вашиот еснаф!", + "delete": "Избриши еснаф", + "description:": "Опис:", + "disoveryTitle": "Откривање на еснафи ($1 {{PLURAL:$1|ставка|ставки}})", + "editingTemplate": "Уредување на $1", + "emptytext": "Необично. Овој еснаф нема канали!?", + "emptytitle": "Чудно место", + "guilds": "Еснафи", + "helpTips?": "Испраќајте корисни совети за вашиот еснаф!", + "howJoin": "Како се станува член на вашиот еснаф?", + "icon:": "Икона:", + "invites": "Покани", + "joinConfirm": "Се придружувате на $1?", + "leave": "Напушти еснаф", + "loadingDiscovery": "Вчитувам...", + "makeInvite": "Направи покана", + "markRead": "Означи како прочитано", + "memberCount": "$1 {{PLURAL:$1|член|членови}}", + "mute": "Занеми еснаф", + "name:": "Име:", + "nameNoMatch": "Имињата не се совпаѓаат", + "noDelete": "Не сакам", + "noLeave": "Не сакам", + "none": "Ништо", + "notifications": "Известувања", + "onlyMentions": "само @спомнувања", + "overview": "Преглед", + "region:": "Регион:", + "roles": "Улоги", + "ruleId": "Канал за правила:", + "selectnoti": "Изберете вид на известувања:", + "sendrandomwelcome?": "Испрати произволна порака кога некој ќе се придружи во еснафот:", + "serverName": "Име на еснафот:", + "settings": "Нагодувања", + "settingsFor": "Нагодувања за $1", + "splash": "Ударник за Откривање:", + "stickWelcomeReact?": "Поттикнете ги членовите на вашиот еснаф да реагираат со налепница кога некој ќе се придружи!", + "systemSelect:": "Канал за системски пораки:", + "tempCreatedBy": "Предлошката ја создал:", + "tempUseCount": "Предлошката е употребена {{PLURAL:$1|еднаш|$1 пати}}", + "template": "Предлошка:", + "templateDesc": "Опис на предлошката:", + "templateName": "Име на предлошката:", + "templateNameShort": "Името на предлошката мора да има барем 2 знака", + "templateURL": "Врска на предлошката: $1", + "templates": "Предлошки", + "templcateMetaDesc": "Предлошката им овозможува на другите да го користат овој еснаф како основа за нивните еснафи. Ќе ги копира каналите, улогите и нагодувањата на овој еснаф, но не и неговите, ботови и иконата.", + "topic:": "Тема:", + "unknownRole": "@непозната-улога", + "viewTemplate": "Преглед на предлошката", + "yesDelete": "Да, сигурен сум", + "yesLeave": "Да, сигурен сум" + }, + "hideBlockedMessages": "Го имате блокирано овој корисник. Стиснете за да ги скриете поракиве.", + "home": { + "icon": "Икона на примерокот на $1", + "uptimeStats": "Време на линија: \n Сето време: $1%\nНеделава: $2%\nДенес: $3%", + "warnOffiline": "Примерокот не на линија. Не можам да поврзам" + }, + "htmlPages": { + "addBot": "Додава во еснаф", + "alreadyHave": "Веќе имате сметка?", + "box1Items": "Лични пораки|Поддршка за реакции|Покани|Префрлање на сметки|Кориснички нагодувања|Разработувачки портал|Ботовски покани|Поддршка за преводи", + "box1title": "Ферми (Fermi) е клиент складен со Spacebar кој се стреми да биде што подобар, со многу функции како:", + "box3description": "Секогаш цениме помош, било во облик на пријавување грешки или код, па дури и ако ни посочите печатни грешки.", + "box3title": "Учествувајте во Ферми", + "compatableInstances": "Примероци складни со Spacebar:", + "createAccount": "Создај сметка", + "dobField": "Датум на раѓање:", + "emailField": "Е-пошта:", + "idpermissions": "Ова ќе му овозможи на ботот да:", + "instanceField": "Примерок:", + "loaddesc": "Не треба да потрае долго", + "loadingText": "Ферми се вчитува", + "loginButton": "Најава", + "noAccount": "Немате сметка?", + "openClient": "Отвори клиент", + "pw2Field": "Повторно внесете ја лозинката:", + "pwField": "Лозинка:", + "switchaccounts": "Промени сметки", + "trans": "Преведи", + "transDesc": "Да го преведеме Ферми на вашиот јазик!", + "transTitle": "Преведете го Ферми", + "userField": "Корисничко име:", + "welcomeJank": "Добре дојдовте во Ферми" + }, + "incorrectURLS": "## Овој примерок веројатно испрати неисправни врски.\n### Ако сте сопственик на примерокот, погледајте [тука](https://docs.spacebar.chat/setup/server/) под *Connecting from remote machines* за да го исправите проблемот.\n Дали сакате Ферми автоматски да се обидува да ја поправи оваа грешка за да ви овозможи да се поврзете со примерокот?", + "instInfo": "Информации за примерокот", + "instanceInfo": { + "contact": "Пишете на администратори на примерокот", + "frontPage": "[Насловна страница]($1)", + "name": "Инфо за примерокот", + "publicUrl": "[Јавна врска]($1)", + "tosPage": "[Услови на користење]($1)" + }, + "instanceStats": { + "members": "Пораки: $1", + "messages": "Пораки: $1", + "name": "Статистика за примерокот: $1", + "servers": "Еснафи: $1", + "users": "Зачленети корисници: $1" + }, + "interactions": { + "ephemeralDismiss": "Отфрли порака", + "failed": "Опходувањето не успеа", + "nonsence": "Опслужувачот испрати содржина од видот $1, која не постои", + "notImpl": "Ферми сè уште го нема воведено видот на составница $1", + "onlyYou": "Само вие можете да ја видите поракава", + "replyline": "Започнато опходување", + "started": "Го обработувам опходувањето...", + "worked": "Опходувањето успеа" + }, + "invite": { + "accept": "Прифати", + "alreadyJoined": "Веќе сте придружени", + "channel:": "Канал:", + "createInvite": "Создај покана", + "createdAt": "Создадено на $1", + "expireAfter": "Истекува по:", + "expires": "Истекува: $1", + "forChannel": "За каналот: $1", + "inviteLinkCode": "Врска/код на поканата", + "inviteMaker": "Создавач на поканата", + "invitedBy": "Поканети сте од $1", + "joinUsing": "Придружете се со покана", + "loginOrCreateAccount": "Најавете се или направете сметка ⇌", + "longInvitedBy": "$1 ве покани да се придружите на $2", + "maxUses": "Највеќе употреби: $1", + "never": "Никогаш", + "noAccount": "Направете сметка за да ја прифатите поканата", + "notFound": "Поканата не е пронајдена", + "subtext": "до $1 во $2", + "used": "Употребено {{PLURAL:$1|еднаш|$1 пати}}." + }, + "inviteOptions": { + "12h": "12 часа", + "1d": "1 ден", + "1h": "1 час", + "30d": "30 дена", + "30m": "30 минути", + "6h": "6 часа", + "7d": "7 дена", + "limit": "$1 {{PLURAL:$1|употреба|употреби}}", + "never": "Никогаш", + "noLimit": "Без граница", + "title": "Покани луѓе" + }, + "jankInfo": "Информации за клиентот", + "leaveGuild": "Напушти еснаф", + "leaving": "Го напуштате Spacebar", + "loaded": "Вчитано", + "localuser": { + "2faCode:": "Код:", + "2faCodeGive": "Вашата TOTP-тајна е: $1. Ова ќе создаде 6 цифри, со период на двочинителска шифра од 30 секунди.", + "2faDisable": "Оневозможи двочинителска заверка", + "2faEnable": "Овозможи двочинителска заверка", + "CheckUpdate": "Провери поднови", + "PasswordsNoMatch": "Лозинките не се совпаѓаат", + "TOSURL": "Врска за услови на употреба:", + "VoiceWarning": "Дали сигурно сакате да го овозможите ова? Ова е многу експериментална работа и веројатно ќе ви предизвика проблеми. (оваа функција е за нашите разработувачи, па затоа не овозможувајте ја ако не знаете што правите)", + "accentColor": "Боја на нагласокот:", + "accountSettings": "Нагодувања на сметката", + "addBot": "Додај бот", + "addStatus": "Додај статус", + "advancedBot": "Напредни нагодувања за бот", + "appName": "Име на прилогот:", + "areYouSureDelete": "Дали сигурно сакате да ја избришете вашата сметка. Ако сте сигурни, внесете ја фразата $1", + "badCode": "Неважечки код", + "badPassword": "Неисправна лозинка", + "botAvatar": "Аватар на ботот:", + "botInviteCreate": "Создавач на покани за ботови", + "botUsername": "Корисничко име на ботот:", + "changeDiscriminator": "Смени дискриминатор", + "changeEmail": "Смени е-пошта", + "changePassword": "Смени лозинка", + "changeUsername": "Смени корисничко име", + "clearCache": "Исчисти меѓусклад", + "confirmAddBot": "Дали сигурно сакате да додадете бот во прилогов? Ова е неповратно.", + "confirmReset": "Дали сигурно сакате да добиете нова шифра за ботот? Ботот ќе престане да работи додека не го подновите.", + "confuseNoBot": "Од некоја причина овој прилог (сè уште) нема бот.", + "connections": "Поврзувања", + "createApp": "Создај прилог", + "customSound": "Прилагоден звук", + "customStatusWarn": "Spacebar засега не поддржува прикажување на прилагоден статус. Иако го прифаќа статусот, не прави ништо со него", + "deleteAccount": "Бришење на сметка", + "deleteAccountButton": "Избриши сметка", + "description": "Опис:", + "devPortal": "Разработувачки портал", + "disableConnection": "Оваа врска е оневозможена од опслужувачка страна", + "editingBot": "Уредување на ботот $1", + "install": "Воспостави", + "installDesc": "Ако го воспоставите Ферми, тоа ќе ви овозможи да го отворате во свој прозорец и тој да се однесува како посебен прилог! Можете и да продолжите да го користите од прелистувачот, и тој ќе си работи исто.", + "installJank": "Воспостави го Ферми", + "language": "Јазик:", + "manageBot": "Управување со бот", + "manageInstance": "Управување со примерокот", + "mustTypePhrase": "За да си ја избришете сметката, ќе мора да ја внесете фразата", + "newDiscriminator": "Нов дискриминатор:", + "newEmail:": "Нова е-пошта", + "newPassword:": "Нова лозинка:", + "newUsername": "Ново корисничко име:", + "noToken": "Не ја знам шифрата, па затоа не можам да ја зачувам во localStorage", + "noUpdates": "Не најдов поднови", + "notisound": "Звук на известувањата:", + "oldPassword:": "Стара лозинка:", + "password:": "Лозинка", + "privacyPolcyURL": "Врска за правила за лични податоци:", + "appID": "Назнака на прилогот: $1", + "showSecret": "Прикажи клиентска тајна", + "clientSecret": "Клиентска тајна: $1", + "secret": "Тајна", + "publicAvaliable": "Ботот да биде јавно поканлив?", + "refreshPage": "Превчитај и примени", + "requireCode": "Да барам код за покана на ботот?", + "resetToken": "Нова шифра", + "saveToken": "Зачувај шифра во месен склад", + "setUp2fa": "Поставање на двочинителска заверка", + "setUp2faInstruction": "Ископирајте ја оваа тајна во вашиот TOTP-прилог (временски ограничена еднократна лозинка) за да создадете ваши кодови за двочинителска заверка", + "settings": "Нагодувања", + "sillyDeleteConfirmPhrase": "Ни лук јал, ни лук мирисал", + "soundTooLarge": "Звукот што сакате да го подигнете е преголем. Обидете се повторно", + "status": "Статус", + "team:": "Екипа:", + "theme:": "Изглед", + "themesAndSounds": "Изгледи и звуци", + "tokenDisplay": "Шифра: $1", + "trace": "Траги", + "trusted": "Домени од доверба", + "trustedDesc": "Кога ќе стиснете на врски од овие домени, тие ***нема*** да ви побараат дозвола за отворање како другите врски. Давајте го ова само на URL-а на кои им верувате", + "updateSettings": "Поднови нагодувања", + "updatesYay": "Подновите се најдени!", + "userSettings": "Кориснички нагодувања" + }, + "login": { + "allGood": "Сè е во ред", + "checking": "Проверка на примерокот", + "enterPAgain": "Повторно внесете ја новата лозинка:", + "invalid": "Неважечки примерок. Обидете се повторно", + "login": "Најава", + "newPassword": "Нова лозинка:", + "pasteInfo": "Тука прекопирајте ја URL за повраток:", + "recover": "Ја заборавивте лозинката?", + "recovery": "Заборавена лозинка", + "waiting": "Се чека на проверка на примерокот" + }, + "logout": { + "error": { + "cancel": "Откажи", + "cont": "Сепак продолжи", + "desc": "Ферми не може да ве одјави,\n дали сепак сакате да ја отстраните сметката?", + "title": "Се појави грешка при одјавувањето" + }, + "logout": "Одјава" + }, + "manageInstance": { + "AreYouSureStop": "Дали сигурно сакате да го запрете примероков?", + "TokenFormats": { + "JSON": "JSON", + "URLs": "URL-а на поканите", + "plain": "Прост" + }, + "clientURL": "URL на клиентот:", + "copy": "Копирај", + "count": "Број:", + "create": "Создај", + "createTokens": "Создај шифри за зачленување", + "format": "Формат:", + "genericType": "Општ", + "length": "Должина:", + "regType": "Пријави вид на URL за шифра", + "stop": "Запри го примерокот" + }, + "media": { + "artist": "Уметник: $1", + "composer": "Композитор: $1", + "download": "Преземи податотека", + "length": "Траење: $1 минути и $2 секунди", + "loading": "Вчитувам", + "moreInfo": "Повеќе инфо", + "notFound": "Податотеката не е пронајдена" + }, + "member": { + "ban": "Забрани го $1 од $2", + "kick": "Исфрли го $1 од $2", + "nick:": "Прекар:", + "persist": "Избриши го само корисникот:", + "reason:": "Причина:", + "timeout": "Времено исклучување на $1", + "timeoutTime": "Траење на временото исклучување:" + }, + "message": { + "andMore": "$1 и повеќе!", + "attached": "Испрати прилог", + "delete": "Избриши порака", + "report": "Пријави порака", + "deleted": "Избришана порака", + "edit": "Уреди порака", + "edited": "(изменето)", + "fullMessage": "Целосна порака:", + "pin": "$1 закачи порака", + "reactedWith": "Реагирано со $1", + "reactionAdd": "Додај реакција", + "reactions": "Погл. реакции", + "reactionsTitle": "Реакции", + "retry": "Повторно испрати порака со грешка", + "viewrest": "Погл. останати" + }, + "report": { + "back": "Назад", + "next": "Следно", + "cancel": "Откажи", + "submit": "Поднеси", + "blockUser": "Блокирај корисник", + "timeout": "Времено исклучи корисник", + "summary": "Краток опис на пријавата:" + }, + "messageLink": "Порака во бр. $1", + "muteDuration": "Вклучи звук во:", + "nevermind": "Не сакам", + "no": "Не", + "noEmojiFont": "Користи системски емотикон", + "noMessages": "Тука нема пораки. Бидете првите што ќе кажете нешто!", + "noPins": "Тука засега нема ништо", + "noStickers": "Се чини дека сте на наобично место. Нема налепници на располагање!", + "notiClump": "Добивте $1 известувања од $2", + "notiVolume": "Гласност на известувањата:", + "otherAt": "$1 во $2", + "permissions": { + "descriptions": { + "ADMINISTRATOR": "Ги дозволува сите дозволи и ги заобиколува наметнувањата на дозволите на каналот. Ова е опасна дозвола!", + "MANAGE_GUILD": "Овозможува управување и уредување на еснафот", + "VIEW_AUDIT_LOG": "Му овозможува на корисникот да го гледа дневникот на ревизија", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Овозможува гледање аналитика на претплата на улоги", + "VIEW_GUILD_INSIGHTS": "Му овозможува на корисникот да гледа аналитика за еснаф", + "BYPASS_SLOWMODE": "Бавниот режим повеќе не го засега овој корисник", + "MENTION_EVERYONE": "Му овозможува на корисникот да ги спомнува сите", + "MANAGE_NICKNAMES": "Му овозможува на корисникот да менува прекари на други корисници", + "PIN_MESSAGES": "Му овозможува на корисникот да прикачува пораки", + "SEND_POLLS": "Овозможува испраќање на анкети", + "MANAGE_MESSAGES": "Му овозможува на корисникот да брише пораки што не се негови", + "MANAGE_THREADS": "Му овозможува на корисникот да брише и архивира нишки, и да ги гледа сите приватни нишки", + "MANAGE_CHANNELS": "Му овозможува на корисникот да управува со и уредува канали", + "MANAGE_ROLES": "Му овозможува на корисникот да уредува и управува со улоги", + "MANAGE_WEBHOOKS": "Овозможува управување со и уредување на семрежни пресретници", + "CREATE_EVENTS": "Овозможува создавање на закажани настани, како и уредување и бришење на оние создадени од тековниот корисник.", + "MANAGE_EVENTS": "Му овозможува на корисникот да уредува и управува со настани", + "CREATE_GUILD_EXPRESSIONS": "Овозможува создавање на емотикони и звуци во гласовната управувачница, како и уредување и бришење на постоечките создадени од тековниот корисник.", + "MANAGE_GUILD_EXPRESSIONS": "Овозможува управување со емотикони, налепници и гласовната управувачница", + "MODERATE_MEMBERS": "Му овозможува на корисникот времено да исклучува други корисници за да ги спречи да испраќаат и реагираат на пораки во разговор во живо и нишки, и да зборуваат во гласовни и трибински канали", + "KICK_MEMBERS": "Овозможува на корисникот да исфрла членови од еснафот", + "BAN_MEMBERS": "Овозможува на корисникот да забранува членови од еснафот", + "MOVE_MEMBERS": "Му овозможува на корисникот да преместува членови од еден на друг гласовен канал", + "MUTE_MEMBERS": "Му овозможува на корисникот да занемува на други корисници", + "DEAFEN_MEMBERS": "Му овозможува на корисникот да заглушува други членови", + "CHANGE_NICKNAME": "Му овозможува на корисникот да го менува својот прекар", + "VIEW_CHANNEL": "Му овозможува на корисникот да го гледа каналот", + "READ_MESSAGE_HISTORY": "Му овозможува на корисникот да ја чита историјата на пораки", + "ADD_REACTIONS": "Му овозможув ана корисникот да додава реакции во пораки", + "SEND_MESSAGES": "Му овозможува на корисникот да испраќа пораки", + "SEND_MESSAGES_IN_THREADS": "Му овозможува на корисникот да испраќа пораки во нишки", + "SEND_TTS_MESSAGES": "Му овозможува на корисникот да испраќа пораки од текст во говор", + "USE_EXTERNAL_EMOJIS": "Му овозможува на корисникот да употребува надворешни емотикони", + "USE_EXTERNAL_STICKERS": "Му овозможува на корисникот да користи надворешни налепници", + "EMBED_LINKS": "Дозволи автовметнување на врските испратени од овој корисник", + "ATTACH_FILES": "Му овозможува на корисникот да приложува податотеки", + "SEND_VOICE_MESSAGES": "Овозможува испраќање гласовни пораки", + "CREATE_INSTANT_INVITE": "Им овозможува на корисниците да создаваат покани за еснафот", + "CREATE_PUBLIC_THREADS": "Му овозможува на корисникот да создава јавни нишки", + "CREATE_PRIVATE_THREADS": "Му овозможува на корисникот да создава приватни нишки", + "CONNECT": "Му овозможува на корисникот да се поврзе со гласовен канал", + "SPEAK": "Му овозможува на корисникот да зборува во гласовен канал", + "USE_VAD": "Им овозможува на корисниците да зборуваат во гласовен канал едноставно разговарајќи", + "STREAM": "Му овозможува на корисникот да емитува пренос", + "USE_SOUNDBOARD": "Овозможува употреба на гласовна управувачница во гласовен канал", + "USE_EXTERNAL_SOUNDS": "Овозможува употреба на прилагодени звуци од гласовна управувачница од други еснафи", + "PRIORITY_SPEAKER": "Овозможува употреба на приоритетен говорник во гласовен канал", + "REQUEST_TO_SPEAK": "Му овозможува на корисникот да побара да зборува во трибински канал", + "USE_EMBEDDED_ACTIVITIES": "Му овозможува на корисникот да употребува вметнати активности", + "USE_APPLICATION_COMMANDS": "Му овозможува на корисникот да користи приложни наредби", + "USE_EXTERNAL_APPS": "Овозможува кориснички воспоставените прилози да испраќаат јавни одговори. Кога е оневозможено, корисниците сепак ќе можат да ги користат нивните прилози, но одговорите ќе бидат краткотрајни. Ова важи само за прилозите кои не се воедно воспоставени на еснафот." + }, + "readableNames": { + "ADD_REACTIONS": "Додавање на реакции", + "ADMINISTRATOR": "Администратор", + "ATTACH_FILES": "Приложи податотеки", + "BAN_MEMBERS": "Забранување на членови", + "BYPASS_SLOWMODE": "Заобиколи бавен режим", + "CHANGE_NICKNAME": "Смени прекар", + "CONNECT": "Поврзување", + "CREATE_EVENTS": "Создавање на настани", + "CREATE_GUILD_EXPRESSIONS": "Создавање на изрази", + "CREATE_INSTANT_INVITE": "Создај покана", + "CREATE_PRIVATE_THREADS": "Создавање на приватни нишки", + "CREATE_PUBLIC_THREADS": "Создавање на јавни нишки", + "DEAFEN_MEMBERS": "Заглуши членови", + "EMBED_LINKS": "Вметни врски", + "KICK_MEMBERS": "Исфрлање на членови", + "MANAGE_CHANNELS": "Управување со канали", + "MANAGE_EVENTS": "Управување со настани", + "MANAGE_GUILD": "Управување со еснафи", + "MANAGE_GUILD_EXPRESSIONS": "Управување со изрази", + "MANAGE_MESSAGES": "Управување со пораки", + "MANAGE_NICKNAMES": "Управување со прекари", + "MANAGE_ROLES": "Управување со улоги", + "MANAGE_THREADS": "Управување со нишки", + "MANAGE_WEBHOOKS": "Управување со семрежни пресретници", + "MENTION_EVERYONE": "Спомни ги @everyone, @here и сите улоги", + "MODERATE_MEMBERS": "Времено исклучување на корисници", + "MOVE_MEMBERS": "Премести членови", + "MUTE_MEMBERS": "Занеми членови", + "PIN_MESSAGES": "Закачи пораки", + "PRIORITY_SPEAKER": "Приоритетен говорник", + "READ_MESSAGE_HISTORY": "Прочитај ја историјата на пораки", + "REQUEST_TO_SPEAK": "Побарај да зборуваш", + "SEND_MESSAGES": "Испрати пораки", + "SEND_MESSAGES_IN_THREADS": "Испраќање на пораки во нишки", + "SEND_POLLS": "Создавање на анкети", + "SEND_TTS_MESSAGES": "Испраќајте пораки од текст во говор", + "SEND_VOICE_MESSAGES": "Испраќање на гласовни пораки", + "SPEAK": "Зборувај", + "STREAM": "Видео", + "USE_APPLICATION_COMMANDS": "Користи наредби од прилогот", + "USE_EMBEDDED_ACTIVITIES": "Употреба на активности", + "USE_EXTERNAL_APPS": "Употреба на надворешни прилози", + "USE_EXTERNAL_EMOJIS": "Корисни надворешни емотикони", + "USE_EXTERNAL_SOUNDS": "Употреба на надворешни звуци", + "USE_EXTERNAL_STICKERS": "Употреба на надворешни налепници", + "USE_SOUNDBOARD": "Употреба на гласовна управувачница", + "USE_VAD": "Користи откривање на гласовна активност", + "VIEW_AUDIT_LOG": "Гледање на дневник на ревизија", + "VIEW_CHANNEL": "Погл. канали", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Гледање аналитика на монетизација на создавачот", + "VIEW_GUILD_INSIGHTS": "Погл. аналитика за еснаф" + } + }, + "pinMessage": "Закачи порака", + "profile": { + "bio": "За мене:", + "joined": "Направена сметка: $1", + "joinedMember": "Се придружи на $1: $2", + "mut": "Взаемни еснафи", + "mutFriends": "Заеднички пријатели", + "permInfo": "Дозволи", + "userInfo": "Кориснички информации" + }, + "profileColor": "Боја на профилот", + "pronouns": "Лич. заменки:", + "readableName": "македонски", + "recentEmoji": "Скорешни емотикони", + "register": { + "DOBError": "Датум на раѓање: $1", + "agreeTOS": "Се согласувам со[Условите на употреба]($1):", + "emailError": "Е-пошта: $1", + "noTOS": "Овој примерок нема Услови на употреба, но сепак прифатете ги:", + "passwordError:": "Лозинка: $1", + "register": "Зачленување", + "tos": "Ќе мора да се согласите со Условите на употреба", + "usernameError": "Корисничко име: $1" + }, + "remove": "Отстрани", + "renderJoinAvatars": "Прикажувај аватари за нови членови", + "reply": "Одговори", + "replyingTo": "Одговор на $1", + "retrying": "Се обидувам повторно...", + "role": { + "color": "Боја", + "colors": { + "name": "Број на бои:", + "one": "Една", + "two": "Две", + "three": "Три", + "secondColor": "Втора боја:", + "thirdColor": "Трета боја:" + }, + "confirmDelete": "Дали сигурно сакате да го избришете $1?", + "copyId": "Копирај назнака на улога", + "delete": "Отстрани правило", + "deleting": "Ја бришам улогата...", + "displaySettings": "Нагодувања за приказ", + "hoisted": "Издигнато:", + "mentionable": "Дозволи секому да ја спомнува оваа улога:", + "name": "Име на улогата:", + "perms": "Дозволи", + "remove": "Отстрани улога", + "roleEmoji": "Емотикон на улогата:", + "roleFileIcon": "Икона на улогата:", + "roles": "Улоги" + }, + "search": { + "back": "Назад", + "new": "Ново", + "next": "Следно", + "nofind": "Нема пораки што одговараат на пребараното. Можеби треба да го проширите пребарувањето", + "old": "Старо", + "page": "Страница $1", + "search": "Пребарај" + }, + "searchGifs": "Пребарај по Тенор", + "settings": { + "clear": "Исчисти", + "img": "Подигни слика", + "save": "Зачувај промени", + "unsaved": "Внимавајте. Имате незачувани промени", + "updates": { + "serviceWorkerMode": { + "disabled": "Искл.", + "enabled": "Вкл.", + "offlineOnly": "Само вонмрежно", + "title": "Нагодување за услужниот работник", + "unregistered": "Исклучено (вкл. податотечен меѓусклад)" + } + } + }, + "showBlockedMessages": "Го имате блокирано овој корисник. Стиснете за да {{PLURAL:$1|ја видите блокираната порака|видите $1 блокирани пораки}}.", + "spoiler": "Разоткривање", + "sticker": { + "confirmDel": "Дали сигурно сакате да ја избришете налепницава?", + "del": "Избриши налепница", + "desc": "Опис", + "errEmjMust": "Мора да има емотикон за вашата налепница", + "errFileMust": "Мора да има слика за вашата налепница", + "image": "Слика:", + "name": "Име:", + "tags": "Поврзан емотикон:", + "title": "Налепници", + "upload": "Подигни налепници" + }, + "submit": "поднеси", + "switchAccounts": "Промени сметки ⇌", + "todayAt": "Денес во $1", + "trace": { + "totalTime": "$2: $1 мс", + "traces": "$1 ($2 мс) $3" + }, + "typing": "$2 {{PLURAL:$1|пишува|пишуваат}}", + "unableToConnect": "Не можам да се поврзам со опслужувачот на Spacebar. Одјавете се и пак најавете се.", + "unableToPin": "Не можам да ја закачам пораката", + "unpinMessage": "Откачи порака", + "updateAv": "Има поднови на располагање", + "uploadBanner": "Подигнете транспарент:", + "uploadFilesText": "Тука подигајте ги податотеките!", + "uploadPfp": "Подигнете профилна слика:", + "useTemplate": "Користи го $1 како предлошка", + "useTemplateButton": "Користи предлошка", + "usedFermi": "Употребен Ферми", + "user": { + "report": "Пријави корисник", + "reportApp": "Пријави прилог", + "addRole": "Додај улоги", + "ban": "Забрани член", + "block": "Блокирај корисник", + "confirmInstBan": "Дали сиурно сакате да го забраните $1? од примерокот", + "copyId": "Копирај корисничка назнака", + "dnd": "Не вознемирувај", + "editNick": "Уреди прекар", + "editServerProfile": "Уреди еснафски профил", + "friendReq": "Барање за пријателство", + "idle": "Неактивен", + "instanceBan": "Забрана од примерок", + "invisible": "Невидлив", + "kick": "Исфрли член", + "message": "Порака до корисникот", + "nick": { + "foe": "Постави прекар на непријател", + "friend": "Постави прекар на пријател", + "stalker": "Постави прекар на барателот", + "stalking": "Постави прекар на бараниот" + }, + "offline": "Вон линија", + "online": "На линија", + "remove": "Отстрани корисник", + "removeRole": "Отстрани улоги", + "sendMessage": "Порака @$1", + "timeout": "Времено исклучен корисник", + "unTimeout": "Отстрани времена исклученост на корисникот", + "unban": "Отстрани забрана на $1", + "unblock": "Одблокирај корисник" + }, + "userping": { + "resolving": "решавачки корисник", + "unknown": "@непознат" + }, + "vc": { + "joinForStream": "Приклучете се на гласовниот канал за да гледате", + "joiningStream": "Се приклучувам на преносот...", + "joinstream": "Гледај пренос", + "leavestream": "Пренос во живо", + "stopstream": "Запри го преносот" + }, + "webauth": { + "addKey": "Додај клуч", + "keyname": "Назив на клучот:", + "manage": "Управување со безбедносни клучеви" + }, + "webhook": "СЕМРЕЖЕН ПРЕСРЕТНИК", + "webhooks": { + "EnterWebhookName": "Внесете име на семрежен пресретник", + "areYouSureDelete": "Дали сигурно сакате да го избришете $1?", + "avatar": "Аватар", + "base": "Семрежни пресретници", + "channel": "Канал", + "copyURL": "Ископирај URL на семрежниот пресретник", + "createdAt": "Создадено на $1", + "createdBy": "Создадено од:", + "deleteWebhook": "Избриши семрежен пресретник", + "name": "Име:", + "newWebHook": "Нов семрежен пресретник", + "notFound": "Корисникот повеќе не е во еснафот", + "sillyDefaults": "Пресретко\nСретко Помагач\nБот Многу Вреден", + "token": "Шифра за семрежниот пресредник: „$1“", + "type": "Вид: $1", + "type1": "Дојдовни", + "type2": "Следач на канал", + "type3": "Прилог", + "url": "URL на семрежниот пресретник: `$1`" + }, + "welcomeMessages": "Добре дојдовте $1! Мило ни е што ве гледаме!\nЗфраво, драго ни е што се запознаваме $1!\nДобре дојде, се надеваме дека донесовте пица $1!", + "widget": "Посреднички елемент за еснафот", + "widgetEnabled": "Посредничкиот елемент е овозможен", + "yes": "Да", + "yesterdayAt": "Вчера во $1" +} diff --git a/translations/nl.json b/translations/nl.json new file mode 100644 index 00000000..4e6e9307 --- /dev/null +++ b/translations/nl.json @@ -0,0 +1,960 @@ +{ + "@metadata": { + "authors": [ + "ABPMAB", + "HanV", + "McDutchie", + "Siebrand" + ] + }, + "2faCode": "2FA-code:", + "404": { + "404": "fout:404 - Pagina niet gevonden", + "app": "Naar de app", + "blog": "De Fermi-blog", + "home": "Startpagina", + "listtitle": "Misschien wilde u naar een van deze plekken gaan", + "login": "Aanmelden", + "report": "Als u deze pagina binnen de cliënt hebt gevonden, meld dit dan:", + "reset": "Wachtwoord opnieuw instellen", + "title": "Bent u verdwaald?", + "whatelse": "Wat denkt u dat er nog meer moet gebeuren?", + "whereever": "Waar dit ook is" + }, + "onboarding": { + "name": "Inwerken", + "disable": "Inwerken uitschakelen", + "addChannel": "Kanaal toevoegen", + "channel": "Kanaal:", + "desc": "Beschrijving:", + "deleteChannel": "Kanaal voor inwerken verwijderen", + "enable": "Inwerken inschakelen", + "title": "Welkom bij $1!" + }, + "DMs": { + "add": "Iemand aan directe bericht toevoegen", + "close": "Directe bericht sluiten", + "copyId": "Directe bericht-ID kopiëren", + "markRead": "Markeren als gelezen", + "name": "Directe berichten" + }, + "ok": "Ok", + "dismiss": "Sluiten", + "UrlGen": "Bot-uitnodigingsgenerator", + "Voice": { + "userVol": "Gebruikersvolume:", + "status": { + "badWS": "Foute reactie van WS", + "conectionFailed": "Verbinding mislukt", + "done": "Verbonden", + "left": "Heeft de voicechat verlaten", + "makingOffer": "Aanbod wordt gedaan", + "noSDP": "Geen SDP", + "notconnected": "Niet verbonden", + "sendingStreams": "Audiostreams verzenden", + "startingRTC": "RTC-verbinding wordt gemaakt", + "waitingMainWS": "Wachten het hoofd-WS", + "waitingURL": "Wachten op spraak-URL", + "wsAuth": "wachten op autorisatie van WS", + "wsOpen": "Wachten tot WS opengaat" + } + }, + "accessibility": { + "gifSettings": { + "always": "Altijd", + "hover": "Bij zweven", + "never": "Nooit" + }, + "name": "Toegankelijkheid", + "playGif": "GIFs afspelen:", + "playIcon": "Geanimeerde pictogrammen afspelen:", + "roleColors": "Rolkleuren uitschakelen:", + "gradientColors": "Kleurverloop uitschakelen:", + "decorations": "Avatarversieringen inschakelen:" + }, + "accountNotStart": "Account kan niet worden gestart", + "add": "Toevoegen", + "applications": { + "delete": "Toepassing verwijderen", + "sure": "Weet u zeker dat u $1 wilt verwijderen?" + }, + "badge": { + "active_developer": "Actieve ontwikkelaar", + "application_guild_subscription": "heeft premium", + "automod": "gebruikt AutoMod", + "bot_commands": "ondersteunt opdrachten", + "bug_hunter_level_1": "Foutenjager niveau 1", + "bug_hunter_level_2": "Foutenjager niveau 2", + "certified_moderator": "Moderator", + "early_supporter": "Eerste ondersteuner", + "guild_booster_lvl1": "Versterkt gilde", + "guild_booster_lvl2": "Versterkt gilde", + "guild_booster_lvl3": "Versterkt gilde", + "guild_booster_lvl4": "Versterkt gilde", + "guild_booster_lvl5": "Versterkt gilde", + "guild_booster_lvl6": "Versterkt gilde", + "guild_booster_lvl7": "Versterkt gilde", + "guild_booster_lvl8": "Versterkt gilde", + "guild_booster_lvl9": "Al behoorlijk lang versterkt gilde", + "hypesquad": "Vibesquad [PLACEHOLDER]", + "hypesquad_house_1": "Vrijmoedigheid", + "hypesquad_house_2": "Uitmuntendheid", + "hypesquad_house_3": "Evenwicht", + "legacy_username": "heeft oude gebruikersnaam", + "partner": "Instantiepartner", + "premium": "Premium", + "quest_completed": "heeft een missie voltooid", + "staff": "Serverpersoneel", + "verified_developer": "Geverifieerde ontwikkelaar" + }, + "bio": "Bio:", + "blankMessage": "Leeg bericht", + "blog": { + "blog": "Blog", + "blogUpdates": "Actualiteiten:", + "desc": "Lees de blog of abonneer u erop voor nieuwtjes over Fermi!", + "fermi": "Blog van Fermi", + "gotoPost": "Naar het artikel", + "wantUpdates": "Wilt u wekelijks actualiteiten ontvangen over de ontwikkeling van Fermi?" + }, + "bot": "ROBOT", + "botGuilds": "Gilden waar de robot lid van is:", + "botSettings": "Robot-instellingen", + "cancel": "Annuleren", + "channel": { + "SlowmodeCool": "Afkoeltijd voor langzame modus: $1", + "TimeOutCool": "Time-out bereikt na: $1", + "allowIcons": "Aangepaste kanaalpictogrammen toestaan", + "announcement": "Aankondigingen", + "copyId": "Kanaal-ID kopiëren", + "copyIdCat": "Categorie-ID kopiëren", + "createCatagory": "Categorie aanmaken", + "createChannel": "Kanaal aanmaken", + "creating": "Kanaal wordt aangemaakt", + "delete": "Kanaal verwijderen", + "deleteCat": "Categorie verwijderen", + "icon": "Pictogram:", + "makeInvite": "Uitnodiging maken", + "markRead": "Markeren als gelezen", + "mute": "Kanaal dempen", + "name": "Kanaal", + "name:": "Kanaalnaam:", + "nsfw:": "Waarschuwing: inhoud niet geschikt voor minderjarigen:", + "permissions": "Machtigingen", + "selectCatName": "Naam van de categorie", + "selectName": "Naam van het kanaal", + "selectType": "Selecteer kanaaltype", + "settings": "Instellingen", + "settingsFor": "Instellingen voor $1", + "slowmode": "Langzame modus:", + "text": "Tekst", + "timedOutUntil": "Time-out bereikt na: $1", + "topic:": "Onderwerp:", + "typebox": "Bericht in $1", + "unmute": "Dempen kanaal opheffen", + "voice": "Spraak", + "deleteThread": "Onderwerp verwijderen", + "hideThreads": "Verbergen na inactiviteit", + "forum": "Forum" + }, + "threads": { + "leave": "Discussie verlaten", + "join": "Aan discussie meedoen", + "name": "Onderwerpnaam:", + "editTags": "Labels bewerken" + }, + "forum": { + "creorsear": "Bericht aanmaken of zoeken", + "next": "Volgende", + "back": "Terug", + "newPost": "Nieuw bericht maken", + "post": "Plaatsen", + "sortOptions": { + "sortby": { + "title": "Sorteren op", + "recent": "Recent actief", + "posted": "Geplaatst op" + }, + "sortOrder": { + "title": "Sorteervolgorde", + "recent": "Nieuwste eerst", + "old": "Oudste eerst" + }, + "tagMatch": { + "title": "Labelmatching", + "some": "Sommige labels matchen", + "all": "Alle labels matchen" + }, + "name": "Sorteeropties" + }, + "errors": { + "tagsReq": "Labels zijn vereist", + "requireText": "Het bericht mag niet leeg zijn" + }, + "allTags": "Alle labels", + "settings": { + "editTags": "Labels bewerken:", + "editTag": "Label bewerken", + "tagName": "Labelnaam:", + "moderated": "Alleen overlegmoderators mogen labels toepassen:", + "addTag": "Label toevoegen", + "delTag": "Label verwijderen" + } + }, + "channelLink": "# $1", + "clientDesc": "Cliëntversie: $1 \n\n[Sluit u aan bij het officiële Fermi-gilde]($2/invite/USgYJo?instance=https%3A%2F%2Fspacebar.chat)\n\n[Help Fermi te vertalen](https://translatewiki.net/wiki/Translating:JankClient#sortable:3=desc) \n\n[Help Fermi te maken](https://github.com/MathMan05/Fermi)\n\n[Help de server waar Fermi op bouwt te onderhouden](https://github.com/spacebarchat/server)\n\n[Lees het blog](https://blog.fermi.chat/)\n\nBerekende rechten: $3", + "commands": { + "errorNotValid": "$1 is geen geldige keuze voor $2", + "required": "$1 is een vereist onderdeel van deze opdracht" + }, + "confirmGuildLeave": "Weet u zeker dat u $1 wilt verlaten?", + "copyLink": "Koppeling naar bericht kopiëren", + "copyRegLink": "Koppeling kopiëren", + "copymessageid": "Bericht-ID kopiëren", + "copyrawtext": "Ruwe tekst kopiëren", + "createAccount": "Account aanmaken", + "delete": "Verwijderen", + "deleteConfirm": "Weet u zeker dat u dit wilt verwijderen?", + "devSettings": { + "badUser": "Logging voor slechte gebruikersobjecten inschakelen die te veel gegevens verzenden:", + "cache": "Cachefunctie voor kaartbestanden van de Service Worker inschakelen:", + "cacheDesc": "kaartbestanden worden hoe dan ook geladen; dit zorgt er alleen voor dat ze in de cache staan wanneer er een nieuwe update wordt uitgebracht.", + "captureTrace": "Deze instelling geeft Fermi de opdracht om _trace-eigenschappen van de server vast te leggen. Het inschakelen hiervan kan ertoe leiden dat progressieve JSON-decodering niet werkt; mogelijk is een herladen nodig.", + "clearWellKnowns": ".well-known-records opnieuw ophalen:", + "description": "Deze instellingen zijn bedoeld voor ontwikkelaars van Spacebar/Fermi. Als u niet weet wat u doet, kunt u deze instellingen beter niet wijzigen.", + "gatewayComp": "Gateway-compressie uitschakelen:", + "reportSystem": "Experimentele rapportagesysteem inschakelen:", + "logGateway": "Ontvangen gatewaygebeurtenissen loggen (logniveau info):", + "name": "Ontwikkelaarsinstellingen", + "traces": "Traces zichtbaar maken:" + }, + "deviceManage": { + "city": "Plaats: $1", + "continent": "Continent: $1", + "country": "Land: $1", + "curSes": "Dit is de huidige sessie, u moet afmelden via het andere menu", + "estimateWarn": "Waarschuwing: Al deze gegevens is slechts een inschatting en kunnen onjuist zijn.", + "ip": "Laatst bekende IP-adres: $1", + "last": "Ongeveer voor het laatst gebruikt: $1", + "latitude": "Breedtegraad: $1", + "logout": "Afmelden", + "longitude": "Lengtegraad: $1", + "manageDev": "Apparaat beheren", + "postal": "Postcode: $1", + "region": "Regio: $1", + "title": "Sessies beheren" + }, + "discovery": "Ontdekking", + "dms": "Directe berichten", + "edit": "Bewerken", + "emoji": { + "confirmDel": "Weet u zeker dat u deze emoji wilt verwijderen?", + "found": { + "not": "Deze emoji komt niet uit een van uw gilden. U moet u bij het gilde aansluiten om deze emoji te kunnen gebruiken.", + "other": "Deze emoji komt uit een van uw andere gilden.", + "private": "Het gilde voor deze emoji kan niet worden gevonden", + "this": "Deze emoji komt uit dit gilde." + }, + "from": "Deze emoji is van", + "image:": "Afbeelding:", + "join": "Bij gilde aansluiten", + "name:": "Naam:", + "privateGuild": "Dit gilde is privé", + "title": "Emoji's", + "upload": "Emoji's uploaden" + }, + "emojiSelect": "Emoji:", + "errorReconnect": "Kan geen verbinding maken met de server. Het wordt over **$1** seconden opnieuw geprobeerd…", + "favoriteGifs": "Favoriete GIF's", + "folder": { + "color": "Mapkleur:", + "create": "Nieuwe map maken", + "edit": "Map bewerken", + "name": "Mapnaam:" + }, + "form": { + "captcha": "Wacht even, bent u wel een mens?" + }, + "friends": { + "addfriend": "Vriend toevoegen", + "addfriendpromt": "Vrienden toevoegen via gebruikersnaam:", + "requestsent": "Aanvraag verzonden!", + "all": "Alle", + "all:": "Alle vrienden:", + "blocked": "Geblokkeerd", + "blockedusers": "Geblokkeerde gebruikers:", + "bu": "Geblokkeerde gebruiker", + "discnotfound": "Discriminator niet gevonden", + "friendlist": "Vriendenlijst", + "friends": "Vrienden", + "notfound": "Gebruiker niet gevonden", + "online": "Online", + "online:": "Online vrienden:", + "pending": "In behandeling", + "pending:": "Wachtende vriendschapsverzoeken:", + "removeFriend": "Vriend verwijderen" + }, + "goThere": "Laten we gaan", + "goThereTrust": "Ja, en vertrouw dit domein vanaf nu", + "goingToURL": "Deze hyperlink brengt u naar $1. Weet u zeker dat u daarheen wilt gaan?", + "group": { + "createdm": "Maak directe bericht!", + "edit": "Groepschat bewerken", + "select": "Vrienden kiezen" + }, + "guild": { + "COMMUNITY": "Lidmaatschap aanvragen", + "disableInvites": "Uitnodigingen uitschakelen:", + "DISCOVERABLE": "Ontdekking", + "INVITES_DISABLED": "Alleen op uitnodiging", + "adminMenu": { + "changePerms": "Machtigingen wijzigen om te zoeken", + "finding": "Beheerders zoeken", + "ownName": "Eigenaar", + "owner": "Zoek de eigenaar op", + "permission": "Machtigingen:" + }, + "report": "Gilde rapporteren", + "admins": "Beheerders zoeken", + "all": "Alle", + "banReason": "Reden voor ban: $1", + "bannedBy": "Verbannen door:", + "banner:": "Banner:", + "bans": "Bannen", + "boostMessage?": "Stuur een bericht wanneer iemand uw gilde versterkt!", + "community": "Gemeenschap", + "confirmDelete": "Weet u zeker dat u $1 wilt verwijderen?", + "confirmLeave": "Weet u zeker dat u weg wilt?", + "copyId": "Gilde-ID kopiëren", + "create": "Volledig nieuw gilde maken", + "createFromTemplate": "Gilde maken op basis van sjabloon", + "createNewTemplate": "Nieuw sjabloon maken", + "creating": "Gilde wordt aangemaakt", + "default": "Standaard ($1)", + "defaultNoti": "Stel de standaardmeldingsinstellingen van uw gilde in!", + "delete": "Gilde verwijderen", + "description:": "Beschrijving:", + "disoveryTitle": "Gilden ontdekken ($1 vermelding{{PLURAL:$1||en}})", + "editingTemplate": "Bewerken van $1", + "emptytext": "Wat vreemd, dit gilde heeft geen kanalen!?", + "emptytitle": "Vreemde plek", + "guilds": "Gilden", + "helpTips?": "Stuur nuttige tips voor uw gilde!", + "howJoin": "Hoe kunnen mensen zich bij uw gilde aansluiten?", + "icon:": "Pictogram:", + "invites": "Uitnodigingen", + "joinConfirm": "Bij $1 aansluiten?", + "leave": "Gilde verlaten", + "loadingDiscovery": "Aan het laden…", + "makeInvite": "Uitnodiging maken", + "markRead": "Markeren als gelezen", + "memberCount": "$1 {{PLURAL:$1|lid|leden}}", + "mute": "Gilde dempen", + "name:": "Naam:", + "nameNoMatch": "Namen komen niet overeen", + "noDelete": "Laat maar", + "noLeave": "Laat maar", + "none": "Geen", + "notifications": "Meldingen", + "onlyMentions": "Alleen @vermeldingen", + "overview": "Overzicht", + "region:": "Regio:", + "roles": "Rollen", + "ruleId": "Regelskanaal:", + "selectnoti": "Selecteer type melding:", + "sendrandomwelcome?": "Stuur een willekeurig bericht wanneer iemand zich bij dit gilde aansluit:", + "serverName": "Naam van het gilde:", + "settings": "Instellingen", + "settingsFor": "Instellingen voor $1", + "splash": "Ontdekkingssplash:", + "stickWelcomeReact?": "Laat leden van uw gilde met een sticker reageren wanneer iemand zich aansluit!", + "systemSelect:": "Systeemberichtenkanaal:", + "tempCreatedBy": "Sjabloon gemaakt door:", + "tempUseCount": "Sjabloon is $1 {{PLURAL:$1|keer}} gebruikt", + "template": "Sjabloonkoppeling of -code om gilde van te bouwen:", + "templateDesc": "Sjabloonbeschrijving:", + "templateName": "Sjabloonnaam:", + "templateNameShort": "De sjabloonnaam moet ten minste 2 tekens lang zijn", + "templateURL": "Sjabloonkoppeling: $1", + "templates": "Sjablonen", + "templcateMetaDesc": "Met een sjabloon kunnen anderen dit gilde gebruiken als basis voor hun eigen gilden. De kanalen, rollen en instellingen van dit gilde worden gekopieerd, maar niet de berichten binnen het gilde, de robots of het gildepictogram.", + "topic:": "Onderwerp:", + "unknownRole": "@unknown-role", + "viewTemplate": "Sjabloon bekijken", + "yesDelete": "Ja, ik weet het zeker", + "yesLeave": "Ja, ik weet het zeker" + }, + "hideBlockedMessages": "U heeft deze gebruiker geblokkeerd. Klik om deze berichten te verbergen.", + "home": { + "icon": "Serverpictogram van $1", + "uptimeStats": "Beschikbaarheid: \nAlle tijden: $1%\nDeze week: $2%\nVandaag: $3%", + "warnOffiline": "Server is offline, kan geen verbinding maken" + }, + "htmlPages": { + "addBot": "Toevoegen aan gilde", + "alreadyHave": "Heeft u al een account?", + "box1Items": "Directe berichten|Ondersteuning voor reacties|Uitnodigingen|Van account wisselen|Gebruikersinstellingen|Ontwikkelaarsportaal|Robot-uitnodigingen|Ondersteuning voor vertaling", + "box1title": "Fermi is een Spacebar-compatibele cliënt die ernaar streeft om zo goed mogelijk te zijn met veel functies, waaronder:", + "box3description": "We stellen hulp altijd op prijs, of het nu gaat om het melden van bugs, het bijdragen van code of het verbeteren van typefouten.", + "box3title": "Aan Fermi bijdragen", + "compatableInstances": "Spacebar-compatibele servers:", + "createAccount": "Account aanmaken", + "dobField": "Geboortedatum:", + "emailField": "E-mail:", + "idpermissions": "Hiermee kan de robot:", + "instanceField": "Server:", + "loaddesc": "Dit zou niet lang moeten duren", + "loadingText": "Fermi wordt geladen", + "loginButton": "Aanmelden", + "noAccount": "Nog geen account?", + "openClient": "Cliënt openen", + "pw2Field": "Voer het wachtwoord opnieuw in:", + "pwField": "Wachtwoord:", + "switchaccounts": "Van account wisselen", + "trans": "Vertalen", + "transDesc": "U kunt helpen Fermi in uw eigen taal te vertalen!", + "transTitle": "Fermi vertalen", + "userField": "Gebruikersnaam:", + "welcomeJank": "Welkom bij Fermi" + }, + "incorrectURLS": "## Deze instantie heeft waarschijnlijk de verkeerde koppelingen verzonden.\n### Als u de eigenaar van de instantie bent, raadpleeg dan [hier](https://docs.spacebar.chat/setup/server/) onder *Verbinding maken vanaf externe machines* om het probleem op te lossen.\n Wilt u dat Fermi automatisch probeert deze fout te herstellen, zodat u verbinding kunt maken met de instantie?", + "instInfo": "Serverinformatie", + "instanceInfo": { + "contact": "Instantiebeheerders mailen", + "frontPage": "[Voorpagina]($1)", + "name": "Instantie-gegevens", + "publicUrl": "[Openbare koppeing]($1)", + "tosPage": "[Gebruiksvoorwaardenpagina]($1)" + }, + "instanceStats": { + "members": "Leden: $1", + "messages": "Berichten: $1", + "name": "Serverstatistieken: $1", + "servers": "Gilden: $1", + "users": "Geregistreerde gebruikers: $1" + }, + "interactions": { + "ephemeralDismiss": "Bericht verwijderen", + "failed": "Interactie mislukt", + "nonsence": "De server heeft een component van het type $1 verzonden dat niet bestaat", + "notImpl": "Fermi ondersteunt componenttype $1 nog niet", + "onlyYou": "Alleen jij kunt dit bericht zien", + "replyline": "Interactie gestart", + "started": "Interactie verwerken...", + "worked": "Interactie geslaagd" + }, + "invite": { + "accept": "Accepteren", + "alreadyJoined": "Al lid", + "channel:": "Kanaal:", + "createInvite": "Uitnodiging maken", + "createdAt": "Gemaakt op $1", + "expireAfter": "Verloopt na:", + "expires": "Verloopt: $1", + "forChannel": "Voor kanaal: $1", + "inviteLinkCode": "Uitnodigingskoppeling/-code", + "inviteMaker": "Uitnodigingsmaker", + "invitedBy": "U bent uitgenodigd door $1", + "joinUsing": "Lid worden van een gilde via een uitnodiging", + "loginOrCreateAccount": "Aanmelden of een account aanmaken ⇌", + "longInvitedBy": "$1 heeft u uitgenodigd om lid te worden van $2", + "maxUses": "Gebruikslimiet: $1", + "never": "Nooit", + "noAccount": "Maak een account aan om de uitnodiging te accepteren", + "notFound": "De uitnodiging kon niet gevonden worden", + "subtext": "bij $1 in $2", + "used": "$1 {{PLURAL:$1|keer}} gebruikt:" + }, + "inviteOptions": { + "12h": "12 uur", + "1d": "1 dag", + "1h": "1 uur", + "30d": "30 dagen", + "30m": "30 minuten", + "6h": "6 uur", + "7d": "7 dagen", + "limit": "$1 {{PLURAL:$1|keer}} gebruikt", + "never": "Nooit", + "noLimit": "Geen limiet", + "title": "Mensen uitnodigen" + }, + "jankInfo": "Cliëntinformatie", + "leaveGuild": "Gilde verlaten", + "leaving": "U verlaat Spacebar", + "loaded": "Geladen", + "localuser": { + "2faCode:": "Code:", + "2faCodeGive": "Uw TOTP-geheim is: $1. Hiermee wordt een 2FA-token van 6 cijfers gegenereerd met een geldigheidsduur van 30 seconden.", + "2faDisable": "Tweetrapsauthenticatie uitschakelen", + "2faEnable": "Tweetrapsauthenticatie inschakelen", + "CheckUpdate": "Controleren op updates", + "PasswordsNoMatch": "Wachtwoorden komen niet overeen", + "TOSURL": "Koppeling naar de gebruiksvoorwaarden:", + "VoiceWarning": "Weet u zeker dat u dit wilt inschakelen? Het is erg experimenteel en kan problemen opleveren. (Deze functie is er voor ontwikkelaars. Blijf er liever van af als u niet weet wat u doet.)", + "accentColor": "Thema-accentkleur:", + "accountSettings": "Accountinstellingen", + "addBot": "Robot toevoegen", + "addStatus": "Status toevoegen", + "advancedBot": "Robotinstellingen voor gevorderden", + "appName": "Toepassingsnaam:", + "areYouSureDelete": "Weet u zeker dat u uw account wilt verwijderen? Zo ja, voer dan de zin $1 in.", + "badCode": "Onjuiste code", + "badPassword": "Onjuist wachtwoord", + "botAvatar": "Robot-avatar:", + "botInviteCreate": "Robot-uitnodigingsmaker", + "botUsername": "Robot-gebruikersnaam:", + "changeDiscriminator": "Discriminator wijzigen", + "changeEmail": "E-mailadres wijzigen", + "changePassword": "Wachtwoord wijzigen", + "changeUsername": "Gebruikersnaam wijzigen", + "clearCache": "Cache wissen", + "confirmAddBot": "Weet u zeker dat u een robot aan deze toepassing wilt toevoegen? Er is geen weg terug.", + "confirmReset": "Weet u zeker dat u het robottoken opnieuw wilt instellen? De robot stopt met werken totdat u hem bijwerkt.", + "confuseNoBot": "Om een of andere reden heeft deze toepassing (nog) geen robot.", + "connections": "Verbindingen", + "createApp": "Toepassing aanmaken", + "customSound": "Aangepast geluid", + "customStatusWarn": "Spacebar ondersteunt nog geen aangepaste statusweergave. Hoewel de status wordt geaccepteerd, wordt er verder niets mee gedaan.", + "deleteAccount": "Account verwijderen", + "deleteAccountButton": "Account verwijderen", + "description": "Beschrijving:", + "devPortal": "Ontwikkelaarsportaal", + "disableConnection": "Deze verbinding is bij de server uitgeschakeld", + "editingBot": "Bewerken van de robot $1", + "install": "Installeren", + "installDesc": "Na installeren kunt u Fermi in een eigen venster openen en als aparte app gebruiken! U kunt Fermi ook gewoon in de webbrowser blijven gebruiken en dan werkt het precies hetzelfde.", + "installJank": "Fermi installeren", + "language": "Taal:", + "manageBot": "Robot beheren", + "manageInstance": "Instantie beheren", + "mustTypePhrase": "Om uw account te verwijderen moet u de zin typen", + "newDiscriminator": "Nieuwe discriminator:", + "newEmail:": "Nieuw e-mailadres", + "newPassword:": "Nieuw wachtwoord:", + "newUsername": "Nieuwe gebruikersnaam:", + "noToken": "Het token is niet bekend en kan dus helaas niet worden opgeslagen in localStorage", + "noUpdates": "Geen updates gevonden", + "notisound": "Meldingsgeluid:", + "oldPassword:": "Oud wachtwoord:", + "password:": "Wachtwoord", + "privacyPolcyURL": "Link privacybeleid:", + "appID": "Toepassing ID: $1", + "showSecret": "Clientgeheim weergeven", + "clientSecret": "Cliëntgeheim: $1", + "secret": "Geheim .", + "publicAvaliable": "De robot publiekelijk uitnodigbaar maken?", + "refreshPage": "Vernieuw de pagina om toe te passen", + "requireCode": "Codetoekenning vereisen om de robot uit te nodigen?", + "resetToken": "Token opnieuw instellen", + "saveToken": "Token lokaal opslaan", + "setUp2fa": "Tweetrapsauthenticatie instellen", + "setUp2faInstruction": "Kopieer dit geheim naar uw TOTP-app (Time-based One-Time Password) om uw 2FA aanmeldcode te genereren.", + "settings": "Instellingen", + "sillyDeleteConfirmPhrase": "Shrek is liefde, Shrek is leven", + "soundTooLarge": "Het geluid dat u probeerde te uploaden was te groot. Probeer het nog eens.", + "status": "Status", + "team:": "Team:", + "theme:": "Thema", + "themesAndSounds": "Thema's & geluiden", + "tokenDisplay": "Token: $1", + "trace": "Sporen", + "trusted": "Vertrouwde domeinen", + "trustedDesc": "Wanneer u via koppelingen op dit domein komt, wordt u ***niet*** om toestemming gevraagd om deze te openen, zoals bij andere koppelingen. Geef dit alleen aan domeinen die u vertrouwt, zoals'https://fermi.chat'.", + "updateSettings": "Instellingen bijwerken", + "updatesYay": "Updates gevonden!", + "userSettings": "Uw publieke profiel" + }, + "login": { + "allGood": "Alles goed", + "checking": "Server wordt gecontroleerd", + "enterPAgain": "Voer het nieuwe wachtwoord nogmaals in:", + "invalid": "Onjuiste instantie, probeer het nog eens", + "login": "Aanmelden", + "newPassword": "Nieuw wachtwoord:", + "pasteInfo": "Plak hier de herstel-link:", + "recover": "Wachtwoord vergeten?", + "recovery": "Wachtwoord vergeten", + "waiting": "Wacht totdat de instantie gecontroleerd is" + }, + "logout": { + "error": { + "cancel": "Annuleren", + "cont": "Toch doorgaan", + "desc": "Fermi heeft u niet kunnen afmelden,\n wilt u het account toch verwijderen?", + "title": "Er is een fout opgetreden bij het afmelden" + }, + "logout": "Afmelden" + }, + "manageInstance": { + "AreYouSureStop": "Weet u zeker dat u deze server wilt stoppen?", + "TokenFormats": { + "JSON": "In JSON-indeling", + "URLs": "Links om uit te nodigen", + "plain": "Eenvoudig" + }, + "clientURL": "Cliënt-link:", + "copy": "Kopiëren", + "count": "Aantal:", + "create": "Aanmaken", + "createTokens": "Registratie-tokens aanmaken", + "format": "Formaat:", + "genericType": "Generiek", + "length": "Lengte:", + "regType": "link-type registratie-token", + "stop": "Server stoppen" + }, + "media": { + "artist": "Artiest: $1", + "composer": "Componist: $1", + "download": "Mediabestand downloaden", + "length": "Lengte: $1 minuten en $2 seconden", + "loading": "Wordt geladen", + "moreInfo": "Meer informatie", + "notFound": "Mediabestand kon niet worden gevonden" + }, + "member": { + "ban": "$1 uit $2 verbannen", + "kick": "$1 uit $2 zetten", + "nick:": "Bijnaam:", + "persist": "Alleen gebruiker verwijderen:", + "reason:": "Reden:", + "timeout": "Time-out $1", + "timeoutTime": "Time-outduur:" + }, + "message": { + "messages": "$1 {{PLURAL:$1|bericht|berichten}} >", + "andMore": "$1 en meer!", + "attached": "bijlage verzonden", + "delete": "Bericht verwijderen", + "report": "Bericht rapporteren", + "deleted": "Verwijderd bericht", + "edit": "Bericht bewerken", + "edited": "(bewerkt)", + "fullMessage": "Volledig bericht:", + "pin": "$1 heeft een bericht vastgezet", + "thread": "$1 heeft een thread aangemaakt: $2", + "reactedWith": "Reageerde met $1", + "reactionAdd": "Reactie toevoegen", + "createThread": "Discussie aanmaken", + "threadOptions": "Thread-opties", + "reactions": "Reacties bekijken", + "reactionsTitle": "Reacties", + "retry": "Bericht met fout opnieuw verzenden", + "viewrest": "De rest bekijken" + }, + "report": { + "back": "Terug", + "next": "Volgende", + "cancel": "Annuleren", + "submit": "Opslaan", + "blockUser": "Gebruiker blokkeren", + "timeout": "Timeout voor lid", + "summary": "Rapportsamenvatting:" + }, + "messageLink": "Bericht in # $1", + "muteDuration": "Ontdempen in:", + "nevermind": "Laat maar", + "no": "Nee", + "noEmojiFont": "Emoji van het systeem gebruiken", + "noMessages": "Er zijn nog geen berichten, wees de eerste die iets zegt!", + "noPins": "Hier is voor u nog niets te zien", + "noStickers": "Dat is jammer, er zijn geen stickers die u kunt gebruiken!", + "notiClump": "U heeft $1 meldingen van $2 gekregen", + "notiVolume": "Meldingsvolume:", + "otherAt": "$1 om $2", + "permissions": { + "descriptions": { + "ADMINISTRATOR": "Geeft alle onderstaande machtigingen aan rolleden en omzeilt alle overschrijvingen van machtigingen. Alleen U en leden/bots die u volledig vertrouwt, zouden deze UITGEBREIDE machtiging mogen hebben!", + "MANAGE_GUILD": "Maakt het beheren en bewerken van het gilde mogelijk.", + "VIEW_AUDIT_LOG": "Hiermee kunnen rol-leden het inspectielogboek bekijken", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Laat rolleden inzichten over rolabonnementen bekijken.", + "VIEW_GUILD_INSIGHTS": "Hiermee kunnen rol-leden inzichten over het gilde bekijken", + "BYPASS_SLOWMODE": "De langzame modus heeft geen effect meer op rol-leden.", + "MENTION_EVERYONE": "Hiermee kunnen rol-leden iedereen (@everyone) vermelden", + "MANAGE_NICKNAMES": "Hiermee kunnen rol-leden de bijnamen van andere leden wijzigen.", + "PIN_MESSAGES": "Hiermee kunnen rol-leden berichten vastzetten.", + "SEND_POLLS": "Maakt het mogelijk om peilingen te versturen.", + "MANAGE_MESSAGES": "Hiermee kunnen rol-leden berichten van anderen verwijderen.", + "MANAGE_THREADS": "Hiermee kunnen rol-leden discussies verwijderen en archiveren en alle privédiscussies bekijken.", + "MANAGE_CHANNELS": "Hiermee kunnen rol-leden kanalen beheren en verwijderen.", + "MANAGE_ROLES": "Hiermee kunnen rol-leden rollen bewerken en verwijderen", + "MANAGE_WEBHOOKS": "Maakt beheren en verwijderen van webhooks mogelijk", + "CREATE_EVENTS": "Maakt het mogelijk geplande evenementen aan te maken.", + "MANAGE_EVENTS": "Laat rolleden evenementen bewerken en verwijderen (bestaand en toekomstig).", + "CREATE_GUILD_EXPRESSIONS": "Maakt het voor rol-leden mogelijk emoji’s, stickers en geluidspaneel-geluiden te maken.", + "MANAGE_GUILD_EXPRESSIONS": "Laat rolleden emoji’s, stickers en geluidspaneel-geluiden bewerken en verwijderen.", + "MODERATE_MEMBERS": "Hiermee kunnen rol-leden gebruikers een time-out geven (om zo voorkomen dat ze berichten in chat en discussies verzenden of erop reageren en dat ze in spraak- en podiumkanalen spreken).", + "KICK_MEMBERS": "Hiermee kunnen rol-leden leden uit het gilde zetten.", + "BAN_MEMBERS": "Hiermee kunnen rol-leden leden uit het gilde verbannen.", + "MOVE_MEMBERS": "Hiermee kunnen rol-leden leden tussen spraakkanalen verplaatsen", + "MUTE_MEMBERS": "Hiermee kunnen rol-leden andere leden dempen in spraakkanalen.", + "DEAFEN_MEMBERS": "Hiermee kunnen rol-leden andere leden in spraakkanalen niets laten horen (hierdoor zal ook die persoon automatisch niets horen).", + "CHANGE_NICKNAME": "Hiermee kunnen rol-leden de eigen bijnaam wijzigen.", + "VIEW_CHANNEL": "Hiermee kunnen rol-leden naar het tekstkanalen kijken.", + "READ_MESSAGE_HISTORY": "Hiermee kunnen rol-leden de berichtengeschiedenis en de tekstkanalen lezen.", + "ADD_REACTIONS": "Hiermee kunnen rol-leden reacties op berichten in tekstkanalen toevoegen.", + "SEND_MESSAGES": "Hiermee kunnen rol-leden berichten sturen in tekstkanalen.", + "SEND_MESSAGES_IN_THREADS": "Hiermee kunnen rol-leden berichten in discussies zetten.", + "SEND_TTS_MESSAGES": "Hiermee kunnen rol-leden tekst-naar-spraakberichten in tekstkanalen sturen", + "USE_EXTERNAL_EMOJIS": "Hiermee kunnen rol-leden externe emoji’s in tekstkanalen gebruiken.", + "USE_EXTERNAL_STICKERS": "Hiermee kunnen rolleden externe stickers gebruiken in tekstkanalen.", + "EMBED_LINKS": "Sta toe dat koppelingen die door rolleden worden verzonden, automatisch worden ingesloten in tekstkanalen.", + "ATTACH_FILES": "Hiermee kunnen rolleden bestanden bijvoegen in tekstkanalen.", + "SEND_VOICE_MESSAGES": "Maakt het mogelijk om spraakberichten te verzenden in tekstkanalen.", + "CREATE_INSTANT_INVITE": "Hiermee kunnen rolleden uitnodigingen voor het gilde aanmaken.", + "CREATE_PUBLIC_THREADS": "Hiermee kunnen rolleden openbaar overleg starten.", + "CREATE_PRIVATE_THREADS": "Hiermee kunnen rolleden privé-overleg aanmaken.", + "CONNECT": "Hiermee kunnen rolleden verbinding maken met spraakkanalen.", + "SPEAK": "Hiermee kunnen rolleden via spraakkanalen communiceren.", + "USE_VAD": "Hiermee kunnen rolleden in spraakkanalen spreken door simpelweg te praten (indien uitgeschakeld, kunnen ze alleen spreken via de Push-To-Talk-toets).", + "STREAM": "Hiermee kunnen rolleden streamen in spraakkanalen.", + "USE_SOUNDBOARD": "Hiermee kunnen rolleden een soundboard gebruiken in spraakkanalen.", + "USE_EXTERNAL_SOUNDS": "Hiermee kunnen rolleden soundboardgeluiden van andere gilden gebruiken in spraakkanalen.", + "PRIORITY_SPEAKER": "Laat rolleden prioriteitsspreker in spraakkanalen worden.", + "REQUEST_TO_SPEAK": "Laat rolleden een spreekverzoek indienen in de podiumkanalen.", + "USE_EMBEDDED_ACTIVITIES": "Hiermee kunnen rolleden ingebedde activiteiten gebruiken.", + "USE_APPLICATION_COMMANDS": "Hiermee kunnen rolleden toepassingsopdrachten gebruiken.", + "USE_EXTERNAL_APPS": "Hiermee kunnen rolleden hun toepassingenreacties openbaar in het kanaal weergeven. Indien uitgeschakeld, kunnen gebruikers hun apps nog steeds gebruiken, maar zijn de reacties alleen voor henzelf zichtbaar. Dit geldt alleen voor apps die niet ook in het gilde zijn geïnstalleerd." + }, + "readableNames": { + "ADD_REACTIONS": "Reacties toevoegen", + "ADMINISTRATOR": "Beheerder", + "ATTACH_FILES": "Bestanden bijvoegen", + "BAN_MEMBERS": "Leden verbannen", + "BYPASS_SLOWMODE": "Langzame modus omzeilen", + "CHANGE_NICKNAME": "Bijnaam wijzigen", + "CONNECT": "Verbinden", + "CREATE_EVENTS": "Evenementen maken", + "CREATE_GUILD_EXPRESSIONS": "Uitdrukkingen maken", + "CREATE_INSTANT_INVITE": "Uitnodiging maken", + "CREATE_PRIVATE_THREADS": "Privédiscussies maken", + "CREATE_PUBLIC_THREADS": "Openbare discussies maken", + "DEAFEN_MEMBERS": "Leden doof maken", + "EMBED_LINKS": "Koppelingen inbedden", + "KICK_MEMBERS": "Leden uitzetten", + "MANAGE_CHANNELS": "Kanalen beheren", + "MANAGE_EVENTS": "Evenementen beheren", + "MANAGE_GUILD": "Gilde beheren", + "MANAGE_GUILD_EXPRESSIONS": "Uitdrukkingen beheren", + "MANAGE_MESSAGES": "Berichten beheren", + "MANAGE_NICKNAMES": "Bijnamen beheren", + "MANAGE_ROLES": "Rollen beheren", + "MANAGE_THREADS": "Discussies beheren", + "MANAGE_WEBHOOKS": "Webhooks beheren", + "MENTION_EVERYONE": "@everyone, @here en alle rollen vermelden", + "MODERATE_MEMBERS": "Leden een time-out geven", + "MOVE_MEMBERS": "Leden verplaatsen", + "MUTE_MEMBERS": "Leden dempen", + "PIN_MESSAGES": "Berichten vastzetten", + "PRIORITY_SPEAKER": "Voorrangsspreker", + "READ_MESSAGE_HISTORY": "Berichtgeschiedenis lezen", + "REQUEST_TO_SPEAK": "Vragen om te spreken", + "SEND_MESSAGES": "Berichten sturen", + "SEND_MESSAGES_IN_THREADS": "Berichten in discussies verzenden", + "SEND_POLLS": "Peilingen maken", + "SEND_TTS_MESSAGES": "Tekst-naar-spraakberichten sturen", + "SEND_VOICE_MESSAGES": "Spraakberichten verzenden", + "SPEAK": "Spreken", + "STREAM": "Video", + "USE_APPLICATION_COMMANDS": "Toepassingsopdrachten gebruiken", + "USE_EMBEDDED_ACTIVITIES": "Activiteiten gebruiken", + "USE_EXTERNAL_APPS": "Externe apps gebruiken", + "USE_EXTERNAL_EMOJIS": "Externe emoji’s gebruiken", + "USE_EXTERNAL_SOUNDS": "Externe geluiden gebruiken", + "USE_EXTERNAL_STICKERS": "Externe stickers gebruiken", + "USE_SOUNDBOARD": "Geluidspaneel gebruiken", + "USE_VAD": "Spraakactiviteitsherkenning gebruiken", + "VIEW_AUDIT_LOG": "Inspectielogboek bekijken", + "VIEW_CHANNEL": "Kanalen bekijken", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Monetisatie-analyses voor makers bekijken", + "VIEW_GUILD_INSIGHTS": "Gilde-inzichten bekijken" + } + }, + "pinMessage": "Bericht vastzetten", + "profile": { + "bio": "Over mij:", + "joined": "Account aangemaakt: $1", + "joinedMember": "Toegetreden tot $1: $2", + "mut": "Onderlinge gilden", + "mutFriends": "Wederzijdse vrienden", + "permInfo": "Machtigingen", + "userInfo": "Gebruikersinfo" + }, + "profileColor": "Profielkleur", + "pronouns": "Voornaamwoorden:", + "readableName": "Nederlands", + "recentEmoji": "Recente emoji", + "register": { + "DOBError": "Geboortedatum: $1", + "agreeTOS": "Ik ga akkoord met de [Servicevoorwaarden]($1):", + "emailError": "E-mail: $1", + "noTOS": "Voor deze server zijn er geen servicevoorwaarden, accepteer ze toch maar:", + "passwordError:": "Wachtwoord: $1", + "register": "Inschrijven", + "tos": "U moet instemmen met de voorwaarden", + "usernameError": "Gebruikersnaam: $1" + }, + "remove": "Verwijderen", + "renderJoinAvatars": "Profielafbeeldingen weergeven voor nieuwe leden:", + "reply": "Antwoorden", + "replyingTo": "Reageren op $1", + "retrying": "Opnieuw proberen...", + "role": { + "color": "Eerste kleur", + "colors": { + "name": "Kleurenschema:", + "one": "Monochroom", + "two": "Tweekleurig", + "three": "Driekleurig", + "secondColor": "Tweede kleur:", + "thirdColor": "Derde kleur:" + }, + "confirmDelete": "Weet u zeker dat u $1 wilt verwijderen?", + "copyId": "Rol-ID kopiëren", + "delete": "Rol verwijderen", + "deleting": "Rol wordt verwijderd…", + "displaySettings": "Weergave-instellingen", + "hoisted": "Rolleden apart weergeven van online leden; ze worden weergegeven op basis van de hoogste rolgroep waartoe ze behoren:", + "mentionable": "Iedereen toestaan deze rol te pingen:", + "name": "Rolnaam:", + "perms": "Rechten", + "remove": "Rol intrekken", + "roleEmoji": "Rol-emoji:", + "roleFileIcon": "Rolpictogram:", + "roles": "Rollen" + }, + "search": { + "back": "Terug", + "new": "Nieuw", + "next": "Volgende", + "nofind": "Er zijn er geen berichten gevonden met uw zoekopdracht. Misschien vind u wat u zoekt als u de zoekopdracht verbreedt.", + "old": "Oud", + "page": "Pagina $1", + "search": "Zoeken" + }, + "searchGifs": "Zoeken in $1", + "settings": { + "clear": "Wissen", + "img": "Afbeelding uploaden", + "save": "Wijzigingen opslaan", + "unsaved": "Voorzichtig, u hebt niet-opgeslagen wijzigingen", + "updates": { + "serviceWorkerMode": { + "disabled": "Uit", + "enabled": "Aan", + "offlineOnly": "Alleen offline", + "title": "Service Worker-instelling", + "unregistered": "Uitgeschakeld (inclusief mediacache)" + } + } + }, + "showBlockedMessages": "U heeft deze gebruiker geblokkeerd. Klik om {{PLURAL:$1|het geblokkeerde bericht|de $1 geblokkeerde berichten}} te bekijken.", + "spoiler": "Spoiler", + "sticker": { + "confirmDel": "Weet u zeker dat u deze sticker wilt verwijderen?", + "del": "Sticker verwijderen", + "desc": "Beschrijving", + "errEmjMust": "U moet een emoji aan uw sticker koppelen", + "errFileMust": "Moet een afbeelding bij uw sticker voegen", + "image": "Afbeelding:", + "name": "Naam:", + "tags": "Gekoppelde emoji:", + "title": "Stickers", + "upload": "Stickers uploaden" + }, + "submit": "Opslaan", + "switchAccounts": "Van account wisselen ⇌", + "todayAt": "Vandaag om $1", + "trace": { + "totalTime": "$2: $1ms", + "traces": "$1 ($2ms) $3" + }, + "typing": "$2 {{PLURAL:$1|is|zijn}} aan het typen", + "unableToConnect": "Kan geen verbinding maken met de Spacebar-server. Probeer u af en weer aan te melden.", + "unableToPin": "Bericht kan niet worden vastgezet", + "unpinMessage": "Bericht losmaken", + "updateAv": "Updates beschikbaar", + "uploadBanner": "Banner uploaden:", + "uploadFilesText": "Upload hier uw bestanden!", + "uploadPfp": "Profielafbeelding uploaden:", + "useTemplate": "$1 als sjabloon gebruiken", + "useTemplateButton": "Sjabloon gebruiken", + "usedFermi": "verzonden via Fermi", + "user": { + "report": "Gebruiker rapporteren", + "deleted": "Gebruiker verwijderd", + "reportApp": "Toepassing rapporteren", + "addRole": "Rollen toevoegen", + "ban": "Lid verbannen", + "block": "Gebruiker blokkeren", + "confirmInstBan": "Weet u zeker dat u de server $1 wilt verbannen?", + "copyId": "Gebruikers-ID kopiëren", + "dnd": "Niet storen", + "editNick": "Bijnaam bewerken", + "editServerProfile": "Gildeprofiel bewerken", + "friendReq": "Vriendschapsverzoek", + "idle": "Inactief", + "instanceBan": "Serverban", + "invisible": "Onzichtbaar", + "kick": "Lid uitzetten", + "message": "Gebruiker bericht sturen", + "nick": { + "foe": "Vijandige bijnaam instellen", + "friend": "Vriendelijke bijnaam instellen", + "stalker": "Bijnaam van de aanvrager instellen", + "stalking": "Bijnaam van de gevraagde instellen" + }, + "offline": "Offline", + "online": "Online", + "remove": "Gebruiker verwijderen", + "removeRole": "Rollen verwijderen", + "sendMessage": "Bericht sturen aan @$1", + "timeout": "Time-out voor lid", + "unTimeout": "Time-out voor lid verwijderen", + "unban": "$1 ontbannen", + "unblock": "Gebruiker deblokkeren" + }, + "userping": { + "resolving": "gebruiken aan het opzoeken", + "unknown": "@onbekende-gebruiker" + }, + "vc": { + "joinForStream": "Word lid van het spraakkanaal om te kijken", + "joiningStream": "Verbinden met uitzending…", + "joinstream": "Uitzending bekijken", + "leavestream": "Uitzending verlaten", + "stopstream": "Uitzending stoppen" + }, + "webauth": { + "addKey": "Sleutel toevoegen", + "keyname": "Sleutelnaam:", + "manage": "Beveiligingssleutels beheren" + }, + "webhook": "WEBHOOK", + "webhooks": { + "EnterWebhookName": "Voer de webhook-naam in", + "areYouSureDelete": "Weet u zeker dat u $1 wilt verwijderen?", + "avatar": "Avatar", + "base": "Webhooks", + "channel": "Webhook-uitvoer naar kanaal:", + "copyURL": "Webhook-koppeling kopiëren", + "createdAt": "Aangemaakt op $1", + "createdBy": "Aangemaakt door:", + "deleteWebhook": "Webhook verwijderen", + "name": "Webhooknaam:", + "newWebHook": "Nieuwe webhook", + "notFound": "Gebruiker is niet meer in het gilde", + "sillyDefaults": "Kapitein Haak\nHaakje-de-voorste\nVan de haak op de taak\nHAAK 9000", + "token": "Webhook-token: `$1`", + "type": "Type: $1", + "type1": "Inkomend", + "type2": "Kanaalvolger", + "type3": "Toepassing", + "url": "Webhook-koppeling: `$1`" + }, + "welcomeMessages": "Welkom $1! Leuk dat je er bent!\nHoi $1, leuk je te ontmoeten!\nDag $1, schuif ook gezellig aan!\nKom erbij $1, heb je pizza bij je?", + "widget": "Gilde-widget", + "widgetEnabled": "Widget ingeschakeld", + "yes": "Ja", + "yesterdayAt": "Gisteren om $1" +} diff --git a/translations/pl.json b/translations/pl.json new file mode 100644 index 00000000..06c654a1 --- /dev/null +++ b/translations/pl.json @@ -0,0 +1,888 @@ +{ + "@metadata": { + "authors": [ + "Atvalerie", + "Woytecr" + ] + }, + "2faCode": "Kod 2FA:", + "404": { + "404": "404 Strona nie znaleziona", + "app": "Do aplikacji", + "blog": "Blog Fermi", + "home": "Strona główna", + "listtitle": "Może miałeś zamiar wybrać się do jednego z tych miejsc", + "login": "Zaloguj się", + "report": "Jeżeli znalazłeś tę stronę w kliencie, zgłoś ją:", + "reset": "Strona resetowania hasła", + "title": "Wygląda na to, że się zgubiłeś", + "whatelse": "Co jeszcze Twoim zdaniem powinno się wydarzyć?", + "whereever": "Gdziekolwiek to jest" + }, + "DMs": { + "add": "Dodaj kogoś do tej wiadomości prywatnej", + "close": "Zamknij prywatną wiadomość", + "copyId": "Kopiuj identyfikator DM", + "markRead": "Oznacz jako przeczytane", + "name": "Prywatne Wiadomości" + }, + "UrlGen": "Generator adresów URL", + "Voice": { + "status": { + "badWS": "Zła odpowiedź od WS", + "conectionFailed": "Połączenie nie powiodło się", + "done": "Połączony", + "left": "Opuszczono voice chat", + "makingOffer": "Składanie oferty", + "noSDP": "Brak SDP", + "notconnected": "Nie połączono", + "sendingStreams": "Wysyłanie strumieni audio", + "startingRTC": "Rozpoczęcie połączenia RTC", + "waitingMainWS": "Oczekiwanie na głowny WS", + "waitingURL": "Oczekiwanie na adres URL głosu", + "wsAuth": "oczekiwanie na autoryzację WS", + "wsOpen": "Oczekiwanie na otwarcie WS" + } + }, + "accessibility": { + "gifSettings": { + "always": "Zawsze", + "hover": "Po najechaniu myszą", + "never": "Nigdy" + }, + "name": "Dostępność", + "playGif": "Odtwarzaj pliki GIF na:", + "playIcon": "Odtwarzaj animowane ikony na:", + "roleColors": "Wyłącz kolory ról" + }, + "accountNotStart": "Nie można uruchomić konta", + "add": "Dodaj", + "applications": { + "delete": "Usuń aplikację", + "sure": "Czy na pewno chcesz usunąć $1?" + }, + "badge": { + "active_developer": "Aktywny programista", + "application_guild_subscription": "Ma premium", + "automod": "Używa automoda", + "bot_commands": "Wspiera polecenia", + "bug_hunter_level_1": "Łowca błędów poziom 1", + "bug_hunter_level_2": "Łowca błędów poziom 2", + "certified_moderator": "Moderator", + "early_supporter": "Wczesny wspierający", + "guild_booster_lvl1": "Wzmocniona gildia", + "guild_booster_lvl2": "Wzmocniona gildia", + "guild_booster_lvl3": "Wzmocniona gildia", + "guild_booster_lvl4": "Wzmocniona gildia", + "guild_booster_lvl5": "Wzmocniona gildia", + "guild_booster_lvl6": "Wzmocniona gildia", + "guild_booster_lvl7": "Wzmocniona gildia", + "guild_booster_lvl8": "Wzmocniona gildia", + "guild_booster_lvl9": "Wzmocniona gildia przez długi czas", + "hypesquad": "Rzecz chroniona prawem autorskim", + "hypesquad_house_1": "Bravery", + "hypesquad_house_2": "Brilliance", + "hypesquad_house_3": "Balance", + "legacy_username": "Ma starszą nazwę użytkownika", + "partner": "Partner instancji", + "premium": "Premium", + "quest_completed": "wykonał zadanie", + "staff": "Administratorzy instancji", + "verified_developer": "Zweryfikowany programista" + }, + "bio": "O mnie:", + "blankMessage": "Pusta wiadomość", + "blog": { + "blog": "Blog", + "blogUpdates": "Aktualizacje bloga:", + "desc": "Przeczytaj lub subskrybuj blog, aby uzyskać informacje na temat Fermi!", + "fermi": "Blog Fermi", + "gotoPost": "Przejdź do postu", + "wantUpdates": "Czy chcesz otrzymywać cotygodniowe aktualizacje blogowe na temat stanu Fermi?" + }, + "bot": "BOT", + "botGuilds": "Gildie, w których bot się znajduje:", + "botSettings": "Ustawienia botów", + "cancel": "Anuluj", + "channel": { + "SlowmodeCool": "Czas Spowolnienia: $1", + "TimeOutCool": "W przerwie do: $1", + "allowIcons": "Zezwalaj na niestandardowe ikony kanałów", + "announcement": "Ogłoszeniowy", + "copyId": "Kopiuj identyfikator kanału", + "copyIdCat": "Kopiuj identyfikator kategorii", + "createCatagory": "Utwórz kategorię", + "createChannel": "Utwórz kanał", + "creating": "Tworzenie kanału", + "delete": "Usuń kanał", + "deleteCat": "Usuń kategorię", + "icon": "Ikona:", + "makeInvite": "Zaproś", + "markRead": "Oznacz jako przeczytane", + "mute": "Wycisz kanał", + "name": "Kanał", + "name:": "Nazwa:", + "nsfw:": "NSFW:", + "permissions": "Uprawnienia", + "selectCatName": "Nazwa kategorii", + "selectName": "Nazwa kanału", + "selectType": "Wybierz typ kanału", + "settings": "Ustawienia", + "settingsFor": "Ustawienia dla $1", + "slowmode": "Tryb powolny:", + "text": "Tekstowy", + "timedOutUntil": "W przerwie do: $1", + "topic:": "Temat:", + "typebox": "Wiadomość w $1", + "unmute": "Wyłącz wyciszenie kanału", + "voice": "Głosowy" + }, + "channelLink": "# $1", + "clientDesc": "Wersja klienta: $1\n\n[Dołącz do oficjalnej gildii Fermi]($2/invite/USgYJo?instance=https%3A%2F%2Fspacebar.chat)\n\n[Pomóż w tłumaczeniu Fermi](https://translatewiki.net/wiki/Translating:JankClient#sortable:3=desc)\n\n[Pomóż w tworzeniu Fermi](https://github.com/MathMan05/Fermi)\n\n[Pomóż w utrzymaniu serwera, na którym opiera się Fermi](https://github.com/spacebarchat/server)\n\n[Czytaj blog](https://blog.fermi.chat/)\n\nObliczone uprawnienia: $3", + "commands": { + "errorNotValid": "$1 nie jest prawidłowym wyborem dla $2", + "required": "$1 jest wymaganą częścią tej komendy" + }, + "confirmGuildLeave": "Czy na pewno chcesz opuścić $1", + "copyLink": "Kopiuj link do wiadomości", + "copymessageid": "Kopiuj identyfikator wiadomości", + "copyrawtext": "Kopiuj surowy tekst", + "createAccount": "Utwórz konto", + "delete": "Usuń", + "deleteConfirm": "Czy na pewno chcesz to usunąć?", + "devSettings": { + "badUser": "Włączono logowanie złych obiektów użytkowników, które wysyłają zbyt dużo informacji:", + "cache": "Włącz buforowanie plików map przy użyciu Service Workera:", + "cacheDesc": "pliki map zostaną załadowane w każdym razie, to tylko upewni się, że są w pamięci podręcznej, gdy pojawi się nowa aktualizacja.", + "captureTrace": "To ustawienie mówi Fermi aby przechwytywał właściwości _trace z serwera, włączenie tej opcji może spowodować, że progresywne dekodowanie JSON nie będzie działać (może to wymagać ponownego załadowania).", + "clearWellKnowns": "Pobierz ponownie rekordy .well-known", + "description": "Są one przeznaczone dla programistów Spacebar/Fermi. Jeśli nie wiesz, co robisz, nie zmieniaj tych ustawień.", + "gatewayComp": "Wyłącz kompresję bramy", + "reportSystem": "Włącz eksperymentalny system zgłaszania", + "logGateway": "Log odbieranych zdarzeń bramy (informacje na poziomie logów):", + "name": "Ustawienia programisty", + "traces": "Ujawnij ślady:" + }, + "deviceManage": { + "city": "Miasto: $1", + "continent": "Kontynent: $1", + "country": "Kraj: $1", + "curSes": "To jest obecna sesja, musisz się wylogować za pomocą innego menu", + "estimateWarn": "Uwaga: Wszystkie te informacje są jedynie przypuszczeniami i mogą być nieprawdziwe.", + "ip": "Ostatni znany adres IP: $1", + "last": "Przybliżone ostatnie użycie: $1", + "latitude": "Szerokość geograficzna: $1", + "logout": "Wyloguj", + "longitude": "Długość geograficzna: $1", + "manageDev": "Zarządzaj urządzeniem", + "postal": "Poczta: $1", + "region": "Region: $1", + "title": "Zarządzaj sesjami" + }, + "discovery": "Odkrywanie", + "dms": "Prywatne Wiadomości", + "edit": "Edytuj", + "emoji": { + "confirmDel": "Czy na pewno chcesz usunąć tę emoji?", + "found": { + "not": "Ta emoji nie pochodzi z żadnej z Twoich gildii. Aby móc używać tej emoji, musisz do niej dołączyć.", + "other": "Ta emoji pochodzi z jednej z Twoich innych gildii.", + "private": "Nie można znaleźć gildii, z której pochodzi to emoji", + "this": "Ta emoji pochodzi z tej gildii." + }, + "from": "Ta emoji pochodzi z", + "image:": "Zdjęcie:", + "join": "Dołącz do gildii", + "name:": "Nazwa:", + "privateGuild": "Ta gildia jest prywatna", + "title": "Emoji", + "upload": "Wrzuć emoji" + }, + "emojiSelect": "Emoji:", + "errorReconnect": "Nie można połączyć się z serwerem, ponowna próba za **$1** sekund...", + "favoriteGifs": "Ulubione Gify", + "folder": { + "color": "Kolor folderu:", + "create": "Utwórz nowy folder", + "edit": "Edytuj folder", + "name": "Nazwa folderu:" + }, + "form": { + "captcha": "Poczekaj, czy jesteś człowiekiem?" + }, + "friends": { + "addfriend": "Dodaj znajomego", + "addfriendpromt": "Dodaj znajomego za pomocą nazwy użytkownika:", + "all": "Wszyscy", + "all:": "Wszyscy znajomi:", + "blocked": "Zablokowani", + "blockedusers": "Zablokowani użytkownicy:", + "bu": "Zablokowany użytkownik", + "discnotfound": "Dyskryminator nie został znaleziony", + "friendlist": "Lista znajomych", + "friends": "Znajomi", + "notfound": "Użytkownik nie został znaleziony", + "online": "Dostępni", + "online:": "Dostępni znajomi:", + "pending": "Oczekujące", + "pending:": "Oczekujące zaproszenia do znajomych:", + "removeFriend": "Usuń znajomego" + }, + "goThere": "Przejdź tam", + "goThereTrust": "Przejdź tam i zaufaj w przyszłości", + "goingToURL": "Przechodzisz do $1. Czy na pewno chcesz tam przejść?", + "group": { + "createdm": "Utwórz DM!", + "edit": "Edytuj czat grupowy", + "select": "Wybierz znajomych" + }, + "guild": { + "COMMUNITY": "Prośba o dołączenie", + "DISCOVERABLE": "Odkryciem", + "INVITES_DISABLED": "Tylko z zaproszeniem", + "adminMenu": { + "changePerms": "Zmień uprawnienia, aby znaleźć", + "finding": "Znajdowanie administratorów", + "ownName": "Właściciel", + "owner": "Znajdź właściciela", + "permission": "Uprawnienia:" + }, + "report": "Zgłoś Gildię", + "admins": "Znajdź administratorów", + "all": "wszystkie", + "banReason": "Powód bana: $1", + "bannedBy": "Zbanowany przez:", + "banner:": "Baner:", + "bans": "Bany", + "boostMessage?": "Wyślij wiadomość, gdy ktoś wzmocni Twoją gildię!", + "community": "Społeczność", + "confirmDelete": "Czy na pewno chcesz usunąć $1?", + "confirmLeave": "Czy na pewno chcesz wyjść?", + "copyId": "Kopiuj identyfikator gildii", + "create": "Utwórz gildię", + "createFromTemplate": "Gildia z szablonu", + "createNewTemplate": "Utwórz nowy szablon", + "creating": "Tworzenie gildii", + "default": "Domyślnie ($1)", + "defaultNoti": "Ustaw domyślne ustawienia powiadomień dla swojej gildii!", + "delete": "Usuń gildię", + "description:": "Opis:", + "disoveryTitle": "Odkrywanie gildii ($1 {{PLURAL:$1|gildia|gildie|gildii}})", + "editingTemplate": "Edytowanie $1", + "emptytext": "Jesteś w dziwnym miejscu, ta gildia nie ma kanałów", + "emptytitle": "Dziwne miejsce", + "guilds": "Gildie", + "helpTips?": "Wyślij pomocne wskazówki dla swojej gildii!", + "howJoin": "W jaki sposób ludzie mogą dołączyć do twojej gildii?", + "icon:": "Ikona:", + "invites": "Zaproszenia", + "joinConfirm": "Dołącz do $1?", + "leave": "Opuść gildię", + "loadingDiscovery": "Ładowanie...", + "makeInvite": "Zaproś", + "markRead": "Oznacz jako przeczytane", + "memberCount": "$1 {{PLURAL:$1|członek|członków}}", + "mute": "Wycisz gildię", + "name:": "Nazwa:", + "nameNoMatch": "Nazwy nie pasują", + "noDelete": "Nieważne", + "noLeave": "Nieważne", + "none": "brak", + "notifications": "Powiadomienia", + "onlyMentions": "tylko wzmianki", + "overview": "Przegląd", + "region:": "Region:", + "roles": "Role", + "ruleId": "Kanał z zasadami:", + "selectnoti": "Wybierz typ powiadomień", + "sendrandomwelcome?": "Wyślij losową wiadomość, gdy ktoś dołączy do tej gildii", + "serverName": "Nazwa gildii:", + "settings": "Ustawienia", + "settingsFor": "Ustawienia dla $1", + "splash": "Baner odkrywania", + "stickWelcomeReact?": "Zachęć członków swojej gildii do zareagowania naklejką, gdy ktoś dołączy!", + "systemSelect:": "Kanał wiadomości systemowych:", + "tempCreatedBy": "Szablon stworzony przez:", + "tempUseCount": "Szablon został użyty $1 {{PLURAL:$1|raz|razy}}", + "template": "Szablon:", + "templateDesc": "Opis szablonu:", + "templateName": "Nazwa szablonu:", + "templateNameShort": "Nazwa szablonu musi mieć co najmniej 2 znaki", + "templateURL": "Adres URL szablonu: $1", + "templates": "Szablony", + "templcateMetaDesc": "Szablon pozwala innym użytkownikom na użycie tej gildii jako podstawy dla własnych gildii. Skopiuje on kanały, role i ustawienia gildii, ale nie wiadomości, botów ani ikony gildii.", + "topic:": "Temat:", + "unknownRole": "@nieznana-rola", + "viewTemplate": "Wyświetl szablon", + "yesDelete": "Tak, jestem pewien", + "yesLeave": "Tak, jestem pewien" + }, + "hideBlockedMessages": "Zablokowany użytkownik, kliknij, aby ukryć wiadomości.", + "home": { + "icon": "Ikona instancji $1", + "uptimeStats": "Czas działania:\n Cały czas: $1%\nW tym tygodniu: $2%\nDzisiaj: $3%", + "warnOffiline": "Instancja jest offline, nie można się połączyć" + }, + "htmlPages": { + "addBot": "Dodaj do gildii", + "alreadyHave": "Posiadasz już konto?", + "box1Items": "Wiadomości|Obsługa reakcji|Zaproszenia|Zmiana kont|Ustawienia użytkownika|Portal programisty|Zaproszenia botów|Obsługa tłumaczeń", + "box1title": "Fermi to klient kompatybilny z Spacebar, który stara się być tak dobry, jak to tylko możliwe, oferując wiele funkcji, w tym:", + "box3description": "Zawsze doceniamy pomoc, niezależnie czy jest ona w postaci zgłoszeń błędów, kodu, czy nawet wskazania literówek.", + "box3title": "Współtwórz Fermi", + "compatableInstances": "Instancje kompatybilne z Spacebarem:", + "createAccount": "Utwórz konto", + "dobField": "Data urodzenia:", + "emailField": "Adres e-mail:", + "idpermissions": "Umożliwi to botowi:", + "instanceField": "Instancja:", + "loaddesc": "To nie powinno zająć dużo czasu", + "loadingText": "Fermi się ładuje", + "loginButton": "Zaloguj się", + "noAccount": "Nie masz konta?", + "openClient": "Otwórz klient", + "pw2Field": "Wprowadź hasło ponownie:", + "pwField": "Hasło:", + "switchaccounts": "Przełącz konta", + "trans": "Przetłumacz", + "transDesc": "Możesz pomóc w tłumaczeniu Fermi na swój język!", + "transTitle": "Przetłumacz Fermi", + "userField": "Nazwa użytkownika:", + "welcomeJank": "Witaj w Fermi" + }, + "incorrectURLS": "## Ta instancja prawdopodobnie wysłała niepoprawne adresy URL.\n### Jeśli jesteś właścicielem instancji proszę zapoznaj się z [tym](https://docs.spacebar.chat/setup/server/) pod *Connecting from remote machines* aby rozwiązać problem.\n Czy chcesz aby Fermi automatycznie spróbował naprawić ten błąd, aby umożliwić Ci połączenie się z instancją?", + "instInfo": "Informacje o instancji", + "instanceInfo": { + "contact": "Wyślij maila do administratorów instancji", + "frontPage": "[Strona główna]($1)", + "name": "Informacje o instancji", + "publicUrl": "[Publiczny adres URL]($1)", + "tosPage": "[Strona TOS]($1)" + }, + "instanceStats": { + "members": "Członkowie: $1", + "messages": "Wiadomości: $1", + "name": "Statystyki instancji: $1", + "servers": "Gildie: $1", + "users": "Zarejestrowani użytkownicy: $1" + }, + "interactions": { + "ephemeralDismiss": "Ukryj wiadomość", + "failed": "Interakcja nie powiodła się", + "nonsence": "Serwer wysłał komponent typu $1, który nie istnieje", + "notImpl": "Fermi nie zaimplementował jeszcze typu komponentu $1", + "onlyYou": "Tylko Ty możesz zobaczyć tę wiadomość", + "replyline": "Rozpoczęto interakcję", + "started": "Przetwarzanie interakcji...", + "worked": "Interakcja powiodła się" + }, + "invite": { + "accept": "Akceptuj", + "alreadyJoined": "Już dołączyłeś", + "channel:": "Kanał:", + "createInvite": "Stwórz zaproszenie", + "createdAt": "Utworzono w $1", + "expireAfter": "Wygaśnięcie po:", + "expires": "Wygasa: $1", + "forChannel": "Dla kanału: $1", + "inviteLinkCode": "Link/kod zaproszenia", + "inviteMaker": "Twórca zaproszeń", + "invitedBy": "Zostałeś zaproszony przez $1", + "joinUsing": "Dołącz za pomocą zaproszenia", + "loginOrCreateAccount": "Zaloguj się lub stwórz nowe konto ⇌", + "longInvitedBy": "$1 zaprosił cię do dołączenia do $2", + "maxUses": "Maksymalna liczba użyć: $1", + "never": "Nigdy", + "noAccount": "Utwórz konto, aby przyjąć zaproszenie", + "notFound": "Nie znaleziono zaproszenia", + "subtext": "do $1 w $2", + "used": "Użyte $1 {{PLURAL:$1|raz|razy}}" + }, + "inviteOptions": { + "12h": "12 godzin", + "1d": "1 dzień", + "1h": "1 godzina", + "30d": "30 dni", + "30m": "30 minut", + "6h": "6 godzin", + "7d": "7 dni", + "limit": "$1 {{PLURAL:$1|użycie|użycia|użyć}}", + "never": "Nigdy", + "noLimit": "Bez limitu", + "title": "Zaproś ludzi" + }, + "jankInfo": "Informacje o kliencie", + "leaveGuild": "Opuść Gildię", + "leaving": "Opuszczasz Spacebar", + "loaded": "Załadowano", + "localuser": { + "2faCode:": "Kod:", + "2faCodeGive": "Twój sekret to: $1, ma 6 cyfr i jest ważny przez 30 sekund", + "2faDisable": "Wyłącz 2FA", + "2faEnable": "Włącz 2FA", + "CheckUpdate": "Sprawdź aktualizacje", + "PasswordsNoMatch": "Hasła nie są identyczne", + "TOSURL": "Adres URL Warunków korzystania z usługi:", + "VoiceWarning": "Czy na pewno chcesz to włączyć? Ta funkcja jest bardzo eksperymentalna i może spowodować problemy. (ta funkcja jest przeznaczona dla programistów, nie włączaj jej, jeśli nie wiesz, co robisz)", + "accentColor": "Kolor akcentowy:", + "accountSettings": "Ustawienia konta", + "addBot": "Dodaj bota", + "addStatus": "Dodaj status", + "advancedBot": "Zaawansowane ustawienia bota", + "appName": "Nazwa aplikacji:", + "areYouSureDelete": "Czy na pewno chcesz usunąć konto? Jeżeli tak, wprowadź frazę $1", + "badCode": "Nieprawidłowy kod", + "badPassword": "Nieprawidłowe hasło", + "botAvatar": "Awatar bota:", + "botInviteCreate": "Twórca zaproszeń bota", + "botUsername": "Nazwa użytkownika bota:", + "changeDiscriminator": "Zmiana dyskryminatora", + "changeEmail": "Zmień e-mail", + "changePassword": "Zmień hasło", + "changeUsername": "Zmiana nazwy użytkownika", + "clearCache": "Wyczyść pamięć podręczną", + "confirmAddBot": "Czy na pewno chcesz dodać bota do tej aplikacji? Tego nie da się cofnąć.", + "confirmReset": "Czy na pewno chcesz zresetować token bota? Twój bot przestanie działać, dopóki go nie zaktualizujesz.", + "confuseNoBot": "Z jakiegoś powodu ta aplikacja nie ma bota (jeszcze).", + "connections": "Połączenia", + "createApp": "Utwórz aplikację", + "customSound": "Dźwięk niestandardowy", + "customStatusWarn": "W tej chwili Spacebar nie obsługuje wyświetlania niestandardowych statusów, więc choć go zaakceptuje, nic z nim nie zrobi", + "deleteAccount": "Usunięcie konta", + "deleteAccountButton": "Usuń konto", + "description": "Opis:", + "devPortal": "Portal programistów", + "disableConnection": "To połączenie zostało wyłączone po stronie serwera", + "editingBot": "Edytowanie bota $1", + "install": "Instaluj", + "installDesc": "Zainstalowanie Fermi pozwoli ci na otworzenie go w jego własnym oknie oraz będzie on działać jak osobna aplikacja! Możesz też po prostu korzystać z Fermi w przeglądarce, tak jak dotychczas, i będzie działać tak samo.", + "installJank": "Zainstaluj Fermi", + "language": "Język:", + "manageBot": "Zarządzaj botem", + "manageInstance": "Zarządzanie instancją", + "mustTypePhrase": "Aby usunąć konto musisz wpisać frazę", + "newDiscriminator": "Nowy dyskryminator:", + "newEmail:": "Nowy e-mail", + "newPassword:": "Nowe hasło:", + "newUsername": "Nowa nazwa użytkownika:", + "noToken": "Nie znam tokenu więc nie mogę go zapisać do localStorage, przepraszam", + "noUpdates": "Nie znaleziono aktualizacji", + "notisound": "Dźwięk powiadomienia:", + "oldPassword:": "Stare hasło:", + "password:": "Hasło", + "privacyPolcyURL": "URL polityki prywatności:", + "appID": "Identyfikator aplikacji: $1", + "showSecret": "Pokaż sekret klienta", + "clientSecret": "Sekret klienta: $1", + "secret": "Sekret", + "publicAvaliable": "Czy bot może być publicznie zapraszany?", + "refreshPage": "Odśwież, aby zastosować", + "requireCode": "Czy zaproszenie bota wymaga przyznania kodu?", + "resetToken": "Resetuj token", + "saveToken": "Zapisz token do localStorage", + "setUp2fa": "Konfiguracja 2FA", + "setUp2faInstruction": "Skopiuj ten sekret do swojej aplikacji TOTP (time-based one time password)", + "settings": "Ustawienia", + "sillyDeleteConfirmPhrase": "Shrek to milosc, Shrek to zycie", + "soundTooLarge": "Dźwięk, który próbowałeś przesłać, był za duży, spróbuj ponownie", + "status": "Status", + "team:": "Zespół:", + "theme:": "Motyw", + "themesAndSounds": "Motywy i dźwięki", + "tokenDisplay": "Token: $1", + "trace": "Ślady", + "trusted": "Zaufane domeny", + "trustedDesc": "Te domeny, gdy klikniesz na linki z nich, ***nie*** poproszą cię o pozwolenie na otwarcie jak inne linki, dawaj to tylko adresom URL którym ufasz", + "updateSettings": "Ustawienia aktualizacji", + "updatesYay": "Znaleziono aktualizacje!", + "userSettings": "Ustawienia użytkownika" + }, + "login": { + "allGood": "Wszystko w porządku", + "checking": "Sprawdzanie instancji", + "enterPAgain": "Wprowadź ponownie nowe hasło:", + "invalid": "Nieprawidłowa instancja, spróbuj ponownie", + "login": "Zaloguj się", + "newPassword": "Nowe hasło:", + "pasteInfo": "Wklej adres URL odzyskiwania tutaj:", + "recover": "Nie pamiętasz hasła?", + "recovery": "Zapomniane hasło", + "waiting": "Czekanie aby sprawdzić instancję" + }, + "logout": { + "error": { + "cancel": "Anuluj", + "cont": "Kontynuuj mimo to", + "desc": "Fermi nie mógł cię wylogować\n Czy mimo wszystko chcesz usunąć konto?", + "title": "Wystąpił błąd podczas próby wylogowania" + }, + "logout": "Wyloguj się" + }, + "manageInstance": { + "AreYouSureStop": "Czy na pewno chcesz zatrzymać instancję?", + "TokenFormats": { + "JSON": "W formie JSON", + "URLs": "Adresy URL zaproszeń", + "plain": "Zwykły tekst" + }, + "clientURL": "URL klienta:", + "copy": "Kopiuj", + "count": "Ilość:", + "create": "Utwórz", + "createTokens": "Stwórz tokeny rejestracyjne", + "format": "Format:", + "genericType": "Ogólny", + "length": "Długość:", + "regType": "Typ adresu URL tokenu rejestracyjnego", + "stop": "Zatrzymaj instancję" + }, + "media": { + "artist": "Artysta: $1", + "composer": "Kompozytor: $1", + "download": "Pobierz multimedia", + "length": "Długość: $1 minut i $2 sekund", + "loading": "Ładowanie", + "moreInfo": "Więcej informacji", + "notFound": "Nie znaleziono multimediów" + }, + "member": { + "ban": "Zbanuj $1 z $2", + "kick": "Wyrzuć $1 z $2", + "nick:": "Pseudonim:", + "persist": "Usuń tylko użytkownika:", + "reason:": "Powód:", + "timeout": "Przerwa $1", + "timeoutTime": "Czas przerwy:" + }, + "message": { + "andMore": "$1, i więcej!", + "attached": "Wysłano załącznik", + "delete": "Usuń wiadomość", + "report": "Zgłoś wiadomość", + "deleted": "Usunięta wiadomość", + "edit": "Edytuj wiadomość", + "edited": "(edytowane)", + "fullMessage": "Pełna wiadomość:", + "pin": "$1 przypiął wiadomość", + "reactedWith": "Zareagował z $1", + "reactionAdd": "Dodaj reakcję", + "reactions": "Wyświetl reakcje", + "reactionsTitle": "Reakcje", + "retry": "Wyślij ponownie błędną wiadomość", + "viewrest": "Wyświetl resztę" + }, + "report": { + "back": "Wstecz", + "next": "Dalej", + "cancel": "Anuluj", + "submit": "Wyślij", + "blockUser": "Zablokuj użytkownika", + "timeout": "Wyślij członka na przerwę", + "summary": "Podsumowanie zgłoszenia:" + }, + "messageLink": "Wiadomość w # $1", + "muteDuration": "Koniec wyciszenia za:", + "nevermind": "Nieważne", + "no": "Nie", + "noEmojiFont": "Użyj emoji systemowych", + "noMessages": "Wygląda na to, że nie ma tu żadnych wiadomości, bądź pierwszy, który coś powie!", + "noPins": "Wygląda na to, że jeszcze nic tu nie ma...", + "noStickers": "Wygląda na to, że jesteś w dziwnym miejscu, nie ma żadnych naklejek, których mógłbyś użyć!", + "notiClump": "Otrzymałeś $1 powiadomień od $2", + "notiVolume": "Głośność powiadomień:", + "otherAt": "$1 o $2", + "permissions": { + "descriptions": { + "ADMINISTRATOR": "Nadaje wszystkie uprawnienia i pomija ograniczenia kanałów. Nadanie tego uprawnienia może być ryzykowne!", + "MANAGE_GUILD": "Pozwala na zarządzanie i edytowanie ustawień gildii", + "VIEW_AUDIT_LOG": "Pozwala użytkownikowi na przeglądanie rejestru zmian", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Pozwala na wyświetlanie statystyk subskrypcji ról", + "VIEW_GUILD_INSIGHTS": "Pozwala użytkownikowi przeglądać statystyki gildii", + "BYPASS_SLOWMODE": "Tryb powolny nie ma już wpływu na tego użytkownika", + "MENTION_EVERYONE": "Pozwala użytkownikowi na wspomnienie wszystkich", + "MANAGE_NICKNAMES": "Pozwala użytkownikowi na zmianę pseudonimów innych członków", + "PIN_MESSAGES": "Umożliwia użytkownikowi przypinanie wiadomości", + "SEND_POLLS": "Pozwala na wysyłanie ankiet", + "MANAGE_MESSAGES": "Pozwala użytkownikowi na usuwanie cudzych wiadomości", + "MANAGE_THREADS": "Pozwala użytkownikowi na usuwanie i archiwizowanie wątków oraz przeglądanie wszystkich prywatnych wątków", + "MANAGE_CHANNELS": "Pozwala użytkownikowi na zarządzanie i edytowanie kanałów", + "MANAGE_ROLES": "Pozwala użytkownikowi na edytowanie i zarządzanie rolami", + "MANAGE_WEBHOOKS": "Pozwala na zarządzanie i edytowanie webhooków", + "CREATE_EVENTS": "Pozwala na tworzenie zaplanowanych wydarzeń oraz edycję i usuwanie tych utworzonych przez użytkownika.", + "MANAGE_EVENTS": "Pozwala użytkownikowi na zarządzanie i edytowanie wydarzeniami", + "CREATE_GUILD_EXPRESSIONS": "Pozwala na tworzenie emoji, naklejek i dźwięków oraz edytowanie i usuwanie tych utworzonych przez użytkownika.", + "MANAGE_GUILD_EXPRESSIONS": "Pozwala na zarządzanie emoji, naklejkami i dźwiękami", + "MODERATE_MEMBERS": "Pozwala użytkownikowi wyciszać innych użytkowników czasowo, aby uniemożliwić im wysyłanie wiadomości, reagowanie na nie oraz mówienie w kanałach głosowych i scenicznych", + "KICK_MEMBERS": "Pozwala użytkownikowi wyrzucać członków z gildii", + "BAN_MEMBERS": "Pozwala użytkownikowi banować członków z gildii", + "MOVE_MEMBERS": "Pozwala użytkownikowi na przenoszenie członków pomiędzy kanałami głosowymi", + "MUTE_MEMBERS": "Pozwala użytkownikowi wyciszać innych członków", + "DEAFEN_MEMBERS": "Pozwala użytkownikowi ogłuszać innych członków", + "CHANGE_NICKNAME": "Pozwala użytkownikowi na zmianę własnego pseudonimu", + "VIEW_CHANNEL": "Pozwala użytkownikowi wyświetlać kanał", + "READ_MESSAGE_HISTORY": "Pozwala użytkownikowi na czytanie historii wiadomości", + "ADD_REACTIONS": "Pozwala użytkownikowi na dodawanie reakcji do wiadomości", + "SEND_MESSAGES": "Pozwala użytkownikowi wysyłać wiadomości", + "SEND_MESSAGES_IN_THREADS": "Pozwala użytkownikowi na wysyłanie wiadomości w wątkach", + "SEND_TTS_MESSAGES": "Pozwala użytkownikowi wysyłać wiadomości text-to-speech (tekst na mowę)", + "USE_EXTERNAL_EMOJIS": "Pozwala użytkownikowi na używanie zewnętrznych emoji", + "USE_EXTERNAL_STICKERS": "Pozwala użytkownikowi na korzystanie z zewnętrznych naklejek", + "EMBED_LINKS": "Pozwala na wyświetlanie osadzonej zawartości z linków wysyłanych przez tego użytkownika", + "ATTACH_FILES": "Pozwala użytkownikowi na załączanie plików", + "SEND_VOICE_MESSAGES": "Pozwala na wysyłanie wiadomości głosowych", + "CREATE_INSTANT_INVITE": "Pozwala użytkownikowi tworzyć zaproszenia do gildii", + "CREATE_PUBLIC_THREADS": "Pozwala użytkownikowi na tworzenie publicznych wątków", + "CREATE_PRIVATE_THREADS": "Pozwala użytkownikowi na tworzenie prywatnych wątków", + "CONNECT": "Pozwala użytkownikowi na połączenie się z kanałem głosowym", + "SPEAK": "Pozwala użytkownikowi mówić w kanale głosowym", + "USE_VAD": "Pozwala użytkownikowi na mówienie w kanale głosowym bez konieczności korzystania z funkcji Naciśnij i Mów", + "STREAM": "Pozwala użytkownikowi na streamowanie", + "USE_SOUNDBOARD": "Pozwala na korzystanie z dźwięków w kanale głosowym", + "USE_EXTERNAL_SOUNDS": "Pozwala na korzystanie z dźwięków z innych gildii", + "PRIORITY_SPEAKER": "Pozwala na korzystanie z funkcji priorytetowego rozmówcy w kanale głosowym", + "REQUEST_TO_SPEAK": "Pozwala użytkownikowi poprosić o głos w kanale scenicznym", + "USE_EMBEDDED_ACTIVITIES": "Pozwala użytkownikowi na korzystanie z wbudowanych aktywności", + "USE_APPLICATION_COMMANDS": "Pozwala użytkownikowi na korzystanie z poleceń aplikacji", + "USE_EXTERNAL_APPS": "Pozwala aplikacjom zainstalowanym przez użytkownika na wysyłanie publicznych odpowiedzi. Po wyłączeniu tej opcji użytkownicy nadal będą mogli korzystać ze swoich aplikacji, ale odpowiedzi będa prywatne. Dotyczy to tylko aplikacji które nie są również zainstalowane w gildii." + }, + "readableNames": { + "ADD_REACTIONS": "Dodawanie reakcji", + "ADMINISTRATOR": "Administrator", + "ATTACH_FILES": "Załączanie plików", + "BAN_MEMBERS": "Banowanie członków", + "BYPASS_SLOWMODE": "Omiń tryb powolny", + "CHANGE_NICKNAME": "Zmiana pseudonimu", + "CONNECT": "Połącz", + "CREATE_EVENTS": "Tworzenie wydarzeń", + "CREATE_GUILD_EXPRESSIONS": "Tworzenie wyrażeń", + "CREATE_INSTANT_INVITE": "Tworzenie zaproszeń", + "CREATE_PRIVATE_THREADS": "Tworzenie prywatnych wątków", + "CREATE_PUBLIC_THREADS": "Tworzenie publicznych wątków", + "DEAFEN_MEMBERS": "Ogłuszanie członków", + "EMBED_LINKS": "Wyświetlanie podglądu linków", + "KICK_MEMBERS": "Wyrzucanie członków", + "MANAGE_CHANNELS": "Zarządzanie kanałami", + "MANAGE_EVENTS": "Zarządzanie wydarzeniami", + "MANAGE_GUILD": "Zarządzanie gildią", + "MANAGE_GUILD_EXPRESSIONS": "Zarządzanie wyrażeniami", + "MANAGE_MESSAGES": "Zarządzanie wiadomościami", + "MANAGE_NICKNAMES": "Zarządzanie pseudonimami", + "MANAGE_ROLES": "Zarządzanie rolami", + "MANAGE_THREADS": "Zarządzanie wątkami", + "MANAGE_WEBHOOKS": "Zarządzanie webhookami", + "MENTION_EVERYONE": "Wspominanie @everyone, @here oraz wszystkich ról", + "MODERATE_MEMBERS": "Wyciszanie członków czasowo", + "MOVE_MEMBERS": "Przenoszenie członków", + "MUTE_MEMBERS": "Wyciszanie członków", + "PIN_MESSAGES": "Przypnij wiadomości", + "PRIORITY_SPEAKER": "Priorytetowy rozmówca", + "READ_MESSAGE_HISTORY": "Czytanie historii wiadomości", + "REQUEST_TO_SPEAK": "Prośba o głos", + "SEND_MESSAGES": "Wysyłanie wiadomości", + "SEND_MESSAGES_IN_THREADS": "Wysyłanie wiadomości w wątkach", + "SEND_POLLS": "Tworzenie ankiet", + "SEND_TTS_MESSAGES": "Wysyłanie wiadomości text-to-speech", + "SEND_VOICE_MESSAGES": "Wysyłanie wiadomości głosowych", + "SPEAK": "Mówienie", + "STREAM": "Wideo", + "USE_APPLICATION_COMMANDS": "Używanie poleceń aplikacji", + "USE_EMBEDDED_ACTIVITIES": "Używanie aktywności", + "USE_EXTERNAL_APPS": "Używanie zewnętrznych aplikacji", + "USE_EXTERNAL_EMOJIS": "Używanie zewnętrznych emoji", + "USE_EXTERNAL_SOUNDS": "Używanie zewnętrznych dźwięków", + "USE_EXTERNAL_STICKERS": "Używanie zewnętrznych naklejek", + "USE_SOUNDBOARD": "Używanie dźwięków", + "USE_VAD": "Używanie Aktywności Głosowej", + "VIEW_AUDIT_LOG": "Wyświetlanie dziennika zdarzeń", + "VIEW_CHANNEL": "Wyświetlanie kanałów", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Wyświetlanie analityk monetyzacji twórcy", + "VIEW_GUILD_INSIGHTS": "Wyświetlanie statystyk gildii" + } + }, + "pinMessage": "Przypnij wiadomość", + "profile": { + "bio": "O mnie:", + "joined": "Założono konto: $1", + "joinedMember": "Dołączył $1: $2", + "mut": "Wspólne Gildie", + "mutFriends": "Wspólni Znajomi", + "permInfo": "Uprawnienia", + "userInfo": "Informacje o użytkowniku" + }, + "profileColor": "Kolor profilu", + "pronouns": "Zaimki:", + "readableName": "Polski", + "recentEmoji": "Ostatnie emoji", + "register": { + "DOBError": "Data urodzenia: $1", + "agreeTOS": "Zgadzam się z [Warunkami korzystania z usługi]($1):", + "emailError": "E-mail: $1", + "noTOS": "Ta instancja nie posiada warunków korzystania z usługi, zaakceptuj je mimo tego:", + "passwordError:": "Hasło: $1", + "register": "Zarejestruj się", + "tos": "Musisz zaakceptować Warunki korzystania z usługi", + "usernameError": "Nazwa użytkownika: $1" + }, + "remove": "Usuń", + "renderJoinAvatars": "Wyświetl awatary dla nowych członków:", + "reply": "Odpowiedz", + "replyingTo": "Odpowiadasz do $1", + "retrying": "Ponawianie próby...", + "role": { + "color": "Kolor", + "colors": { + "name": "Liczba kolorów:", + "one": "Jeden", + "two": "Dwa", + "three": "Trzy", + "secondColor": "Drugi kolor:", + "thirdColor": "Trzeci kolor:" + }, + "confirmDelete": "Czy na pewno chcesz usunąć $1?", + "copyId": "Kopiuj identyfikator roli", + "delete": "Usuń rolę", + "deleting": "Usuwanie roli...", + "displaySettings": "Ustawienia wyświetlania", + "hoisted": "Wyświetlana osobno", + "mentionable": "Zezwól wszystkim na wzmiankę tej roli:", + "name": "Nazwa roli:", + "perms": "Uprawnienia", + "remove": "Zabierz rolę", + "roleEmoji": "Emoji roli:", + "roleFileIcon": "Ikona roli:", + "roles": "Role" + }, + "search": { + "back": "Wstecz", + "new": "Nowy", + "next": "Dalej", + "nofind": "Wygląda na to, że nie ma wiadomości, które pasują do twojego wyszukiwania. Spróbuj je poszerzyć, aby znaleźć to, czego szukasz", + "old": "Stary", + "page": "Strona $1", + "search": "Szukaj" + }, + "searchGifs": "Wyszukaj w Tenor", + "settings": { + "clear": "Wyczyść", + "img": "Wrzuć zdjęcie", + "save": "Zapisz zmiany", + "unsaved": "Uważaj, masz niezapisane zmiany", + "updates": { + "serviceWorkerMode": { + "disabled": "Wyłączony", + "enabled": "Włączony", + "offlineOnly": "Tylko offline", + "title": "Ustawienia Service Workera", + "unregistered": "Wyłączone (w tym pamięć podręczna multimediów)" + } + } + }, + "showBlockedMessages": "Zablokowany użytkownik, kliknij, aby zobaczyć $1 {{PLURAL:$1|zablokowaną wiadomość|zablokowane wiadomości|zablokowanych wiadomości}}.", + "spoiler": "Spoiler", + "sticker": { + "confirmDel": "Czy na pewno chcesz usunąć tą naklejkę?", + "del": "Usuń naklejkę", + "desc": "Opis", + "errEmjMust": "Do naklejki musi należeć emoji", + "errFileMust": "Naklejka musi zawierać zdjęcie", + "image": "Zdjęcie:", + "name": "Nazwa:", + "tags": "Powiązane emoji:", + "title": "Naklejki", + "upload": "Wrzuć naklejki" + }, + "submit": "Wyślij", + "switchAccounts": "Przełącz konta ⇌", + "todayAt": "Dzisiaj o $1", + "trace": { + "totalTime": "$2: $1ms", + "traces": "$1 ($2ms) $3" + }, + "typing": "$2 {{PLURAL:$1|pisze|piszą}}", + "unableToConnect": "Nie można połączyć się z serwerem Spacebar. Spróbuj się wylogować i zalogować ponownie.", + "unableToPin": "Nie można przypiąć wiadomości", + "unpinMessage": "Odepnij wiadomość", + "updateAv": "Dostępne aktualizacje", + "uploadBanner": "Zmień baner:", + "uploadFilesText": "Wrzuć swoje pliki tutaj!", + "uploadPfp": "Zmień awatar:", + "useTemplate": "Użyj $1 jako szablonu", + "useTemplateButton": "Użyj szablonu", + "usedFermi": "Użył Fermi", + "user": { + "report": "Zgłoś użytkownika", + "reportApp": "Zgłoś aplikację", + "addRole": "Dodaj role", + "ban": "Zbanuj członka", + "block": "Zablokuj użytkownika", + "confirmInstBan": "Czy na pewno chcesz zbanować $1 z instancji?", + "copyId": "Kopiuj identyfikator użytkownika", + "dnd": "Nie przeszkadzaj", + "editNick": "Edytuj pseudonim", + "editServerProfile": "Edytuj profil gildii", + "friendReq": "Zaproszenie do znajomych", + "idle": "Bezczynny", + "instanceBan": "Ban instancji", + "invisible": "Niewidzialny", + "kick": "Wyrzuć członka", + "message": "Napisz do użytkownika", + "nick": { + "foe": "Ustaw pseudonim wroga", + "friend": "Ustaw pseudonim znajomego", + "stalker": "Ustaw pseudonim osoby zapraszającej", + "stalking": "Ustaw pseudonim osoby zaproszonej" + }, + "offline": "Niedostępny", + "online": "Dostępny", + "remove": "Usuń użytkownika", + "removeRole": "Zabierz role", + "sendMessage": "Wyślij wiadomość do @$1", + "timeout": "Wyślij członka na przerwę", + "unTimeout": "Usuń przerwę członkowi", + "unban": "Odbanuj $1", + "unblock": "Odblokuj użytkownika" + }, + "userping": { + "resolving": "rozwiązywanie użytkownika", + "unknown": "@nieznany" + }, + "vc": { + "joinForStream": "Dołącz do kanału głosowego, aby oglądać", + "joiningStream": "Dołączanie do streamu...", + "joinstream": "Oglądaj stream", + "leavestream": "Opuść stream", + "stopstream": "Zatrzymaj stream" + }, + "webauth": { + "addKey": "Dodaj klucz", + "keyname": "Nazwa klucza:", + "manage": "Zarządzaj kluczami bezpieczeństwa" + }, + "webhook": "WEBHOOK", + "webhooks": { + "EnterWebhookName": "Wprowadź nazwę webhooka", + "areYouSureDelete": "Czy na pewno chcesz usunąć $1?", + "avatar": "Awatar", + "base": "Webhooki", + "channel": "Kanał", + "copyURL": "Kopiuj adres URL webhooka", + "createdAt": "Utworzono: $1", + "createdBy": "Utworzony przez:", + "deleteWebhook": "Usuń webhook", + "name": "Nazwa:", + "newWebHook": "Nowy webhook", + "notFound": "Użytkownik nie jest już w gildii", + "sillyDefaults": "Captain Hook\nBillie Hooks\nSpidey bot", + "token": "Token webhooka: `$1`", + "type": "Typ: $1", + "type1": "Nadchodzące", + "type2": "Obserwujący kanał", + "type3": "Aplikacja", + "url": "Adres URL webhooka: `$1`" + }, + "welcomeMessages": "Welcome $1! Nice to see ya!\nHello, nice to meat you $1!\nWelcome, hope you brought pizza $1!", + "widget": "Widżet gildii", + "widgetEnabled": "Widżet włączony", + "yes": "Tak", + "yesterdayAt": "Wczoraj o $1" +} diff --git a/translations/pms.json b/translations/pms.json new file mode 100644 index 00000000..c98df44c --- /dev/null +++ b/translations/pms.json @@ -0,0 +1,385 @@ +{ + "@metadata": { + "authors": [ + "Borichèt" + ] + }, + "2faCode": "Còdes 2FA:", + "404": { + "404": "404 Pàgina nen trovà", + "app": "Vers l'aplicassion", + "blog": "Lë scartari Fermi", + "home": "Pàgina d'intrada", + "listtitle": "Peul desse ch'a vorìa andé ant un ëd costi pòst", + "login": "Intré ant ël sistema", + "report": "S'a l'ha trovà costa pàgina ant ël client, për piasì ch'a lo signala:", + "reset": "Pàgina ëd riampostassion ëd ciav", + "title": "As dirìa ch'a l'é sperdusse", + "whatelse": "Cò' d'àutr a prensa ch'a dovrìa sucedie?", + "whereever": "An qualsëssìa pòst a sia" + }, + "DMs": { + "add": "Gionté cheidun a cost MD", + "close": "Saré ël mëssagi diret", + "copyId": "Copié l'identificativ dël mëssagi diret", + "markRead": "Marché coma lesù", + "name": "Mëssagi diret" + }, + "UrlGen": "Generator d'URL", + "Voice": { + "status": { + "badWS": "Rispòsta lòfia da WS", + "conectionFailed": "Ël colegament a l'ha falì", + "done": "Colegà", + "left": "Chità la ciaciarada vocal", + "makingOffer": "Fé n'oferta", + "noSDP": "Gnun SDP", + "notconnected": "Nen colegà", + "sendingStreams": "Mandé dij fluss sonor", + "startingRTC": "Ancaminé un colegament RTC", + "waitingMainWS": "Atèisa për ël WS prinsipal", + "waitingURL": "Atèisa për l'URL vocal", + "wsAuth": "atèisa për l'autorisassion dël WS", + "wsOpen": "Atèisa për la duvertura dël WS" + } + }, + "accessibility": { + "gifSettings": { + "always": "Sempe", + "hover": "Dzorvòl", + "never": "Mai" + }, + "name": "Acessibilità", + "playGif": "Fé andé ij gif su:", + "playIcon": "Fé andé le plance animà su:", + "roleColors": "Disativé ij color dij ròj" + }, + "accountNotStart": "Impossìbil anviaré ël cont", + "add": "Gionté", + "applications": { + "delete": "Eliminé l'aplicassion", + "sure": "É-lo sigur ëd vorèj eliminé $1?" + }, + "badge": { + "active_developer": "Dësvlupador ativ", + "application_guild_subscription": "A l'ha un premi", + "automod": "Dovré la modìfica automàtica", + "bot_commands": "A manten ij comand", + "bug_hunter_level_1": "Cassador ëd givo livel 1", + "bug_hunter_level_2": "Cassador ëd givo livel 2", + "certified_moderator": "Moderator", + "early_supporter": "Sostegn dla prima ora", + "guild_booster_lvl1": "Corporassion ranforsà", + "guild_booster_lvl2": "Corporassion ranforsà", + "guild_booster_lvl3": "Corporassion ranforsà", + "guild_booster_lvl4": "Corporassion ranforsà", + "guild_booster_lvl5": "Corporassion ranforsà", + "guild_booster_lvl6": "Corporassion ranforsà", + "guild_booster_lvl7": "Corporassion ranforsà", + "guild_booster_lvl8": "Corporassion ranforsà", + "guild_booster_lvl9": "Corporassion ranforsà da 'n bon moment", + "hypesquad": "Còsa sota drit d'autor", + "hypesquad_house_1": "Coragi", + "hypesquad_house_2": "Briliantëssa", + "hypesquad_house_3": "Echilibri", + "legacy_username": "A l'ha në stranòm ardità", + "partner": "Cambrada dl'istansa", + "premium": "A pagament", + "quest_completed": "a l'ha fàit n'arcesta", + "staff": "Përsonal ëd l'istansa", + "verified_developer": "Dësvlupador verificà" + }, + "bio": "Biografìa:", + "blankMessage": "Mëssagi veuid", + "blog": { + "blog": "Scartari", + "blogUpdates": "Agiornament a lë scartari:", + "desc": "Lese o abonesse a lë scartari për dj'agiornament su Fermi!", + "fermi": "Scartari ëd Fermi", + "gotoPost": "Andé a la publicassion" + }, + "botGuilds": "Corporassion a le quaj a aparten ël trigomiro:", + "botSettings": "Paràmeter dël trigomiro", + "cancel": "Anulé", + "channel": { + "SlowmodeCool": "Temp d'atèisa për ël meud meusi: $1", + "TimeOutCool": "An pàusa fin-a a: $1", + "allowIcons": "Permëtte dle plance ëd canal përsonalisà", + "announcement": "Anonsi", + "copyId": "Copié l'identificativ dël canal", + "copyIdCat": "Copié l'identificativ dla categorìa", + "createCatagory": "Creé na categorìa", + "createChannel": "Creé un canal", + "creating": "Creassion dël canal", + "delete": "Eliminé ël canal", + "deleteCat": "Eliminé la categorìa", + "icon": "Plancia:", + "makeInvite": "Anvité", + "markRead": "Marché coma lesù", + "mute": "Fé ste ciuto ël canal", + "name": "Canal", + "name:": "Nòm:", + "nsfw:": "NSFW:", + "permissions": "Përmess", + "selectCatName": "Nòm ëd la categorìa", + "selectName": "Nòm dël canal", + "selectType": "Selessioné la sòrt ëd canal", + "settings": "Gust", + "settingsFor": "Gust për $1", + "slowmode": "Meud meusi:", + "text": "Test", + "timedOutUntil": "An pàusa fin-a a: $1", + "topic:": "Argoment:", + "typebox": "Mëssagi an $1", + "unmute": "Fé torna parlé ël canal", + "voice": "Vos" + }, + "channelLink": "# $1", + "confirmGuildLeave": "É-lo sigur dë volej chité $1?", + "copyLink": "Copié la liura dël mëssagi", + "copymessageid": "Copié l'identificativ dël mëssagi", + "copyrawtext": "Copié ël test cru", + "delete": "Eliminé", + "deleteConfirm": "É-lo sigur ëd vorèj dëscancelé sòn?", + "devSettings": { + "badUser": "Conession abilità për j'oget dij cativ utent ch'a mando tròpe anformassion:", + "cache": "Abilité ël servissi d'archivi ëd carte për ij travajeur dla memòria local:", + "cacheDesc": "j'archivi dle carte as cariëran an tuti ij cas, sòn a sigurërà mach che a son ant la memòria local cand a ven fòra un neuv agiornament.", + "captureTrace": "Cost paràmeter a dis a Fermi ëd ciapé dle propietà _pista dal servent. Soa abilitassion a podrìa fé nen marcé la decodificassion JSON progressiva (a podrìa essie damanca ëd carié torna)", + "description": "Costi a son për ij dësvlupador ëd Spacebar e Fermi. S'a sa nen lòn ch'a fa, për piasì ch'a fasa pa dij paciòch con costi paràmeter.", + "gatewayComp": "Disabilité la compression ëd passarela", + "logGateway": "L'argistr a l'ha arseivù dj'eveniment ëd passarela (anformassion an sël livel ëd l'argistr):", + "name": "Gust dël dësvlupador", + "traces": "Smon-e le piste:" + }, + "favoriteGifs": "Gif preferì", + "hideBlockedMessages": "A l'ha blocà cost utent, ch'a sgnaca pr'ëstërmé si mëssagi", + "home": { + "icon": "Plancia dl'istansa ëd $1", + "uptimeStats": "Temp ëd disponibilità: \n Tut ël temp: $1%\nSta sman-a: $2%\nAncheuj: $3%", + "warnOffiline": "L'istansa a l'é fòra 'd linia, impossìbil coleghesse" + }, + "htmlPages": { + "addBot": "Gionté a la corporassion", + "alreadyHave": "Ha-lo già un cont?", + "box1title": "Fermi a l'é un client compatìbil con Spacebar ch'a sërca a esse mej ch'as peul con tante fonsionalità, dont:", + "createAccount": "Creé un cont", + "dobField": "Dàita ëd nassensa:", + "emailField": "Adrëssa eletrònica:", + "idpermissions": "Sòn a përmëttra al trigomiro ëd:", + "instanceField": "Istansa:", + "loaddesc": "A dovrìa nen vorèje tant", + "loadingText": "Fermi a l'é an camin ch'as caria", + "loginButton": "Rintré ant ël sistema", + "noAccount": "Ha-lo nen un cont?", + "openClient": "Duverté ël client", + "pw2Field": "Ch'a buta torna la ciav:", + "pwField": "Ciav:", + "switchaccounts": "Cangé 'd cont", + "userField": "Stranòm:", + "welcomeJank": "Bin-ëvnù an Fermi" + }, + "interactions": { + "ephemeralDismiss": "Scarté ël mëssagi", + "failed": "L'antërassion a l'ha falì", + "nonsence": "Ël servent a l'ha mandà un component ëd sòrt $1 ch'a esist pa", + "notImpl": "Fermi a l'ha ancor nen butà an euvra la sòrt ëd component$1", + "onlyYou": "Mach chiel a peul vëdde 's mëssagi", + "replyline": "Antërassion ancaminà", + "started": "Antërassion an tratament...", + "worked": "L'antërassion a l'ha marcià" + }, + "leaveGuild": "Chité la corporassion", + "logout": { + "error": { + "cancel": "Anulé", + "cont": "Continué malgré tut", + "desc": "Fermi a l'ha nen podù felo seurte dal sistema,\n veul-lo malgré tut dëscancelé ël cont?", + "title": "A-i é staje n'eror an surtend dal sistema" + }, + "logout": "Seurte dal sistema" + }, + "media": { + "artist": "Artista: $1", + "composer": "Compositor: $1", + "download": "Dëscarié ël mojen", + "length": "Durà: $1 minute e $2 second", + "loading": "Antramentre ch'as caria", + "moreInfo": "Pi d'anformassion", + "notFound": "Ël mojen a l'é nen ëstàit trovà" + }, + "messageLink": "Mëssagi an # $1", + "muteDuration": "Buté ël sonor an:", + "no": "Nò", + "noMessages": "A-i é gnun mëssagi ambelessì, ch'a sia ël prim a dì cheicòs!", + "noStickers": "A smija ch'a sia an na situassion foravìa, a-i é gnun adesiv da dovré për chiel!", + "notiClump": "A l'ha $1 notìfiche da $2", + "otherAt": "$1 a $2", + "permissions": { + "descriptions": { + "ADMINISTRATOR": "A përmet tute j'autorisassion e a passa aranda dle riscriture d'autorisassion. A l'é un përmess pericolos!", + "MANAGE_GUILD": "A përmet la gestion e la modìfica dla corporassion", + "VIEW_AUDIT_LOG": "A përmet a l'utent ëd vëdde l'argistr ëd consultassion", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "A përmet ëd vëdde j'anformassion an sj'abonament ai ròj", + "VIEW_GUILD_INSIGHTS": "A përmet a l'utent ëd vëdde j'anformassion an sla corporassion", + "BYPASS_SLOWMODE": "Ël meud meusi a l'ha pi gnun efet ansima a cost utent-sì", + "MENTION_EVERYONE": "A përmet a l'utent ëd massioné tuti quanti", + "MANAGE_NICKNAMES": "A përmet a l'utent ëd modifiché jë stranòm ëd j'àutri mèmber", + "PIN_MESSAGES": "A përmet a l'utent ëd taché dij mëssagi an evidensa", + "SEND_POLLS": "A përmet ëd mandé dij sondagi", + "MANAGE_MESSAGES": "A përmet a l'utent d'eliminé dij mëssagi ch'a son nen sò", + "MANAGE_THREADS": "A përmet a l'utent d'eliminé e d'archivié ij fij ëd discussion e ëd vëdde tuti ij fij privà", + "MANAGE_CHANNELS": "A përmet a l'utent ëd gestì e ëd modifiché ij canaj", + "MANAGE_ROLES": "A përmet a l'utent ëd modifiché e organisé ij ròj", + "MANAGE_WEBHOOKS": "A përmet la gestion e la modìfica dij gancio an sl'aragnà", + "CREATE_EVENTS": "A përmet ëd creé dj'eveniment pianificà e ëd modifiché e eliminé coj creà da l'utent atual.", + "MANAGE_EVENTS": "A përmet a l'utent ëd modifiché e gestì j'eveniment", + "CREATE_GUILD_EXPRESSIONS": "A përmet ëd creé dij sorisin, dj'adesiv e dij con ëd carta sonora e d'eliminé coj creà da l'utent atual.", + "MANAGE_GUILD_EXPRESSIONS": "A përmet la gestion dij sorisin, dj'adesiv e dla carta sonora", + "MODERATE_MEMBERS": "A përmet a l'utent ëd bloché temporaniament j'àutri utent, për ampedije ëd mandé e ëd reagì ai mëssagi ant la ciaciarada e ij fij ëd discussion e ëd parlé ant ij canaj vocaj e ëd sena.", + "KICK_MEMBERS": "A përmet a l'utent ëd taparé via dij mèmber da la corporassion", + "BAN_MEMBERS": "A përmet a l'utent ëd taparé via dij mèmber da la corporassion", + "MOVE_MEMBERS": "A përmet a l'utent ëd tramudé ij mèmber antra ij canaj vocaj", + "MUTE_MEMBERS": "A përmet a l'utent ëd gavé ël son ëd j'àutri mèmber", + "DEAFEN_MEMBERS": "A përmet a l'utent d'ampedì j'àutri dë scoté", + "CHANGE_NICKNAME": "A përmet a l'utent ëd modifiché sò stranòm", + "VIEW_CHANNEL": "A përmet a l'utent ëd vëdde ël canal", + "READ_MESSAGE_HISTORY": "A përmet a l'utent ëd lese lë stòrich dij mëssagi", + "ADD_REACTIONS": "A përmet a l'utent ëd gionté dle reassion ai mëssagi", + "SEND_MESSAGES": "A përmet a l'utent ëd mandé dij mëssagi", + "SEND_MESSAGES_IN_THREADS": "A përmet a l'utent ëd mandé dij mëssagin ant ij fij ëd discussion", + "SEND_TTS_MESSAGES": "A përmet a l'utent ëd mandé dij mëssagi ëd sìntesi vocal", + "USE_EXTERNAL_EMOJIS": "A përmet a l'utent ëd dovré dij sorisin estern", + "USE_EXTERNAL_STICKERS": "A përmet a l'utent ëd dovré dj'adesiv estern", + "EMBED_LINKS": "Permëtte l'antëgrassion automàtica dle liure mandà da cost utent", + "ATTACH_FILES": "A përmet a l'utent ëd taché dj'archivi", + "SEND_VOICE_MESSAGES": "A përmet ëd mandé dij mëssagi vocaj", + "CREATE_INSTANT_INVITE": "A përmet a l'utent ëd creé dj'anvit për la corporassion", + "CREATE_PUBLIC_THREADS": "A përmet a l'utent ëd creé dij fij ëd discussion pùblich", + "CREATE_PRIVATE_THREADS": "A përmet a l'utent ëd creé dij fij ëd discussion privà", + "CONNECT": "A përmet a l'utent ëd coleghesse a 'n canal vocal", + "SPEAK": "A përmet a l'utent ëd parlé ant un canal vocal", + "USE_VAD": "A përmet a j'utent ëd parlé ant un canal vocal mach an parland", + "STREAM": "A përmet a l'utent ëd difonde", + "USE_SOUNDBOARD": "A përmet ëd divré la tàula dij son ant un canal vocal", + "USE_EXTERNAL_SOUNDS": "A përmet l'usagi ëd son përsonalisà da carte sonore ch'a ven-o da d'àutre corporassion", + "PRIORITY_SPEAKER": "A përmet ëd dovré n'àut parlant prioritari ant un canal vocal", + "REQUEST_TO_SPEAK": "A përmet a l'utent ëd ciamé ëd parlé ant ël canal ëd sena", + "USE_EMBEDDED_ACTIVITIES": "A përmet a l'utent ëd dovré dj'atività antëgrà", + "USE_APPLICATION_COMMANDS": "A përmet a l'utent ëd dovré ij comand ëd l'aplicassion", + "USE_EXTERNAL_APPS": "A përmet a j'aplicassion anstalà da l'utent ëd mandé dle rispòste pùbliche. Cand s'opsion a l'é disabilità, j'utent a son istess autorisà a dovré soe aplicassion, ma le rispòste a sarà temporanie. Sòn as àplica mach a j'aplicassion nen anstalà an sla corporassion." + }, + "readableNames": { + "ADD_REACTIONS": "Gionté dle reassion", + "ADMINISTRATOR": "Aministrator", + "ATTACH_FILES": "Taché dj'archivi", + "BAN_MEMBERS": "Taparé via dij mèmber", + "BYPASS_SLOWMODE": "Passé aranda al meud meusi", + "CHANGE_NICKNAME": "Cangé dë stranòm", + "CONNECT": "Coleghesse", + "CREATE_EVENTS": "Creé dj'eveniment", + "CREATE_GUILD_EXPRESSIONS": "Creé dj'espression", + "CREATE_INSTANT_INVITE": "Creé n'anvit", + "CREATE_PRIVATE_THREADS": "Creé dij fij ëd discussion privà", + "CREATE_PUBLIC_THREADS": "Creé dij fij ëd discussion pùblich", + "DEAFEN_MEMBERS": "Rende ij mèmber ciòrgn", + "EMBED_LINKS": "Antëgré dle liure", + "KICK_MEMBERS": "Sbate fòra dij mèmber", + "MANAGE_CHANNELS": "Gestì ij canaj", + "MANAGE_EVENTS": "Gestì j'eveniment", + "MANAGE_GUILD": "Gestì la corporassion", + "MANAGE_GUILD_EXPRESSIONS": "Gestì j'espression", + "MANAGE_MESSAGES": "Gestì ij mëssagi", + "MANAGE_NICKNAMES": "Gestì jë stranòm", + "MANAGE_ROLES": "Gestì ij ròj", + "MANAGE_THREADS": "Gestì ij fij ëd discussion", + "MANAGE_WEBHOOKS": "Gestì ij gancio ëd l'aragnà", + "MENTION_EVERYONE": "Massioné @everyone, @here e tuti ij ròj", + "MODERATE_MEMBERS": "Bloché dij mèmber", + "MOVE_MEMBERS": "Tramudé dij mèmber", + "MUTE_MEMBERS": "Fé sté ciuto ij mèmber", + "PIN_MESSAGES": "Taché dij mëssagi an evidensa", + "PRIORITY_SPEAKER": "Orator prioritari", + "READ_MESSAGE_HISTORY": "Lese la stòria dij mëssagi", + "REQUEST_TO_SPEAK": "Ciamé ëd parlé", + "SEND_MESSAGES": "Mandé dij mëssagi", + "SEND_MESSAGES_IN_THREADS": "Mandé dij mëssagi an dij fij ëd discussion", + "SEND_POLLS": "Creé dij sondagi", + "SEND_TTS_MESSAGES": "Mandé dij mëssage ëd sìntesi vocal", + "SEND_VOICE_MESSAGES": "Mandé dij mëssagi vocaj", + "SPEAK": "Parlé", + "STREAM": "Filmà", + "USE_APPLICATION_COMMANDS": "Dovré ij comand ëd l'aplicassion", + "USE_EMBEDDED_ACTIVITIES": "Dovré dj'atività", + "USE_EXTERNAL_APPS": "Dovré dj'aplicassion esterne", + "USE_EXTERNAL_EMOJIS": "Dovré dij sorisin estern", + "USE_EXTERNAL_SOUNDS": "Dovré dij son estern", + "USE_EXTERNAL_STICKERS": "Dovré dj'adesiv estern", + "USE_SOUNDBOARD": "Dovré la tàula dij son", + "USE_VAD": "Dovré l'andividuassion ëd l'atività vocal", + "VIEW_AUDIT_LOG": "Smon-e l'argistr ëd contròl", + "VIEW_CHANNEL": "Smon-e ij canaj", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Smon-e j'anàlisi ëd monetisassion dij creator", + "VIEW_GUILD_INSIGHTS": "Vëdde j'anformassion an sla corporassion" + } + }, + "pinMessage": "Taché ël mëssagi an evidensa", + "profile": { + "bio": "A propòsit ëd mi:", + "joined": "Cont creà: $1", + "joinedMember": "A l'é rivà $1: $2", + "mut": "Corporassion mutuaj", + "mutFriends": "Amis comun", + "permInfo": "Përmess", + "userInfo": "Anformassion an sl'utent" + }, + "profileColor": "Color dël profil", + "pronouns": "Përnòm:", + "readableName": "piemontèis", + "recentEmoji": "Sorisin recent", + "reply": "Rësponde", + "searchGifs": "Sërché su Tenor", + "showBlockedMessages": "A l'ha blocà cost utent, ch'a sgnaca për vëdde ij $1 {{PLURAL:$1|mëssagi}} blocà.", + "spoiler": "Antissipassion", + "switchAccounts": "Cangé 'd cont ⇌", + "todayAt": "Ancheuj a $1", + "typing": "$2 {{PLURAL:$1|a l'é|a son}} an camin ch'a scrivo", + "unableToPin": "Impossìbil taché ël mëssagi an evidensa", + "uploadBanner": "Carié un tilèt:", + "uploadPfp": "Carié na fòto për ël profil:", + "usedFermi": "Fermi dovrà", + "userping": { + "resolving": "arzolussion ëd l'utent", + "unknown": "@dësconossù" + }, + "vc": { + "joinForStream": "Gionz-se a la ciamà vocal për beiché", + "joiningStream": "Conession al fluss...", + "joinstream": "Beiché ël fluss", + "leavestream": "Chité ël fluss", + "stopstream": "Fërmé ël fluss" + }, + "webhooks": { + "EnterWebhookName": "Buté ël nòm dël gancio dl'aragnà", + "areYouSureDelete": "É-lo sigur ëd vorèj eliminé $1?", + "avatar": "Ampërsonificassion", + "base": "Gancio dl'aragnà", + "channel": "Canal", + "copyURL": "Copié l'URL dël gancio dl'aragnà", + "createdAt": "Creà ël $1", + "createdBy": "Creà da:", + "deleteWebhook": "Eliminé ël gancio dl'aragnà", + "name": "Nòm:", + "newWebHook": "Neuv gancio dl'aragnà", + "notFound": "L'utent a l'é pa pi ant la corporassion", + "sillyDefaults": "Giandoja\nGiaco Tross\nGioanin Pet Pet Sigala", + "token": "Geton dël gancio dl'aragnà: `$1`", + "type": "Sòrt: $1", + "type1": "Intrant", + "type2": "Abonà al canal", + "type3": "Aplicassion", + "url": "url dël gancio dl'aragnà: `$1`" + }, + "yes": "Bò", + "yesterdayAt": "Jer a $1" +} diff --git a/translations/pt-br.json b/translations/pt-br.json new file mode 100644 index 00000000..4f1ba5c4 --- /dev/null +++ b/translations/pt-br.json @@ -0,0 +1,956 @@ +{ + "@metadata": { + "authors": [ + "Eduardoaddad", + "Erol", + "Illyria", + "S0rs immanis12", + "Sixaxis", + "YuriNikolai" + ] + }, + "2faCode": "Código 2FA:", + "404": { + "404": "Erro: 404 - Página não encontrada", + "app": "Ao aplicativo", + "blog": "O Blogue Fermi", + "home": "Menu inicial", + "listtitle": "Talvez você quis ir em um desses lugares", + "login": "Login", + "report": "Se você encontrou essa página dentro do app, por favor, denuncie:", + "reset": "Página de redefinição de senha", + "title": "Parece que você tá perdido", + "whatelse": "O que mais você acha que deveria acontecer?", + "whereever": "O que quer que isso seja" + }, + "onboarding": { + "name": "Onboarding", + "disable": "Desabilitar onboarding", + "addChannel": "Adicionar canal", + "channel": "Canal:", + "desc": "Descrição:", + "deleteChannel": "Apagar canal do onboarding", + "enable": "Habilitar onboarding", + "title": "Bem-vindo à $1" + }, + "DMs": { + "add": "Adicione alguém para este PV", + "close": "Fechar PV", + "copyId": "Copiar ID do PV", + "markRead": "Marcar como lido", + "name": "Mensagens Privadas" + }, + "ok": "Ok", + "dismiss": "Ignorar", + "UrlGen": "Gerador de convite do bot", + "Voice": { + "status": { + "badWS": "Resposta ruim do WS", + "conectionFailed": "Conexão falhou", + "done": "Conectado", + "left": "Saiu do canal de voz", + "makingOffer": "Fazendo oferta", + "noSDP": "Sem SDP", + "notconnected": "Não conectado", + "sendingStreams": "Enviando audio", + "startingRTC": "Conectando ao RTC", + "waitingMainWS": "Esperando pelo WS principal", + "waitingURL": "Esperando pela URL de voz", + "wsAuth": "Esperando autorização do WS", + "wsOpen": "Esperando o WS abrir" + } + }, + "accessibility": { + "gifSettings": { + "always": "Sempre", + "hover": "Ao passar o mouse", + "never": "Nunca" + }, + "name": "Acessibilidade", + "playGif": "Tocar GIFs:", + "playIcon": "Tocar ícones animados:", + "roleColors": "Desabilitar cores dos cargo", + "gradientColors": "Desabilitar gradientes", + "decorations": "Habilitar decorações de avatar:" + }, + "accountNotStart": "Conta falhou ao iniciar", + "add": "Adicionar", + "applications": { + "delete": "Apagar aplicativo", + "sure": "Tem certeza de que quer apagar $1?" + }, + "badge": { + "active_developer": "Desenvolvedor Ativo", + "application_guild_subscription": "Tem premium", + "automod": "utiliza Automod", + "bot_commands": "Suporta comandos", + "bug_hunter_level_1": "Caçador de bugs nível 1", + "bug_hunter_level_2": "Caçador de bugs nível 2", + "certified_moderator": "Moderador", + "early_supporter": "Apoiador inicial", + "guild_booster_lvl1": "Impulsionou guilda", + "guild_booster_lvl2": "Impulsionou guilda", + "guild_booster_lvl3": "Impulsionou guilda", + "guild_booster_lvl4": "Impulsionou guilda", + "guild_booster_lvl5": "Impulsionou guilda", + "guild_booster_lvl6": "Impulsionou guilda", + "guild_booster_lvl7": "Impulsionou guilda", + "guild_booster_lvl8": "Impulsionou guilda", + "guild_booster_lvl9": "Impulsionou guilda por um bom tempo", + "hypesquad": "Coisa com direitos autorais", + "hypesquad_house_1": "Bravura", + "hypesquad_house_2": "Brilhança", + "hypesquad_house_3": "Balança", + "legacy_username": "Tem nome de usuário legacy", + "partner": "Parceiro da instância", + "premium": "Premium", + "quest_completed": "Completou uma missão", + "staff": "Staff da instância", + "verified_developer": "Desenvolvedor Verificado" + }, + "bio": "Bio:", + "blankMessage": "Mensagem vazia", + "blog": { + "blog": "Blogue", + "blogUpdates": "Atualizações do blog:", + "desc": "Leia ou se inscreva no blogue para receber atualizações sobre o Fermi!", + "fermi": "Blog do Fermi", + "gotoPost": "Ir ao post", + "wantUpdates": "Você gostaria de receber atualizações semanais do blog sobre o estado do Fermi?" + }, + "bot": "BOT", + "botGuilds": "Guildas em que o robô está:", + "botSettings": "Configurações do bot", + "cancel": "Cancelar", + "channel": { + "SlowmodeCool": "Espera do modo lento: $1", + "TimeOutCool": "Castigado até: $1", + "allowIcons": "Permitir ícones de canal customizados", + "announcement": "Anúncios", + "copyId": "Copiar id do canal", + "copyIdCat": "Copiar id da categoria", + "createCatagory": "Criar categoria", + "createChannel": "Criar canal", + "creating": "Criando canal", + "delete": "Apagar canal", + "deleteCat": "Apagar categoria", + "icon": "Ícone:", + "makeInvite": "Criar convite", + "markRead": "Marcar como lido", + "mute": "Silenciar Canal", + "name": "Canal", + "name:": "Nome:", + "nsfw:": "NSFW:", + "permissions": "Permissões", + "selectCatName": "Nome da categoria", + "selectName": "Nome do canal", + "selectType": "Selecionar tipo do canal", + "settings": "Configurações", + "settingsFor": "Configurações para $1", + "slowmode": "Modo lento:", + "text": "Texto", + "timedOutUntil": "Castigado até: $1", + "topic:": "Tópico:", + "typebox": "Converse em $1", + "unmute": "Dessilenciar Canal", + "voice": "Voz", + "deleteThread": "Apagar tópico", + "hideThreads": "Esconder após inatividade", + "forum": "Fórum" + }, + "threads": { + "leave": "Sair do tópico", + "join": "Entrar no tópico", + "name": "Nome:", + "editTags": "Editar tags" + }, + "forum": { + "creorsear": "Crie ou procure por um post", + "next": "Próximo", + "back": "Anterior", + "newPost": "Fazer um novo post", + "post": "Postar", + "sortOptions": { + "sortby": { + "title": "Filtrar por", + "recent": "Ativo recentemente", + "posted": "Data de postagem" + }, + "sortOrder": { + "title": "Filtrar ordem", + "recent": "Recentes primeiro", + "old": "Antigos primeiro" + }, + "tagMatch": { + "title": "Etiqueta corresponde", + "some": "Corresponde a algumas etiquetas", + "all": "Corresponde a todas as tags" + }, + "name": "Opções de filtragem" + }, + "errors": { + "tagsReq": "Etiqueta são necessárias", + "requireText": "Mensagem não pode estar vazia" + }, + "allTags": "Todas as etiquetas", + "settings": { + "editTags": "Editar etiquetas:", + "editTag": "Editar etiqueta", + "tagName": "Nome da etiqueta:", + "moderated": "Permitir que somente moderadores de tópicos apliquem etiqueta:", + "addTag": "Adicionar etiqueta", + "delTag": "Apagar etiqueta" + } + }, + "channelLink": "# $1", + "clientDesc": "Versão do cliente: $1\n\n[Entre na guilda oficial do Fermi]($2/invite/USgYJo?instance=https%3A%2F%2Fspacebar.chat)\n\n[Ajude a traduzir o Fermi](https://translatewiki.net/wiki/Translating:JankClient#sortable:3=desc) \n\n[Ajude a criar o Fermi](https://github.com/MathMan05/Fermi)\n\n[Ajude a manter o servidor que o Fermi depende](https://github.com/spacebarchat/server)\n\n[Leia o blog](https://blog.fermi.chat/)\n\nDireitos calculados: $3", + "commands": { + "errorNotValid": "$1 não é uma escolha válida para $2", + "required": "$1 é uma parte necessária desse comando" + }, + "confirmGuildLeave": "Tem certeza de que quer sair $1?", + "copyLink": "Copiar link da mensagem", + "copyRegLink": "Copiar link", + "copymessageid": "Copiar ID da mensagem", + "copyrawtext": "Copiar texto bruto", + "createAccount": "Criar Conta", + "delete": "Apagar", + "deleteConfirm": "Tem certeza de que quer apagar isso?", + "devSettings": { + "badUser": "Habilitar o registro no console de objetos de usuário ruins que enviam informação demais:", + "cache": "Ativar arquivos de mapa de cache do Trabalhador de Serviços:", + "cacheDesc": "Os arquivos de mapa ainda vão carregar, isso só vai garantir que eles estão em um cache quando novas atualizações cheguem.", + "captureTrace": "Essa configuração diz ao Fermi para capturar propriedades _trace do servidor, ativar isso pode causar a decodificação progressiva do JSON a parar de funcionar (talvez exija um recarregamento)", + "clearWellKnowns": "Re-obter registros .well-known", + "description": "Essas são para desenvolvedores do Spacebar/Fermi, se você não sabe o que está fazendo, por favor não mexa com elas.", + "gatewayComp": "Desabilitar compressão do Gateway", + "reportSystem": "Habilitar sistema experimental de denúncias", + "logGateway": "Registrar no console eventos de gateway:", + "name": "Configurações de desenvolvedor", + "traces": "Expôr traços:" + }, + "deviceManage": { + "city": "Cidade: $1", + "continent": "Continente: $1", + "country": "País: $1", + "curSes": "Essa é a sessão atual, você deve sair pelo outro menu", + "estimateWarn": "Aviso: toda essa informação só é uma suposição, pode estar incorreta.", + "ip": "Último IP reconhecido: $1", + "last": "Usado por último há aproximadamente: $1", + "latitude": "Latitude: $1", + "logout": "Sair", + "longitude": "Longitude: $1", + "manageDev": "Gerenciar dispositivo", + "postal": "Postal: $1", + "region": "Região: $1", + "title": "Gerenciar sessões" + }, + "discovery": "Descobrir", + "dms": "Mensagens Privadas", + "edit": "Editar", + "emoji": { + "confirmDel": "Tem certeza de que deseja excluir este emoji?", + "found": { + "not": "Esse emoji não é de uma das suas guildas, você deve entrar para poder usá-lo", + "other": "Esse emoji é de uma de suas outras guildas.", + "private": "A guilda para esse emoji não pôde ser encontrada", + "this": "Este emoji é desta guilda." + }, + "from": "Esse emoji é de", + "image:": "Imagem:", + "join": "Entrar na guilda", + "name:": "Nome:", + "privateGuild": "Essa guilda é privada", + "title": "Emojis", + "upload": "Enviar Emojis" + }, + "emojiSelect": "Emoji:", + "errorReconnect": "Impossível conectar ao servidor, tentando novamente em **$1** segundos...", + "favoriteGifs": "Gifs Favoritos", + "folder": { + "color": "Cor da pasta:", + "create": "Criar nova pasta", + "edit": "Editar pasta", + "name": "Nome da pasta:" + }, + "form": { + "captcha": "Espera, você é humano?" + }, + "friends": { + "addfriend": "Adicionar como amigo", + "addfriendpromt": "Adicionar como amigo por nome de usuário:", + "all": "Todos", + "all:": "Todos os amigos:", + "blocked": "Bloqueado(a)", + "blockedusers": "Usuários bloqueados:", + "bu": "Usuário Bloqueado", + "discnotfound": "Discriminador não encontrado", + "friendlist": "Lista de Amigos", + "friends": "Amigos", + "notfound": "Usuário não encontrado", + "online": "Online", + "online:": "Amigos online:", + "pending": "Pendente", + "pending:": "Pedidos pendentes:", + "removeFriend": "Remover amigo" + }, + "goThere": "Sim, vamos lá", + "goThereTrust": "Sim e confie nesse site no futuro", + "goingToURL": "Você está a caminho de $1. Tem certeza de que quer sair?", + "group": { + "createdm": "Criar grupo!", + "edit": "Editar Grupo", + "select": "Selecionar amigos" + }, + "guild": { + "COMMUNITY": "Aplicar para entrar", + "disableInvites": "Desabilitar convites:", + "DISCOVERABLE": "Descoberta", + "INVITES_DISABLED": "Somente convite", + "adminMenu": { + "changePerms": "Mudar permissões para encontrar", + "finding": "Encontrando Administradores", + "ownName": "Dono", + "owner": "Encontrar o dono", + "permission": "Permissões:" + }, + "report": "Denunciar guilda", + "admins": "Encontrar Administradores", + "all": "Todas", + "banReason": "Motivo do banimento: $1", + "bannedBy": "Banido(a) por:", + "banner:": "Banner:", + "bans": "Banimentos", + "boostMessage?": "Envie uma mensagem quando alguém impulsionar sua guilda!", + "community": "Comunidade", + "confirmDelete": "Tem certeza de que quer apagar $1?", + "confirmLeave": "Tem certeza de que quer sair?", + "copyId": "Copiar id da guilda", + "create": "Criar guilda do zero", + "createFromTemplate": "Criar guilda de um modelo", + "createNewTemplate": "Criar um novo modelo", + "creating": "Criando guilda", + "default": "Padrão ($1)", + "defaultNoti": "Configure a configuração de notificações padrão da sua guilda!", + "delete": "Apagar guilda", + "description:": "Descrição:", + "disoveryTitle": "Descobrir guildas ($1 {{PLURAL:$1|guilda|guildas}})", + "editingTemplate": "Editando $1", + "emptytext": "Que curioso, essa guilda não tem nenhum canal!?", + "emptytitle": "Lugar estranho", + "guilds": "Guildas", + "helpTips?": "Envie dicas úteis para sua guilda!", + "howJoin": "Como pessoas podem se juntar a sua guilda?", + "icon:": "Ícone:", + "invites": "Convites", + "joinConfirm": "Entrar em $1?", + "leave": "Sair da guilda", + "loadingDiscovery": "Carregando...", + "makeInvite": "Criar convite", + "markRead": "Marcar como lida", + "memberCount": "$1 {{PLURAL:$1|membro|membros}}", + "mute": "Silenciar Guilda", + "name:": "Nome:", + "nameNoMatch": "Nomes não correspondem", + "noDelete": "Deixa pra lá", + "noLeave": "Deixa pra lá", + "none": "Nenhuma", + "notifications": "Notificações", + "onlyMentions": "Somente @menções", + "overview": "Visão geral", + "region:": "Região:", + "roles": "Cargos", + "ruleId": "Canal de regras:", + "selectnoti": "Selecionar tipo de notificações:", + "sendrandomwelcome?": "Enviar uma mensagem aleatória quando alguém entrar nesta guilda", + "serverName": "Nome da guilda:", + "settings": "Configurações", + "settingsFor": "Configurações para $1", + "splash": "Splash no Descobrir:", + "stickWelcomeReact?": "Motive membros da sua guilda a reagir com um adesivo quando alguém entra!", + "systemSelect:": "Canal de mensagens do sistema:", + "tempCreatedBy": "Modelo criado por:", + "tempUseCount": "Modelo já foi usado $1 {{PLURAL:$1|vez|vezes}}", + "template": "Modelo:", + "templateDesc": "Descrição do Modelo:", + "templateName": "Nome do Modelo:", + "templateNameShort": "Nome do modelo precisa ter pelo menos 2 caracteres.", + "templateURL": "URL do Modelo: $1", + "templates": "Modelos", + "templcateMetaDesc": "Um modelo permite que outros usem essa guilda como uma base para suas próprias guildas, irá copiar os canais, cargos e configurações dessa guilda, mas não as mensagens dessa guilda, bots ou seu ícone.", + "topic:": "Tópico:", + "unknownRole": "@cargo-desconhecido", + "viewTemplate": "Ver Modelo", + "yesDelete": "Sim, tenho certeza", + "yesLeave": "Sim, tenho certeza" + }, + "hideBlockedMessages": "Você bloqueou este usuário, clique para esconder essas mensagens.", + "home": { + "icon": "Ícone da instância $1", + "uptimeStats": "Tempo vivo:\n Todo o tempo: $1\nEsta semana: $2\nHoje: $3", + "warnOffiline": "Instância está offline, impossível se conectar" + }, + "htmlPages": { + "addBot": "Adicionar à guilda", + "alreadyHave": "Já tem uma conta?", + "box1Items": "Mensagens diretas|Suporte a reações|Convites|Troca de contas|Configurações de usuário|Portal do Desenvolvedor|Convites de bot|Suporte a traduções", + "box1title": "Fermi é um cliente compatível com o Spacebar que busca ser o melhor possível com muitas funções, incluindo:", + "box3description": "Nós sempre apreciamos ajuda, tanto denúncias de bugs ou código, e até apontando erros gramáticos.", + "box3title": "Contribua ao Fermi", + "compatableInstances": "Instâncias compatíveis com o Spacebar:", + "createAccount": "Criar conta", + "dobField": "Data de nascimento:", + "emailField": "E-mail:", + "idpermissions": "Permite que o bot:", + "instanceField": "Instância:", + "loaddesc": "Não deve levar muito tempo", + "loadingText": "Fermi está carregando", + "loginButton": "Entrar", + "noAccount": "Não possui uma conta?", + "openClient": "Abrir Cliente", + "pw2Field": "Digite a senha novamente:", + "pwField": "Senha:", + "switchaccounts": "Trocar Contas", + "trans": "Traduzir", + "transDesc": "Você pode ajudar com a tradução do Fermi para o seu idioma!", + "transTitle": "Traduzir o Fermi", + "userField": "Nome de usuário:", + "welcomeJank": "Bem-vindo(a) ao Fermi" + }, + "incorrectURLS": "## Esta instância provávelmente enviou as URLs incorretas.\n### Se você for o dono da instância por favor veja [isso](https://docs.spacebar.chat/setup/server/) após *Connecting from remote machines* para corrigir o problema.\n Você gostaria que o Fermi corigisse esse problema automáticamente para permitir que conecte a instância?", + "instInfo": "Informação da Instância", + "instanceInfo": { + "contact": "Escrever um e-mail para os Administradores da Instância", + "frontPage": "[Página Inicial]($1)", + "name": "Informações da Instância", + "publicUrl": "[URL Pública]($1)", + "tosPage": "[Termos de Serviço]($1)" + }, + "instanceStats": { + "members": "Membros: $1", + "messages": "Mensagens: $1", + "name": "Informações da Instância: $1", + "servers": "Guildas: $1", + "users": "Usuários registrados: $1" + }, + "interactions": { + "ephemeralDismiss": "Ocultar Mensagem", + "failed": "Interação falhou", + "nonsence": "O servidor enviou um componente de tipo $1 que não existe", + "notImpl": "Fermi ainda não implementou componente de tipo $1 ainda", + "onlyYou": "Só você pode ver essa mensagem", + "replyline": "Iniciou interação", + "started": "Processando interação...", + "worked": "Interação sucedida" + }, + "invite": { + "accept": "Aceitar", + "alreadyJoined": "Já entrou", + "channel:": "Canal:", + "createInvite": "Criar convite", + "createdAt": "Criado em $1", + "expireAfter": "Expirar após:", + "expires": "Expira: $1", + "forChannel": "Para o canal: $1", + "inviteLinkCode": "Código ou link de convite", + "inviteMaker": "Criador de Convites", + "invitedBy": "Você foi convidado por $1", + "joinUsing": "Entrar usando convite", + "loginOrCreateAccount": "Entre ou crie uma conta", + "longInvitedBy": "$1 te convidou para entrar $2", + "maxUses": "Máximo de usos: $1", + "never": "Nunca", + "noAccount": "Crie uma conta para aceitar o convite", + "notFound": "Convite não pôde ser encontrado", + "subtext": "para $1 em $2", + "used": "Usado $1 {{PLURAL:$1|vez|vezes}}" + }, + "inviteOptions": { + "12h": "12 Horas", + "1d": "1 Dia", + "1h": "1 Hora", + "30d": "30 Dias", + "30m": "30 Minutos", + "6h": "6 Horas", + "7d": "7 Dias", + "limit": "$1 {{PLURAL:$1|uso|usos}}", + "never": "Nunca", + "noLimit": "Sem limite", + "title": "Convidar pessoas" + }, + "jankInfo": "Informação do Cliente", + "leaveGuild": "Sair da Guilda", + "leaving": "Você está saindo do Spacebar", + "loaded": "Carregado", + "localuser": { + "2faCode:": "Código:", + "2faCodeGive": "Seu segredo é: $1 e tem 6 digitos, com um período de token de 30 segundos", + "2faDisable": "Desabilitar 2FA", + "2faEnable": "Habilitar 2FA", + "CheckUpdate": "Checar atualizações", + "PasswordsNoMatch": "As senhas não coincidem", + "TOSURL": "URL dos Termos de Serviço:", + "VoiceWarning": "Você tem certeza que quer habilitar isto? É bem experimental e pode causar problemas. (esta função é somente para desenvolvedores, não a ative se não sabe o que está fazendo)", + "accentColor": "Cor de destaque:", + "accountSettings": "Configurações da conta", + "addBot": "Adicionar bot", + "addStatus": "Adicionar status", + "advancedBot": "Configurações de bot avançadas", + "appName": "Nome do aplicativo:", + "areYouSureDelete": "Você tem certeza que quer apagar sua conta? Se sim, digite $1", + "badCode": "Código inválido", + "badPassword": "Senha incorreta", + "botAvatar": "Avatar do bot:", + "botInviteCreate": "Criador de convite de bot", + "botUsername": "Nome do bot:", + "changeDiscriminator": "Mudar discriminador", + "changeEmail": "Alterar e-mail", + "changePassword": "Mudar senha", + "changeUsername": "Mudar nome de usuário", + "clearCache": "Limpar cache", + "confirmAddBot": "Tem certeza de que quer adicionar um bot a esta aplicação? Não tem volta.", + "confirmReset": "Tem certeza que quer resetar o token do bot? Seu bot vai parar de funcionar até que o atualize.", + "confuseNoBot": "Por algum motivo, esta aplicação não tem um bot (ainda)", + "connections": "Conexões", + "createApp": "Criar aplicativo", + "customSound": "Som Customizado", + "customStatusWarn": "Spacebar não tem suporte a status customizados no momento, então enquanto será aceito, nada será feito com ele.", + "deleteAccount": "Apagar conta", + "deleteAccountButton": "Apagar conta", + "description": "Descrição:", + "devPortal": "Portal do desenvolvedor", + "disableConnection": "Essa conexão foi desabilitada no lado do servidor", + "editingBot": "Editando bot $1", + "install": "Instalar", + "installDesc": "Instalar o Fermi te permite abrir ele em uma janela e que ele aja como um app próprio! Você também pode só continuar a usar o Fermi no navegador e irá funcionar do mesmo jeito.", + "installJank": "Instalar Fermi", + "language": "Língua:", + "manageBot": "Gerenciar bot", + "manageInstance": "Gerenciar instância", + "mustTypePhrase": "Para apagar sua conta, você deve digitar a frase.", + "newDiscriminator": "Novo discriminador:", + "newEmail:": "Novo e-mail", + "newPassword:": "Nova senha:", + "newUsername": "Novo nome de usuário:", + "noToken": "Não sabemos o token então não podemos salvá-lo para o localStorage, foi mal", + "noUpdates": "Nenhuma atualização encontrada", + "notisound": "Som de notificação:", + "oldPassword:": "Senha antiga:", + "password:": "Senha", + "privacyPolcyURL": "Link da política de privacidade:", + "appID": "ID do aplicativo: $1", + "showSecret": "Mostrar segredo do cliente", + "clientSecret": "Segredo do cliente: $1", + "secret": "Segredo", + "publicAvaliable": "Criar bot publicamente convidável?", + "refreshPage": "Recarregue para aplicar", + "requireCode": "Exigir código para convidar o bot?", + "resetToken": "Redefinir o Token", + "saveToken": "Salvar token para o localStorage", + "setUp2fa": "Configuração do 2FA", + "setUp2faInstruction": "Copiar este segredo para seu aplicativo totp (gerenciador de senhas temporárias)", + "settings": "Configurações", + "sillyDeleteConfirmPhrase": "Shrek é amor, shrek é vida", + "soundTooLarge": "O som que você tentou enviar é grande demais, tente novamente", + "status": "Status", + "team:": "Time:", + "theme:": "Tema", + "themesAndSounds": "Temas & Sons", + "tokenDisplay": "Token: $1", + "trace": "Trilhas", + "trusted": "Domínios Confiados", + "trustedDesc": "Esses domínios não vão te perguntar por permissão ao clicá-los como outros links, só permita links que você confia.", + "updateSettings": "Configurações de atualização", + "updatesYay": "Atualizações foram encontradas!", + "userSettings": "Configurações do Usuário" + }, + "login": { + "allGood": "Tudo certo", + "checking": "Checando instância", + "enterPAgain": "Digite a senha novamente:", + "invalid": "Instância inválida, tente novamente", + "login": "Login", + "newPassword": "Nova senha:", + "pasteInfo": "Cole o URL de recuperação aqui:", + "recover": "Esqueceu a senha?", + "recovery": "Esqueceu a senha", + "waiting": "Esperando para checar instância" + }, + "logout": { + "error": { + "cancel": "Cancelar", + "cont": "Continuar mesmo assim", + "desc": "Fermi não conseguiu encerrar a sessão,\n deseja remover a conta mesmo assim?", + "title": "Ocorreu um erro ao tentar encerrar a sessão" + }, + "logout": "Encerrar a sessão" + }, + "manageInstance": { + "AreYouSureStop": "Você tem certeza que quer parar essa instância?", + "TokenFormats": { + "JSON": "Formatado em JSON", + "URLs": "URLs de convite", + "plain": "Simples" + }, + "clientURL": "URL do Cliente:", + "copy": "Copiar", + "count": "Contagem:", + "create": "Criar", + "createTokens": "Criar tokens de registro", + "format": "Formato:", + "genericType": "Genérico", + "length": "Tamanho:", + "regType": "Registrar tipo da URL do token", + "stop": "Parar instância" + }, + "media": { + "artist": "Artista: $1", + "composer": "Compositor: $1", + "download": "Baixar mídia", + "length": "Duração: $1 minutos e $2 segundos", + "loading": "Carregando", + "moreInfo": "Mais informações", + "notFound": "Não foi possível encontrar a mídia" + }, + "member": { + "ban": "Banir $1 de $2", + "kick": "Expulsar $1 de $2", + "nick:": "Apelido:", + "persist": "Apagar somente o usuário:", + "reason:": "Motivo:", + "timeout": "Castigo $1", + "timeoutTime": "Tempo de Castigo:" + }, + "message": { + "andMore": "$1, e mais!", + "attached": "Enviou um anexo", + "delete": "Excluir mensagem", + "report": "Denunciar mensagem", + "deleted": "Mensagem excluída", + "edit": "Editar mensagem", + "edited": "editado", + "fullMessage": "Mensagem completa:", + "pin": "$1 fixou uma mensagem", + "reactedWith": "Reagiu com $1", + "reactionAdd": "Adicionar reação", + "threadOptions": "Opções do tópico", + "reactions": "Ver reações", + "reactionsTitle": "Reações", + "retry": "Reenviar mensagem com erro", + "viewrest": "Ver o resto" + }, + "report": { + "back": "Voltar", + "next": "Próximo", + "cancel": "Cancelar", + "submit": "Enviar", + "blockUser": "Bloquear usuário", + "timeout": "Castigar membro", + "summary": "Sumário do reporte:" + }, + "messageLink": "Mensageie # $1", + "muteDuration": "Dessilenciar em:", + "nevermind": "Deixa pra lá", + "no": "Não", + "noEmojiFont": "Usar emojis do sistema", + "noMessages": "Nada aqui ainda, seja o primeiro a dizer algo!", + "noPins": "Parece que não tem nada aqui... ainda", + "noStickers": "Isso é meio estranho, não tem nenhum adesivo aqui para usar!", + "notiClump": "Você recebeu $1 notificações de $2", + "notiVolume": "Volume da notificação:", + "otherAt": "$1 às $2", + "permissions": { + "descriptions": { + "ADMINISTRATOR": "Permite todas as permissões e ultrapassa permissões específicas. Esta é uma permissão perigosa!", + "MANAGE_GUILD": "Permite o gerenciamento e alterações da guilda", + "VIEW_AUDIT_LOG": "Permite que o usuário visualize o registro", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Permite que o usuário visualize detalhes de assinatura de cargo", + "VIEW_GUILD_INSIGHTS": "Permite que o usuário veja detalhes da guilda", + "BYPASS_SLOWMODE": "Modo lento não afetará mais este usuário", + "MENTION_EVERYONE": "Permite que o usuário mencione everyone", + "MANAGE_NICKNAMES": "Permite que o usuário mude o apelido de outros membros", + "PIN_MESSAGES": "Permite o usuário fixar mensagens", + "SEND_POLLS": "Permite que o usuário envie enquetes", + "MANAGE_MESSAGES": "Permite que o usuário apague mensagens que não são suas", + "MANAGE_THREADS": "Permite que o usuário apague e arquive tópicos e que veja todos os tópicos privados", + "MANAGE_CHANNELS": "Permite que o usuário gerencie e altere canais", + "MANAGE_ROLES": "Permite que o usuário altere e gerencie cargos", + "MANAGE_WEBHOOKS": "Permite que o usuário gerencie e altere webhooks", + "CREATE_EVENTS": "Permite a criação de eventos programados e que altere e apague esses criados pelo usuário atual.", + "MANAGE_EVENTS": "Permite que o usuário altere e gerencie eventos", + "CREATE_GUILD_EXPRESSIONS": "Permite que o usuário crie emojis, adesivos, e sons da soundboard, e alterar e apagar esses criados pelo usuário atual.", + "MANAGE_GUILD_EXPRESSIONS": "Permite que o usuário gerencie emojis, adesivos e a soundboard", + "MODERATE_MEMBERS": "Permite que o usuário deixe outros usuários de castigo para previnir que mandem ou reajam a mensagens no chat e em tópicos, e que falem em canais de voz e canais stage", + "KICK_MEMBERS": "Permite que o usuário expulse membros da guilda", + "BAN_MEMBERS": "Permite que o usuário faça o banimento de membros da guilda", + "MOVE_MEMBERS": "Permite que o usuário mova membros entre canais de voz", + "MUTE_MEMBERS": "Permite que o usuário silencie outros membros", + "DEAFEN_MEMBERS": "Permite que o usuário ensurdeça outro membros", + "CHANGE_NICKNAME": "Permite que o usuário mude o próprio apelido", + "VIEW_CHANNEL": "Permite que o usuário veja o canal", + "READ_MESSAGE_HISTORY": "Permite que o usuário leia o histórico de mensagens", + "ADD_REACTIONS": "Permite que o usuário adicione reações às mensagens", + "SEND_MESSAGES": "Permite que o usuário envie mensagens", + "SEND_MESSAGES_IN_THREADS": "Permite que o usuário envie mensagens em tópicos", + "SEND_TTS_MESSAGES": "Permite que o usuário envie mensagens texto-para-fala", + "USE_EXTERNAL_EMOJIS": "Permite que o usuário use emojis externos", + "USE_EXTERNAL_STICKERS": "Permite que o usuário use adesivos externos", + "EMBED_LINKS": "Permite que links enviados por este usuário mostrem um embed", + "ATTACH_FILES": "Permite que o usuário anexe arquivos", + "SEND_VOICE_MESSAGES": "Permite que o usuário envie mensagens de voz", + "CREATE_INSTANT_INVITE": "Permite que o usuário crie convites para a guilda", + "CREATE_PUBLIC_THREADS": "Permite que o usuário crie tópicos públicos", + "CREATE_PRIVATE_THREADS": "Permite que o usuário crie tópicos privados", + "CONNECT": "Permite que o usuário conecte a um canal de voz", + "SPEAK": "Permite que o usuário fale em um canal de voz", + "USE_VAD": "Permite que usuários falem em um canal de voz simplesmente falando", + "STREAM": "Permite que o usuário transmita", + "USE_SOUNDBOARD": "Permite que o usuário use a soundboard em um canal de voz", + "USE_EXTERNAL_SOUNDS": "Permite a utilização de sons customizados da soundboard de outras guildas", + "PRIORITY_SPEAKER": "Permite o uso de fala com prioridade em um canal de voz", + "REQUEST_TO_SPEAK": "Permite que o usuário peça para falar em um canal stage", + "USE_EMBEDDED_ACTIVITIES": "Permite que o usuário use atividades embutidas", + "USE_APPLICATION_COMMANDS": "Permite que o usuário utilize comandos de aplicação", + "USE_EXTERNAL_APPS": "Permite que aplicativos instalados pelo usuário enviem respostas públicas. Quando desabilitado, usuários ainda poderão usar seus aplicativos mas as respostas serão temporárias. Isso só se aplica a aplicativos não instalados na guilda." + }, + "readableNames": { + "ADD_REACTIONS": "Adicionar reações", + "ADMINISTRATOR": "Administrador", + "ATTACH_FILES": "Anexar arquivos", + "BAN_MEMBERS": "Banir membros", + "BYPASS_SLOWMODE": "Ignorar o modo lento", + "CHANGE_NICKNAME": "Mudar apelido", + "CONNECT": "Conectar", + "CREATE_EVENTS": "Criar eventos", + "CREATE_GUILD_EXPRESSIONS": "Criar expressões", + "CREATE_INSTANT_INVITE": "Criar convite", + "CREATE_PRIVATE_THREADS": "Criar tópicos privados", + "CREATE_PUBLIC_THREADS": "Criar tópicos públicos", + "DEAFEN_MEMBERS": "Ensurdecer membros", + "EMBED_LINKS": "Embutidos de links", + "KICK_MEMBERS": "Expulsar membros", + "MANAGE_CHANNELS": "Gerenciar canais", + "MANAGE_EVENTS": "Gerenciar eventos", + "MANAGE_GUILD": "Gerenciar guilda", + "MANAGE_GUILD_EXPRESSIONS": "Gerenciar expressões", + "MANAGE_MESSAGES": "Gerenciar mensagens", + "MANAGE_NICKNAMES": "Gerenciar apelidos", + "MANAGE_ROLES": "Gerenciar cargos", + "MANAGE_THREADS": "Gerenciar tópicos", + "MANAGE_WEBHOOKS": "Gerenciar webhooks", + "MENTION_EVERYONE": "Mencionar @everyone, @here e todos os cargos", + "MODERATE_MEMBERS": "Castigar membros", + "MOVE_MEMBERS": "Mover membros", + "MUTE_MEMBERS": "Silenciar membros", + "PIN_MESSAGES": "Fixar mensagens", + "PRIORITY_SPEAKER": "Falante com prioridade", + "READ_MESSAGE_HISTORY": "Ler histórico de mensagens", + "REQUEST_TO_SPEAK": "Pedir para falar", + "SEND_MESSAGES": "Enviar mensagens", + "SEND_MESSAGES_IN_THREADS": "Enviar mensagens em tópicos", + "SEND_POLLS": "Criar enquetes", + "SEND_TTS_MESSAGES": "Enviar mensagens texto-para-fala", + "SEND_VOICE_MESSAGES": "Enviar mensagens de voz", + "SPEAK": "Falar", + "STREAM": "Vídeo", + "USE_APPLICATION_COMMANDS": "Usar comandos de aplicativos", + "USE_EMBEDDED_ACTIVITIES": "Usar atividades", + "USE_EXTERNAL_APPS": "Usar aplicativos externos", + "USE_EXTERNAL_EMOJIS": "Usar emojis externos", + "USE_EXTERNAL_SOUNDS": "Usar sons externos", + "USE_EXTERNAL_STICKERS": "Usar adesivos externos", + "USE_SOUNDBOARD": "Usar soundboard", + "USE_VAD": "Usar detecção de atividade de voz", + "VIEW_AUDIT_LOG": "Ver registro", + "VIEW_CHANNEL": "Ver canais", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Ver análise de monetização de criador", + "VIEW_GUILD_INSIGHTS": "Ver detalhes da guilda" + } + }, + "pinMessage": "Fixar mensagem", + "profile": { + "bio": "Sobre mim:", + "joined": "Conta criada em: $1", + "joinedMember": "Entrou em $1 em: $2", + "mut": "Guildas mútuas", + "mutFriends": "Amigos mútuos", + "permInfo": "Permissões", + "userInfo": "Informações do usuário" + }, + "profileColor": "Cor do perfil:", + "pronouns": "Pronomes:", + "readableName": "Português do Brasil", + "recentEmoji": "Emojis Recentes", + "register": { + "DOBError": "Data de Nascimento: $1", + "agreeTOS": "Eu concordo com os [Termos de Serviço]($1):", + "emailError": "E-mail: $1", + "noTOS": "Essa instância não tem Termos de Serviço, aceitar de qualquer jeito:", + "passwordError:": "Senha: $1", + "register": "Registrar", + "tos": "Você deve aceitar os Termos de Serviço", + "usernameError": "Nome de usuário: $1" + }, + "remove": "Remover", + "renderJoinAvatars": "Mostrar avatares de membros novos:", + "reply": "Responder", + "replyingTo": "Respondendo a $1", + "retrying": "Tentando novamente...", + "role": { + "color": "Cor", + "colors": { + "name": "Número de cores:", + "one": "Uma", + "two": "Duas", + "three": "Três", + "secondColor": "Segunda cor:", + "thirdColor": "Terceira cor:" + }, + "confirmDelete": "Tem certeza de que quer apagar $1?", + "copyId": "Copiar ID do cargo", + "delete": "Apagar cargo", + "deleting": "Apagando cargo...", + "displaySettings": "Configurações de exibição", + "hoisted": "Levantado:", + "mentionable": "Permitir que qualquer um possa marcar este cargo:", + "name": "Nome do cargo:", + "perms": "Permissões", + "remove": "Remover cargo", + "roleEmoji": "Emoji do cargo:", + "roleFileIcon": "Ícone do cargo:", + "roles": "Cargos" + }, + "search": { + "back": "Voltar", + "new": "Novo", + "next": "Próximo", + "nofind": "Parece ter nenhuma mensagem que bate com sua pesquisa, talvez tente deixar sua pesquisa mais ampla para encontrar o que você quer", + "old": "Antigo", + "page": "Página $1", + "search": "Pesquisar" + }, + "searchGifs": "Pesquisar no Tenor", + "settings": { + "clear": "Limpar", + "img": "Enviar imagem", + "save": "Salvar mudanças", + "unsaved": "Cuidado, você tem mudanças que não foram salvas", + "updates": { + "serviceWorkerMode": { + "disabled": "Desligado", + "enabled": "Ligado", + "offlineOnly": "Somente offline", + "title": "Configuração do Trabalhador de Serviço", + "unregistered": "Desabilitado (incluindo cache de mídia)" + } + } + }, + "showBlockedMessages": "Você bloqueou este usuário, clique para ver $1 {{PLURAL:$1|mensagem|mensagens}} bloqueadas.", + "spoiler": "Spoiler", + "sticker": { + "confirmDel": "Tem certeza que quer apagar esse adesivo?", + "del": "Apagar adesivo", + "desc": "Descrição", + "errEmjMust": "Deve incluir um emoji com seu adesivo", + "errFileMust": "Deve incluir uma imagem para seu adesivo", + "image": "Imagem:", + "name": "Nome:", + "tags": "Emoji associado:", + "title": "Adesivos", + "upload": "Enviar adesivos" + }, + "submit": "Enviar", + "switchAccounts": "Trocar contas", + "todayAt": "Hoje às $1", + "trace": { + "totalTime": "$2: $1ms", + "traces": "$1 ($2ms) $3" + }, + "typing": "$2 {{PLURAL:$1|está|estão}} digitando", + "unableToConnect": "Impossível conectar ao servidor Spacebar. Por favor tente sair e entrar novamente.", + "unableToPin": "Não foi possível fixar a mensagem", + "unpinMessage": "Desfixar essa mensagem", + "updateAv": "Atualizações Disponíveis", + "uploadBanner": "Enviar banner:", + "uploadFilesText": "Carregue seus arquivos aqui!", + "uploadPfp": "Enviar foto de perfil:", + "useTemplate": "Usar $1 como modelo", + "useTemplateButton": "Usar modelo", + "usedFermi": "Usou Fermi", + "user": { + "report": "Denunciar usuário", + "reportApp": "Denunciar aplicativo", + "addRole": "Adicionar cargos", + "ban": "Banir membro", + "block": "Bloquear usuário", + "confirmInstBan": "Tem certeza de que quer banir $1 da instância?", + "copyId": "Copiar ID do usuário", + "dnd": "Não-perturbe", + "editNick": "Editar Apelido", + "editServerProfile": "Editar perfil da guilda", + "friendReq": "Pedido de amizade", + "idle": "Ausente", + "instanceBan": "Banir da instância", + "invisible": "Invisível", + "kick": "Expulsar membro", + "message": "Enviar mensagem", + "nick": { + "foe": "Definir apelido de inimigo", + "friend": "Definir apelido de amigo", + "stalker": "Definir apelido do solicitador", + "stalking": "Definir apelido do solicitado" + }, + "offline": "Offline", + "online": "Online", + "remove": "Remover usuário", + "removeRole": "Remover cargos", + "sendMessage": "Conversar com @$1", + "timeout": "Castigar membro", + "unTimeout": "Remover Castigo do membro", + "unban": "Desbanir $1", + "unblock": "Desbloquear usuário" + }, + "userping": { + "resolving": "Resolvendo usuário", + "unknown": "@Desconhecido" + }, + "vc": { + "joinForStream": "Entre no canal para assistir", + "joiningStream": "Juntando-se a transmissão...", + "joinstream": "Assistir transmissão", + "leavestream": "Sair da transmissão", + "stopstream": "Interromper transmissão" + }, + "webauth": { + "addKey": "Adicionar chave", + "keyname": "Nome da chave:", + "manage": "Gerenciar chaves de segurança" + }, + "webhook": "WEBHOOK", + "webhooks": { + "EnterWebhookName": "Digite o nome do Webhook", + "areYouSureDelete": "Tem certeza de que quer apagar $1?", + "avatar": "Avatar", + "base": "Webhooks", + "channel": "Canal", + "copyURL": "Copiar URL do Webhook", + "createdAt": "Criado em $1", + "createdBy": "Criado por:", + "deleteWebhook": "Apagar Webhook", + "name": "Nome:", + "newWebHook": "Novo Webhook", + "notFound": "Usuário não está mais na guilda", + "sillyDefaults": "Capitão Gancho\nBillie Hooks\nBot-aranha", + "token": "Token do webhook: `$1`", + "type": "Tipo: $1", + "type1": "Recebido", + "type2": "Seguidor do Canal", + "type3": "Aplicativo", + "url": "URL do webhook: `$1`" + }, + "welcomeMessages": "Bem-vindo(a) $1! Bom te ver!\nOlá, prazer em te conhecer $1!\nBem-vindo(a), espero que tenha trago pizza $1!", + "widget": "Widget da Guilda", + "widgetEnabled": "Widget habilitado", + "yes": "Sim", + "yesterdayAt": "Ontem às $1" +} diff --git a/translations/qqq.json b/translations/qqq.json index 04e5e26c..8ca085b7 100644 --- a/translations/qqq.json +++ b/translations/qqq.json @@ -1,21 +1,60 @@ { "@metadata": { + "comment": "Don't know how often I'll update this top part lol", "last-updated": "2024/11/4", "locale": "en", - "comment": "Don't know how often I'll update this top part lol", "authors": [ - "MathMan05" + "Angelsn", + "MathMan05", + "McDutchie", + "Vl1" ] }, - "readableName": "This should be the name of the language in the language, please do not translate english into the language for this name", + "404": { + "whatelse": "what's the context of this message?" + }, + "DMs": { + "add": "I was thinking of using \"Mensaje Directo (MD)\" to refer to DM, but \"chat\" is shorter but if you want me to use the first one you can tell me" + }, + "ok": "Assuming that it's an Ok button", + "Voice": { + "status": { + "badWS": "what WS mean?" + } + }, + "botGuilds": "This means \"the guilds that the bot is in\" (i.e., is a member of).", + "errorReconnect": "Uses MarkDown", + "guild": { + "disoveryTitle": "$1 is the number of guilds discovered" + }, "htmlPages": { "box1Items": "this string is slightly atypical, it has a list of items separated by |, please try to keep the same list size as this" }, + "localuser": { + "description": "This object contains strings related to the logged in user, which is mostly the settings for the user", + "sillyDeleteConfirmPhrase": "This is a silly phrase, do not translate this directly, make a joke that is easy to type in your language", + "updateSettings": "Title of a page that allows you to adjust the update settings" + }, + "permissions": { + "descriptions": { + "CREATE_GUILD_EXPRESSIONS": "The soundboard is a feature that lets you play short audio clips, like sound effects or voice lines, in voice channels for everyone to hear.", + "MANAGE_GUILD_EXPRESSIONS": "The soundboard is a feature that lets you play short audio clips, like sound effects or voice lines, in voice channels for everyone to hear.", + "DEAFEN_MEMBERS": "To deafen means to temporarily disable your ability to hear any audio from a voice channel, while also muting your own microphone so others cannot hear you. Unlike muting, which only stops others from hearing you, deafening affects both incoming and outgoing audio, completely disabling your participation in the voice channel's audio.", + "USE_SOUNDBOARD": "The soundboard is a feature that lets you play short audio clips, like sound effects or voice lines, in voice channels for everyone to hear.", + "USE_EXTERNAL_SOUNDS": "The soundboard is a feature that lets you play short audio clips, like sound effects or voice lines, in voice channels for everyone to hear." + }, + "readableNames": { + "DEAFEN_MEMBERS": "To deafen means to temporarily disable your ability to hear any audio from a voice channel, while also muting your own microphone so others cannot hear you. Unlike muting, which only stops others from hearing you, deafening affects both incoming and outgoing audio, completely disabling your participation in the voice channel's audio.", + "USE_SOUNDBOARD": "The soundboard is a feature that lets you play short audio clips, like sound effects or voice lines, in voice channels for everyone to hear." + } + }, + "readableName": "{{doc-important|This should be the name of the language you are translating into, in that language. Please DO NOT translate this into your language’s word for “English”!}}", "register": { "agreeTOS": "uses MarkDown" }, - "localuser": { - "description": "This object contains strings related to the logged in user, which is mostly the settings for the user" + "typing": "$1 is the number of people typing and $2 is the names of the people typing separated by commas", + "webhooks": { + "sillyDefaults": "{{doc-important|This is just a list of silly default names for webhooks, do not feel the need to translate dirrectly, and no need for the same count}}" }, - "errorReconnect": "Uses MarkDown" + "welcomeMessages": "These are welcome messages, they are meant to be silly, you do not need to directly translated them, there may even be a different count of messages, but only have the username once per message." } diff --git a/translations/ru.json b/translations/ru.json index 28f989d7..d46b24f6 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -1,393 +1,569 @@ { "@metadata": { + "comment": "Русский перевод Jank Client", "last-updated": "2024/15/24", "locale": "ru", - "comment": "Русский перевод Jank Client", "authors": [ + "Amire80", + "Aprenik", + "Dolbaeb", + "EneshTranslates", + "Kaganer", "Kareyac", + "Melted", + "Movses", "Okras", + "Pacha Tchernof", "StealthTheAB", - "StealthTheAngryBird" + "StealthTheAngryBird", + "Vl1", + "Yupik" ] }, - "readableName": "Английский", - "reply": "Ответить", - "copyrawtext": "Скопировать текст", - "copymessageid": "Копировать ID сообщения", - "permissions": { - "descriptions": { - "CREATE_INSTANT_INVITE": "Позволяет участникам приглашать новых участников в эту гильдию", - "KICK_MEMBERS": "Позволяет участникам удалять других участников из этой гильдии", - "BAN_MEMBERS": "Позволяет участникам банить других участников в этой гильдии", - "ADMINISTRATOR": "Участники с этим правом имеют все права и обходят особые права и ограничения каналов. Давать это право опасно!", - "MANAGE_CHANNELS": "Позволяет участникам управлять каналами и редактировать их", - "MANAGE_GUILD": "Позволяет управление и изменение этой гильдии", - "ADD_REACTIONS": "Позволяет участникам добавлять реакции на сообщения", - "VIEW_AUDIT_LOG": "Позволяет участнику просматривать журнал аудита", - "PRIORITY_SPEAKER": "Даёт участникам больше шансов быть услышанными в голосовых каналах", - "STREAM": "Позволяет участникам показывать свой экран", - "VIEW_CHANNEL": "Позволяет участникам просматривать каналы (кроме приватных) по умолчанию", - "SEND_MESSAGES": "Позволяет участникам отправлять сообщения в текстовых каналах", - "SEND_TTS_MESSAGES": "Даёт участникам возможность отправлять TTS-сообщения", - "MANAGE_MESSAGES": "Позволяет пользователям удалять сообщения других пользователей", - "EMBED_LINKS": "Позволяет отображать контент ссылок в текстовых каналах", - "ATTACH_FILES": "Позволяет пользователям прикреплять файлы к сообщениям", - "READ_MESSAGE_HISTORY": "Позволяет пользователям читать историю сообщений", - "MENTION_EVERYONE": "Позволяет пользователю упоминать всех", - "USE_EXTERNAL_EMOJIS": "Позволяет пользователям использовать эмодзи из других гильдий", - "VIEW_GUILD_INSIGHTS": "Позволяет пользователям просматривать аналитику гильдии", - "CONNECT": "Позволяет пользователям подключаться к голосовым каналам", - "SPEAK": "Позволяет пользователям говорить в голосовых каналах", - "MUTE_MEMBERS": "Позволяет пользователям выключать микрофон другим пользователям", - "DEAFEN_MEMBERS": "Позволяет пользователям заглушать других пользователей", - "MOVE_MEMBERS": "Позволяет пользователям перемещать других пользователей между каналами", - "USE_VAD": "Позволяет пользователям говорить в голосовых каналах просто разговаривая", - "CHANGE_NICKNAME": "Позволяет пользователям изменять их собственные никнеймы", - "MANAGE_NICKNAMES": "Позволяет пользователям изменять никнеймы другим пользователям", - "MANAGE_ROLES": "Позволяет пользователям редактировать и управлять ролями", - "MANAGE_WEBHOOKS": "Даёт управление и изменение вебхуков", - "MANAGE_GUILD_EXPRESSIONS": "Даёт управление эмодзи, стикерами и звуковыми панелями", - "USE_APPLICATION_COMMANDS": "Позволяет пользователям исользовать команды приложений", - "REQUEST_TO_SPEAK": "Позволяет пользователям говорить в канале-трибуне", - "MANAGE_EVENTS": "Позволяет пользователям редактировать и управлять событиями", - "MANAGE_THREADS": "Позволяет пользователям удалять, архивировать, а также просматривать приватные ветки", - "CREATE_PUBLIC_THREADS": "Позволяет пользователям создавать публичные ветки", - "CREATE_PRIVATE_THREADS": "Позволяет пользователям создавать приватные ветки", - "USE_EXTERNAL_STICKERS": "Позволяет пользователям использовать внешние стикеры", - "SEND_MESSAGES_IN_THREADS": "Позволяет пользователям отправлять сообщения в ветках", - "USE_EMBEDDED_ACTIVITIES": "Позволяет пользователям использовать активности", - "MODERATE_MEMBERS": "Позволяет пользователям отключать других пользователей, чтобы запретить им отправлять или реагировать на сообщения в чате и темах, а также разговаривать в голосовых каналах и на трибунах.", - "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Даёт просмотр аналитики подписок на роли", - "USE_SOUNDBOARD": "Позволяет пользователям использовать звуки из звуковой панели в голосовых каналах", - "CREATE_GUILD_EXPRESSIONS": "Позволяет добавлять эмодзи, стикеры и звуки, а также удалять и изменять те, что были соданы другими пользователями.", - "CREATE_EVENTS": "Позволяет проводить события, а также удалять и изменять те, что были соданы другими пользователями.", - "USE_EXTERNAL_SOUNDS": "Даёт использование звуков из других серверов", - "SEND_VOICE_MESSAGES": "Позволяет отправлять голосовые сообщения", - "SEND_POLLS": "Позволяет создавать голосования", - "USE_EXTERNAL_APPS": "Позволяет установленным пользователем приложениям отправлять публичные ответы. Если отключено, пользователи по-прежнему смогут использовать свои приложения, но ответы будут эфемерными. Это применимо только к приложениям, которые не установлены на сервере." - }, - "readableNames": { - "CREATE_INSTANT_INVITE": "Создание приглашения", - "KICK_MEMBERS": "Выгонять участников", - "BAN_MEMBERS": "Банить участников", - "ADMINISTRATOR": "Администратор", - "MANAGE_CHANNELS": "Управлять каналами", - "MANAGE_GUILD": "Управлять гильдией", - "ADD_REACTIONS": "Добавлять реакции", - "VIEW_AUDIT_LOG": "Просматривать журнал аудита", - "PRIORITY_SPEAKER": "Приоритетный режим", - "STREAM": "Видео", - "VIEW_CHANNEL": "Просматривать каналы", - "SEND_MESSAGES": "Отправлять сообщения", - "SEND_TTS_MESSAGES": "Отправлять TTS-сообщения", - "MANAGE_MESSAGES": "Управлять сообщениями", - "EMBED_LINKS": "Вставлять контент ссылок", - "ATTACH_FILES": "Прикреплять файлы", - "READ_MESSAGE_HISTORY": "Читать историю сообщений", - "MENTION_EVERYONE": "Упоминать @everyone, @here и все роли", - "USE_EXTERNAL_EMOJIS": "Использовать сторонние эмодзи", - "VIEW_GUILD_INSIGHTS": "Просмотр аналитики гильдии", - "CONNECT": "Подключаться", - "SPEAK": "Говорить", - "MUTE_MEMBERS": "Отключать участникам микрофон", - "DEAFEN_MEMBERS": "Отключать участникам звук", - "MOVE_MEMBERS": "Перемещать участников", - "USE_VAD": "Использовать режим активации по голосу", - "CHANGE_NICKNAME": "Изменять никнейм", - "MANAGE_NICKNAMES": "Управлять никнеймами", - "MANAGE_ROLES": "Управлять ролями", - "MANAGE_WEBHOOKS": "Управлять вебхуками (webhooks)", - "MANAGE_GUILD_EXPRESSIONS": "Управлять выражениями", - "USE_APPLICATION_COMMANDS": "Использовать команды приложения", - "REQUEST_TO_SPEAK": "Попросить вступить", - "MANAGE_EVENTS": "Управлять событиями", - "MANAGE_THREADS": "Управлять ветками", - "CREATE_PUBLIC_THREADS": "Создавать публичные ветки", - "CREATE_PRIVATE_THREADS": "Создавать приватные ветки", - "USE_EXTERNAL_STICKERS": "Использовать сторонние стикеры", - "SEND_MESSAGES_IN_THREADS": "Отправлять сообщения в ветках", - "USE_EMBEDDED_ACTIVITIES": "Использовать активности", - "MODERATE_MEMBERS": "Отправлять участников подумать о своём поведении", - "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Просматривать аналитику монетизации создателей", - "USE_SOUNDBOARD": "Использовать звуковую панель", - "CREATE_GUILD_EXPRESSIONS": "Создавать выражения", - "CREATE_EVENTS": "Создавать события", - "USE_EXTERNAL_SOUNDS": "Использовать внешние звуки", - "SEND_VOICE_MESSAGES": "Отправлять голосовые сообщения", - "SEND_POLLS": "Создавать голосования", - "USE_EXTERNAL_APPS": "Использовать внешние приложения" + "2faCode": "Код двухфакторной аутентификации:", + "404": { + "reset": "Страница сброса пароля" + }, + "DMs": { + "close": "Закрыть ЛС", + "copyId": "Копировать ID ЛС", + "markRead": "Пометить как прочитанное", + "name": "Личные сообщения" + }, + "UrlGen": "Генератор URL", + "Voice": { + "status": { + "badWS": "Плохой ответ от WS", + "conectionFailed": "Ошибка соединения", + "done": "Подключен", + "left": "Левый голосовой чат", + "noSDP": "Нет СДП", + "notconnected": "Не подключен", + "sendingStreams": "Отправка аудиопотоков", + "startingRTC": "Запуск RTC-соединения" } }, - "hideBlockedMessages": "Вы заблокировали этого пользователя, нажмите чтобы скрыть эти сообщения.", - "showBlockedMessages": "Вы заблокировали этого пользователя, нажмите, чтобы просмотреть $1 заблокированное/-ых {{PLURAL:$1| сообщение|сообщений}}.", - "deleteConfirm": "Вы точно уверены в том, что хотите удалить это?", - "yes": "Да", - "no": "Нет", - "todayAt": "Сегодня, в $1", - "yesterdayAt": "Вчера, в $1", - "otherAt": "$1 в $2", - "botSettings": "Настройки бота", - "uploadPfp": "Загрузить картинку профиля:", - "uploadBanner": "Загрузить баннер:", - "pronouns": "Местоимения:", + "accessibility": { + "gifSettings": { + "always": "Всегда", + "never": "Никогда" + }, + "playGif": "Воспроизвести GIF-анимации:", + "roleColors": "Отключить цвета ролей" + }, + "accountNotStart": "Не удаётся запустить учётную запись", + "badge": { + "active_developer": "Активный разработчик", + "application_guild_subscription": "Имеет premium", + "automod": "Использует автомод", + "bot_commands": "Поддерживает команды", + "bug_hunter_level_1": "Охотник за ошибками 1 уровня", + "bug_hunter_level_2": "Охотник за ошибками 2 уровня", + "certified_moderator": "Модератор", + "early_supporter": "Ранее поддержавший", + "guild_booster_lvl1": "Буст гильдии", + "guild_booster_lvl2": "Буст гильдии", + "guild_booster_lvl3": "Буст гильдии", + "guild_booster_lvl4": "Буст гильдии", + "guild_booster_lvl5": "Буст гильдии", + "guild_booster_lvl6": "Буст гильдии", + "guild_booster_lvl7": "Буст гильдии", + "guild_booster_lvl8": "Буст гильдии", + "guild_booster_lvl9": "Буст гильдии довольно долго", + "hypesquad": "Вещь, защищённая авторским правом", + "hypesquad_house_1": "Bravery", + "hypesquad_house_2": "Brilliance", + "hypesquad_house_3": "Balance", + "legacy_username": "Имеет старое имя пользователя", + "partner": "Партнёр инстанции", + "premium": "Premium", + "quest_completed": "выполнил задание", + "staff": "Сотрудники инстанции", + "verified_developer": "Проверенный разработчик" + }, "bio": "Обо мне:", - "profileColor": "Цвет профиля", - "botGuilds": "Гильдии, в которых находится бот:", - "leaveGuild": "Покинуть гильдию", - "confirmGuildLeave": "Вы уверены, что хотите покинуть $1", - "UrlGen": "Генератор URL", - "typing": "$2 {{PLURAL:$1|печатают|печатает}}", - "noMessages": "Видимо здесь пока что нет сообщений, будьте первыми, кто напишет сюда!", "blankMessage": "Пустое сообщение", + "blog": { + "blog": "Блог", + "desc": "Читайте или подписывайтесь на блог, чтобы быть в курсе новостей о Fermi!", + "fermi": "Блог Fermi" + }, + "botGuilds": "Сервера, в которых находится бот:", + "botSettings": "Настройки бота", + "cancel": "Отмена", "channel": { + "announcement": "Объявление", "copyId": "Копировать ID канала", - "markRead": "Пометить как прочитанное", - "settings": "Настройки", + "createCatagory": "Создать категорию", + "createChannel": "Создать канал", "delete": "Удалить канал", "makeInvite": "Создать приглашение", - "settingsFor": "Настройки $1", - "voice": "Голос", - "text": "Текст", - "announcement": "Объявление", + "markRead": "Пометить как прочитанное", "name:": "Название канала:", - "topic:": "Тема канала:", "nsfw:": "Возрастное ограничение:", - "selectType": "Выберите тип канала", - "selectName": "Название канала", + "permissions": "Разрешения", "selectCatName": "Название категории", - "createChannel": "Создать канал", - "createCatagory": "Создать категорию" + "selectName": "Название канала", + "selectType": "Выберите тип канала", + "settings": "Настройки", + "settingsFor": "Настройки $1", + "text": "Текст", + "topic:": "Тема канала:", + "voice": "Голос" }, - "switchAccounts": "Переключение между аккаунтами ⇌", - "accountNotStart": "Аккаунт не может быть запущен", - "home": { - "uptimeStats": "Время онлайн: \n Всё время: $1\nЭта неделя: $2\nЭтот день: $3", - "warnOffiline": "Инстанция не в сети, невозможно подключиться" + "confirmGuildLeave": "Вы уверены, что хотите покинуть $1", + "copymessageid": "Копировать ID сообщения", + "copyrawtext": "Скопировать текст", + "delete": "Удалить", + "deleteConfirm": "Вы точно уверены в том, что хотите удалить это?", + "devSettings": { + "badUser": "Включено ведение журнала некорректных пользовательских объектов, которые отправляют слишком много информации:", + "description": "Эти параметры предназначены для разработчиков Spacebar/Fermi. Если вы не уверены, что делаете, пожалуйста, не изменяйте их.", + "logGateway": "Записывать полученные события шлюза (уровень журнала — info):", + "name": "Developer Settings", + "traces": "Показать трассировки:" }, - "htmlPages": { - "idpermissions": "Это позволит боту:", - "addBot": "Добавить на сервер", - "loadingText": "Jank Client загружается", - "loaddesc": "Это не должно занять много времени", - "switchaccounts": "Переключение между учётными записями", - "instanceField": "Инстанция:", - "emailField": "Эл. почта:", - "pwField": "Пароль:", - "loginButton": "Войти", - "noAccount": "Нет учётной записи?", - "userField": "Имя пользователя:", - "pw2Field": "Введите пароль ещё раз:", - "dobField": "Дата рождения:", - "createAccount": "Создать аккаунт", - "alreadyHave": "Уже есть аккаунт?", - "openClient": "Открыть Клиент", - "welcomeJank": "Добро пожаловать в Jank Client", - "box1title": "Jank Client — это клиент, совместимый с Spacebar, стремящийся быть максимально хорошим и обладающий множеством функций, включая:", - "box1Items": "Личные сообщения|Поддержка реакций|Приглашения|Переключение аккаунтов|Настройки пользователя|Портал разработчиков|Приглашения ботов|Поддержка переводов", - "compatableInstances": "Инстанции, совместимые с Spacebar:", - "box3title": "Внести вклад в Jank Client", - "box3description": "Мы всегда ценим любую помощь, будь то в виде отчетов об ошибках, кода или даже просто указания на некоторые опечатки." + "emoji": { + "confirmDel": "Вы уверены, что хотите удалить этот эмодзи?", + "image:": "Изображение:", + "name:": "Имя:", + "title": "Эмодзи", + "upload": "Загрузить эмодзи" }, - "register": { - "passwordError:": "Пароль: $1", - "usernameError": "Имя пользователя: $1", - "emailError": "Эл. почта: $1", - "DOBError": "Дата рождения: $1", - "agreeTOS": "Я соглашаюсь с [Условиями Использования]($1):", - "noTOS": "На этой инстанции нет Условий Использования, но всё равно примите их:" + "errorReconnect": "Не удалось подключиться к серверу, повтор попытки через $1 секунд...", + "friends": { + "addfriend": "Добавить в друзья", + "addfriendpromt": "Добавить друзей по имени пользователя:", + "all": "Все", + "all:": "Все друзья:", + "blocked": "Заблокированные", + "blockedusers": "Заблокированные пользователи:", + "discnotfound": "Дискриминатор не найден:", + "friendlist": "Список друзей", + "friends": "Друзья", + "notfound": "Пользователь не найден", + "online": "В сети", + "online:": "Друзья в сети:", + "pending": "Ожидающие", + "pending:": "Ожидающиеся заявки в друзья:", + "removeFriend": "Удалить из друзей" }, - "leaving": "Вы покидаете Spacebar", - "goingToURL": "Вы переходите на сайт $1. Вы уверены в том, что хотите перейти туда?", "goThere": "Перейти", "goThereTrust": "Перейти и доверять в будущем", - "nevermind": "Не сейчас", - "submit": "подать", + "goingToURL": "Вы переходите на сайт $1. Вы уверены в том, что хотите перейти туда?", "guild": { + "adminMenu": { + "ownName": "Владелец", + "permission": "Разрешения:" + }, + "all": "все", + "banner:": "Баннер:", + "boostMessage?": "Отправляйте сообщение, когда кто-то бустит вашу гильдию!", + "confirmDelete": "Вы уверены, что хотите удалить $1?", + "confirmLeave": "Вы уверены, что хотите выйти?", "copyId": "Копировать ID гильдии", - "markRead": "Пометить как прочитанное", - "notifications": "Уведомления", - "leave": "Покинуть гильдию", - "settings": "Настройки", + "create": "Создать сервер", + "default": "По умолчанию ($1)", + "defaultNoti": "Установите настройки уведомлений по умолчанию для вашей гильдии!", "delete": "Удалить гильдию", + "description:": "Описание:", + "disoveryTitle": "Путешествие по гильдиям ($1 {{PLURAL:$1|запись|записи|записей}})", + "emptytext": "Вы в странном положении, в этой гильдии нет каналов.", + "emptytitle": "Странное место", + "helpTips?": "Отправьте полезные советы для вашей гильдии!", + "icon:": "Иконка:", + "leave": "Покинуть сервер", + "loadingDiscovery": "Загрузка...", "makeInvite": "Создать приглашение", - "settingsFor": "Настройки $1", + "markRead": "Пометить как прочитанное", "name:": "Название:", - "topic:": "Тема:", - "icon:": "Иконка:", + "noDelete": "Не сейчас", + "noLeave": "Не сейчас", + "none": "никакие", + "notifications": "Уведомления", + "onlyMentions": "только упоминания", "overview": "Обзор", - "banner:": "Баннер:", "region:": "Регион:", "roles": "Роли", "selectnoti": "Выбрать тип уведомлений", - "all": "все", - "onlyMentions": "только упоминания", - "none": "никакие", - "confirmLeave": "Вы уверены, что хотите выйти?", - "yesLeave": "Да, я уверен", - "noLeave": "Не сейчас", - "confirmDelete": "Вы уверены, что хотите удалить $1?", + "sendrandomwelcome?": "Отправлять случайное сообщение, когда кто-то присоединяется к этой гильдии", "serverName": "Название сервера:", + "settings": "Настройки", + "settingsFor": "Настройки $1", + "stickWelcomeReact?": "Поощряйте участников вашей гильдии отправлять стикер, когда кто-то присоединяется!", + "systemSelect:": "Канал системных сообщений:", + "template": "Шаблон:", + "topic:": "Тема:", "yesDelete": "Да, я уверен", - "noDelete": "Не сейчас", - "create": "Создать гильдию", - "loadingDiscovery": "Загрузка...", - "disoveryTitle": "Путешествие по гильдиям ($1) {{PLURAL:$1|запись|записи|записей}}", - "default": "По умолчанию ($1)" + "yesLeave": "Да, я уверен" }, - "role": { - "displaySettings": "Настройки отображения", - "name": "Название роли:", - "hoisted": "Отображать раздельно:", - "mentionable": "Разрешить всем упоминать эту роль:", - "color": "Цвет", - "remove": "Убрать роль", - "delete": "Удалить роль", - "confirmDelete": "Вы уверены в том, что хотите удалить роль $1?" + "hideBlockedMessages": "Вы заблокировали этого пользователя, нажмите чтобы скрыть эти сообщения.", + "home": { + "uptimeStats": "Время онлайн: \n Всё время: $1\nЭта неделя: $2\nЭтот день: $3", + "warnOffiline": "Инстанция не в сети, невозможно подключиться" }, - "settings": { - "unsaved": "Осторожно, вы не сохранили изменения", - "save": "Сохранить изменения" + "htmlPages": { + "addBot": "Добавить на сервер", + "alreadyHave": "Уже есть учётная запись?", + "box1Items": "Личные сообщения|Поддержка реакций|Приглашения|Переключение аккаунтов|Настройки пользователя|Портал разработчиков|Приглашения ботов|Поддержка переводов", + "box1title": "Jank Client — это клиент, совместимый с Spacebar, стремящийся быть максимально хорошим и обладающий множеством функций, включая:", + "box3description": "Мы всегда ценим любую помощь, будь то в виде отчетов об ошибках, кода или даже просто указания на некоторые опечатки.", + "box3title": "Внести вклад в Jank Client", + "compatableInstances": "Инстанции, совместимые с Spacebar:", + "createAccount": "Создать учётную запись", + "dobField": "Дата рождения:", + "emailField": "Эл. почта:", + "idpermissions": "Это позволит боту:", + "instanceField": "Инстанция:", + "loaddesc": "Это не должно занять много времени", + "loadingText": "Jank Client загружается", + "loginButton": "Войти", + "noAccount": "Нет учётной записи?", + "openClient": "Открыть Клиент", + "pw2Field": "Введите пароль ещё раз:", + "pwField": "Пароль:", + "switchaccounts": "Переключение между учётными записями", + "userField": "Имя пользователя:", + "welcomeJank": "Добро пожаловать в Jank Client" }, - "localuser": { - "settings": "Настройки", - "userSettings": "Настройки пользователя", - "themesAndSounds": "Темы и звуки", - "theme:": "Тема", - "notisound": "Звук уведомления:", - "accentColor": "Акцентный цвет:", - "enableEVoice": "Включить экспериментальную поддержку голосовых каналов", - "VoiceWarning": "Вы точно хотите включить это? Эта функция очень экспериментальная и может вызвать проблемы. (эта функция создана для разработчиков, пожалуйста не включайте если не знаете, что делаете)", - "updateSettings": "Обновить настройки", - "swSettings": "Service Worker setting", - "SWOff": "Выкл.", - "SWOffline": "Только вне сети", - "SWOn": "Вкл.", - "clearCache": "Очистить кеш", - "CheckUpdate": "Проверить наличие обновлений", - "accountSettings": "Настройки аккаунта", - "2faDisable": "Выключить двухфакторную аутентификацию", - "badCode": "Неверный код", - "2faEnable": "Включить двухфакторную аутентификацию", - "2faCode:": "Код:", - "setUp2fa": "Настройка двухфакторной аутентификации", - "badPassword": "Неверный пароль", - "setUp2faInstruction": "Скопируйте этот код в в приложение одноразового пароля по времени", + "instanceStats": { + "members": "Участники: $1", + "messages": "Сообщения: $1", + "name": "Статистика инстанции: $1", + "servers": "Сервера: $1", + "users": "Зарегистрированные пользователи: $1" + }, + "invite": { + "accept": "Принять", + "alreadyJoined": "Уже в гильдии", + "channel:": "Канал:", + "createInvite": "Создать приглашение", + "expireAfter": "Истекает через:", + "inviteLinkCode": "Ссылка-приглашение/Код", + "inviteMaker": "Создатель приглашений", + "invitedBy": "Вас пригласил пользователь $1", + "joinUsing": "Присоединиться с помощью приглашения", + "loginOrCreateAccount": "Войдите или создайте аккаунт ⇌", + "longInvitedBy": "Пользователь $1 пригласил вас на $2", + "never": "Никогда", + "noAccount": "Создайте учётную запись, чтобы принять это приглашение", + "subtext": "до $1 в $2" + }, + "inviteOptions": { + "12h": "12 часов", + "1d": "1 день", + "1h": "1 час", + "30d": "30 дней", + "30m": "30 минут", + "6h": "6 часов", + "7d": "7 дней", + "limit": "$1 {{PLURAL:$1|использование|использования|использований}}", + "never": "Никогда", + "noLimit": "Нет ограничений", + "title": "Пригласить людей" + }, + "leaveGuild": "Покинуть сервер", + "leaving": "Вы покидаете Spacebar", + "localuser": { + "2faCode:": "Код:", "2faCodeGive": "Вот ваш шестизначный код: $1. Он имеет ограничение в 30 секунд", + "2faDisable": "Выключить двухфакторную аутентификацию", + "2faEnable": "Включить двухфакторную аутентификацию", + "CheckUpdate": "Проверить наличие обновлений", + "PasswordsNoMatch": "Пароли не совпадают", + "TOSURL": "URL Условий Использования:", + "VoiceWarning": "Вы точно хотите включить это? Оно находится на стадии доработки. (Эта функция создана для разработчиков, пожалуйста, не включайте, если вы не знаете, что делаете)", + "accentColor": "Акцентный цвет:", + "accountSettings": "Настройки аккаунта", + "addBot": "Добавить бота", + "advancedBot": "Расширенные настройки бота", + "appName": "Название приложения:", + "areYouSureDelete": "Вы уверены, что хотите удалить свою учётную запись? Если да, введите фразу $1", + "badCode": "Неверный код", + "badPassword": "Неверный пароль", + "botAvatar": "Аватар бота:", + "botInviteCreate": "Создатель приглашения бота", + "botUsername": "Название бота:", "changeDiscriminator": "Сменить дискриминатор", - "newDiscriminator": "Новый дискриминатор:", "changeEmail": "Сменить адрес эл. почты", - "password:": "Пароль", - "newEmail:": "Новый адрес эл. почты", - "changeUsername": "Сменить имя пользователя", - "newUsername": "Новое имя пользователя:", "changePassword": "Сменить пароль", - "oldPassword:": "Старый пароль:", - "newPassword:": "Новый пароль:", - "PasswordsNoMatch": "Пароли не совпадают", - "disableConnection": "Это подключение было отключено на стороне сервера", - "devPortal": "Портал разработчика", + "changeUsername": "Сменить имя пользователя", + "clearCache": "Очистить кеш", + "confirmAddBot": "Вы уверены в том, что хотите добавить бота в это приложение? Пути назад нет.", + "confirmReset": "Вы уверены в том, что хотите сбросить токен бота? Ваш бот перестанет работать до того как вы его обновите.", + "confuseNoBot": "По какой-то причине, у этого приложения нет бота (пока что).", + "connections": "Подключения", "createApp": "Создать приложение", - "team:": "Команда:", - "appName": "Название приложения:", + "customSound": "Пользовательский звук", + "deleteAccount": "Удаление учётной записи", + "deleteAccountButton": "Удалить учётную запись", "description": "Описание:", + "devPortal": "Портал разработчика", + "disableConnection": "Это подключение было отключено на стороне сервера", + "editingBot": "Изменение бота $1", + "install": "Установить", + "language": "Язык:", + "manageBot": "Управлять ботом", + "mustTypePhrase": "Чтобы удалить свою учётную запись, вам необходимо ввести фразу", + "newDiscriminator": "Новый дискриминатор:", + "newEmail:": "Новый адрес эл. почты", + "newPassword:": "Новый пароль:", + "newUsername": "Новое имя пользователя:", + "noToken": "Токен неизвестен, так что его нельзя сохранить localStorage, извините", + "notisound": "Звук уведомления:", + "oldPassword:": "Старый пароль:", + "password:": "Пароль", "privacyPolcyURL": "URL Политики безопасности:", - "TOSURL": "URL Условий Использования:", "publicAvaliable": "Сделать бота публично приглашаемым?", "requireCode": "Требовать код для приглашения бота?", - "manageBot": "Управлять ботом", - "addBot": "Добавить бота", - "confirmAddBot": "Вы уверены в том, что хотите добавить бота в это приложение? Пути назад нет.", - "confuseNoBot": "По какой-то причине, у этого приложения нет бота (пока что).", - "editingBot": "Изменение бота $1", - "botUsername": "Название бота:", - "botAvatar": "Аватар бота:", "resetToken": "Сбросить токен", - "confirmReset": "Вы уверены в том, что хотите сбросить токен бота? Ваш бот перестанет работать до того как вы его обновите.", - "tokenDisplay": "Токен: $1", "saveToken": "Сохранить токен в localStorage", - "noToken": "Токен неизвестен, так что его нельзя сохранить localStorage, извините", - "advancedBot": "Расширенные настройки бота", - "botInviteCreate": "Создатель приглашения бота", - "language": "Язык:", - "connections": "Подключения" + "setUp2fa": "Настройка двухфакторной аутентификации", + "setUp2faInstruction": "Скопируйте этот код в в приложение одноразового пароля по времени", + "settings": "Настройки", + "sillyDeleteConfirmPhrase": "Шрек - это любовь, Шрек - это жизнь", + "soundTooLarge": "Звук, который вы пытались загрузить, был слишком большим, попробуйте ещё раз.", + "status": "Статус", + "team:": "Команда:", + "theme:": "Тема", + "themesAndSounds": "Темы и звуки", + "tokenDisplay": "Токен: $1", + "updateSettings": "Настройки обновлений", + "userSettings": "Настройки пользователя" + }, + "login": { + "allGood": "Всё в порядке", + "checking": "Проверка инстанции", + "invalid": "Неверная инстанция, попрбуйте снова", + "newPassword": "Новый пароль:", + "waiting": "Ожидание для проверки инстанции" + }, + "logout": { + "error": { + "cancel": "Отмена", + "cont": "Всё равно продолжить", + "desc": "Fermi не удалось выйти из аккаунта,\n Вы все равно хотите удалить учетную запись?", + "title": "Произошла ошибка при попытке выхода из аккаунта" + }, + "logout": "Выйти" + }, + "manageInstance": { + "copy": "Скопировать", + "format": "Формат:", + "length": "Длина:" + }, + "media": { + "download": "Загрузить медиа", + "length": "Продолжительность: $1 минут(ы) $2 секунд(ы)", + "loading": "Загружается", + "moreInfo": "Подробнее", + "notFound": "Медиа не найдено" + }, + "member": { + "ban": "Забанить $1 из $2", + "kick": "Выгнать $1 из $2", + "nick:": "Псевдоним:", + "reason:": "Причина:" }, "message": { - "reactionAdd": "Добавить реакцию", + "attached": "Отправлено вложение", "delete": "Удалить сообщение", + "deleted": "Удалённое сообщение", "edit": "Редактировать сообщение", - "edited": "(отредактировано)" + "edited": "(отредактировано)", + "reactionAdd": "Добавить реакцию" }, - "instanceStats": { - "name": "Статистика инстанции: $1", - "users": "Зарегистрированные пользователи: $1", - "servers": "Сервера: $1", - "messages": "Сообщения: $1", - "members": "Участники: $1" - }, - "inviteOptions": { - "title": "Пригласить людей", - "30m": "30 минут", - "1h": "1 час", - "6h": "6 часов", - "12h": "12 часов", - "1d": "1 день", - "7d": "7 дней", - "30d": "30 дней", - "never": "Никогда", - "limit": "$1 {{PLURAL:$1|использование|использования|использований}}", - "noLimit": "Нет ограничений" + "nevermind": "Не сейчас", + "no": "Нет", + "noMessages": "Похоже, здесь нет сообщений. Будьте первым, кто что-нибудь напишет!", + "otherAt": "$1 в $2", + "permissions": { + "descriptions": { + "ADMINISTRATOR": "Участники с этим правом имеют все права и обходят особые права и ограничения каналов (например, эти участники получают доступ ко всем приватным каналам). Давать это право опасно!", + "MANAGE_GUILD": "Возможность управлять сервером", + "VIEW_AUDIT_LOG": "Позволяет участнику просматривать журнал аудита", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Даёт просмотр аналитики подписок на роли", + "VIEW_GUILD_INSIGHTS": "Позволяет пользователям просматривать аналитику гильдии", + "MENTION_EVERYONE": "Позволяет пользователю упоминать всех", + "MANAGE_NICKNAMES": "Позволяет пользователям изменять никнеймы другим пользователям", + "SEND_POLLS": "Позволяет создавать голосования", + "MANAGE_MESSAGES": "Позволяет пользователям удалять сообщения других пользователей", + "MANAGE_THREADS": "Позволяет пользователям удалять, архивировать, а также просматривать приватные ветки", + "MANAGE_CHANNELS": "Позволяет участникам управлять, создавать и удалять каналы", + "MANAGE_ROLES": "Позволяет пользователям редактировать и управлять ролями", + "MANAGE_WEBHOOKS": "Даёт управление и изменение вебхуков", + "CREATE_EVENTS": "Позволяет проводить события, а также удалять и изменять те, что были соданы другими пользователями.", + "MANAGE_EVENTS": "Позволяет пользователям редактировать и управлять событиями", + "CREATE_GUILD_EXPRESSIONS": "Позволяет добавлять эмодзи, стикеры и звуки, а также удалять и изменять те, что были соданы другими пользователями.", + "MANAGE_GUILD_EXPRESSIONS": "Даёт возможность прикреплять эмодзи, отправлять стикеры и использовать звуковую панель.", + "MODERATE_MEMBERS": "Позволяет пользователям отключать других пользователей, чтобы запретить им отправлять или реагировать на сообщения в чате и темах, а также разговаривать в голосовых каналах и на трибунах.", + "KICK_MEMBERS": "Возможность выгонять участников", + "BAN_MEMBERS": "Возможность банить участников", + "MOVE_MEMBERS": "Позволяет пользователям перемещать других пользователей между каналами", + "MUTE_MEMBERS": "Позволяет пользователям выключать микрофон другим пользователям", + "DEAFEN_MEMBERS": "Позволяет пользователям заглушать других пользователей", + "CHANGE_NICKNAME": "Позволяет пользователям изменять их собственные никнеймы", + "VIEW_CHANNEL": "Позволяет участникам просматривать каналы (кроме приватных) по умолчанию", + "READ_MESSAGE_HISTORY": "Позволяет пользователям читать историю сообщений", + "ADD_REACTIONS": "Позволяет участникам добавлять реакции на сообщения", + "SEND_MESSAGES": "Позволяет участникам отправлять сообщения в текстовых каналах", + "SEND_MESSAGES_IN_THREADS": "Позволяет пользователям отправлять сообщения в ветках", + "SEND_TTS_MESSAGES": "Даёт участникам возможность отправлять TTS-сообщения", + "USE_EXTERNAL_EMOJIS": "Позволяет пользователям использовать эмодзи из других гильдий", + "USE_EXTERNAL_STICKERS": "Позволяет пользователям использовать внешние стикеры", + "EMBED_LINKS": "Позволяет отображать контент ссылок в текстовых каналах", + "ATTACH_FILES": "Позволяет пользователям прикреплять файлы к сообщениям", + "SEND_VOICE_MESSAGES": "Позволяет отправлять голосовые сообщения", + "CREATE_INSTANT_INVITE": "Право создавать приглашения", + "CREATE_PUBLIC_THREADS": "Позволяет пользователям создавать публичные ветки", + "CREATE_PRIVATE_THREADS": "Позволяет пользователям создавать приватные ветки", + "CONNECT": "Позволяет пользователям подключаться к голосовым каналам", + "SPEAK": "Позволяет пользователям говорить в голосовых каналах", + "USE_VAD": "Позволяет пользователям говорить в голосовых каналах просто разговаривая", + "STREAM": "Позволяет участникам показывать свой экран", + "USE_SOUNDBOARD": "Позволяет пользователям использовать звуки из звуковой панели в голосовых каналах", + "USE_EXTERNAL_SOUNDS": "Даёт использование звуков из других серверов", + "PRIORITY_SPEAKER": "Даёт участникам больше шансов быть услышанными в голосовых каналах", + "REQUEST_TO_SPEAK": "Позволяет пользователям говорить в канале-трибуне", + "USE_EMBEDDED_ACTIVITIES": "Позволяет пользователям использовать активности", + "USE_APPLICATION_COMMANDS": "Позволяет пользователям исользовать команды приложений", + "USE_EXTERNAL_APPS": "Позволяет установленным пользователем приложениям отправлять публичные ответы. Если отключено, пользователи по-прежнему смогут использовать свои приложения, но ответы будут эфемерными. Это применимо только к приложениям, которые не установлены на сервере." + }, + "readableNames": { + "ADD_REACTIONS": "Добавить реакцию", + "ADMINISTRATOR": "Администратор", + "ATTACH_FILES": "Прикреплять файлы", + "BAN_MEMBERS": "Забанить участника", + "CHANGE_NICKNAME": "Изменять никнейм", + "CONNECT": "Подключаться", + "CREATE_EVENTS": "Создавать события", + "CREATE_GUILD_EXPRESSIONS": "Создавать выражения", + "CREATE_INSTANT_INVITE": "Создать приглашение", + "CREATE_PRIVATE_THREADS": "Создавать приватные ветки", + "CREATE_PUBLIC_THREADS": "Создавать публичные ветки", + "DEAFEN_MEMBERS": "Заглушить участника", + "EMBED_LINKS": "Вставлять контент ссылок", + "KICK_MEMBERS": "Выгнать участника", + "MANAGE_CHANNELS": "Управлять каналами", + "MANAGE_EVENTS": "Управлять событиями", + "MANAGE_GUILD": "Управлять гильдией", + "MANAGE_GUILD_EXPRESSIONS": "Управлять выражениями", + "MANAGE_MESSAGES": "Управлять сообщениями", + "MANAGE_NICKNAMES": "Управлять никнеймами", + "MANAGE_ROLES": "Управлять ролями", + "MANAGE_THREADS": "Управлять ветками", + "MANAGE_WEBHOOKS": "Управлять вебхуками (webhooks)", + "MENTION_EVERYONE": "Упоминать @everyone, @here и все роли", + "MODERATE_MEMBERS": "Отправлять участников подумать о своём поведении", + "MOVE_MEMBERS": "Переместить участника", + "MUTE_MEMBERS": "Замьютить участника", + "PRIORITY_SPEAKER": "Приоритетный режим", + "READ_MESSAGE_HISTORY": "Читать историю сообщений", + "REQUEST_TO_SPEAK": "Попросить выступить", + "SEND_MESSAGES": "Отправлять сообщения", + "SEND_MESSAGES_IN_THREADS": "Отправлять сообщения в ветках", + "SEND_POLLS": "Создавать голосования", + "SEND_TTS_MESSAGES": "Отправлять TTS-сообщения", + "SEND_VOICE_MESSAGES": "Отправлять голосовые сообщения", + "SPEAK": "Говорить", + "STREAM": "Видео", + "USE_APPLICATION_COMMANDS": "Использовать команды приложения", + "USE_EMBEDDED_ACTIVITIES": "Использовать активности", + "USE_EXTERNAL_APPS": "Использовать внешние приложения", + "USE_EXTERNAL_EMOJIS": "Использовать сторонние эмодзи", + "USE_EXTERNAL_SOUNDS": "Использовать внешние звуки", + "USE_EXTERNAL_STICKERS": "Использовать сторонние стикеры", + "USE_SOUNDBOARD": "Использовать звуковую панель", + "USE_VAD": "Использовать режим активации по голосу", + "VIEW_AUDIT_LOG": "Посмотреть журнал аудита", + "VIEW_CHANNEL": "Просматривать каналы", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Просматривать аналитику монетизации создателей", + "VIEW_GUILD_INSIGHTS": "Просмотр аналитики гильдии" + } }, - "2faCode": "Код двухфакторной аутентификации:", - "invite": { - "invitedBy": "Вас пригласил пользователь $1", - "alreadyJoined": "Уже в гильдии", - "accept": "Принять", - "noAccount": "Создайте аккаунт, чтобы принять это приглашение", - "longInvitedBy": "Пользователь $1 пригласил вас на $2", - "loginOrCreateAccount": "Войдите или создайте аккаунт ⇌", - "joinUsing": "Присоединиться с помощью приглашения", - "inviteLinkCode": "Ссылка-приглашение/Код" + "pinMessage": "Закрепить сообщение", + "profile": { + "permInfo": "Разрешения", + "userInfo": "Информация об участнике" }, - "friends": { - "addfriend": "Добавить в друзья", - "addfriendpromt": "Добавить друзей по имени пользователя:", - "notfound": "Пользователь не найден", - "all:": "Все друзья:", - "friendlist": "Список друзей", - "friends": "Друзья" + "profileColor": "Цвет профиля", + "pronouns": "Местоимения:", + "readableName": "Русский", + "register": { + "DOBError": "Дата рождения: $1", + "agreeTOS": "Я соглашаюсь с [Условиями Использования]($1):", + "emailError": "Эл. почта: $1", + "noTOS": "На этой инстанции нет Условий Использования, но всё равно примите их:", + "passwordError:": "Пароль: $1", + "usernameError": "Имя пользователя: $1" }, + "reply": "Ответить", "replyingTo": "В ответ $1", - "DMs": { - "copyId": "Копировать ID ЛС", - "markRead": "Пометить как прочитанное", - "close": "Закрыть ЛС" + "retrying": "Повтор...", + "role": { + "color": "Цвет", + "confirmDelete": "Вы уверены в том, что хотите удалить роль $1?", + "delete": "Удалить роль", + "displaySettings": "Настройки отображения", + "hoisted": "Отображать раздельно:", + "mentionable": "Разрешить всем упоминать эту роль:", + "name": "Название роли:", + "remove": "Убрать роль" + }, + "search": { + "back": "Назад", + "next": "Далее", + "page": "Страница $1" + }, + "settings": { + "img": "Загрузить изображение", + "save": "Сохранить изменения", + "unsaved": "Осторожно, вы не сохранили изменения", + "updates": { + "serviceWorkerMode": { + "disabled": "Выкл.", + "enabled": "Вкл.", + "offlineOnly": "Только вне сети", + "title": "Service Worker setting" + } + } }, + "showBlockedMessages": "Вы заблокировали этого пользователя, нажмите, чтобы просмотреть $1 заблокированное/-ых {{PLURAL:$1| сообщение|сообщений}}.", + "spoiler": "Спойлер", + "sticker": { + "del": "Удалить стикер", + "desc": "Описание", + "title": "Стикеры" + }, + "submit": "подать", + "switchAccounts": "Переключение между аккаунтами ⇌", + "todayAt": "Сегодня, в $1", + "typing": "$2 {{PLURAL:$1|печатают|печатает}}", + "unableToConnect": "Не удалось подключиться к серверу Spacebar. Пожалуйста, попробуйте выйти и ещё раз войти.", + "unableToPin": "Не удалось закрепить сообщение", + "uploadBanner": "Загрузить баннер:", + "uploadFilesText": "Загрузите свои файлы сюда!", + "uploadPfp": "Загрузить картинку профиля:", "user": { - "copyId": "Скопировать ID пользователя", - "online": "В сети", - "offline": "Не в сети", - "message": "Написать пользователю", + "addRole": "Добавить роли", + "ban": "Забанить участника", "block": "Заблокировать пользователя", - "unblock": "Разблокировать пользователя", + "copyId": "Скопировать ID пользователя", + "editServerProfile": "Редактировать профиль сервера", "friendReq": "Запрос дружбы", "kick": "Выгнать участника", - "ban": "Забанить участника", - "addRole": "Добавить роли", - "removeRole": "Убрать роли" - }, - "login": { - "checking": "Проверка инстанции", - "allGood": "Всё в порядке", - "invalid": "Неверная инстанция, попрбуйте снова", - "waiting": "Ожидание для проверки инстанции" + "message": "Написать пользователю", + "offline": "Не в сети", + "online": "В сети", + "removeRole": "Убрать роли", + "unblock": "Разблокировать участника" }, - "member": { - "kick": "Выгнать $1 из $2", - "reason:": "Причина:", - "ban": "Забанить $1 из $2" + "vc": { + "joiningStream": "Присоединяемся к стриму...", + "joinstream": "Смотреть стрим", + "leavestream": "Прекратить просмотр", + "stopstream": "Прекратить стрим" }, - "uploadFilesText": "Загрузите свои файлы сюда!", - "errorReconnect": "Не удалось подключиться к серверу, повтор попытки через $1 секунд...", - "retrying": "Повтор...", - "unableToConnect": "Не удалось подключиться к серверу Spacebar. Пожалуйста, попробуйте выйти и ещё раз войти." + "yes": "Да", + "yesterdayAt": "Вчера, в $1" } diff --git a/translations/tok.json b/translations/tok.json new file mode 100644 index 00000000..26d4deb5 --- /dev/null +++ b/translations/tok.json @@ -0,0 +1,159 @@ +{ + "@metadata": { + "authors": [ + "Illyria", + "Ookap", + "Palinasin", + "Sylvie" + ] + }, + "404": { + "login": "o kama lon sijelo" + }, + "ok": "pona", + "accessibility": { + "gifSettings": { + "always": "lon tenpo ale", + "never": "lon tenpo ala" + } + }, + "bio": "lipu mi:", + "bot": "jan ilo", + "channel": { + "createChannel": "o pali e tomo toki", + "delete": "o weka e tomo toki", + "name": "tomo toki", + "name:": "nimi pi tomo toki:", + "selectName": "nimi pi tomo toki", + "settings": "o ante e tomo toki", + "settingsFor": "o ante e tomo toki $1", + "text": "lipu", + "voice": "kalama" + }, + "threads": { + "name": "nimi:" + }, + "copyLink": "pali sin nasin lipu", + "copyrawtext": "pali sin lipu", + "createAccount": "o pali e sijelo sin", + "delete": "o weka", + "deleteConfirm": "sina wile li sijelo weka?", + "edit": "o ante", + "emoji": { + "found": { + "this": "sitelen Emosi ni la tan ma ni" + }, + "from": "sitelen Emosi ni li tan", + "image:": "sitelen:", + "join": "o kama ma", + "name:": "nimi:", + "title": "Emosi", + "upload": "o pana Emosi" + }, + "emojiSelect": "Emosi:", + "favoriteGifs": "sitelen ni li pona tawa sina", + "folder": { + "color": "kule poki:", + "name": "nimi poki:" + }, + "friends": { + "all": "jan pona ale", + "all:": "jan pona ale:", + "friends": "jan pona", + "online": "lon" + }, + "guild": { + "all": "ale", + "community": "kulupu ante", + "delete": "o weka ma", + "description:": "lipu ma:", + "icon:": "sitelen lili:", + "leave": "o tawa ma", + "loadingDiscovery": "tenpo lon la ilo Pemi li kama...", + "name:": "nimi:", + "none": "weka" + }, + "htmlPages": { + "createAccount": "o pali e sijelo", + "emailField": "nimi Email:", + "loadingText": "ilo Pemi li kama", + "loginButton": "o kama lon sijelo", + "pwField": "nimi open:", + "trans": "o ante toki", + "welcomeJank": "o kama pona lon ilo Pemi" + }, + "instInfo": "toki pi ilo tan", + "interactions": { + "onlyYou": "toki ni li lon tawa sina taso" + }, + "jankInfo": "toki pi ilo lukin", + "leaveGuild": "o tawa ma", + "localuser": { + "changeEmail": "o ante e nimi Email", + "deleteAccount": "weka sijelo", + "deleteAccountButton": "o weka sijelo", + "newEmail:": "nimi Email sin", + "newPassword:": "nimi open sin:", + "password:": "nimi open" + }, + "login": { + "login": "o kama lon sijelo" + }, + "logout": { + "error": { + "cont": "o tawa", + "desc": "ilo Pemi li ken ala weka e sina tan sijelo ilo sina. taso, sina wile ala wile weka e sijelo ilo sina?" + }, + "logout": "o weka sijelo" + }, + "member": { + "reason:": "tan:" + }, + "messageLink": "o pana toki insa poki toki # $1", + "permissions": { + "readableNames": { + "ADMINISTRATOR": "jan lawa", + "CREATE_INSTANT_INVITE": "o pali nimi kama sin", + "SEND_MESSAGES": "o pana e toki" + } + }, + "readableName": "toki pona", + "register": { + "emailError": "nimi Email: $1", + "passwordError:": "nimi open: $1", + "register": "o pali e sijelo" + }, + "role": { + "color": "kule la ni wan", + "colors": { + "one": "wan", + "two": "tu", + "secondColor": "kule la ni nanpa tu" + } + }, + "settings": { + "save": "o pana e ante" + }, + "sticker": { + "del": "o weka sitelen ko", + "image": "sitelen:", + "name": "nimi:", + "title": "sitelen ko", + "upload": "o pana e sitelen ko" + }, + "submit": "o pana", + "typing": "$2 {{PLURAL:$1|li}} kama toki", + "uploadFilesText": "o pana e lipu lon ni", + "user": { + "deleted": "sijelo weka" + }, + "vc": { + "joinstream": "sitelen mute lukin", + "leavestream": "o tawa e sitelen tawa" + }, + "webhooks": { + "areYouSureDelete": "sina wile li $1 weka?", + "deleteWebhook": "o weka", + "name": "nimi:" + } +} diff --git a/translations/tr.json b/translations/tr.json index 64e6d15f..51801052 100644 --- a/translations/tr.json +++ b/translations/tr.json @@ -1,373 +1,529 @@ { "@metadata": { + "comment": "Sizli bizli", "last-updated": "2024/11/24", "locale": "tr", - "comment": "Sizli bizli", "authors": [ - "Erkin Alp Güney" + "Botorfj", + "Erkin Alp Güney", + "Erkinalp", + "Hedda", + "Mory", + "Sakblue", + "SaldırganSincap", + "Slickdaddy" ] }, - "readableName": "Türkçe", - "reply": "Yanıtla", - "copyrawtext": "Ham metni kopyala", - "copymessageid": "Mesaj kimliğini kopyala", - "permissions": { - "descriptions": { - "CREATE_INSTANT_INVITE": "Kullanıcının sunucu için davet oluşturmasına izin verir", - "KICK_MEMBERS": "Kullanıcının sunucudan üyeleri atmasına izin verir", - "BAN_MEMBERS": "Kullanıcının sunucudan üyeleri yasaklamasına izin verir", - "ADMINISTRATOR": "Tüm izinlere izin verir ve kanal izin geçersiz kılmalarını atlar. Bu tehlikeli bir izindir!", - "MANAGE_CHANNELS": "Kullanıcının kanalları yönetmesine ve düzenlemesine izin verir", - "MANAGE_GUILD": "Sunucunun yönetimine ve düzenlenmesine izin verir", - "ADD_REACTIONS": "Kullanıcının mesajlara tepki eklemesine izin verir", - "VIEW_AUDIT_LOG": "Kullanıcının denetim günlüğünü görüntülemesine izin verir", - "PRIORITY_SPEAKER": "Ses kanalında öncelikli konuşmacı kullanımına izin verir", - "STREAM": "Kullanıcının yayın yapmasına izin verir", - "VIEW_CHANNEL": "Kullanıcının kanalı görüntülemesine izin verir", - "SEND_MESSAGES": "Kullanıcının mesaj göndermesine izin verir", - "SEND_TTS_MESSAGES": "Kullanıcının metinden sese mesajlar göndermesine izin verir", - "MANAGE_MESSAGES": "Kullanıcının kendisine ait olmayan mesajları silmesine izin verir", - "EMBED_LINKS": "Bu kullanıcı tarafından gönderilen bağlantıların otomatik olarak gömülmesine izin verir", - "ATTACH_FILES": "Kullanıcının dosya eklemesine izin verir", - "READ_MESSAGE_HISTORY": "Kullanıcının mesaj geçmişini okumasına izin verir", - "MENTION_EVERYONE": "Kullanıcının herkesi etiketlemesine izin verir", - "USE_EXTERNAL_EMOJIS": "Kullanıcının harici emojileri kullanmasına izin verir", - "VIEW_GUILD_INSIGHTS": "Kullanıcının sunucu içgörülerini görmesine izin verir", - "CONNECT": "Kullanıcının bir ses kanalına bağlanmasına izin verir", - "SPEAK": "Kullanıcının bir ses kanalında konuşmasına izin verir", - "MUTE_MEMBERS": "Kullanıcının diğer üyeleri susturmasına izin verir", - "DEAFEN_MEMBERS": "Kullanıcının diğer üyeleri sağırlamasına izin verir", - "MOVE_MEMBERS": "Kullanıcının üyeleri ses kanalları arasında taşımasına izin verir", - "USE_VAD": "Kullanıcıların ses etkinliği ile konuşmasına izin verir", - "CHANGE_NICKNAME": "Kullanıcının kendi takma adını değiştirmesine izin verir", - "MANAGE_NICKNAMES": "Kullanıcının diğer üyelerin takma adlarını değiştirmesine izin verir", - "MANAGE_ROLES": "Kullanıcının rolleri düzenlemesine ve yönetmesine izin verir", - "MANAGE_WEBHOOKS": "Webhook'ların yönetimine ve düzenlenmesine izin verir", - "MANAGE_GUILD_EXPRESSIONS": "Emoji, çıkartma ve ses panolarını yönetmeye izin verir", - "USE_APPLICATION_COMMANDS": "Kullanıcının uygulama komutlarını kullanmasına izin verir", - "REQUEST_TO_SPEAK": "Kullanıcının sahne kanalında konuşma isteğinde bulunmasına izin verir", - "MANAGE_EVENTS": "Kullanıcının etkinlikleri düzenlemesine ve yönetmesine izin verir", - "MANAGE_THREADS": "Kullanıcının konuları silmesine ve arşivlemesine ve tüm özel konuları görüntülemesine izin verir", - "CREATE_PUBLIC_THREADS": "Kullanıcının genel konular oluşturmasına izin verir", - "CREATE_PRIVATE_THREADS": "Kullanıcının özel konular oluşturmasına izin verir", - "USE_EXTERNAL_STICKERS": "Kullanıcının harici çıkartmaları kullanmasına izin verir", - "SEND_MESSAGES_IN_THREADS": "Kullanıcının konularda mesaj göndermesine izin verir", - "USE_EMBEDDED_ACTIVITIES": "Kullanıcının gömülü etkinlikleri kullanmasına izin verir", - "MODERATE_MEMBERS": "Kullanıcının diğer kullanıcıları susturmasına izin verir", - "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Abonelik içgörülerini görüntülemeye izin verir", - "USE_SOUNDBOARD": "Ses panosunu bir ses kanalında kullanmaya izin verir", - "CREATE_GUILD_EXPRESSIONS": "Emoji, çıkartma ve ses panosu sesleri oluşturmaya ve kullanıcının oluşturduklarını düzenlemeye ve silmeye izin verir", - "CREATE_EVENTS": "Zamanlanmış etkinlikler oluşturmaya ve kullanıcının oluşturduklarını düzenlemeye ve silmeye izin verir", - "USE_EXTERNAL_SOUNDS": "Diğer sunuculardan özel ses panosu seslerini kullanmaya izin verir", - "SEND_VOICE_MESSAGES": "Sesli mesajlar göndermeye izin verir", - "SEND_POLLS": "Anketler göndermeye izin verir", - "USE_EXTERNAL_APPS": "Kullanıcı tarafından yüklenen uygulamaların herkese açık yanıtlar göndermesine izin verir. Devre dışı bırakıldığında, kullanıcılar yine de uygulamalarını kullanabilir ancak yanıtlar geçici olacaktır. Bu, sunucuya yüklenmemiş uygulamalar için geçerlidir." - }, - "readableNames": { - "CREATE_INSTANT_INVITE": "Davet oluştur", - "KICK_MEMBERS": "Üyeleri at", - "BAN_MEMBERS": "Üyeleri yasakla", - "ADMINISTRATOR": "Yönetici", - "MANAGE_CHANNELS": "Kanalları yönet", - "MANAGE_GUILD": "Sunucuyu yönet", - "ADD_REACTIONS": "Tepkiler ekle", - "VIEW_AUDIT_LOG": "Denetim günlüğünü görüntüle", - "PRIORITY_SPEAKER": "Öncelikli konuşmacı", - "STREAM": "Video", - "VIEW_CHANNEL": "Kanalları görüntüle", - "SEND_MESSAGES": "Mesaj gönder", - "SEND_TTS_MESSAGES": "Metinden sese mesaj gönder", - "MANAGE_MESSAGES": "Mesajları yönet", - "EMBED_LINKS": "Bağlantıları göm", - "ATTACH_FILES": "Dosyaları ekle", - "READ_MESSAGE_HISTORY": "Mesaj geçmişini oku", - "MENTION_EVERYONE": "@everyone, @here ve tüm rolleri etiketle", - "USE_EXTERNAL_EMOJIS": "Harici emojileri kullan", - "VIEW_GUILD_INSIGHTS": "Sunucu içgörülerini görüntüle", - "CONNECT": "Bağlan", - "SPEAK": "Konuş", - "MUTE_MEMBERS": "Üyeleri sustur", - "DEAFEN_MEMBERS": "Üyeleri sağırla", - "MOVE_MEMBERS": "Üyeleri taşı", - "USE_VAD": "Ses etkinliği algılamayı kullan", - "CHANGE_NICKNAME": "Takma adı değiştir", - "MANAGE_NICKNAMES": "Takma adları yönet", - "MANAGE_ROLES": "Rolleri yönet", - "MANAGE_WEBHOOKS": "Webhook'ları yönet", - "MANAGE_GUILD_EXPRESSIONS": "İfadeleri yönet", - "USE_APPLICATION_COMMANDS": "Uygulama komutlarını kullan", - "REQUEST_TO_SPEAK": "Konuşma isteğinde bulun", - "MANAGE_EVENTS": "Etkinlikleri yönet", - "MANAGE_THREADS": "Konuları yönet", - "CREATE_PUBLIC_THREADS": "Genel konular oluştur", - "CREATE_PRIVATE_THREADS": "Özel konular oluştur", - "USE_EXTERNAL_STICKERS": "Harici çıkartmaları kullan", - "SEND_MESSAGES_IN_THREADS": "Konularda mesaj gönder", - "USE_EMBEDDED_ACTIVITIES": "Etkinlikleri kullan", - "MODERATE_MEMBERS": "Üyeleri zaman aşımına uğrat", - "VIEW_CREATOR_MONETIZATION_ANALYTICS": "İçerik üretici gelir analitiğini görüntüle", - "USE_SOUNDBOARD": "Ses panosunu kullan", - "CREATE_GUILD_EXPRESSIONS": "İfadeler oluştur", - "CREATE_EVENTS": "Etkinlikler oluştur", - "USE_EXTERNAL_SOUNDS": "Harici sesleri kullan", - "SEND_VOICE_MESSAGES": "Sesli mesajlar gönder", - "SEND_POLLS": "Anketler oluştur", - "USE_EXTERNAL_APPS": "Harici uygulamaları kullan" + "2faCode": "2FA kodu:", + "DMs": { + "close": "DM'yi kapat", + "copyId": "DM kimliğini kopyala", + "markRead": "Okundu olarak işaretle" + }, + "UrlGen": "URL oluşturucu", + "Voice": { + "status": { + "conectionFailed": "Bağlantı kurulamadı", + "done": "Bağlandı", + "left": "Sesli sohbetten ayrıldı", + "notconnected": "Bağlı değil" } }, - "hideBlockedMessages": "Bu kullanıcıyı engellediniz, bu mesajları gizlemek için tıklayın.", - "showBlockedMessages": "Bu kullanıcıyı engellediniz, $1 engellenmiş mesajı görmek için tıklayın.", - "deleteConfirm": "Bunu silmek istediğinizden emin misiniz?", - "yes": "Evet", - "no": "Hayır", - "todayAt": "Bugün saat $1", - "yesterdayAt": "Dün saat $1", - "otherAt": "$1 tarihinde saat $2", - "botSettings": "Bot Ayarları", - "uploadPfp": "Profil resmi yükle:", - "uploadBanner": "Afiş yükle:", - "pronouns": "Zamirler:", + "accessibility": { + "gifSettings": { + "always": "Her zaman", + "hover": "Üzerindeyken", + "never": "Asla" + }, + "name": "Erişilebilirlik", + "playGif": "GIF'leri şurada oynat:", + "roleColors": "Rol renklerini devre dışı et" + }, + "accountNotStart": "Hesap başlatılamadı", "bio": "Biyografi:", - "profileColor": "Profil rengi", - "botGuilds": "Botun bulunduğu sunucular:", - "leaveGuild": "Sunucudan ayrıl", - "confirmGuildLeave": "$1 sunucusundan ayrılmak istediğinizden emin misiniz", - "UrlGen": "URL oluşturucu", - "typing": "$2 {{PLURAL:$1|yazıyor|yazıyorlar}}", - "noMessages": "Burada mesaj görünmüyor, ilk siz bir şey söyleyin!", "blankMessage": "Boş Mesaj", + "blog": { + "blog": "Blog" + }, + "botGuilds": "Botun bulunduğu sunucular:", + "botSettings": "Bot Ayarları", + "cancel": "İptal", "channel": { + "announcement": "Duyurular", "copyId": "Kanal kimliğini kopyala", - "markRead": "Okundu olarak işaretle", - "settings": "Ayarlar", + "copyIdCat": "Kategori kimliğini kopyala", + "createCatagory": "Kategori oluştur", + "createChannel": "Kanal oluştur", "delete": "Kanalı sil", + "deleteCat": "Kategoriyi sil", + "icon": "Simge:", "makeInvite": "Davet oluştur", - "settingsFor": "$1 için ayarlar", - "voice": "Ses", - "text": "Metin", - "announcement": "Duyurular", + "markRead": "Okundu olarak işaretle", + "mute": "Kanalı Sessize Al", + "name": "Kanal", "name:": "Ad:", - "topic:": "Konu:", "nsfw:": "NSFW:", - "selectType": "Kanal türünü seç", - "selectName": "Kanal adı", + "permissions": "İzinler", "selectCatName": "Kategori adı", - "createChannel": "Kanal oluştur", - "createCatagory": "Kategori oluştur" + "selectName": "Kanal adı", + "selectType": "Kanal türünü seç", + "settings": "Ayarlar", + "settingsFor": "$1 için ayarlar", + "text": "Metin", + "topic:": "Konu:", + "unmute": "Kanalın Sesini Aç", + "voice": "Ses" }, - "switchAccounts": "Hesapları Değiştir ⇌", - "accountNotStart": "Hesap başlatılamadı", - "home": { - "uptimeStats": "Çalışma Süresi: \n Tüm zamanlar: $1%\nBu hafta: $2%\nBugün: $3%", - "warnOffiline": "Örnek çevrimdışı, bağlanılamıyor" + "commands": { + "errorNotValid": "$1 $2 için geçerli bir seçenek değildir.", + "required": "$1 bu komutun gerekli bir parçasıdır." }, - "htmlPages": { - "idpermissions": "Bu bot şunlara izin verecek:", - "addBot": "Sunucuya ekle", - "loadingText": "Jank Client yükleniyor", - "loaddesc": "Bu uzun sürmemeli", - "switchaccounts": "Hesapları Değiştir", - "instanceField": "Örnek:", - "emailField": "E-posta:", - "pwField": "Parola:", - "loginButton": "Giriş Yap", - "noAccount": "Hesabınız yok mu?", - "userField": "Kullanıcı adı:", - "pw2Field": "Parolayı tekrar girin:", - "dobField": "Doğum tarihi:", - "createAccount": "Hesap oluştur", - "alreadyHave": "Zaten bir hesabınız var mı?", - "openClient": "İstemciyi Aç", - "welcomeJank": "Jank Client'a Hoş Geldiniz", - "box1title": "Jank Client, birçok özellikle mümkün olduğunca iyi olmaya çalışan bir Spacebar uyumlu istemcidir, bunlar arasında:", - "box1Items": "Doğrudan Mesajlaşma|Tepkiler desteği|Davetler|Hesap değiştirme|Kullanıcı ayarları|Geliştirici portalı|Bot davetleri|Çeviri desteği", - "compatableInstances": "Spacebar Uyumlu Örnekler:", - "box3title": "Jank Client'a Katkıda Bulunun", - "box3description": "Her zaman yardımı takdir ederiz, ister hata raporları, ister kod şeklinde olsun, hatta sadece bazı yazım hatalarını işaret etseniz bile." + "confirmGuildLeave": "$1 sunucusundan ayrılmak istediğinizden emin misiniz", + "copyLink": "Mesaj bağlantısını kopyala", + "copymessageid": "Mesaj kimliğini kopyala", + "copyrawtext": "Ham metni kopyala", + "createAccount": "Hesap Oluştur", + "delete": "Sil", + "deleteConfirm": "Bunu silmek istediğinizden emin misiniz?", + "devSettings": { + "captureTrace": "Bu ayar, Fermi’ye sunucudan _trace özelliklerini yakalamasını söyler; bunu etkinleştirmek, aşamalı JSON çözümlemenin çalışmamasına neden olabilir ve yeniden yükleme gerektirebilir.", + "description": "Bunlar Spacebar/Fermi geliştiricileri içindir. Eğer ne yaptığınızı bilmiyorsanız lütfen bu ayarlarla oynamayın.", + "name": "Geliştirici Ayarları", + "traces": "İzleri ortaya çıkar:" }, - "register": { - "passwordError:": "Parola: $1", - "usernameError": "Kullanıcı adı: $1", - "emailError": "E-posta: $1", - "DOBError": "Doğum Tarihi: $1", - "agreeTOS": "[Hizmet Şartlarını]($1) kabul ediyorum:", - "noTOS": "Bu örneğin Hizmet Şartları yok, yine de TOS'u kabul et:" + "discovery": "Keşfet", + "edit": "Düzenle", + "errorReconnect": "Sunucuya bağlanılamadı, **$1** saniye içinde yeniden denenecek...", + "favoriteGifs": "Favori Gifler", + "form": { + "captcha": "Bir saniye, siz insan mısınız?" }, - "leaving": "Spacebar'dan ayrılıyorsunuz", - "goingToURL": "$1 adresine gidiyorsunuz. Oraya gitmek istediğinizden emin misiniz?", "goThere": "Oraya git", "goThereTrust": "Oraya git ve gelecekte güven", - "nevermind": "Boşver", - "submit": "Gönder", + "goingToURL": "$1 adresine gidiyorsunuz. Oraya gitmek istediğinizden emin misiniz?", "guild": { + "DISCOVERABLE": "Keşfet", + "adminMenu": { + "ownName": "Sahip", + "permission": "İzinler:" + }, + "all": "hepsi", + "banReason": "Yasaklanma nedeni: $1", + "bannedBy": "Tarafından yasaklanıldı:", + "banner:": "Afiş:", + "community": "Topluluk", + "confirmDelete": "$1 sunucusunu silmek istediğinizden emin misiniz?", + "confirmLeave": "Ayrılmak istediğinizden emin misiniz?", "copyId": "Sunucu kimliğini kopyala", - "markRead": "Okundu olarak işaretle", - "notifications": "Bildirimler", - "leave": "Sunucudan ayrıl", - "settings": "Ayarlar", + "create": "Sunucu oluştur", "delete": "Sunucuyu sil", + "description:": "Açıklama:", + "disoveryTitle": "Sunucu keşfi ($1 {{PLURAL:$1|girdi}})", + "emptytext": "Garip bir durumdasınız, bu guild'de hiç kanal yok", + "emptytitle": "Garip durum", + "guilds": "Guildler", + "icon:": "Simge:", + "invites": "Davetler", + "joinConfirm": "$1'e katıl?", + "leave": "Sunucudan ayrıl", + "loadingDiscovery": "Yükleniyor...", "makeInvite": "Davet oluştur", - "settingsFor": "$1 için ayarlar", + "markRead": "Okundu olarak işaretle", + "mute": "Guild'i Sessize Al", "name:": "Ad:", - "topic:": "Konu:", - "icon:": "Simge:", + "noDelete": "Boşver", + "noLeave": "Boşver", + "none": "hiçbiri", + "notifications": "Bildirimler", + "onlyMentions": "sadece bahsetmeler", "overview": "Genel Bakış", - "banner:": "Afiş:", "region:": "Bölge:", "roles": "Roller", "selectnoti": "Bildirim türünü seç", - "all": "hepsi", - "onlyMentions": "sadece bahsetmeler", - "none": "hiçbiri", - "confirmLeave": "Ayrılmak istediğinizden emin misiniz?", - "yesLeave": "Evet, eminim", - "noLeave": "Boşver", - "confirmDelete": "$1 sunucusunu silmek istediğinizden emin misiniz?", - "serverName": "Sunucu adı:", + "sendrandomwelcome?": "Birisi guild'e katılınca rastgele cümle gönder", + "serverName": "Kulüp adı:", + "settings": "Ayarlar", + "settingsFor": "$1 için ayarlar", + "systemSelect:": "Sistem mesajları kanalı:", + "template": "Şablon:", + "templateNameShort": "Şablon adı en az 2 karakter uzunluğunda olmalıdır", + "topic:": "Konu:", + "viewTemplate": "Şablonu Görüntüle", "yesDelete": "Evet, eminim", - "noDelete": "Boşver", - "create": "Sunucu oluştur", - "loadingDiscovery": "Yükleniyor...", - "disoveryTitle": "Sunucu keşfi ($1) girdi" + "yesLeave": "Evet, eminim" }, - "role": { - "displaySettings": "Görüntü Ayarları", - "name": "Rol adı:", - "hoisted": "Listede göster:", - "mentionable": "Herkes bu rolü etiketleyebilir:", - "color": "Renk", - "remove": "Rolü kaldır", - "delete": "Rolü Sil", - "confirmDelete": "$1 silmek istediğinizden emin misiniz?" - }, - "settings": { - "unsaved": "Dikkatli olun, kaydedilmemiş değişiklikleriniz var", - "save": "Değişiklikleri kaydet" + "hideBlockedMessages": "Bu kullanıcıyı engellediniz, bu mesajları gizlemek için tıklayın.", + "home": { + "uptimeStats": "Çalışma Süresi: \n Tüm zamanlar: $1%\nBu hafta: $2%\nBugün: $3%", + "warnOffiline": "Örnek çevrimdışı, bağlanılamıyor" }, - "localuser": { - "settings": "Ayarlar", - "userSettings": "Kullanıcı Ayarları", - "themesAndSounds": "Temalar & Sesler", - "theme:": "Tema", - "notisound": "Bildirim sesi:", - "accentColor": "Vurgu rengi:", - "enableEVoice": "Deneysel Ses desteğini etkinleştir", - "VoiceWarning": "Bunu etkinleştirmek istediğinizden emin misiniz, bu çok deneysel ve sorunlara neden olabilir. (bu özellik geliştiriciler içindir, ne yaptığınızı bilmiyorsanız lütfen etkinleştirmeyin)", - "updateSettings": "Ayarları Güncelle", - "swSettings": "Servis Çalışanı ayarı", - "SWOff": "Kapalı", - "SWOffline": "Yalnızca Çevrimdışı", - "SWOn": "Açık", - "clearCache": "Önbelleği temizle", - "CheckUpdate": "Güncellemeleri kontrol et", - "accountSettings": "Hesap Ayarları", - "2faDisable": "2FA'yı Devre Dışı Bırak", - "badCode": "Geçersiz kod", - "2faEnable": "2FA'yı Etkinleştir", - "2faCode:": "Kod:", - "setUp2fa": "2FA Kurulumu", - "badPassword": "Yanlış parola", - "setUp2faInstruction": "Bu gizli anahtarı TOTP uygulamanıza kopyalayın", + "htmlPages": { + "addBot": "Kulübe ekle", + "alreadyHave": "Zaten bir hesabınız var mı?", + "box1Items": "Doğrudan Mesajlaşma|Tepkiler desteği|Davetler|Hesap değiştirme|Kullanıcı ayarları|Geliştirici portalı|Bot davetleri|Çeviri desteği", + "box1title": "Jank Client, birçok özellikle mümkün olduğunca iyi olmaya çalışan bir Spacebar uyumlu istemcidir, bunlar arasında:", + "box3description": "Her zaman yardımı takdir ederiz, ister hata raporları, ister kod şeklinde olsun, hatta sadece bazı yazım hatalarını işaret etseniz bile.", + "box3title": "Jank Client'a Katkıda Bulunun", + "compatableInstances": "Spacebar Uyumlu Örnekler:", + "createAccount": "Hesap oluştur", + "dobField": "Doğum tarihi:", + "emailField": "E-posta:", + "idpermissions": "Bu bot şunlara izin verecek:", + "instanceField": "Örnek:", + "loaddesc": "Bu uzun sürmemeli", + "loadingText": "Jank Client yükleniyor", + "loginButton": "Giriş Yap", + "noAccount": "Hesabınız yok mu?", + "openClient": "İstemciyi Aç", + "pw2Field": "Parolayı tekrar girin:", + "pwField": "Parola:", + "switchaccounts": "Hesapları Değiştir", + "trans": "Çevir", + "transDesc": "Fermi'yi kendi dilinize çevirmeye yardım edebilirsiniz!", + "transTitle": "Fermi'yi Çevir", + "userField": "Kullanıcı adı:", + "welcomeJank": "Jank Client'a Hoş Geldiniz" + }, + "instanceStats": { + "members": "Üyeler: $1", + "messages": "Mesajlar: $1", + "name": "Örnek istatistikleri: $1", + "servers": "Kulüpler: $1", + "users": "Kayıtlı kullanıcılar: $1" + }, + "interactions": { + "onlyYou": "Bu mesajı yalnızca siz görebilirsiniz" + }, + "invite": { + "accept": "Kabul et", + "alreadyJoined": "Zaten katıldınız", + "inviteLinkCode": "Davet Bağlantısı/Kodu", + "invitedBy": "$1 sizi davet etti", + "joinUsing": "Daveti kullanarak katıl", + "loginOrCreateAccount": "Giriş yapın veya hesap oluşturun ⇌", + "longInvitedBy": "$1 sizi $2'ya katılmaya davet etti", + "never": "Asla", + "noAccount": "Daveti kabul etmek için bir hesap oluşturun" + }, + "inviteOptions": { + "12h": "12 Saat", + "1d": "1 Gün", + "1h": "1 Saat", + "30d": "30 Gün", + "30m": "30 Dakika", + "6h": "6 Saat", + "7d": "7 Gün", + "limit": "$1 {{PLURAL:$1|kullanım}}", + "never": "Asla", + "noLimit": "Sınırsız", + "title": "Kişileri Davet Et" + }, + "leaveGuild": "Sunucudan ayrıl", + "leaving": "Spacebar'dan ayrılıyorsunuz", + "localuser": { + "2faCode:": "Kod:", "2faCodeGive": "Gizli anahtarınız: $1 ve 6 haneli, 30 saniyelik bir token süresi var", + "2faDisable": "2FA'yı Devre Dışı Bırak", + "2faEnable": "2FA'yı Etkinleştir", + "CheckUpdate": "Güncellemeleri kontrol et", + "PasswordsNoMatch": "Parolalar eşleşmiyor", + "TOSURL": "Hizmet Şartları URL'si:", + "VoiceWarning": "Bunu etkinleştirmek istediğinizden emin misiniz, bu çok deneysel ve sorunlara neden olabilir. (bu özellik geliştiriciler içindir, ne yaptığınızı bilmiyorsanız lütfen etkinleştirmeyin)", + "accentColor": "Vurgu rengi:", + "accountSettings": "Hesap Ayarları", + "addBot": "Bot ekle", + "addStatus": "Durum ekle", + "advancedBot": "Gelişmiş Bot Ayarları", + "appName": "Uygulama adı:", + "badCode": "Geçersiz kod", + "badPassword": "Yanlış parola", + "botAvatar": "Bot avatarı:", + "botInviteCreate": "Bot Davet Oluşturucu", + "botUsername": "Bot kullanıcı adı:", "changeDiscriminator": "Ayırıcıyı değiştir", - "newDiscriminator": "Yeni ayırıcı:", "changeEmail": "E-postayı değiştir", - "password:": "Parola", - "newEmail:": "Yeni e-posta", - "changeUsername": "Kullanıcı adını değiştir", - "newUsername": "Yeni kullanıcı adı:", "changePassword": "Parolayı değiştir", - "oldPassword:": "Eski parola:", - "newPassword:": "Yeni parola:", - "PasswordsNoMatch": "Parolalar eşleşmiyor", - "disableConnection": "Bu bağlantı sunucu tarafından devre dışı bırakıldı", - "devPortal": "Geliştirici Portalı", + "changeUsername": "Kullanıcı adını değiştir", + "clearCache": "Önbelleği temizle", + "confirmAddBot": "Bu uygulamaya bir bot eklemek istediğinizden emin misiniz? Geri dönüşü yok.", + "confirmReset": "Bot token'ini sıfırlamak istediğinizden emin misiniz? Botunuz, güncelleyene kadar çalışmayı durduracak.", + "confuseNoBot": "Nedense, bu uygulamanın henüz bir botu yok.", + "connections": "Bağlantılar", "createApp": "Uygulama oluştur", - "team:": "Takım:", - "appName": "Uygulama adı:", + "customSound": "Özel Ses", "description": "Açıklama:", + "devPortal": "Geliştirici Portalı", + "disableConnection": "Bu bağlantı sunucu tarafından devre dışı bırakıldı", + "editingBot": "Bot $1 düzenleniyor", + "install": "Yükle", + "installDesc": "Fermi'yi yüklemek onu farklı bir uygulama gibi açmanıza sağlayacaktır! Ayrıca Fermi'yi web tarayıcınızda kullanmaya devam edebilirsiniz ve aynı şekilde çalışacaktır.", + "installJank": "Fermi'yi yükle", + "language": "Dil:", + "manageBot": "Botu yönet", + "newDiscriminator": "Yeni ayırıcı:", + "newEmail:": "Yeni e-posta", + "newPassword:": "Yeni parola:", + "newUsername": "Yeni kullanıcı adı:", + "noToken": "Token'i bilmiyorum, bu yüzden localStorage'a kaydedemem, üzgünüm", + "notisound": "Bildirim sesi:", + "oldPassword:": "Eski parola:", + "password:": "Parola", "privacyPolcyURL": "Gizlilik politikası URL'si:", - "TOSURL": "Hizmet Şartları URL'si:", "publicAvaliable": "Botun herkese açık olarak davet edilmesine izin verilsin mi?", "requireCode": "Botu davet etmek için kod izni gerekiyor mu?", - "manageBot": "Botu yönet", - "addBot": "Bot ekle", - "confirmAddBot": "Bu uygulamaya bir bot eklemek istediğinizden emin misiniz? Geri dönüşü yok.", - "confuseNoBot": "Nedense, bu uygulamanın henüz bir botu yok.", - "editingBot": "Bot $1 düzenleniyor", - "botUsername": "Bot kullanıcı adı:", - "botAvatar": "Bot avatarı:", "resetToken": "Token'i sıfırla", - "confirmReset": "Bot token'ini sıfırlamak istediğinizden emin misiniz? Botunuz, güncelleyene kadar çalışmayı durduracak.", - "tokenDisplay": "Token: $1", "saveToken": "Token'i localStorage'a kaydet", - "noToken": "Token'i bilmiyorum, bu yüzden localStorage'a kaydedemem, üzgünüm", - "advancedBot": "Gelişmiş Bot Ayarları", - "botInviteCreate": "Bot Davet Oluşturucu", - "language": "Dil:" + "setUp2fa": "2FA Kurulumu", + "setUp2faInstruction": "Bu gizli anahtarı TOTP uygulamanıza kopyalayın", + "settings": "Ayarlar", + "soundTooLarge": "Yüklemeye çalıştığınız ses çok büyük, tekrar deneyin", + "status": "Durum", + "team:": "Takım:", + "theme:": "Tema", + "themesAndSounds": "Temalar & Sesler", + "tokenDisplay": "Token: $1", + "updateSettings": "Ayarları Güncelle", + "userSettings": "Kullanıcı Ayarları" }, - "instanceStats": { - "name": "Örnek istatistikleri: $1", - "users": "Kayıtlı kullanıcılar: $1", - "servers": "Sunucular: $1", - "messages": "Mesajlar: $1", - "members": "Üyeler: $1" + "login": { + "allGood": "Her şey yolunda", + "checking": "Örnek kontrol ediliyor", + "invalid": "Geçersiz örnek, tekrar deneyin", + "waiting": "Örneği kontrol etmek için bekleniyor" }, - "inviteOptions": { - "title": "Kişileri Davet Et", - "30m": "30 Dakika", - "1h": "1 Saat", - "6h": "6 Saat", - "12h": "12 Saat", - "1d": "1 Gün", - "7d": "7 Gün", - "30d": "30 Gün", - "never": "Asla", - "limit": "$1 kullanım", - "noLimit": "Sınırsız" + "logout": { + "error": { + "cancel": "İptal", + "cont": "Yine de devam et", + "desc": "Fermi sizi çıkış yaptıramadı,\n hesabı yine de kaldırmak istiyor musunuz?", + "title": "Oturumunuzu kapatmaya çalışırken bir hata oluştu" + }, + "logout": "Oturumu kapat" }, - "2faCode": "2FA kodu:", - "invite": { - "invitedBy": "$1 sizi davet etti", - "alreadyJoined": "Zaten katıldınız", - "accept": "Kabul et", - "noAccount": "Daveti kabul etmek için bir hesap oluşturun", - "longInvitedBy": "$1 sizi $2'ya katılmaya davet etti", - "loginOrCreateAccount": "Giriş yapın veya hesap oluşturun ⇌", - "joinUsing": "Daveti kullanarak katıl", - "inviteLinkCode": "Davet Bağlantısı/Kodu" + "media": { + "artist": "Sanatçı: $1", + "download": "Medyayı indir", + "length": "Süre: $1 dakika ve $2 saniye", + "loading": "Yükleniyor", + "moreInfo": "Daha fazla bilgi", + "notFound": "Medya bulunamadı" + }, + "member": { + "ban": "$1'ı $2'dan yasakla", + "kick": "$1'ı $2'dan at", + "reason:": "Sebep:" + }, + "message": { + "delete": "Mesajı Sil", + "deleted": "Silinmiş mesaj", + "edit": "Mesajı Düzenle", + "edited": "(düzenlendi)", + "reactionsTitle": "Tepkiler" + }, + "nevermind": "Boşver", + "no": "Hayır", + "noMessages": "Burada mesaj görünmüyor, ilk siz bir şey söyleyin!", + "notiClump": "$2'den $1 bildiriminiz var", + "notiVolume": "Bildirim sesi:", + "otherAt": "$1 tarihinde saat $2", + "permissions": { + "descriptions": { + "ADMINISTRATOR": "Tüm izinlere izin verir ve kanal izin geçersiz kılmalarını atlar. Bu tehlikeli bir izindir!", + "MANAGE_GUILD": "Sunucunun yönetimine ve düzenlenmesine izin verir", + "VIEW_AUDIT_LOG": "Kullanıcının denetim günlüğünü görüntülemesine izin verir", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "Abonelik içgörülerini görüntülemeye izin verir", + "VIEW_GUILD_INSIGHTS": "Kullanıcının sunucu içgörülerini görmesine izin verir", + "MENTION_EVERYONE": "Kullanıcının herkesi etiketlemesine izin verir", + "MANAGE_NICKNAMES": "Kullanıcının diğer üyelerin takma adlarını değiştirmesine izin verir", + "SEND_POLLS": "Anketler göndermeye izin verir", + "MANAGE_MESSAGES": "Kullanıcının kendisine ait olmayan mesajları silmesine izin verir", + "MANAGE_THREADS": "Kullanıcının konuları silmesine ve arşivlemesine ve tüm özel konuları görüntülemesine izin verir", + "MANAGE_CHANNELS": "Kullanıcının kanalları yönetmesine ve düzenlemesine izin verir", + "MANAGE_ROLES": "Kullanıcının rolleri düzenlemesine ve yönetmesine izin verir", + "MANAGE_WEBHOOKS": "Webhook'ların yönetimine ve düzenlenmesine izin verir", + "CREATE_EVENTS": "Zamanlanmış etkinlikler oluşturmaya ve kullanıcının oluşturduklarını düzenlemeye ve silmeye izin verir", + "MANAGE_EVENTS": "Kullanıcının etkinlikleri düzenlemesine ve yönetmesine izin verir", + "CREATE_GUILD_EXPRESSIONS": "Emoji, çıkartma ve ses panosu sesleri oluşturmaya ve kullanıcının oluşturduklarını düzenlemeye ve silmeye izin verir", + "MANAGE_GUILD_EXPRESSIONS": "Emojileri, çıkartmaları ve ses panosunu yönetmeye olanak tanır", + "MODERATE_MEMBERS": "Kullanıcının diğer kullanıcıları susturmasına izin verir", + "KICK_MEMBERS": "Kullanıcının sunucudan üyeleri atmasına izin verir", + "BAN_MEMBERS": "Kullanıcının sunucudan üyeleri yasaklamasına izin verir", + "MOVE_MEMBERS": "Kullanıcının üyeleri ses kanalları arasında taşımasına izin verir", + "MUTE_MEMBERS": "Kullanıcının diğer üyeleri susturmasına izin verir", + "DEAFEN_MEMBERS": "Kullanıcının diğer üyeleri sağırlamasına izin verir", + "CHANGE_NICKNAME": "Kullanıcının kendi takma adını değiştirmesine izin verir", + "VIEW_CHANNEL": "Kullanıcının kanalı görüntülemesine izin verir", + "READ_MESSAGE_HISTORY": "Kullanıcının mesaj geçmişini okumasına izin verir", + "ADD_REACTIONS": "Kullanıcının mesajlara tepki eklemesine izin verir", + "SEND_MESSAGES": "Kullanıcının mesaj göndermesine izin verir", + "SEND_MESSAGES_IN_THREADS": "Kullanıcının konularda mesaj göndermesine izin verir", + "SEND_TTS_MESSAGES": "Kullanıcının metinden sese mesajlar göndermesine izin verir", + "USE_EXTERNAL_EMOJIS": "Kullanıcının harici emojileri kullanmasına izin verir", + "USE_EXTERNAL_STICKERS": "Kullanıcının harici çıkartmaları kullanmasına izin verir", + "EMBED_LINKS": "Bu kullanıcı tarafından gönderilen bağlantıların otomatik olarak gömülmesine izin verir", + "ATTACH_FILES": "Kullanıcının dosya eklemesine izin verir", + "SEND_VOICE_MESSAGES": "Sesli mesajlar göndermeye izin verir", + "CREATE_INSTANT_INVITE": "Kullanıcının sunucu için davet oluşturmasına izin verir", + "CREATE_PUBLIC_THREADS": "Kullanıcının genel konular oluşturmasına izin verir", + "CREATE_PRIVATE_THREADS": "Kullanıcının özel konular oluşturmasına izin verir", + "CONNECT": "Kullanıcının bir ses kanalına bağlanmasına izin verir", + "SPEAK": "Kullanıcının bir ses kanalında konuşmasına izin verir", + "USE_VAD": "Kullanıcıların ses etkinliği ile konuşmasına izin verir", + "STREAM": "Kullanıcının yayın yapmasına izin verir", + "USE_SOUNDBOARD": "Ses panosunu bir ses kanalında kullanmaya izin verir", + "USE_EXTERNAL_SOUNDS": "Diğer kulüplerden özel ses panosu seslerini kullanmaya izin verir", + "PRIORITY_SPEAKER": "Ses kanalında öncelikli konuşmacı kullanımına izin verir", + "REQUEST_TO_SPEAK": "Kullanıcının sahne kanalında konuşma isteğinde bulunmasına izin verir", + "USE_EMBEDDED_ACTIVITIES": "Kullanıcının gömülü etkinlikleri kullanmasına izin verir", + "USE_APPLICATION_COMMANDS": "Kullanıcının uygulama komutlarını kullanmasına izin verir", + "USE_EXTERNAL_APPS": "Kullanıcı tarafından yüklenen uygulamaların herkese açık yanıtlar göndermesine izin verir. Devre dışı bırakıldığında, kullanıcılar yine de uygulamalarını kullanabilir ancak yanıtlar geçici olacaktır. Bu, kulübe yüklenmemiş uygulamalar için geçerlidir." + }, + "readableNames": { + "ADD_REACTIONS": "Tepkiler ekle", + "ADMINISTRATOR": "Yönetici", + "ATTACH_FILES": "Dosyaları ekle", + "BAN_MEMBERS": "Üyeleri yasakla", + "CHANGE_NICKNAME": "Takma adı değiştir", + "CONNECT": "Bağlan", + "CREATE_EVENTS": "Etkinlikler oluştur", + "CREATE_GUILD_EXPRESSIONS": "İfadeler oluştur", + "CREATE_INSTANT_INVITE": "Davet oluştur", + "CREATE_PRIVATE_THREADS": "Özel konular oluştur", + "CREATE_PUBLIC_THREADS": "Genel konular oluştur", + "DEAFEN_MEMBERS": "Üyeleri sağırla", + "EMBED_LINKS": "Bağlantıları göm", + "KICK_MEMBERS": "Üyeleri at", + "MANAGE_CHANNELS": "Kanalları yönet", + "MANAGE_EVENTS": "Etkinlikleri yönet", + "MANAGE_GUILD": "Sunucuyu yönet", + "MANAGE_GUILD_EXPRESSIONS": "İfadeleri yönet", + "MANAGE_MESSAGES": "Mesajları yönet", + "MANAGE_NICKNAMES": "Takma adları yönet", + "MANAGE_ROLES": "Rolleri yönet", + "MANAGE_THREADS": "Konuları yönet", + "MANAGE_WEBHOOKS": "Webhook'ları yönet", + "MENTION_EVERYONE": "@everyone, @here ve tüm rolleri etiketle", + "MODERATE_MEMBERS": "Üyeleri zaman aşımına uğrat", + "MOVE_MEMBERS": "Üyeleri taşı", + "MUTE_MEMBERS": "Üyeleri sustur", + "PRIORITY_SPEAKER": "Öncelikli konuşmacı", + "READ_MESSAGE_HISTORY": "Mesaj geçmişini oku", + "REQUEST_TO_SPEAK": "Konuşma isteğinde bulun", + "SEND_MESSAGES": "Mesaj gönder", + "SEND_MESSAGES_IN_THREADS": "Konularda mesaj gönder", + "SEND_POLLS": "Anketler oluştur", + "SEND_TTS_MESSAGES": "Metinden sese mesaj gönder", + "SEND_VOICE_MESSAGES": "Sesli mesajlar gönder", + "SPEAK": "Konuş", + "STREAM": "Video", + "USE_APPLICATION_COMMANDS": "Uygulama komutlarını kullan", + "USE_EMBEDDED_ACTIVITIES": "Etkinlikleri kullan", + "USE_EXTERNAL_APPS": "Harici uygulamaları kullan", + "USE_EXTERNAL_EMOJIS": "Harici emojileri kullan", + "USE_EXTERNAL_SOUNDS": "Harici sesleri kullan", + "USE_EXTERNAL_STICKERS": "Harici çıkartmaları kullan", + "USE_SOUNDBOARD": "Ses panosunu kullan", + "USE_VAD": "Ses etkinliği algılamayı kullan", + "VIEW_AUDIT_LOG": "Denetim günlüğünü görüntüle", + "VIEW_CHANNEL": "Kanalları görüntüle", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "İçerik üretici gelir analitiğini görüntüle", + "VIEW_GUILD_INSIGHTS": "Sunucu içgörülerini görüntüle" + } + }, + "pinMessage": "Mesajı Sabitle", + "profile": { + "bio": "Hakkımda:", + "mut": "Ortak Guildler", + "mutFriends": "Ortak Arkadaşlar", + "permInfo": "İzinler", + "userInfo": "Kullanıcı Bilgisi" }, + "profileColor": "Profil rengi", + "pronouns": "Zamirler:", + "readableName": "Türkçe", + "recentEmoji": "En Son Emoji", + "register": { + "DOBError": "Doğum Tarihi: $1", + "agreeTOS": "[Hizmet Şartlarını]($1) kabul ediyorum:", + "emailError": "E-posta: $1", + "noTOS": "Bu örneğin Hizmet Şartları yok, yine de TOS'u kabul et:", + "passwordError:": "Parola: $1", + "register": "Kaydol", + "tos": "Kullanım şartlarını kabul etmeniz gerekiyor", + "usernameError": "Kullanıcı adı: $1" + }, + "reply": "Yanıtla", "replyingTo": "$1 yanıtlıyor", - "DMs": { - "copyId": "DM kimliğini kopyala", - "markRead": "Okundu olarak işaretle", - "close": "DM'yi kapat" + "retrying": "Yeniden deneniyor...", + "role": { + "color": "Renk", + "confirmDelete": "$1 silmek istediğinizden emin misiniz?", + "copyId": "Rol kimliğini kopyala", + "delete": "Rolü Sil", + "deleting": "Rol siliniyor...", + "displaySettings": "Görüntü Ayarları", + "hoisted": "Listede göster:", + "mentionable": "Herkes bu rolü etiketleyebilir:", + "name": "Rol adı:", + "perms": "İzinler", + "remove": "Rolü kaldır", + "roleEmoji": "Rol emojisi:", + "roleFileIcon": "Rol Simgesi:", + "roles": "Roller" + }, + "searchGifs": "Tenor'da Arayın", + "settings": { + "clear": "Temizle", + "img": "Resim yükle", + "save": "Değişiklikleri kaydet", + "unsaved": "Dikkatli olun, kaydedilmemiş değişiklikleriniz var", + "updates": { + "serviceWorkerMode": { + "disabled": "Kapalı", + "enabled": "Açık", + "offlineOnly": "Yalnızca Çevrimdışı", + "title": "Servis Çalışanı ayarı" + } + } }, + "showBlockedMessages": "Bu kullanıcıyı engellediniz, $1 engellenen {{PLURAL:$1| mesaj|mesajları}} görmek için tıklayın.", + "spoiler": "Spoiler", + "submit": "Gönder", + "switchAccounts": "Hesapları Değiştir ⇌", + "todayAt": "Bugün saat $1", + "typing": "$2 {{PLURAL:$1|yazıyor|yazıyorlar}}", + "unableToConnect": "Spacebar sunucusuna bağlanılamıyor. Lütfen çıkış yapıp tekrar deneyin.", + "unableToPin": "Mesaj sabitlenemiyor", + "uploadBanner": "Afiş yükle:", + "uploadPfp": "Profil resmi yükle:", "user": { - "copyId": "Kullanıcı kimliğini kopyala", - "online": "Çevrimiçi", - "offline": "Çevrimdışı", - "message": "Kullanıcıya mesaj gönder", + "addRole": "Roller ekle", + "ban": "Üyeyi yasakla", "block": "Kullanıcıyı engelle", - "unblock": "Engeli kaldır", + "copyId": "Kullanıcı kimliğini kopyala", "friendReq": "Arkadaşlık isteği", "kick": "Üyeyi at", - "ban": "Üyeyi yasakla", - "addRole": "Roller ekle", - "removeRole": "Roller kaldır" + "message": "Kullanıcıya mesaj gönder", + "offline": "Çevrimdışı", + "online": "Çevrimiçi", + "removeRole": "Roller kaldır", + "unblock": "Engeli kaldır" }, - "login": { - "checking": "Örnek kontrol ediliyor", - "allGood": "Her şey yolunda", - "invalid": "Geçersiz örnek, tekrar deneyin", - "waiting": "Örneği kontrol etmek için bekleniyor" + "vc": { + "joinForStream": "İzlemek için Sesli Kanal'a katılın.", + "joiningStream": "Yayına katılınılıyor...", + "joinstream": "Yayını izle", + "leavestream": "Yayından ayrıl", + "stopstream": "Yayını Durdur" }, - "member": { - "kick": "$1'ı $2'dan at", - "reason:": "Sebep:", - "ban": "$1'ı $2'dan yasakla" + "webhooks": { + "base": "Webhook'lar", + "channel": "Kanal", + "copyURL": "Webhook URL'sini kopyala", + "createdAt": "$1 tarihinde oluşturuldu", + "name": "Ad:", + "newWebHook": "Yeni Webhook" }, - "errorReconnect": "Sunucuya bağlanılamadı, **$1** saniye içinde yeniden denenecek...", - "retrying": "Yeniden deneniyor...", - "unableToConnect": "Spacebar sunucusuna bağlanılamıyor. Lütfen çıkış yapıp tekrar deneyin." + "yes": "Evet", + "yesterdayAt": "Dün saat $1" } diff --git a/translations/vi.json b/translations/vi.json new file mode 100644 index 00000000..33b58c15 --- /dev/null +++ b/translations/vi.json @@ -0,0 +1,17 @@ +{ + "@metadata": { + "authors": [ + "Bapham123", + "Duongvu" + ] + }, + "logout": { + "error": { + "cancel": "Hủy bỏ" + }, + "logout": "Đăng xuất" + }, + "pinMessage": "Ghim tin nhắn", + "readableName": "Tiếng Anh", + "reply": "Trả lời" +} diff --git a/translations/zh-hans.json b/translations/zh-hans.json new file mode 100644 index 00000000..27796c92 --- /dev/null +++ b/translations/zh-hans.json @@ -0,0 +1,966 @@ +{ + "@metadata": { + "authors": [ + "0x48fe75df30", + "22593", + "ExoHyper2026", + "Gsdn", + "GuoPC", + "Key0121", + "Pawsdt", + "Peterxy12", + "Plana", + "Wilf233" + ] + }, + "2faCode": "双因素认证代码:", + "404": { + "404": "错误:404——页面未找到", + "app": "前往应用程序", + "blog": "Fermi 博客", + "home": "首页", + "listtitle": "也许您是想去这些地方", + "login": "登录", + "report": "如果您在客户端发现此页面,请回报:", + "reset": "密码重置页面", + "title": "看来您迷路了", + "whatelse": "您认为应该还会发生什么事?", + "whereever": "无论这在哪里" + }, + "onboarding": { + "name": "入门", + "disable": "禁用入门", + "addChannel": "添加频道", + "channel": "频道:", + "desc": "描述:", + "deleteChannel": "从入门中删除频道", + "enable": "启用入门", + "title": "欢迎来到$1!" + }, + "DMs": { + "add": "将某人添加到此私信", + "close": "关闭私信", + "copyId": "复制私信 ID", + "markRead": "标记为已读", + "name": "私信" + }, + "ok": "确定", + "dismiss": "关闭", + "UrlGen": "机器人邀请生成器", + "Voice": { + "userVol": "用户数量:", + "status": { + "badWS": "WS 回应不佳", + "conectionFailed": "连接失败", + "done": "已连接", + "left": "已离开语音聊天", + "makingOffer": "产生 offer", + "noSDP": "无 SDP", + "notconnected": "未连接", + "sendingStreams": "发送音频流", + "startingRTC": "启动 RTC 连接", + "waitingMainWS": "等待主 WS", + "waitingURL": "等待语音 URL", + "wsAuth": "等待 WS 授权", + "wsOpen": "等待 Web 打开" + } + }, + "accessibility": { + "gifSettings": { + "always": "总是", + "hover": "悬停", + "never": "永不" + }, + "name": "无障碍", + "playGif": "播放动图:", + "playIcon": "播放动态图标:", + "roleColors": "禁用角色颜色:", + "gradientColors": "禁用渐变着色:", + "decorations": "启用头像装饰:" + }, + "accountNotStart": "账号无法启动", + "add": "添加", + "applications": { + "delete": "删除应用程序", + "sure": "您确定要删除$1吗?" + }, + "badge": { + "active_developer": "活跃开发者", + "application_guild_subscription": "拥有高级会员", + "automod": "使用自动模式", + "bot_commands": "支持的命令", + "bug_hunter_level_1": "程序错误猎人 1 级", + "bug_hunter_level_2": "程序错误猎人 2 级", + "certified_moderator": "版主", + "early_supporter": "早期支持者", + "guild_booster_lvl1": "提升公会", + "guild_booster_lvl2": "提升公会", + "guild_booster_lvl3": "提升公会", + "guild_booster_lvl4": "提升公会", + "guild_booster_lvl5": "提升公会", + "guild_booster_lvl6": "提升公会", + "guild_booster_lvl7": "提升公会", + "guild_booster_lvl8": "提升公会", + "guild_booster_lvl9": "已提升了公会一段时间", + "hypesquad": "氛围小队[占位符]", + "hypesquad_house_1": "勇敢之家", + "hypesquad_house_2": "卓越之家", + "hypesquad_house_3": "均衡之家", + "legacy_username": "拥有旧用户名", + "partner": "实例合作伙伴", + "premium": "溢价", + "quest_completed": "完成了一项任务", + "staff": "实例人员", + "verified_developer": "已验证开发者" + }, + "bio": "个人简介:", + "blankMessage": "空白消息", + "blog": { + "blog": "博客", + "blogUpdates": "博客更新:", + "desc": "阅读或订阅博客,获取 Fermi 的最新动态!", + "fermi": "Fermi 博客", + "gotoPost": "前往帖子", + "wantUpdates": "您想收到有关 Fermi 状态的每周博客更新吗?" + }, + "bot": "机器人", + "botGuilds": "公会机器人位于:", + "botSettings": "机器人设置", + "cancel": "取消", + "channel": { + "SlowmodeCool": "慢速模式冷却时间:$1", + "TimeOutCool": "超时至:$1", + "allowIcons": "允许自定义频道图标", + "announcement": "公告", + "copyId": "复制频道 ID", + "copyIdCat": "复制分类 ID", + "createCatagory": "创建分类", + "createChannel": "创建频道", + "creating": "创建频道", + "delete": "删除频道", + "deleteCat": "删除分类", + "icon": "图标:", + "makeInvite": "发出邀请", + "markRead": "标记为已读", + "mute": "静音频道", + "name": "频道", + "name:": "频道名称:", + "nsfw:": "工作场所不宜/成人内容警告:", + "permissions": "权限", + "selectCatName": "分类名称", + "selectName": "频道名称", + "selectType": "选择频道类型", + "settings": "设置", + "settingsFor": "$1的设置", + "slowmode": "慢速模式:", + "text": "文字", + "timedOutUntil": "超时至:$1", + "topic:": "主题:", + "typebox": "在$1的消息", + "unmute": "取消静音频道", + "voice": "语音", + "deleteThread": "删除帖子", + "hideThreads": "不活跃时隐藏", + "forum": "论坛" + }, + "threads": { + "leave": "留下帖子", + "join": "加入帖子", + "name": "帖子名称:", + "editTags": "编辑标签" + }, + "forum": { + "creorsear": "创建或搜索帖子", + "next": "下一步", + "back": "返回", + "newPost": "发布新帖子", + "post": "帖子", + "sortOptions": { + "sortby": { + "title": "排序方式", + "recent": "最近活跃", + "posted": "发布日期" + }, + "sortOrder": { + "title": "排序顺序", + "recent": "按时间倒序", + "old": "按时间正序" + }, + "tagMatch": { + "title": "标签匹配", + "some": "匹配一些", + "all": "匹配所有标签" + }, + "name": "排序选项" + }, + "errors": { + "tagsReq": "标签是必需的", + "requireText": "消息不得为空" + }, + "allTags": "所有标签", + "settings": { + "editTags": "编辑标签:", + "editTag": "编辑标签", + "tagName": "标签名称:", + "moderated": "仅允许版主添加标签:", + "addTag": "添加标签", + "delTag": "删除标签" + } + }, + "channelLink": "# $1", + "clientDesc": "客户端版本:$1 \n\n[加入 Fermi 官方公会]($2/invite\n/USgYJo?instance=https%3A%2F%2Fspacebar.chat)\n\n[帮助翻译 Fermi](https://translatewiki.net/wiki/Translating:JankClient#sortable:3=desc)\n\n[帮助开发 Fermi](https://github.com/MathMan05/Fermi)\n\n[帮助维护 Fermi 所依赖的服务器](https://github.com/spacebarchat/server)\n\n[阅读博客](https://blog.fermi.chat/)\n\n计算权限:$3", + "commands": { + "errorNotValid": "$1不是$2的有效选择", + "required": "$1是此命令的必要组成部分" + }, + "confirmGuildLeave": "您确定您要离开$1", + "copyLink": "复制消息链接", + "copyRegLink": "复制链接", + "copymessageid": "复制消息ID", + "copyrawtext": "复制原始文本", + "createAccount": "创建账号", + "delete": "删除", + "deleteConfirm": "您确定要删除此内容吗?", + "devSettings": { + "badUser": "启用对发送过多信息的恶意用户对象的日志记录:", + "cache": "启用 Service Worker 缓存映射文件", + "cacheDesc": "无论哪种方式,map 文件仍然会加载,这是确保在新的更新推出时它们已在缓存中。", + "captureTrace": "此设置会指示 Fermi 从服务器捕获 _trace 属性,启用此设置可能会导致渐进式 JSON 解码无法运作(可能需要重新加载)。", + "clearWellKnowns": "重新获取.well-known记录:", + "description": "这些设置是给 Spacebar/Fermi 的开发者用的,如果你不知道自己在做什么,请不要更改这些设置。", + "gatewayComp": "禁用网关压缩:", + "reportSystem": "启用实验性报告系统:", + "logGateway": "记录接收到的网关事件(日志级别信息):", + "name": "开发者设置", + "traces": "暴露痕迹:" + }, + "deviceManage": { + "city": "城市:$1", + "continent": "大陆:$1", + "country": "国家:$1", + "curSes": "这是当前会话,您需要通过另一个菜单注销。", + "estimateWarn": "警告:所有这些信息仅为最合理的推测,可能存在错误。", + "ip": "最后已知的IP地址:$1", + "last": "大致最后使用时间:$1", + "latitude": "纬度:$1", + "logout": "退出", + "longitude": "经度:$1", + "manageDev": "管理设备", + "postal": "邮政编码:$1", + "region": "地区:$1", + "title": "管理会话" + }, + "discovery": "探索", + "dms": "私信", + "edit": "编辑", + "emoji": { + "confirmDel": "您确定要删除此表情符号吗?", + "found": { + "not": "此表情符号不属于您的公会,请加入发布该表情的公会以使用它", + "other": "此表情符号来自您的其他公会。", + "private": "找不到此表情符号的公会", + "this": "此表情符号来自此公会。" + }, + "from": "此表情符号来自", + "image:": "图片:", + "join": "加入公会", + "name:": "名称:", + "privateGuild": "此为私密公会", + "title": "表情符号", + "upload": "上传emoji" + }, + "emojiSelect": "表情符号:", + "errorReconnect": "无法连接到服务器,将在 **$1** 秒后重试…", + "favoriteGifs": "收藏的GIF", + "folder": { + "color": "文件夹颜色:", + "create": "创建新文件夹", + "edit": "编辑文件夹", + "name": "文件夹名称:" + }, + "form": { + "captcha": "等等,你是人类吗?" + }, + "friends": { + "addfriend": "添加朋友", + "addfriendpromt": "通过用户名添加好友:", + "requestsent": "请求已发送!", + "all": "全部", + "all:": "全部好友:", + "blocked": "已封禁", + "blockedusers": "被封禁用户:", + "bu": "封禁的用户", + "discnotfound": "未找到鉴别器", + "friendlist": "好友列表", + "friends": "朋友", + "notfound": "用户未找到", + "online": "在线", + "online:": "在线好友:", + "pending": "待处理", + "pending:": "待处理的好友请求:", + "removeFriend": "删除好友" + }, + "goThere": "是的,去吧", + "goThereTrust": "确认并自此确信该域名", + "goingToURL": "该超链接将会使您跳转至$1。您确定要去那里吗?", + "group": { + "createdm": "创建私信!", + "edit": "编辑群聊", + "select": "选择好友" + }, + "guild": { + "COMMUNITY": "申请加入", + "disableInvites": "禁用邀请:", + "DISCOVERABLE": "探索", + "INVITES_DISABLED": "仅邀請", + "adminMenu": { + "changePerms": "更改权限以查找", + "finding": "寻找管理员", + "ownName": "所有者", + "owner": "寻找所有者", + "permission": "权限:" + }, + "report": "举报公会", + "admins": "寻找管理员", + "all": "所有", + "banReason": "禁止原因:$1", + "bannedBy": "被禁止由:", + "banner:": "横幅:", + "bans": "禁止", + "boostMessage?": "当有人提升您的公会时发送讯息!", + "community": "社群", + "confirmDelete": "您确定要删除$1吗?", + "confirmLeave": "您确定您要离开吗?", + "copyId": "复制公会 ID", + "create": "从头开始创建公会", + "createFromTemplate": "从模板创建公会", + "createNewTemplate": "创建新模板", + "creating": "创建公会", + "default": "默认值($1)", + "defaultNoti": "设置您的公会的默认通知设置!", + "delete": "删除公会", + "description:": "描述:", + "disoveryTitle": "公会发现($1 {{PLURAL:$1|项目}})", + "editingTemplate": "编辑$1", + "emptytext": "很奇怪,这个公会没有频道!?", + "emptytitle": "奇怪的地方", + "guilds": "公会", + "helpTips?": "为您的公会发送有用的提示!", + "howJoin": "其他人如何加入您的公会?", + "icon:": "图标:", + "invites": "邀请", + "joinConfirm": "加入$1 ?", + "leave": "离开公会", + "loadingDiscovery": "加载中…", + "makeInvite": "发出邀请", + "markRead": "标记为已读", + "memberCount": "$1个{{PLURAL:$1|成员}}", + "mute": "静音公会", + "name:": "名称:", + "nameNoMatch": "名称不符", + "noDelete": "没关系", + "noLeave": "没关系", + "none": "无", + "notifications": "通知", + "onlyMentions": "仅限@提及", + "overview": "概要", + "region:": "区域:", + "roles": "角色", + "ruleId": "规则频道:", + "selectnoti": "选择通知类型:", + "sendrandomwelcome?": "当有人加入此公会时发送随机信息:", + "serverName": "公会名称:", + "settings": "设置", + "settingsFor": "$1的设置", + "splash": "发现亮点:", + "stickWelcomeReact?": "当有人加入公会时,提示您的公会成员使用贴纸做出反应!", + "systemSelect:": "系统消息频道:", + "tempCreatedBy": "模板创建者:", + "tempUseCount": "模板已使用$1{{PLURAL:$1|次}}", + "template": "用于创建公会的模板链接/代码:", + "templateDesc": "模板描述:", + "templateName": "模板名称:", + "templateNameShort": "模板名称必须至少包含 2 个字符", + "templateURL": "模板链接:$1", + "templates": "模板", + "templcateMetaDesc": "模板允许其他人使用该公会作为自己公会的基础内容:这会复制该公会的频道、角色和设置,但不会复制公会内部的消息、机器人或公会图标。", + "topic:": "主题:", + "unknownRole": "@不明角色", + "viewTemplate": "查看模板", + "yesDelete": "是的,我确定", + "yesLeave": "是的,我确定" + }, + "hideBlockedMessages": "您已封禁此用户,点击即可隐藏这些消息。", + "home": { + "icon": "$1的实例图标", + "uptimeStats": "运作时间:\n 所有时间:$1%\n本周:$2%\n今日:$3%", + "warnOffiline": "实例已离线,无法连接" + }, + "htmlPages": { + "addBot": "添加到公会", + "alreadyHave": "已经拥有账号?", + "box1Items": "私信|反应支持|邀请|账号切换|用户设置|开发者门户|机器人邀请|翻译支持", + "box1title": "Fermi 是一款与 Spacebar 兼容的客户端,致力于尽所能提供多项功能,包括:", + "box3description": "我们始终感谢大家的帮助,无论是提交错误报告、代码,还是指出拼写错误。", + "box3title": "为 Fermi 做出贡献", + "compatableInstances": "Spacebar 相容实例:", + "createAccount": "创建账号", + "dobField": "出生日期:", + "emailField": "电子邮件:", + "idpermissions": "这将允许机器人:", + "instanceField": "实例:", + "loaddesc": "这应该不会花太长时间", + "loadingText": "Fermi 正在加载", + "loginButton": "登录", + "noAccount": "还没有账号?", + "openClient": "打开客户端", + "pw2Field": "再次输入密码:", + "pwField": "密码:", + "switchaccounts": "切换账号", + "trans": "翻译", + "transDesc": "您可以帮助将 Fermi 翻译成您自己的语言!", + "transTitle": "翻译 Fermi", + "userField": "用户名:", + "welcomeJank": "欢迎来到 Fermi" + }, + "incorrectURLS": "## 此实例可能发送了错误的链接。 \n### 如果您是实例所有者,请参阅[此处](https://docs.spacebar.chat/setup/server/)下的“从远程电脑连线”来修复此问题。 \n 您想要 Fermi 自动尝试修复此错误,以让您连接到实例吗?", + "instInfo": "实例信息", + "instanceInfo": { + "contact": "电子邮件联系实例管理员", + "frontPage": "[前端页面]($1)", + "name": "实例信息", + "publicUrl": "[公共链接]($1)", + "tosPage": "[服务条款页面]($1)" + }, + "instanceStats": { + "members": "成员:$1", + "messages": "消息:$1", + "name": "实例统计:$1", + "servers": "公会:$1", + "users": "注册用户:$1" + }, + "interactions": { + "ephemeralDismiss": "忽略消息", + "failed": "交互失败", + "nonsence": "服务器发送了一个类型为$1的组件,但该组件不存在", + "notImpl": "Fermi 尚不可執行组件类型$1", + "onlyYou": "只有您可看到此消息", + "replyline": "开始交互", + "started": "处理交互...", + "worked": "交互成功" + }, + "invite": { + "accept": "接受", + "alreadyJoined": "已加入", + "channel:": "频道:", + "createInvite": "创建邀请", + "createdAt": "创建于 $1", + "expireAfter": "有效期至:", + "expires": "有效期至:$1", + "forChannel": "用于频道:$1", + "inviteLinkCode": "邀请链接/代码", + "inviteMaker": "邀请制作者", + "invitedBy": "您已收到来自$1的邀请", + "joinUsing": "使用邀请加入公会", + "loginOrCreateAccount": "登录或建立账号⇌", + "longInvitedBy": "$1邀请您加入$2", + "maxUses": "最多:$1", + "never": "永不", + "noAccount": "创建账号以接受邀请", + "notFound": "找不到邀请", + "subtext": "到$2里的$1", + "used": "使用了$1{{PLURAL:$1|次}}:" + }, + "inviteOptions": { + "12h": "12 小时", + "1d": "1 天", + "1h": "1 小时", + "30d": "30 天", + "30m": "30 分钟", + "6h": "6 小时", + "7d": "7 天", + "limit": "$1次{{PLURAL:$1|使用}}", + "never": "永不", + "noLimit": "无限制", + "title": "邀请他人" + }, + "jankInfo": "客户端信息", + "leaveGuild": "离开公会", + "leaving": "您正在离开 Spacebar", + "loaded": "已加载", + "localuser": { + "2faCode:": "代码:", + "2faCodeGive": "您的TOTP(基于时间的一次性密码)秘钥为:$1。这将生成一个6位的双因素验证令牌,有效期为30秒。", + "2faDisable": "禁用双因素验证", + "2faEnable": "启用双因素验证", + "CheckUpdate": "检查更新", + "PasswordsNoMatch": "密码不匹配", + "TOSURL": "服务条款链接:", + "VoiceWarning": "您确定要启用这个功能吗?这是非常实验性的,可能会引发问题。(此功能仅供开发者使用,如果您不确定操作,请不要启用。)", + "accentColor": "主题强调色:", + "accountSettings": "账户设置", + "addBot": "添加机器人", + "addStatus": "添加状态", + "advancedBot": "高级机器人设置", + "appName": "应用程序名称:", + "areYouSureDelete": "您确定要删除您的账号吗?如果是,请输入$1", + "badCode": "无效代码", + "badPassword": "密码不正确", + "botAvatar": "机器人头像:", + "botInviteCreate": "机器人邀请创建者", + "botUsername": "机器人用户名:", + "changeDiscriminator": "更改鉴别器", + "changeEmail": "更改电子邮件", + "changePassword": "更改密码", + "changeUsername": "更改用户名", + "clearCache": "清除缓存", + "confirmAddBot": "您确定要在此应用程序中添加机器人吗?此操作无法取消。", + "confirmReset": "您确定要重置机器人令牌吗?您的机器人将停止工作,直到您更新为止。", + "confuseNoBot": "出于某种原因,此应用程序目前还没有机器人。", + "connections": "连接", + "createApp": "创建应用程序", + "customSound": "自定义音效", + "customStatusWarn": "Spacebar 目前不支持显示自定义状态,因此它虽然会接受状态但不会对其做出任何行为", + "deleteAccount": "删除账号", + "deleteAccountButton": "删除账号", + "description": "描述:", + "devPortal": "开发者门户", + "disableConnection": "此连接已在服务器端禁用。", + "editingBot": "编辑机器人$1", + "install": "安装", + "installDesc": "安装 Fermi 后,您可以在其单独的窗口中开启,就像 Fermi 自己的应用程序一样!您也可以像以前那样在网页浏览器中使用 Fermi,功能是相同的。", + "installJank": "安装 Fermi", + "language": "语言:", + "manageBot": "管理机器人", + "manageInstance": "管理实例", + "mustTypePhrase": "要删除您的账号,您必须输入短语", + "newDiscriminator": "新鉴别器:", + "newEmail:": "新电子邮件", + "newPassword:": "新密码:", + "newUsername": "新用户名:", + "noToken": "很抱歉,因未能得知令牌而无法保存到本地存储", + "noUpdates": "找不到更新", + "notisound": "通知音效:", + "oldPassword:": "旧密码:", + "password:": "密码", + "privacyPolcyURL": "隐私政策链接:", + "appID": "应用程序 ID:$1", + "showSecret": "显示客户端密钥", + "clientSecret": "客户端密钥:$1", + "secret": "密钥", + "publicAvaliable": "允许公开邀请机器人吗?", + "refreshPage": "刷新以应用", + "requireCode": "需要代码授予才能邀请机器人?", + "resetToken": "重置令牌", + "saveToken": "将令牌保存到本地存储", + "setUp2fa": "双因素认证设置", + "setUp2faInstruction": "将该密钥复制到您的TOTP(基于时间的一次性密码)应用程序中以生成您的2FA(双因素验证)登录代码。", + "settings": "设置", + "sillyDeleteConfirmPhrase": "Shrek is love, Shrek is life", + "soundTooLarge": "您尝试上传的音频文件过大,请重试", + "status": "状态", + "team:": "团队:", + "theme:": "主题", + "themesAndSounds": "主题与音效", + "tokenDisplay": "令牌:$1", + "trace": "轨迹", + "trusted": "受信任域名", + "trustedDesc": "当您点击链接跳转到这些域名时,系统***不会***像其他链接那样提示您授予打开权限,请仅对您信任的域名授予权限,例如https://fermi.chat。", + "updateSettings": "更新设置", + "updatesYay": "找到了更新!", + "userSettings": "您的公开个人资料" + }, + "login": { + "allGood": "一切顺利", + "checking": "检查实例", + "enterPAgain": "再次输入新密码:", + "invalid": "实例无效,请重试", + "login": "登录", + "newPassword": "新密码:", + "pasteInfo": "在此处粘贴恢复链接:", + "recover": "忘记密码?", + "recovery": "忘记密码", + "waiting": "等待检查实例中" + }, + "logout": { + "error": { + "cancel": "取消", + "cont": "仍要继续", + "desc": "Fermi 无法将您登出,您是否仍要移除该账号?", + "title": "尝试将您登出时发生错误" + }, + "logout": "登出" + }, + "manageInstance": { + "AreYouSureStop": "您确定要停止此实例吗?", + "TokenFormats": { + "JSON": "JSON 格式", + "URLs": "邀请链接", + "plain": "纯文本" + }, + "clientURL": "客户端链接:", + "copy": "复制", + "count": "数量:", + "create": "创建", + "createTokens": "创建注册令牌", + "format": "格式:", + "genericType": "通用", + "length": "长度:", + "regType": "注册令牌链接类型", + "stop": "停止实例" + }, + "media": { + "artist": "艺术家:$1", + "composer": "作曲家:$1", + "download": "下载媒体", + "length": "时长:$1分$2秒", + "loading": "加载中", + "moreInfo": "更多信息", + "notFound": "找不到媒体" + }, + "member": { + "ban": "从$2禁止$1", + "kick": "从$2踢出$1", + "nick:": "昵称:", + "persist": "仅删除用户:", + "reason:": "原因:", + "timeout": "超时 $1", + "timeoutTime": "超时期限:" + }, + "message": { + "messages": "$1条{{PLURAL:$1|消息}}>", + "andMore": "$1,还有更多!", + "attached": "已发送附件", + "delete": "删除消息", + "report": "举报消息", + "deleted": "已删除消息", + "edit": "编辑消息", + "edited": "(已编辑)", + "fullMessage": "完整消息:", + "pin": "$1已置顶一条消息", + "thread": "$1已创建一条讨论消息:$2", + "reactedWith": "以$1反应", + "reactionAdd": "添加反应", + "createThread": "创建帖子", + "threadOptions": "帖子选项", + "reactions": "查看反应", + "reactionsTitle": "回应", + "retry": "重新发送错误消息", + "viewrest": "查看剩余内容" + }, + "report": { + "back": "返回", + "next": "下一步", + "cancel": "取消", + "submit": "提交", + "blockUser": "封禁用户", + "timeout": "超时的成员", + "summary": "举报摘要:" + }, + "messageLink": "在 # $1 的消息", + "muteDuration": "取消静音:", + "nevermind": "没关系", + "no": "否", + "noEmojiFont": "使用系统表情符号", + "noMessages": "这里还没有消息,来抢先发表意见!", + "noPins": "这里目前并未对你有用的东西……", + "noStickers": "很不幸,这里没有贴纸可以供您使用!", + "notiClump": "您已收到来自$2的$1通知", + "notiVolume": "通知音量:", + "otherAt": "$1于$2", + "permissions": { + "descriptions": { + "ADMINISTRATOR": "授予该身份组成员所有权限,并无视'''所有'''频道权限覆写。只有'''您'''和您绝对信任的成员/机器人才应拥有这个'''危险'''的权限!!", + "MANAGE_GUILD": "允许该身份组成员管理和编辑公会。", + "VIEW_AUDIT_LOG": "允许该身份组成员查看审计日志。", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "允许该身份组成员查看角色订阅洞见。", + "VIEW_GUILD_INSIGHTS": "允许该身份组成员查看公会洞见。", + "BYPASS_SLOWMODE": "慢速模式不再影响该身份组成员", + "MENTION_EVERYONE": "允许该身份组成员用@提及所有人。", + "MANAGE_NICKNAMES": "允许该身份组成员更改其他成员的昵称。", + "PIN_MESSAGES": "允许该身份组成员置顶消息", + "SEND_POLLS": "允许该身份组成员发起投票。", + "MANAGE_MESSAGES": "允许该身份组成员删除不属于自己的消息。", + "MANAGE_THREADS": "允许该身份组成员删除和归档帖子,并查看所有私密帖子。", + "MANAGE_CHANNELS": "允许该身份组成员编辑/删除频道。", + "MANAGE_ROLES": "允许该身份组成员编辑/删除身份组。", + "MANAGE_WEBHOOKS": "允许该身份组成员编辑/删除Webhook.", + "CREATE_EVENTS": "允许该身份组成员创建计划事件。", + "MANAGE_EVENTS": "允许身份组成员编辑/删除已有和未来活动。", + "CREATE_GUILD_EXPRESSIONS": "允许该身份组成员创建表情符号(emoji、贴纸和音效)。", + "MANAGE_GUILD_EXPRESSIONS": "允许该身份组成员编辑/删除表情符号(emoji、贴纸和音效)。", + "MODERATE_MEMBERS": "允许该身份组成员禁言公会成员(阻止其在聊天和主题帖中发送或回复消息,以及在语音频道和表演频道中发言)。", + "KICK_MEMBERS": "允许该身份组成员将成员移出公会。", + "BAN_MEMBERS": "允许该身份组成员封禁公会成员。", + "MOVE_MEMBERS": "允许该身份组成员在语音频道之间移动成员。", + "MUTE_MEMBERS": "允许该身份组成员在语音频道中将其他成员静音。", + "DEAFEN_MEMBERS": "允许该身份组成员在语音频道中屏蔽其他成员(屏蔽成员也会自动将其静音)。", + "CHANGE_NICKNAME": "允许该身份组成员更改自己的昵称", + "VIEW_CHANNEL": "允许该身份组成员查看文本频道。", + "READ_MESSAGE_HISTORY": "允许该身份组成员查看文本频道的消息历史记录。", + "ADD_REACTIONS": "允许该身份组成员在文本频道中对消息添加回应。", + "SEND_MESSAGES": "允许该身份组成员在文本频道中发送消息。", + "SEND_MESSAGES_IN_THREADS": "允许该身份组成员在帖子中发送消息。", + "SEND_TTS_MESSAGES": "允许该身份组成员在文本频道中发送文本转语音消息。", + "USE_EXTERNAL_EMOJIS": "允许该身份组成员在文本频道中使用外部表情符号。", + "USE_EXTERNAL_STICKERS": "允许该身份组成员在文本频道中使用外部贴纸。", + "EMBED_LINKS": "允许该身份组成员发送的链接自动嵌入到文本频道中。", + "ATTACH_FILES": "允许该身份组成员在文本频道中附加文件。", + "SEND_VOICE_MESSAGES": "允许在文本频道中发送语音消息。", + "CREATE_INSTANT_INVITE": "允许该身份组成员为公会创建邀请。", + "CREATE_PUBLIC_THREADS": "允许该身份组成员创建公开帖子", + "CREATE_PRIVATE_THREADS": "允许该身份组成员创建私密帖子", + "CONNECT": "允许该身份组成员连接到语音频道。", + "SPEAK": "允许该身份组成员在语音频道中发言", + "USE_VAD": "允许该身份组成员通过简易对话在语音频道中发言(禁用此功能后,用户只能通过按住按钮来说话)。", + "STREAM": "允许该身份组成员在语音频道中直播。", + "USE_SOUNDBOARD": "允许该身份组成员在语音频道中使用音效板。", + "USE_EXTERNAL_SOUNDS": "允许该身份组成员在语音频道中使用其他公会的自定义音效。", + "PRIORITY_SPEAKER": "允许该身份组成员在语音通道中优先发言。", + "REQUEST_TO_SPEAK": "允许该身份组成员请求在表演频道发言。", + "USE_EMBEDDED_ACTIVITIES": "允许该身份组成员使用嵌入式活动。", + "USE_APPLICATION_COMMANDS": "允许该身份组成员使用应用程序命令。", + "USE_EXTERNAL_APPS": "允许该身份组成员以应用程序产生的回复在频道中公开显示(禁用此功能后,用户仍可使用其应用,但回复将仅他们自己可见。此功能仅适用于未安装到公会的应用程序)。" + }, + "readableNames": { + "ADD_REACTIONS": "添加反应", + "ADMINISTRATOR": "管理员", + "ATTACH_FILES": "附加文件", + "BAN_MEMBERS": "禁止成员", + "BYPASS_SLOWMODE": "绕过慢速模式", + "CHANGE_NICKNAME": "更改昵称", + "CONNECT": "连接", + "CREATE_EVENTS": "创建活动", + "CREATE_GUILD_EXPRESSIONS": "创建表达", + "CREATE_INSTANT_INVITE": "创建邀请", + "CREATE_PRIVATE_THREADS": "创建私密帖子", + "CREATE_PUBLIC_THREADS": "创建公开帖子", + "DEAFEN_MEMBERS": "蒙蔽成员", + "EMBED_LINKS": "嵌入链接", + "KICK_MEMBERS": "踢出成员", + "MANAGE_CHANNELS": "管理频道", + "MANAGE_EVENTS": "管理活动", + "MANAGE_GUILD": "管理公会", + "MANAGE_GUILD_EXPRESSIONS": "创建表达", + "MANAGE_MESSAGES": "管理消息", + "MANAGE_NICKNAMES": "管理昵称", + "MANAGE_ROLES": "管理角色", + "MANAGE_THREADS": "管理帖子", + "MANAGE_WEBHOOKS": "管理 Webhook", + "MENTION_EVERYONE": "提及 @everyone、@here 和所有角色", + "MODERATE_MEMBERS": "暂停成员", + "MOVE_MEMBERS": "移动成员", + "MUTE_MEMBERS": "静音成员", + "PIN_MESSAGES": "固定消息", + "PRIORITY_SPEAKER": "优先发言人", + "READ_MESSAGE_HISTORY": "阅读消息历史记录", + "REQUEST_TO_SPEAK": "请求发言", + "SEND_MESSAGES": "发送消息", + "SEND_MESSAGES_IN_THREADS": "在帖子中发送消息", + "SEND_POLLS": "创建投票", + "SEND_TTS_MESSAGES": "发送文本转语音消息", + "SEND_VOICE_MESSAGES": "发送语音留言", + "SPEAK": "发言", + "STREAM": "视频", + "USE_APPLICATION_COMMANDS": "使用应用程序命令", + "USE_EMBEDDED_ACTIVITIES": "使用活动", + "USE_EXTERNAL_APPS": "使用外部应用程序", + "USE_EXTERNAL_EMOJIS": "使用外部表情符号", + "USE_EXTERNAL_SOUNDS": "使用外部音效", + "USE_EXTERNAL_STICKERS": "使用外部贴纸", + "USE_SOUNDBOARD": "使用音效板", + "USE_VAD": "使用语音活动检测", + "VIEW_AUDIT_LOG": "查看审计日志", + "VIEW_CHANNEL": "查看频道", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "查看创建者变现分析", + "VIEW_GUILD_INSIGHTS": "查看公会洞见" + } + }, + "pinMessage": "固定消息", + "profile": { + "bio": "关于我:", + "joined": "账户已创建: $1", + "joinedMember": "已加入$1:$2", + "mut": "互助公会", + "mutFriends": "共同好友", + "permInfo": "权限", + "userInfo": "用户信息" + }, + "profileColor": "个人档案色", + "pronouns": "称谓:", + "readableName": "简体中文", + "recentEmoji": "最近的表情符号", + "register": { + "DOBError": "出生日期:$1", + "agreeTOS": "我同意[服务条款]($1):", + "emailError": "电子邮件:$1", + "noTOS": "此实例没有服务条款,但请接受服务条款:", + "passwordError:": "密码:$1", + "register": "注册", + "tos": "您必须同意服务条款", + "usernameError": "用户名:$1" + }, + "remove": "移除", + "renderJoinAvatars": "显示新成员的头像:", + "reply": "重播", + "replyingTo": "正在回复给$1", + "retrying": "正在重试…", + "role": { + "color": "第一颜色", + "colors": { + "name": "配色方案:", + "one": "单色", + "two": "双色", + "three": "三色", + "secondColor": "第二颜色:", + "thirdColor": "第三颜色:" + }, + "confirmDelete": "您确定要删除$1吗?", + "copyId": "复制角色 ID", + "delete": "删除角色", + "deleting": "正在删除角色…", + "displaySettings": "显示设置", + "hoisted": "将该身份组成员与其他在线成员分开显示(他们将根据其所属的最高身份组进行排名):", + "mentionable": "允许任何人 ping 此角色:", + "name": "角色名称:", + "perms": "权限", + "remove": "移除角色", + "roleEmoji": "角色表情符号:", + "roleFileIcon": "角色图标:", + "roles": "角色" + }, + "search": { + "back": "上一页", + "new": "新", + "next": "下一页", + "nofind": "似乎没有符合您搜索条件的讯息,请尝试扩大搜索条件,以找出您想要的内容", + "old": "旧", + "page": "第 $1 页", + "search": "搜索" + }, + "searchGifs": "搜索$1", + "settings": { + "clear": "清除", + "img": "上传图片", + "save": "保存更改", + "unsaved": "注意,您有未保存的更改", + "updates": { + "serviceWorkerMode": { + "disabled": "关", + "enabled": "开", + "offlineOnly": "仅离线", + "title": "Service Worker 设置", + "unregistered": "关闭(包括媒体缓存)" + } + } + }, + "showBlockedMessages": "您已封禁该名使用者,点击查看已封禁的 $1 件{{PLURAL:$1|消息}}。", + "spoiler": "剧透", + "sticker": { + "confirmDel": "您确定要删除此贴纸吗?", + "del": "删除贴纸", + "desc": "描述", + "errEmjMust": "贴纸必须搭配表情符号", + "errFileMust": "必须包含用于您的贴纸的图片", + "image": "图片:", + "name": "名称:", + "tags": "相关表情符号:", + "title": "贴纸", + "upload": "上传贴纸" + }, + "submit": "提交", + "switchAccounts": "切换账号 ⇌", + "todayAt": "今日于$1", + "trace": { + "totalTime": "$2:$1 毫秒", + "traces": "$1($2 毫秒)$3" + }, + "typing": "$2{{PLURAL:$1|正在}}输入", + "unableToConnect": "无法连接到 Spacebar 服务器。请尝试登出并重新登录。", + "unableToPin": "无法固定消息", + "unpinMessage": "取消置顶消息", + "updateAv": "可用更新", + "uploadBanner": "上传横幅:", + "uploadFilesText": "请在此处上传文件!", + "uploadPfp": "上传头像:", + "useTemplate": "以$1为模板", + "useTemplateButton": "使用模板", + "usedFermi": "通过Fermi发送", + "user": { + "report": "举报用户", + "deleted": "已删除的用户", + "reportApp": "举报应用程序", + "addRole": "添加角色", + "ban": "禁止成员", + "block": "封禁用户", + "confirmInstBan": "您确定要封禁实例$1吗?", + "copyId": "复制用户 ID", + "dnd": "请勿打扰", + "editNick": "编辑昵称", + "editServerProfile": "编辑公会资料", + "friendReq": "好友请求", + "idle": "闲置", + "instanceBan": "实例封禁", + "invisible": "不可见", + "kick": "踢出成员", + "message": "发消息给用户", + "nick": { + "foe": "设置敌人的昵称", + "friend": "设置好友昵称", + "stalker": "设置要求的昵称", + "stalking": "设置要求的昵称。" + }, + "offline": "离线", + "online": "在线", + "remove": "移除用户", + "removeRole": "移除角色", + "sendMessage": "发送消息@$1", + "timeout": "超时的成员", + "unTimeout": "移除成员超时", + "unban": "解封$1", + "unblock": "解封用户" + }, + "userping": { + "resolving": "正在解析用户", + "unknown": "@未知用户" + }, + "vc": { + "joinForStream": "加入语音频道以观看", + "joiningStream": "正在加入直播…", + "joinstream": "观看直播", + "leavestream": "离开直播", + "stopstream": "停止直播" + }, + "webauth": { + "addKey": "添加密钥", + "keyname": "密钥名称:", + "manage": "管理安全密钥" + }, + "webhook": "WEBHOOK", + "webhooks": { + "EnterWebhookName": "输入Webhook名称", + "areYouSureDelete": "您确定要删除$1吗?", + "avatar": "头像", + "base": "Webhook", + "channel": "Webhook输出到频道:", + "copyURL": "复制Webhook链接", + "createdAt": "创建于$1", + "createdBy": "创建由:", + "deleteWebhook": "删除Webhook", + "name": "Webhook名称:", + "newWebHook": "新Webhook", + "notFound": "用户不再属于该公会", + "sillyDefaults": "Captain Hook\nBillie Hooks\nSpidey bot", + "token": "Webhook 令牌:`$1`", + "type": "类型:$1", + "type1": "传入", + "type2": "频道关注者", + "type3": "应用程序", + "url": "Webhook链接:`$1`" + }, + "welcomeMessages": "欢迎,$1!很高兴看到您!\n您好,很高兴见到您,$1!\n欢迎,希望您带了些披萨,$1!", + "widget": "公会小工具", + "widgetEnabled": "已启用小工具", + "yes": "是", + "yesterdayAt": "昨日$1" +} diff --git a/translations/zh-hant.json b/translations/zh-hant.json new file mode 100644 index 00000000..f48c76f8 --- /dev/null +++ b/translations/zh-hant.json @@ -0,0 +1,958 @@ +{ + "@metadata": { + "authors": [ + "Key0121", + "Kly", + "LowensteinYang", + "張詠涵" + ] + }, + "2faCode": "雙因素驗證代碼:", + "404": { + "404": "錯誤代號:404--找不到頁面", + "app": "前往應用程式", + "blog": "Fermi 部落格", + "home": "首頁", + "listtitle": "也許您是想去這些地方", + "login": "登入", + "report": "如果您在客戶端發現此頁面,請回報:", + "reset": "重設密碼頁面", + "title": "看來您迷路了", + "whatelse": "您認為應該還會發生什麼事?", + "whereever": "無論這在哪裡" + }, + "onboarding": { + "name": "入門", + "disable": "停用入門功能", + "addChannel": "新增頻道", + "channel": "頻道:", + "desc": "描述:", + "deleteChannel": "從入門裡刪除頻道", + "enable": "啟用入門功能", + "title": "歡迎來到$1!" + }, + "DMs": { + "add": "將某人添加到此私訊", + "close": "關閉私訊", + "copyId": "複製私訊 ID", + "markRead": "標記為已讀", + "name": "私訊" + }, + "ok": "確定", + "dismiss": "去除", + "UrlGen": "機器人邀請生成器", + "Voice": { + "status": { + "badWS": "WebSocket回應錯誤", + "conectionFailed": "連接失敗", + "done": "已連接", + "left": "已離開語音聊天", + "makingOffer": "產生 offer", + "noSDP": "無 SDP", + "notconnected": "未連接", + "sendingStreams": "傳送音訊串流", + "startingRTC": "開始 RTC 連接", + "waitingMainWS": "等待主WebSocket中", + "waitingURL": "等待語音URL", + "wsAuth": "等待WebSocket授權中", + "wsOpen": "等待WebSocket開啟中" + } + }, + "accessibility": { + "gifSettings": { + "always": "一律使用", + "hover": "懸停時", + "never": "永不使用" + }, + "name": "無障礙", + "playGif": "播放GIF動圖:", + "playIcon": "播放動態圖示:", + "roleColors": "停用角色顏色:", + "gradientColors": "停用漸層色彩:", + "decorations": "啟用頭像裝飾:" + }, + "accountNotStart": "帳號無法啟動", + "add": "添加", + "applications": { + "delete": "刪除應用程式", + "sure": "您確定要刪除$1嗎?" + }, + "badge": { + "active_developer": "活躍開發者", + "application_guild_subscription": "擁有高級會員", + "automod": "使用自動模式", + "bot_commands": "支援指令", + "bug_hunter_level_1": "程式錯誤獵人 1 級", + "bug_hunter_level_2": "程式錯誤獵人 2 級", + "certified_moderator": "版主", + "early_supporter": "早期支援者", + "guild_booster_lvl1": "促進公會", + "guild_booster_lvl2": "促進公會", + "guild_booster_lvl3": "促進公會", + "guild_booster_lvl4": "促進公會", + "guild_booster_lvl5": "促進公會", + "guild_booster_lvl6": "促進公會", + "guild_booster_lvl7": "促進公會", + "guild_booster_lvl8": "促進公會", + "guild_booster_lvl9": "已促進了公會一段時間", + "hypesquad": "活力小隊 [佔位符]", + "hypesquad_house_1": "膽識之家", + "hypesquad_house_2": "卓越之家", + "hypesquad_house_3": "均衡之家", + "legacy_username": "擁有舊的使用者名稱", + "partner": "個體的合作夥伴", + "premium": "高級會員", + "quest_completed": "已完成一個任務", + "staff": "管理個體的人員", + "verified_developer": "已驗證的開發者" + }, + "bio": "個人簡介:", + "blankMessage": "空白訊息", + "blog": { + "blog": "部落格", + "blogUpdates": "部落格更新:", + "desc": "閱讀或訂閱部落格來獲得 Fermi 的最新資訊!", + "fermi": "Fermi 部落格", + "gotoPost": "前往文章", + "wantUpdates": "您想取得有關 Fermi 狀態的每週部落格更新嗎?" + }, + "bot": "機器人", + "botGuilds": "公會機器人位於:", + "botSettings": "機器人設定", + "cancel": "取消", + "channel": { + "SlowmodeCool": "慢速模式冷卻時間:$1", + "TimeOutCool": "超時至:$1", + "allowIcons": "允許自訂頻道圖示", + "announcement": "公告", + "copyId": "複製頻道 ID", + "copyIdCat": "複製分類 ID", + "createCatagory": "建立分類", + "createChannel": "建立頻道", + "creating": "建立頻道", + "delete": "刪除頻道", + "deleteCat": "刪除分類", + "icon": "圖示:", + "makeInvite": "做出邀請", + "markRead": "標記為已讀", + "mute": "靜音的頻道", + "name": "頻道", + "name:": "頻道名稱:", + "nsfw:": "工作場所不宜/成人內容警告:", + "permissions": "權限", + "selectCatName": "分類名稱", + "selectName": "頻道名稱", + "selectType": "選擇頻道類型", + "settings": "設定", + "settingsFor": "$1的設定", + "slowmode": "慢速模式:", + "text": "文字", + "timedOutUntil": "逾時至:$1", + "topic:": "主題:", + "typebox": "在$1的訊息", + "unmute": "取消噤聲頻道", + "voice": "語音", + "deleteThread": "刪除討論串", + "hideThreads": "不活躍後隱藏", + "forum": "論壇" + }, + "threads": { + "leave": "離開討論串", + "join": "加入討論串", + "name": "討論串名稱:", + "editTags": "編輯標籤" + }, + "forum": { + "creorsear": "建立或搜尋貼文", + "next": "下一個", + "back": "上一個", + "newPost": "發表新貼文", + "post": "貼文", + "sortOptions": { + "sortby": { + "title": "排序依", + "recent": "最近活躍", + "posted": "發布日期" + }, + "sortOrder": { + "title": "排序順序", + "recent": "最新優先", + "old": "較舊優先" + }, + "tagMatch": { + "title": "標籤匹配", + "some": "部分匹配標籤", + "all": "全部匹配標籤" + }, + "name": "排序選項" + }, + "errors": { + "tagsReq": "標籤為必需", + "requireText": "訊息不可為空" + }, + "allTags": "全部標籤", + "settings": { + "editTags": "編輯標籤:", + "editTag": "編輯標籤", + "tagName": "標籤名稱:", + "moderated": "僅允許討論串版主套用標籤:", + "addTag": "新增標籤", + "delTag": "刪除標籤" + } + }, + "channelLink": "# $1", + "clientDesc": "客戶端版本:$1 \n\n[加入 Fermi 官方公會]($2/invite\n/USgYJo?instance=https%3A%2F%2Fspacebar.chat)\n\n[協助翻譯 Fermi](https://translatewiki.net/wiki/Translating:JankClient#sortable:3=desc)\n\n[協助開發 Fermi](https://github.com/MathMan05/Fermi)\n\n[協助維護 Fermi 所依賴的伺服器](https://github.com/spacebarchat/server)\n\n[閱讀部落格](https://blog.fermi.chat/)\n\n計算權限:$3", + "commands": { + "errorNotValid": "$1不是$2的有效選擇", + "required": "$1是此指令的必需部分" + }, + "confirmGuildLeave": "您確定您要離開$1", + "copyLink": "複製訊息連結", + "copyRegLink": "複製連結", + "copymessageid": "複製訊息 ID", + "copyrawtext": "複製原始文字", + "createAccount": "建立帳號", + "delete": "刪除", + "deleteConfirm": "您確定要刪除這個嗎?", + "devSettings": { + "badUser": "啟用記錄發送過多資訊的惡意使用者對象:", + "cache": "啟用 Service Worker 快取映射檔:", + "cacheDesc": "映射檔案在任何情況都會載入,這樣是為了確保在新版本發佈時能已快取。", + "captureTrace": "此設定會指示 Fermi 從伺服器擷取 _trace 屬性,啟用此可能會導致漸進式 JSON 解碼無法運作(可能需要重新載入)。", + "clearWellKnowns": "重新取得 .well-known 紀錄:", + "description": "這些是給 Spacebar/Fermi 的開發人員使用,若您不清楚用途,請不要變動這些設定。", + "gatewayComp": "停用閘道器壓縮:", + "reportSystem": "啟用實驗性回報系統:", + "logGateway": "記錄接收到的閘道器事件(日誌等級資訊):", + "name": "開發人員設定", + "traces": "暴露痕跡:" + }, + "deviceManage": { + "city": "城市:$1", + "continent": "大陸:$1", + "country": "國家:$1", + "curSes": "此為目前連線階段,您需要透過另一個選單登出。", + "estimateWarn": "提醒:以上資訊僅為推測,可能有誤。", + "ip": "最後已知 IP:$1", + "last": "上次大約使用:$1", + "latitude": "緯度:$1", + "logout": "登出", + "longitude": "經度:$1", + "manageDev": "管理裝置", + "postal": "郵遞區號:$1", + "region": "地區:$1", + "title": "管理連線階段" + }, + "discovery": "探索", + "dms": "私訊", + "edit": "編輯", + "emoji": { + "confirmDel": "您確定要刪除這個表情符號?", + "found": { + "not": "此表情符號不屬於您的公會,請加入發佈該表情符號的公會才能使用它。", + "other": "這個表情符號來自您的另一個公會。", + "private": "找不到此表情符號的公會", + "this": "這個表情符號來自此公會。" + }, + "from": "這個表情符號來自", + "image:": "圖片:", + "join": "加入公會", + "name:": "名稱:", + "privateGuild": "此為私人公會", + "title": "表情符號", + "upload": "上傳表情符號" + }, + "emojiSelect": "表情符號:", + "errorReconnect": "無法連接到伺服器,將在 **$1** 秒後重試…", + "favoriteGifs": "收藏的 Gif 圖檔", + "folder": { + "color": "資料夾色彩:", + "create": "建立新資料夾", + "edit": "編輯資料夾", + "name": "資料夾名稱:" + }, + "form": { + "captcha": "請等一下,您是人類嗎?" + }, + "friends": { + "addfriend": "新增好友", + "addfriendpromt": "透過使用者名稱新增好友:", + "all": "全部", + "all:": "全部好友:", + "blocked": "已封鎖", + "blockedusers": "已封鎖使用者:", + "bu": "已封禁使用者", + "discnotfound": "找不到鑑別器", + "friendlist": "好友清單", + "friends": "好友", + "notfound": "找不到使用者", + "online": "線上", + "online:": "線上好友:", + "pending": "待辦", + "pending:": "待辦的好友請求:", + "removeFriend": "移除好友" + }, + "goThere": "好的,去吧", + "goThereTrust": "確認並從此信任該網域", + "goingToURL": "此超連結將會送你到$1。您確定要去那裡嗎?", + "group": { + "createdm": "建立私訊!", + "edit": "編輯群組聊天", + "select": "選擇好友" + }, + "guild": { + "COMMUNITY": "申請加入", + "disableInvites": "停用邀請:", + "DISCOVERABLE": "發現", + "INVITES_DISABLED": "僅邀請", + "adminMenu": { + "changePerms": "更改權限來尋找", + "finding": "尋找管理員", + "ownName": "擁有者", + "owner": "尋找擁有者", + "permission": "權限:" + }, + "report": "回報公會", + "admins": "尋找管理員", + "all": "全部", + "banReason": "禁止原因:$1", + "bannedBy": "被禁止由:", + "banner:": "橫幅:", + "bans": "禁止", + "boostMessage?": "當有人促進您的公會時發送訊息!", + "community": "社群", + "confirmDelete": "您確定要刪除$1嗎?", + "confirmLeave": "您確定您要離開嗎?", + "copyId": "複製公會 ID", + "create": "從頭開始建立公會", + "createFromTemplate": "從模板建立公會", + "createNewTemplate": "建立新模板", + "creating": "建立公會", + "default": "預設($1)", + "defaultNoti": "設定您的公會的預設通知設定!", + "delete": "刪除公會", + "description:": "描述:", + "disoveryTitle": "公會發現($1 {{PLURAL:$1|項目}})", + "editingTemplate": "編輯$1", + "emptytext": "真怪,這個公會沒有頻道?", + "emptytitle": "奇怪地方", + "guilds": "公會", + "helpTips?": "為您的公會發送有用的提示!", + "howJoin": "其他人如何加入您的公會?", + "icon:": "圖示:", + "invites": "邀請", + "joinConfirm": "加入$1?", + "leave": "離開公會", + "loadingDiscovery": "載入中…", + "makeInvite": "做出邀請", + "markRead": "標記為已讀", + "memberCount": "$1 個{{PLURAL:$1|成員}}", + "mute": "噤聲公會", + "name:": "名稱:", + "nameNoMatch": "名稱不符", + "noDelete": "沒事", + "noLeave": "沒事", + "none": "無", + "notifications": "通知", + "onlyMentions": "僅限@提及", + "overview": "概要", + "region:": "區域:", + "roles": "角色", + "ruleId": "規則頻道:", + "selectnoti": "選擇通知類型:", + "sendrandomwelcome?": "當有人加入此公會時發送隨機訊息:", + "serverName": "公會名稱:", + "settings": "設定", + "settingsFor": "$1的設定", + "splash": "發現亮點:", + "stickWelcomeReact?": "當有人加入時,提示您的公會成員用貼圖做出反應!", + "systemSelect:": "系統訊息頻道:", + "tempCreatedBy": "模板建立者:", + "tempUseCount": "模板已使用 $1 {{PLURAL:$1|次}}", + "template": "用於建立公會的模板連結/代碼:", + "templateDesc": "模板描述:", + "templateName": "模板名稱:", + "templateNameShort": "模板名稱必須至少包含 2 個字元", + "templateURL": "模板連結:$1", + "templates": "模板", + "templcateMetaDesc": "模板允許其他人使用該公會作為自己公會的基礎內容:這會複製該公會的頻道、角色與設定,但不會複製公會內部的訊息、機器人、公會圖示。", + "topic:": "主題:", + "unknownRole": "@不明角色", + "viewTemplate": "查看模板", + "yesDelete": "是的,我確定", + "yesLeave": "是的,我確定" + }, + "hideBlockedMessages": "您已封鎖該名使用者,點擊即可隱藏這些訊息。", + "home": { + "icon": "$1的實例圖示", + "uptimeStats": "運作時間:\n 所有時間:$1%\n本週:$2%\n今日:$3%", + "warnOffiline": "實例為離線狀態,無法連接" + }, + "htmlPages": { + "addBot": "新增到公會", + "alreadyHave": "已經有帳號了嗎?", + "box1Items": "直接訊息傳遞|反應支援|邀請|帳號切換|使用者設定|開發者入口網站|機器人邀請|翻譯支援", + "box1title": "Fermi 是一款與 Spacebar 相容的客戶端,致力於盡所能提供多項功能,包括:", + "box3description": "我們一向感謝任何協助,無論是錯誤報告、程式碼,或是指出一些拼字錯誤。", + "box3title": "為 Fermi 做出貢獻", + "compatableInstances": "Spacebar 相容實例:", + "createAccount": "建立帳號", + "dobField": "出生日期:", + "emailField": "電子郵件:", + "idpermissions": "這將允許機器人:", + "instanceField": "實例:", + "loaddesc": "這應該不會花很長時間", + "loadingText": "Fermi 載入中", + "loginButton": "登入", + "noAccount": "還沒有帳號?", + "openClient": "開啟客戶端", + "pw2Field": "再次輸入密碼:", + "pwField": "密碼:", + "switchaccounts": "切換帳號", + "trans": "翻譯", + "transDesc": "您可以協助將 Fermi 翻譯成您自己的語言!", + "transTitle": "翻譯 Fermi", + "userField": "使用者名稱:", + "welcomeJank": "歡迎來到 Fermi" + }, + "incorrectURLS": "## 此實例可能發送了錯誤的連結。 \n### 如果您是實例擁有者,請參閱[此處](https://docs.spacebar.chat/setup/server/)下的「從遠端電腦連線」來修正此問題。 \n 您想要 Fermi 自動嘗試修復此錯誤,以讓您連接到實例嗎?", + "instInfo": "實例資訊", + "instanceInfo": { + "contact": "電子郵件實例管理員", + "frontPage": "[前端頁面]($1)", + "name": "實例資訊", + "publicUrl": "[公共連結]($1)", + "tosPage": "[服務條款頁面]($1)" + }, + "instanceStats": { + "members": "成員:$1", + "messages": "訊息:$1", + "name": "實例統計:$1", + "servers": "公會:$1", + "users": "已註冊的使用者:$1" + }, + "interactions": { + "ephemeralDismiss": "忽略訊息", + "failed": "互動失敗", + "nonsence": "伺服器傳送的類型成分「$1」不存在", + "notImpl": "Fermi 尚不可執行類型成分「$1」", + "onlyYou": "僅有您才能查看此訊息", + "replyline": "開始互動", + "started": "處理互動…", + "worked": "互動成功" + }, + "invite": { + "accept": "接受", + "alreadyJoined": "已加入", + "channel:": "頻道:", + "createInvite": "建立邀請", + "createdAt": "建立於 $1", + "expireAfter": "期限至:", + "expires": "期限:$1", + "forChannel": "用於頻道:$1", + "inviteLinkCode": "邀請連結/代碼", + "inviteMaker": "邀請製作者", + "invitedBy": "您已收到來自$1的邀請", + "joinUsing": "使用邀請加入公會", + "loginOrCreateAccount": "登入或建立帳號⇌", + "longInvitedBy": "$1邀請您加入$2", + "maxUses": "最多:$1", + "never": "永不", + "noAccount": "建立帳號來接受邀請", + "notFound": "找不到邀請", + "subtext": "到$2裡的$1", + "used": "使用過 $1 {{PLURAL:$1|次}}:" + }, + "inviteOptions": { + "12h": "12 小時", + "1d": "1 天", + "1h": "1 小時", + "30d": "30 天", + "30m": "30 分", + "6h": "6 小時", + "7d": "7 天", + "limit": "$1 次{{PLURAL:$1|使用}}", + "never": "永不", + "noLimit": "無限制", + "title": "邀請他人" + }, + "jankInfo": "客戶端資訊", + "leaveGuild": "離開公會", + "leaving": "您正在離開 Spacebar", + "loaded": "已載入", + "localuser": { + "2faCode:": "代碼:", + "2faCodeGive": "您的一次性驗證碼是:$1(6 位數字),這將產生一個6位數的雙因素驗證代碼、其有效期為30秒。", + "2faDisable": "停用雙因素驗證", + "2faEnable": "啟用雙因素驗證", + "CheckUpdate": "檢查更新", + "PasswordsNoMatch": "密碼不符合", + "TOSURL": "服務條款的連結:", + "VoiceWarning": "您確定要啟用這個功能嗎?這是非常實驗性的,可能會引發問題。(此功能僅供開發者使用,如果您不確定操作,請不要啟用。)", + "accentColor": "主題強調色彩:", + "accountSettings": "帳號設定", + "addBot": "新增機器人", + "addStatus": "新增狀態", + "advancedBot": "進階機器人設定", + "appName": "應用程式名稱:", + "areYouSureDelete": "您確定要刪除您的帳號嗎?如果是,請輸入詞組$1", + "badCode": "無效代碼", + "badPassword": "密碼不正確", + "botAvatar": "機器人頭像:", + "botInviteCreate": "機器人邀請建立者", + "botUsername": "機器人使用者名稱:", + "changeDiscriminator": "更改鑑別器", + "changeEmail": "更改電子郵件", + "changePassword": "更改密碼", + "changeUsername": "更改使用者名稱", + "clearCache": "清除快取", + "confirmAddBot": "您確定要在此應用程式中新增機器人嗎?此操作無法取消。", + "confirmReset": "您確定要重新設定機器人的權杖嗎?您的機器人將停止運作,直到您更新為止。", + "confuseNoBot": "由於某種原因,此應用程式目前還沒有機器人。", + "connections": "連接", + "createApp": "建立應用程式", + "customSound": "自訂音效", + "customStatusWarn": "Spacebar 目前不支援顯示自訂狀態,因此雖然會接受狀態但不會對此做出任何行為", + "deleteAccount": "帳號刪除", + "deleteAccountButton": "刪除帳號", + "description": "描述:", + "devPortal": "開發者入口網站", + "disableConnection": "此連接已被伺服器端禁用", + "editingBot": "編輯機器人$1", + "install": "安裝", + "installDesc": "安裝 Fermi 後,您可以在其獨立的視窗中開啟,就像 Fermi 自己的應用程式一樣!您也可以像以前那樣在網頁瀏覽器中使用 Fermi,功能是相同的。", + "installJank": "安裝 Fermi", + "language": "語言:", + "manageBot": "管理機器人", + "manageInstance": "管理實例", + "mustTypePhrase": "要刪除您的帳號,您必須輸入片語", + "newDiscriminator": "新鑑別器:", + "newEmail:": "新電子郵件", + "newPassword:": "新密碼:", + "newUsername": "新使用者名稱:", + "noToken": "很抱歉,因無從得知權杖沒辦法儲存到本地端裝置", + "noUpdates": "無更新可用", + "notisound": "通知音效:", + "oldPassword:": "舊密碼:", + "password:": "密碼", + "privacyPolcyURL": "隱私政策連結:", + "appID": "應用程式 ID: $1", + "showSecret": "顯示客戶端密鑰", + "clientSecret": "客戶端密鑰:$1", + "secret": "密鑰", + "publicAvaliable": "讓機器人可以公開邀請?", + "refreshPage": "重整以套用", + "requireCode": "需要代碼授予才能邀請機器人?", + "resetToken": "重新設定權杖", + "saveToken": "將權杖儲存到本地端裝置", + "setUp2fa": "雙因素驗證設定", + "setUp2faInstruction": "將此密碼複製到您的 TOTP(基於時間的一次性密碼)應用程式以產生您的雙因素驗證登入代碼", + "settings": "設定", + "sillyDeleteConfirmPhrase": "本來應該從從容容、游刃有餘;現在變成匆匆忙忙、連滾帶爬", + "soundTooLarge": "您嘗試上傳的音訊檔案太大,請重試", + "status": "狀態", + "team:": "團隊:", + "theme:": "主題", + "themesAndSounds": "主題與音效", + "tokenDisplay": "權杖:$1", + "trace": "軌跡", + "trusted": "信任網域", + "trustedDesc": "當您點擊導向這些網域的連結時,系統將『不會』像其他連結那樣要求您授權開啟。請僅將此權限授予您信任的網域,例如:'https://fermi.chat'", + "updateSettings": "更新設定", + "updatesYay": "已找到更新!", + "userSettings": "使用者設定" + }, + "login": { + "allGood": "一切都好", + "checking": "檢查實例", + "enterPAgain": "再次輸入新密碼:", + "invalid": "無效實例,請重試", + "login": "登入", + "newPassword": "新密碼:", + "pasteInfo": "請將恢復連結貼到此處:", + "recover": "忘記密碼?", + "recovery": "已忘記的密碼", + "waiting": "等待檢查實例" + }, + "logout": { + "error": { + "cancel": "取消", + "cont": "仍要繼續", + "desc": "Fermi 無法將您登出,您是否仍要移除該帳號?", + "title": "嘗試將您登出時發生錯誤" + }, + "logout": "登出" + }, + "manageInstance": { + "AreYouSureStop": "您確定要停止此實例嗎?", + "TokenFormats": { + "JSON": "JSON 格式", + "URLs": "邀請連結", + "plain": "純文字" + }, + "clientURL": "客戶端連結:", + "copy": "複製", + "count": "數量:", + "create": "建立", + "createTokens": "建立註冊權杖", + "format": "格式:", + "genericType": "通用", + "length": "長度:", + "regType": "註冊權杖連結類型", + "stop": "停止實例" + }, + "media": { + "artist": "藝術家:$1", + "composer": "作曲家:$1", + "download": "下載媒體", + "length": "時長:$1 分 $2 秒", + "loading": "載入中", + "moreInfo": "更多資訊", + "notFound": "找不到媒體" + }, + "member": { + "ban": "禁止$1進入$2", + "kick": "從$2踢出$1", + "nick:": "暱稱:", + "persist": "僅刪除使用者:", + "reason:": "原因:", + "timeout": "逾時 $1", + "timeoutTime": "逾時期間:" + }, + "message": { + "messages": "$1 則{{PLURAL:$1|訊息}} >", + "andMore": "$1,還有更多!", + "attached": "已發送附件", + "delete": "删除訊息", + "report": "回報訊息", + "deleted": "已删除的訊息", + "edit": "編輯訊息", + "edited": "(已編輯)", + "fullMessage": "完整訊息:", + "pin": "$1已置頂一則訊息", + "thread": "$1已建立一個討論串:$2", + "reactedWith": "以$1反應", + "reactionAdd": "添加反應", + "createThread": "建立討論串", + "threadOptions": "討論串選項", + "reactions": "查看反應", + "reactionsTitle": "反應", + "retry": "重新發送錯誤訊息", + "viewrest": "查看剩餘內容" + }, + "report": { + "back": "返回", + "next": "下一個", + "cancel": "取消", + "submit": "送出", + "blockUser": "封鎖使用者", + "timeout": "逾時成員", + "summary": "回報摘要:" + }, + "messageLink": "在 # $1 中的訊息", + "muteDuration": "取消噤聲在:", + "nevermind": "沒事", + "no": "否", + "noEmojiFont": "使用系統表情符號", + "noMessages": "這裡還沒有訊息,請搶先發表意見!", + "noPins": "昔人已乘黃鶴去,此地空餘黃鶴樓。黃鶴一去不復返,白雲千載空悠悠...", + "noStickers": "太慘了,這裡沒有貼圖能供您使用!", + "notiClump": "您已收到來自$2的通知$1", + "notiVolume": "通知音量:", + "otherAt": "於$2的$1", + "permissions": { + "descriptions": { + "ADMINISTRATOR": "會授予角色成員以下「所有」的權限、並越過所有權限覆寫。只有您和您絕對信任的成員/機器人才能擁有這樣危險的權限!", + "MANAGE_GUILD": "允許角色成員管理和編輯公會。", + "VIEW_AUDIT_LOG": "允許角色成員查看稽核日誌。", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "允許角色成員查看角色訂閱洞察。", + "VIEW_GUILD_INSIGHTS": "允許角色成員查看公會洞察。", + "BYPASS_SLOWMODE": "慢速模式不再影響角色成員。", + "MENTION_EVERYONE": "允許角色成員提及 @所有人。", + "MANAGE_NICKNAMES": "允許角色成員更改其他成員的暱稱", + "PIN_MESSAGES": "允許角色成員將訊息置頂", + "SEND_POLLS": "允許角色成員發送投票調查", + "MANAGE_MESSAGES": "允許角色成員刪除不屬於自己的訊息", + "MANAGE_THREADS": "允許角色成員刪除和存檔討論串,並查看所有非公開討論串", + "MANAGE_CHANNELS": "允許角色成員編輯或刪除頻道。", + "MANAGE_ROLES": "允許角色成員編輯或刪除角色。", + "MANAGE_WEBHOOKS": "允許角色成員編輯或刪除 webhook。", + "CREATE_EVENTS": "允許角色成員建立計劃的事件。", + "MANAGE_EVENTS": "允許角色成員編輯或刪除(既有和未來)事件", + "CREATE_GUILD_EXPRESSIONS": "允許角色成員建立各種表達(表情符號、貼圖、音效面板用音效)。", + "MANAGE_GUILD_EXPRESSIONS": "允許使用者編輯或刪除各種表達(表情符號、貼圖、音效面板用音效)。", + "MODERATE_MEMBERS": "允許角色成員對公會成員實施暫停權限(禁止其在聊天或討論串中發送或回應訊息,以及在語音或表演頻道中發言)。", + "KICK_MEMBERS": "允許角色成員將成員踢出公會。", + "BAN_MEMBERS": "允許角色成員禁止成員加入公會。", + "MOVE_MEMBERS": "允許角色成員在語音頻道之間移動成員。", + "MUTE_MEMBERS": "允許角色成員在語音頻道噤聲其他成員。", + "DEAFEN_MEMBERS": "允許角色成員在語音頻道中讓其他成員變聾(讓成員變聾也會自動將其變為靜音)。", + "CHANGE_NICKNAME": "允許角色成員更改自己的暱稱。", + "VIEW_CHANNEL": "允許角色成員查看文字頻道。", + "READ_MESSAGE_HISTORY": "允許角色成員讀取文字頻道的訊息歷史記錄。", + "ADD_REACTIONS": "允許角色成員在文字頻道中添加對訊息的反應。", + "SEND_MESSAGES": "允許角色成員在文字頻道中發送訊息。", + "SEND_MESSAGES_IN_THREADS": "允許角色成員在討論串發送訊息。", + "SEND_TTS_MESSAGES": "允許角色成員在文字頻道中發送文字轉語音訊息。", + "USE_EXTERNAL_EMOJIS": "允許角色成員在文字頻道中使用外部表情符號。", + "USE_EXTERNAL_STICKERS": "允許角色成員在文字頻道中使用外部貼圖。", + "EMBED_LINKS": "允許由角色成員在文字頻道中所發送的連結自動內嵌。", + "ATTACH_FILES": "允許角色成員在文字頻道中附加檔案。", + "SEND_VOICE_MESSAGES": "允許在文字頻道中發送語音訊息。", + "CREATE_INSTANT_INVITE": "允許角色成員為公會建立邀請。", + "CREATE_PUBLIC_THREADS": "允許角色成員建立公開討論串。", + "CREATE_PRIVATE_THREADS": "允許角色成員建立非公開討論串。", + "CONNECT": "允許角色成員連接到語音頻道。", + "SPEAK": "允許角色成員在語音頻道中發言。", + "USE_VAD": "允許角色成員在語音頻道中直接說話(當此功能關閉時,他們只能透過按下通話鍵輸入來說話)。", + "STREAM": "允許角色成員在語音頻道中直播。", + "USE_SOUNDBOARD": "允許角色成員在語音頻道中使用音效面板。", + "USE_EXTERNAL_SOUNDS": "允許角色成員在語音頻道中使用來自其他公會的音效面板音效。", + "PRIORITY_SPEAKER": "允許角色成員在語音頻道中優先發言。", + "REQUEST_TO_SPEAK": "允許角色成員請求在表演頻道中發言。", + "USE_EMBEDDED_ACTIVITIES": "允許角色成員使用內嵌活動。", + "USE_APPLICATION_COMMANDS": "允許角色成員使用應用程式指令。", + "USE_EXTERNAL_APPS": "允許角色成員將應用程式回應公開顯示於頻道中(當此功能關閉時,使用者仍可使用其應用程式,但回應僅對自身可見。此設定僅適用於未同時安裝至公會的應用程式)。" + }, + "readableNames": { + "ADD_REACTIONS": "增加反應", + "ADMINISTRATOR": "管理員", + "ATTACH_FILES": "附加檔案", + "BAN_MEMBERS": "禁止成員", + "BYPASS_SLOWMODE": "繞過慢速模式", + "CHANGE_NICKNAME": "更改暱稱", + "CONNECT": "連接", + "CREATE_EVENTS": "建立事件", + "CREATE_GUILD_EXPRESSIONS": "建立表達", + "CREATE_INSTANT_INVITE": "建立邀請", + "CREATE_PRIVATE_THREADS": "建立非公開討論串", + "CREATE_PUBLIC_THREADS": "建立公開討論串", + "DEAFEN_MEMBERS": "蒙蔽成員", + "EMBED_LINKS": "內嵌連結", + "KICK_MEMBERS": "踢出成員", + "MANAGE_CHANNELS": "管理頻道", + "MANAGE_EVENTS": "管理事件", + "MANAGE_GUILD": "管理公會", + "MANAGE_GUILD_EXPRESSIONS": "管理表達", + "MANAGE_MESSAGES": "管理訊息", + "MANAGE_NICKNAMES": "管理暱稱", + "MANAGE_ROLES": "管理角色", + "MANAGE_THREADS": "管理討論串", + "MANAGE_WEBHOOKS": "管理 webhook", + "MENTION_EVERYONE": "提及 @everyone、@here 和所有角色", + "MODERATE_MEMBERS": "暫停成員", + "MOVE_MEMBERS": "移動成員", + "MUTE_MEMBERS": "噤聲成員", + "PIN_MESSAGES": "置頂訊息", + "PRIORITY_SPEAKER": "優先發言者", + "READ_MESSAGE_HISTORY": "閱讀訊息歷史紀錄", + "REQUEST_TO_SPEAK": "請求發言", + "SEND_MESSAGES": "發送訊息", + "SEND_MESSAGES_IN_THREADS": "在討論串中發送訊息", + "SEND_POLLS": "建立投票", + "SEND_TTS_MESSAGES": "發送文字轉語音訊息", + "SEND_VOICE_MESSAGES": "傳送語音訊息", + "SPEAK": "發言", + "STREAM": "影片", + "USE_APPLICATION_COMMANDS": "使用應用程式指令", + "USE_EMBEDDED_ACTIVITIES": "使用活動", + "USE_EXTERNAL_APPS": "使用外部應用程式", + "USE_EXTERNAL_EMOJIS": "使用外部表情符號", + "USE_EXTERNAL_SOUNDS": "使用外部音效", + "USE_EXTERNAL_STICKERS": "使用外部貼圖", + "USE_SOUNDBOARD": "使用音效面板", + "USE_VAD": "使用語音活動偵測", + "VIEW_AUDIT_LOG": "查看稽核日誌", + "VIEW_CHANNEL": "查看頻道", + "VIEW_CREATOR_MONETIZATION_ANALYTICS": "查看建立者變現分析", + "VIEW_GUILD_INSIGHTS": "查看公會洞見" + } + }, + "pinMessage": "置頂訊息", + "profile": { + "bio": "關於我:", + "joined": "已建立帳號:$1", + "joinedMember": "已加入$1:$2", + "mut": "共同公會", + "mutFriends": "共同好友", + "permInfo": "權限", + "userInfo": "使用者資訊" + }, + "profileColor": "個人檔案色彩", + "pronouns": "稱謂:", + "readableName": "繁體中文", + "recentEmoji": "近期的表情符號", + "register": { + "DOBError": "出生日期:$1", + "agreeTOS": "我同意[服務條款]($1):", + "emailError": "電子郵件:$1", + "noTOS": "此實例沒有服務條款,無論如何請接受服務條款:", + "passwordError:": "密碼:$1", + "register": "註冊", + "tos": "您必須同意服務條款", + "usernameError": "使用者名稱:$1" + }, + "remove": "移除", + "renderJoinAvatars": "顯示新成員的頭像:", + "reply": "回覆", + "replyingTo": "正在回覆給$1", + "retrying": "正在重試…", + "role": { + "color": "第一種顏色", + "colors": { + "name": "顏色方案:", + "one": "單色", + "two": "雙色", + "three": "三色", + "secondColor": "第二種顏色:", + "thirdColor": "第三種顏色:" + }, + "confirmDelete": "您確定要刪除$1嗎?", + "copyId": "複製角色 ID", + "delete": "刪除角色", + "deleting": "正在刪除角色…", + "displaySettings": "顯示設定", + "hoisted": "將角色成員與線上成員分開顯示(角色成員將透過其隸屬的最高層級角色群組進行提升):", + "mentionable": "允許任何人 ping 該角色:", + "name": "角色名稱:", + "perms": "權限", + "remove": "移除角色", + "roleEmoji": "角色表情符號:", + "roleFileIcon": "角色圖示:", + "roles": "角色" + }, + "search": { + "back": "上一頁", + "new": "新", + "next": "下一頁", + "nofind": "似乎沒有符合您搜尋條件的訊息,請嘗試擴大搜尋條件,以找出您想要的內容", + "old": "舊", + "page": "第 $1 頁", + "search": "搜尋" + }, + "searchGifs": "搜尋$1", + "settings": { + "clear": "清除", + "img": "上傳圖片", + "save": "儲存更改", + "unsaved": "請留意,您有尚未儲存的更改", + "updates": { + "serviceWorkerMode": { + "disabled": "關", + "enabled": "開", + "offlineOnly": "僅離線", + "title": "Service Worker 設定", + "unregistered": "關閉(包括媒體快取)" + } + } + }, + "showBlockedMessages": "您已封鎖該名使用者,點擊查看已封鎖的 $1 件{{PLURAL:$1|訊息}}。", + "spoiler": "劇透", + "sticker": { + "confirmDel": "您確定要刪除這個貼圖嗎?", + "del": "刪除貼圖", + "desc": "描述", + "errEmjMust": "表情符號必須搭配您的貼圖", + "errFileMust": "必須包含用於您的貼圖的圖片", + "image": "圖片:", + "name": "名稱:", + "tags": "相關表情符號:", + "title": "貼圖", + "upload": "上傳貼圖" + }, + "submit": "提交", + "switchAccounts": "切換帳號 ⇌", + "todayAt": "今日於$1", + "trace": { + "totalTime": "$2:$1 毫秒", + "traces": "$1($2 毫秒)$3" + }, + "typing": "$2{{PLURAL:$1|正在}}輸入", + "unableToConnect": "無法連接到 Spacebar 伺服器。請嘗試登出並重新登入。", + "unableToPin": "無法置頂訊息", + "unpinMessage": "取消置頂訊息", + "updateAv": "有更新可用", + "uploadBanner": "上傳橫幅:", + "uploadFilesText": "在此上傳您的檔案!", + "uploadPfp": "上傳頭像:", + "useTemplate": "使用$1作為模板", + "useTemplateButton": "使用模板", + "usedFermi": "透過 Fermi 發送", + "user": { + "report": "回報使用者", + "deleted": "已刪除使用者", + "reportApp": "回報申請", + "addRole": "新增角色", + "ban": "禁止成員", + "block": "封鎖使用者", + "confirmInstBan": "您確定要禁止實例$1嗎?", + "copyId": "複製使用者 ID", + "dnd": "請勿打擾", + "editNick": "編輯暱稱", + "editServerProfile": "編輯公會設定檔", + "friendReq": "好友請求", + "idle": "閒置", + "instanceBan": "禁止實例", + "invisible": "不可見", + "kick": "踢出成員", + "message": "訊息給使用者", + "nick": { + "foe": "設定仇家暱稱", + "friend": "設定好友暱稱", + "stalker": "設定請求者的暱稱", + "stalking": "設定請求的暱稱" + }, + "offline": "離線", + "online": "線上", + "remove": "移除使用者", + "removeRole": "移除角色", + "sendMessage": "發送訊息@$1", + "timeout": "逾時成員", + "unTimeout": "移除成員上的逾時", + "unban": "解禁$1", + "unblock": "解除封鎖使用者" + }, + "userping": { + "resolving": "解析使用者", + "unknown": "@不明使用者" + }, + "vc": { + "joinForStream": "加入語音頻道來觀看", + "joiningStream": "正在加入直播…", + "joinstream": "觀看直播", + "leavestream": "離開直播", + "stopstream": "停止直播" + }, + "webauth": { + "addKey": "新增金鑰:", + "keyname": "金鑰名稱:", + "manage": "管理安全金鑰" + }, + "webhook": "WEBHOOK", + "webhooks": { + "EnterWebhookName": "輸入 Webhook 名稱", + "areYouSureDelete": "您確定要刪除$1嗎?", + "avatar": "頭像", + "base": "Webhook", + "channel": "Webhook 輸出到頻道:", + "copyURL": "複製 Webhook 連結", + "createdAt": "建立於 $1", + "createdBy": "建立由:", + "deleteWebhook": "刪除 Webhook", + "name": "Webhook 名稱:", + "newWebHook": "新 Webhook", + "notFound": "使用者不再屬於該公會", + "sillyDefaults": "Captain Hook\nBillie Hooks\nSpidey bot", + "token": "Webhook 權杖:`$1`", + "type": "類型:$1", + "type1": "傳入", + "type2": "頻道追蹤者", + "type3": "應用程式", + "url": "Webhook 連結:`$1`" + }, + "welcomeMessages": "歡迎,$1!很高興看到您!\n$1您好,請問幾位?\n$1歡迎光臨,入內請脫鞋!", + "widget": "公會小工具", + "widgetEnabled": "已啟用小工具", + "yes": "是", + "yesterdayAt": "昨日在$1" +} diff --git a/tsconfig.json b/tsconfig.json index 5ec89b35..52fa1021 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,41 +1,33 @@ { - "compilerOptions": { - "allowSyntheticDefaultImports": true, - "alwaysStrict": true, - "declaration": true, - "declarationMap": true, - "esModuleInterop": true, - "importHelpers": false, - "incremental": true, - "lib": [ - "esnext", - "DOM", - "webworker" - ], - "module": "ESNext", - "moduleResolution": "Bundler", - "newLine": "lf", - "noEmitHelpers": false, - "noFallthroughCasesInSwitch": true, - "noImplicitReturns": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "preserveConstEnums": true, - "pretty": true, - "removeComments": false, - "resolveJsonModule": true, - "sourceMap": true, - "strict": true, - "target": "ESNext", - "useDefineForClassFields": true, - "resolvePackageJsonImports": true, - "skipLibCheck": true, - "outDir": "./dist" - }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "node_modules" - ] + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "alwaysStrict": true, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "importHelpers": false, + "incremental": true, + "lib": ["esnext", "DOM", "webworker", "DOM.AsyncIterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "newLine": "lf", + "noEmitHelpers": false, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "preserveConstEnums": true, + "pretty": true, + "removeComments": false, + "resolveJsonModule": true, + "sourceMap": true, + "strict": true, + "target": "es2022", + "useDefineForClassFields": true, + "resolvePackageJsonImports": true, + "skipLibCheck": true, + "outDir": "./dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] }