mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-11-09 20:00:12 +00:00
Compare commits
2 Commits
8ac3bb9711
...
76bb95098c
Author | SHA1 | Date | |
---|---|---|---|
76bb95098c | |||
0e241f56fb |
@ -579,11 +579,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-accounts" class="unfocused">
|
<div id="tab-accounts" class="unfocused">
|
||||||
<div class="card @low dark:~d_neutral accounts mb-4">
|
<div class="card @low dark:~d_neutral accounts mb-4 overflow-visible">
|
||||||
<div class="flex-expand row">
|
<div class="flex-expand row">
|
||||||
<div class="row">
|
<div class="row w-6/12">
|
||||||
<span class="text-3xl font-bold mr-2 col">{{ .strings.accounts }}</span>
|
<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 }}">
|
<input type="search" class="col sm field ~neutral @low input search ml-2 mr-2" id="accounts-search" placeholder="{{ .strings.search }}">
|
||||||
|
<div id="accounts-filter-dropdown" class="col sm dropdown pb-0i" tabindex="0">
|
||||||
|
<span class="h-100 sm button ~neutral @low center mb-2" 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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<span class="col sm button ~neutral @low center mb-2" id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span>
|
<span class="col sm button ~neutral @low center mb-2" id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span>
|
||||||
|
@ -118,6 +118,7 @@
|
|||||||
"accessJFA": "Access jfa-go",
|
"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",
|
"sortingBy": "Sorting By",
|
||||||
|
"filters": "Filters",
|
||||||
"clickToRemoveFilter": "Click to remove this filter."
|
"clickToRemoveFilter": "Click to remove this filter."
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
5
scripts/account-gen/go.mod
Normal file
5
scripts/account-gen/go.mod
Normal 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
|
2
scripts/account-gen/go.sum
Normal file
2
scripts/account-gen/go.sum
Normal 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
122
scripts/account-gen/main.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -790,101 +790,108 @@ export class accountsList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
search = (query: String): string[] => {
|
private _queries: { [field: string]: { name: string, getter: string, bool: boolean, string: boolean, date: boolean, dependsOnTableHeader?: string, show?: boolean }} = {
|
||||||
const queries: { [field: string]: { name: string, getter: string, bool: boolean, string: boolean, date: boolean }} = {
|
"id": {
|
||||||
"id": {
|
name: "Jellyfin ID",
|
||||||
name: "Jellyfin ID",
|
getter: "id",
|
||||||
getter: "id",
|
bool: false,
|
||||||
bool: false,
|
string: true,
|
||||||
string: true,
|
date: false
|
||||||
date: false
|
},
|
||||||
},
|
"label": {
|
||||||
"label": {
|
name: "Label",
|
||||||
name: "Label",
|
getter: "label",
|
||||||
getter: "label",
|
bool: true,
|
||||||
bool: true,
|
string: true,
|
||||||
string: true,
|
date: false
|
||||||
date: false
|
},
|
||||||
},
|
"username": {
|
||||||
"username": {
|
name: "Username",
|
||||||
name: "Username",
|
getter: "name",
|
||||||
getter: "name",
|
bool: false,
|
||||||
bool: false,
|
string: true,
|
||||||
string: true,
|
date: false
|
||||||
date: false
|
},
|
||||||
},
|
"name": {
|
||||||
"name": {
|
name: "Username",
|
||||||
name: "Username",
|
getter: "name",
|
||||||
getter: "name",
|
bool: false,
|
||||||
bool: false,
|
string: true,
|
||||||
string: true,
|
date: false,
|
||||||
date: false
|
show: false
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
name: "Admin",
|
name: "Admin",
|
||||||
getter: "admin",
|
getter: "admin",
|
||||||
bool: true,
|
bool: true,
|
||||||
string: false,
|
string: false,
|
||||||
date: false
|
date: false
|
||||||
},
|
},
|
||||||
"disabled": {
|
"disabled": {
|
||||||
name: "Disabled",
|
name: "Disabled",
|
||||||
getter: "disabled",
|
getter: "disabled",
|
||||||
bool: true,
|
bool: true,
|
||||||
string: false,
|
string: false,
|
||||||
date: false
|
date: false
|
||||||
},
|
},
|
||||||
"access-jfa": {
|
"access-jfa": {
|
||||||
name: "Access jfa-go",
|
name: "Access jfa-go",
|
||||||
getter: "accounts_admin",
|
getter: "accounts_admin",
|
||||||
bool: true,
|
bool: true,
|
||||||
string: false,
|
string: false,
|
||||||
date: false
|
date: false,
|
||||||
},
|
dependsOnTableHeader: "accounts-header-access-jfa"
|
||||||
"email": {
|
},
|
||||||
name: "Email",
|
"email": {
|
||||||
getter: "email",
|
name: "Email",
|
||||||
bool: true,
|
getter: "email",
|
||||||
string: true,
|
bool: true,
|
||||||
date: false
|
string: true,
|
||||||
},
|
date: false,
|
||||||
"telegram": {
|
dependsOnTableHeader: "accounts-header-email"
|
||||||
name: "Telegram",
|
},
|
||||||
getter: "telegram",
|
"telegram": {
|
||||||
bool: true,
|
name: "Telegram",
|
||||||
string: true,
|
getter: "telegram",
|
||||||
date: false
|
bool: true,
|
||||||
},
|
string: true,
|
||||||
"matrix": {
|
date: false,
|
||||||
name: "Matrix",
|
dependsOnTableHeader: "accounts-header-telegram"
|
||||||
getter: "matrix",
|
},
|
||||||
bool: true,
|
"matrix": {
|
||||||
string: true,
|
name: "Matrix",
|
||||||
date: false
|
getter: "matrix",
|
||||||
},
|
bool: true,
|
||||||
"discord": {
|
string: true,
|
||||||
name: "Discord",
|
date: false,
|
||||||
getter: "discord",
|
dependsOnTableHeader: "accounts-header-matrix"
|
||||||
bool: true,
|
},
|
||||||
string: true,
|
"discord": {
|
||||||
date: false
|
name: "Discord",
|
||||||
},
|
getter: "discord",
|
||||||
"expiry": {
|
bool: true,
|
||||||
name: "Expiry",
|
string: true,
|
||||||
getter: "expiry",
|
date: false,
|
||||||
bool: true,
|
dependsOnTableHeader: "accounts-header-discord"
|
||||||
string: false,
|
},
|
||||||
date: true
|
"expiry": {
|
||||||
},
|
name: "Expiry",
|
||||||
"last-active": {
|
getter: "expiry",
|
||||||
name: "Last Active",
|
bool: true,
|
||||||
getter: "last_active",
|
string: false,
|
||||||
bool: true,
|
date: true,
|
||||||
string: false,
|
dependsOnTableHeader: "accounts-header-expiry"
|
||||||
date: true
|
},
|
||||||
}
|
"last-active": {
|
||||||
|
name: "Last Active",
|
||||||
|
getter: "last_active",
|
||||||
|
bool: true,
|
||||||
|
string: false,
|
||||||
|
date: true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
search = (query: String): string[] => {
|
||||||
const filterArea = document.getElementById("accounts-filter-area");
|
const filterArea = document.getElementById("accounts-filter-area");
|
||||||
filterArea.textContent = "";
|
filterArea.textContent = "";
|
||||||
|
|
||||||
@ -945,9 +952,9 @@ export class accountsList {
|
|||||||
}
|
}
|
||||||
const split = [word.substring(0, word.indexOf(":")), word.substring(word.indexOf(":")+1)];
|
const split = [word.substring(0, word.indexOf(":")), word.substring(word.indexOf(":")+1)];
|
||||||
|
|
||||||
if (!(split[0] in queries)) continue;
|
if (!(split[0] in this._queries)) continue;
|
||||||
|
|
||||||
const queryFormat = queries[split[0]];
|
const queryFormat = this._queries[split[0]];
|
||||||
|
|
||||||
if (queryFormat.bool) {
|
if (queryFormat.bool) {
|
||||||
let isBool = false;
|
let isBool = false;
|
||||||
@ -1004,7 +1011,8 @@ export class accountsList {
|
|||||||
|
|
||||||
filterCard.addEventListener("click", () => {
|
filterCard.addEventListener("click", () => {
|
||||||
for (let quote of [`"`, `'`, ``]) {
|
for (let quote of [`"`, `'`, ``]) {
|
||||||
this._search.value = this._search.value.replace(split[0] + ":" + quote + split[1] + quote, "");
|
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._search.oninput((null as Event));
|
||||||
})
|
})
|
||||||
@ -1024,6 +1032,7 @@ export class accountsList {
|
|||||||
if (queryFormat.date) {
|
if (queryFormat.date) {
|
||||||
// -1 = Before, 0 = On, 1 = After, 2 = No symbol, assume 0
|
// -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 compareType = (split[1][0] == ">") ? 1 : ((split[1][0] == "<") ? -1 : ((split[1][0] == "=") ? 0 : 2));
|
||||||
|
let unmodifiedValue = split[1];
|
||||||
if (compareType != 2) {
|
if (compareType != 2) {
|
||||||
split[1] = split[1].substring(1);
|
split[1] = split[1].substring(1);
|
||||||
}
|
}
|
||||||
@ -1046,7 +1055,8 @@ export class accountsList {
|
|||||||
|
|
||||||
filterCard.addEventListener("click", () => {
|
filterCard.addEventListener("click", () => {
|
||||||
for (let quote of [`"`, `'`, ``]) {
|
for (let quote of [`"`, `'`, ``]) {
|
||||||
this._search.value = this._search.value.replace(split[0] + ":" + quote + split[1] + quote, "");
|
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._search.oninput((null as Event));
|
||||||
@ -1823,7 +1833,7 @@ export class accountsList {
|
|||||||
const headerNames: string[] = ["username", "access-jfa", "email", "telegram", "matrix", "discord", "expiry", "last-active"];
|
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"];
|
const headerGetters: string[] = ["name", "accounts_admin", "email", "telegram", "matrix", "discord", "expiry", "last_active"];
|
||||||
for (let i = 0; i < headerNames.length; i++) {
|
for (let i = 0; i < headerNames.length; i++) {
|
||||||
const header: HTMLTableHeaderCellElement = document.getElementsByClassName("accounts-header-" + headerNames[i])[0] as HTMLTableHeaderCellElement;
|
const header: HTMLTableHeaderCellElement = document.querySelector(".accounts-header-" + headerNames[i]) as HTMLTableHeaderCellElement;
|
||||||
if (header !== null) {
|
if (header !== null) {
|
||||||
this._columns[header.className] = new Column(header, Object.getOwnPropertyDescriptor(user.prototype, headerGetters[i]).get);
|
this._columns[header.className] = new Column(header, Object.getOwnPropertyDescriptor(user.prototype, headerGetters[i]).get);
|
||||||
}
|
}
|
||||||
@ -1854,6 +1864,85 @@ export class accountsList {
|
|||||||
});
|
});
|
||||||
|
|
||||||
defaultSort();
|
defaultSort();
|
||||||
|
|
||||||
|
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>Match Text`;
|
||||||
|
|
||||||
|
// 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 = () => {
|
reload = () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user