1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-11-09 20:00:12 +00:00

Compare commits

...

26 Commits

Author SHA1 Message Date
Qutyba
f81224a2a6 translation from Weblate (Arabic)
Currently translated at 100.0% (42 of 42 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/form/ar/
2023-06-14 21:42:07 +02:00
Qutyba
8760152159 Translated using Weblate (Arabic)
Currently translated at 100.0% (10 of 10 strings)

Translation: jfa-go/Telegram/Matrix/Discord bots
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/chat-bots/ar/
2023-06-14 21:42:07 +02:00
Kovács Tamás
5694f30a94 translation from Weblate (Hungarian)
Currently translated at 100.0% (42 of 42 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/form/hu/
2023-06-14 21:42:07 +02:00
Gabriele Bizzon
156478b381 translation from Weblate (Portuguese (Brazil))
Currently translated at 100.0% (42 of 42 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/form/pt_BR/
2023-06-14 21:42:07 +02:00
Rafael Gale
ad416b9cb2 translation from Weblate (Spanish)
Currently translated at 100.0% (42 of 42 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/form/es/
2023-06-14 21:42:07 +02:00
StunBeta
2e39a5e573 Translated using Weblate (French)
Currently translated at 100.0% (10 of 10 strings)

Translation: jfa-go/Telegram/Matrix/Discord bots
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/chat-bots/fr/
2023-06-14 21:42:07 +02:00
StunBeta
cab099d77f Translated using Weblate (French)
Currently translated at 100.0% (112 of 112 strings)

Translation: jfa-go/Setup
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/setup/fr/
2023-06-14 21:42:07 +02:00
StunBeta
0b5e93fd60 Translated using Weblate (French)
Currently translated at 100.0% (23 of 23 strings)

Translation: jfa-go/Common Strings
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/common-strings/fr/
2023-06-14 21:42:07 +02:00
StunBeta
6e2ba78204 translation from Weblate (French)
Currently translated at 97.6% (41 of 42 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/form/fr/
2023-06-14 21:42:07 +02:00
115f5ae6a3
Merge accounts sort/filter
Accounts Sort/Filter, UI adjustments
2023-06-14 20:41:27 +01:00
bf12016315
accounts: make filter names translatable 2023-06-14 19:59:38 +01:00
b544931ee5
accounts: fix id filtering, make string translatable 2023-06-14 18:57:30 +01:00
9cef626b28
accounts: filter dropdown appears over announce one 2023-06-14 18:52:33 +01:00
708d382a3f
accounts: fix hiding of search options header for default sort 2023-06-14 18:43:46 +01:00
f24ea4a5f8
accounts: fix sizing of sorting by button 2023-06-14 18:41:47 +01:00
6ddd09ff1f
accounts: add header to "actions" and "search options" 2023-06-14 18:38:12 +01:00
ddc560e862
accounts: move filter button, add clear search
filter button now on left due to the dropdown being huge.
2023-06-14 17:36:41 +01:00
6f452c62de
accounts: fix search bugs, adjust top bar layout
search bar is now massive with a small filter button next to it.
Action buttons are on their own row.
Also fixed dealing with going from a search with filters in to an empty
one, search() is now called for any change at all to the input.
2023-06-14 17:15:24 +01:00
76bb95098c
accounts: add list of available filters, fix deletion of existing date filters
The "Filters" button gives a list of filterable fields, and buttons to
select the type, including true/false, text match, and on/before/after a
date. When clicked, the appropriate values are put in the search box and
the cursor is placed if any input is needed.

Dates and strings are also now matched correctly, and case-insensitively when
deleting a filter.
2023-06-14 14:01:05 +01:00
0e241f56fb
scripts: add script to generate fake accounts
might be useful for screenshots too, currently just using for testing
the sorting/filtering.
2023-06-14 11:50:22 +01:00
8ac3bb9711
accounts: show list row of search filters, click to remove
any filters in your search box will show as little button/chip things on
the row below. Clicking them will remove them from the search.
2023-06-13 22:27:08 +01:00
ff62f8821a
accounts: filter by date, with =, <, >
Uses "any-date-parser" library to understand more date/time types.
Format is: "<field>:<equals, less than, greater than><date>", where the
part after the colon uses =, <, >. Omitting a symbol is the same as
using "=".
2023-06-13 17:19:24 +01:00
90c433443f
accounts: filter by string field, general search
string fields can now be searched by with the "<field>:<value>" syntax,
also added back a better general search, that supports essentially all
string fields, including Jellyfin ID.
2023-06-13 13:55:40 +01:00
8a37663c89
accounts: start advanced search filter support
uses the same format "<field>:<value>", but supports quoted <values>
(allows for spaces in them), and lays groundwork to support string and
date-type field filtering. Truthiness is supported, meaning you can
check if an email is set with "email:yes" for example.
2023-06-13 13:39:13 +01:00
bc4015ac50
accounts: add indicator of sorted column at top of list
When clicking on a column to sort by it, a button with "Sorting By:
<column>" appears. Clicking it will reset the sort, which defaults for
ascending username.
2023-06-13 12:13:12 +01:00
f13c0d78a8
accounts: ability to sort by columns
click on column header in account table to sort by it, click again to
change sort direction. Works for all of them.
2023-06-13 11:07:56 +01:00
18 changed files with 1026 additions and 332 deletions

View File

@ -490,10 +490,10 @@ a:hover:not(.lang-link):not(.\~urge), a:active:not(.lang-link):not(.\~urge) {
text-align: center;
}
.search {
/* .search {
max-width: 15rem;
min-width: 10rem;
}
} */
td.img-circle {
width: 32px;
@ -548,6 +548,11 @@ div.card:contains(section.banner.footer) {
padding-bottom: 0px !important
}
.mx-0i {
margin-left: 0px !important;
margin-right: 0px !important
}
.text-center-i {
text-align: center !important;
}

View File

@ -579,16 +579,30 @@
</div>
</div>
<div id="tab-accounts" class="unfocused">
<div class="card @low dark:~d_neutral accounts mb-4">
<div class="flex-expand row">
<div class="row">
<span class="text-3xl font-bold mr-2 col">{{ .strings.accounts }}</span>
<input type="search" class="col sm field ~neutral @low input search ml-2 mr-2" id="accounts-search" placeholder="{{ .strings.search }}">
<div class="card @low dark:~d_neutral accounts mb-4 overflow-visible">
<div class="flex-expand align-middle">
<span class="text-3xl font-bold mr-4">{{ .strings.accounts }}</span>
<div id="accounts-filter-dropdown" class="dropdown z-10" tabindex="0">
<span class="h-100 button ~neutral @low center" id="accounts-filter-button">{{ .strings.filters }}</span>
<div class="dropdown-display">
<div class="card ~neutral @low mt-2" id="accounts-filter-list">
<p class="supra pb-2">{{ .strings.filters }}</p>
</div>
<div class="row">
<span class="col sm button ~neutral @low center mb-2" id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span>
<div id="accounts-announce-dropdown" class="col sm dropdown pb-0i" tabindex="0">
<span class="h-100 sm button ~info @low center mb-2" id="accounts-announce">{{ .strings.announce }}</span>
</div>
</div>
<input type="search" class="field ~neutral @low input search ml-2 mr-2" id="accounts-search" placeholder="{{ .strings.search }}">
<span class="button ~neutral @low center -ml-8" id="accounts-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></span>
</div>
<div class="supra py-1 sm hidden" id="accounts-search-options-header">{{ .strings.searchOptions }}</div>
<div class="row -mx-2">
<button type="button" class="button ~neutral @low center m-2 hidden"><span id="accounts-sort-by-field"></span> <i class="ri-close-line ml-2 text-2xl"></i></button>
<span id="accounts-filter-area"></span>
</div>
<div class="supra py-1 sm">{{ .strings.actions }}</div>
<div class="row -mx-2">
<span class="col button ~neutral @low center max-w-[20%]" id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span>
<div id="accounts-announce-dropdown" class="col dropdown pb-0i max-w-[20%]" tabindex="0">
<span class="w-100 button ~info @low center" id="accounts-announce">{{ .strings.announce }}</span>
<div class="dropdown-display">
<div class="card ~neutral @low">
<span class="supra sm">{{ .strings.templates }}</span>
@ -596,41 +610,40 @@
</div>
</div>
</div>
<span class="col sm button ~urge @low center mb-2" id="accounts-modify-user">{{ .strings.modifySettings }}</span>
<span class="col sm button ~warning @low center mb-2" id="accounts-extend-expiry">{{ .strings.extendExpiry }}</span>
<div id="accounts-disable-enable-dropdown" class="col sm dropdown manual pb-0i" tabindex="0">
<span class="h-100 sm button ~positive @low center mb-2" id="accounts-disable-enable">{{ .strings.disable }}</span>
<span class="col button ~urge @low center max-w-[20%]" id="accounts-modify-user">{{ .strings.modifySettings }}</span>
<span class="col button ~warning @low center max-w-[20%]" id="accounts-extend-expiry">{{ .strings.extendExpiry }}</span>
<div id="accounts-disable-enable-dropdown" class="col dropdown manual pb-0i max-w-[20%]" tabindex="0">
<span class="w-100 button ~positive @low center" id="accounts-disable-enable">{{ .strings.disable }}</span>
<div class="dropdown-display">
<div class="card ~neutral @low">
<span class="button ~urge sm full-width accounts-announce-template-button" id="accounts-enable-expiry">{{ .strings.setExpiry }}</span>
<span class="button ~urge full-width accounts-announce-template-button" id="accounts-enable-expiry">{{ .strings.setExpiry }}</span>
</div>
</div>
</div>
<span class="col sm button ~info @low center mb-2 unfocused" id="accounts-send-pwr">{{ .strings.sendPWR }}</span>
<span class="col sm button ~critical @low center mb-2" id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
<span class="col button ~info @low center unfocused max-w-[20%]" id="accounts-send-pwr">{{ .strings.sendPWR }}</span>
<span class="col button ~critical @low center max-w-[20%]" id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
</div>
</div>
<div class="card @low accounts-header table-responsive mt-8">
<div class="card @low accounts-header table-responsive mt-2">
<table class="table text-base leading-4">
<thead>
<tr>
<th><input type="checkbox" value="" id="accounts-select-all"></th>
<th class="table-inline my-2">{{ .strings.username }}</th>
<th class="table-inline my-2 grid gap-4 place-items-stretch accounts-header-username">{{ .strings.username }}</th>
{{ if .jellyfinLogin }}
<th class="text-center-i">{{ .strings.accessJFA }}</th>
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-access-jfa">{{ .strings.accessJFA }}</th>
{{ end }}
<th>{{ .strings.emailAddress }}</th>
<th class="grid gap-4 place-items-stretch accounts-header-email">{{ .strings.emailAddress }}</th>
{{ if .telegramEnabled }}
<th class="text-center-i">Telegram</th>
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-telegram">Telegram</th>
{{ end }}
{{ if .matrixEnabled }}
<th class="text-center-i">Matrix</th>
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-matrix">Matrix</th>
{{ end }}
{{ if .discordEnabled }}
<th class="text-center-i">Discord</th>
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-discord">Discord</th>
{{ end }}
<th>{{ .strings.expiry }}</th>
<th>{{ .strings.lastActiveTime }}</th>
<th class="grid gap-4 place-items-stretch accounts-header-expiry">{{ .strings.expiry }}</th>
<th class="grid gap-4 place-items-stretch accounts-header-last-active">{{ .strings.lastActiveTime }}</th>
</tr>
</thead>
<tbody id="accounts-list"></tbody>

View File

@ -37,6 +37,8 @@
"advancedSettings": "Advanced Settings",
"lastActiveTime": "Last Active",
"from": "From",
"after": "After",
"before": "Before",
"user": "User",
"expiry": "Expiry",
"userExpiry": "User Expiry",
@ -114,7 +116,15 @@
"deleteTemplate": "Delete template",
"templateEnterName": "Enter a name to save this template.",
"accessJFA": "Access jfa-go",
"accessJFASettings": "Cannot be changed as either \"Admin Only\" or \"Allow All\" has been set in Settings > General."
"accessJFASettings": "Cannot be changed as either \"Admin Only\" or \"Allow All\" has been set in Settings > General.",
"sortingBy": "Sorting By",
"filters": "Filters",
"clickToRemoveFilter": "Click to remove this filter.",
"clearSearch": "Clear search",
"actions": "Actions",
"searchOptions": "Search Options",
"matchText": "Match Text",
"jellyfinID": "Jellyfin ID"
},
"notifications": {
"changedEmailAddress": "Changed email address of {n}.",

View File

@ -7,7 +7,7 @@
"username": "Nom d'utilisateur",
"name": "Nom",
"password": "Mot de passe",
"emailAddress": "Addresse Email",
"emailAddress": "Adresse e-mail",
"submit": "Soumettre",
"success": "Succès",
"continue": "Continuer",
@ -23,6 +23,8 @@
"linkDiscord": "Lier Discord",
"linkMatrix": "Lier Matrix",
"send": "Envoyer",
"contactDiscord": "Contacter par Discord"
"contactDiscord": "Contacter par Discord",
"refresh": "Actualiser",
"required": "Requis"
}
}

View File

@ -33,7 +33,9 @@
"errorCaptcha": "كلمة التحقق غير صحيحة.",
"errorPassword": "تحقق من متطلبات كلمة المرور.",
"errorNoMatch": "كلمات المرور غير متطابقة.",
"verified": "تم التحقق من الحساب."
"verified": "تم التحقق من الحساب.",
"errorAccountLinked": "الحساب موجود مسبقا.",
"errorEmailLinked": "الايميل مستعمل مسبقا."
},
"validationStrings": {
"length": {

View File

@ -33,7 +33,9 @@
"errorNoEmail": "Correo electrónico requerido.",
"errorCaptcha": "Captcha Incorrecto.",
"errorPassword": "Requisitos para la contraseña.",
"errorNoMatch": "Las contraseñas no coinciden."
"errorNoMatch": "Las contraseñas no coinciden.",
"errorAccountLinked": "La cuenta ya está en uso.",
"errorEmailLinked": "El correo electrónico ya está en uso."
},
"validationStrings": {
"length": {

View File

@ -53,6 +53,12 @@
"errorDiscordVerification": "Vérification Discord requise.",
"errorMatrixVerification": "Vérification Matrix requise.",
"errorUnknown": "Erreur inconnue.",
"verified": "Compte vérifié."
"verified": "Compte vérifié.",
"errorEmailLinked": "E-mail déjà utilisé.",
"errorNoEmail": "E-mail requis.",
"errorCaptcha": "Captcha incorrect.",
"errorPassword": "Vérifiez les exigences relatives au mot de passe.",
"errorNoMatch": "Les mots de passe ne correspondent pas.",
"errorAccountLinked": "Compte déjà utilisé."
}
}

View File

@ -10,15 +10,15 @@
"username": "Felhasználónév",
"password": "Jelszó",
"reEnterPassword": "Jelszó megerősítése",
"reEnterPasswordInvalid": "A jelszók nem egyeznek",
"reEnterPasswordInvalid": "A jelszavak nem egyeznek",
"createAccountButton": "Fiók létrehozása",
"passwordRequirementsHeader": "Jelszó követelmények",
"successHeader": "Siker!",
"confirmationRequired": "E-mail megerősítés szükséges",
"confirmationRequiredMessage": "Kérjük ellenőrizze az e-mail címére küldött üzenetet, a fiók ellenőrzéséhez.",
"yourAccountIsValidUntil": "A fiókja eddig lesz érvényes: {date}.",
"sendPIN": "Az alábbi kódot küldje el a botnak, majd itt csatolja össze a fiókját.",
"sendPINDiscord": "Discordon haszánlja a {command} parancsot, a {server_channel} csatornában, majd írja be a PIN-t.",
"sendPIN": "Az alábbi PIN-t küldje el a botnak, majd itt csatolja össze a fiókját.",
"sendPINDiscord": "Írja be a {command} parancsot a {server_channel} Discord csatornába, adja meg a PIN-t.",
"matrixEnterUser": "Írja be a felhasználója azonosítóját majd nyomja meg a beküldés gombot. A kapott kódot ide írja be."
},
"notifications": {
@ -33,12 +33,14 @@
"verified": "Fiók ellenőrizve.",
"errorPassword": "Ellenőrizze a jelszó követelményeket.",
"errorCaptcha": "Hibás Captcha.",
"errorNoMatch": "A jelszavak nem egyeznek."
"errorNoMatch": "A jelszavak nem egyeznek.",
"errorEmailLinked": "Az e-mail cím már használatban van.",
"errorAccountLinked": "A fiók már használatban van."
},
"validationStrings": {
"length": {
"singular": "Legalább {n} karakter",
"plural": "Legalább {n} karakter."
"plural": "Legalább {n} karakter szükséges."
},
"uppercase": {
"singular": "Legalább {n} nagybetűs karakter",

View File

@ -34,7 +34,9 @@
"errorNoEmail": "Email necessário.",
"errorCaptcha": "Captcha incorreto.",
"errorPassword": "Verifique os requisitos de senha.",
"errorNoMatch": "As senhas não coincidem."
"errorNoMatch": "As senhas não coincidem.",
"errorEmailLinked": "Este E-mail já está sendo utilizado.",
"errorAccountLinked": "Esta conta já está sendo utilizada."
},
"validationStrings": {
"length": {

View File

@ -19,7 +19,8 @@
"errorUserDisabled": "L'utilisateur est peut-être désactivé.",
"error404": "404, vérifiez l'URL interne.",
"errorInvalidUserPass": "Nom d'utilisateur/mot de passe invalide.",
"errorNotAdmin": "L'utilisateur n'est pas autorisé à gérer le serveur."
"errorNotAdmin": "L'utilisateur n'est pas autorisé à gérer le serveur.",
"errorConnectionRefused": "Connexion refusée."
},
"startPage": {
"welcome": "Bienvenue !",
@ -58,7 +59,9 @@
"authorizeWithJellyfin": "Autoriser avec Jellyfin/Emby : Les Informations de connexion sont partagées avec Jellyfin, ce qui permet plusieurs utilisateurs.",
"authorizeManual": "Utilisateur et Mot de passe : Renseignez manuellement le nom d'utilisateur et le mot de passe.",
"adminOnly": "Administrateurs seulement (recommandé)",
"emailNotice": "Votre adresse e-mail peut être utilisée pour recevoir des notifications."
"emailNotice": "Votre adresse e-mail peut être utilisée pour recevoir des notifications.",
"allowAll": "Autoriser tous les utilisateurs de Jellyfin à se connecter",
"allowAllDescription": "Non recommandé, vous devez autoriser individuellement les utilisateurs à se connecter une fois la configuration effectuée."
},
"jellyfinEmby": {
"title": "Jellyfin/Emby",

View File

@ -3,7 +3,7 @@
"name": "العربية (AR)"
},
"strings": {
"startMessage": "مرحبا!\nضع رقمك السري للتحقق من حسابك",
"startMessage": "مرحبا!\nضع رقمك السري للتحقق من حسابك.",
"discordStartMessage": "مرحبا!\nضع رقمك السري `/pin <PIN>` للتحقق من حسابك.",
"matrixStartMessage": "أهلا\nأدخل رقم التعريف الشخصي أدناه في صفحة الاشتراك في Jellyfin للتحقق من حسابك.",
"invalidPIN": "رقم التعريف الشخصي هذا غير صالح ، حاول مرة أخرى.",

View File

@ -4,9 +4,13 @@
},
"strings": {
"startMessage": "Salut !\nEntrez votre code PIN Jellyfin ici pour vérifier votre compte.",
"matrixStartMessage": "Salut !\nEntre votre code PIN Jellyfin dans la page dinscription pour vérifier votre compte.",
"matrixStartMessage": "Salut !\nEntrez le code PIN ci-dessous dans la page dinscription Jellyfin pour vérifier votre compte.",
"invalidPIN": "Ce code PIN est invalide, réessayez.",
"pinSuccess": "Succès ! Vous pouvez maintenant retourner à la page dinscription.",
"languageMessage": "Note : Découvrez les langues disponibles avec {command} et paramétrez la langue souhaitée avec {command} <language code>."
"languageMessage": "Note : Découvrez les langues disponibles avec {command} et paramétrez la langue souhaitée avec {command} <language code>.",
"discordStartMessage": "Salut!\n Entrez votre code PIN avec '/pin <PIN>' pour vérifier votre compte.",
"discordDMs": "Veuillez vérifier vos DM pour une réponse.",
"languageSet": "Langue définie sur {language}.",
"languageMessageDiscord": "Note: définissez votre langue avec /lang <nom de la langue>."
}
}

377
package-lock.json generated
View File

@ -12,9 +12,10 @@
"@ts-stack/markdown": "^1.4.0",
"@types/node": "^20.3.0",
"a17t": "^0.10.1",
"any-date-parser": "^1.5.4",
"browserslist": "^4.21.7",
"cheerio": "^1.0.0-rc.12",
"esbuild": "^0.18.1",
"esbuild": "^0.18.2",
"fs-cheerio": "^3.0.0",
"inline-source": "^8.0.2",
"jsdom": "^22.1.0",
@ -56,9 +57,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.1.tgz",
"integrity": "sha512-8+QS98jqdreHLvCojIke8NjcuelB+Osysazr15EhkUIuG0Ov5WK26XgPYWViCTjHnKQxbpS86/JryBOkEpyrBA==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.2.tgz",
"integrity": "sha512-YAnQBHlY0IvYtvY0avnXjI8ywW23emEjk5XExqbFmypath+Snq9MgY1IS47rnqBKVSqnl0ElDt221ZgaeRrkXg==",
"cpu": [
"arm"
],
@ -71,9 +72,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.1.tgz",
"integrity": "sha512-l5V0IWGTYfQMzl4ulgIHKMZPwabIS4a39ZvtkPwL6LYiX3UtL76sylA6eFKufJCB43mwEYqbXoBSMn++NpxILw==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.2.tgz",
"integrity": "sha512-1Y2pb0hLdmji8I0zBwNsYSDN7zJSQqufgLOuOsrrod00WEAgKywQR5MB/E046Is/YTP4bgcPS4BioaSDBaLaTg==",
"cpu": [
"arm64"
],
@ -86,9 +87,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.1.tgz",
"integrity": "sha512-1y8/bRek6EYxQeGTUfwL2mmj6NAeXZ3h5YSc4W2Y/kduI1B8VhT4x5X0VxrcGkIKef4N5qCdziRxvei/YUfESg==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.2.tgz",
"integrity": "sha512-P047Mh3pj8uYVE3A/B3QDX6nG8dKbHLJ+48R6Y0CRXCJ5PkXJxdHOTaS8SYs6eSR3FFU6/YQ5TishQXVHX7F5A==",
"cpu": [
"x64"
],
@ -101,9 +102,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.1.tgz",
"integrity": "sha512-FFT/on9qQTOntdloQvgfwFkRhNI5l/TCNXZS1CpH6JQd0boR637aThi9g9FYs4o31Ao72/jPZFDiRln5Cu6R2A==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.2.tgz",
"integrity": "sha512-a3Rkqd0tGVYMEKNy9SstWEdeBmM60l8FVD5o4rmwHr3xO1LbLqtCJSrWGbnf37hevo6m437mURVmpEHOmkXeTA==",
"cpu": [
"arm64"
],
@ -116,9 +117,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.1.tgz",
"integrity": "sha512-p/ZIUt+NlW8qRNVTXoKJgRuc49teazvmXBquoGOm5D6IAimTfWJVJrEivqpoMKyDS/0/PxDMRM2lrkxlSa7XeQ==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.2.tgz",
"integrity": "sha512-cvH58adz9L10JNsIcgtkWNS/1eutjRTi3rtWz1s3ZhR64BpdmkxJBAXE/UjqybyNAWLhaN8mPJdlYI2f+tQA7g==",
"cpu": [
"x64"
],
@ -131,9 +132,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.1.tgz",
"integrity": "sha512-eYUDR3thO96ULRf4rJcG9TJ/sQc6Z/YNe16mC/KvVeAOtzmeTXiPMETEv/iMqTCxZhYkHyQG/mYbAxPBWC2mcg==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.2.tgz",
"integrity": "sha512-68rGMGUdgmq+c5IvseCMqY4yaa2CAY/DIILMBA6bEU1caISF7fXnV69B1uU4s3ERuVDcasVVwiAFyNxCtkS6Zg==",
"cpu": [
"arm64"
],
@ -146,9 +147,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.1.tgz",
"integrity": "sha512-w03zjxyg51qktv0JKsV+AbY3uSb1Awifs8IkKQSUXdP3sdPxxmPzZLrlJ1+LjKZRiSa4yP/Haayi/hriNVoIdQ==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.2.tgz",
"integrity": "sha512-ZSR9On/rXoYuAtrXo5hYKy7OuZwKZyFh2rr6L3TX4UeR1tWLf84aLyAFt7e0tlRbh4zNgqFx+ePWmsSHw7L9Bw==",
"cpu": [
"x64"
],
@ -161,9 +162,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.1.tgz",
"integrity": "sha512-d6FXeb8F/cuXtSZuVHQN0Rz3gs3g2Xy/M4KJJRzbKsBx3pwCQuRdSrYxcr7g0PFN8geIOspiLQnUzwONyMA3Bw==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.2.tgz",
"integrity": "sha512-jAbA75qJ70T5AOdmw9X8675ppeRfj7j57sOypoZ4mQlfQ/LKF8eoeLzTYVo8+aqLKqeIIl0vQ4hKOB0FyG98Zg==",
"cpu": [
"arm"
],
@ -176,9 +177,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.1.tgz",
"integrity": "sha512-dHlvkKAVlYNt5LPg1GUha99QiaEGKEC21zpHVAxs7hhW6EkR8nN3iWmyndGXxVJm4K7e4lKAzl8ekPqSr5gAXQ==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.2.tgz",
"integrity": "sha512-DFKavAzbu/n9HXWuetxmYN10XnfzW7FgOgpcrGD8eXaiu77KdgB+OVWA83x9FtDYtsoFpfdlDuVFAQFfrhu77A==",
"cpu": [
"arm64"
],
@ -191,9 +192,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.1.tgz",
"integrity": "sha512-QHS4duBPuAsLZP82sNeoqTXAJ1mNU4QcfmYtBN/jNvQJXb6n0im8F4ljFSAQbivt1jl1OnKL8HhlLUeKY75nLg==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.2.tgz",
"integrity": "sha512-VEaK3Z+vJyDwwPsP0sovaEw1foDzrMs7XQNYEIFkOwMjSe2BipKRKUUyrznil0p8qqsK7U8W+T7oNqZpgdnD2Q==",
"cpu": [
"ia32"
],
@ -206,9 +207,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.1.tgz",
"integrity": "sha512-g4YSiF/qBvXvJhSowxaR7Ei/79otL48Qfjviuo+FpXREykA9nSe407T5ZvezFXryFgdf44Fe8lWpjvtQ+n42cQ==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.2.tgz",
"integrity": "sha512-Af1uZdB0oeJo4PW67l9aw94oakSamFxhC6ltC2eDkndozd9QygVNMTF7s7uxTLjo+BJqyVqG9wjmLCYF1o4NmA==",
"cpu": [
"loong64"
],
@ -221,9 +222,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.1.tgz",
"integrity": "sha512-/G1fzmaR5u2S9wgQhiQEhWRct0+GMpuNjhll59uv5Tjojlma9MUPinVnvpw9Re+Idb6gxe6kmzUxFP2YkC/svg==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.2.tgz",
"integrity": "sha512-WcTbt61+9dREuOFKXac4Qg+3OuRhLxPL9lmkI2P7fGuq/fWS2qq+AvGGVLMyk+OtXGDjyQolcEDeYlRoOmjRYQ==",
"cpu": [
"mips64el"
],
@ -236,9 +237,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.1.tgz",
"integrity": "sha512-NkDjIvleUc3lSV1VI3QE9Oh5mz3nY11H5TCbi4DJ8X09FGwHN5pDVXdAsQYPGjlt/frXZzq6x7vMmTOb5VyBog==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.2.tgz",
"integrity": "sha512-Ov+VHayvCPb52axma6+xm8QDawRjwHscPXedHg4U92DxlhKQ0H+6onRiC3J9kKI50p8pKKypprpCWrRrXjZN7Q==",
"cpu": [
"ppc64"
],
@ -251,9 +252,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.1.tgz",
"integrity": "sha512-IhN7Nz+HyDRnMQOLcCl6m5BgQMITMhS9O1hOqgAUIy6FI0m/0zTSkZHtvMmSIpOy1uleaGqfNDA9SM3nBeTATQ==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.2.tgz",
"integrity": "sha512-qW37zzKKN9C5l5LnVDriOK0eZRzQeixhtrfd5C78PAsTE15GeHU9G0oyT/u/IkNjEBjXWpTZOOHKNbjhrvuL9g==",
"cpu": [
"riscv64"
],
@ -266,9 +267,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.1.tgz",
"integrity": "sha512-u9iRg0eUUIyBbg5hANvRBYRaAnhVemAA2+pi3IgrzQTMeR/uPHQtJI3XInNZkNR6ACA4Fdl8N941p81XygeqWQ==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.2.tgz",
"integrity": "sha512-izzEFMRO8LaQIlX22+fTgP5I7Os3T51mtAWsRNpZ5pMfQIa9PqtgFAoRcb10DV+/YkH/TMMxQIlevUvDS6E4vw==",
"cpu": [
"s390x"
],
@ -281,9 +282,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.1.tgz",
"integrity": "sha512-0QeWU0a0+RmxPCDt+plXS7/hVMJtfde/LaSzs6X3UTr4FYA0hYpnwDzGXxumcPLzt5c8ctugPuKat0tmRb7noQ==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.2.tgz",
"integrity": "sha512-y5yqQ1ww4FfI9bQ1ZP/0k1rcgA6Ql2/AgzvqpowN0Q5tXDZkCavPdJbFXKrqA43vd1UTXt+AutTHYJ7km6e2Eg==",
"cpu": [
"x64"
],
@ -296,9 +297,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.1.tgz",
"integrity": "sha512-eFL7sxibN8wKuwrRf3HRkcjELALlfl/TavJ8P4J+BJfnkXOITF7zx4tTmGkzK8OwAR9snT2kEfp1Ictc80atGw==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.2.tgz",
"integrity": "sha512-usNjpKFf83X4o60gdMD47NCblaSZ6DARf31/FyCzxOgnF80mJ+RhDs9RTqgyfH8KyduO5mjgInw9+ct286ayYA==",
"cpu": [
"x64"
],
@ -311,9 +312,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.1.tgz",
"integrity": "sha512-riCQUnngF2xYUzr0XDdrGEEz0nqnocch0No7SBIQM22wTRi5gt5WqTQexGd/2u2Z19d0rVYQbKBelaJ1dwe9bg==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.2.tgz",
"integrity": "sha512-6urzy1+VwcPuhG+5jwHA8lD9E87E5+ey3qKw2EhRS+qUmMxLvfwP8szWC2JHVGZDPEDge6fgn0pBj+y9rxDLwQ==",
"cpu": [
"x64"
],
@ -326,9 +327,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.1.tgz",
"integrity": "sha512-6lTop2k+GMkWlrwMy2+55xIBXKfXOi6uzWYypXZZP8HxXG3Mb5N4O71z2KzisVNJtYK2VlQRNbmGtUzIQIhHAw==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.2.tgz",
"integrity": "sha512-SMZPTACsvpKYAIl9o8nhnmMn6/lp62iMeV/2EBMtj+sW6dXwW9b0cLjihkBv4PG1CCRlwWKPZo43imqZxC95ZA==",
"cpu": [
"x64"
],
@ -341,9 +342,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.1.tgz",
"integrity": "sha512-d6wt4g9GluZp7xCmgpm7gY6wy0mjcBHbKeeK9MYrlWNFJd8KBcD2uCil8kFuaH3Dt6AUz62D0wIoDETFsZ01Tg==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.2.tgz",
"integrity": "sha512-H2zzjPdzSDNwUnZdZf9/xfm0CYqHFXuenCMAx+tRzIRqWUT6MmZ9/q7722KnAZ6uPpq0RLs7EjCIIfmt6CaRGg==",
"cpu": [
"arm64"
],
@ -356,9 +357,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.1.tgz",
"integrity": "sha512-z51DOtcwECu4WlqJUhu39AVnnpaVmTvXei0EQxc99QK7ZJyn4tj0EelYkMBZckpqzqB/GyGSLwEclKtRJ0l2uw==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.2.tgz",
"integrity": "sha512-lfyjTN+FrKgvNvrH7nOLtaz58J/8coZOo4LQwgBMP4D7ZOurhvluXS3GjePLzq9GbWnJDZdKCKbMKhZPPcdJJA==",
"cpu": [
"ia32"
],
@ -371,9 +372,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.1.tgz",
"integrity": "sha512-6tdeuCLT+l9QuCFaYsNtULO6xH2fgJObvICMCsOZvkqIey6FUXVVju5aO+OZjxswy7WgKadhI1k/nq2wQSmB+Q==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.2.tgz",
"integrity": "sha512-Q4nIjqWXjxkELwd7kVepsJxbQ/6ERNsHpjz1j+IKjwSYw+g06U0RQOy5xh848AHvgr9itnGLa3cT2G5t0dBFsw==",
"cpu": [
"x64"
],
@ -630,6 +631,11 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/any-date-parser": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/any-date-parser/-/any-date-parser-1.5.4.tgz",
"integrity": "sha512-S4gl9UmXNk9XXSQxp5w5harUD6aM0fepyL3dZM/B3znX57sWf792hS2UvvCFIHECfpsqfKbQ+cqWBky4AKyRIg=="
},
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
@ -1641,9 +1647,9 @@
}
},
"node_modules/esbuild": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.1.tgz",
"integrity": "sha512-ZUvsIx2wPjpj86b805UwbGJu47Kxgr2UTio9rGhWpBr1oOVk88exzoieOgTweX0UcVCwSAk3z2WvNALpTcpQZw==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.2.tgz",
"integrity": "sha512-1P4sK9gXVcjvrrUjE94Hbo9goU+T6U1sdzLf+JJ+3uI6GEb4e4n3Wrqto9hZHUWabblpT2ifmC61LhZnLyTNFw==",
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
@ -1652,28 +1658,28 @@
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.18.1",
"@esbuild/android-arm64": "0.18.1",
"@esbuild/android-x64": "0.18.1",
"@esbuild/darwin-arm64": "0.18.1",
"@esbuild/darwin-x64": "0.18.1",
"@esbuild/freebsd-arm64": "0.18.1",
"@esbuild/freebsd-x64": "0.18.1",
"@esbuild/linux-arm": "0.18.1",
"@esbuild/linux-arm64": "0.18.1",
"@esbuild/linux-ia32": "0.18.1",
"@esbuild/linux-loong64": "0.18.1",
"@esbuild/linux-mips64el": "0.18.1",
"@esbuild/linux-ppc64": "0.18.1",
"@esbuild/linux-riscv64": "0.18.1",
"@esbuild/linux-s390x": "0.18.1",
"@esbuild/linux-x64": "0.18.1",
"@esbuild/netbsd-x64": "0.18.1",
"@esbuild/openbsd-x64": "0.18.1",
"@esbuild/sunos-x64": "0.18.1",
"@esbuild/win32-arm64": "0.18.1",
"@esbuild/win32-ia32": "0.18.1",
"@esbuild/win32-x64": "0.18.1"
"@esbuild/android-arm": "0.18.2",
"@esbuild/android-arm64": "0.18.2",
"@esbuild/android-x64": "0.18.2",
"@esbuild/darwin-arm64": "0.18.2",
"@esbuild/darwin-x64": "0.18.2",
"@esbuild/freebsd-arm64": "0.18.2",
"@esbuild/freebsd-x64": "0.18.2",
"@esbuild/linux-arm": "0.18.2",
"@esbuild/linux-arm64": "0.18.2",
"@esbuild/linux-ia32": "0.18.2",
"@esbuild/linux-loong64": "0.18.2",
"@esbuild/linux-mips64el": "0.18.2",
"@esbuild/linux-ppc64": "0.18.2",
"@esbuild/linux-riscv64": "0.18.2",
"@esbuild/linux-s390x": "0.18.2",
"@esbuild/linux-x64": "0.18.2",
"@esbuild/netbsd-x64": "0.18.2",
"@esbuild/openbsd-x64": "0.18.2",
"@esbuild/sunos-x64": "0.18.2",
"@esbuild/win32-arm64": "0.18.2",
"@esbuild/win32-ia32": "0.18.2",
"@esbuild/win32-x64": "0.18.2"
}
},
"node_modules/escalade": {
@ -6791,135 +6797,135 @@
}
},
"@esbuild/android-arm": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.1.tgz",
"integrity": "sha512-8+QS98jqdreHLvCojIke8NjcuelB+Osysazr15EhkUIuG0Ov5WK26XgPYWViCTjHnKQxbpS86/JryBOkEpyrBA==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.2.tgz",
"integrity": "sha512-YAnQBHlY0IvYtvY0avnXjI8ywW23emEjk5XExqbFmypath+Snq9MgY1IS47rnqBKVSqnl0ElDt221ZgaeRrkXg==",
"optional": true
},
"@esbuild/android-arm64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.1.tgz",
"integrity": "sha512-l5V0IWGTYfQMzl4ulgIHKMZPwabIS4a39ZvtkPwL6LYiX3UtL76sylA6eFKufJCB43mwEYqbXoBSMn++NpxILw==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.2.tgz",
"integrity": "sha512-1Y2pb0hLdmji8I0zBwNsYSDN7zJSQqufgLOuOsrrod00WEAgKywQR5MB/E046Is/YTP4bgcPS4BioaSDBaLaTg==",
"optional": true
},
"@esbuild/android-x64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.1.tgz",
"integrity": "sha512-1y8/bRek6EYxQeGTUfwL2mmj6NAeXZ3h5YSc4W2Y/kduI1B8VhT4x5X0VxrcGkIKef4N5qCdziRxvei/YUfESg==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.2.tgz",
"integrity": "sha512-P047Mh3pj8uYVE3A/B3QDX6nG8dKbHLJ+48R6Y0CRXCJ5PkXJxdHOTaS8SYs6eSR3FFU6/YQ5TishQXVHX7F5A==",
"optional": true
},
"@esbuild/darwin-arm64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.1.tgz",
"integrity": "sha512-FFT/on9qQTOntdloQvgfwFkRhNI5l/TCNXZS1CpH6JQd0boR637aThi9g9FYs4o31Ao72/jPZFDiRln5Cu6R2A==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.2.tgz",
"integrity": "sha512-a3Rkqd0tGVYMEKNy9SstWEdeBmM60l8FVD5o4rmwHr3xO1LbLqtCJSrWGbnf37hevo6m437mURVmpEHOmkXeTA==",
"optional": true
},
"@esbuild/darwin-x64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.1.tgz",
"integrity": "sha512-p/ZIUt+NlW8qRNVTXoKJgRuc49teazvmXBquoGOm5D6IAimTfWJVJrEivqpoMKyDS/0/PxDMRM2lrkxlSa7XeQ==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.2.tgz",
"integrity": "sha512-cvH58adz9L10JNsIcgtkWNS/1eutjRTi3rtWz1s3ZhR64BpdmkxJBAXE/UjqybyNAWLhaN8mPJdlYI2f+tQA7g==",
"optional": true
},
"@esbuild/freebsd-arm64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.1.tgz",
"integrity": "sha512-eYUDR3thO96ULRf4rJcG9TJ/sQc6Z/YNe16mC/KvVeAOtzmeTXiPMETEv/iMqTCxZhYkHyQG/mYbAxPBWC2mcg==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.2.tgz",
"integrity": "sha512-68rGMGUdgmq+c5IvseCMqY4yaa2CAY/DIILMBA6bEU1caISF7fXnV69B1uU4s3ERuVDcasVVwiAFyNxCtkS6Zg==",
"optional": true
},
"@esbuild/freebsd-x64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.1.tgz",
"integrity": "sha512-w03zjxyg51qktv0JKsV+AbY3uSb1Awifs8IkKQSUXdP3sdPxxmPzZLrlJ1+LjKZRiSa4yP/Haayi/hriNVoIdQ==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.2.tgz",
"integrity": "sha512-ZSR9On/rXoYuAtrXo5hYKy7OuZwKZyFh2rr6L3TX4UeR1tWLf84aLyAFt7e0tlRbh4zNgqFx+ePWmsSHw7L9Bw==",
"optional": true
},
"@esbuild/linux-arm": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.1.tgz",
"integrity": "sha512-d6FXeb8F/cuXtSZuVHQN0Rz3gs3g2Xy/M4KJJRzbKsBx3pwCQuRdSrYxcr7g0PFN8geIOspiLQnUzwONyMA3Bw==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.2.tgz",
"integrity": "sha512-jAbA75qJ70T5AOdmw9X8675ppeRfj7j57sOypoZ4mQlfQ/LKF8eoeLzTYVo8+aqLKqeIIl0vQ4hKOB0FyG98Zg==",
"optional": true
},
"@esbuild/linux-arm64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.1.tgz",
"integrity": "sha512-dHlvkKAVlYNt5LPg1GUha99QiaEGKEC21zpHVAxs7hhW6EkR8nN3iWmyndGXxVJm4K7e4lKAzl8ekPqSr5gAXQ==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.2.tgz",
"integrity": "sha512-DFKavAzbu/n9HXWuetxmYN10XnfzW7FgOgpcrGD8eXaiu77KdgB+OVWA83x9FtDYtsoFpfdlDuVFAQFfrhu77A==",
"optional": true
},
"@esbuild/linux-ia32": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.1.tgz",
"integrity": "sha512-QHS4duBPuAsLZP82sNeoqTXAJ1mNU4QcfmYtBN/jNvQJXb6n0im8F4ljFSAQbivt1jl1OnKL8HhlLUeKY75nLg==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.2.tgz",
"integrity": "sha512-VEaK3Z+vJyDwwPsP0sovaEw1foDzrMs7XQNYEIFkOwMjSe2BipKRKUUyrznil0p8qqsK7U8W+T7oNqZpgdnD2Q==",
"optional": true
},
"@esbuild/linux-loong64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.1.tgz",
"integrity": "sha512-g4YSiF/qBvXvJhSowxaR7Ei/79otL48Qfjviuo+FpXREykA9nSe407T5ZvezFXryFgdf44Fe8lWpjvtQ+n42cQ==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.2.tgz",
"integrity": "sha512-Af1uZdB0oeJo4PW67l9aw94oakSamFxhC6ltC2eDkndozd9QygVNMTF7s7uxTLjo+BJqyVqG9wjmLCYF1o4NmA==",
"optional": true
},
"@esbuild/linux-mips64el": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.1.tgz",
"integrity": "sha512-/G1fzmaR5u2S9wgQhiQEhWRct0+GMpuNjhll59uv5Tjojlma9MUPinVnvpw9Re+Idb6gxe6kmzUxFP2YkC/svg==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.2.tgz",
"integrity": "sha512-WcTbt61+9dREuOFKXac4Qg+3OuRhLxPL9lmkI2P7fGuq/fWS2qq+AvGGVLMyk+OtXGDjyQolcEDeYlRoOmjRYQ==",
"optional": true
},
"@esbuild/linux-ppc64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.1.tgz",
"integrity": "sha512-NkDjIvleUc3lSV1VI3QE9Oh5mz3nY11H5TCbi4DJ8X09FGwHN5pDVXdAsQYPGjlt/frXZzq6x7vMmTOb5VyBog==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.2.tgz",
"integrity": "sha512-Ov+VHayvCPb52axma6+xm8QDawRjwHscPXedHg4U92DxlhKQ0H+6onRiC3J9kKI50p8pKKypprpCWrRrXjZN7Q==",
"optional": true
},
"@esbuild/linux-riscv64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.1.tgz",
"integrity": "sha512-IhN7Nz+HyDRnMQOLcCl6m5BgQMITMhS9O1hOqgAUIy6FI0m/0zTSkZHtvMmSIpOy1uleaGqfNDA9SM3nBeTATQ==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.2.tgz",
"integrity": "sha512-qW37zzKKN9C5l5LnVDriOK0eZRzQeixhtrfd5C78PAsTE15GeHU9G0oyT/u/IkNjEBjXWpTZOOHKNbjhrvuL9g==",
"optional": true
},
"@esbuild/linux-s390x": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.1.tgz",
"integrity": "sha512-u9iRg0eUUIyBbg5hANvRBYRaAnhVemAA2+pi3IgrzQTMeR/uPHQtJI3XInNZkNR6ACA4Fdl8N941p81XygeqWQ==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.2.tgz",
"integrity": "sha512-izzEFMRO8LaQIlX22+fTgP5I7Os3T51mtAWsRNpZ5pMfQIa9PqtgFAoRcb10DV+/YkH/TMMxQIlevUvDS6E4vw==",
"optional": true
},
"@esbuild/linux-x64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.1.tgz",
"integrity": "sha512-0QeWU0a0+RmxPCDt+plXS7/hVMJtfde/LaSzs6X3UTr4FYA0hYpnwDzGXxumcPLzt5c8ctugPuKat0tmRb7noQ==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.2.tgz",
"integrity": "sha512-y5yqQ1ww4FfI9bQ1ZP/0k1rcgA6Ql2/AgzvqpowN0Q5tXDZkCavPdJbFXKrqA43vd1UTXt+AutTHYJ7km6e2Eg==",
"optional": true
},
"@esbuild/netbsd-x64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.1.tgz",
"integrity": "sha512-eFL7sxibN8wKuwrRf3HRkcjELALlfl/TavJ8P4J+BJfnkXOITF7zx4tTmGkzK8OwAR9snT2kEfp1Ictc80atGw==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.2.tgz",
"integrity": "sha512-usNjpKFf83X4o60gdMD47NCblaSZ6DARf31/FyCzxOgnF80mJ+RhDs9RTqgyfH8KyduO5mjgInw9+ct286ayYA==",
"optional": true
},
"@esbuild/openbsd-x64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.1.tgz",
"integrity": "sha512-riCQUnngF2xYUzr0XDdrGEEz0nqnocch0No7SBIQM22wTRi5gt5WqTQexGd/2u2Z19d0rVYQbKBelaJ1dwe9bg==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.2.tgz",
"integrity": "sha512-6urzy1+VwcPuhG+5jwHA8lD9E87E5+ey3qKw2EhRS+qUmMxLvfwP8szWC2JHVGZDPEDge6fgn0pBj+y9rxDLwQ==",
"optional": true
},
"@esbuild/sunos-x64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.1.tgz",
"integrity": "sha512-6lTop2k+GMkWlrwMy2+55xIBXKfXOi6uzWYypXZZP8HxXG3Mb5N4O71z2KzisVNJtYK2VlQRNbmGtUzIQIhHAw==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.2.tgz",
"integrity": "sha512-SMZPTACsvpKYAIl9o8nhnmMn6/lp62iMeV/2EBMtj+sW6dXwW9b0cLjihkBv4PG1CCRlwWKPZo43imqZxC95ZA==",
"optional": true
},
"@esbuild/win32-arm64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.1.tgz",
"integrity": "sha512-d6wt4g9GluZp7xCmgpm7gY6wy0mjcBHbKeeK9MYrlWNFJd8KBcD2uCil8kFuaH3Dt6AUz62D0wIoDETFsZ01Tg==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.2.tgz",
"integrity": "sha512-H2zzjPdzSDNwUnZdZf9/xfm0CYqHFXuenCMAx+tRzIRqWUT6MmZ9/q7722KnAZ6uPpq0RLs7EjCIIfmt6CaRGg==",
"optional": true
},
"@esbuild/win32-ia32": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.1.tgz",
"integrity": "sha512-z51DOtcwECu4WlqJUhu39AVnnpaVmTvXei0EQxc99QK7ZJyn4tj0EelYkMBZckpqzqB/GyGSLwEclKtRJ0l2uw==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.2.tgz",
"integrity": "sha512-lfyjTN+FrKgvNvrH7nOLtaz58J/8coZOo4LQwgBMP4D7ZOurhvluXS3GjePLzq9GbWnJDZdKCKbMKhZPPcdJJA==",
"optional": true
},
"@esbuild/win32-x64": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.1.tgz",
"integrity": "sha512-6tdeuCLT+l9QuCFaYsNtULO6xH2fgJObvICMCsOZvkqIey6FUXVVju5aO+OZjxswy7WgKadhI1k/nq2wQSmB+Q==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.2.tgz",
"integrity": "sha512-Q4nIjqWXjxkELwd7kVepsJxbQ/6ERNsHpjz1j+IKjwSYw+g06U0RQOy5xh848AHvgr9itnGLa3cT2G5t0dBFsw==",
"optional": true
},
"@jridgewell/gen-mapping": {
@ -7110,6 +7116,11 @@
"color-convert": "^2.0.1"
}
},
"any-date-parser": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/any-date-parser/-/any-date-parser-1.5.4.tgz",
"integrity": "sha512-S4gl9UmXNk9XXSQxp5w5harUD6aM0fepyL3dZM/B3znX57sWf792hS2UvvCFIHECfpsqfKbQ+cqWBky4AKyRIg=="
},
"any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
@ -7885,32 +7896,32 @@
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
},
"esbuild": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.1.tgz",
"integrity": "sha512-ZUvsIx2wPjpj86b805UwbGJu47Kxgr2UTio9rGhWpBr1oOVk88exzoieOgTweX0UcVCwSAk3z2WvNALpTcpQZw==",
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.2.tgz",
"integrity": "sha512-1P4sK9gXVcjvrrUjE94Hbo9goU+T6U1sdzLf+JJ+3uI6GEb4e4n3Wrqto9hZHUWabblpT2ifmC61LhZnLyTNFw==",
"requires": {
"@esbuild/android-arm": "0.18.1",
"@esbuild/android-arm64": "0.18.1",
"@esbuild/android-x64": "0.18.1",
"@esbuild/darwin-arm64": "0.18.1",
"@esbuild/darwin-x64": "0.18.1",
"@esbuild/freebsd-arm64": "0.18.1",
"@esbuild/freebsd-x64": "0.18.1",
"@esbuild/linux-arm": "0.18.1",
"@esbuild/linux-arm64": "0.18.1",
"@esbuild/linux-ia32": "0.18.1",
"@esbuild/linux-loong64": "0.18.1",
"@esbuild/linux-mips64el": "0.18.1",
"@esbuild/linux-ppc64": "0.18.1",
"@esbuild/linux-riscv64": "0.18.1",
"@esbuild/linux-s390x": "0.18.1",
"@esbuild/linux-x64": "0.18.1",
"@esbuild/netbsd-x64": "0.18.1",
"@esbuild/openbsd-x64": "0.18.1",
"@esbuild/sunos-x64": "0.18.1",
"@esbuild/win32-arm64": "0.18.1",
"@esbuild/win32-ia32": "0.18.1",
"@esbuild/win32-x64": "0.18.1"
"@esbuild/android-arm": "0.18.2",
"@esbuild/android-arm64": "0.18.2",
"@esbuild/android-x64": "0.18.2",
"@esbuild/darwin-arm64": "0.18.2",
"@esbuild/darwin-x64": "0.18.2",
"@esbuild/freebsd-arm64": "0.18.2",
"@esbuild/freebsd-x64": "0.18.2",
"@esbuild/linux-arm": "0.18.2",
"@esbuild/linux-arm64": "0.18.2",
"@esbuild/linux-ia32": "0.18.2",
"@esbuild/linux-loong64": "0.18.2",
"@esbuild/linux-mips64el": "0.18.2",
"@esbuild/linux-ppc64": "0.18.2",
"@esbuild/linux-riscv64": "0.18.2",
"@esbuild/linux-s390x": "0.18.2",
"@esbuild/linux-x64": "0.18.2",
"@esbuild/netbsd-x64": "0.18.2",
"@esbuild/openbsd-x64": "0.18.2",
"@esbuild/sunos-x64": "0.18.2",
"@esbuild/win32-arm64": "0.18.2",
"@esbuild/win32-ia32": "0.18.2",
"@esbuild/win32-x64": "0.18.2"
}
},
"escalade": {

View File

@ -20,9 +20,10 @@
"@ts-stack/markdown": "^1.4.0",
"@types/node": "^20.3.0",
"a17t": "^0.10.1",
"any-date-parser": "^1.5.4",
"browserslist": "^4.21.7",
"cheerio": "^1.0.0-rc.12",
"esbuild": "^0.18.1",
"esbuild": "^0.18.2",
"fs-cheerio": "^3.0.0",
"inline-source": "^8.0.2",
"jsdom": "^22.1.0",

View File

@ -0,0 +1,5 @@
module github.com/hrfee/jfa-go/scripts/account-gen
go 1.20
require github.com/hrfee/mediabrowser v0.3.8 // indirect

View File

@ -0,0 +1,2 @@
github.com/hrfee/mediabrowser v0.3.8 h1:y0iBCb6jE3QKcsiCJSYva2fFPHRn4UA+sGRzoPuJ/Dk=
github.com/hrfee/mediabrowser v0.3.8/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=

122
scripts/account-gen/main.go Normal file
View File

@ -0,0 +1,122 @@
package main
import (
"bufio"
"fmt"
"log"
"math/rand"
"os"
"strconv"
"strings"
"time"
"github.com/hrfee/mediabrowser"
)
var (
names = []string{"Aaron", "Agnes", "Bridget", "Brandon", "Dolly", "Drake", "Elizabeth", "Erika", "Geoff", "Graham", "Haley", "Halsey", "Josie", "John", "Kayleigh", "Luka", "Melissa", "Nasreen", "Paul", "Ross", "Sam", "Talib", "Veronika", "Zaynab"}
)
const (
PASSWORD = "test"
COUNT = 10
)
func main() {
fmt.Println("Usage: account-gen <server> <username> <password, or file://path to file containing password>")
var server, username, password string
reader := bufio.NewReader(os.Stdin)
if len(os.Args) > 1 {
server = os.Args[1]
} else {
fmt.Print("Server Address: ")
server, _ = reader.ReadString('\n')
server = strings.TrimSuffix(server, "\n")
}
if len(os.Args) > 2 {
username = os.Args[2]
} else {
fmt.Print("Username: ")
username, _ = reader.ReadString('\n')
username = strings.TrimSuffix(username, "\n")
}
if len(os.Args) > 3 {
password = os.Args[3]
if strings.HasPrefix(password, "file://") {
p, err := os.ReadFile(strings.TrimPrefix(password, "file://"))
if err != nil {
log.Fatalf("Failed to read password file \"%s\": %+v\n", password, err)
}
password = strings.TrimSuffix(string(p), "\n")
}
} else {
fmt.Print("Password: ")
password, _ = reader.ReadString('\n')
password = strings.TrimSuffix(password, "\n")
}
jf, err := mediabrowser.NewServer(
mediabrowser.JellyfinServer,
server,
"jfa-go-account-gen-script",
"0.0.1",
"testing",
"my_left_foot",
mediabrowser.NewNamedTimeoutHandler("Jellyfin Account Gen", "\""+server+"\"", true),
30,
)
if err != nil {
log.Fatalf("Failed to connect to Jellyin @ \"%s\": %+v\n", server, err)
}
_, status, err := jf.Authenticate(username, password)
if status != 200 || err != nil {
log.Fatalf("Failed to authenticate: %+v\n", err)
}
jfTemp, err := mediabrowser.NewServer(
mediabrowser.JellyfinServer,
server,
"jfa-go-account-gen-script",
"0.0.1",
"fake-activity",
"my_left_foot",
mediabrowser.NewNamedTimeoutHandler("Jellyfin Account Gen", "\""+server+"\"", true),
30,
)
if err != nil {
log.Fatalf("Failed to connect to Jellyin @ \"%s\": %+v\n", server, err)
}
rand.Seed(time.Now().Unix())
for i := 0; i < COUNT; i++ {
name := names[rand.Intn(len(names))] + strconv.Itoa(rand.Intn(100))
user, status, err := jf.NewUser(name, PASSWORD)
if (status != 200 && status != 201 && status != 204) || err != nil {
log.Fatalf("Failed to create user \"%s\" (%d): %+v\n", name, status, err)
}
if rand.Intn(100) > 65 {
user.Policy.IsAdministrator = true
}
if rand.Intn(100) > 80 {
user.Policy.IsDisabled = true
}
status, err = jf.SetPolicy(user.ID, user.Policy)
if (status != 200 && status != 201 && status != 204) || err != nil {
log.Fatalf("Failed to set policy for user \"%s\" (%d): %+v\n", name, status, err)
}
if rand.Intn(100) > 20 {
jfTemp.Authenticate(name, PASSWORD)
}
}
}

View File

@ -3,6 +3,7 @@ import { templateEmail } from "../modules/settings.js";
import { Marked } from "@ts-stack/markdown";
import { stripMarkdown } from "../modules/stripmd.js";
import { DiscordUser, newDiscordSearch } from "../modules/discord.js";
const dateParser = require("any-date-parser");
interface User {
id: string;
@ -38,6 +39,7 @@ interface announcementTemplate {
var addDiscord: (passData: string) => void;
class user implements User {
private _id = "";
private _row: HTMLTableRowElement;
private _check: HTMLInputElement;
private _username: HTMLSpanElement;
@ -66,7 +68,6 @@ class user implements User {
private _userLabel: string;
private _labelEditButton: HTMLElement;
private _accounts_admin: HTMLInputElement
id = "";
private _selected: boolean;
lastNotifyMethod = (): string => {
@ -459,6 +460,20 @@ class user implements User {
this._label.classList.add("chip", "~gray", "mr-2");
}
}
matchesSearch = (query: string): boolean => {
return (
this.name.includes(query) ||
this.label.includes(query) ||
this.discord.includes(query) ||
this.email.includes(query) ||
this.id.includes(query) ||
this.label.includes(query) ||
this.matrix.includes(query) ||
this.telegram.includes(query)
);
}
private _checkEvent = new CustomEvent("accountCheckEvent");
private _uncheckEvent = new CustomEvent("accountUncheckEvent");
@ -675,6 +690,9 @@ class user implements User {
}
});
get id() { return this._id; }
set id(v: string) { this._id = v; }
update = (user: User) => {
this.id = user.id;
@ -737,7 +755,7 @@ export class accountsList {
private _selectAll = document.getElementById("accounts-select-all") as HTMLInputElement;
private _users: { [id: string]: user };
private _sortedByName: string[] = [];
private _ordering: string[] = [];
private _checkCount: number = 0;
private _inSearch = false;
// Whether the enable/disable button should enable or not.
@ -748,6 +766,14 @@ export class accountsList {
private _addUserEmail = this._addUserForm.querySelector("input[type=email]") as HTMLInputElement;
private _addUserPassword = this._addUserForm.querySelector("input[type=password]") as HTMLInputElement;
// Columns for sorting.
private _columns: { [className: string]: Column } = {};
private _activeSortColumn: string;
private _sortingByButton = document.getElementById("accounts-sort-by-field") as HTMLButtonElement;
private _filterArea = document.getElementById("accounts-filter-area");
private _searchOptionsHeader = document.getElementById("accounts-search-options-header");
// Whether the "Extend expiry" is extending or setting an expiry.
private _settingExpiry = false;
@ -769,40 +795,337 @@ export class accountsList {
}
}
search = (query: string): string[] => {
query = query.toLowerCase()
let result: string[] = [];
if (query.includes(":")) { // Support admin:<true/false> and disabled:<true/false>
const words = query.split(" ");
showHideSearchOptionsHeader = () => {
const sortingBy = !(this._sortingByButton.parentElement.classList.contains("hidden"));
const hasFilters = this._filterArea.textContent != "";
console.log("sortingBy", sortingBy, "hasFilters", hasFilters);
if (sortingBy || hasFilters) {
this._searchOptionsHeader.classList.remove("hidden");
} else {
this._searchOptionsHeader.classList.add("hidden");
}
}
private _queries: { [field: string]: { name: string, getter: string, bool: boolean, string: boolean, date: boolean, dependsOnTableHeader?: string, show?: boolean }} = {
"id": {
// We don't use a translation here to circumvent the name substitution feature.
name: "Jellyfin/Emby ID",
getter: "id",
bool: false,
string: true,
date: false
},
"label": {
name: window.lang.strings("label"),
getter: "label",
bool: true,
string: true,
date: false
},
"username": {
name: window.lang.strings("username"),
getter: "name",
bool: false,
string: true,
date: false
},
"name": {
name: window.lang.strings("username"),
getter: "name",
bool: false,
string: true,
date: false,
show: false
},
"admin": {
name: window.lang.strings("admin"),
getter: "admin",
bool: true,
string: false,
date: false
},
"disabled": {
name: window.lang.strings("disabled"),
getter: "disabled",
bool: true,
string: false,
date: false
},
"access-jfa": {
name: window.lang.strings("accessJFA"),
getter: "accounts_admin",
bool: true,
string: false,
date: false,
dependsOnTableHeader: "accounts-header-access-jfa"
},
"email": {
name: window.lang.strings("emailAddress"),
getter: "email",
bool: true,
string: true,
date: false,
dependsOnTableHeader: "accounts-header-email"
},
"telegram": {
name: "Telegram",
getter: "telegram",
bool: true,
string: true,
date: false,
dependsOnTableHeader: "accounts-header-telegram"
},
"matrix": {
name: "Matrix",
getter: "matrix",
bool: true,
string: true,
date: false,
dependsOnTableHeader: "accounts-header-matrix"
},
"discord": {
name: "Discord",
getter: "discord",
bool: true,
string: true,
date: false,
dependsOnTableHeader: "accounts-header-discord"
},
"expiry": {
name: window.lang.strings("expiry"),
getter: "expiry",
bool: true,
string: false,
date: true,
dependsOnTableHeader: "accounts-header-expiry"
},
"last-active": {
name: window.lang.strings("lastActiveTime"),
getter: "last_active",
bool: true,
string: false,
date: true
}
}
search = (query: String): string[] => {
console.log(this._queries);
this._filterArea.textContent = "";
query = query.toLowerCase();
let result: string[] = [...this._ordering];
// console.log("initial:", result);
// const words = query.split(" ");
let words: string[] = [];
// FIXME: SPLIT BY SPACE, UNLESS IN QUOTES
let quoteSymbol = ``;
let queryStart = -1;
let lastQuote = -1;
for (let i = 0; i < query.length; i++) {
if (queryStart == -1 && query[i] != " " && query[i] != `"` && query[i] != `'`) {
queryStart = i;
}
if ((query[i] == `"` || query[i] == `'`) && (quoteSymbol == `` || query[i] == quoteSymbol)) {
if (lastQuote != -1) {
lastQuote = -1;
quoteSymbol = ``;
} else {
lastQuote = i;
quoteSymbol = query[i];
}
}
if (query[i] == " " || i == query.length-1) {
if (lastQuote != -1) {
continue;
} else {
let end = i+1;
if (query[i] == " ") {
end = i;
while (i+1 < query.length && query[i+1] == " ") {
i += 1;
}
}
words.push(query.substring(queryStart, end).replace(/['"]/g, ""));
console.log("pushed", words);
queryStart = -1;
}
}
}
query = "";
for (let word of words) {
if (word.includes(":")) {
const querySplit = word.split(":")
let state = false;
if (querySplit[1] == "true" || querySplit[1] == "yes") {
state = true;
}
for (let id in this._users) {
const user = this._users[id];
let attrib: boolean;
if (querySplit[0] == "admin") { attrib = user.admin; }
else if (querySplit[0] == "disabled") { attrib = user.disabled; }
if (attrib == state) { result.push(id); }
}
} else { query += word + " "; }
if (!word.includes(":")) {
let cachedResult = [...result];
for (let id of cachedResult) {
const u = this._users[id];
if (!u.matchesSearch(word)) {
result.splice(result.indexOf(id), 1);
}
}
if (query == "") { return result; }
for (let id in this._users) {
const user = this._users[id];
if (user.name.toLowerCase().includes(query)) {
result.push(id);
} else if (user.email.toLowerCase().includes(query)) {
result.push(id);
continue;
}
const split = [word.substring(0, word.indexOf(":")), word.substring(word.indexOf(":")+1)];
if (!(split[0] in this._queries)) continue;
const queryFormat = this._queries[split[0]];
if (queryFormat.bool) {
let isBool = false;
let boolState = false;
if (split[1] == "true" || split[1] == "yes" || split[1] == "t" || split[1] == "y") {
isBool = true;
boolState = true;
} else if (split[1] == "false" || split[1] == "no" || split[1] == "f" || split[1] == "n") {
isBool = true;
boolState = false;
}
if (isBool) {
// FIXME: Generate filter card for each filter class
const filterCard = document.createElement("span");
filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter");
filterCard.classList.add("button", "~" + (boolState ? "positive" : "critical"), "@high", "center", "m-2");
filterCard.innerHTML = `
<span class="font-bold mr-2">${queryFormat.name}</span>
<i class="text-2xl ri-${boolState? "checkbox" : "close"}-circle-fill"></i>
`;
filterCard.addEventListener("click", () => {
for (let quote of [`"`, `'`, ``]) {
this._search.value = this._search.value.replace(split[0] + ":" + quote + split[1] + quote, "");
}
this._search.oninput((null as Event));
})
this._filterArea.appendChild(filterCard);
// console.log("is bool, state", boolState);
// So removing elements doesn't affect us
let cachedResult = [...result];
for (let id of cachedResult) {
const u = this._users[id];
const value = Object.getOwnPropertyDescriptor(user.prototype, queryFormat.getter).get.call(u);
// console.log("got", queryFormat.getter + ":", value);
// Remove from result if not matching query
if (!((value && boolState) || (!value && !boolState))) {
// console.log("not matching, result is", result);
result.splice(result.indexOf(id), 1);
}
}
return result;
continue
}
}
if (queryFormat.string) {
const filterCard = document.createElement("span");
filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter");
filterCard.classList.add("button", "~neutral", "@low", "center", "m-2", "h-full");
filterCard.innerHTML = `
<span class="font-bold mr-2">${queryFormat.name}:</span> "${split[1]}"
`;
filterCard.addEventListener("click", () => {
for (let quote of [`"`, `'`, ``]) {
let regex = new RegExp(split[0] + ":" + quote + split[1] + quote, "ig");
this._search.value = this._search.value.replace(regex, "");
}
this._search.oninput((null as Event));
})
this._filterArea.appendChild(filterCard);
let cachedResult = [...result];
for (let id of cachedResult) {
const u = this._users[id];
const value = Object.getOwnPropertyDescriptor(user.prototype, queryFormat.getter).get.call(u);
if (!(value.includes(split[1]))) {
result.splice(result.indexOf(id), 1);
}
}
continue;
}
if (queryFormat.date) {
// -1 = Before, 0 = On, 1 = After, 2 = No symbol, assume 0
let compareType = (split[1][0] == ">") ? 1 : ((split[1][0] == "<") ? -1 : ((split[1][0] == "=") ? 0 : 2));
let unmodifiedValue = split[1];
if (compareType != 2) {
split[1] = split[1].substring(1);
}
if (compareType == 2) compareType = 0;
let attempt: { year?: number, month?: number, day?: number, hour?: number, minute?: number } = dateParser.attempt(split[1]);
// Month in Date objects is 0-based, so make our parsed date that way too
if ("month" in attempt) attempt["month"] -= 1;
let date: Date = (Date as any).fromString(split[1]) as Date;
console.log("Read", attempt, "and", date);
if ("invalid" in (date as any)) continue;
const filterCard = document.createElement("span");
filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter");
filterCard.classList.add("button", "~neutral", "@low", "center", "m-2", "h-full");
filterCard.innerHTML = `
<span class="font-bold mr-2">${queryFormat.name}:</span> ${(compareType == 1) ? window.lang.strings("after")+" " : ((compareType == -1) ? window.lang.strings("before")+" " : "")}${split[1]}
`;
filterCard.addEventListener("click", () => {
for (let quote of [`"`, `'`, ``]) {
let regex = new RegExp(split[0] + ":" + quote + unmodifiedValue + quote, "ig");
this._search.value = this._search.value.replace(regex, "");
}
this._search.oninput((null as Event));
})
this._filterArea.appendChild(filterCard);
let cachedResult = [...result];
for (let id of cachedResult) {
const u = this._users[id];
const unixValue = Object.getOwnPropertyDescriptor(user.prototype, queryFormat.getter).get.call(u);
if (unixValue == 0) {
result.splice(result.indexOf(id), 1);
continue;
}
let value = new Date(unixValue*1000);
const getterPairs: [string, () => number][] = [["year", Date.prototype.getFullYear], ["month", Date.prototype.getMonth], ["day", Date.prototype.getDate], ["hour", Date.prototype.getHours], ["minute", Date.prototype.getMinutes]];
// When doing > or < <time> with no date, we need to ignore the rest of the Date object
if (compareType != 0 && Object.keys(attempt).length == 2 && "hour" in attempt && "minute" in attempt) {
const temp = new Date(date.valueOf());
temp.setHours(value.getHours(), value.getMinutes());
value = temp;
console.log("just hours/minutes workaround, value set to", value);
}
let match = true;
if (compareType == 0) {
for (let pair of getterPairs) {
if (pair[0] in attempt) {
if (compareType == 0 && attempt[pair[0]] != pair[1].call(value)) {
match = false;
break;
}
}
}
} else if (compareType == -1) {
match = (value < date);
} else if (compareType == 1) {
match = (value > date);
}
if (!match) {
result.splice(result.indexOf(id), 1);
}
}
}
}
return result
};
get selectAll(): boolean { return this._selectAll.checked; }
set selectAll(state: boolean) {
@ -821,36 +1144,6 @@ export class accountsList {
add = (u: User) => {
let domAccount = new user(u);
this._users[u.id] = domAccount;
this.unhide(u.id);
}
unhide = (id: string) => {
const keys = Object.keys(this._users);
if (keys.length == 0) {
this._table.appendChild(this._users[id].asElement());
return;
}
this._sortedByName = keys.sort((a, b) => this._users[a].name.localeCompare(this._users[b].name));
let index = this._sortedByName.indexOf(id)+1;
if (index == this._sortedByName.length-1) {
this._table.appendChild(this._users[id].asElement());
return;
}
while (index < this._sortedByName.length) {
if (this._table.contains(this._users[this._sortedByName[index]].asElement())) {
this._table.insertBefore(this._users[id].asElement(), this._users[this._sortedByName[index]].asElement());
return;
}
index++;
}
this._table.appendChild(this._users[id].asElement());
}
hide = (id: string) => {
const el = this._users[id].asElement();
if (this._table.contains(el)) {
this._table.removeChild(this._users[id].asElement());
}
}
private _checkCheckCount = () => {
@ -862,10 +1155,10 @@ export class accountsList {
this._modifySettings.classList.add("unfocused");
this._deleteUser.classList.add("unfocused");
if (window.emailEnabled || window.telegramEnabled) {
this._announceButton.classList.add("unfocused");
this._announceButton.parentElement.classList.add("unfocused");
}
this._extendExpiry.classList.add("unfocused");
this._disableEnable.classList.add("unfocused");
this._disableEnable.parentElement.classList.add("unfocused");
this._sendPWR.classList.add("unfocused");
} else {
let visibleCount = 0;
@ -885,7 +1178,7 @@ export class accountsList {
this._deleteUser.classList.remove("unfocused");
this._deleteUser.textContent = window.lang.quantity("deleteUser", list.length);
if (window.emailEnabled || window.telegramEnabled) {
this._announceButton.classList.remove("unfocused");
this._announceButton.parentElement.classList.remove("unfocused");
}
let anyNonExpiries = list.length == 0 ? true : false;
let allNonExpiries = true;
@ -903,7 +1196,7 @@ export class accountsList {
}
if (showDisableEnable && this._users[id].disabled != this._shouldEnable) {
showDisableEnable = false;
this._disableEnable.classList.add("unfocused");
this._disableEnable.parentElement.classList.add("unfocused");
}
if (!showDisableEnable && anyNonExpiries) { break; }
if (!this._users[id].lastNotifyMethod()) {
@ -939,7 +1232,7 @@ export class accountsList {
this._disableEnable.classList.add("~warning");
this._disableEnable.classList.remove("~positive");
}
this._disableEnable.classList.remove("unfocused");
this._disableEnable.parentElement.classList.remove("unfocused");
this._disableEnable.textContent = message;
}
}
@ -1428,6 +1721,18 @@ export class accountsList {
window.modals.extendExpiry.show();
}
setVisibility = (users: string[], visible: boolean) => {
this._table.textContent = "";
for (let id of this._ordering) {
if (visible && users.indexOf(id) != -1) {
this._table.appendChild(this._users[id].asElement());
} else if (!visible && users.indexOf(id) == -1) {
this._table.appendChild(this._users[id].asElement());
}
}
}
constructor() {
this._populateNumbers();
this._users = {};
@ -1476,13 +1781,13 @@ export class accountsList {
this._deleteUser.classList.add("unfocused");
this._announceButton.onclick = this.announce;
this._announceButton.classList.add("unfocused");
this._announceButton.parentElement.classList.add("unfocused");
this._extendExpiry.onclick = () => { this.extendExpiry(); };
this._extendExpiry.classList.add("unfocused");
this._disableEnable.onclick = this.enableDisableUsers;
this._disableEnable.classList.add("unfocused");
this._disableEnable.parentElement.classList.add("unfocused");
this._enableExpiry.onclick = () => { this.extendExpiry(true); };
this._enableExpiryNotify.onchange = () => {
@ -1508,35 +1813,26 @@ export class accountsList {
this._deleteNotify.checked = false;
}*/
const setVisibility = (users: string[], visible: boolean) => {
for (let id in this._users) {
if (users.indexOf(id) != -1) {
if (visible) {
this.unhide(id);
} else {
this.hide(id);
}
} else {
if (visible) {
this.hide(id);
} else {
this.unhide(id);
}
}
}
}
this._search.oninput = () => {
const onchange = () => {
const query = this._search.value;
if (!query) {
setVisibility(Object.keys(this._users), true);
// this.setVisibility(this._ordering, true);
this._inSearch = false;
} else {
this._inSearch = true;
setVisibility(this.search(query), true);
// this.setVisibility(this.search(query), true);
}
this.setVisibility(this.search(query), true);
this._checkCheckCount();
this.showHideSearchOptionsHeader();
};
this._search.oninput = onchange;
const clearSearchButton = document.getElementById("accounts-search-clear") as HTMLSpanElement;
clearSearchButton.addEventListener("click", () => {
this._search.value = "";
onchange();
});
this._announceTextarea.onkeyup = this.loadPreview;
addDiscord = newDiscordSearch(window.lang.strings("linkDiscord"), window.lang.strings("searchDiscordUser"), window.lang.strings("add"), (user: DiscordUser, id: string) => {
@ -1559,6 +1855,123 @@ export class accountsList {
insertText(this._announceTextarea, announceVarUsername.children[0].textContent);
this.loadPreview();
};
const headerNames: string[] = ["username", "access-jfa", "email", "telegram", "matrix", "discord", "expiry", "last-active"];
const headerGetters: string[] = ["name", "accounts_admin", "email", "telegram", "matrix", "discord", "expiry", "last_active"];
for (let i = 0; i < headerNames.length; i++) {
const header: HTMLTableHeaderCellElement = document.querySelector(".accounts-header-" + headerNames[i]) as HTMLTableHeaderCellElement;
if (header !== null) {
this._columns[header.className] = new Column(header, Object.getOwnPropertyDescriptor(user.prototype, headerGetters[i]).get);
}
}
// Start off sorting by Name
const defaultSort = () => {
this._activeSortColumn = document.getElementsByClassName("accounts-header-" + headerNames[0])[0].className;
document.dispatchEvent(new CustomEvent("header-click", { detail: this._activeSortColumn }));
this._columns[this._activeSortColumn].ascending = true;
this._columns[this._activeSortColumn].hideIcon();
this._sortingByButton.parentElement.classList.add("hidden");
this.showHideSearchOptionsHeader();
};
this._sortingByButton.parentElement.addEventListener("click", defaultSort);
document.addEventListener("header-click", (event: CustomEvent) => {
this._ordering = this._columns[event.detail].sort(this._users);
this._activeSortColumn = event.detail;
this._sortingByButton.innerHTML = this._columns[event.detail].buttonContent;
this._sortingByButton.parentElement.classList.remove("hidden");
// console.log("ordering by", event.detail, ": ", this._ordering);
if (!(this._inSearch)) {
this.setVisibility(this._ordering, true);
} else {
this.setVisibility(this.search(this._search.value), true);
}
this.showHideSearchOptionsHeader();
});
defaultSort();
this.showHideSearchOptionsHeader();
const filterList = document.getElementById("accounts-filter-list");
const fillInFilter = (name: string, value: string, offset?: number) => {
this._search.value = name + ":" + value + " " + this._search.value;
this._search.focus();
let newPos = name.length + 1 + value.length;
if (typeof offset !== 'undefined')
newPos += offset;
this._search.setSelectionRange(newPos, newPos);
this._search.oninput(null as any);
};
// Generate filter buttons
for (let queryName of Object.keys(this._queries)) {
const query = this._queries[queryName];
if ("show" in query && !query.show) continue;
if ("dependsOnTableHeader" in query && query.dependsOnTableHeader) {
const el = document.querySelector("."+query.dependsOnTableHeader);
if (el === null) continue;
}
const container = document.createElement("span") as HTMLSpanElement;
container.classList.add("button", "button-xl", "~neutral", "@low", "mb-1", "mr-2");
container.innerHTML = `<span class="mr-2">${query.name}</span>`;
if (query.bool) {
const pos = document.createElement("button") as HTMLButtonElement;
pos.type = "button";
pos.ariaLabel = `Filter by "${query.name}": True`;
pos.classList.add("button", "~positive", "ml-2");
pos.innerHTML = `<i class="ri-checkbox-circle-fill"></i>`;
pos.addEventListener("click", () => fillInFilter(queryName, "true"));
const neg = document.createElement("button") as HTMLButtonElement;
neg.type = "button";
neg.ariaLabel = `Filter by "${query.name}": False`;
neg.classList.add("button", "~critical", "ml-2");
neg.innerHTML = `<i class="ri-close-circle-fill"></i>`;
neg.addEventListener("click", () => fillInFilter(queryName, "false"));
container.appendChild(pos);
container.appendChild(neg);
}
if (query.string) {
const button = document.createElement("button") as HTMLButtonElement;
button.type = "button";
button.classList.add("button", "~urge", "ml-2");
button.innerHTML = `<i class="ri-equal-line mr-2"></i>${window.lang.strings("matchText")}`;
// Position cursor between quotes
button.addEventListener("click", () => fillInFilter(queryName, `""`, -1));
container.appendChild(button);
}
if (query.date) {
const onDate = document.createElement("button") as HTMLButtonElement;
onDate.type = "button";
onDate.classList.add("button", "~urge", "ml-2");
onDate.innerHTML = `<i class="ri-calendar-check-line mr-2"></i>On Date`;
onDate.addEventListener("click", () => fillInFilter(queryName, `"="`, -1));
const beforeDate = document.createElement("button") as HTMLButtonElement;
beforeDate.type = "button";
beforeDate.classList.add("button", "~urge", "ml-2");
beforeDate.innerHTML = `<i class="ri-calendar-check-line mr-2"></i>Before Date`;
beforeDate.addEventListener("click", () => fillInFilter(queryName, `"<"`, -1));
const afterDate = document.createElement("button") as HTMLButtonElement;
afterDate.type = "button";
afterDate.classList.add("button", "~urge", "ml-2");
afterDate.innerHTML = `<i class="ri-calendar-check-line mr-2"></i>After Date`;
afterDate.addEventListener("click", () => fillInFilter(queryName, `">"`, -1));
container.appendChild(onDate);
container.appendChild(beforeDate);
container.appendChild(afterDate);
}
filterList.appendChild(container);
}
}
reload = () => {
@ -1579,9 +1992,98 @@ export class accountsList {
this._users[id].remove();
delete this._users[id];
}
// console.log("reload, so sorting by", this._activeSortColumn);
this._ordering = this._columns[this._activeSortColumn].sort(this._users);
if (!(this._inSearch)) {
this.setVisibility(this._ordering, true);
} else {
this.setVisibility(this.search(this._search.value), true);
}
this._checkCheckCount();
}
});
this.loadTemplates();
}
}
type GetterReturnType = Boolean | boolean | String | Number | number;
type Getter = () => GetterReturnType;
// When a column is clicked, it broadcasts it's name and ordering to be picked up and stored by accountsList
// When list is refreshed, accountList calls method of the specific Column and re-orders accordingly.
// Listen for broadcast event from others, check its not us by comparing the header className in the message, then hide the arrow icon
class Column {
private _header: HTMLTableHeaderCellElement;
private _headerContent: string;
private _getter: Getter;
private _ascending: boolean;
private _active: boolean;
constructor(header: HTMLTableHeaderCellElement, getter: Getter) {
this._header = header;
this._headerContent = this._header.textContent;
this._getter = getter;
this._ascending = true;
this._active = false;
this._header.addEventListener("click", () => {
// If we are the active sort column, a click means to switch between ascending/descending.
if (this._active) {
this._ascending = !this._ascending;
console.log("was already active, switching direction to", this._ascending ? "ascending" : "descending");
} else {
console.log("wasn't active keeping direction as", this._ascending ? "ascending" : "descending");
}
this._active = true;
this._header.setAttribute("aria-sort", this._headerContent);
this.updateHeader();
document.dispatchEvent(new CustomEvent("header-click", { detail: this._header.className }));
});
document.addEventListener("header-click", (event: CustomEvent) => {
if (event.detail != this._header.className) {
this._active = false;
this._header.removeAttribute("aria-sort");
this.hideIcon();
}
});
}
hideIcon = () => {
this._header.textContent = this._headerContent;
}
updateHeader = () => {
this._header.innerHTML = `
<span class="">${this._headerContent}</span>
<i class="ri-arrow-${this._ascending? "up" : "down"}-s-line" aria-hidden="true"></i>
`;
}
// Returns the inner HTML to show in the "Sorting By" button.
get buttonContent() {
return `<span class="font-bold">` + window.lang.strings("sortingBy") + ": " + `</span>` + this._headerContent;
}
get ascending() { return this._ascending; }
set ascending(v: boolean) {
this._ascending = v;
if (!this._active) return;
this.updateHeader();
this._header.setAttribute("aria-sort", this._headerContent);
document.dispatchEvent(new CustomEvent("header-click", { detail: this._header.className }));
}
// Sorts the user list. previouslyActive is whether this column was previously sorted by, indicating that the direction should change.
sort = (users: { [id: string]: user }): string[] => {
let userIDs = Object.keys(users);
userIDs.sort((a: string, b: string): number => {
const av: GetterReturnType = this._getter.call(users[a]);
const bv: GetterReturnType = this._getter.call(users[b]);
if (av < bv) return this._ascending ? -1 : 1;
if (av > bv) return this._ascending ? 1 : -1;
return 0;
});
return userIDs;
}
}