refactor
This commit is contained in:
parent
92cefff212
commit
36128864aa
5 changed files with 153 additions and 92 deletions
40
Cargo.lock
generated
40
Cargo.lock
generated
|
@ -202,7 +202,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
|||
[[package]]
|
||||
name = "based"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.hydrar.de/jmarya/based#a5ecf145a93b96b2545a68ce8e98b44ab50379a2"
|
||||
source = "git+https://git.hydrar.de/jmarya/based?branch=ui#78e3d6b798eb32d85ab58dd8891457f7ab6f75aa"
|
||||
dependencies = [
|
||||
"bcrypt",
|
||||
"chrono",
|
||||
|
@ -255,9 +255,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.7.0"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
|
||||
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
@ -485,9 +485,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.6.0"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
|
||||
checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
|
@ -535,7 +535,7 @@ version = "0.4.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7"
|
||||
dependencies = [
|
||||
"bitflags 2.7.0",
|
||||
"bitflags 2.8.0",
|
||||
"proc-macro2",
|
||||
"proc-macro2-diagnostics",
|
||||
"quote",
|
||||
|
@ -1449,7 +1449,7 @@ version = "0.1.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||
dependencies = [
|
||||
"bitflags 2.7.0",
|
||||
"bitflags 2.8.0",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
@ -1488,9 +1488,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
version = "0.4.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
|
@ -1562,9 +1562,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
|||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
|
||||
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
|
@ -1710,7 +1710,7 @@ version = "0.10.68"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
|
||||
dependencies = [
|
||||
"bitflags 2.7.0",
|
||||
"bitflags 2.8.0",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
|
@ -2005,7 +2005,7 @@ version = "0.5.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
|
||||
dependencies = [
|
||||
"bitflags 2.7.0",
|
||||
"bitflags 2.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2317,7 +2317,7 @@ version = "0.38.43"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
|
||||
dependencies = [
|
||||
"bitflags 2.7.0",
|
||||
"bitflags 2.8.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
|
@ -2420,7 +2420,7 @@ version = "2.11.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.7.0",
|
||||
"bitflags 2.8.0",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
|
@ -2695,7 +2695,7 @@ checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233"
|
|||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.7.0",
|
||||
"bitflags 2.8.0",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"chrono",
|
||||
|
@ -2739,7 +2739,7 @@ checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613"
|
|||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.7.0",
|
||||
"bitflags 2.8.0",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
"crc",
|
||||
|
@ -2890,7 +2890,7 @@ version = "0.6.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
|
||||
dependencies = [
|
||||
"bitflags 2.7.0",
|
||||
"bitflags 2.8.0",
|
||||
"core-foundation",
|
||||
"system-configuration-sys 0.6.0",
|
||||
]
|
||||
|
@ -3354,9 +3354,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
|||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.11.1"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4"
|
||||
checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"serde",
|
||||
|
|
|
@ -4,7 +4,7 @@ version = "0.1.0"
|
|||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
based = { git = "https://git.hydrar.de/jmarya/based", features = ["htmx"]}
|
||||
based = { git = "https://git.hydrar.de/jmarya/based", features = ["htmx"], branch = "ui" }
|
||||
bytesize = "1.3.0"
|
||||
chrono = "0.4.39"
|
||||
env_logger = "0.11.6"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use based::auth::MaybeUser;
|
||||
use based::page::{Shell, htmx_link, render_page};
|
||||
use based::request::{RawResponse, RequestContext, StringResponse, respond_with};
|
||||
use based::ui::components::Shell;
|
||||
use based::ui::{prelude::*, render_page};
|
||||
use maud::{PreEscaped, html};
|
||||
use pacco::pkg::mirror::MirrorRepository;
|
||||
use rocket::http::{ContentType, Status};
|
||||
|
@ -24,25 +25,33 @@ pub async fn index_page(
|
|||
let repos: Vec<String> = Repository::list();
|
||||
|
||||
let content = html!(
|
||||
div class="flex justify-between" {
|
||||
(Flex(html! {
|
||||
h1 class="text-4xl font-bold pb-6" { "Repositories" };
|
||||
@if let Some(user) = user.take_user() {
|
||||
a class="text-lg" href="/account" { (user.username) };
|
||||
};
|
||||
};
|
||||
}).justify(Justify::Between))
|
||||
|
||||
div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6" {
|
||||
@for repo in repos {
|
||||
(htmx_link(&format!("/{repo}"), "flex items-center gap-4 p-4 bg-gray-100 dark:bg-gray-700 rounded-lg shadow-md transition hover:bg-gray-200 dark:hover:bg-gray-600", "", html! {
|
||||
p class="font-medium text-gray-800 dark:text-gray-100" {
|
||||
(repo)
|
||||
|
||||
// TODO : Implement class transition
|
||||
(Link(&format!("/{repo}"),
|
||||
Rounded(
|
||||
Padding(
|
||||
Shadow::medium(
|
||||
Flex(Hover(
|
||||
Background(Gray::_600, Nothing()),
|
||||
Background(Gray::_700,
|
||||
Div().vanish().push(Text(&repo).medium().color(&Gray::_100))
|
||||
.push(html! {
|
||||
@if config.is_mirrored_repo(&repo) {
|
||||
div class="inline-block px-3 py-1 text-sm font-medium text-white bg-blue-500 rounded-full" { "Mirrored" };
|
||||
(Padding(Rounded(Background(Blue::_500, Text("Mirrored").white().medium().sm())).size(Size::Full)).x(ScreenValue::_3).y(ScreenValue::_1))
|
||||
};
|
||||
};
|
||||
}))
|
||||
})
|
||||
))).items_center().gap(4))).all(ScreenValue::_4))
|
||||
.size(Size::Large)))
|
||||
}
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
render(content, "Repositories", ctx).await
|
||||
|
|
139
src/routes/ui.rs
139
src/routes/ui.rs
|
@ -1,8 +1,6 @@
|
|||
use based::{
|
||||
page::htmx_link,
|
||||
request::{RequestContext, StringResponse},
|
||||
};
|
||||
use maud::{PreEscaped, html};
|
||||
use based::request::{RequestContext, StringResponse};
|
||||
use based::ui::prelude::*;
|
||||
use maud::{PreEscaped, Render, html};
|
||||
use rocket::{State, get};
|
||||
|
||||
use pacco::pkg::{Package, Repository, arch::Architecture, find_package_by_name};
|
||||
|
@ -40,9 +38,10 @@ pub async fn pkg_ui(
|
|||
let content = html! {
|
||||
// Package Name
|
||||
div class="flex flex-wrap gap-2 justify-center items-center" {
|
||||
(htmx_link(&format!("/{}", repo.name), "text-3xl font-bold text-gray-800 dark:text-gray-100", "", html! {
|
||||
(repo.name)
|
||||
}))
|
||||
(Link(&format!("/{}", repo.name),
|
||||
Text(&repo.name).bold().color(&Gray::_100)._3xl()
|
||||
).use_htmx())
|
||||
|
||||
p class="font-bold p-2 text-gray-400" { "/" };
|
||||
h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100 pr-2" {
|
||||
(pkg.name)
|
||||
|
@ -50,18 +49,20 @@ pub async fn pkg_ui(
|
|||
|
||||
@if pkg.is_signed() {
|
||||
div class="flex items-center gap-2 text-slate-300 pr-4" {
|
||||
span class="text-2xl font-bold" {
|
||||
"✓"
|
||||
}
|
||||
span class="text-sm font-medium" { "Signed" }
|
||||
(Span("✓")._2xl().bold())
|
||||
(Span("Signed").sm().medium())
|
||||
// TODO : Add more info: Who signed? + Public Key recv
|
||||
}
|
||||
}
|
||||
|
||||
@for arch in arch {
|
||||
span class="px-3 py-1 text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded-full" {
|
||||
(arch.to_string())
|
||||
};
|
||||
(Rounded(
|
||||
Padding(
|
||||
Background(Gray::_700,
|
||||
Span(&arch.to_string()).medium().sm())
|
||||
).x(ScreenValue::_3).y(ScreenValue::_1)
|
||||
).size(Size::Full)
|
||||
)
|
||||
}
|
||||
|
||||
a href=(format!("/pkg/{}/{}/{}", pkg.repo, pkg.arch.to_string(), pkg.file_name())) class="ml-4 inline-flex items-center px-2 py-2 bg-gray-200 text-black font-xs rounded-lg shadow-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50" {
|
||||
|
@ -103,9 +104,11 @@ pub async fn pkg_ui(
|
|||
ul class="space-y-1" {
|
||||
@for version in versions {
|
||||
li class="text-gray-800 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400 transition" {
|
||||
(htmx_link(&format!("/{}/{}?ver={version}", repo.name, &pkg.name), if pkg.version.as_ref().map(|x| *x == Package::version(&version).0).unwrap_or_default() { "text-blue-500" } else { "" }, "", html! {
|
||||
(version)
|
||||
}))
|
||||
(Link(&format!("/{}/{}?ver={version}", repo.name, &pkg.name),
|
||||
if pkg.version.as_ref()
|
||||
.map(|x| *x == Package::version(&version).0).unwrap_or_default()
|
||||
{ Text(&version).color(&Blue::_500) } else { Text(&version) }
|
||||
).use_htmx())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -180,6 +183,28 @@ pub async fn pkg_ui(
|
|||
Some(render(content, pkg_name, ctx).await)
|
||||
}
|
||||
|
||||
pub fn arch_card(a: &str, repo_name: &str, current: bool) -> PreEscaped<String> {
|
||||
let url = if current {
|
||||
&format!("/{repo_name}")
|
||||
} else {
|
||||
&format!("/{repo_name}?arch={a}")
|
||||
};
|
||||
|
||||
let link = Link(&url, Text(&a).sm().medium().color(&Gray::_300)).use_htmx();
|
||||
|
||||
Rounded(
|
||||
Padding(if current {
|
||||
Background(Blue::_500, link)
|
||||
} else {
|
||||
Background(Gray::_700, link)
|
||||
})
|
||||
.x(ScreenValue::_3)
|
||||
.y(ScreenValue::_1),
|
||||
)
|
||||
.size(Size::Full)
|
||||
.render()
|
||||
}
|
||||
|
||||
#[get("/<repo>?<arch>")]
|
||||
pub async fn repo_ui(
|
||||
repo: &str,
|
||||
|
@ -212,18 +237,12 @@ pub async fn repo_ui(
|
|||
@for a in architectures {
|
||||
@if let Some(arch) = arch.as_ref() {
|
||||
@if arch.to_string() == a {
|
||||
(htmx_link(&format!("/{}", repo.name), "px-3 py-1 text-sm font-medium bg-blue-400 dark:bg-blue-500 text-gray-600 dark:text-gray-300 rounded-full", "", html! {
|
||||
(a)
|
||||
}));
|
||||
(arch_card(&a, &repo.name, true))
|
||||
} @else {
|
||||
(htmx_link(&format!("/{}?arch={}", repo.name, a), "px-3 py-1 text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded-full", "", html! {
|
||||
(a)
|
||||
}));
|
||||
(arch_card(&a, &repo.name, false))
|
||||
}
|
||||
} @else {
|
||||
(htmx_link(&format!("/{}?arch={}", repo.name, a), "px-3 py-1 text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded-full", "", html! {
|
||||
(a)
|
||||
}));
|
||||
(arch_card(&a, &repo.name, false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -232,18 +251,41 @@ pub async fn repo_ui(
|
|||
// Package list
|
||||
ul class="space-y-4" {
|
||||
@for pkg in packages {
|
||||
(htmx_link(&format!("/{}/{pkg}", repo.name), "flex items-center gap-4 p-4 bg-gray-100 dark:bg-gray-700 rounded-lg shadow-md transition hover:bg-gray-200 dark:hover:bg-gray-600", "", html! {
|
||||
div class="w-10 h-10 flex items-center justify-center rounded-full bg-blue-500 text-white font-semibold" {
|
||||
{(pkg.chars().next().unwrap_or_default().to_uppercase())}
|
||||
};
|
||||
p class="font-medium flex-1 text-gray-800 dark:text-gray-100 max-w-fit" {
|
||||
(pkg)
|
||||
};
|
||||
div class="flex items-center gap-2 text-slate-300 pr-4" {
|
||||
span class="text-2xl font-bold" { "✓" };
|
||||
span class="text-sm font-medium" { "Signed" };
|
||||
};
|
||||
}))
|
||||
(
|
||||
Link(
|
||||
&format!("/{}/{pkg}", repo.name),
|
||||
// TODO : Implement transition class
|
||||
Flex(
|
||||
Padding(
|
||||
Hover(
|
||||
Background(Gray::_600, Nothing()),
|
||||
Background(Gray::_700,
|
||||
Rounded(
|
||||
Shadow::medium(
|
||||
Div().vanish()
|
||||
.push(
|
||||
html! { (Sized(ScreenValue::_10, ScreenValue::_10, Rounded(
|
||||
Background(Blue::_500, Flex(Text(
|
||||
&pkg.chars().next().unwrap_or_default().to_uppercase().to_string()
|
||||
).white().semibold()).full_center().group()
|
||||
)).size(Size::Full))) }
|
||||
)
|
||||
.push(html! { (Width(ScreenValue::fit, Text(&pkg).medium().color(&Gray::_100))) })
|
||||
.push(html! {
|
||||
(
|
||||
Flex(
|
||||
Padding(
|
||||
Div().vanish()
|
||||
.push(Span("✓")._2xl().bold().color(&Slate::_300))
|
||||
.push(Span("Signed").sm().medium().color(&Slate::_300))
|
||||
).right(ScreenValue::_4)
|
||||
).items_center().gap(2).group()
|
||||
)
|
||||
})
|
||||
)
|
||||
).size(Size::Large)))).all(ScreenValue::_4)).items_center().gap(4)
|
||||
).use_htmx()
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -303,12 +345,15 @@ pub fn build_info(key: String, value: String) -> PreEscaped<String> {
|
|||
}
|
||||
|
||||
pub fn key_value(key: String, value: String) -> PreEscaped<String> {
|
||||
html! {
|
||||
div class="flex items-center" {
|
||||
span class="font-bold w-32" { (format!("{key}: ")) };
|
||||
span class="ml-2" { (value) };
|
||||
};
|
||||
}
|
||||
Flex(
|
||||
Div()
|
||||
.vanish()
|
||||
.push(Width(ScreenValue::_32, Span(&format!("{key}: ")).bold()))
|
||||
.push(Margin(Span(&value)).left(ScreenValue::_2)),
|
||||
)
|
||||
.items_center()
|
||||
.group()
|
||||
.render()
|
||||
}
|
||||
|
||||
pub fn take_out<T>(v: &mut Vec<T>, f: impl Fn(&T) -> bool) -> (&mut Vec<T>, Option<T>) {
|
||||
|
@ -350,13 +395,13 @@ pub fn pkg_list_info(key: &str, value: &str) -> PreEscaped<String> {
|
|||
});
|
||||
|
||||
html! {
|
||||
div class="flex items-center" {
|
||||
(Flex(html! {
|
||||
span class="font-bold w-32" { (format!("{key}: ")) };
|
||||
div class="flex flex-wrap" {
|
||||
@for pkg in pkgs {
|
||||
(pkg)
|
||||
};
|
||||
};
|
||||
};
|
||||
}).items_center().group())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use based::{
|
||||
auth::{Session, Sessions, User, csrf::CSRF},
|
||||
page::htmx_link,
|
||||
request::{RequestContext, StringResponse, api::to_uuid, respond_html},
|
||||
ui::AttrExtendable,
|
||||
};
|
||||
use maud::{PreEscaped, html};
|
||||
use rocket::{
|
||||
|
@ -14,6 +14,7 @@ use rocket::{
|
|||
};
|
||||
|
||||
use super::render;
|
||||
use based::ui::prelude::*;
|
||||
|
||||
#[get("/login")]
|
||||
pub async fn login(ctx: RequestContext) -> StringResponse {
|
||||
|
@ -83,7 +84,7 @@ pub async fn new_api_key(user: User, csrf: &str, session_name: &str) -> StringRe
|
|||
if session_name.is_empty() {
|
||||
return respond_html(
|
||||
html! {
|
||||
div id="next_session" {};
|
||||
(Div().id("next_session"))
|
||||
(user.update_csrf().await)
|
||||
}
|
||||
.into_string(),
|
||||
|
@ -99,7 +100,7 @@ pub async fn new_api_key(user: User, csrf: &str, session_name: &str) -> StringRe
|
|||
span class="text-red-500" { (api.token) };
|
||||
};
|
||||
|
||||
div id="next_session" {};
|
||||
(Div().id("next_session"))
|
||||
(user.update_csrf().await)
|
||||
}
|
||||
.into_string(),
|
||||
|
@ -124,7 +125,14 @@ pub async fn account_page(user: User, ctx: RequestContext) -> StringResponse {
|
|||
h2 class="text-xl font-semibold mb-2" { (user.username) };
|
||||
};
|
||||
|
||||
(htmx_link("/passwd", "mb-6 bg-green-500 text-white py-2 px-6 rounded hover:bg-green-600", "", html! { "Change Password" }))
|
||||
(Link("/passwd", Margin(
|
||||
Rounded(Padding(
|
||||
Hover(
|
||||
Background(Green::_600, Nothing()),
|
||||
Background(Green::_500, Text("Change Password").white())
|
||||
)
|
||||
).x(ScreenValue::_6).y(ScreenValue::_2))
|
||||
).bottom(ScreenValue::_6)).use_htmx())
|
||||
|
||||
section class="mb-6 mt-6" {
|
||||
h3 class="text-lg font-semibold mb-4" { "Active Sessions" };
|
||||
|
@ -134,8 +142,7 @@ pub async fn account_page(user: User, ctx: RequestContext) -> StringResponse {
|
|||
(build_session_block(&ses, &user).await)
|
||||
};
|
||||
|
||||
div id="next_session" {};
|
||||
|
||||
(Div().id("next_session"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue