2 Commits

Author SHA1 Message Date
fd80b7fc10 🎨 Replace sir with turf + move css to dedicated scss files 2023-08-06 21:27:20 +02:00
88b063b770 ⬆️ Bump dependencies versions 2023-08-06 11:58:49 +02:00
16 changed files with 436 additions and 447 deletions

View File

@@ -6,19 +6,19 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
dioxus = "0.3.2" dioxus = "0.4.0"
sir = { version = "0.3.0", features = ["dioxus"] } dioxus-desktop = "0.4.0"
dioxus-desktop = "0.3.0"
# matrix-sdk = { version = "0.6.2", features = ["js"] } # matrix-sdk = { version = "0.6.2", features = ["js"] }
matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", branch = "main" , features = ["js"]} matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", branch = "main" , features = ["js"]}
anyhow = "1.0.71" anyhow = "1.0.72"
url = "2.4.0" url = "2.4.0"
tokio = "1.29.1" tokio = "1.29.1"
dirs = "5.0.1" dirs = "5.0.1"
ctrlc-async = "3.2.2" ctrlc-async = "3.2.2"
tracing-subscriber = "0.3.17" tracing-subscriber = "0.3.17"
dioxus-free-icons = { version = "0.6.0", features = ["material-design-icons-navigation", "ionicons"] } dioxus-free-icons = { version = "0.7.0", features = ["material-design-icons-navigation", "ionicons"] }
thiserror = "1.0.44" thiserror = "1.0.44"
turf = "0.5.0"
[build] [build]
target = "x86_64-unknown-linux-gnu" target = "x86_64-unknown-linux-gnu"

5
build.rs Normal file
View File

@@ -0,0 +1,5 @@
fn main() {
// Tell Cargo to rerun this build script if any SCSS file
// in the 'src' directory or its subdirectories changes.
println!("cargo:rerun-if-changed=src/**/*.scss");
}

45
src/_base.scss Normal file
View File

@@ -0,0 +1,45 @@
body {
height: 100vh;
width: 100vw;
margin: 0px;
padding: 0px;
outline: 0px;
font-family: Tahoma, sans-serif;
}
#main {
height: 100%;
width: 100%;
}
.aeroButton {
height: 50%;
min-height: 16px;
aspect-ratio: 1;
background-color: transparent;
border: 2px solid transparent;
background-size: contain !important;
margin-right: 1%;
}
.aeroButton:hover {
border-image: url(./images/aerobutton_border.png) 2 round;
}
.aeroButton:active {
border-image: url(./images/aerobutton_border_down.png) 2 round;
}
.button {
height: 50%;
min-height: 16px;
aspect-ratio: 1;
background-color: transparent;
border: 2px solid transparent;
background-size: contain !important;
margin-right: 1%;
}
.button:hover {
border-image: url(./images/button_border.png) 2 round;
}
.button:active {
border-image: url(./images/button_border_down.png) 2 round;
}

View File

