mod client;
mod components;
mod route;
mod views;
use dioxus::{
core::Element,
document,
prelude::{component, dioxus_elements, rsx},
};
use dioxus_i18n::{
prelude::{I18n, I18nConfig, Locale, use_init_i18n},
t,
unic_langid::langid,
};
use dioxus_router::Router;
use dioxus_signals::{GlobalSignal, ReadableExt as _, Signal};
use fluent_langneg::{NegotiationStrategy, convert_vec_str_to_langids, negotiate_languages};
use manganis::{Asset, asset};
use tracing::{info, warn};
use url::Url;
use crate::{
client::Config,
components::{Banner, Footer},
route::Route,
};
const FAVICON: Asset = asset!("/assets/favicon.ico");
const MAIN_CSS: Asset = asset!("/assets/styling/main.css");
static CONFIG: GlobalSignal<ConfigState> = Signal::global(|| ConfigState::Initial);
fn main() {
dioxus::launch(App);
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ConfigState {
Initial,
Loading,
Failed { message: String },
NoConfigPresent,
ConfigLoaded { config: Config },
}
#[component]
fn App() -> Element {
let mut i18n = use_init_i18n(|| {
I18nConfig::new(langid!("en-US"))
.with_fallback(langid!("en-US"))
.with_locale(Locale::new_static(
langid!("en-US"),
include_str!("../i18n/en-US.ftl"),
))
.with_locale(Locale::new_static(
langid!("de-DE"),
include_str!("../i18n/de-DE.ftl"),
))
});
let initialize = move |_| async move {
initialize_i18n(&mut i18n).await;
initialize_config(&CONFIG).await;
};
rsx! {
document::Link { rel: "icon", href: FAVICON }
document::Link { rel: "stylesheet", href: MAIN_CSS }
div {
onmounted: initialize,
match &*CONFIG.read_unchecked() {
ConfigState::ConfigLoaded { config } => rsx! {
for banner in config.banner.iter().flatten() {
Banner { message: banner.message.to_string() }
}
Router::<Route> {}
if let Some(footer) = &config.footer {
Footer {
links: footer.links.iter().flatten().cloned().collect()
}
}
},
ConfigState::NoConfigPresent => rsx! {
{ t!("config-not-present") }
},
ConfigState::Failed{message} => rsx! {
{ t!("config-loading-failed", message: message.to_string()) }
},
ConfigState::Loading | ConfigState::Initial=> rsx! {
{ t!("config-loading") }
}
}
},
}
}
async fn initialize_i18n(i18n: &mut I18n) {
let Ok(languages) = document::eval("return navigator.languages;").await else {
warn!("couldn't load user language.");
return;
};
let serde_json::Value::Array(languages) = languages else {
warn!("not an array value: {languages}");
return;
};
let requested = match convert_vec_str_to_langids(
languages
.into_iter()
.filter_map(|s| s.as_str().map(str::to_string)),
) {
Ok(l) => l,
Err(e) => {
warn!("Error reading requested languages: {e}");
return;
}
};
let available = match convert_vec_str_to_langids(["en-US", "de-DE"]) {
Ok(l) => l,
Err(e) => {
warn!("Error reading available languages: {e}");
return;
}
};
let languages = negotiate_languages(&requested, &available, None, NegotiationStrategy::Lookup);
if let Some(language) = languages.into_iter().next() {
let language = match language.to_string().parse() {
Ok(l) => l,
Err(e) => {
warn!("Error converting language identifier: {e}");
return;
}
};
i18n.set_language(language);
}
}
async fn initialize_config(state: &GlobalSignal<ConfigState>) {
if !matches!(&*state.read(), ConfigState::Initial) {
return;
}
*state.write() = ConfigState::Loading;
let Ok(base_url) = document::eval("return document.URL;").await else {
warn!("couldn't load base url.");
*state.write() = ConfigState::Failed {
message: "couldn't load base url.".to_string(),
};
return;
};
let serde_json::Value::String(base_url) = base_url else {
warn!("not a string value: {base_url}");
*state.write() = ConfigState::Failed {
message: format!("not a string value: {base_url}"),
};
return;
};
info!("base url: {base_url:?}");
let base_url = match base_url.parse::<Url>() {
Ok(url) => url,
Err(e) => {
warn!("Couldn't parse base url {base_url}, reason: {e}");
*state.write() = ConfigState::Failed {
message: format!("Couldn't parse base url {base_url}, reason: {e}"),
};
return;
}
};
match crate::client::get_page_config(base_url).await {
Ok(Some(config)) => {
*state.write() = ConfigState::ConfigLoaded { config };
}
Ok(None) => {
*state.write() = ConfigState::NoConfigPresent;
}
Err(e) => {
*state.write() = ConfigState::Failed {
message: format!("Failed to load config: {e}"),
};
}
}
}