@@ -1,30 +1,13 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use sir::css;
turf::style_sheet!("src/components/avatar_selector.scss");
pub fn AvatarSelector(cx: Scope) -> Element { pub fn AvatarSelector(cx: Scope) -> Element {
let selector = css!(
"
position: relative;
//left: 35%;
height: 100%;
aspect-ratio: 1;
"
);
let picture = css!(
"
position: absolute;
height: 75%;
aspect-ratio: 1;
bottom: 17.5%;
right: 18%;
"
);
cx.render(rsx! { cx.render(rsx! {
style { STYLE_SHEET },
div { div {
class: "{selector}", class: ClassName::SELECTOR,
svg { svg {
view_box: "0 0 100 100", view_box: "0 0 100 100",
linearGradient { linearGradient {
@@ -69,7 +52,7 @@ pub fn AvatarSelector(cx: Scope) -> Element {
// }, // },
}, },
img { img {
class: "{picture}", class: ClassName::PICTURE,
src: "./images/default-avatar.png", src: "./images/default-avatar.png",
}, },
}, },

View File

@@ -0,0 +1,14 @@
.selector {
position: relative;
height: 100%;
aspect-ratio: 1;
.picture {
position: absolute;
height: 75%;
aspect-ratio: 1;
bottom: 17.5%;
right: 18%;
}
}

View File

@@ -1,10 +1,13 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_free_icons::icons::io_icons::IoChevronDown; use dioxus_free_icons::icons::io_icons::IoChevronDown;
use dioxus_free_icons::Icon; use dioxus_free_icons::Icon;
use sir::global_css;
turf::style_sheet!("src/components/contacts.scss");
fn ContactsArrow(cx: Scope) -> Element { fn ContactsArrow(cx: Scope) -> Element {
cx.render(rsx! { cx.render(rsx! {
style { STYLE_SHEET },
Icon { Icon {
icon: IoChevronDown, icon: IoChevronDown,
}, },
@@ -12,86 +15,26 @@ fn ContactsArrow(cx: Scope) -> Element {
} }
pub fn Contacts(cx: Scope) -> Element { pub fn Contacts(cx: Scope) -> Element {
// TODO: Use @extend once implemented (https://github.com/kaj/rsass/issues/65)
global_css!(
"
.contacts {
height: 72%;
width: 100%;
background-color: white;
font-size: 8pt;
&.active {
ul {
opacity: 0;
}
svg {
transform: rotate(180deg);
}
}
.header {
height: 2%;
width: 98%;
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
margin: 0;
margin-left: 1%;
padding-top: 1%;
font-weight: bold;
}
ul {
height: 100%;
margin: 0;
overflow: hidden;
opacity: 1;
transition: 0.4s ease;
}
li {
list-style-type: none;
height: 2%;
margin: 0 auto;
cursor: pointer;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
img {
height: 100%;
aspect-ratio: 1;
}
}
svg {
transition: 0.4s ease;
}
.contact {
list-style-type: none
margin: 0 auto
text-align: left
cursor: pointer
}
}
"
);
let show_contacts = use_state(cx, || false); let show_contacts = use_state(cx, || false);
let contacts_active = if **show_contacts { "active" } else { "" };
let classes = vec![
ClassName::CONTACTS,
if **show_contacts {
ClassName::ACTIVE
} else {
""
},
]
.join(" ");
cx.render(rsx! { cx.render(rsx! {
style { STYLE_SHEET },
div { div {
class: "contacts {contacts_active}", class: "{classes}",
p { p {
class: "header", class: ClassName::HEADER,
onclick: move |_| show_contacts.set(!show_contacts), onclick: move |_| show_contacts.set(!show_contacts),
ContactsArrow {}, ContactsArrow {},

View File

@@ -0,0 +1,65 @@
.contacts {
height: 72%;
width: 100%;
background-color: white;
font-size: 8pt;
&.active {
ul {
opacity: 0;
}
svg {
transform: rotate(180deg);
}
}
.header {
height: 2%;
width: 98%;
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
margin: 0;
margin-left: 1%;
padding-top: 1%;
font-weight: bold;
}
ul {
height: 100%;
margin: 0;
overflow: hidden;
opacity: 1;
transition: 0.4s ease;
}
li {
list-style-type: none;
height: 2%;
margin: 0 auto;
cursor: pointer;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
img {
height: 100%;
aspect-ratio: 1;
}
}
svg {
transition: 0.4s ease;
}
.contact {
list-style-type: none;
margin: 0 auto;
text-align: left;
cursor: pointer
}
}

View File

@@ -1,151 +1,60 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use sir::css;
use crate::app_settings::AppSettings; use crate::app_settings::AppSettings;
use crate::components::contacts::Contacts; use crate::components::contacts::Contacts;
use crate::components::header::Header; use crate::components::header::Header;
use crate::components::user_infos::UserInfos; use crate::components::user_infos::UserInfos;
turf::style_sheet!("src/components/contacts_window.scss");
pub fn ContactWindow(cx: Scope) -> Element { pub fn ContactWindow(cx: Scope) -> Element {
let app_context = use_shared_state::<AppSettings>(cx).unwrap(); let app_context = use_shared_state::<AppSettings>(cx).unwrap();
let root = css!(
"
width: 100%;
height: 100%;
background-color: #ECF6F9;
font-family: \"Tahoma\", sans-serif;
border: thin solid #707070;
border-radius: 8px;
box-shadow: 0 0 5px #00000050;
"
);
let header = css!(
"
height: 10%;
width: 100%;
"
);
let title_bar = css!(
"
height: 60%;
width: 100%;
background:
linear-gradient(180deg, #7DC5E3, #3883A3);
"
);
let user_info = css!(
"
height: 40%;
width: 100%;
background:
linear-gradient(180deg, #00658B, #0077A6);
"
);
let contacts_nav = css!(
"
height: calc(31/1080*100%);
background:
linear-gradient(180deg, #00658B, #0077A6);
"
);
let contacts_nav_inner = css!(
"
margin-left: 1%;
margin-right: 1%;
height: 100%;
display: flex;
align-items: center;
"
);
let search = css!(
"
height: calc(38/1080*100%);
width: 100%;
border-bottom: thin solid #e2eaf3;
"
);
// TODO: Remove following div
let search_inner = css!(
"
height: 100%;
width: 98%;
padding-left: 1%;
display: flex;
flex-direction: row;
align-items: center;
"
);
let search_input = css!(
"
height: calc(23/38*100%);
width: 100%;
margin-right: 1%;
border: thin solid #c7c7c7;
box-shadow: inset 0 0 calc(3/1080*100%) #0000002a;
font-size: 8pt;
padding-left: 1%;
"
);
let footer = css!(
"
height: 10%;
"
);
cx.render(rsx! { cx.render(rsx! {
div { style { STYLE_SHEET },
class: "{root}",
div { div {
class: "{header}", class: ClassName::CONTACTS_WINDOW,
div { div {
class: "{title_bar}", class: ClassName::HEADER,
div {
class: ClassName::TITLE_BAR,
}, },
div { div {
class: "{user_info}", class: ClassName::USER_INFO,
}, },
UserInfos {}, UserInfos {},
}, },
div { div {
class: "{contacts_nav}", class: ClassName::CONTACTS_NAV,
div { div {
class: "{contacts_nav_inner}", class: ClassName::INNER,
button { button {
class: "aero-button", class: ClassName::AERO_BUTTON,
style: "background: url(./images/letter.png) center no-repeat", style: "background: url(./images/letter.png) center no-repeat",
}, },
button { button {
class: "aero-button", class: ClassName::AERO_BUTTON,
style: "background: url(./images/directory.png) no-repeat center", style: "background: url(./images/directory.png) no-repeat center",
}, },
button { button {
class: "aero-button", class: ClassName::AERO_BUTTON,
style: "background: url(./images/news.png) no-repeat center", style: "background: url(./images/news.png) no-repeat center",
}, },
button { button {
class: "aero-button flex-right", class: ClassName::FLEX_RIGHT_AERO_BUTTON,
style: "background: url(./images/brush.png) no-repeat center", style: "background: url(./images/brush.png) no-repeat center",
}, },
button { button {
class: "aero-button", class: ClassName::AERO_BUTTON,
style: "background: url(./images/settings.png) no-repeat center", style: "background: url(./images/settings.png) no-repeat center",
}, },
@@ -154,24 +63,24 @@ pub fn ContactWindow(cx: Scope) -> Element {
}, },
div { div {
class: "{search}", class: ClassName::SEARCH,
div { div {
class: "{search_inner}", class: ClassName::INNER,
input { input {
class: "{search_input}", class: ClassName::SEARCH_INPUT,
placeholder: "Find a contact...", placeholder: "Find a contact...",
r#type: "text", r#type: "text",
}, },
button { button {
class: "button", class: ClassName::BUTTON,
style: "background: url(./images/add_user.png) no-repeat center", style: "background: url(./images/add_user.png) no-repeat center",
}, },
button { button {
class: "button", class: ClassName::BUTTON,
style: "background: url(./images/tbc_transfert.png) no-repeat center", style: "background: url(./images/tbc_transfert.png) no-repeat center",
}, },
}, },
@@ -180,7 +89,7 @@ pub fn ContactWindow(cx: Scope) -> Element {
Contacts {}, Contacts {},
div { div {
class: "{footer}", class: ClassName::FOOTER,
}, },
}, },
}) })

View File

@@ -0,0 +1,84 @@
@import "../_base.scss";
.contactsWindow {
width: 100%;
height: 100%;
background-color: #ECF6F9;
font-family: "Tahoma", sans-serif;
border: thin solid #707070;
border-radius: 8px;
box-shadow: 0 0 5px #00000050;
.header {
height: 10%;
width: 100%;
.titleBar {
height: 60%;
width: 100%;
background:
linear-gradient(180deg, #7DC5E3, #3883A3);
}
.userInfo {
height: 40%;
width: 100%;
background:
linear-gradient(180deg, #00658B, #0077A6);
}
}
.contactsNav {
height: calc(31/1080*100%);
background:
linear-gradient(180deg, #00658B, #0077A6);
.inner {
margin-left: 1%;
margin-right: 1%;
height: 100%;
display: flex;
align-items: center;
.flexRightAeroButton {
@extend .aeroButton;
margin-left: auto;
}
}
}
.search {
height: calc(38/1080*100%);
width: 100%;
border-bottom: thin solid #e2eaf3;
.inner {
height: 100%;
width: 98%;
padding-left: 1%;
display: flex;
flex-direction: row;
align-items: center;
.searchInput {
height: calc(23/38*100%);
width: 100%;
margin-right: 1%;
border: thin solid #c7c7c7;
box-shadow: inset 0 0 calc(3/1080*100%) #0000002a;
font-size: 8pt;
padding-left: 1%;
}
}
}
.footer {
height: 10%;
}
}

View File

@@ -1,25 +1,13 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use sir::css;
turf::style_sheet!("src/components/header.scss");
pub fn Header(cx: Scope) -> Element { pub fn Header(cx: Scope) -> Element {
let header = css!(
"
height: 100%;
width: 100%;
display: flex;
img {
height: 100%;
}
svg {
fill: white;
}
"
);
cx.render(rsx! { cx.render(rsx! {
style { STYLE_SHEET },
div { div {
class: "{header}", class: ClassName::ROOT,
img { img {
// src: "./assets/live-logo2.png" // src: "./assets/live-logo2.png"
src: "./images/logo-msn.png" src: "./images/logo-msn.png"

View File

@@ -0,0 +1,13 @@
.root {
height: 100%;
width: 100%;
display: flex;
img {
height: 100%;
}
svg {
fill: white;
}
}

View File

@@ -1,5 +1,4 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use sir::css;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
@@ -8,6 +7,8 @@ use crate::components::avatar_selector::AvatarSelector;
use crate::components::header::Header; use crate::components::header::Header;
use crate::matrix_client::{LoginStyle, MatrixClient}; use crate::matrix_client::{LoginStyle, MatrixClient};
turf::style_sheet!("src/components/login.scss");
pub fn Login(cx: Scope) -> Element { pub fn Login(cx: Scope) -> Element {
let app_context = use_shared_state::<AppSettings>(cx).unwrap(); let app_context = use_shared_state::<AppSettings>(cx).unwrap();
@@ -17,68 +18,8 @@ pub fn Login(cx: Scope) -> Element {
let empty_placeholder = String::from(""); let empty_placeholder = String::from("");
let root = css!(
"
width: 90%;
height: 98%;
display: flex;
flex-direction: column;
align-items: center;
padding: 5%;
padding-top: 2%;
background: linear-gradient(rgb(138, 191, 209), rgb(236, 246, 249) 10%);
"
);
let header = css!(
"
height: 5%;
width: 100%;
"
);
let body = css!(
"
height: 50%;
width: 50%;
max-width: 400px;
display: flex;
flex-direction: column;
justify-content: center;
padding-bottom: 3%;
"
);
let footer_buttons = css!(
"
width: 100%;
padding-top: 5%;
display: flex;
justify-content: center;
"
);
let avatar_selector = css!(
"
height: 30%;
width: 100%;
"
);
let invalid_input_css = css!(
"
border-color: red;
"
);
let password_class = if **invalid_login { let password_class = if **invalid_login {
invalid_input_css ClassName::INVALID_INPUT
} else { } else {
"" ""
}; };
@@ -114,18 +55,20 @@ pub fn Login(cx: Scope) -> Element {
}; };
cx.render(rsx! { cx.render(rsx! {
div { style { STYLE_SHEET },
class: "{root}",
div { div {
class: "{header}", class: ClassName::ROOT,
div {
class: ClassName::HEADER,
Header {}, Header {},
}, },
div { div {
class: "{body}", class: ClassName::BODY,
div { div {
class: "{avatar_selector}", class: ClassName::AVATAR_SELECTOR,
AvatarSelector {}, AvatarSelector {},
}, },
@@ -166,9 +109,9 @@ pub fn Login(cx: Scope) -> Element {
}, },
div { div {
class: "{footer_buttons}", class: ClassName::FOOTER_BUTTONS,
input { input {
class: "button", class: ClassName::BUTTON,
onclick: run_matrix_client, onclick: run_matrix_client,
r#type: "submit", r#type: "submit",
value: "sign in", value: "sign in",

51
src/components/login.scss Normal file
View File

@@ -0,0 +1,51 @@
@import "../_base.scss";
.root {
width: 90%;
height: 98%;
display: flex;
flex-direction: column;
align-items: center;
padding: 5%;
padding-top: 2%;
background: linear-gradient(rgb(138, 191, 209), rgb(236, 246, 249) 10%);
.header {
height: 5%;
width: 100%;
}
.body {
height: 50%;
width: 50%;
max-width: 400px;
display: flex;
flex-direction: column;
justify-content: center;
padding-bottom: 3%;
.invalidInput {
border-color: red;
}
.avatarSelector {
height: 30%;
width: 100%;
}
.footerButtons {
width: 100%;
padding-top: 5%;
display: flex;
justify-content: center;
}
}
}

View File

@@ -1,123 +1,52 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_free_icons::icons::md_navigation_icons::MdArrowDropDown; use dioxus_free_icons::icons::md_navigation_icons::MdArrowDropDown;
use dioxus_free_icons::Icon; use dioxus_free_icons::Icon;
use sir::{css, global_css};
use crate::components::avatar_selector::AvatarSelector; use crate::components::avatar_selector::AvatarSelector;
fn DownArrowIcon(cx: Scope) -> Element { turf::style_sheet!("src/components/user_infos.scss");
let style = css!(
"
color: transparent;
path:last-child {
fill: white;
}
"
);
fn DownArrowIcon(cx: Scope) -> Element {
cx.render(rsx! { cx.render(rsx! {
style { STYLE_SHEET },
Icon { Icon {
class: "{style}", class: ClassName::DOWN_ARROW_ICON,
icon: MdArrowDropDown, icon: MdArrowDropDown,
} }
}) })
} }
pub fn UserInfos(cx: Scope) -> Element { pub fn UserInfos(cx: Scope) -> Element {
global_css!(
"
.flex-right {
margin-left: auto;
}
.user-info {
position: relative;
height: 75%;
width: 99%;
top: -75%;
left: 1%;
aspect-ratio: 1;
z-index: 1;
display: flex;
flex-direction: row;
align-items: center;
.avatar-selector {
height: 100%;
aspect-ratio: 1;
}
.info-container {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
height: 100%;
width: 100%;
// font-size: 8pt;
.user-id {
height: 30%;
width: fit-content;
display: flex;
text-align: begin;
align-items: center;
.user-name {
display: inline-block;
width: fit-content;
color: white;
margin: 0;
// font-size: 10pt;
}
.user-status {
display: inline-block;
width: fit-content;
color: #B9DDE7;
}
}
.user-message {
width: fit-content;
height: 30%;
display: flex;
text-align: begin;
align-items: center;
margin: 0;
color: white;
}
}
}"
);
cx.render(rsx! { cx.render(rsx! {
div { style { STYLE_SHEET },
class: "user-info",
div { div {
class: "avatar-selector", class: ClassName::USER_INFO,
div {
class: ClassName::AVATAR_SELECTOR,
AvatarSelector {}, AvatarSelector {},
}, },
div { div {
class: "info-container", class: ClassName::INFO_CONTAINER,
div { div {
class: "aero-button user-id", class: ClassName::USER_ID,
p { p {
class: "user-name", class: ClassName::USER_NAME,
"SUPER USER" "SUPER USER"
}, },
p { p {
class: "user-status", class: ClassName::USER_STATUS,
"(Busy)", "(Busy)",
}, },
DownArrowIcon {}, DownArrowIcon {},
}, },
div { div {
class: "aero-button user-message", class: ClassName::USER_MESSAGE,
p { p {
"My message", "My message",
} }

View File

@@ -0,0 +1,71 @@
@import "../_base.scss"
.userInfo {
position: relative;
height: 75%;
width: 99%;
top: -75%;
left: 1%;
aspect-ratio: 1;
z-index: 1;
display: flex;
flex-direction: row;
align-items: center;
.avatarSelector {
height: 100%;
aspect-ratio: 1;
}
.infoContainer {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
height: 100%;
width: 100%;
.userId {
@extend .aeroButton;
height: 30%;
width: fit-content;
display: flex;
text-align: begin;
align-items: center;
.userName {
display: inline-block;
width: fit-content;
color: white;
margin: 0;
}
.userStatus {
display: inline-block;
width: fit-content;
color: #B9DDE7;
}
}
.userMessage {
@extend .aeroButton;
width: fit-content;
height: 30%;
display: flex;
text-align: begin;
align-items: center;
margin: 0;
color: white;
}
}
}
.downArrowIcon {
color: transparent;
path:last-child {
fill: white;
}
}

View File

@@ -2,8 +2,6 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_desktop::Config; use dioxus_desktop::Config;
use sir::{global_css, AppStyle};
pub mod app_settings; pub mod app_settings;
pub mod components; pub mod components;
pub mod matrix_client; pub mod matrix_client;
@@ -19,70 +17,18 @@ fn App(cx: Scope<AppSettings>) -> Element {
let app_context = use_shared_state::<AppSettings>(cx).unwrap(); let app_context = use_shared_state::<AppSettings>(cx).unwrap();
global_css!(
"
body {
height: 100vh;
width: 100vw;
margin: 0px;
padding: 0px;
outline: 0px;
font-family: Tahoma, sans-serif;
}
#main {
height: 100%;
width: 100%;
}
.aero-button {
height: 50%;
min-height: 16px;
aspect-ratio: 1;
background-color: transparent;
border: 2px solid transparent;
background-size: contain !important;
margin-right: 1%;
}
.aero-button:hover {
border-image: url(./images/aerobutton_border.png) 2 round;
}
.aero-button:active {
border-image: url(./images/aerobutton_border_down.png) 2 round;
}
.button {
height: 50%;
min-height: 16px;
aspect-ratio: 1;
background-color: transparent;
border: 2px solid transparent;
background-size: contain !important;
margin-right: 1%;
}
.button:hover {
border-image: url(./images/button_border.png) 2 round;
}
.button:active {
border-image: url(./images/button_border_down.png) 2 round;
}
"
);
// let window = dioxus_desktop::use_window(cx); // let window = dioxus_desktop::use_window(cx);
// let dom = VirtualDom::new(ControlWindow); // let dom = VirtualDom::new(ControlWindow);
// window.new_window(dom, cx.props.clone()); // window.new_window(dom, cx.props.clone());
cx.render(rsx! {
if app_context.read().store.is_logged { if app_context.read().store.is_logged {
cx.render(rsx! { rsx!(ContactWindow {})
AppStyle {},
ContactWindow {},
})
} else {
cx.render(rsx! {
AppStyle {},
Login {}
})
} }
else {
rsx!(Login {})
}
})
} }
#[tokio::main] #[tokio::main]